mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-23 09:15:31 +00:00
🚧 Add CRUD endpoints and tables for labels
This commit is contained in:
@@ -148,6 +148,9 @@ export enum ActionsEnum {
|
|||||||
updateAlertRule = "updateAlertRule",
|
updateAlertRule = "updateAlertRule",
|
||||||
deleteAlertRule = "deleteAlertRule",
|
deleteAlertRule = "deleteAlertRule",
|
||||||
listAlertRules = "listAlertRules",
|
listAlertRules = "listAlertRules",
|
||||||
|
listOrgLabels = "listOrgLabels",
|
||||||
|
createOrgLabel = "createOrgLabel",
|
||||||
|
updateOrgLabel = "updateOrgLabel",
|
||||||
getAlertRule = "getAlertRule",
|
getAlertRule = "getAlertRule",
|
||||||
createHealthCheck = "createHealthCheck",
|
createHealthCheck = "createHealthCheck",
|
||||||
updateHealthCheck = "updateHealthCheck",
|
updateHealthCheck = "updateHealthCheck",
|
||||||
|
|||||||
@@ -162,6 +162,45 @@ export const resources = pgTable("resources", {
|
|||||||
wildcard: boolean("wildcard").notNull().default(false)
|
wildcard: boolean("wildcard").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const labels = pgTable("labels", {
|
||||||
|
labelId: serial("labelId").primaryKey(),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
color: varchar("color").notNull(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const siteLabels = pgTable("siteLabels", {
|
||||||
|
siteLabelId: serial("siteLabelId").primaryKey(),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
labelId: integer("labelId")
|
||||||
|
.references(() => labels.labelId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const resourceLabels = pgTable("resourceLabels", {
|
||||||
|
resourceLabelId: serial("resourceLabelId").primaryKey(),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.references(() => resources.resourceId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
labelId: integer("labelId")
|
||||||
|
.references(() => labels.labelId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
targetId: serial("targetId").primaryKey(),
|
targetId: serial("targetId").primaryKey(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
@@ -196,9 +235,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId").references(() => sites.siteId, {
|
siteId: integer("siteId")
|
||||||
onDelete: "cascade"
|
.references(() => sites.siteId, {
|
||||||
}).notNull(),
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
name: varchar("name"),
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
@@ -1097,19 +1138,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
|||||||
complete: boolean("complete").notNull().default(false)
|
complete: boolean("complete").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const statusHistory = pgTable("statusHistory", {
|
export const statusHistory = pgTable(
|
||||||
id: serial("id").primaryKey(),
|
"statusHistory",
|
||||||
entityType: varchar("entityType").notNull(),
|
{
|
||||||
entityId: integer("entityId").notNull(),
|
id: serial("id").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
entityType: varchar("entityType").notNull(),
|
||||||
.notNull()
|
entityId: integer("entityId").notNull(),
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
orgId: varchar("orgId")
|
||||||
status: varchar("status").notNull(),
|
.notNull()
|
||||||
timestamp: integer("timestamp").notNull(),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
}, (table) => [
|
status: varchar("status").notNull(),
|
||||||
index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp),
|
timestamp: integer("timestamp").notNull()
|
||||||
index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp),
|
},
|
||||||
]);
|
(table) => [
|
||||||
|
index("idx_statusHistory_entity").on(
|
||||||
|
table.entityType,
|
||||||
|
table.entityId,
|
||||||
|
table.timestamp
|
||||||
|
),
|
||||||
|
index("idx_statusHistory_org_timestamp").on(
|
||||||
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
@@ -1179,3 +1231,4 @@ export type RoundTripMessageTracker = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type Network = InferSelectModel<typeof networks>;
|
export type Network = InferSelectModel<typeof networks>;
|
||||||
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
export type StatusHistory = InferSelectModel<typeof statusHistory>;
|
||||||
|
export type Label = InferSelectModel<typeof labels>;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
|
import * as labels from "#private/routers/labels";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -732,6 +733,30 @@ authenticated.get(
|
|||||||
alertRule.getAlertRule
|
alertRule.getAlertRule
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/labels",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listOrgLabels),
|
||||||
|
labels.listOrgLabels
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/labels",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.createOrgLabel),
|
||||||
|
labels.createOrgLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.patch(
|
||||||
|
"/org/:orgId/label/:labelId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateOrgLabel),
|
||||||
|
labels.updateOrgLabel
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/health-checks",
|
"/org/:orgId/health-checks",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
|
|||||||
144
server/private/routers/labels/createOrgLabel.ts
Normal file
144
server/private/routers/labels/createOrgLabel.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
labels,
|
||||||
|
resourceLabels,
|
||||||
|
resources,
|
||||||
|
siteLabels,
|
||||||
|
sites,
|
||||||
|
type Label
|
||||||
|
} from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z.strictObject({
|
||||||
|
name: z.string().nonempty(),
|
||||||
|
color: z
|
||||||
|
.string()
|
||||||
|
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
|
||||||
|
.nonempty(),
|
||||||
|
siteId: z.number().int().optional(),
|
||||||
|
resourceId: z.number().int().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function createOrgLabel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, color, siteId, resourceId } = parsedBody.data;
|
||||||
|
|
||||||
|
if (siteId) {
|
||||||
|
const siteCount = await db.$count(sites, eq(sites.siteId, siteId));
|
||||||
|
|
||||||
|
if (siteCount === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Site with Id ${siteId} doesn't exist.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceId) {
|
||||||
|
const resourceCount = await db.$count(
|
||||||
|
sites,
|
||||||
|
eq(resources.resourceId, resourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resourceCount === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`Resource with Id ${resourceId} doesn't exist.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = await db.transaction(async (tx) => {
|
||||||
|
const [label] = await tx
|
||||||
|
.insert(labels)
|
||||||
|
.values({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (siteId) {
|
||||||
|
await tx.insert(siteLabels).values({
|
||||||
|
siteId,
|
||||||
|
labelId: label.labelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceId) {
|
||||||
|
await tx.insert(resourceLabels).values({
|
||||||
|
resourceId,
|
||||||
|
labelId: label.labelId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response<CreateOrEditLabelResponse>(res, {
|
||||||
|
data: { label },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Org Label created successfully",
|
||||||
|
status: HttpCode.CREATED
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
server/private/routers/labels/index.ts
Normal file
16
server/private/routers/labels/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./listOrgLabels";
|
||||||
|
export * from "./createOrgLabel";
|
||||||
|
export * from "./updateOrgLabel";
|
||||||
155
server/private/routers/labels/listOrgLabels.ts
Normal file
155
server/private/routers/labels/listOrgLabels.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, labels } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { and, asc, eq, like, sql } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
const listLabelsSchema = z.object({
|
||||||
|
pageSize: z.coerce
|
||||||
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.optional()
|
||||||
|
.catch(20)
|
||||||
|
.default(20)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 20,
|
||||||
|
description: "Number of items per page"
|
||||||
|
}),
|
||||||
|
page: z.coerce
|
||||||
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.optional()
|
||||||
|
.catch(1)
|
||||||
|
.default(1)
|
||||||
|
.openapi({
|
||||||
|
type: "integer",
|
||||||
|
default: 1,
|
||||||
|
description: "Page number to retrieve"
|
||||||
|
}),
|
||||||
|
query: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
function queryLabelsBase() {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
labelId: labels.labelId,
|
||||||
|
name: labels.name,
|
||||||
|
color: labels.color
|
||||||
|
})
|
||||||
|
.from(labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrgLabels(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedQuery = listLabelsSchema.safeParse(req.query);
|
||||||
|
if (!parsedQuery.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedQuery.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pageSize, page, query } = parsedQuery.data;
|
||||||
|
|
||||||
|
const conditions = [and(eq(labels.orgId, orgId))];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
conditions.push(
|
||||||
|
like(
|
||||||
|
sql`LOWER(${labels.name})`,
|
||||||
|
"%" + query.toLowerCase() + "%"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = queryLabelsBase().where(and(...conditions));
|
||||||
|
|
||||||
|
// we need to add `as` so that drizzle filters the result as a subquery
|
||||||
|
const countQuery = db.$count(
|
||||||
|
queryLabelsBase()
|
||||||
|
.where(and(...conditions))
|
||||||
|
.as("filtered_labels")
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelListQuery = baseQuery
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset(pageSize * (page - 1))
|
||||||
|
.orderBy(asc(labels.name));
|
||||||
|
|
||||||
|
const [totalCount, rows] = await Promise.all([
|
||||||
|
countQuery,
|
||||||
|
labelListQuery
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response<ListOrgLabelsResponse>(res, {
|
||||||
|
data: {
|
||||||
|
labels: rows,
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
pageSize,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Labels retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
server/private/routers/labels/updateOrgLabel.ts
Normal file
109
server/private/routers/labels/updateOrgLabel.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
labels,
|
||||||
|
resourceLabels,
|
||||||
|
resources,
|
||||||
|
siteLabels,
|
||||||
|
sites,
|
||||||
|
type Label
|
||||||
|
} from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty(),
|
||||||
|
labelId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateLabelBodySchema = z.strictObject({
|
||||||
|
name: z.string().min(1).max(255).optional(),
|
||||||
|
color: z
|
||||||
|
.string()
|
||||||
|
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
|
||||||
|
.nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function updateOrgLabel(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { orgId, labelId } = parsedParams.data;
|
||||||
|
|
||||||
|
const parsedBody = updateLabelBodySchema.safeParse(req.body);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(labels)
|
||||||
|
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return next(createHttpError(HttpCode.NOT_FOUND, "Label not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, color } = parsedBody.data;
|
||||||
|
|
||||||
|
const [label] = await db
|
||||||
|
.update(labels)
|
||||||
|
.set({
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
})
|
||||||
|
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return response<CreateOrEditLabelResponse>(res, {
|
||||||
|
data: {
|
||||||
|
label
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Label updated successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
server/routers/labels/types.ts
Normal file
10
server/routers/labels/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Label } from "@server/db";
|
||||||
|
import type { PaginatedResponse } from "@server/types/Pagination";
|
||||||
|
|
||||||
|
export type ListOrgLabelsResponse = PaginatedResponse<{
|
||||||
|
labels: Omit<Label, "orgId">[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type CreateOrEditLabelResponse = {
|
||||||
|
label: Label;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user