@raclettejs/core 0.1.18 → 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 +6 -0
- package/package.json +2 -2
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +84 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/index.ts +2 -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/interactionLink/interactionLink.service.ts +94 -9
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.ts +2 -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/tag/routes/index.ts +2 -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/tag.service.ts +72 -1
- package/services/frontend/src/core/lib/data/dataApi.ts +10 -1
- package/services/frontend/src/core/lib/eggs/snow.ts +32 -43
- package/services/frontend/src/orchestrator/i18n/de-DE.json +14 -0
- package/services/frontend/src/orchestrator/i18n/en-EU.json +14 -0
- package/services/frontend/src/orchestrator/i18n/sk.json +14 -0
- package/yarn.lock +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ 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
|
+
|
|
10
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>
|
|
11
17
|
|
|
12
18
|
### Changed
|
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",
|
|
@@ -337,6 +337,90 @@ export class CompositionService {
|
|
|
337
337
|
payload.body.deleted = [composition._id]
|
|
338
338
|
fastify.emit("compositionHardDeleted", payload)
|
|
339
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
|
+
|
|
340
424
|
return payload
|
|
341
425
|
}
|
|
342
426
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -130,16 +130,16 @@ export class InteractionLinkService {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* Core function to get interaction links by
|
|
133
|
+
* Core function to get interaction links by interactionLink ID (returns raw data)
|
|
134
134
|
*/
|
|
135
|
-
async
|
|
135
|
+
async _readInteractionLinksByInteractionLink(
|
|
136
136
|
fastify: PluginFastifyInstance,
|
|
137
|
-
|
|
137
|
+
interactionLinkId: string,
|
|
138
138
|
): Promise<InteractionLinkType[]> {
|
|
139
139
|
try {
|
|
140
140
|
return await this.interactionLinkModel
|
|
141
141
|
.find({
|
|
142
|
-
|
|
142
|
+
interactionLink: interactionLinkId,
|
|
143
143
|
isDeleted: false,
|
|
144
144
|
})
|
|
145
145
|
.lean()
|
|
@@ -150,16 +150,16 @@ export class InteractionLinkService {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* Get interaction links by
|
|
153
|
+
* Get interaction links by interactionLink ID with payload wrapping
|
|
154
154
|
*/
|
|
155
|
-
async
|
|
155
|
+
async readInteractionLinksByInteractionLink(
|
|
156
156
|
fastify: PluginFastifyInstance,
|
|
157
157
|
requestData: FrontendPayloadRequestData,
|
|
158
|
-
|
|
158
|
+
interactionLinkId: string,
|
|
159
159
|
): Promise<FrontendPayload<InteractionLinkType[]>> {
|
|
160
|
-
const interactionLinks = await this.
|
|
160
|
+
const interactionLinks = await this._readInteractionLinksByInteractionLink(
|
|
161
161
|
fastify,
|
|
162
|
-
|
|
162
|
+
interactionLinkId,
|
|
163
163
|
)
|
|
164
164
|
|
|
165
165
|
return createInteractionLinkPayload(fastify, interactionLinks, requestData)
|
|
@@ -439,6 +439,91 @@ export class InteractionLinkService {
|
|
|
439
439
|
|
|
440
440
|
return createInteractionLinkPayload(fastify, interactionLinks, requestData)
|
|
441
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
|
+
}
|
|
442
527
|
}
|
|
443
528
|
|
|
444
529
|
export const createInteractionLinkService = (
|
|
@@ -4,6 +4,7 @@ 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"
|
|
8
9
|
import restore from "./route.interactionLink.restore"
|
|
9
10
|
|
|
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
|
|
|
13
14
|
await fastify.patch("/interactionLink/:_id", patch(fastify))
|
|
14
15
|
await fastify.patch("/interactionLink/:_id/restore", restore(fastify))
|
|
15
16
|
await fastify.post("/interactionLink", post(fastify))
|
|
17
|
+
await fastify.post("/interactionLink/bulk", postBulk(fastify))
|
|
16
18
|
await fastify.delete("/interactionLink/:_id", remove(fastify))
|
|
17
19
|
await fastify.delete("/interactionLink/:_id/hard", hardDelete(fastify))
|
|
18
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
|
+
}
|
package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import getAll from "./route.tag.getAll"
|
|
|
4
4
|
import hardDelete from "./route.tag.hardDelete"
|
|
5
5
|
import patch from "./route.tag.patch"
|
|
6
6
|
import post from "./route.tag.post"
|
|
7
|
+
import postBulk from "./route.tag.post.bulk"
|
|
7
8
|
import remove from "./route.tag.remove"
|
|
8
9
|
import restore from "./route.tag.restore"
|
|
9
10
|
|
|
@@ -13,6 +14,7 @@ const registerRoutes = async (fastify: PluginFastifyInstance) => {
|
|
|
13
14
|
await fastify.patch("/tag/:_id", patch(fastify))
|
|
14
15
|
await fastify.patch("/tag/:_id/restore", restore(fastify))
|
|
15
16
|
await fastify.post("/tag/:project/:_id", post(fastify))
|
|
17
|
+
await fastify.post("/tag/bulk", postBulk(fastify))
|
|
16
18
|
await fastify.delete("/tag/:_id", remove(fastify))
|
|
17
19
|
await fastify.delete("/tag/:_id/hard", hardDelete(fastify))
|
|
18
20
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { TagBody } from "../tag.service"
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify"
|
|
3
|
+
import type { PluginFastifyInstance } from "types"
|
|
4
|
+
import { tagCreateSchema } from "../tag.schema"
|
|
5
|
+
|
|
6
|
+
export default (fastify: PluginFastifyInstance) => {
|
|
7
|
+
const tagService = fastify.custom.tagService
|
|
8
|
+
|
|
9
|
+
const handler = async (
|
|
10
|
+
req: FastifyRequest<{
|
|
11
|
+
Body: TagBody[]
|
|
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 tags")
|
|
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 tagService.createTags(
|
|
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: ["tagsCreated"],
|
|
47
|
+
},
|
|
48
|
+
schema: {
|
|
49
|
+
summary: "Create multiple tags in bulk",
|
|
50
|
+
tags: ["core/tag"],
|
|
51
|
+
body: {
|
|
52
|
+
type: "array",
|
|
53
|
+
items: tagCreateSchema,
|
|
54
|
+
minItems: 1,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts
CHANGED
|
@@ -8,7 +8,9 @@ import type { PluginFastifyInstance } from "types"
|
|
|
8
8
|
import { v4 as uuidv4, validate } from "uuid"
|
|
9
9
|
import { createTagPayload } from "./helpers/payload"
|
|
10
10
|
import type { Model } from "mongoose"
|
|
11
|
-
|
|
11
|
+
export type TagBody = Partial<Omit<TagType, "_id">> & {
|
|
12
|
+
_id?: string
|
|
13
|
+
}
|
|
12
14
|
export class TagService {
|
|
13
15
|
private tagModel: Model<TagType>
|
|
14
16
|
|
|
@@ -369,6 +371,75 @@ export class TagService {
|
|
|
369
371
|
|
|
370
372
|
fastify.emit("tagHardDeleted", payload)
|
|
371
373
|
|
|
374
|
+
return payload
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Core function to create multiple tags (returns raw data)
|
|
378
|
+
*/
|
|
379
|
+
async _createTags(
|
|
380
|
+
fastify: PluginFastifyInstance,
|
|
381
|
+
tagBodies: TagBody[],
|
|
382
|
+
userId: string,
|
|
383
|
+
): Promise<TagType[]> {
|
|
384
|
+
try {
|
|
385
|
+
const tagsToCreate: TagBody[] = []
|
|
386
|
+
|
|
387
|
+
// Validate and prepare all tags
|
|
388
|
+
for (const tagBody of tagBodies) {
|
|
389
|
+
if (tagBody._id) {
|
|
390
|
+
const uuidValid = validate(tagBody._id)
|
|
391
|
+
|
|
392
|
+
if (!uuidValid) {
|
|
393
|
+
throw new Error(`Invalid ID - not a valid uuid v4: ${tagBody._id}`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const duplicate = await this.tagModel.findOne({
|
|
397
|
+
_id: tagBody._id,
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
if (duplicate) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`An entry with this id already exists: ${tagBody._id}`,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
tagBody._id = uuidv4()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
tagsToCreate.push({
|
|
410
|
+
...tagBody,
|
|
411
|
+
author: userId,
|
|
412
|
+
lastEditor: userId,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Bulk insert
|
|
417
|
+
const tags = await this.tagModel.insertMany(tagsToCreate)
|
|
418
|
+
|
|
419
|
+
fastify.log.info(`[API] Created ${tags.length} tags in bulk`)
|
|
420
|
+
|
|
421
|
+
return tags.map((comp) => (comp.toObject ? comp.toObject() : comp))
|
|
422
|
+
} catch (error: any) {
|
|
423
|
+
fastify.log.error(error.message)
|
|
424
|
+
throw error
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Create multiple tags with payload wrapping and event emission
|
|
430
|
+
*/
|
|
431
|
+
async createTags(
|
|
432
|
+
fastify: PluginFastifyInstance,
|
|
433
|
+
requestData: FrontendPayloadRequestData,
|
|
434
|
+
tagBodies: TagBody[],
|
|
435
|
+
userId: string,
|
|
436
|
+
): Promise<FrontendPayload<TagType[]>> {
|
|
437
|
+
const tags = await this._createTags(fastify, tagBodies, userId)
|
|
438
|
+
|
|
439
|
+
const payload = await createTagPayload(fastify, tags, requestData)
|
|
440
|
+
|
|
441
|
+
fastify.emit("tagsCreated", payload)
|
|
442
|
+
|
|
372
443
|
return payload
|
|
373
444
|
}
|
|
374
445
|
}
|
|
@@ -29,7 +29,16 @@ const $writeDataApi = (
|
|
|
29
29
|
const _executeParams = clone(executeParams)
|
|
30
30
|
errorSubject$.next(null)
|
|
31
31
|
responseSubject$.next({})
|
|
32
|
-
|
|
32
|
+
let queryParams
|
|
33
|
+
if (Array.isArray(_executeParams)) {
|
|
34
|
+
if (Array.isArray(_params)) {
|
|
35
|
+
queryParams = _params.concat(_executeParams)
|
|
36
|
+
}
|
|
37
|
+
queryParams = _executeParams
|
|
38
|
+
} else {
|
|
39
|
+
queryParams = mergeDeepRight(_params, _executeParams)
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
if (options.responseType === "stream") {
|
|
34
43
|
return writeData(actionId, operationDefinition, queryParams, options)
|
|
35
44
|
}
|
|
@@ -7,11 +7,13 @@ export default () => {
|
|
|
7
7
|
|
|
8
8
|
// This function allows you to turn on and off the snow
|
|
9
9
|
const toggle_snow = () => {
|
|
10
|
-
const check_box = document.getElementById(
|
|
10
|
+
const check_box = document.getElementById(
|
|
11
|
+
"toggle_snow",
|
|
12
|
+
) as HTMLInputElement | null
|
|
11
13
|
const snowEl = document.getElementById("snow")
|
|
12
14
|
|
|
13
15
|
if (snowEl) {
|
|
14
|
-
snowEl.style.display = check_box
|
|
16
|
+
snowEl.style.display = check_box?.checked ? "block" : "none"
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -29,87 +31,74 @@ export default () => {
|
|
|
29
31
|
const randomDuration = 8 + Math.random() * 15 // Duration between 8-23s
|
|
30
32
|
const randomDelay = Math.random() * 10 // Delay between 0-10s
|
|
31
33
|
const randomOpacity = 0.3 + Math.random() * 0.7 // Opacity between 0.3-1
|
|
32
|
-
const randomAnimation = Math.floor(Math.random() *
|
|
34
|
+
const randomAnimation = Math.floor(Math.random() * 3) + 1 // Choose from 3 animations
|
|
33
35
|
|
|
34
|
-
snowflake.style.setProperty("--start-x", `${randomX}
|
|
35
|
-
snowflake.style.setProperty("--scale", randomScale)
|
|
36
|
+
snowflake.style.setProperty("--start-x", `${randomX}%`)
|
|
37
|
+
snowflake.style.setProperty("--scale", randomScale.toString())
|
|
36
38
|
snowflake.style.setProperty("--duration", `${randomDuration}s`)
|
|
37
39
|
snowflake.style.setProperty("--delay", `${randomDelay}s`)
|
|
38
|
-
snowflake.style.setProperty("--opacity", randomOpacity)
|
|
40
|
+
snowflake.style.setProperty("--opacity", randomOpacity.toString())
|
|
39
41
|
snowflake.style.setProperty("--animation-name", `fall-${randomAnimation}`)
|
|
40
42
|
|
|
41
43
|
fragment.appendChild(snowflake)
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
document.getElementById("snow")
|
|
46
|
+
document.getElementById("snow")?.appendChild(fragment)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
// Optimized CSS with
|
|
49
|
+
// Optimized CSS with GPU acceleration and simplified animations
|
|
48
50
|
const createOptimizedCSS = () => {
|
|
49
51
|
const css = `
|
|
50
52
|
#snow {
|
|
51
53
|
position: fixed;
|
|
52
54
|
top: 0;
|
|
53
55
|
left: 0;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
right: 0;
|
|
57
|
+
bottom: 0;
|
|
56
58
|
pointer-events: none;
|
|
57
59
|
z-index: 1000;
|
|
60
|
+
overflow: hidden;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
.snowflake {
|
|
61
64
|
position: absolute;
|
|
65
|
+
top: -10px;
|
|
62
66
|
width: 8px;
|
|
63
67
|
height: 8px;
|
|
64
68
|
background: white;
|
|
65
69
|
border-radius: 50%;
|
|
66
|
-
|
|
70
|
+
|
|
71
|
+
/* GPU acceleration - critical for performance */
|
|
67
72
|
will-change: transform;
|
|
73
|
+
transform: translateZ(0);
|
|
74
|
+
backface-visibility: hidden;
|
|
75
|
+
|
|
76
|
+
/* Simplified glow effect - much lighter than drop-shadow filter */
|
|
77
|
+
box-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
|
|
68
78
|
|
|
69
79
|
/* Use CSS custom properties for individual variations */
|
|
70
80
|
left: var(--start-x);
|
|
71
81
|
opacity: var(--opacity);
|
|
72
|
-
transform: scale(var(--scale));
|
|
73
82
|
animation: var(--animation-name) var(--duration) var(--delay) linear infinite;
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
/*
|
|
85
|
+
/* Simplified to 3 falling patterns - uses only transform for best performance */
|
|
77
86
|
@keyframes fall-1 {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
75% { transform: translateY(75vh) translateX(8px) scale(var(--scale)); }
|
|
82
|
-
100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
|
|
87
|
+
to {
|
|
88
|
+
transform: translateY(100vh) translateX(20px) scale(var(--scale)) translateZ(0);
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
@keyframes fall-2 {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
100% { transform: translateY(100vh) translateX(-5px) scale(var(--scale)); }
|
|
93
|
+
to {
|
|
94
|
+
transform: translateY(100vh) translateX(-20px) scale(var(--scale)) translateZ(0);
|
|
95
|
+
}
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
@keyframes fall-3 {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
70% { transform: translateY(70vh) translateX(15px) scale(var(--scale)); }
|
|
97
|
-
100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
@keyframes fall-4 {
|
|
101
|
-
0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
|
|
102
|
-
35% { transform: translateY(35vh) translateX(20px) scale(var(--scale)); }
|
|
103
|
-
65% { transform: translateY(65vh) translateX(-8px) scale(var(--scale)); }
|
|
104
|
-
100% { transform: translateY(100vh) translateX(12px) scale(var(--scale)); }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@keyframes fall-5 {
|
|
108
|
-
0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
|
|
109
|
-
15% { transform: translateY(15vh) translateX(-5px) scale(var(--scale)); }
|
|
110
|
-
45% { transform: translateY(45vh) translateX(12px) scale(var(--scale)); }
|
|
111
|
-
80% { transform: translateY(80vh) translateX(-7px) scale(var(--scale)); }
|
|
112
|
-
100% { transform: translateY(100vh) translateX(3px) scale(var(--scale)); }
|
|
99
|
+
to {
|
|
100
|
+
transform: translateY(100vh) translateX(0px) scale(var(--scale)) translateZ(0);
|
|
101
|
+
}
|
|
113
102
|
}
|
|
114
103
|
`
|
|
115
104
|
|
|
@@ -142,7 +131,7 @@ export default () => {
|
|
|
142
131
|
} else {
|
|
143
132
|
// Remove existing snow
|
|
144
133
|
snowEl.remove()
|
|
145
|
-
//
|
|
134
|
+
// Remove styles as well
|
|
146
135
|
const styles = document.getElementById("snow-styles")
|
|
147
136
|
if (styles) {
|
|
148
137
|
styles.remove()
|
|
@@ -75,8 +75,22 @@
|
|
|
75
75
|
"openInFloatingWindow": "In extrafenster öffnen",
|
|
76
76
|
"showDeleted": "Zeige gelöschte",
|
|
77
77
|
"hideDeleted": "Verstecke gelöschte",
|
|
78
|
+
"selectAll": "Alle auswählen",
|
|
79
|
+
"selected": "ausgewählt",
|
|
80
|
+
"noItemsSelected": "Keine Elemente ausgewählt. Bitte wählen Sie mindestens ein Element aus, um fortzufahren.",
|
|
78
81
|
"cancel": "Abbrechen",
|
|
79
82
|
"close": "Schließen",
|
|
83
|
+
"itemsCount": "Anzahl der Elemente",
|
|
84
|
+
"selectedItems": "Ausgewählte Elemente",
|
|
85
|
+
"selectItemsToExport": "Elemente zum Exportieren auswählen",
|
|
86
|
+
"export": "Exportieren",
|
|
87
|
+
"selectItemsToImport": "Elemente zum Importieren auswählen",
|
|
88
|
+
"import": "Importieren",
|
|
89
|
+
"exportSummary": "Export-Zusammenfassung",
|
|
90
|
+
"exportSuccessMessage": "Elemente wurden erfolgreich exportiert",
|
|
91
|
+
"importSummary": "Import-Zusammenfassung",
|
|
92
|
+
"importSuccessMessage": "Elemente bereit zum Importieren",
|
|
93
|
+
"saveImported": "Importierte Elemente speichern",
|
|
80
94
|
"clear": "Leeren",
|
|
81
95
|
"copyToClipboard$name": "{name} in Zwischenablage kopieren",
|
|
82
96
|
"timeAgo": "vor {time}",
|
|
@@ -75,8 +75,22 @@
|
|
|
75
75
|
"openInFloatingWindow": "Open in floating window",
|
|
76
76
|
"showDeleted": "Show deleted",
|
|
77
77
|
"hideDeleted": "Hide deleted",
|
|
78
|
+
"selectAll": "Select All",
|
|
79
|
+
"selected": "selected",
|
|
80
|
+
"noItemsSelected": "No items selected. Please select at least one item to continue.",
|
|
78
81
|
"cancel": "Cancel",
|
|
79
82
|
"close": "Close",
|
|
83
|
+
"itemsCount": "Items count",
|
|
84
|
+
"selectedItems": "Selected items",
|
|
85
|
+
"selectItemsToExport": "Select Items to Export",
|
|
86
|
+
"export": "Export",
|
|
87
|
+
"selectItemsToImport": "Select Items to Import",
|
|
88
|
+
"import": "Import",
|
|
89
|
+
"exportSummary": "Export Summary",
|
|
90
|
+
"exportSuccessMessage": "Items have been successfully exported",
|
|
91
|
+
"importSummary": "Import Summary",
|
|
92
|
+
"importSuccessMessage": "Items ready to import",
|
|
93
|
+
"saveImported": "Save Imported Items",
|
|
80
94
|
"clear": "Clear",
|
|
81
95
|
"copyToClipboard$name": "Copy {name} to clipboard",
|
|
82
96
|
"timeAgo": "{time} ago",
|
|
@@ -75,8 +75,22 @@
|
|
|
75
75
|
"openInFloatingWindow": "Otvoriť v plávajúcom okne",
|
|
76
76
|
"showDeleted": "relácia vymazaná",
|
|
77
77
|
"hideDeleted": "skryť odstránené",
|
|
78
|
+
"selectAll": "Vybrať všetko",
|
|
79
|
+
"selected": "vybrané",
|
|
80
|
+
"noItemsSelected": "Nie sú vybrané žiadne položky. Vyberte aspoň jednu položku pre pokračovanie.",
|
|
78
81
|
"cancel": "Zrušiť",
|
|
79
82
|
"close": "Zavrieť",
|
|
83
|
+
"itemsCount": "Počet položiek",
|
|
84
|
+
"selectedItems": "Vybrané položky",
|
|
85
|
+
"selectItemsToExport": "Vyberte položky na export",
|
|
86
|
+
"export": "Exportovať",
|
|
87
|
+
"selectItemsToImport": "Vyberte položky na import",
|
|
88
|
+
"import": "Importovať",
|
|
89
|
+
"exportSummary": "Súhrn exportu",
|
|
90
|
+
"exportSuccessMessage": "Položky boli úspešne exportované",
|
|
91
|
+
"importSummary": "Súhrn importu",
|
|
92
|
+
"importSuccessMessage": "Položky pripravené na import",
|
|
93
|
+
"saveImported": "Uložiť importované položky",
|
|
80
94
|
"clear": "Vyčistiť",
|
|
81
95
|
"copyToClipboard$name": "Skopírovať {name} do schránky",
|
|
82
96
|
"timeAgo": "pred {time}",
|
package/yarn.lock
CHANGED
|
@@ -295,10 +295,10 @@
|
|
|
295
295
|
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b"
|
|
296
296
|
integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==
|
|
297
297
|
|
|
298
|
-
"@raclettejs/types@^0.1.
|
|
299
|
-
version "0.1.
|
|
300
|
-
resolved "https://registry.yarnpkg.com/@raclettejs/types/-/types-0.1.
|
|
301
|
-
integrity sha512-
|
|
298
|
+
"@raclettejs/types@^0.1.19":
|
|
299
|
+
version "0.1.19"
|
|
300
|
+
resolved "https://registry.yarnpkg.com/@raclettejs/types/-/types-0.1.19.tgz#5f17afcc3b8f2d4849c828648711c426fdb2f74d"
|
|
301
|
+
integrity sha512-EdIMglaM+ivsvkVaXZKVKBoyXsLivulloveYPEV70Lm1iLeja6l7IywyM82dMdEgNMmpEMlG8r2TjHyXu9MPkg==
|
|
302
302
|
dependencies:
|
|
303
303
|
"@types/node" "24.5.2"
|
|
304
304
|
fastify "5.6.0"
|