@raclettejs/core 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/cli.js +1 -1
  3. package/dev/README.md +4 -0
  4. package/dist/cli.js +135 -124
  5. package/dist/cli.js.map +4 -4
  6. package/dist/index.js +9 -8
  7. package/dist/index.js.map +4 -4
  8. package/package.json +9 -5
  9. package/services/backend/delay-api-debugger.sh +0 -0
  10. package/services/backend/package.json +1 -0
  11. package/services/backend/src/app.ts +4 -0
  12. package/services/backend/src/core/config/configService.ts +10 -1
  13. package/services/backend/src/core/events/eventEmitter.ts +32 -7
  14. package/services/backend/src/core/http/request-params.plugin.ts +2 -2
  15. package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +4 -1
  16. package/services/backend/src/core/pluginSystem/pluginTypes.ts +3 -1
  17. package/services/backend/src/core/sockets/createSystemEventHandlers.ts +7 -0
  18. package/services/backend/src/core/sockets/index.ts +61 -13
  19. package/services/backend/src/core/sockets/socketManager.ts +452 -105
  20. package/services/backend/src/core/sockets/socketTypes.ts +13 -1
  21. package/services/backend/src/core/sockets/workSessionJoinPayload.ts +106 -0
  22. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.get.ts +3 -1
  23. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.getAll.ts +3 -1
  24. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.hardDelete.ts +16 -19
  25. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.patch.ts +3 -1
  26. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.post.ts +3 -1
  27. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/routes/route.account.remove.ts +3 -1
  28. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +86 -3
  29. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/index.ts +2 -0
  30. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.get.ts +3 -1
  31. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.getAll.ts +3 -1
  32. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.hardDelete.ts +16 -19
  33. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.patch.ts +3 -1
  34. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +60 -0
  35. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.ts +3 -1
  36. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.remove.ts +3 -1
  37. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +12 -15
  38. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +96 -12
  39. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.ts +2 -0
  40. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.get.ts +3 -1
  41. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.getAll.ts +4 -2
  42. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.hardDelete.ts +12 -17
  43. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.patch.ts +3 -1
  44. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +62 -0
  45. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.ts +3 -1
  46. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.remove.ts +3 -1
  47. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +12 -15
  48. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +1 -0
  49. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.get.ts +3 -1
  50. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.getAll.ts +4 -2
  51. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.hardDelete.ts +3 -1
  52. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.patch.ts +3 -1
  53. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.post.ts +4 -2
  54. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/route.project.remove.ts +3 -1
  55. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/index.ts +2 -0
  56. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.get.ts +1 -0
  57. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.getAll.ts +1 -0
  58. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.hardDelete.ts +10 -15
  59. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.patch.ts +3 -1
  60. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +60 -0
  61. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.ts +3 -1
  62. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.remove.ts +3 -1
  63. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +12 -15
  64. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +73 -3
  65. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.get.ts +1 -0
  66. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.getAll.ts +1 -0
  67. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.hardDelete.ts +10 -15
  68. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patch.ts +4 -6
  69. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchLastProject.ts +4 -2
  70. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.post.ts +3 -1
  71. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.remove.ts +3 -4
  72. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +12 -15
  73. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/events/index.ts +45 -0
  74. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/crud.ts +12 -0
  75. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/helpers/payload.ts +21 -0
  76. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/index.ts +55 -0
  77. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.ts +19 -0
  78. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.get.ts +53 -0
  79. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.getAll.ts +39 -0
  80. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.hardDelete.ts +53 -0
  81. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.patch.ts +58 -0
  82. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.post.ts +53 -0
  83. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/routes/route.workSession.remove.ts +52 -0
  84. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.model.ts +65 -0
  85. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.schema.ts +125 -0
  86. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/workSession/workSession.service.ts +351 -0
  87. package/services/backend/src/corePlugins/raclette__core/backend/index.ts +2 -0
  88. package/services/backend/src/corePlugins/raclette__core/frontend/generated-config.ts +159 -24
  89. package/services/backend/src/modules/authentication/index.ts +255 -4
  90. package/services/backend/src/modules/hooks/rate-limiter.ts +21 -0
  91. package/services/backend/src/types/custom-fastify.d.ts +17 -0
  92. package/services/backend/src/utils/request.utils.ts +4 -1
  93. package/services/backend/types/index.d.ts +15 -1
  94. package/services/backend/yarn.lock +13 -4
  95. package/services/frontend/index.html +1 -1
  96. package/services/frontend/provision/entrypoint.sh +0 -0
  97. package/services/frontend/src/core/lib/configService.ts +1 -1
  98. package/services/frontend/src/core/lib/data/dataApi.ts +10 -1
  99. package/services/frontend/src/core/lib/dataHelper.ts +1 -1
  100. package/services/frontend/src/core/lib/eggs/snow.ts +66 -48
  101. package/services/frontend/src/core/lib/helpers/index.ts +1 -0
  102. package/services/frontend/src/core/lib/helpers/removeEmpty.ts +69 -0
  103. package/services/frontend/src/core/lib/httpClient.ts +19 -0
  104. package/services/frontend/src/core/main.ts +20 -10
  105. package/services/frontend/src/core/setup/socket.ts +4 -3
  106. package/services/frontend/src/core/setup/socketClient.ts +31 -3
  107. package/services/frontend/src/core/setup/socketEvents.ts +92 -8
  108. package/services/frontend/src/core/store/index.ts +5 -1
  109. package/services/frontend/src/core/store/reducers/compositions/index.ts +2 -2
  110. package/services/frontend/src/core/store/reducers/interactionLinks/index.ts +1 -1
  111. package/services/frontend/src/core/store/reducers/widgets/reducers.ts +1 -2
  112. package/services/frontend/src/core/store/types/index.ts +0 -1
  113. package/services/frontend/src/core/types/PluginRegistrar.ts +8 -6
  114. package/services/frontend/src/core/types/Store.ts +1 -0
  115. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +68 -8
  116. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +17 -5
  117. package/services/frontend/src/orchestrator/components/welcomeScreen/AdminWelcomeScreen.vue +1 -1
  118. package/services/frontend/src/orchestrator/composables/index.ts +1 -0
  119. package/services/frontend/src/orchestrator/composables/usePluginApi.ts +0 -3
  120. package/services/frontend/src/orchestrator/composables/useRouteState.ts +23 -4
  121. package/services/frontend/src/orchestrator/composables/useWidgetLifecycle.ts +166 -0
  122. package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
  123. package/services/frontend/src/orchestrator/i18n/de-DE.json +15 -1
  124. package/services/frontend/src/orchestrator/i18n/en-EU.json +15 -1
  125. package/services/frontend/src/orchestrator/i18n/sk.json +15 -1
  126. package/services/frontend/src/orchestrator/index.ts +34 -4
  127. package/services/frontend/src/orchestrator/router/routeParserHelper.ts +106 -1
  128. package/services/frontend/src/orchestrator/types/Widgets.ts +2 -0
  129. package/services/frontend/types.ts +1 -0
  130. package/src/README.md +404 -0
  131. package/src/types.ts +19 -5
  132. package/templates/README.md +0 -0
  133. package/types/index.ts +18 -4
  134. package/services/backend/.gitignore +0 -17
  135. package/services/frontend/.gitignore +0 -35
  136. package/yarn.lock +0 -2937
