Vadim пре 3 месеци
родитељ
комит
ebec58d716

+ 86 - 5
src/api/v_0.1.0/client/client-pe-api.ts

@@ -56,7 +56,6 @@ class ClientPartEntitiesApi {
           peTypeId: z.string().uuid(),
           peTypeCode: z.string(),
           peTypeName: z.string(),
-          eventInstId: z.string().uuid(),
           ownerId: z.string().uuid(),
           name: z.string(),
         }),
@@ -80,13 +79,17 @@ class ClientPartEntitiesApi {
           eventInstId: z.string().uuid(),
           name: z.string(),
           ownerId: z.string().uuid(),
-          fields: z.array(CustomFieldWithValue),
+          fields: z.array(
+            CustomFieldWithValue.extend({ peFfId: z.string().uuid() }),
+          ),
           members: z.array(
             z.object({
-              memberId: z.string(),
+              peMemberId: z.string(),
               userId: z.string().uuid(),
               email: z.string().email(),
-              fields: z.array(CustomFieldWithValue),
+              fields: z.array(
+                CustomFieldWithValue.extend({ userEfId: z.string().uuid() }),
+              ),
             }),
           ),
           invites: z.array(
@@ -94,8 +97,9 @@ class ClientPartEntitiesApi {
               peInviteId: z.string().uuid(),
               peInviteUuid: z.string().uuid(),
               name: z.string(),
-              limitVal: z.number().int(),
+              limitVal: z.number().int().nullable(),
               countVal: z.number().int(),
+              expirationDate: z.string().datetime().nullable(),
             }),
           ),
         }),
@@ -131,6 +135,83 @@ class ClientPartEntitiesApi {
       peId: z.string().uuid(),
     }),
   };
