Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(deployment): implement deployment settings api #771

Merged
merged 1 commit into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -22,7 +22,7 @@
}

export abstract class BaseRepository<
T extends PgTableWithColumns<any>,

Check warning on line 25 in apps/api/src/core/repositories/base.repository.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Unexpected any. Specify a different type
Input extends BaseRecordInput<string | number>,
Output extends BaseRecordOutput<string | number>
> {
Expand Down Expand Up @@ -66,7 +66,7 @@
}

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 All @@ -24,7 +25,7 @@
this.handle = this.handle.bind(this);
}

async handle<E extends Env = any>(error: Error, c: Context<E>): Promise<Response> {

Check warning on line 28 in apps/api/src/core/services/hono-error-handler/hono-error-handler.service.ts

View workflow job for this annotation

GitHub Actions / validate-n-build

Unexpected any. Specify a different type
this.logger.error(error);

if (isHttpError(error)) {
Expand All @@ -36,6 +37,10 @@
return c.json({ error: "BadRequestError", data: error.errors }, { status: 400 });
}

if (error instanceof ForbiddenError) {
return c.json({ error: "ForbiddenError", message: "Forbidden" }, { status: 403 });
stalniy marked this conversation as resolved.
Show resolved Hide resolved
}

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,36 @@
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 { data: setting };
}

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

@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 { data: setting };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";

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

export const DeploymentSettingResponseSchema = z.object({
data: DeploymentSettingSchema
});

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 DeploymentSetting = z.infer<typeof DeploymentSettingSchema>;
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, { returning: true });
}
}
Loading
Loading