Skip to content

Commit

Permalink
feat(deployment): implement deployment settings api
Browse files Browse the repository at this point in the history
refs #714
  • Loading branch information
ygrishajev committed Feb 3, 2025
1 parent 600c7cd commit a6c6534
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 4 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { RequestContextInterceptor } from "@src/core/services/request-context-in
import { HonoInterceptor } from "@src/core/types/hono-interceptor.type";
import packageJson from "../package.json";
import { chainDb, syncUserSchema, userDb } from "./db/dbConnection";
import { deploymentSettingRouter } from "./deployment/routes/deployment-setting/deployment-setting.router";
import { clientInfoMiddleware } from "./middlewares/clientInfoMiddleware";
import { apiRouter } from "./routers/apiRouter";
import { dashboardRouter } from "./routers/dashboardRouter";
Expand Down Expand Up @@ -83,6 +84,7 @@ appHono.route("/", stripePricesRouter);
appHono.route("/", createAnonymousUserRouter);
appHono.route("/", getAnonymousUserRouter);
appHono.route("/", sendVerificationEmailRouter);
appHono.route("/", deploymentSettingRouter);

appHono.get("/status", c => {
const version = packageJson.version;
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/auth/services/ability/ability.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export class AbilityService {
{ action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "read", subject: "User", conditions: { id: "${user.id}" } },
{ action: "read", subject: "StripePrice" },
{ action: "create", subject: "VerificationEmail", conditions: { id: "${user.id}" } }
{ action: "create", subject: "VerificationEmail", conditions: { id: "${user.id}" } },
{ action: "manage", subject: "DeploymentSetting", conditions: { userId: "${user.id}" } }
],
REGULAR_ANONYMOUS_USER: [
{ action: ["create", "read", "sign"], subject: "UserWallet", conditions: { userId: "${user.id}" } },
{ action: "read", subject: "User", conditions: { id: "${user.id}" } }
{ action: "read", subject: "User", conditions: { id: "${user.id}" } },
{ action: "manage", subject: "DeploymentSetting", conditions: { userId: "${user.id}" } }
],
SUPER_USER: [{ action: "manage", subject: "all" }]
};
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/core/repositories/base.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export abstract class BaseRepository<
}