+
+  POST_Invite = {
+    req: {
+      body: z.object({
+        name: z.string(),
+        limitVal: z.number().int().nullable(),
+        expirationDate: z.string().datetime().nullable(),
+      }),
+      params: z.object({
+        peId: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+      peInviteId: z.string().uuid(),
+    }),
+  };
+
+  GET_Invites = {
+    req: z.object({
+      peId: z.string().uuid(),
+    }),
+    res: z.object({
+      code: z.enum(["success"]),
+      invites: z.array(
+        z.object({
+          peInviteId: z.string().uuid(),
+          peInviteUuid: z.string().uuid(),
+          name: z.string(),
+          limitVal: z.number().int().nullable(),
+          countVal: z.number().int(),
+          expirationDate: z.string().datetime().nullable(),
+        }),
+      ),
+    }),
+  };
+
+  GET_InviteInfo = {
+    req: {
+      params: z.object({
+        peInviteUuid: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+      invite: z.object({
+        peInviteId: z.string().uuid(),
+        peInviteUuid: z.string().uuid(),
+        peId: z.string().uuid(),
+        peName: z.string(),
+        peOwnerId: z.string().uuid(),
+      }),
+    }),
+  };
+
+  POST_AcceptInvite = {
+    req: {
+      params: z.object({
+        peInviteUuid: z.string().uuid(),
+      }),
+    },
+    res: z.discriminatedUnion("code", [
+      z.object({
+        code: z.literal("success"),
+        peId: z.string().uuid(),
+      }),
+      z.object({
+        code: z.literal("inviteNotFound"),
+      }),
+      z.object({
+        code: z.literal("inviteLimitExceeded"),
+      }),
+      z.object({
+        code: z.literal("peMemberAlreadyExists"),
+      }),
+    ]),
+  };
 }
 
 export const clientPartEntitiesApi = new ClientPartEntitiesApi();

+ 10 - 0
src/db/db-schema.ts

@@ -156,6 +156,16 @@ const DbSchema = {
       peTypeId: z.string().uuid(),
       eventInstId: z.string().uuid(),
       ownerId: z.string().uuid(),
+      name: z.string(),
+    },
+    peInvites: {
+      peInviteId: z.string().uuid(),
+      peInviteUuid: z.string().uuid(),
+      peId: z.string().uuid(),
+      limitVal: z.number().int().nullable(),
+      countVal: z.number().int(),
+      name: z.string(),
+      expirationDate: z.string().datetime().nullable(),
     },
     peMembers: {
       peMemberId: z.string().uuid(),

+ 143 - 108
src/modules/client/activities/participant-entities/c-pe-controller.ts

@@ -13,12 +13,10 @@ import { RouterUtils } from "#utils/router-utils.js";
 
 import { Request, Response } from "express";
 import sessionService from "#modules/users/auth/services/session-service.js";
-import {
-  CustomFieldWithUserCopyValue,
-  CustomFieldWithValue,
-} from "#api/v_0.1.0/types/pe-types.js";
+import { CustomFieldWithUserCopyValue } from "#api/v_0.1.0/types/pe-types.js";
 import { ApiError } from "#exceptions/api-error.js";
-import { v7 } from "uuid";
+import { v4, v7 } from "uuid";
+import { cPeService } from "./c-pe-service.js";
 
 class ClientPeController {
   async getEventPeTypes(
@@ -209,7 +207,6 @@ class ClientPeController {
         peTypeId: DbSchema.act.partEntities.peTypeId,
         peTypeCode: DbSchema.act.peTypes.code,
         peTypeName: DbSchema.act.peTypes.name,
-        eventInstId: DbSchema.act.partEntities.eventInstId,
         ownerId: DbSchema.act.partEntities.ownerId,
         name: DbSchema.act.partEntities.name,
       }),
@@ -259,77 +256,9 @@ class ClientPeController {
 
     // валделец
     if (isOwner) {
-      const pe = await selPool.maybeOne(sql.type(
-        z.object({
-          peId: DbSchema.act.partEntities.peId,
-          peTypeId: DbSchema.act.partEntities.peTypeId,
-          peTypeCode: DbSchema.act.peTypes.code,
-          peTypeName: DbSchema.act.peTypes.name,
-          eventInstId: DbSchema.act.partEntities.eventInstId,
-          name: DbSchema.act.partEntities.name,
-          ownerId: DbSchema.act.partEntities.ownerId,
-          fields: z.array(CustomFieldWithValue),
-          members: z.array(
-            z.object({
-              memberId: DbSchema.act.peMembers.peMemberId,
-              userId: DbSchema.act.peMembers.userId,
-              email: DbSchema.usr.users.email,
-              fields: z.array(CustomFieldWithValue),
-            }),
-          ),
-          invites: z.array(
-            z.object({
-              peInviteId: DbSchema.act.peInvites.peInviteId,
-              peInviteUuid: DbSchema.act.peInvites.peInviteUuid,
-              name: DbSchema.act.peInvites.name,
-              limitVal: DbSchema.act.peInvites.limitVal,
-              countVal: DbSchema.act.peInvites.countVal,
-            }),
-          ),
-        }),
-      )`
-        select
-          pe.pe_id "peId",
-          pe.pe_type_id "peTypeId",
-          pe.pe_type_code "peTypeCode",
-          pe.pe_type_name "peTypeName",
-          pe.event_inst_id "eventInstId",
-          pe.name,
-          pe.owner_id "ownerId",
-          pe.fields::jsonb,
-          pe.members::jsonb,
-                  coalesce(jsonb_agg(jsonb_build_object(
-                  'peInviteId',
-                  i.pe_invite_id,
-                  'peInviteUuid',
-                  i.pe_invite_uuid,
-                  'name',
-                  i."name",
-                  'limitVal',
-                  i.limit_val,
-                  'countVal',
-                  i.count_val 
-          )) filter (
-        where
-          i.pe_id is not null),
-          '[]'::jsonb) as invites
-        from
-          act.pe_with_fields_and_members pe
-        left join act.pe_invites i on
-          i.pe_id = pe.pe_id
-        where
-          pe.pe_id = ${peId}
-        group by 
-          pe.pe_id,
-          pe.pe_type_id,
-          pe.pe_type_code,
-          pe.pe_type_name,
-          pe.event_inst_id,
-          pe.name,
-          pe.owner_id,
-          pe.fields,
-          pe.members
-    `);
+      const pe = await cPeService.getPeWithFieldsAndValues(peId);
+      const members = await cPeService.getMembers(peId);
+      const invites = await cPeService.getInvites(peId);
 
       if (!pe)
         throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
@@ -337,7 +266,12 @@ class ClientPeController {
 
       RouterUtils.validAndSendResponse(api.client.pe.GET_PartEntity.res, res, {
         code: "success",
-        pe: { ...pe, userRole: "owner" },
+        pe: {
+          ...pe,
+          members: [...members],
+          invites: [...invites],
+          userRole: "owner",
+        },
       });
       return;
     }
@@ -357,36 +291,7 @@ class ClientPeController {
 
     if (!isMember) throw ApiError.ForbiddenError();
 
-    const pe = await selPool.maybeOne(sql.type(
-      z.object({
-        peMemberId: DbSchema.act.peMembers.peMemberId,
-        peId: DbSchema.act.partEntities.peId,
-        peTypeId: DbSchema.act.partEntities.peTypeId,
-        peTypeCode: DbSchema.act.peTypes.code,
-        peTypeName: DbSchema.act.peTypes.name,
-        name: DbSchema.act.partEntities.name,
-        eventInstId: DbSchema.act.partEntities.eventInstId,
-        ownerId: DbSchema.act.partEntities.ownerId,
-      }),
-    )`
-      select
-        pm.pe_member_id "peMemberId",
-        pe.pe_id "peId",
-        pt.pe_type_id "peTypeId",
-        pt.code "peTypeCode",
-        pt.name "peTypeName",
-        pe.event_inst_id "eventInstId",
-        pe.owner_id "ownerId",
-        pe.name
-      from
-        act.pe_members pm
-      left join act.part_entities pe on
-        pe.pe_id = pm.pe_member_id
-      left join act.pe_types pt on
-        pt.pe_type_id = pe.pe_type_id
-      where
-        pe.pe_id = ${peId}
-  `);
+    const pe = await cPeService.getSimplePe(peId);
     if (!pe)
       throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена");
 
@@ -395,6 +300,136 @@ class ClientPeController {
       pe: { ...pe, userRole: "member" },
     });
   }
+
+  async createInvite(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const { name, limitVal, expirationDate } =
+      api.client.pe.POST_Invite.req.body.parse(req.body);
+    const { peId } = api.client.pe.POST_Invite.req.params.parse(req.params);
+
+    const isOwner = await cPeService.checkPeOwner(user.userId, peId);
+
+    if (!isOwner) throw ApiError.ForbiddenError();
+
+    const peInviteId = v7();
+    const peInviteUuid = v4();
+
+    await updPool.transaction(async (t) => {
+      await t.query(sql.unsafe`
+        insert into act.pe_invites 
+          (pe_invite_id, pe_invite_uuid, pe_id, limit_val, name, expiration_date)
+        values
+          (${peInviteId}, ${peInviteUuid}, ${peId}, ${limitVal}, ${name}, ${expirationDate})
+      `);
+    });
+
+    RouterUtils.validAndSendResponse(api.client.pe.POST_Invite.res, res, {
+      code: "success",
+      peInviteId,
+    });
+  }
+
+  async getInvites(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const { peId } = api.client.pe.GET_Invites.req.parse(req.params);
+
+    const isOwner = await cPeService.checkPeOwner(user.userId, peId);
+
+    if (!isOwner) throw ApiError.ForbiddenError();
+
+    const invites = await cPeService.getInvites(peId);
+
+    RouterUtils.validAndSendResponse(api.client.pe.GET_Invites.res, res, {
+      code: "success",
+      invites: [...invites],
+    });
+  }
+
+  async getInviteInfo(req: Request, res: Response) {
+    const { peInviteUuid } = api.client.pe.GET_InviteInfo.req.params.parse(
+      req.params,
+    );
+
+    const invite = await cPeService.getInviteInfo(peInviteUuid);
+
+    if (!invite)
+      throw ApiError.BadRequest("inviteNotFound", "Приглашение не найдено");
+
+    RouterUtils.validAndSendResponse(api.client.pe.GET_InviteInfo.res, res, {
+      code: "success",
+      invite,
+    });
+  }
+
+  async acceptInvite(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const { peInviteUuid } = api.client.pe.POST_AcceptInvite.req.params.parse(
+      req.params,
+    );
+
+    const invite = await cPeService.getInviteInfo(peInviteUuid);
+
+    if (!invite) {
+      RouterUtils.validAndSendResponse(
+        api.client.pe.POST_AcceptInvite.res,
+        res,
+        {
+          code: "inviteNotFound",
+        },
+        400,
+      );
+      return;
+    }
+
+    if (invite.limitVal && invite.countVal >= invite.limitVal) {
+      RouterUtils.validAndSendResponse(
+        api.client.pe.POST_AcceptInvite.res,
+        res,
+        {
+          code: "inviteLimitExceeded",
+        },
+        400,
+      );
+      return;
+    }
+
+    // TODO: много лишних данных
+    const peMembers = await cPeService.getMembers(invite.peId);
+    const isFound = peMembers.find((m) => m.userId === user.userId);
+    if (isFound) {
+      RouterUtils.validAndSendResponse(
+        api.client.pe.POST_AcceptInvite.res,
+        res,
+        {
+          code: "peMemberAlreadyExists",
+        },
+        400,
+      );
+      return;
+    }
+
+    updPool.transaction(async (t) => {
+      t.query(sql.unsafe`
+      insert into act.pe_members 
+        (pe_member_id, pe_id, user_id)
+      values
+        (${v7()}, ${invite.peId}, ${user.userId})
+    `);
+
+      t.query(sql.unsafe`
+      update act.pe_invites
+      set
+        count_val = count_val + 1
+      where
+        pe_id = ${invite.peId}
+    `);
+    });
+
+    RouterUtils.validAndSendResponse(api.client.pe.POST_AcceptInvite.res, res, {
+      code: "success",
+      peId: invite.peId,
+    });
+  }
 }
 
 export const clientPeController = new ClientPeController();

+ 22 - 0
src/modules/client/activities/participant-entities/c-pe-router.ts

@@ -20,3 +20,25 @@ router.post("/create/", RouterUtils.asyncHandler(clientPeController.createPe));
 router.get("/myPes/", RouterUtils.asyncHandler(clientPeController.getMyPes));
 
 router.get("/:peId", RouterUtils.asyncHandler(clientPeController.getPe));
+
+// router.get("/:peId/members", RouterUtils.asyncHandler(clientPeController.getMembers));
+
+router.get(
+  "/:peId/invites",
+  RouterUtils.asyncHandler(clientPeController.getInvites),
+);
+
+router.post(
+  "/:peId/invite",
+  RouterUtils.asyncHandler(clientPeController.createInvite),
+);
+
+router.get(
+  "/invite/:peInviteUuid",
+  RouterUtils.asyncHandler(clientPeController.getInviteInfo),
+);
+
+router.post(
+  "/invite/:peInviteUuid/accept",
+  RouterUtils.asyncHandler(clientPeController.acceptInvite),
+);

+ 166 - 0
src/modules/client/activities/participant-entities/c-pe-service.ts

@@ -0,0 +1,166 @@
+import { CustomFieldWithValue } from "#api/v_0.1.0/types/pe-types.js";
+import { DbSchema } from "#db/db-schema.js";
+import { selPool } from "#db/db.js";
+import { sql } from "slonik";
+import { z } from "zod";
+
+class CPeService {
+  async checkPeOwner(userId: string, peId: string) {
+    return await selPool.exists(sql.unsafe`
+            select
+                pe_id
+            from
+                act.part_entities pe
+            where
+                pe.owner_id = ${userId}
+                and pe.pe_id = ${peId}
+        `);
+  }
+
+  async getPeWithFieldsAndValues(peId: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        peId: z.string().uuid(),
+        peTypeId: z.string().uuid(),
+        peTypeCode: z.string(),
+        peTypeName: z.string(),
+        eventInstId: z.string().uuid(),
+        ownerId: z.string().uuid(),
+        name: z.string(),
+        fields: z.array(
+          CustomFieldWithValue.extend({
+            peFfId: z.string().uuid(),
+          }),
+        ),
+      }),
+    )`
+      select
+        pe_id "peId",
+        pe_type_id "peTypeId",
+        pe_type_code "peTypeCode",
+        pe_type_name "peTypeName",
+        event_inst_id "eventInstId",
+        name,
+        owner_id "ownerId",
+        fields
+      from
+        act.pe_with_fields_and_values
+      where
+        pe_id = ${peId}
+    `);
+  }
+
+  async getSimplePe(peId: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        peMemberId: DbSchema.act.peMembers.peMemberId,
+        peId: DbSchema.act.partEntities.peId,
+        peTypeId: DbSchema.act.partEntities.peTypeId,
+        peTypeCode: DbSchema.act.peTypes.code,
+        peTypeName: DbSchema.act.peTypes.name,
+        name: DbSchema.act.partEntities.name,
+        eventInstId: DbSchema.act.partEntities.eventInstId,
+        ownerId: DbSchema.act.partEntities.ownerId,
+      }),
+    )`
+      select
+        pm.pe_member_id "peMemberId",
+        pe.pe_id "peId",
+        pt.pe_type_id "peTypeId",
+        pt.code "peTypeCode",
+        pt.name "peTypeName",
+        pe.event_inst_id "eventInstId",
+        pe.owner_id "ownerId",
+        pe.name
+      from
+        act.pe_members pm
+      left join act.part_entities pe on
+        pe.pe_id = pm.pe_member_id
+      left join act.pe_types pt on
+        pt.pe_type_id = pe.pe_type_id
+      where
+        pe.pe_id = ${peId}
+  `);
+  }
+
+  async getInvites(peId: string) {
+    return await selPool.any(sql.type(
+      z.object({
+        peInviteId: DbSchema.act.peInvites.peInviteId,
+        peInviteUuid: DbSchema.act.peInvites.peInviteUuid,
+        name: DbSchema.act.peInvites.name,
+        limitVal: DbSchema.act.peInvites.limitVal,
+        countVal: DbSchema.act.peInvites.countVal,
+        expirationDate: DbSchema.act.peInvites.expirationDate,
+      }),
+    )`
+      select
+        pe_invite_id "peInviteId",
+        pe_invite_uuid "peInviteUuid",
+        name,
+        limit_val "limitVal",
+        count_val "countVal",
+        expiration_date "expirationDate"
+      from
+        act.pe_invites
+      where
+        pe_id = ${peId}
+    `);
+  }
+
+  async getMembers(peId: string) {
+    return await selPool.any(sql.type(
+      z.object({
+        peMemberId: z.string().uuid(),
+        userId: z.string().uuid(),
+        email: z.string().email(),
+        fields: z.array(
+          CustomFieldWithValue.extend({
+            userEfId: z.string().uuid(),
+          }),
+        ),
+      }),
+    )`
+      select
+        pe_member_id "peMemberId",
+        user_id "userId",
+        email,
+        fields
+      from
+        act.pe_members_with_fields_and_values
+      where
+        pe_id = ${peId}
+    `);
+  }
+
+  async getInviteInfo(peInviteUuid: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        peInviteId: DbSchema.act.peInvites.peInviteId,
+        peInviteUuid: DbSchema.act.peInvites.peInviteUuid,
+        peId: DbSchema.act.peInvites.peId,
+        peName: DbSchema.act.partEntities.name,
+        peOwnerId: DbSchema.act.partEntities.ownerId,
+        limitVal: DbSchema.act.peInvites.limitVal,
+        countVal: DbSchema.act.peInvites.countVal,
+      }),
+    )`
+      select
+        i.pe_invite_id "peInviteId",
+        i.pe_invite_uuid "peInviteUuid",
+        i.pe_id "peId",
+        pe."name" "peName",
+        pe.owner_id "peOwnerId",
+        i.limit_val "limitVal",
+        i.count_val "countVal"
+      from
+        act.pe_invites i
+      join act.part_entities pe on
+        pe.pe_id = i.pe_id
+      where
+        pe_invite_uuid = ${peInviteUuid}
+    `);
+  }
+}
+
+export const cPeService = new CPeService();