@@ -0,0 +1,106 @@
1
+ import { v4 as uuidv4 } from "uuid"
2
+
3
+ /**
4
+ * Builds the joinConfirmation payload shape for work-session guests.
5
+ * Keeps virtual composition and simulated interaction-link logic out of socketManager.
6
+ */
7
+
8
+ interface WorkSessionLike {
9
+ _id: string
10
+ userId: string
11
+ createdAt?: Date | string
12
+ updatedAt?: Date | string
13
+ [key: string]: unknown
14
+ }
15
+
16
+ interface CompositionLike {
17
+ title?: string
18
+ description?: string
19
+ tags?: unknown[]
20
+ icon?: string
21
+ slotLayout?: string
22
+ widgetsLayout?: unknown[]
23
+ status?: string
24
+ createdAt?: string
25
+ updatedAt?: string
26
+ [key: string]: unknown
27
+ }
28
+
29
+ /** Virtual composition sent to frontend (pathname.default matches route /:workSession/id=...) */
30
+ function buildVirtualComposition(
31
+ composition: CompositionLike,
32
+ workSession: WorkSessionLike,
33
+ ): Record<string, unknown> {
34
+ const now = new Date()
35
+ return {
36
+ _id: uuidv4(),
37
+ title: composition.title ?? "Shared Session",
38
+ description:
39
+ composition.description ?? "Shared composition for anonymous access",
40
+ tags: composition.tags ?? [],
41
+ icon: composition.icon,
42
+ pathname: { default: "workSession" },
43
+ slotLayout: composition.slotLayout ?? "full",
44
+ widgetsLayout: composition.widgetsLayout ?? [],
45
+ status: composition.status ?? "live",
46
+ author: workSession.userId,
47
+ lastEditor: workSession.userId,
48
+ isDeleted: false,
49
+ createdAt: composition.createdAt ?? now.toISOString(),
50
+ updatedAt: composition.updatedAt ?? now.toISOString(),
51
+ }
52
+ }
53
+
54
+ /** Simulated interaction link so the frontend can resolve the composition like a normal project. */
55
+ function buildSimulatedInteractionLink(
56
+ workSession: WorkSessionLike,
57
+ virtualComposition: { _id: string; [key: string]: unknown },
58
+ ): Record<string, unknown> {
59
+ const created = workSession.createdAt
60
+ ? new Date(workSession.createdAt)
61
+ : new Date()
62
+ const updated = workSession.updatedAt
63
+ ? new Date(workSession.updatedAt)
64
+ : new Date()
65
+ return {
66
+ _id: `session_${workSession._id}_interactionLink`,
67
+ composition: virtualComposition._id,
68
+ slotType: "page",
69
+ triggers: [],
70
+ queryParameters: [],
71
+ description: "Shared session composition",
72
+ tags: [],
73
+ author: workSession.userId,
74
+ lastEditor: workSession.userId,
75
+ isLandingPage: true,
76
+ isDeleted: false,
77
+ createdAt: created,
78
+ updatedAt: updated,
79
+ }
80
+ }
81
+
82
+ export interface WorkSessionJoinPayloadData {
83
+ compositions: unknown[]
84
+ interactionLinks: unknown[]
85
+ projectConfig: Record<string, unknown>
86
+ }
87
+
88
+ /**
89
+ * Build the data part of joinConfirmation for a work session (no token).
90
+ * Socket manager adds token and decoded after creating the JWT.
91
+ */
92
+ export function buildWorkSessionJoinPayload(
93
+ workSession: WorkSessionLike,
94
+ composition: CompositionLike,
95
+ ): WorkSessionJoinPayloadData {
96
+ const virtualComposition = buildVirtualComposition(composition, workSession)
97
+ const simulatedInteractionLink = buildSimulatedInteractionLink(
98
+ workSession,
99
+ virtualComposition as { _id: string; [key: string]: unknown },
100
+ )
101
+ return {
102
+ compositions: [virtualComposition],
103
+ interactionLinks: [simulatedInteractionLink],
104
+ projectConfig: {},
105
+ }
106
+ }
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import { Type } from "@sinclair/typebox"
4
4
  import type { PluginFastifyInstance } from "types"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  const ParamsSchema = Type.Object({
7
9
  _id: Type.String(),
8
10
  })