async findOneBy(query?: Partial<Output>) {
return this.toOutput(
return await this.toOutput(
await this.queryCursor.findFirst({
where: this.queryToWhere(query)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LoggerService } from "@akashnetwork/logging";
import { ForbiddenError } from "@casl/ability";
import { context, trace } from "@opentelemetry/api";
import type { Event } from "@sentry/types";
import type { Context, Env } from "hono";
Expand Down Expand Up @@ -36,6 +37,10 @@ export class HonoErrorHandlerService {
return c.json({ error: "BadRequestError", data: error.errors }, { status: 400 });
}

if (error instanceof ForbiddenError) {
return c.json({ error: "ForbiddenError", message: "Forbidden" }, { status: 403 });
}

await this.reportError(error, c);

return c.json({ error: "InternalServerError" }, { status: 500 });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from "http-assert";
import { singleton } from "tsyringe";

import { Protected } from "@src/auth/services/auth.service";
import {
CreateDeploymentSettingRequest,
DeploymentSettingResponse,
FindDeploymentSettingParams,
UpdateDeploymentSettingRequest
} from "@src/deployment/http-schemas/deployment-setting.schema";
import { DeploymentSettingService } from "@src/deployment/services/deployment-setting/deployment-setting.service";

@singleton()
export class DeploymentSettingController {
constructor(private readonly deploymentSettingService: DeploymentSettingService) {}

@Protected([{ action: "read", subject: "DeploymentSetting" }])
async findByUserIdAndDseq(params: FindDeploymentSettingParams): Promise<DeploymentSettingResponse> {
const setting = await this.deploymentSettingService.findByUserIdAndDseq(params);

assert(setting, 404, "Deployment setting not found");

return setting;
}

@Protected([{ action: "create", subject: "DeploymentSetting" }])
async create(input: CreateDeploymentSettingRequest["data"]): Promise<DeploymentSettingResponse> {
return await this.deploymentSettingService.create(input);
}

@Protected([{ action: "update", subject: "DeploymentSetting" }])
async update(params: FindDeploymentSettingParams, input: UpdateDeploymentSettingRequest["data"]): Promise<DeploymentSettingResponse> {
const setting = await this.deploymentSettingService.update(params, input);

assert(setting, 404, "Deployment setting not found");

return setting;
}
}
46 changes: 46 additions & 0 deletions apps/api/src/deployment/http-schemas/deployment-setting.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from "zod";

export const DeploymentSettingResponseSchema = z.object({
id: z.number(),
userId: z.string(),
dseq: z.string(),
autoTopUpEnabled: z.boolean(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime()
});

export const CreateDeploymentSettingRequestSchema = z.object({
data: z.object({
userId: z.string().openapi({
description: "User ID"
}),
dseq: z.string().openapi({
description: "Deployment sequence number"
}),
autoTopUpEnabled: z.boolean().default(false).openapi({
description: "Whether auto top-up is enabled for this deployment"
})
})
});

export const UpdateDeploymentSettingRequestSchema = z.object({
data: z.object({
autoTopUpEnabled: z.boolean().openapi({
description: "Whether auto top-up is enabled for this deployment"
})
})
});

export const FindDeploymentSettingParamsSchema = z.object({
userId: z.string().openapi({
description: "User ID"
}),
dseq: z.string().openapi({
description: "Deployment sequence number"
})
});

export type DeploymentSettingResponse = z.infer<typeof DeploymentSettingResponseSchema>;
export type CreateDeploymentSettingRequest = z.infer<typeof CreateDeploymentSettingRequestSchema>;
export type UpdateDeploymentSettingRequest = z.infer<typeof UpdateDeploymentSettingRequestSchema>;
export type FindDeploymentSettingParams = z.infer<typeof FindDeploymentSettingParamsSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { Users } from "@src/user/model-schemas";

type Table = ApiPgTables["DeploymentSettings"];
export type DeploymentSettingsInput = Partial<Table["$inferInsert"]>;
export type DeploymentSettingsOutput = Table["$inferSelect"];
export type DeploymentSettingsDbOutput = Table["$inferSelect"];
export type DeploymentSettingsOutput = Omit<DeploymentSettingsDbOutput, "createdAt" | "updatedAt"> & {
createdAt: string;
updatedAt: string;
};

export type AutoTopUpDeployment = {
id: number;
Expand Down Expand Up @@ -64,4 +68,14 @@ export class DeploymentSettingRepository extends BaseRepository<Table, Deploymen
}
} while (lastId);
}

protected toOutput(payload: DeploymentSettingsDbOutput): DeploymentSettingsOutput {
return payload
? {
...payload,
createdAt: payload.createdAt.toISOString(),
updatedAt: payload.updatedAt.toISOString()
}
: undefined;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { createRoute as createOpenApiRoute } from "@hono/zod-openapi";
import { container } from "tsyringe";
import { z } from "zod";

import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
import { DeploymentSettingController } from "@src/deployment/controllers/deployment-setting/deployment-setting.controller";
import {
CreateDeploymentSettingRequestSchema,
DeploymentSettingResponseSchema,
FindDeploymentSettingParamsSchema,
UpdateDeploymentSettingRequestSchema
} from "@src/deployment/http-schemas/deployment-setting.schema";

const getRoute = createOpenApiRoute({
method: "get",
path: "/v1/deployment-settings/{userId}/{dseq}",
summary: "Get deployment settings by user ID and dseq",
tags: ["Deployment Settings"],
request: {
params: FindDeploymentSettingParamsSchema
},
responses: {
200: {
description: "Returns deployment settings",
content: {
"application/json": {
schema: DeploymentSettingResponseSchema
}
}
},
404: {
description: "Deployment settings not found",
content: {
"application/json": {
schema: z.object({
message: z.string()
})
}
}
}
}
});

const postRoute = createOpenApiRoute({
method: "post",
path: "/v1/deployment-settings",
summary: "Create deployment settings",
tags: ["Deployment Settings"],
request: {
body: {
content: {
"application/json": {
schema: CreateDeploymentSettingRequestSchema
}
}
}
},
responses: {
201: {
description: "Deployment settings created successfully",
content: {
"application/json": {
schema: DeploymentSettingResponseSchema
}
}
}
}
});

const patchRoute = createOpenApiRoute({
method: "patch",
path: "/v1/deployment-settings/{userId}/{dseq}",
summary: "Update deployment settings",
tags: ["Deployment Settings"],
request: {
params: FindDeploymentSettingParamsSchema,
body: {
content: {
"application/json": {
schema: UpdateDeploymentSettingRequestSchema
}
}
}
},
responses: {
200: {
description: "Deployment settings updated successfully",
content: {
"application/json": {
schema: DeploymentSettingResponseSchema
}
}
},
404: {
description: "Deployment settings not found",
content: {
"application/json": {
schema: z.object({
message: z.string()
})
}
}
}
}
});

export const deploymentSettingRouter = new OpenApiHonoHandler();

deploymentSettingRouter.openapi(getRoute, async function routeGetDeploymentSettings(c) {
const params = c.req.valid("param");
const result = await container.resolve(DeploymentSettingController).findByUserIdAndDseq(params);

return c.json(result, 200);
});

deploymentSettingRouter.openapi(postRoute, async function routeCreateDeploymentSettings(c) {
const { data } = c.req.valid("json");
const result = await container.resolve(DeploymentSettingController).create(data);
return c.json(result, 201);
});

deploymentSettingRouter.openapi(patchRoute, async function routeUpdateDeploymentSettings(c) {
const params = c.req.valid("param");
const { data } = c.req.valid("json");
const result = await container.resolve(DeploymentSettingController).update(params, data);
return c.json(result, 200);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { singleton } from "tsyringe";

import { AuthService } from "@src/auth/services/auth.service";
import { FindDeploymentSettingParams } from "@src/deployment/http-schemas/deployment-setting.schema";
import {
DeploymentSettingRepository,
DeploymentSettingsInput,
DeploymentSettingsOutput
} from "@src/deployment/repositories/deployment-setting/deployment-setting.repository";

@singleton()
export class DeploymentSettingService {
constructor(
private readonly deploymentSettingRepository: DeploymentSettingRepository,
private readonly authService: AuthService
) {}

async findByUserIdAndDseq(params: FindDeploymentSettingParams): Promise<DeploymentSettingsOutput> {
return await this.deploymentSettingRepository.accessibleBy(this.authService.ability, "read").findOneBy(params);
}

async create(input: DeploymentSettingsInput): Promise<DeploymentSettingsOutput> {
return await this.deploymentSettingRepository.accessibleBy(this.authService.ability, "create").create(input);
}

async update(params: FindDeploymentSettingParams, input: Pick<DeploymentSettingsInput, "autoTopUpEnabled">): Promise<DeploymentSettingsOutput> {
return await this.deploymentSettingRepository.accessibleBy(this.authService.ability, "update").updateBy(params, input);
}
}
Loading

0 comments on commit a6c6534

Please sign in to comment.