From 6c1399ddd43d73c25f7994fc5f6d24025de38e34 Mon Sep 17 00:00:00 2001 From: Akhil Reddy Date: Sat, 21 Dec 2024 16:26:01 +0530 Subject: [PATCH] Add permits module --- src/organization/organization.schema.ts | 3 +- src/permit/permit.controller.ts | 87 ++++++++++++++++++ src/permit/permit.route.ts | 94 +++++++++++++++++++ src/permit/permit.schema.ts | 102 +++++++++++++++++++++ src/permit/permit.service.ts | 116 ++++++++++++++++++++++++ src/routes.ts | 2 + src/server.ts | 8 +- src/user/user.schema.ts | 1 + src/user/user.service.ts | 1 + 9 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 src/permit/permit.controller.ts create mode 100644 src/permit/permit.route.ts create mode 100644 src/permit/permit.schema.ts create mode 100644 src/permit/permit.service.ts diff --git a/src/organization/organization.schema.ts b/src/organization/organization.schema.ts index c42a5ff..9e6a019 100644 --- a/src/organization/organization.schema.ts +++ b/src/organization/organization.schema.ts @@ -23,12 +23,13 @@ export const orgModel = mongoose.model( isClient: Boolean, status: String, createdAt: Date, - createdBy: mongoose.Types.ObjectId, + createdBy: String, updatedAt: Date, }).index({ tenantId: 1, domain: 1 }, { unique: true }) ); const orgCore = { + _id: z.string().optional(), name: z.string().max(30), domain: z.string().max(30), avatar: z.string().url().optional(), diff --git a/src/permit/permit.controller.ts b/src/permit/permit.controller.ts new file mode 100644 index 0000000..aadc80a --- /dev/null +++ b/src/permit/permit.controller.ts @@ -0,0 +1,87 @@ +import { FastifyReply, FastifyRequest } from "fastify"; +import { CreatePermitInput, UpdatePermitInput } from "./permit.schema"; +import { + createPermit, + deletePermit, + getPermit, + listPermits, + updatePermit, +} from "./permit.service"; +import { PageQueryParams } from "../pagination"; + +export async function createPermitHandler( + req: FastifyRequest, + res: FastifyReply +) { + const input = req.body as CreatePermitInput; + + try { + const permit = await createPermit(input, req.user.tenantId); + return res.code(201).send(permit); + } catch (err) { + return err; + } +} + +export async function getPermitHandler(req: FastifyRequest, res: FastifyReply) { + const { permitId } = req.params as { permitId: string }; + + try { + const permit = await getPermit(permitId, req.user.tenantId); + if (permit === null) + return res.code(404).send({ error: "resource not foound" }); + + return res.code(200).send(permit); + } catch (err) { + return err; + } +} + +export async function listPermitsHandler( + req: FastifyRequest, + res: FastifyReply +) { + const queryParams = req.query as PageQueryParams; + + try { + const authUser = req.user; + const orgList = await listPermits(queryParams, authUser.tenantId); + return res.code(200).send(orgList); + } catch (err) { + return err; + } +} + +export async function updatePermitHandler( + req: FastifyRequest, + res: FastifyReply +) { + const input = req.body as UpdatePermitInput; + const { permitId } = req.params as { permitId: string }; + + try { + const updatedOrg = await updatePermit(input, permitId, req.user.tenantId); + if (!updatedOrg) return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(updatedOrg); + } catch (err) { + return err; + } +} + +export async function deletePermitHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { permitId } = req.params as { permitId: string }; + + try { + const deleteResult = await deletePermit(permitId, req.user.tenantId); + if (deleteResult.deletedCount === 0) + return res.code(404).send({ error: "resource not found" }); + + return res.code(204).send(); + } catch (err) { + return err; + } +} diff --git a/src/permit/permit.route.ts b/src/permit/permit.route.ts new file mode 100644 index 0000000..3a7c859 --- /dev/null +++ b/src/permit/permit.route.ts @@ -0,0 +1,94 @@ +import { FastifyInstance } from "fastify"; +import { + createPermitHandler, + deletePermitHandler, + getPermitHandler, + listPermitsHandler, + updatePermitHandler, +} from "./permit.controller"; +import { $permit } from "./permit.schema"; + +export async function permitRoutes(fastify: FastifyInstance) { + fastify.post( + "/", + { + schema: { + body: $permit("createPermitInput"), + response: { + 201: $permit("createPermitResponse"), + }, + }, + config: { requiredClaims: ["permit:write"] }, + preHandler: [fastify.authorize], + }, + createPermitHandler + ); + + fastify.get( + "/:permitId", + { + schema: { + params: { + type: "object", + properties: { + permitId: { type: "string" }, + }, + }, + response: { + 200: $permit("getPermitResponse"), + }, + }, + config: { requiredClaims: ["permit:read"] }, + preHandler: [fastify.authorize], + }, + getPermitHandler + ); + + fastify.get( + "/", + { + schema: { + querystring: $permit("pageQueryParams"), + response: { + 200: $permit("listPermitResponse"), + }, + }, + + config: { requiredClaims: ["permit:read"] }, + preHandler: [fastify.authorize], + }, + listPermitsHandler + ); + + fastify.patch( + "/:permitId", + { + schema: { + params: { + type: "object", + properties: { permitId: { type: "string" } }, + }, + body: $permit("updatePermitInput"), + response: { + 200: $permit("getPermitResponse"), + }, + }, + }, + updatePermitHandler + ); + + fastify.delete( + "/:permitId", + { + schema: { + params: { + type: "object", + properties: { permitId: { type: "string" } }, + }, + }, + config: { requiredClaims: ["permit:delete"] }, + preHandler: [fastify.authorize], + }, + deletePermitHandler + ); +} diff --git a/src/permit/permit.schema.ts b/src/permit/permit.schema.ts new file mode 100644 index 0000000..b94c1b7 --- /dev/null +++ b/src/permit/permit.schema.ts @@ -0,0 +1,102 @@ +import { z } from "zod"; +import mongoose from "mongoose"; +import { buildJsonSchemas } from "fastify-zod"; +import { pageMetadata, pageQueryParams } from "../pagination"; + +export const permitModel = mongoose.model( + "permit", + new mongoose.Schema({ + tenantId: { + type: String, + required: true, + }, + pid: { + type: String, + unique: true, + }, + permitNumber: String, + county: { + type: mongoose.Types.ObjectId, + ref: "organization", + }, + client: { + type: mongoose.Types.ObjectId, + ref: "organization", + }, + permitDate: Date, + stage: String, + status: String, + assignedTo: { + type: mongoose.Types.ObjectId, + ref: "user", + }, + createdAt: Date, + updatedAt: Date, + createdBy: String, + }).index({ tenantId: 1, permitNumber: 1 }, { unique: true }) +); + +const permitCore = { + permitNumber: z.string(), + county: z.string().optional(), + client: z.string().optional(), + permitDate: z.date(), + stage: z.string().optional(), + status: z.string().optional(), + assignedTo: z.string().optional(), +}; + +const createPermitInput = z.object({ + ...permitCore, +}); + +const createPermitResponse = z.object({ + pid: z.string(), + ...permitCore, +}); + +const getPermitResponse = z.object({ + pid: z.string(), + ...permitCore, + county: z.object({ + name: z.string(), + avatar: z.string().optional(), + }), + client: z.object({ + name: z.string(), + avatar: z.string().optional(), + }), + assignedTo: z.object({ + name: z.string(), + avatar: z.string().optional(), + }), +}); + +const listPermitResponse = z.object({ + permits: z.array(getPermitResponse), + metadata: pageMetadata, +}); + +const updatePermitInput = z.object({ + county: z.string().optional(), + client: z.string().optional(), + permitDate: z.date().optional(), + stage: z.string().optional(), + status: z.string().optional(), + assignedTo: z.string().optional(), +}); + +export type CreatePermitInput = z.infer; +export type UpdatePermitInput = z.infer; + +export const { schemas: permitSchemas, $ref: $permit } = buildJsonSchemas( + { + createPermitInput, + createPermitResponse, + getPermitResponse, + listPermitResponse, + updatePermitInput, + pageQueryParams, + }, + { $id: "permit" } +); diff --git a/src/permit/permit.service.ts b/src/permit/permit.service.ts new file mode 100644 index 0000000..f2ea57a --- /dev/null +++ b/src/permit/permit.service.ts @@ -0,0 +1,116 @@ +import { PageQueryParams } from "../pagination"; +import { generateId } from "../utils/id"; +import { + CreatePermitInput, + permitModel, + UpdatePermitInput, +} from "./permit.schema"; + +export async function createPermit(input: CreatePermitInput, tenantId: string) { + const permit = await permitModel.create({ + tenantId: tenantId, + pid: generateId(), + ...input, + }); + + return permit; +} + +export async function getPermit(permitId: string, tenantId: string) { + return await permitModel + .findOne({ + $and: [{ tenantId: tenantId }, { pid: permitId }], + }) + .populate("county") + .populate("client") + .populate("assignedTo"); +} + +export async function listPermits(params: PageQueryParams, tenantId: string) { + const page = params.page || 1; + const pageSize = params.pageSize || 10; + + const permitsList = await permitModel.aggregate([ + { + $match: { $and: [{ tenantId: tenantId }] }, + }, + { + $lookup: { + from: "organizations", + localField: "county", + foreignField: "_id", + as: "countyRec", + }, + }, + { + $lookup: { + from: "organizations", + localField: "client", + foreignField: "_id", + as: "clientRec", + }, + }, + { + $lookup: { + from: "permits", + localField: "assignedTo", + foreignField: "_id", + as: "assignedRec", + }, + }, + { + $project: { + _id: 0, + pid: 1, + permitNumber: 1, + county: { $arrayElemAt: ["$countyRec", 0] }, + client: { $arrayElemAt: ["$clientRec", 0] }, + permitDate: 1, + stage: 1, + status: 1, + assignedTo: { $arrayElemAt: ["$assignedRec", 0] }, + }, + }, + { + $facet: { + metadata: [{ $count: "count" }], + data: [{ $skip: (page - 1) * pageSize }, { $limit: pageSize }], + }, + }, + ]); + + return { + permits: permitsList[0].data, + metadata: { + count: permitsList[0].metadata[0].count, + page, + pageSize, + }, + }; +} + +export async function updatePermit( + input: UpdatePermitInput, + permitId: string, + tenantId: string +) { + const updatePermitResult = await permitModel + .findOneAndUpdate( + { + $and: [{ tenantId: tenantId }, { pid: permitId }], + }, + { ...input, updatedAt: new Date() }, + { new: true } + ) + .populate("county") + .populate("client") + .populate("assignedTo"); + + return updatePermitResult; +} + +export async function deletePermit(permitId: string, tenantId: string) { + return await permitModel.deleteOne({ + $and: [{ tenantId: tenantId }, { pid: permitId }], + }); +} diff --git a/src/routes.ts b/src/routes.ts index 5e60b0c..b558833 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,9 +2,11 @@ import { FastifyInstance } from "fastify"; import userRoutes from "./user/user.route"; import organizationRoutes from "./organization/organization.route"; import { tokenRoutes } from "./tokens/token.route"; +import { permitRoutes } from "./permit/permit.route"; export default async function routes(fastify: FastifyInstance) { fastify.register(userRoutes, { prefix: "/users" }); fastify.register(organizationRoutes, { prefix: "/orgs" }); fastify.register(tokenRoutes, { prefix: "/tokens" }); + fastify.register(permitRoutes, { prefix: "/permits" }); } diff --git a/src/server.ts b/src/server.ts index ce633de..9cdfec3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,7 @@ import { orgSchemas } from "./organization/organization.schema"; import { tokenSchemas } from "./tokens/token.schema"; import { errorHandler } from "./utils/errors"; import { authHandler, authorize } from "./auth"; +import { permitSchemas } from "./permit/permit.schema"; const app = fastify({ logger: true }); @@ -19,7 +20,12 @@ app.setErrorHandler(errorHandler); app.addHook("onRequest", authHandler); app.register(routes, { prefix: "/api/v1" }); -for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) { +for (const schema of [ + ...userSchemas, + ...orgSchemas, + ...tokenSchemas, + ...permitSchemas, +]) { app.addSchema(schema); } diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index 406ae2c..7c4ba90 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -16,6 +16,7 @@ export const userModel = mongoose.model( }, firstName: String, lastName: String, + name: String, email: { type: String, unique: true, diff --git a/src/user/user.service.ts b/src/user/user.service.ts index b2c8aae..93363b3 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -5,6 +5,7 @@ export async function createUser(input: CreateUserInput, tenantId: string) { const user = await userModel.create({ tenantId: tenantId, pid: generateId(), + name: input.firstName + " " + input.lastName, createdAt: new Date(), ...input, });