@@ -39,7 +41,7 @@ export default (fastify: PluginFastifyInstance) => {
39
41
 
40
42
  return {
41
43
  handler,
42
- onRequest: [fastify.authenticate],
44
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
43
45
  config: {
44
46
  type: "dataRead",
45
47
  },
@@ -1,6 +1,8 @@
1
1
  import type { FastifyReply, FastifyRequest } from "fastify"
2
2
  import type { PluginFastifyInstance } from "types"
3
3
 
4
+ const permissions = ["user.isAdmin"]
5
+
4
6
  export default (fastify: PluginFastifyInstance) => {
5
7
  const accountService = fastify.custom.accountService
6
8
 
@@ -20,7 +22,7 @@ export default (fastify: PluginFastifyInstance) => {
20
22
 
21
23
  return {
22
24
  handler,
23
- onRequest: [fastify.authenticate],
25
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
24
26
  config: {
25
27
  type: "dataRead",
26
28
  },
@@ -2,7 +2,8 @@ import type { Static } from "@sinclair/typebox"
2
2
  import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
- import { ForbiddenError } from "@/utils/errors"
5
+
6
+ const permissions = ["user.isAdmin"]
6
7
 
7
8
  const ParamsSchema = Type.Object({
8
9
  _id: Type.String(),
@@ -18,29 +19,25 @@ export default (fastify: PluginFastifyInstance) => {
18
19
  }>,
19
20
  reply: FastifyReply,
20
21
  ) => {
21
- if (req.user.isAdmin) {
22
- try {
23
- const { _id } = req.params
24
-
25
- const payload = await accountService.hardDeleteAccount(
26
- fastify,
27
- req.requestParams,
28
- _id,
29
- )
30
-
31
- return payload
32
- } catch (error: any) {
33
- fastify.log.error(error.message)
34
- return reply.internalServerError(error.message)
35
- }
36
- } else {
37
- throw new ForbiddenError("Only admins can hard delete this core datatype")
22
+ try {
23
+ const { _id } = req.params
24
+
25
+ const payload = await accountService.hardDeleteAccount(
26
+ fastify,
27
+ req.requestParams,
28
+ _id,
29
+ )
30
+
31
+ return payload
32
+ } catch (error: any) {
33
+ fastify.log.error(error.message)
34
+ return reply.internalServerError(error.message)
38
35
  }
39
36
  }
40
37
 
41
38
  return {
42
39
  handler,
43
- onRequest: [fastify.authenticate],
40
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
44
41
  config: {
45
42
  type: "dataHardDeleted",
46
43
  broadcastChannels: ["accountHardDeleted"],
@@ -5,6 +5,8 @@ import type { PluginFastifyInstance } from "types"
5
5
  import { Type } from "@sinclair/typebox"
6
6
  import { accountSchema } from "../account.schema"
7
7
 
8
+ const permissions = ["user.isAdmin"]
9
+
8
10
  const ParamsSchema = Type.Object({
9
11
  _id: Type.String(),
10
12
  })
@@ -40,7 +42,7 @@ export default (fastify: PluginFastifyInstance) => {
40
42
 
41
43
  return {
42
44
  handler,
43
- onRequest: [fastify.authenticate],
45
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
44
46
  config: {
45
47
  type: "dataUpdate",
46
48
  broadcastChannels: ["accountUpdated", "broadcastDataUpdated"],
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { accountSchema } from "../account.schema"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  export default (fastify: PluginFastifyInstance) => {
7
9
  const accountService = fastify.custom.accountService
8
10
 
@@ -29,7 +31,7 @@ export default (fastify: PluginFastifyInstance) => {
29
31
 
30
32
  return {
31
33
  handler,
32
- onRequest: [fastify.authenticate],
34
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
33
35
  config: {
34
36
  type: "dataCreate",
35
37
  broadcastChannels: ["accountCreated"],
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  const ParamsSchema = Type.Object({
7
9
  _id: Type.String(),
8
10
  })
@@ -35,7 +37,7 @@ export default (fastify: PluginFastifyInstance) => {
35
37
 
36
38
  return {
37
39
  handler,
38
- onRequest: [fastify.authenticate],
40
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
39
41
  config: {
40
42
  type: "dataDelete",
41
43
  broadcastChannels: ["accountDeleted"],
@@ -94,11 +94,10 @@ export class CompositionService {
94
94
  */
95
95
  async _readAllCompositions(
96
96
  fastify: PluginFastifyInstance,
97
- filters: Record<string, any> = {},
97
+ filters: Record<string, any> = { isDeleted: false },
98
98
  ): Promise<CompositionType[]> {
99
99
  try {
100
- if (filters.isDeleted !== true && filters.isDeleted !== "true")
101
- filters.isDeleted = false
100
+ filters = { isDeleted: false, ...filters }
102
101
  return await this.compositionModel
103
102
  .find({
104
103
  ...filters,
@@ -337,6 +336,90 @@ export class CompositionService {
337
336
  payload.body.deleted = [composition._id]
338
337
  fastify.emit("compositionHardDeleted", payload)
339
338
 
339
+ return payload
340
+ }
341
+ /**
342
+ * Core function to create multiple compositions (returns raw data)
343
+ */
344
+ async _createCompositions(
345
+ fastify: PluginFastifyInstance,
346
+ compositionBodies: CompositionBody[],
347
+ userId: string,
348
+ ): Promise<CompositionType[]> {
349
+ try {
350
+ const compositionsToCreate: CompositionBody[] = []
351
+
352
+ // Validate and prepare all compositions
353
+ for (const compositionBody of compositionBodies) {
354
+ if (compositionBody._id) {
355
+ const uuidValid = validate(compositionBody._id)
356
+
357
+ if (!uuidValid) {
358
+ throw new Error(
359
+ `Invalid ID - not a valid uuid v4: ${compositionBody._id}`,
360
+ )
361
+ }
362
+
363
+ const duplicate = await this.compositionModel.findOne({
364
+ _id: compositionBody._id,
365
+ })
366
+
367
+ if (duplicate) {
368
+ throw new Error(
369
+ `An entry with this id already exists: ${compositionBody._id}`,
370
+ )
371
+ }
372
+ } else {
373
+ compositionBody._id = uuidv4()
374
+ }
375
+
376
+ compositionsToCreate.push({
377
+ ...compositionBody,
378
+ author: userId,
379
+ lastEditor: userId,
380
+ })
381
+ }
382
+
383
+ // Bulk insert
384
+ const compositions =
385
+ await this.compositionModel.insertMany(compositionsToCreate)
386
+
387
+ fastify.log.info(
388
+ `[API] Created ${compositions.length} compositions in bulk`,
389
+ )
390
+
391
+ return compositions.map((comp) =>
392
+ comp.toObject ? comp.toObject() : comp,
393
+ )
394
+ } catch (error: any) {
395
+ fastify.log.error(error.message)
396
+ throw error
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Create multiple compositions with payload wrapping and event emission
402
+ */
403
+ async createCompositions(
404
+ fastify: PluginFastifyInstance,
405
+ requestData: FrontendPayloadRequestData,
406
+ compositionBodies: CompositionBody[],
407
+ userId: string,
408
+ ): Promise<FrontendPayload<CompositionType[]>> {
409
+ const compositions = await this._createCompositions(
410
+ fastify,
411
+ compositionBodies,
412
+ userId,
413
+ )
414
+
415
+ const payload = await createCompositionPayload(
416
+ fastify,
417
+ compositions,
418
+ requestData,
419
+ )
420
+
421
+ fastify.emit("compositionsCreated", payload)
422
+
340
423
  return payload
341
424
  }
342
425
  }
@@ -4,6 +4,7 @@ import getAll from "./route.composition.getAll"
4
4
  import hardDelete from "./route.composition.hardDelete"
5
5
  import patch from "./route.composition.patch"
6
6
  import post from "./route.composition.post"
7
+ import postBulk from "./route.composition.post.bulk"
7
8
  import remove from "./route.composition.remove"
8
9
  import restore from "./route.composition.restore"
9
10
 
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
13
14
  await fastify.patch("/composition/:_id", patch(fastify))
14
15
  await fastify.patch("/composition/:_id/restore", restore(fastify))
15
16
  await fastify.post("/composition", post(fastify))
17
+ await fastify.post("/composition/bulk", postBulk(fastify))
16
18
  await fastify.delete("/composition/:_id", remove(fastify))
17
19
  await fastify.delete("/composition/:_id/hard", hardDelete(fastify))
18
20
  }
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  const ParamsSchema = Type.Object({
7
9
  _id: Type.String(),
8
10
  })
@@ -39,7 +41,7 @@ export default (fastify: PluginFastifyInstance) => {
39
41
 
40
42
  return {
41
43
  handler,
42
- onRequest: [fastify.authenticate],
44
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
43
45
  config: {
44
46
  type: "dataRead",
45
47
  },
@@ -1,6 +1,8 @@
1
1
  import type { FastifyReply, FastifyRequest } from "fastify"
2
2
  import type { PluginFastifyInstance } from "types"
3
3
 
4
+ const permissions = ["user.isAdmin"]
5
+
4
6
  export default (fastify: PluginFastifyInstance) => {
5
7
  const compositionService = fastify.custom.compositionService
6
8
 
@@ -21,7 +23,7 @@ export default (fastify: PluginFastifyInstance) => {
21
23
 
22
24
  return {
23
25
  handler,
24
- onRequest: [fastify.authenticate],
26
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
25
27
  config: {
26
28
  type: "dataRead",
27
29
  },
@@ -2,7 +2,8 @@ import type { Static } from "@sinclair/typebox"
2
2
  import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
- import { ForbiddenError } from "@/utils/errors"
5
+
6
+ const permissions = ["user.isAdmin"]
6
7
 
7
8
  const ParamsSchema = Type.Object({
8
9
  _id: Type.String(),
@@ -18,29 +19,25 @@ export default (fastify: PluginFastifyInstance) => {
18
19
  }>,
19
20
  reply: FastifyReply,
20
21
  ) => {
21
- if (req.user.isAdmin) {
22
- try {
23
- const { _id } = req.params
24
-
25
- const payload = await compositionService.hardDeleteComposition(
26
- fastify,
27
- req.requestParams,
28
- _id,
29
- )
30
-
31
- return payload
32
- } catch (error: any) {
33
- fastify.log.error(error.message)
34
- return reply.internalServerError(error.message)
35
- }
36
- } else {
37
- throw new ForbiddenError("Only admins can hard delete this core datatype")
22
+ try {
23
+ const { _id } = req.params
24
+
25
+ const payload = await compositionService.hardDeleteComposition(
26
+ fastify,
27
+ req.requestParams,
28
+ _id,
29
+ )
30
+
31
+ return payload
32
+ } catch (error: any) {
33
+ fastify.log.error(error.message)
34
+ return reply.internalServerError(error.message)
38
35
  }
39
36
  }
40
37
 
41
38
  return {
42
39
  handler,
43
- onRequest: [fastify.authenticate],
40
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
44
41
  config: {
45
42
  type: "dataHardDelete",
46
43
  broadcastChannels: ["compositionHardDeleted"],
@@ -5,6 +5,8 @@ import type { PluginFastifyInstance } from "types"
5
5
  import { Type } from "@sinclair/typebox"
6
6
  import { compositionUpdateSchema } from "../composition.schema"
7
7
 
8
+ const permissions = ["user.isAdmin"]
9
+
8
10
  const ParamsSchema = Type.Object({
9
11
  _id: Type.String(),
10
12
  })
@@ -42,7 +44,7 @@ export default (fastify: PluginFastifyInstance) => {
42
44
 
43
45
  return {
44
46
  handler,
45
- onRequest: [fastify.authenticate],
47
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
46
48
  config: {
47
49
  type: "dataUpdate",
48
50
  broadcastChannels: ["compositionUpdated"],
@@ -0,0 +1,60 @@
1
+ import type { CompositionBody } from "../composition.service"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import type { PluginFastifyInstance } from "types"
4
+ import { compositionCreateSchema } from "../composition.schema"
5
+
6
+ const permissions = ["user.isAdmin"]
7
+
8
+ export default (fastify: PluginFastifyInstance) => {
9
+ const compositionService = fastify.custom.compositionService
10
+
11
+ const handler = async (
12
+ req: FastifyRequest<{
13
+ Body: CompositionBody[]
14
+ }>,
15
+ reply: FastifyReply,
16
+ ) => {
17
+ try {
18
+ // Validate that body is an array
19
+ if (!Array.isArray(req.body)) {
20
+ return reply.badRequest("Request body must be an array of compositions")
21
+ }
22
+
23
+ // Validate that array is not empty
24
+ if (req.body.length === 0) {
25
+ return reply.badRequest("Request body cannot be an empty array")
26
+ }
27
+
28
+ const payload = await compositionService.createCompositions(
29
+ fastify,
30
+ req.requestParams,
31
+ req.body,
32
+ req.user._id,
33
+ )
34
+
35
+ return payload
36
+ } catch (error: any) {
37
+ fastify.log.error(error.message)
38
+
39
+ return reply.internalServerError(error.message)
40
+ }
41
+ }
42
+
43
+ return {
44
+ handler,
45
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
46
+ config: {
47
+ type: "dataCreate",
48
+ broadcastChannels: ["compositionsCreated"],
49
+ },
50
+ schema: {
51
+ summary: "Create multiple compositions in bulk",
52
+ tags: ["core/composition"],
53
+ body: {
54
+ type: "array",
55
+ items: compositionCreateSchema,
56
+ minItems: 1,
57
+ },
58
+ },
59
+ }
60
+ }
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { compositionCreateSchema } from "../composition.schema"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  export default (fastify: PluginFastifyInstance) => {
7
9
  const compositionService = fastify.custom.compositionService
8
10
 
@@ -30,7 +32,7 @@ export default (fastify: PluginFastifyInstance) => {
30
32
 
31
33
  return {
32
34
  handler,
33
- onRequest: [fastify.authenticate],
35
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
34
36
  config: {
35
37
  type: "dataCreate",
36
38
  broadcastChannels: ["compositionCreated"],
@@ -3,6 +3,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import type { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
5
 
6
+ const permissions = ["user.isAdmin"]
7
+
6
8
  const ParamsSchema = Type.Object({
7
9
  _id: Type.String(),
8
10
  })
@@ -35,7 +37,7 @@ export default (fastify: PluginFastifyInstance) => {
35
37
 
36
38
  return {
37
39
  handler,
38
- onRequest: [fastify.authenticate],
40
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
39
41
  config: {
40
42
  type: "dataDelete",
41
43
  broadcastChannels: ["compositionDeleted"],
@@ -2,7 +2,8 @@ import type { Static } from "@sinclair/typebox"
2
2
  import type { FastifyReply, FastifyRequest } from "fastify"
3
3
  import { PluginFastifyInstance } from "types"
4
4
  import { Type } from "@sinclair/typebox"
5
- import { ForbiddenError } from "@/utils/errors"
5
+
6
+ const permissions = ["user.isAdmin"]
6
7
 
7
8
  const ParamsSchema = Type.Object({
8
9
  _id: Type.String(),
@@ -17,21 +18,17 @@ export default (fastify: PluginFastifyInstance) => {
17
18
  reply: FastifyReply,
18
19
  ) => {
19
20
  try {
20
- if (req.user.isAdmin) {
21
- const { _id } = req.params
21
+ const { _id } = req.params
22
22
 
23
- const payload = await compositionService.updateComposition(
24
- fastify,
25
- req.requestParams,
26
- _id,
27
- { isDeleted: false },
28
- req.user._id,
29
- )
23
+ const payload = await compositionService.updateComposition(
24
+ fastify,
25
+ req.requestParams,
26
+ _id,
27
+ { isDeleted: false },
28
+ req.user._id,
29
+ )
30
30
 
31
- return payload
32
- } else {
33
- throw new ForbiddenError("Only admins can restore this core datatype")
34
- }
31
+ return payload
35
32
  } catch (error: any) {
36
33
  fastify.log.error(error.message)
37
34
  return reply.internalServerError(error.message)
@@ -40,7 +37,7 @@ export default (fastify: PluginFastifyInstance) => {
40
37
 
41
38
  return {
42
39
  handler,
43
- onRequest: [fastify.authenticate],
40
+ onRequest: [fastify.authenticate, fastify.checkPermissions(permissions)],
44
41
  config: {
45
42
  type: "dataRestore",
46
43
  broadcastChannels: ["compositionRestored"],