@raclettejs/core 0.1.17 → 0.1.19
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.
- package/CHANGELOG.md +30 -1
- package/package.json +2 -2
- package/services/backend/package.json +2 -2
- package/services/backend/src/core/config/configService.ts +4 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +86 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/index.ts +4 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +58 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +55 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +96 -9
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.ts +4 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +60 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +55 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.model.ts +17 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.schema.ts +18 -6
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/index.ts +5 -5
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/index.ts +4 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +58 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +55 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +74 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/index.ts +2 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +55 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +1 -0
- package/services/backend/src/domains/system/routes/route.user.login.ts +4 -2
- package/services/backend/src/shared/types/core/ProjectUpdate.types.ts +1 -0
- package/services/backend/src/shared/types/dataTypes/tags.ts +1 -0
- package/services/frontend/package.json +8 -8
- package/services/frontend/src/core/lib/configService.ts +1 -1
- package/services/frontend/src/core/lib/data/dataApi.ts +10 -1
- package/services/frontend/src/core/lib/data/fetchDataHandler.ts +3 -23
- package/services/frontend/src/core/lib/data/writeDataHandler.ts +8 -13
- package/services/frontend/src/core/lib/dataHelper.ts +15 -0
- package/services/frontend/src/core/lib/eggs/snow.ts +32 -43
- package/services/frontend/src/core/lib/eventWhitelist.ts +4 -13
- package/services/frontend/src/core/main.ts +7 -1
- package/services/frontend/src/orchestrator/LoginApp.vue +0 -1
- package/services/frontend/src/orchestrator/ProductOrchestrator.vue +40 -5
- package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +7 -3
- package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +12 -24
- package/services/frontend/src/orchestrator/components/input/SimpleSearchInput.vue +16 -4
- package/services/frontend/src/orchestrator/components/menu/AppBarMenu.vue +184 -0
- package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +62 -46
- package/services/frontend/src/orchestrator/components/navigation/PageNavigationSidebar.vue +11 -1
- package/services/frontend/src/orchestrator/composables/usePageNavigation.ts +10 -1
- package/services/frontend/src/orchestrator/i18n/de-DE.json +15 -0
- package/services/frontend/src/orchestrator/i18n/en-EU.json +15 -0
- package/services/frontend/src/orchestrator/i18n/sk.json +15 -0
- package/services/frontend/src/orchestrator/router/index.ts +16 -0
- package/yarn.lock +4 -4
- package/services/frontend/src/orchestrator/setup/loginApp.ts +0 -27
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.19] - 12.12.2025 <a href="https://gitlab.com/raclettejs/core/-/compare/v0.1.18...v0.1.19" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Global: added bulk import to interactionLinks, compositions and tags
|
|
15
|
+
|
|
16
|
+
## [0.1.18] - 05.12.2025 <a href="https://gitlab.com/raclettejs/core/-/compare/v0.1.17...v0.1.18" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Backend: project data type deletion routes temporarily disabled. Will be reenabled soon
|
|
21
|
+
- Frontend: updated dependencies
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Frontend: usermenu - added alternate render mode for inline composition if no trigger id/title is set
|
|
26
|
+
- Frontend: Appbar can now house interactionLinks and inline compositions
|
|
27
|
+
- Frontend: appbar can now launch compositions in a floating window
|
|
28
|
+
- Backend: added restore endpoints for tag/user/composition/interactionLink
|
|
29
|
+
- Frontend: Token expiry will now redirect automatically to login. Last location during auth loss will be used for redirect later
|
|
30
|
+
- Frontend: Simple search input can now receive custom label and uses debounce before model update
|
|
31
|
+
- Frontend: PageNavigation can now rail
|
|
32
|
+
- Backend: Project dataType can now hold config values
|
|
33
|
+
- Global: Added new Tagtype "event"
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Backend: isDeleted handling in getAll routes. isDeleted needs to be explicitly true to see deleted entries
|
|
38
|
+
- Backend: TTL for auth token is not derived from raclette.config
|
|
39
|
+
|
|
10
40
|
## [0.1.17] - 06.11.2025 <a href="https://gitlab.com/raclettejs/core/-/compare/v0.1.15...v0.1.17" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
11
41
|
|
|
12
42
|
### Fixed
|
|
@@ -14,7 +44,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
14
44
|
- Frontend: axios Delete handling
|
|
15
45
|
- Frontend: retrieval of deleted compositions/interactionLinks during socket join
|
|
16
46
|
- Frontend: notifications for delete events now show name
|
|
17
|
-
-
|
|
18
47
|
|
|
19
48
|
### Added
|
|
20
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raclettejs/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "racletteJS core package",
|
|
5
5
|
"repository": "https://gitlab.com/raclettejs/core",
|
|
6
6
|
"author": "Pacifico Digital Explorations GmbH <info@raclettejs.com>",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@eslint/js": "9.35.0",
|
|
72
|
-
"@raclettejs/types": "
|
|
72
|
+
"@raclettejs/types": "0.1.19",
|
|
73
73
|
"@types/fs-extra": "11.0.4",
|
|
74
74
|
"@types/js-yaml": "4.0.9",
|
|
75
75
|
"@types/node": "24.5.1",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@fastify/sensible": "6.0.3",
|
|
31
31
|
"@sinclair/typebox": "0.34.41",
|
|
32
32
|
"@types/vary": "1.1.3",
|
|
33
|
-
"axios": "1.
|
|
33
|
+
"axios": "1.13.2",
|
|
34
34
|
"bcrypt": "6.0.0",
|
|
35
35
|
"copyfiles": "2.4.1",
|
|
36
36
|
"fastify": "5.6.0",
|
|
@@ -62,4 +62,4 @@
|
|
|
62
62
|
"typescript": "5.9.2",
|
|
63
63
|
"typescript-eslint": "8.44.0"
|
|
64
64
|
}
|
|
65
|
-
}
|
|
65
|
+
}
|
|
@@ -13,6 +13,7 @@ type RacletteBackendConfig = {
|
|
|
13
13
|
plugins?: string[]
|
|
14
14
|
global?: {
|
|
15
15
|
requireAuthentication?: boolean
|
|
16
|
+
authTokenExpiryDays?: number
|
|
16
17
|
}
|
|
17
18
|
additionalPluginPaths?: string[]
|
|
18
19
|
}
|
|
@@ -130,6 +131,9 @@ export class ConfigService {
|
|
|
130
131
|
public isAuthRequired(): boolean {
|
|
131
132
|
return this.config?.global?.requireAuthentication ?? true
|
|
132
133
|
}
|
|
134
|
+
public getAuthTokenExpiryDays(): number {
|
|
135
|
+
return this.config?.global?.authTokenExpiryDays ?? 30
|
|
136
|
+
}
|
|
133
137
|
public isAdminTagRequired(): boolean {
|
|
134
138
|
return this.config?.onlyAllowLoginWithAdminTag ?? false
|
|
135
139
|
}
|
|
@@ -97,6 +97,8 @@ export class CompositionService {
|
|
|
97
97
|
filters: Record<string, any> = {},
|
|
98
98
|
): Promise<CompositionType[]> {
|
|
99
99
|
try {
|
|
100
|
+
if (filters.isDeleted !== true && filters.isDeleted !== "true")
|
|
101
|
+
filters.isDeleted = false
|
|
100
102
|
return await this.compositionModel
|
|
101
103
|
.find({
|
|
102
104
|
...filters,
|
|
@@ -335,6 +337,90 @@ export class CompositionService {
|
|
|
335
337
|
payload.body.deleted = [composition._id]
|
|
336
338
|
fastify.emit("compositionHardDeleted", payload)
|
|
337
339
|
|
|
340
|
+
return payload
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Core function to create multiple compositions (returns raw data)
|
|
344
|
+
*/
|
|
345
|
+
async _createCompositions(
|
|
346
|
+
fastify: PluginFastifyInstance,
|
|
347
|
+
compositionBodies: CompositionBody[],
|
|
348
|
+
userId: string,
|
|
349
|
+
): Promise<CompositionType[]> {
|
|
350
|
+
try {
|
|
351
|
+
const compositionsToCreate: CompositionBody[] = []
|
|
352
|
+
|
|
353
|
+
// Validate and prepare all compositions
|
|
354
|
+
for (const compositionBody of compositionBodies) {
|
|
355
|
+
if (compositionBody._id) {
|
|
356
|
+
const uuidValid = validate(compositionBody._id)
|
|
357
|
+
|
|
358
|
+
if (!uuidValid) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Invalid ID - not a valid uuid v4: ${compositionBody._id}`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const duplicate = await this.compositionModel.findOne({
|
|
365
|
+
_id: compositionBody._id,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (duplicate) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`An entry with this id already exists: ${compositionBody._id}`,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
compositionBody._id = uuidv4()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
compositionsToCreate.push({
|
|
378
|
+
...compositionBody,
|
|
379
|
+
author: userId,
|
|
380
|
+
lastEditor: userId,
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Bulk insert
|
|
385
|
+
const compositions =
|
|
386
|
+
await this.compositionModel.insertMany(compositionsToCreate)
|
|
387
|
+
|
|
388
|
+
fastify.log.info(
|
|
389
|
+
`[API] Created ${compositions.length} compositions in bulk`,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return compositions.map((comp) =>
|
|
393
|
+
comp.toObject ? comp.toObject() : comp,
|
|
394
|
+
)
|
|
395
|
+
} catch (error: any) {
|
|
396
|
+
fastify.log.error(error.message)
|
|
397
|
+
throw error
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create multiple compositions with payload wrapping and event emission
|
|
403
|
+
*/
|
|
404
|
+
async createCompositions(
|
|
405
|
+
fastify: PluginFastifyInstance,
|
|
406
|
+
requestData: FrontendPayloadRequestData,
|
|
407
|
+
compositionBodies: CompositionBody[],
|
|
408
|
+
userId: string,
|
|
409
|
+
): Promise<FrontendPayload<CompositionType[]>> {
|
|
410
|
+
const compositions = await this._createCompositions(
|
|
411
|
+
fastify,
|
|
412
|
+
compositionBodies,
|
|
413
|
+
userId,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
const payload = await createCompositionPayload(
|
|
417
|
+
fastify,
|
|
418
|
+
compositions,
|
|
419
|
+
requestData,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
fastify.emit("compositionsCreated", payload)
|
|
423
|
+
|
|
338
424
|
return payload
|
|
339
425
|
}
|
|
340
426
|
}
|
|
@@ -4,13 +4,17 @@ 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"
|
|
9
|
+
import restore from "./route.composition.restore"
|
|
8
10
|
|
|
9
11
|
const registerRoutes = async (fastify: PluginFastifyInstance) => {
|
|
10
12
|
await fastify.get("/composition/all", getAll(fastify))
|
|
11
13
|
await fastify.get("/composition/:_id", get(fastify))
|
|
12
14
|
await fastify.patch("/composition/:_id", patch(fastify))
|
|
15
|
+
await fastify.patch("/composition/:_id/restore", restore(fastify))
|
|
13
16
|
await fastify.post("/composition", post(fastify))
|
|
17
|
+
await fastify.post("/composition/bulk", postBulk(fastify))
|
|
14
18
|
await fastify.delete("/composition/:_id", remove(fastify))
|
|
15
19
|
await fastify.delete("/composition/:_id/hard", hardDelete(fastify))
|
|
16
20
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
export default (fastify: PluginFastifyInstance) => {
|
|
7
|
+
const compositionService = fastify.custom.compositionService
|
|
8
|
+
|
|
9
|
+
const handler = async (
|
|
10
|
+
req: FastifyRequest<{
|
|
11
|
+
Body: CompositionBody[]
|
|
12
|
+
}>,
|
|
13
|
+
reply: FastifyReply,
|
|
14
|
+
) => {
|
|
15
|
+
try {
|
|
16
|
+
// Validate that body is an array
|
|
17
|
+
if (!Array.isArray(req.body)) {
|
|
18
|
+
return reply.badRequest("Request body must be an array of compositions")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Validate that array is not empty
|
|
22
|
+
if (req.body.length === 0) {
|
|
23
|
+
return reply.badRequest("Request body cannot be an empty array")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const payload = await compositionService.createCompositions(
|
|
27
|
+
fastify,
|
|
28
|
+
req.requestParams,
|
|
29
|
+
req.body,
|
|
30
|
+
req.user._id,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return payload
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
fastify.log.error(error.message)
|
|
36
|
+
|
|
37
|
+
return reply.internalServerError(error.message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
handler,
|
|
43
|
+
onRequest: [fastify.authenticate],
|
|
44
|
+
config: {
|
|
45
|
+
type: "dataCreate",
|
|
46
|
+
broadcastChannels: ["compositionsCreated"],
|
|
47
|
+
},
|
|
48
|
+
schema: {
|
|
49
|
+
summary: "Create multiple compositions in bulk",
|
|
50
|
+
tags: ["core/composition"],
|
|
51
|
+
body: {
|
|
52
|
+
type: "array",
|
|
53
|
+
items: compositionCreateSchema,
|
|
54
|
+
minItems: 1,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Static } from "@sinclair/typebox"
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
3
|
+
import { PluginFastifyInstance } from "types"
|
|
4
|
+
import { Type } from "@sinclair/typebox"
|
|
5
|
+
import { ForbiddenError } from "@/utils/errors"
|
|
6
|
+
|
|
7
|
+
const ParamsSchema = Type.Object({
|
|
8
|
+
_id: Type.String(),
|
|
9
|
+
})
|
|
10
|
+
type Params = Static<typeof ParamsSchema>
|
|
11
|
+
|
|
12
|
+
export default (fastify: PluginFastifyInstance) => {
|
|
13
|
+
const compositionService = fastify.custom.compositionService
|
|
14
|
+
|
|
15
|
+
const handler = async (
|
|
16
|
+
req: FastifyRequest<{ Params: Params }>,
|
|
17
|
+
reply: FastifyReply,
|
|
18
|
+
) => {
|
|
19
|
+
try {
|
|
20
|
+
if (req.user.isAdmin) {
|
|
21
|
+
const { _id } = req.params
|
|
22
|
+
|
|
23
|
+
const payload = await compositionService.updateComposition(
|
|
24
|
+
fastify,
|
|
25
|
+
req.requestParams,
|
|
26
|
+
_id,
|
|
27
|
+
{ isDeleted: false },
|
|
28
|
+
req.user._id,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return payload
|
|
32
|
+
} else {
|
|
33
|
+
throw new ForbiddenError("Only admins can restore this core datatype")
|
|
34
|
+
}
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
fastify.log.error(error.message)
|
|
37
|
+
return reply.internalServerError(error.message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
handler,
|
|
43
|
+
onRequest: [fastify.authenticate],
|
|
44
|
+
config: {
|
|
45
|
+
type: "dataRestore",
|
|
46
|
+
broadcastChannels: ["compositionRestored"],
|
|
47
|
+
},
|
|
48
|
+
schema: {
|
|
49
|
+
summary: "restore a composition by id",
|
|
50
|
+
description: "Restore a composition",
|
|
51
|
+
tags: ["core/composition"],
|
|
52
|
+
params: ParamsSchema,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -100,6 +100,8 @@ export class InteractionLinkService {
|
|
|
100
100
|
filters: Record<string, any> = {},
|
|
101
101
|
): Promise<InteractionLinkType[]> {
|
|
102
102
|
try {
|
|
103
|
+
if (filters.isDeleted !== true && filters.isDeleted !== "true")
|
|
104
|
+
filters.isDeleted = false
|
|
103
105
|
return await this.interactionLinkModel
|
|
104
106
|
.find({
|
|
105
107
|
...filters,
|
|
@@ -128,16 +130,16 @@ export class InteractionLinkService {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
/**
|
|
131
|
-
* Core function to get interaction links by
|
|
133
|
+
* Core function to get interaction links by interactionLink ID (returns raw data)
|
|
132
134
|
*/
|
|
133
|
-
async
|
|
135
|
+
async _readInteractionLinksByInteractionLink(
|
|
134
136
|
fastify: PluginFastifyInstance,
|
|
135
|
-
|
|
137
|
+
interactionLinkId: string,
|
|
136
138
|
): Promise<InteractionLinkType[]> {
|
|
137
139
|
try {
|
|
138
140
|
return await this.interactionLinkModel
|
|
139
141
|
.find({
|
|
140
|
-
|
|
142
|
+
interactionLink: interactionLinkId,
|
|
141
143
|
isDeleted: false,
|
|
142
144
|
})
|
|
143
145
|
.lean()
|
|
@@ -148,16 +150,16 @@ export class InteractionLinkService {
|
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
151
|
-
* Get interaction links by
|
|
153
|
+
* Get interaction links by interactionLink ID with payload wrapping
|
|
152
154
|
*/
|
|
153
|
-
async
|
|
155
|
+
async readInteractionLinksByInteractionLink(
|
|
154
156
|
fastify: PluginFastifyInstance,
|
|
155
157
|
requestData: FrontendPayloadRequestData,
|
|
156
|
-
|
|
158
|
+
interactionLinkId: string,
|
|
157
159
|
): Promise<FrontendPayload<InteractionLinkType[]>> {
|
|
158
|
-
const interactionLinks = await this.
|
|
160
|
+
const interactionLinks = await this._readInteractionLinksByInteractionLink(
|
|
159
161
|
fastify,
|
|
160
|
-
|
|
162
|
+
interactionLinkId,
|
|
161
163
|
)
|
|
162
164
|
|
|
163
165
|
return createInteractionLinkPayload(fastify, interactionLinks, requestData)
|
|
@@ -437,6 +439,91 @@ export class InteractionLinkService {
|
|
|
437
439
|
|
|
438
440
|
return createInteractionLinkPayload(fastify, interactionLinks, requestData)
|
|
439
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Core function to create multiple interactionLinks (returns raw data)
|
|
444
|
+
*/
|
|
445
|
+
async _createInteractionLinks(
|
|
446
|
+
fastify: PluginFastifyInstance,
|
|
447
|
+
interactionLinkBodies: InteractionLinkBody[],
|
|
448
|
+
userId: string,
|
|
449
|
+
): Promise<InteractionLinkType[]> {
|
|
450
|
+
try {
|
|
451
|
+
const interactionLinksToCreate: InteractionLinkBody[] = []
|
|
452
|
+
|
|
453
|
+
// Validate and prepare all interactionLinks
|
|
454
|
+
for (const interactionLinkBody of interactionLinkBodies) {
|
|
455
|
+
if (interactionLinkBody._id) {
|
|
456
|
+
const uuidValid = validate(interactionLinkBody._id)
|
|
457
|
+
|
|
458
|
+
if (!uuidValid) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`Invalid ID - not a valid uuid v4: ${interactionLinkBody._id}`,
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const duplicate = await this.interactionLinkModel.findOne({
|
|
465
|
+
_id: interactionLinkBody._id,
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
if (duplicate) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`An entry with this id already exists: ${interactionLinkBody._id}`,
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
interactionLinkBody._id = uuidv4()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
interactionLinksToCreate.push({
|
|
478
|
+
...interactionLinkBody,
|
|
479
|
+
author: userId,
|
|
480
|
+
lastEditor: userId,
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Bulk insert
|
|
485
|
+
const interactionLinks = await this.interactionLinkModel.insertMany(
|
|
486
|
+
interactionLinksToCreate,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
fastify.log.info(
|
|
490
|
+
`[API] Created ${interactionLinks.length} interactionLinks in bulk`,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
return interactionLinks.map((comp) =>
|
|
494
|
+
comp.toObject ? comp.toObject() : comp,
|
|
495
|
+
)
|
|
496
|
+
} catch (error: any) {
|
|
497
|
+
fastify.log.error(error.message)
|
|
498
|
+
throw error
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Create multiple interactionLinks with payload wrapping and event emission
|
|
504
|
+
*/
|
|
505
|
+
async createInteractionLinks(
|
|
506
|
+
fastify: PluginFastifyInstance,
|
|
507
|
+
requestData: FrontendPayloadRequestData,
|
|
508
|
+
interactionLinkBodies: InteractionLinkBody[],
|
|
509
|
+
userId: string,
|
|
510
|
+
): Promise<FrontendPayload<InteractionLinkType[]>> {
|
|
511
|
+
const interactionLinks = await this._createInteractionLinks(
|
|
512
|
+
fastify,
|
|
513
|
+
interactionLinkBodies,
|
|
514
|
+
userId,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
const payload = await createInteractionLinkPayload(
|
|
518
|
+
fastify,
|
|
519
|
+
interactionLinks,
|
|
520
|
+
requestData,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
fastify.emit("interactionLinksCreated", payload)
|
|
524
|
+
|
|
525
|
+
return payload
|
|
526
|
+
}
|
|
440
527
|
}
|
|
441
528
|
|
|
442
529
|
export const createInteractionLinkService = (
|
|
@@ -4,13 +4,17 @@ import getAll from "./route.interactionLink.getAll"
|
|
|
4
4
|
import hardDelete from "./route.interactionLink.hardDelete"
|
|
5
5
|
import patch from "./route.interactionLink.patch"
|
|
6
6
|
import post from "./route.interactionLink.post"
|
|
7
|
+
import postBulk from "./route.interactionLink.post.bulk"
|
|
7
8
|
import remove from "./route.interactionLink.remove"
|
|
9
|
+
import restore from "./route.interactionLink.restore"
|
|
8
10
|
|
|
9
11
|
const registerRoutes = async (fastify: PluginFastifyInstance) => {
|
|
10
12
|
await fastify.get("/interactionLink/all", getAll(fastify))
|
|
11
13
|
await fastify.get("/interactionLink/:_id", get(fastify))
|
|
12
14
|
await fastify.patch("/interactionLink/:_id", patch(fastify))
|
|
15
|
+
await fastify.patch("/interactionLink/:_id/restore", restore(fastify))
|
|
13
16
|
await fastify.post("/interactionLink", post(fastify))
|
|
17
|
+
await fastify.post("/interactionLink/bulk", postBulk(fastify))
|
|
14
18
|
await fastify.delete("/interactionLink/:_id", remove(fastify))
|
|
15
19
|
await fastify.delete("/interactionLink/:_id/hard", hardDelete(fastify))
|
|
16
20
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { InteractionLinkBody } from "../interactionLink.service"
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
3
|
+
import type { PluginFastifyInstance } from "types"
|
|
4
|
+
import { interactionLinkCreateSchema } from "../interactionLink.schema"
|
|
5
|
+
|
|
6
|
+
export default (fastify: PluginFastifyInstance) => {
|
|
7
|
+
const interactionLinkService = fastify.custom.interactionLinkService
|
|
8
|
+
|
|
9
|
+
const handler = async (
|
|
10
|
+
req: FastifyRequest<{
|
|
11
|
+
Body: InteractionLinkBody[]
|
|
12
|
+
}>,
|
|
13
|
+
reply: FastifyReply,
|
|
14
|
+
) => {
|
|
15
|
+
try {
|
|
16
|
+
// Validate that body is an array
|
|
17
|
+
if (!Array.isArray(req.body)) {
|
|
18
|
+
return reply.badRequest(
|
|
19
|
+
"Request body must be an array of interactionLinks",
|
|
20
|
+
)
|
|
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 interactionLinkService.createInteractionLinks(
|
|
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],
|
|
46
|
+
config: {
|
|
47
|
+
type: "dataCreate",
|
|
48
|
+
broadcastChannels: ["interactionLinksCreated"],
|
|
49
|
+
},
|
|
50
|
+
schema: {
|
|
51
|
+
summary: "Create multiple interactionLinks in bulk",
|
|
52
|
+
tags: ["core/interactionLink"],
|
|
53
|
+
body: {
|
|
54
|
+
type: "array",
|
|
55
|
+
items: interactionLinkCreateSchema,
|
|
56
|
+
minItems: 1,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Static } from "@sinclair/typebox"
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
3
|
+
import { PluginFastifyInstance } from "types"
|
|
4
|
+
import { Type } from "@sinclair/typebox"
|
|
5
|
+
import { ForbiddenError } from "@/utils/errors"
|
|
6
|
+
|
|
7
|
+
const ParamsSchema = Type.Object({
|
|
8
|
+
_id: Type.String(),
|
|
9
|
+
})
|
|
10
|
+
type Params = Static<typeof ParamsSchema>
|
|
11
|
+
|
|
12
|
+
export default (fastify: PluginFastifyInstance) => {
|
|
13
|
+
const interactionLinkService = fastify.custom.interactionLinkService
|
|
14
|
+
|
|
15
|
+
const handler = async (
|
|
16
|
+
req: FastifyRequest<{ Params: Params }>,
|
|
17
|
+
reply: FastifyReply,
|
|
18
|
+
) => {
|
|
19
|
+
try {
|
|
20
|
+
if (req.user.isAdmin) {
|
|
21
|
+
const { _id } = req.params
|
|
22
|
+
|
|
23
|
+
const payload = await interactionLinkService.updateInteractionLink(
|
|
24
|
+
fastify,
|
|
25
|
+
req.requestParams,
|
|
26
|
+
_id,
|
|
27
|
+
{ isDeleted: false },
|
|
28
|
+
req.user._id,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return payload
|
|
32
|
+
} else {
|
|
33
|
+
throw new ForbiddenError("Only admins can restore this core datatype")
|
|
34
|
+
}
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
fastify.log.error(error.message)
|
|
37
|
+
return reply.internalServerError(error.message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
handler,
|
|
43
|
+
onRequest: [fastify.authenticate],
|
|
44
|
+
config: {
|
|
45
|
+
type: "dataRestore",
|
|
46
|
+
broadcastChannels: ["interactionLinkRestored"],
|
|
47
|
+
},
|
|
48
|
+
schema: {
|
|
49
|
+
summary: "restore a interactionLink by id",
|
|
50
|
+
description: "Restore a interactionLink",
|
|
51
|
+
tags: ["core/interactionLink"],
|
|
52
|
+
params: ParamsSchema,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.model.ts
CHANGED
|
@@ -34,6 +34,23 @@ const projectSchema = new Schema(
|
|
|
34
34
|
|
|
35
35
|
// soft delete
|
|
36
36
|
isDeleted: { type: Boolean, default: false },
|
|
37
|
+
config: {
|
|
38
|
+
type: {
|
|
39
|
+
tags: {
|
|
40
|
+
type: [Schema.Types.String],
|
|
41
|
+
ref: "raclette__core-tag",
|
|
42
|
+
default: [],
|
|
43
|
+
},
|
|
44
|
+
railPageNavigation: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
default: () => ({
|
|
50
|
+
tags: [],
|
|
51
|
+
railPageNavigation: false,
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
37
54
|
},
|
|
38
55
|
{ timestamps: true },
|
|
39
56
|
)
|
package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.schema.ts
CHANGED
|
@@ -2,16 +2,28 @@ import type { Static } from "@sinclair/typebox"
|
|
|
2
2
|
import type { Document } from "mongoose"
|
|
3
3
|
import { Type } from "@sinclair/typebox"
|
|
4
4
|
import { PluginFastifyInstance } from "types"
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* Base Project Schema - Fields common to all operations
|
|
8
7
|
*/
|
|
8
|
+
const projectConfigSchema = Type.Optional(
|
|
9
|
+
Type.Object(
|
|
10
|
+
{
|
|
11
|
+
tags: Type.Array(Type.String(), { default: [] }),
|
|
12
|
+
railPageNavigation: Type.Boolean({ default: false }),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
$id: "#project/config",
|
|
16
|
+
title: "core/project-config",
|
|
17
|
+
},
|
|
18
|
+
),
|
|
19
|
+
)
|
|
9
20
|
const baseProjectSchema = {
|
|
10
21
|
title: Type.Optional(Type.String({ maxLength: 200 })),
|
|
11
22
|
account: Type.Optional(Type.String({ default: null })),
|
|
12
23
|
tags: Type.Optional(Type.Array(Type.String(), { default: [] })),
|
|
13
24
|
description: Type.Optional(Type.String()),
|
|
14
25
|
isDefault: Type.Optional(Type.Boolean({ default: false })),
|
|
26
|
+
config: projectConfigSchema,
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
/**
|
|
@@ -51,11 +63,7 @@ export const projectCreateSchema = Type.Object(
|
|
|
51
63
|
export const projectUpdateSchema = Type.Object(
|
|
52
64
|
{
|
|
53
65
|
_id: Type.Optional(Type.String()),
|
|
54
|
-
|
|
55
|
-
account: Type.Optional(Type.String()),
|
|
56
|
-
tags: Type.Optional(Type.Array(Type.String())),
|
|
57
|
-
description: Type.Optional(Type.String()),
|
|
58
|
-
isDefault: Type.Optional(Type.Boolean()),
|
|
66
|
+
...baseProjectSchema,
|
|
59
67
|
},
|
|
60
68
|
{
|
|
61
69
|
$id: "#project/update",
|
|
@@ -104,4 +112,8 @@ export const registerSchemas = (fastify: PluginFastifyInstance) => {
|
|
|
104
112
|
schema: projectUpdateSchema,
|
|
105
113
|
name: "ProjectUpdate",
|
|
106
114
|
})
|
|
115
|
+
fastify.registerSchema({
|
|
116
|
+
schema: projectConfigSchema,
|
|
117
|
+
name: "ProjectConfig",
|
|
118
|
+
})
|
|
107
119
|
}
|