@elek-io/core 0.16.2 → 0.17.0

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.
@@ -18,6 +18,7 @@ import PQueue from "p-queue";
18
18
  import { createLogger, format, transports } from "winston";
19
19
  import DailyRotateFile from "winston-daily-rotate-file";
20
20
  import Semver from "semver";
21
+ import { isDeepStrictEqual } from "node:util";
21
22
 
22
23
  //#region \0rolldown/runtime.js
23
24
  var __defProp = Object.defineProperty;
@@ -39,7 +40,7 @@ var __exportAll = (all, no_symbols) => {
39
40
  //#region package.json
40
41
  var package_default = {
41
42
  name: "@elek-io/core",
42
- version: "0.16.2",
43
+ version: "0.17.0",
43
44
  description: "Handles core functionality of elek.io Projects like file IO and version control.",
44
45
  homepage: "https://elek.io",
45
46
  repository: "https://github.com/elek-io/core",
@@ -234,11 +235,7 @@ const supportedLanguageSchema = z.enum([
234
235
  "sv",
235
236
  "zh"
236
237
  ]);
237
- const supportedIconSchema = z.enum([
238
- "home",
239
- "plus",
240
- "foobar"
241
- ]);
238
+ const supportedIconSchema = z.enum(["home", "plus"]);
242
239
  const objectTypeSchema = z.enum([
243
240
  "project",
244
241
  "asset",
@@ -253,7 +250,7 @@ const logLevelSchema = z.enum([
253
250
  "info",
254
251
  "debug"
255
252
  ]);
256
- const versionSchema = z.string();
253
+ const versionSchema = z.string().refine((version) => /^\d+\.\d+\.\d+(?:-[\w.]+)?(?:\+[\w.]+)?$/.test(version), "String must follow the Semantic Versioning format (https://semver.org/)");
257
254
  const uuidSchema = z.uuid();
258
255
  /**
259
256
  * A record that can be used to translate a string value into all supported languages
@@ -270,6 +267,41 @@ const translatableBooleanSchema = z.partialRecord(supportedLanguageSchema, z.boo
270
267
  function translatableArrayOf(schema) {
271
268
  return z.partialRecord(supportedLanguageSchema, z.array(schema));
272
269
  }
270
+ const reservedSlugs = new Set([
271
+ "index",
272
+ "new",
273
+ "create",
274
+ "update",
275
+ "delete",
276
+ "edit",
277
+ "list",
278
+ "count",
279
+ "api",
280
+ "admin",
281
+ "collection",
282
+ "collections",
283
+ "entry",
284
+ "entries",
285
+ "asset",
286
+ "assets",
287
+ "project",
288
+ "projects",
289
+ "null",
290
+ "undefined",
291
+ "true",
292
+ "false",
293
+ "constructor",
294
+ "__proto__",
295
+ "prototype",
296
+ "toString",
297
+ "valueOf",
298
+ "login",
299
+ "logout",
300
+ "auth",
301
+ "settings",
302
+ "config"
303
+ ]);
304
+ const slugSchema = z.string().min(1).max(128).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).refine((slug) => !reservedSlugs.has(slug), { message: "This slug is reserved and cannot be used" });
273
305
 
274
306
  //#endregion
275
307
  //#region src/schema/fileSchema.ts
@@ -279,91 +311,36 @@ function translatableArrayOf(schema) {
279
311
  const baseFileSchema = z.object({
280
312
  objectType: objectTypeSchema.readonly(),
281
313
  id: uuidSchema.readonly(),
314
+ coreVersion: versionSchema.readonly(),
282
315
  created: z.string().datetime().readonly(),
283
- updated: z.string().datetime().nullable()
316
+ updated: z.string().datetime().nullable().readonly()
284
317
  });
285
318
  const fileReferenceSchema = z.object({
286
319
  id: uuidSchema,
287
320
  extension: z.string().optional()
288
321
  });
289
-
290
- //#endregion
291
- //#region src/schema/gitSchema.ts
292
322
  /**
293
- * Signature git uses to identify users
323
+ * Schema for the collection index file (collections/index.json).
324
+ * Maps collection UUIDs to their slug.plural values.
325
+ * This is a local performance cache, not git-tracked.
294
326
  */
295
- const gitSignatureSchema = z.object({
296
- name: z.string(),
297
- email: z.string().email()
298
- });
299
- const gitMessageSchema = z.object({
300
- method: z.enum([
301
- "create",
302
- "update",
303
- "delete",
304
- "upgrade"
305
- ]),
306
- reference: z.object({
307
- objectType: objectTypeSchema,
308
- id: uuidSchema,
309
- collectionId: uuidSchema.optional()
310
- })
311
- });
312
- const gitTagSchema = z.object({
313
- id: uuidSchema,
314
- message: z.string(),
315
- author: gitSignatureSchema,
316
- datetime: z.string().datetime()
317
- });
318
- const gitCommitSchema = z.object({
319
- hash: z.string(),
320
- message: gitMessageSchema,
321
- author: gitSignatureSchema,
322
- datetime: z.string().datetime(),
323
- tag: gitTagSchema.nullable()
324
- });
325
- const gitInitOptionsSchema = z.object({ initialBranch: z.string() });
326
- const gitCloneOptionsSchema = z.object({
327
- depth: z.number(),
328
- singleBranch: z.boolean(),
329
- branch: z.string(),
330
- bare: z.boolean()
331
- });
332
- const gitMergeOptionsSchema = z.object({ squash: z.boolean() });
333
- const gitSwitchOptionsSchema = z.object({ isNew: z.boolean().optional() });
334
- const gitLogOptionsSchema = z.object({
335
- limit: z.number().optional(),
336
- between: z.object({
337
- from: z.string(),
338
- to: z.string().optional()
339
- }),
340
- filePath: z.string().optional()
341
- });
342
- const createGitTagSchema = gitTagSchema.pick({ message: true }).extend({
343
- path: z.string(),
344
- hash: z.string().optional()
345
- });
346
- const readGitTagSchema = z.object({
347
- path: z.string(),
348
- id: uuidSchema.readonly()
349
- });
350
- const deleteGitTagSchema = readGitTagSchema.extend({});
351
- const countGitTagsSchema = z.object({ path: z.string() });
327
+ const collectionIndexSchema = z.record(uuidSchema, z.string());
352
328
 
353
329
  //#endregion
354
330
  //#region src/schema/assetSchema.ts
355
331
  const assetFileSchema = baseFileSchema.extend({
356
332
  objectType: z.literal(objectTypeSchema.enum.asset).readonly(),
357
- name: z.string(),
358
- description: z.string(),
333
+ name: z.string().trim().min(1),
334
+ description: z.string().trim().min(1),
359
335
  extension: z.string().readonly(),
360
336
  mimeType: z.string().readonly(),
361
337
  size: z.number().readonly()
362
338
  });
363
- const assetSchema = assetFileSchema.extend({
364
- absolutePath: z.string().readonly(),
365
- history: z.array(gitCommitSchema)
366
- }).openapi("Asset");
339
+ const assetSchema = assetFileSchema.extend({ absolutePath: z.string().readonly() }).openapi("Asset");
340
+ const assetHistorySchema = z.object({
341
+ id: uuidSchema.readonly(),
342
+ projectId: uuidSchema.readonly()
343
+ });
367
344
  const assetExportSchema = assetSchema.extend({});
368
345
  const createAssetSchema = assetFileSchema.pick({
369
346
  name: true,
@@ -393,11 +370,15 @@ const deleteAssetSchema = assetFileSchema.pick({
393
370
  id: true,
394
371
  extension: true
395
372
  }).extend({ projectId: uuidSchema.readonly() });
373
+ const migrateAssetSchema = z.looseObject(assetFileSchema.pick({
374
+ id: true,
375
+ coreVersion: true
376
+ }).shape);
396
377
  const countAssetsSchema = z.object({ projectId: uuidSchema.readonly() });
397
378
 
398
379
  //#endregion
399
380
  //#region src/schema/valueSchema.ts
400
- const ValueTypeSchema = z.enum([
381
+ const valueTypeSchema = z.enum([
401
382
  "string",
402
383
  "number",
403
384
  "boolean",
@@ -412,20 +393,17 @@ const valueContentReferenceSchema = z.union([
412
393
  valueContentReferenceToCollectionSchema,
413
394
  valueContentReferenceToEntrySchema
414
395
  ]);
415
- const directValueBaseSchema = z.object({
416
- objectType: z.literal(objectTypeSchema.enum.value).readonly(),
417
- fieldDefinitionId: uuidSchema.readonly()
418
- });
396
+ const directValueBaseSchema = z.object({ objectType: z.literal(objectTypeSchema.enum.value).readonly() });
419
397
  const directStringValueSchema = directValueBaseSchema.extend({
420
- valueType: z.literal(ValueTypeSchema.enum.string).readonly(),
398
+ valueType: z.literal(valueTypeSchema.enum.string).readonly(),
421
399
  content: translatableStringSchema
422
400
  });
423
401
  const directNumberValueSchema = directValueBaseSchema.extend({
424
- valueType: z.literal(ValueTypeSchema.enum.number).readonly(),
402
+ valueType: z.literal(valueTypeSchema.enum.number).readonly(),
425
403
  content: translatableNumberSchema
426
404
  });
427
405
  const directBooleanValueSchema = directValueBaseSchema.extend({
428
- valueType: z.literal(ValueTypeSchema.enum.boolean).readonly(),
406
+ valueType: z.literal(valueTypeSchema.enum.boolean).readonly(),
429
407
  content: translatableBooleanSchema
430
408
  });
431
409
  const directValueSchema = z.union([
@@ -435,8 +413,7 @@ const directValueSchema = z.union([
435
413
  ]);
436
414
  const referencedValueSchema = z.object({
437
415
  objectType: z.literal(objectTypeSchema.enum.value).readonly(),
438
- fieldDefinitionId: uuidSchema.readonly(),
439
- valueType: z.literal(ValueTypeSchema.enum.reference).readonly(),
416
+ valueType: z.literal(valueTypeSchema.enum.reference).readonly(),
440
417
  content: translatableArrayOf(valueContentReferenceSchema)
441
418
  });
442
419
  const valueSchema = z.union([directValueSchema, referencedValueSchema]);
@@ -451,19 +428,25 @@ const valueSchema = z.union([directValueSchema, referencedValueSchema]);
451
428
  //#region src/schema/entrySchema.ts
452
429
  const entryFileSchema = baseFileSchema.extend({
453
430
  objectType: z.literal(objectTypeSchema.enum.entry).readonly(),
454
- values: z.array(valueSchema)
431
+ values: z.record(slugSchema, valueSchema)
432
+ });
433
+ const entrySchema = entryFileSchema.openapi("Entry");
434
+ const entryHistorySchema = z.object({
435
+ id: uuidSchema.readonly(),
436
+ projectId: uuidSchema.readonly(),
437
+ collectionId: uuidSchema.readonly()
455
438
  });
456
- const entrySchema = entryFileSchema.extend({ history: z.array(gitCommitSchema) }).openapi("Entry");
457
439
  const entryExportSchema = entrySchema.extend({});
458
440
  const createEntrySchema = entryFileSchema.omit({
459
441
  id: true,
460
442
  objectType: true,
443
+ coreVersion: true,
461
444
  created: true,
462
445
  updated: true
463
446
  }).extend({
464
447
  projectId: uuidSchema.readonly(),
465
448
  collectionId: uuidSchema.readonly(),
466
- values: z.array(valueSchema)
449
+ values: z.record(slugSchema, valueSchema)
467
450
  });
468
451
  const readEntrySchema = z.object({
469
452
  id: uuidSchema.readonly(),
@@ -473,6 +456,7 @@ const readEntrySchema = z.object({
473
456
  });
474
457
  const updateEntrySchema = entryFileSchema.omit({
475
458
  objectType: true,
459
+ coreVersion: true,
476
460
  created: true,
477
461
  updated: true
478
462
  }).extend({
@@ -480,6 +464,10 @@ const updateEntrySchema = entryFileSchema.omit({
480
464
  collectionId: uuidSchema.readonly()
481
465
  });
482
466
  const deleteEntrySchema = readEntrySchema.extend({});
467
+ const migrateEntrySchema = z.looseObject(entryFileSchema.pick({
468
+ id: true,
469
+ coreVersion: true
470
+ }).shape);
483
471
  const countEntriesSchema = z.object({
484
472
  projectId: uuidSchema.readonly(),
485
473
  collectionId: uuidSchema.readonly()
@@ -487,7 +475,7 @@ const countEntriesSchema = z.object({
487
475
 
488
476
  //#endregion
489
477
  //#region src/schema/fieldSchema.ts
490
- const FieldTypeSchema = z.enum([
478
+ const fieldTypeSchema = z.enum([
491
479
  "text",
492
480
  "textarea",
493
481
  "email",
@@ -503,64 +491,65 @@ const FieldTypeSchema = z.enum([
503
491
  "asset",
504
492
  "entry"
505
493
  ]);
506
- const FieldWidthSchema = z.enum([
494
+ const fieldWidthSchema = z.enum([
507
495
  "12",
508
496
  "6",
509
497
  "4",
510
498
  "3"
511
499
  ]);
512
- const FieldDefinitionBaseSchema = z.object({
500
+ const fieldDefinitionBaseSchema = z.object({
513
501
  id: uuidSchema.readonly(),
502
+ slug: slugSchema,
514
503
  label: translatableStringSchema,
515
504
  description: translatableStringSchema.nullable(),
516
505
  isRequired: z.boolean(),
517
506
  isDisabled: z.boolean(),
518
507
  isUnique: z.boolean(),
519
- inputWidth: FieldWidthSchema
508
+ inputWidth: fieldWidthSchema
520
509
  });
521
510
  /**
522
511
  * String based Field definitions
523
512
  */
524
- const StringFieldDefinitionBaseSchema = FieldDefinitionBaseSchema.extend({
525
- valueType: z.literal(ValueTypeSchema.enum.string),
513
+ const stringFieldDefinitionBaseSchema = fieldDefinitionBaseSchema.extend({
514
+ valueType: z.literal(valueTypeSchema.enum.string),
526
515
  defaultValue: z.string().nullable()
527
516
  });
528
- const textFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
529
- fieldType: z.literal(FieldTypeSchema.enum.text),
517
+ const textFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
518
+ fieldType: z.literal(fieldTypeSchema.enum.text),
530
519
  min: z.number().nullable(),
531
520
  max: z.number().nullable()
532
521
  });
533
- const textareaFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
534
- fieldType: z.literal(FieldTypeSchema.enum.textarea),
522
+ const textareaFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
523
+ fieldType: z.literal(fieldTypeSchema.enum.textarea),
535
524
  min: z.number().nullable(),
536
525
  max: z.number().nullable()
537
526
  });
538
- const emailFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
539
- fieldType: z.literal(FieldTypeSchema.enum.email),
527
+ const emailFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
528
+ fieldType: z.literal(fieldTypeSchema.enum.email),
540
529
  defaultValue: z.email().nullable()
541
530
  });
542
- const urlFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
543
- fieldType: z.literal(FieldTypeSchema.enum.url),
531
+ const urlFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
532
+ fieldType: z.literal(fieldTypeSchema.enum.url),
544
533
  defaultValue: z.url().nullable()
545
534
  });
546
- const ipv4FieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
547
- fieldType: z.literal(FieldTypeSchema.enum.ipv4),
535
+ const ipv4FieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
536
+ fieldType: z.literal(fieldTypeSchema.enum.ipv4),
548
537
  defaultValue: z.ipv4().nullable()
549
538
  });
550
- const dateFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
551
- fieldType: z.literal(FieldTypeSchema.enum.date),
539
+ const dateFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
540
+ fieldType: z.literal(fieldTypeSchema.enum.date),
552
541
  defaultValue: z.iso.date().nullable()
553
542
  });
554
- const timeFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
555
- fieldType: z.literal(FieldTypeSchema.enum.time),
543
+ const timeFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
544
+ fieldType: z.literal(fieldTypeSchema.enum.time),
556
545
  defaultValue: z.iso.time().nullable()
557
546
  });
558
- const datetimeFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
559
- fieldType: z.literal(FieldTypeSchema.enum.datetime),
547
+ const datetimeFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
548
+ fieldType: z.literal(fieldTypeSchema.enum.datetime),
560
549
  defaultValue: z.iso.datetime().nullable()
561
550
  });
562
- const telephoneFieldDefinitionSchema = StringFieldDefinitionBaseSchema.extend({
563
- fieldType: z.literal(FieldTypeSchema.enum.telephone),
551
+ const telephoneFieldDefinitionSchema = stringFieldDefinitionBaseSchema.extend({
552
+ fieldType: z.literal(fieldTypeSchema.enum.telephone),
564
553
  defaultValue: z.e164().nullable()
565
554
  });
566
555
  const stringFieldDefinitionSchema = z.union([
@@ -577,16 +566,16 @@ const stringFieldDefinitionSchema = z.union([
577
566
  /**
578
567
  * Number based Field definitions
579
568
  */
580
- const NumberFieldDefinitionBaseSchema = FieldDefinitionBaseSchema.extend({
581
- valueType: z.literal(ValueTypeSchema.enum.number),
569
+ const numberFieldDefinitionBaseSchema = fieldDefinitionBaseSchema.extend({
570
+ valueType: z.literal(valueTypeSchema.enum.number),
582
571
  min: z.number().nullable(),
583
572
  max: z.number().nullable(),
584
573
  isUnique: z.literal(false),
585
574
  defaultValue: z.number().nullable()
586
575
  });
587
- const numberFieldDefinitionSchema = NumberFieldDefinitionBaseSchema.extend({ fieldType: z.literal(FieldTypeSchema.enum.number) });
588
- const rangeFieldDefinitionSchema = NumberFieldDefinitionBaseSchema.extend({
589
- fieldType: z.literal(FieldTypeSchema.enum.range),
576
+ const numberFieldDefinitionSchema = numberFieldDefinitionBaseSchema.extend({ fieldType: z.literal(fieldTypeSchema.enum.number) });
577
+ const rangeFieldDefinitionSchema = numberFieldDefinitionBaseSchema.extend({
578
+ fieldType: z.literal(fieldTypeSchema.enum.range),
590
579
  isRequired: z.literal(true),
591
580
  min: z.number(),
592
581
  max: z.number(),
@@ -595,24 +584,27 @@ const rangeFieldDefinitionSchema = NumberFieldDefinitionBaseSchema.extend({
595
584
  /**
596
585
  * Boolean based Field definitions
597
586
  */
598
- const BooleanFieldDefinitionBaseSchema = FieldDefinitionBaseSchema.extend({
599
- valueType: z.literal(ValueTypeSchema.enum.boolean),
587
+ const booleanFieldDefinitionBaseSchema = fieldDefinitionBaseSchema.extend({
588
+ valueType: z.literal(valueTypeSchema.enum.boolean),
600
589
  isRequired: z.literal(true),
601
590
  defaultValue: z.boolean(),
602
591
  isUnique: z.literal(false)
603
592
  });
604
- const toggleFieldDefinitionSchema = BooleanFieldDefinitionBaseSchema.extend({ fieldType: z.literal(FieldTypeSchema.enum.toggle) });
593
+ const toggleFieldDefinitionSchema = booleanFieldDefinitionBaseSchema.extend({ fieldType: z.literal(fieldTypeSchema.enum.toggle) });
605
594
  /**
606
595
  * Reference based Field definitions
607
596
  */
608
- const ReferenceFieldDefinitionBaseSchema = FieldDefinitionBaseSchema.extend({ valueType: z.literal(ValueTypeSchema.enum.reference) });
609
- const assetFieldDefinitionSchema = ReferenceFieldDefinitionBaseSchema.extend({
610
- fieldType: z.literal(FieldTypeSchema.enum.asset),
597
+ const referenceFieldDefinitionBaseSchema = fieldDefinitionBaseSchema.extend({
598
+ valueType: z.literal(valueTypeSchema.enum.reference),
599
+ isUnique: z.literal(false)
600
+ });
601
+ const assetFieldDefinitionSchema = referenceFieldDefinitionBaseSchema.extend({
602
+ fieldType: z.literal(fieldTypeSchema.enum.asset),
611
603
  min: z.number().nullable(),
612
604
  max: z.number().nullable()
613
605
  });
614
- const entryFieldDefinitionSchema = ReferenceFieldDefinitionBaseSchema.extend({
615
- fieldType: z.literal(FieldTypeSchema.enum.entry),
606
+ const entryFieldDefinitionSchema = referenceFieldDefinitionBaseSchema.extend({
607
+ fieldType: z.literal(fieldTypeSchema.enum.entry),
616
608
  ofCollections: z.array(uuidSchema),
617
609
  min: z.number().nullable(),
618
610
  max: z.number().nullable()
@@ -635,18 +627,23 @@ const collectionFileSchema = baseFileSchema.extend({
635
627
  plural: translatableStringSchema
636
628
  }),
637
629
  slug: z.object({
638
- singular: z.string(),
639
- plural: z.string()
630
+ singular: slugSchema,
631
+ plural: slugSchema
640
632
  }),
641
633
  description: translatableStringSchema,
642
634
  icon: supportedIconSchema,
643
635
  fieldDefinitions: z.array(fieldDefinitionSchema)
644
636
  });
645
- const collectionSchema = collectionFileSchema.extend({ history: z.array(gitCommitSchema) }).openapi("Collection");
637
+ const collectionSchema = collectionFileSchema.openapi("Collection");
638
+ const collectionHistorySchema = z.object({
639
+ id: uuidSchema.readonly(),
640
+ projectId: uuidSchema.readonly()
641
+ });
646
642
  const collectionExportSchema = collectionSchema.extend({ entries: z.array(entryExportSchema) });
647
643
  const createCollectionSchema = collectionFileSchema.omit({
648
644
  id: true,
649
645
  objectType: true,
646
+ coreVersion: true,
650
647
  created: true,
651
648
  updated: true
652
649
  }).extend({ projectId: uuidSchema.readonly() });
@@ -655,6 +652,11 @@ const readCollectionSchema = z.object({
655
652
  projectId: uuidSchema.readonly(),
656
653
  commitHash: z.string().optional().readonly()
657
654
  });
655
+ const readBySlugCollectionSchema = z.object({
656
+ slug: slugSchema,
657
+ projectId: uuidSchema.readonly(),
658
+ commitHash: z.string().optional().readonly()
659
+ });
658
660
  const updateCollectionSchema = collectionFileSchema.pick({
659
661
  id: true,
660
662
  name: true,
@@ -665,6 +667,14 @@ const updateCollectionSchema = collectionFileSchema.pick({
665
667
  }).extend({ projectId: uuidSchema.readonly() });
666
668
  const deleteCollectionSchema = readCollectionSchema.extend({});
667
669
  const countCollectionsSchema = z.object({ projectId: uuidSchema.readonly() });
670
+ const migrateCollectionSchema = z.looseObject(collectionFileSchema.pick({
671
+ id: true,
672
+ coreVersion: true
673
+ }).shape);
674
+ const resolveCollectionIdSchema = z.object({
675
+ projectId: uuidSchema.readonly(),
676
+ idOrSlug: z.string()
677
+ });
668
678
 
669
679
  //#endregion
670
680
  //#region src/schema/coreSchema.ts
@@ -681,15 +691,88 @@ const constructorElekIoCoreSchema = elekIoCoreOptionsSchema.partial({
681
691
  }).optional();
682
692
 
683
693
  //#endregion
684
- //#region src/schema/projectSchema.ts
685
- const projectStatusSchema = z.enum([
686
- "foo",
687
- "bar",
688
- "todo"
694
+ //#region src/schema/gitSchema.ts
695
+ /**
696
+ * Signature git uses to identify users
697
+ */
698
+ const gitSignatureSchema = z.object({
699
+ name: z.string().regex(/^[^|]+$/, "Name must not contain pipe characters"),
700
+ email: z.string().email()
701
+ });
702
+ const gitMessageSchema = z.object({
703
+ method: z.enum([
704
+ "create",
705
+ "update",
706
+ "delete",
707
+ "upgrade",
708
+ "release"
709
+ ]),
710
+ reference: z.object({
711
+ objectType: objectTypeSchema,
712
+ id: uuidSchema,
713
+ collectionId: uuidSchema.optional()
714
+ })
715
+ });
716
+ const gitTagMessageSchema = z.discriminatedUnion("type", [
717
+ z.object({
718
+ type: z.literal("release"),
719
+ version: versionSchema
720
+ }),
721
+ z.object({
722
+ type: z.literal("preview"),
723
+ version: versionSchema
724
+ }),
725
+ z.object({
726
+ type: z.literal("upgrade"),
727
+ coreVersion: versionSchema
728
+ })
689
729
  ]);
730
+ const gitTagSchema = z.object({
731
+ id: uuidSchema,
732
+ message: gitTagMessageSchema,
733
+ author: gitSignatureSchema,
734
+ datetime: z.iso.datetime()
735
+ });
736
+ const gitCommitSchema = z.object({
737
+ hash: z.hash("sha1"),
738
+ message: gitMessageSchema,
739
+ author: gitSignatureSchema,
740
+ datetime: z.iso.datetime(),
741
+ tag: gitTagSchema.nullable()
742
+ });
743
+ const gitInitOptionsSchema = z.object({ initialBranch: z.string() });
744
+ const gitCloneOptionsSchema = z.object({
745
+ depth: z.number(),
746
+ singleBranch: z.boolean(),
747
+ branch: z.string(),
748
+ bare: z.boolean()
749
+ });
750
+ const gitMergeOptionsSchema = z.object({ squash: z.boolean() });
751
+ const gitSwitchOptionsSchema = z.object({ isNew: z.boolean().optional() });
752
+ const gitLogOptionsSchema = z.object({
753
+ limit: z.number().optional(),
754
+ between: z.object({
755
+ from: z.string(),
756
+ to: z.string().optional()
757
+ }),
758
+ filePath: z.string().optional()
759
+ });
760
+ const createGitTagSchema = gitTagSchema.pick({ message: true }).extend({
761
+ path: z.string(),
762
+ hash: z.string().optional()
763
+ });
764
+ const readGitTagSchema = z.object({
765
+ path: z.string(),
766
+ id: uuidSchema.readonly()
767
+ });
768
+ const deleteGitTagSchema = readGitTagSchema.extend({});
769
+ const countGitTagsSchema = z.object({ path: z.string() });
770
+
771
+ //#endregion
772
+ //#region src/schema/projectSchema.ts
690
773
  const projectSettingsSchema = z.object({ language: z.object({
691
774
  default: supportedLanguageSchema,
692
- supported: z.array(supportedLanguageSchema)
775
+ supported: z.array(supportedLanguageSchema).refine((langs) => new Set(langs).size === langs.length, { message: "Supported languages must not contain duplicates" })
693
776
  }) });
694
777
  const projectFolderSchema = z.enum([
695
778
  "assets",
@@ -700,22 +783,21 @@ const projectFolderSchema = z.enum([
700
783
  const projectBranchSchema = z.enum(["production", "work"]);
701
784
  const projectFileSchema = baseFileSchema.extend({
702
785
  objectType: z.literal(objectTypeSchema.enum.project).readonly(),
703
- coreVersion: versionSchema,
704
786
  name: z.string().trim().min(1),
705
787
  description: z.string().trim().min(1),
706
788
  version: versionSchema,
707
- status: projectStatusSchema,
708
789
  settings: projectSettingsSchema
709
790
  });
710
- const projectSchema = projectFileSchema.extend({
711
- remoteOriginUrl: z.string().nullable().openapi({ description: "URL of the remote Git repository" }),
791
+ const projectSchema = projectFileSchema.extend({ remoteOriginUrl: z.string().nullable().openapi({ description: "URL of the remote Git repository" }) }).openapi("Project");
792
+ const projectHistorySchema = z.object({ id: uuidSchema.readonly() });
793
+ const projectHistoryResultSchema = z.object({
712
794
  history: z.array(gitCommitSchema).openapi({ description: "Commit history of this Project" }),
713
795
  fullHistory: z.array(gitCommitSchema).openapi({ description: "Full commit history of this Project including all Assets, Collections, Entries and other files" })
714
- }).openapi("Project");
715
- const migrateProjectSchema = projectFileSchema.pick({
796
+ });
797
+ const migrateProjectSchema = z.looseObject(projectFileSchema.pick({
716
798
  id: true,
717
799
  coreVersion: true
718
- }).loose();
800
+ }).shape);
719
801
  const projectExportSchema = projectSchema.extend({
720
802
  assets: z.array(assetExportSchema),
721
803
  collections: z.array(collectionExportSchema)
@@ -740,13 +822,6 @@ const upgradeProjectSchema = z.object({
740
822
  force: z.boolean().optional()
741
823
  });
742
824
  const deleteProjectSchema = readProjectSchema.extend({ force: z.boolean().optional() });
743
- const projectUpgradeSchema = z.object({
744
- to: versionSchema.readonly(),
745
- run: z.function({
746
- input: [projectFileSchema],
747
- output: z.promise(z.void())
748
- })
749
- });
750
825
  const cloneProjectSchema = z.object({ url: z.string() });
751
826
  const listBranchesProjectSchema = z.object({ id: uuidSchema.readonly() });
752
827
  const currentBranchProjectSchema = z.object({ id: uuidSchema.readonly() });
@@ -800,29 +875,29 @@ function getNumberValueContentSchemaFromFieldDefinition(fieldDefinition) {
800
875
  function getStringValueContentSchemaFromFieldDefinition(fieldDefinition) {
801
876
  let schema = null;
802
877
  switch (fieldDefinition.fieldType) {
803
- case FieldTypeSchema.enum.email:
878
+ case fieldTypeSchema.enum.email:
804
879
  schema = z.email();
805
880
  break;
806
- case FieldTypeSchema.enum.url:
881
+ case fieldTypeSchema.enum.url:
807
882
  schema = z.url();
808
883
  break;
809
- case FieldTypeSchema.enum.ipv4:
884
+ case fieldTypeSchema.enum.ipv4:
810
885
  schema = z.ipv4();
811
886
  break;
812
- case FieldTypeSchema.enum.date:
887
+ case fieldTypeSchema.enum.date:
813
888
  schema = z.iso.date();
814
889
  break;
815
- case FieldTypeSchema.enum.time:
890
+ case fieldTypeSchema.enum.time:
816
891
  schema = z.iso.time();
817
892
  break;
818
- case FieldTypeSchema.enum.datetime:
893
+ case fieldTypeSchema.enum.datetime:
819
894
  schema = z.iso.datetime();
820
895
  break;
821
- case FieldTypeSchema.enum.telephone:
896
+ case fieldTypeSchema.enum.telephone:
822
897
  schema = z.e164();
823
898
  break;
824
- case FieldTypeSchema.enum.text:
825
- case FieldTypeSchema.enum.textarea:
899
+ case fieldTypeSchema.enum.text:
900
+ case fieldTypeSchema.enum.textarea:
826
901
  schema = z.string().trim();
827
902
  break;
828
903
  }
@@ -838,10 +913,10 @@ function getStringValueContentSchemaFromFieldDefinition(fieldDefinition) {
838
913
  function getReferenceValueContentSchemaFromFieldDefinition(fieldDefinition) {
839
914
  let schema;
840
915
  switch (fieldDefinition.fieldType) {
841
- case FieldTypeSchema.enum.asset:
916
+ case fieldTypeSchema.enum.asset:
842
917
  schema = z.array(valueContentReferenceToAssetSchema);
843
918
  break;
844
- case FieldTypeSchema.enum.entry:
919
+ case fieldTypeSchema.enum.entry:
845
920
  schema = z.array(valueContentReferenceToEntrySchema);
846
921
  break;
847
922
  }
@@ -867,35 +942,37 @@ function getTranslatableReferenceValueContentSchemaFromFieldDefinition(fieldDefi
867
942
  */
868
943
  function getValueSchemaFromFieldDefinition(fieldDefinition) {
869
944
  switch (fieldDefinition.valueType) {
870
- case ValueTypeSchema.enum.boolean: return directBooleanValueSchema.extend({ content: getTranslatableBooleanValueContentSchemaFromFieldDefinition() });
871
- case ValueTypeSchema.enum.number: return directNumberValueSchema.extend({ content: getTranslatableNumberValueContentSchemaFromFieldDefinition(fieldDefinition) });
872
- case ValueTypeSchema.enum.string: return directStringValueSchema.extend({ content: getTranslatableStringValueContentSchemaFromFieldDefinition(fieldDefinition) });
873
- case ValueTypeSchema.enum.reference: return referencedValueSchema.extend({ content: getTranslatableReferenceValueContentSchemaFromFieldDefinition(fieldDefinition) });
945
+ case valueTypeSchema.enum.boolean: return directBooleanValueSchema.extend({ content: getTranslatableBooleanValueContentSchemaFromFieldDefinition() });
946
+ case valueTypeSchema.enum.number: return directNumberValueSchema.extend({ content: getTranslatableNumberValueContentSchemaFromFieldDefinition(fieldDefinition) });
947
+ case valueTypeSchema.enum.string: return directStringValueSchema.extend({ content: getTranslatableStringValueContentSchemaFromFieldDefinition(fieldDefinition) });
948
+ case valueTypeSchema.enum.reference: return referencedValueSchema.extend({ content: getTranslatableReferenceValueContentSchemaFromFieldDefinition(fieldDefinition) });
874
949
  default: throw new Error(`Error generating schema for unsupported ValueType "${fieldDefinition.valueType}"`);
875
950
  }
876
951
  }
877
952
  /**
953
+ * Builds a z.object shape from field definitions, keyed by slug
954
+ */
955
+ function getValuesShapeFromFieldDefinitions(fieldDefinitions) {
956
+ const shape = {};
957
+ for (const fieldDef of fieldDefinitions) shape[fieldDef.slug] = getValueSchemaFromFieldDefinition(fieldDef);
958
+ return shape;
959
+ }
960
+ /**
878
961
  * Generates a schema for creating a new Entry based on the given Field definitions and Values
879
962
  */
880
963
  function getCreateEntrySchemaFromFieldDefinitions(fieldDefinitions) {
881
- const valueSchemas = fieldDefinitions.map((fieldDefinition) => {
882
- return getValueSchemaFromFieldDefinition(fieldDefinition);
883
- });
884
964
  return z.object({
885
965
  ...createEntrySchema.shape,
886
- values: z.tuple(valueSchemas)
966
+ values: z.object(getValuesShapeFromFieldDefinitions(fieldDefinitions))
887
967
  });
888
968
  }
889
969
  /**
890
970
  * Generates a schema for updating an existing Entry based on the given Field definitions and Values
891
971
  */
892
972
  function getUpdateEntrySchemaFromFieldDefinitions(fieldDefinitions) {
893
- const valueSchemas = fieldDefinitions.map((fieldDefinition) => {
894
- return getValueSchemaFromFieldDefinition(fieldDefinition);
895
- });
896
973
  return z.object({
897
974
  ...updateEntrySchema.shape,
898
- values: z.tuple(valueSchemas)
975
+ values: z.object(getValuesShapeFromFieldDefinitions(fieldDefinitions))
899
976
  });
900
977
  }
901
978
 
@@ -911,7 +988,8 @@ const serviceTypeSchema = z.enum([
911
988
  "Search",
912
989
  "Collection",
913
990
  "Entry",
914
- "Value"
991
+ "Value",
992
+ "Release"
915
993
  ]);
916
994
  function paginatedListOf(schema) {
917
995
  return z.object({
@@ -934,18 +1012,18 @@ const listGitTagsSchema = z.object({ path: z.string() });
934
1012
 
935
1013
  //#endregion
936
1014
  //#region src/schema/userSchema.ts
937
- const UserTypeSchema = z.enum(["local", "cloud"]);
1015
+ const userTypeSchema = z.enum(["local", "cloud"]);
938
1016
  const baseUserSchema = gitSignatureSchema.extend({
939
- userType: UserTypeSchema,
1017
+ userType: userTypeSchema,
940
1018
  language: supportedLanguageSchema,
941
1019
  localApi: z.object({
942
1020
  isEnabled: z.boolean(),
943
1021
  port: z.number()
944
1022
  })
945
1023
  });
946
- const localUserSchema = baseUserSchema.extend({ userType: z.literal(UserTypeSchema.enum.local) });
1024
+ const localUserSchema = baseUserSchema.extend({ userType: z.literal(userTypeSchema.enum.local) });
947
1025
  const cloudUserSchema = baseUserSchema.extend({
948
- userType: z.literal(UserTypeSchema.enum.cloud),
1026
+ userType: z.literal(userTypeSchema.enum.cloud),
949
1027
  id: uuidSchema
950
1028
  });
951
1029
  const userFileSchema = z.union([localUserSchema, cloudUserSchema]);
@@ -1019,6 +1097,97 @@ const logConsoleTransportSchema = logSchema.extend({
1019
1097
  level: z.string()
1020
1098
  });
1021
1099
 
1100
+ //#endregion
1101
+ //#region src/schema/releaseSchema.ts
1102
+ const semverBumpSchema = z.enum([
1103
+ "major",
1104
+ "minor",
1105
+ "patch"
1106
+ ]);
1107
+ const fieldChangeTypeSchema = z.enum([
1108
+ "added",
1109
+ "deleted",
1110
+ "valueTypeChanged",
1111
+ "fieldTypeChanged",
1112
+ "slugChanged",
1113
+ "minMaxTightened",
1114
+ "isRequiredToNotRequired",
1115
+ "isUniqueToNotUnique",
1116
+ "ofCollectionsChanged",
1117
+ "isNotRequiredToRequired",
1118
+ "isNotUniqueToUnique",
1119
+ "labelChanged",
1120
+ "descriptionChanged",
1121
+ "defaultValueChanged",
1122
+ "inputWidthChanged",
1123
+ "isDisabledChanged",
1124
+ "minMaxLoosened"
1125
+ ]);
1126
+ const fieldChangeSchema = z.object({
1127
+ collectionId: uuidSchema,
1128
+ fieldId: uuidSchema,
1129
+ fieldSlug: z.string(),
1130
+ changeType: fieldChangeTypeSchema,
1131
+ bump: semverBumpSchema
1132
+ });
1133
+ const collectionChangeTypeSchema = z.enum(["added", "deleted"]);
1134
+ const collectionChangeSchema = z.object({
1135
+ collectionId: uuidSchema,
1136
+ changeType: collectionChangeTypeSchema,
1137
+ bump: semverBumpSchema
1138
+ });
1139
+ const projectChangeTypeSchema = z.enum([
1140
+ "nameChanged",
1141
+ "descriptionChanged",
1142
+ "defaultLanguageChanged",
1143
+ "supportedLanguageAdded",
1144
+ "supportedLanguageRemoved"
1145
+ ]);
1146
+ const projectChangeSchema = z.object({
1147
+ changeType: projectChangeTypeSchema,
1148
+ bump: semverBumpSchema
1149
+ });
1150
+ const assetChangeTypeSchema = z.enum([
1151
+ "added",
1152
+ "deleted",
1153
+ "metadataChanged",
1154
+ "binaryChanged"
1155
+ ]);
1156
+ const assetChangeSchema = z.object({
1157
+ assetId: uuidSchema,
1158
+ changeType: assetChangeTypeSchema,
1159
+ bump: semverBumpSchema
1160
+ });
1161
+ const entryChangeTypeSchema = z.enum([
1162
+ "added",
1163
+ "deleted",
1164
+ "modified"
1165
+ ]);
1166
+ const entryChangeSchema = z.object({
1167
+ collectionId: uuidSchema,
1168
+ entryId: uuidSchema,
1169
+ changeType: entryChangeTypeSchema,
1170
+ bump: semverBumpSchema
1171
+ });
1172
+ const releaseDiffSchema = z.object({
1173
+ project: projectSchema,
1174
+ bump: semverBumpSchema.nullable(),
1175
+ currentVersion: versionSchema,
1176
+ nextVersion: versionSchema.nullable(),
1177
+ projectChanges: z.array(projectChangeSchema),
1178
+ collectionChanges: z.array(collectionChangeSchema),
1179
+ fieldChanges: z.array(fieldChangeSchema),
1180
+ assetChanges: z.array(assetChangeSchema),
1181
+ entryChanges: z.array(entryChangeSchema)
1182
+ });
1183
+ const prepareReleaseSchema = z.object({ projectId: uuidSchema.readonly() });
1184
+ const createReleaseSchema = z.object({ projectId: uuidSchema.readonly() });
1185
+ const createPreviewReleaseSchema = z.object({ projectId: uuidSchema.readonly() });
1186
+ const releaseResultSchema = z.object({
1187
+ version: versionSchema,
1188
+ diff: releaseDiffSchema
1189
+ });
1190
+
1022
1191
  //#endregion
1023
1192
  //#region src/api/routes/content/v1/projects.ts
1024
1193
  const tags$3 = ["Content API v1"];
@@ -1140,29 +1309,36 @@ const router$5 = createRouter().openapi(createRoute({
1140
1309
  return c.json(count, 200);
1141
1310
  }).openapi(createRoute({
1142
1311
  summary: "Get one Collection",
1143
- description: "Retrieve a Collection by ID",
1312
+ description: "Retrieve a Collection by UUID or slug",
1144
1313
  method: "get",
1145
- path: "/{projectId}/collections/{collectionId}",
1314
+ path: "/{projectId}/collections/{collectionIdOrSlug}",
1146
1315
  tags: tags$2,
1147
1316
  request: { params: z.object({
1148
1317
  projectId: uuidSchema.openapi({ param: {
1149
1318
  name: "projectId",
1150
1319
  in: "path"
1151
1320
  } }),
1152
- collectionId: uuidSchema.openapi({ param: {
1153
- name: "collectionId",
1154
- in: "path"
1155
- } })
1321
+ collectionIdOrSlug: z.string().openapi({
1322
+ param: {
1323
+ name: "collectionIdOrSlug",
1324
+ in: "path"
1325
+ },
1326
+ description: "Collection UUID or slug"
1327
+ })
1156
1328
  }) },
1157
1329
  responses: { [200]: {
1158
1330
  content: { "application/json": { schema: collectionSchema } },
1159
1331
  description: "The requested Collection"
1160
1332
  } }
1161
1333
  }), async (c) => {
1162
- const { projectId, collectionId } = c.req.valid("param");
1334
+ const { projectId, collectionIdOrSlug } = c.req.valid("param");
1335
+ const resolvedId = await c.var.collectionService.resolveCollectionId({
1336
+ projectId,
1337
+ idOrSlug: collectionIdOrSlug
1338
+ });
1163
1339
  const collection = await c.var.collectionService.read({
1164
1340
  projectId,
1165
- id: collectionId
1341
+ id: resolvedId
1166
1342
  });
1167
1343
  return c.json(collection, 200);
1168
1344
  });
@@ -1174,7 +1350,7 @@ const router$4 = createRouter().openapi(createRoute({
1174
1350
  summary: "List Entries",
1175
1351
  description: "Lists all Entries of the given Projects Collection",
1176
1352
  method: "get",
1177
- path: "/{projectId}/collections/{collectionId}/entries",
1353
+ path: "/{projectId}/collections/{collectionIdOrSlug}/entries",
1178
1354
  tags: tags$1,
1179
1355
  request: {
1180
1356
  params: z.object({
@@ -1182,10 +1358,13 @@ const router$4 = createRouter().openapi(createRoute({
1182
1358
  name: "projectId",
1183
1359
  in: "path"
1184
1360
  } }),
1185
- collectionId: uuidSchema.openapi({ param: {
1186
- name: "collectionId",
1187
- in: "path"
1188
- } })
1361
+ collectionIdOrSlug: z.string().openapi({
1362
+ param: {
1363
+ name: "collectionIdOrSlug",
1364
+ in: "path"
1365
+ },
1366
+ description: "Collection UUID or slug"
1367
+ })
1189
1368
  }),
1190
1369
  query: z.object({
1191
1370
  limit: z.string().pipe(z.coerce.number()).optional().openapi({
@@ -1203,8 +1382,12 @@ const router$4 = createRouter().openapi(createRoute({
1203
1382
  description: "A list of Entries for the given Projects Collection"
1204
1383
  } }
1205
1384
  }), async (c) => {
1206
- const { projectId, collectionId } = c.req.valid("param");
1385
+ const { projectId, collectionIdOrSlug } = c.req.valid("param");
1207
1386
  const { limit, offset } = c.req.valid("query");
1387
+ const collectionId = await c.var.collectionService.resolveCollectionId({
1388
+ projectId,
1389
+ idOrSlug: collectionIdOrSlug
1390
+ });
1208
1391
  const entries = await c.var.entryService.list({
1209
1392
  projectId,
1210
1393
  collectionId,
@@ -1216,24 +1399,31 @@ const router$4 = createRouter().openapi(createRoute({
1216
1399
  summary: "Count Entries",
1217
1400
  description: "Counts all Entries of the given Projects Collection",
1218
1401
  method: "get",
1219
- path: "/{projectId}/collections/{collectionId}/entries/count",
1402
+ path: "/{projectId}/collections/{collectionIdOrSlug}/entries/count",
1220
1403
  tags: tags$1,
1221
1404
  request: { params: z.object({
1222
1405
  projectId: uuidSchema.openapi({ param: {
1223
1406
  name: "projectId",
1224
1407
  in: "path"
1225
1408
  } }),
1226
- collectionId: uuidSchema.openapi({ param: {
1227
- name: "collectionId",
1228
- in: "path"
1229
- } })
1409
+ collectionIdOrSlug: z.string().openapi({
1410
+ param: {
1411
+ name: "collectionIdOrSlug",
1412
+ in: "path"
1413
+ },
1414
+ description: "Collection UUID or slug"
1415
+ })
1230
1416
  }) },
1231
1417
  responses: { [200]: {
1232
1418
  content: { "application/json": { schema: z.number() } },
1233
1419
  description: "The number of Entries of the given Projects Collection"
1234
1420
  } }
1235
1421
  }), async (c) => {
1236
- const { projectId, collectionId } = c.req.valid("param");
1422
+ const { projectId, collectionIdOrSlug } = c.req.valid("param");
1423
+ const collectionId = await c.var.collectionService.resolveCollectionId({
1424
+ projectId,
1425
+ idOrSlug: collectionIdOrSlug
1426
+ });
1237
1427
  const count = await c.var.entryService.count({
1238
1428
  projectId,
1239
1429
  collectionId
@@ -1243,17 +1433,20 @@ const router$4 = createRouter().openapi(createRoute({
1243
1433
  summary: "Get one Entry",
1244
1434
  description: "Retrieve an Entry by ID",
1245
1435
  method: "get",
1246
- path: "/{projectId}/collections/{collectionId}/entries/{entryId}",
1436
+ path: "/{projectId}/collections/{collectionIdOrSlug}/entries/{entryId}",
1247
1437
  tags: tags$1,
1248
1438
  request: { params: z.object({
1249
1439
  projectId: uuidSchema.openapi({ param: {
1250
1440
  name: "projectId",
1251
1441
  in: "path"
1252
1442
  } }),
1253
- collectionId: uuidSchema.openapi({ param: {
1254
- name: "collectionId",
1255
- in: "path"
1256
- } }),
1443
+ collectionIdOrSlug: z.string().openapi({
1444
+ param: {
1445
+ name: "collectionIdOrSlug",
1446
+ in: "path"
1447
+ },
1448
+ description: "Collection UUID or slug"
1449
+ }),
1257
1450
  entryId: uuidSchema.openapi({ param: {
1258
1451
  name: "entryId",
1259
1452
  in: "path"
@@ -1264,7 +1457,11 @@ const router$4 = createRouter().openapi(createRoute({
1264
1457
  description: "The requested Entry"
1265
1458
  } }
1266
1459
  }), async (c) => {
1267
- const { projectId, collectionId, entryId } = c.req.valid("param");
1460
+ const { projectId, collectionIdOrSlug, entryId } = c.req.valid("param");
1461
+ const collectionId = await c.var.collectionService.resolveCollectionId({
1462
+ projectId,
1463
+ idOrSlug: collectionIdOrSlug
1464
+ });
1268
1465
  const entry = await c.var.entryService.read({
1269
1466
  projectId,
1270
1467
  collectionId,
@@ -1531,6 +1728,9 @@ const pathTo = {
1531
1728
  collectionFile: (projectId, id) => {
1532
1729
  return Path.join(pathTo.collection(projectId, id), "collection.json");
1533
1730
  },
1731
+ collectionIndex: (projectId) => {
1732
+ return Path.join(pathTo.collections(projectId), "index.json");
1733
+ },
1534
1734
  entries: (projectId, collectionId) => {
1535
1735
  return Path.join(pathTo.collection(projectId, collectionId));
1536
1736
  },
@@ -1753,6 +1953,38 @@ var AbstractCrudService = class {
1753
1953
  }
1754
1954
  };
1755
1955
 
1956
+ //#endregion
1957
+ //#region src/service/migrations/applyMigrations.ts
1958
+ function applyMigrations(data, migrations, targetVersion) {
1959
+ let current = structuredClone(data);
1960
+ while (current["coreVersion"] !== targetVersion) {
1961
+ const migration = migrations.find((m) => m.from === current["coreVersion"]);
1962
+ if (!migration) {
1963
+ current["coreVersion"] = targetVersion;
1964
+ break;
1965
+ }
1966
+ current = migration.run(current);
1967
+ current["coreVersion"] = migration.to;
1968
+ }
1969
+ return current;
1970
+ }
1971
+
1972
+ //#endregion
1973
+ //#region src/service/migrations/assetMigrations.ts
1974
+ const assetMigrations = [];
1975
+
1976
+ //#endregion
1977
+ //#region src/service/migrations/collectionMigrations.ts
1978
+ const collectionMigrations = [];
1979
+
1980
+ //#endregion
1981
+ //#region src/service/migrations/entryMigrations.ts
1982
+ const entryMigrations = [];
1983
+
1984
+ //#endregion
1985
+ //#region src/service/migrations/projectMigrations.ts
1986
+ const projectMigrations = [];
1987
+
1756
1988
  //#endregion
1757
1989
  //#region src/util/shared.ts
1758
1990
  /**
@@ -1795,10 +2027,12 @@ function slug(string) {
1795
2027
  * Service that manages CRUD functionality for Asset files on disk
1796
2028
  */
1797
2029
  var AssetService = class extends AbstractCrudService {
2030
+ coreVersion;
1798
2031
  jsonFileService;
1799
2032
  gitService;
1800
- constructor(options, logService, jsonFileService, gitService) {
2033
+ constructor(coreVersion, options, logService, jsonFileService, gitService) {
1801
2034
  super(serviceTypeSchema.enum.Asset, options, logService);
2035
+ this.coreVersion = coreVersion;
1802
2036
  this.jsonFileService = jsonFileService;
1803
2037
  this.gitService = gitService;
1804
2038
  }
@@ -1818,6 +2052,7 @@ var AssetService = class extends AbstractCrudService {
1818
2052
  name: slug(props.name),
1819
2053
  objectType: "asset",
1820
2054
  id,
2055
+ coreVersion: this.coreVersion,
1821
2056
  created: datetime(),
1822
2057
  updated: null,
1823
2058
  extension: fileType.extension,
@@ -1862,6 +2097,13 @@ var AssetService = class extends AbstractCrudService {
1862
2097
  }
1863
2098
  }
1864
2099
  /**
2100
+ * Returns the commit history of an Asset
2101
+ */
2102
+ async history(props) {
2103
+ assetHistorySchema.parse(props);
2104
+ return this.gitService.log(pathTo.project(props.projectId), { filePath: pathTo.assetFile(props.projectId, props.id) });
2105
+ }
2106
+ /**
1865
2107
  * Copies an Asset to given file path on disk
1866
2108
  */
1867
2109
  async save(props) {
@@ -1970,13 +2212,11 @@ var AssetService = class extends AbstractCrudService {
1970
2212
  * @param projectId The project's ID
1971
2213
  * @param assetFile The AssetFile to convert
1972
2214
  */
1973
- async toAsset(projectId, assetFile, commitHash) {
2215
+ toAsset(projectId, assetFile, commitHash) {
1974
2216
  const assetPath = commitHash ? pathTo.tmpAsset(assetFile.id, commitHash, assetFile.extension) : pathTo.asset(projectId, assetFile.id, assetFile.extension);
1975
- const history = await this.gitService.log(pathTo.project(projectId), { filePath: pathTo.assetFile(projectId, assetFile.id) });
1976
2217
  return {
1977
2218
  ...assetFile,
1978
- absolutePath: assetPath,
1979
- history
2219
+ absolutePath: assetPath
1980
2220
  };
1981
2221
  }
1982
2222
  /**
@@ -1999,7 +2239,8 @@ var AssetService = class extends AbstractCrudService {
1999
2239
  * Migrates an potentially outdated Asset file to the current schema
2000
2240
  */
2001
2241
  migrate(potentiallyOutdatedAssetFile) {
2002
- return assetFileSchema.parse(potentiallyOutdatedAssetFile);
2242
+ const migrated = applyMigrations(migrateAssetSchema.parse(potentiallyOutdatedAssetFile), assetMigrations, this.coreVersion);
2243
+ return assetFileSchema.parse(migrated);
2003
2244
  }
2004
2245
  };
2005
2246
 
@@ -2009,29 +2250,59 @@ var AssetService = class extends AbstractCrudService {
2009
2250
  * Service that manages CRUD functionality for Collection files on disk
2010
2251
  */
2011
2252
  var CollectionService = class extends AbstractCrudService {
2253
+ coreVersion;
2012
2254
  jsonFileService;
2013
2255
  gitService;
2014
- constructor(options, logService, jsonFileService, gitService) {
2256
+ /** In-memory cache for collection indices, keyed by projectId */
2257
+ cachedIndex = /* @__PURE__ */ new Map();
2258
+ /** Promise deduplication for concurrent rebuilds, keyed by projectId */
2259
+ rebuildPromise = /* @__PURE__ */ new Map();
2260
+ constructor(coreVersion, options, logService, jsonFileService, gitService) {
2015
2261
  super(serviceTypeSchema.enum.Collection, options, logService);
2262
+ this.coreVersion = coreVersion;
2016
2263
  this.jsonFileService = jsonFileService;
2017
2264
  this.gitService = gitService;
2018
2265
  }
2019
2266
  /**
2267
+ * Resolves a UUID-or-slug string to a collection UUID.
2268
+ *
2269
+ * If the input matches UUID format, verifies the folder exists on disk first.
2270
+ * If the folder doesn't exist, falls back to slug lookup.
2271
+ * Otherwise, looks up via the index.
2272
+ */
2273
+ async resolveCollectionId(props) {
2274
+ if (uuidSchema.safeParse(props.idOrSlug).success) {
2275
+ const collectionPath = pathTo.collection(props.projectId, props.idOrSlug);
2276
+ if (await Fs.pathExists(collectionPath)) return props.idOrSlug;
2277
+ }
2278
+ const index = await this.getIndex(props.projectId);
2279
+ for (const [uuid, slugValue] of Object.entries(index)) if (slugValue === props.idOrSlug) return uuid;
2280
+ this.cachedIndex.delete(props.projectId);
2281
+ const freshIndex = await this.getIndex(props.projectId);
2282
+ for (const [uuid, slugValue] of Object.entries(freshIndex)) if (slugValue === props.idOrSlug) return uuid;
2283
+ throw new Error(`Collection not found: "${props.idOrSlug}" does not match any collection UUID or slug`);
2284
+ }
2285
+ /**
2020
2286
  * Creates a new Collection
2021
2287
  */
2022
2288
  async create(props) {
2023
2289
  createCollectionSchema.parse(props);
2290
+ this.validateFieldDefinitionSlugUniqueness(props.fieldDefinitions);
2024
2291
  const id = uuid();
2025
2292
  const projectPath = pathTo.project(props.projectId);
2026
2293
  const collectionPath = pathTo.collection(props.projectId, id);
2027
2294
  const collectionFilePath = pathTo.collectionFile(props.projectId, id);
2295
+ const slugPlural = slug(props.slug.plural);
2296
+ const index = await this.getIndex(props.projectId);
2297
+ if (Object.values(index).includes(slugPlural)) throw new Error(`Collection slug "${slugPlural}" is already in use by another collection`);
2028
2298
  const collectionFile = {
2029
2299
  ...props,
2030
2300
  objectType: "collection",
2031
2301
  id,
2302
+ coreVersion: this.coreVersion,
2032
2303
  slug: {
2033
2304
  singular: slug(props.slug.singular),
2034
- plural: slug(props.slug.plural)
2305
+ plural: slugPlural
2035
2306
  },
2036
2307
  created: datetime(),
2037
2308
  updated: null
@@ -2046,7 +2317,9 @@ var CollectionService = class extends AbstractCrudService {
2046
2317
  id
2047
2318
  }
2048
2319
  });
2049
- return this.toCollection(props.projectId, collectionFile);
2320
+ index[id] = slugPlural;
2321
+ await this.writeIndex(props.projectId, index);
2322
+ return this.toCollection(collectionFile);
2050
2323
  }
2051
2324
  /**
2052
2325
  * Returns a Collection by ID
@@ -2057,33 +2330,103 @@ var CollectionService = class extends AbstractCrudService {
2057
2330
  readCollectionSchema.parse(props);
2058
2331
  if (!props.commitHash) {
2059
2332
  const collectionFile = await this.jsonFileService.read(pathTo.collectionFile(props.projectId, props.id), collectionFileSchema);
2060
- return this.toCollection(props.projectId, collectionFile);
2333
+ return this.toCollection(collectionFile);
2061
2334
  } else {
2062
2335
  const collectionFile = this.migrate(JSON.parse(await this.gitService.getFileContentAtCommit(pathTo.project(props.projectId), pathTo.collectionFile(props.projectId, props.id), props.commitHash)));
2063
- return this.toCollection(props.projectId, collectionFile);
2336
+ return this.toCollection(collectionFile);
2064
2337
  }
2065
2338
  }
2066
2339
  /**
2340
+ * Reads a Collection by its slug
2341
+ */
2342
+ async readBySlug(props) {
2343
+ const id = await this.resolveCollectionId({
2344
+ projectId: props.projectId,
2345
+ idOrSlug: props.slug
2346
+ });
2347
+ return this.read({
2348
+ projectId: props.projectId,
2349
+ id,
2350
+ commitHash: props.commitHash
2351
+ });
2352
+ }
2353
+ /**
2354
+ * Returns the commit history of a Collection
2355
+ */
2356
+ async history(props) {
2357
+ collectionHistorySchema.parse(props);
2358
+ return this.gitService.log(pathTo.project(props.projectId), { filePath: pathTo.collectionFile(props.projectId, props.id) });
2359
+ }
2360
+ /**
2067
2361
  * Updates given Collection
2068
2362
  *
2069
- * @todo finish implementing checks for FieldDefinitions and extract methods
2070
- *
2071
- * @param projectId Project ID of the collection to update
2072
- * @param collection Collection to write to disk
2073
- * @returns An object containing information about the actions needed to be taken,
2074
- * before given update can be executed or void if the update was executed successfully
2363
+ * Handles fieldDefinition slug rename cascade and collection slug uniqueness.
2075
2364
  */
2076
2365
  async update(props) {
2077
2366
  updateCollectionSchema.parse(props);
2367
+ this.validateFieldDefinitionSlugUniqueness(props.fieldDefinitions);
2078
2368
  const projectPath = pathTo.project(props.projectId);
2079
2369
  const collectionFilePath = pathTo.collectionFile(props.projectId, props.id);
2370
+ const prevCollectionFile = await this.read(props);
2080
2371
  const collectionFile = {
2081
- ...await this.read(props),
2372
+ ...prevCollectionFile,
2082
2373
  ...props,
2083
2374
  updated: datetime()
2084
2375
  };
2376
+ const oldFieldDefs = prevCollectionFile.fieldDefinitions;
2377
+ const newFieldDefs = props.fieldDefinitions;
2378
+ const slugRenames = [];
2379
+ const oldByUuid = new Map(oldFieldDefs.map((fd) => [fd.id, fd]));
2380
+ for (const newFd of newFieldDefs) {
2381
+ const oldFd = oldByUuid.get(newFd.id);
2382
+ if (oldFd && oldFd.slug !== newFd.slug) slugRenames.push({
2383
+ oldSlug: oldFd.slug,
2384
+ newSlug: newFd.slug
2385
+ });
2386
+ }
2387
+ const filesToGitAdd = [collectionFilePath];
2388
+ if (slugRenames.length > 0) {
2389
+ const entriesPath = pathTo.entries(props.projectId, props.id);
2390
+ if (await Fs.pathExists(entriesPath)) {
2391
+ const entryFiles = (await Fs.readdir(entriesPath)).filter((f) => f.endsWith(".json") && f !== "collection.json");
2392
+ for (const entryFileName of entryFiles) {
2393
+ const entryFilePath = pathTo.entryFile(props.projectId, props.id, entryFileName.replace(".json", ""));
2394
+ try {
2395
+ const entryFile = await this.jsonFileService.read(entryFilePath, entryFileSchema);
2396
+ let changed = false;
2397
+ const newValues = { ...entryFile.values };
2398
+ for (const { oldSlug, newSlug } of slugRenames) if (oldSlug in newValues) {
2399
+ newValues[newSlug] = newValues[oldSlug];
2400
+ delete newValues[oldSlug];
2401
+ changed = true;
2402
+ }
2403
+ if (changed) {
2404
+ const updatedEntryFile = {
2405
+ ...entryFile,
2406
+ values: newValues
2407
+ };
2408
+ await this.jsonFileService.update(updatedEntryFile, entryFilePath, entryFileSchema);
2409
+ filesToGitAdd.push(entryFilePath);
2410
+ }
2411
+ } catch (error) {
2412
+ this.logService.warn({
2413
+ source: "core",
2414
+ message: `Failed to update entry "${entryFileName}" during slug rename cascade: ${error instanceof Error ? error.message : String(error)}`
2415
+ });
2416
+ }
2417
+ }
2418
+ }
2419
+ }
2420
+ const newSlugPlural = slug(props.slug.plural);
2421
+ if (prevCollectionFile.slug.plural !== newSlugPlural) {
2422
+ const index = await this.getIndex(props.projectId);
2423
+ const existingUuid = Object.entries(index).find(([, s]) => s === newSlugPlural);
2424
+ if (existingUuid && existingUuid[0] !== props.id) throw new Error(`Collection slug "${newSlugPlural}" is already in use by another collection`);
2425
+ index[props.id] = newSlugPlural;
2426
+ await this.writeIndex(props.projectId, index);
2427
+ }
2085
2428
  await this.jsonFileService.update(collectionFile, collectionFilePath, collectionFileSchema);
2086
- await this.gitService.add(projectPath, [collectionFilePath]);
2429
+ await this.gitService.add(projectPath, filesToGitAdd);
2087
2430
  await this.gitService.commit(projectPath, {
2088
2431
  method: "update",
2089
2432
  reference: {
@@ -2091,10 +2434,10 @@ var CollectionService = class extends AbstractCrudService {
2091
2434
  id: collectionFile.id
2092
2435
  }
2093
2436
  });
2094
- return this.toCollection(props.projectId, collectionFile);
2437
+ return this.toCollection(collectionFile);
2095
2438
  }
2096
2439
  /**
2097
- * Deletes given Collection (folder), including it's items
2440
+ * Deletes given Collection (folder), including it's Entries
2098
2441
  *
2099
2442
  * The Fields that Collection used are not deleted.
2100
2443
  */
@@ -2111,6 +2454,9 @@ var CollectionService = class extends AbstractCrudService {
2111
2454
  id: props.id
2112
2455
  }
2113
2456
  });
2457
+ const index = await this.getIndex(props.projectId);
2458
+ delete index[props.id];
2459
+ await this.writeIndex(props.projectId, index);
2114
2460
  }
2115
2461
  async list(props) {
2116
2462
  listCollectionsSchema.parse(props);
@@ -2145,7 +2491,8 @@ var CollectionService = class extends AbstractCrudService {
2145
2491
  * Migrates an potentially outdated Collection file to the current schema
2146
2492
  */
2147
2493
  migrate(potentiallyOutdatedCollectionFile) {
2148
- return collectionFileSchema.parse(potentiallyOutdatedCollectionFile);
2494
+ const migrated = applyMigrations(migrateCollectionSchema.parse(potentiallyOutdatedCollectionFile), collectionMigrations, this.coreVersion);
2495
+ return collectionFileSchema.parse(migrated);
2149
2496
  }
2150
2497
  /**
2151
2498
  * Creates an Collection from given CollectionFile
@@ -2153,26 +2500,83 @@ var CollectionService = class extends AbstractCrudService {
2153
2500
  * @param projectId The project's ID
2154
2501
  * @param collectionFile The CollectionFile to convert
2155
2502
  */
2156
- async toCollection(projectId, collectionFile) {
2157
- const history = await this.gitService.log(pathTo.project(projectId), { filePath: pathTo.collectionFile(projectId, collectionFile.id) });
2158
- return {
2159
- ...collectionFile,
2160
- history
2161
- };
2503
+ toCollection(collectionFile) {
2504
+ return { ...collectionFile };
2162
2505
  }
2163
- };
2164
-
2506
+ /**
2507
+ * Gets the collection index, rebuilding from disk if not cached
2508
+ */
2509
+ async getIndex(projectId) {
2510
+ const cached = this.cachedIndex.get(projectId);
2511
+ if (cached) return cached;
2512
+ const pending = this.rebuildPromise.get(projectId);
2513
+ if (pending) return pending;
2514
+ const promise = this.rebuildIndex(projectId);
2515
+ this.rebuildPromise.set(projectId, promise);
2516
+ const result = await promise;
2517
+ this.cachedIndex.set(projectId, result);
2518
+ this.rebuildPromise.delete(projectId);
2519
+ return result;
2520
+ }
2521
+ /**
2522
+ * Writes the index file atomically and updates cache
2523
+ */
2524
+ async writeIndex(projectId, index) {
2525
+ const indexPath = pathTo.collectionIndex(projectId);
2526
+ await Fs.writeFile(indexPath, JSON.stringify(index, null, 2), { encoding: "utf8" });
2527
+ this.cachedIndex.set(projectId, index);
2528
+ }
2529
+ /**
2530
+ * Rebuilds the index by scanning all collection folders
2531
+ */
2532
+ async rebuildIndex(projectId) {
2533
+ this.logService.info({
2534
+ source: "core",
2535
+ message: `Rebuilding Collection index for Project "${projectId}"`
2536
+ });
2537
+ const index = {};
2538
+ const collectionFolders = await folders(pathTo.collections(projectId));
2539
+ for (const folder of collectionFolders) {
2540
+ if (!uuidSchema.safeParse(folder.name).success) continue;
2541
+ try {
2542
+ const collectionFilePath = pathTo.collectionFile(projectId, folder.name);
2543
+ const collectionFile = await this.jsonFileService.read(collectionFilePath, collectionFileSchema);
2544
+ index[collectionFile.id] = collectionFile.slug.plural;
2545
+ } catch (error) {
2546
+ this.logService.warn({
2547
+ source: "core",
2548
+ message: `Skipping collection folder "${folder.name}" during index rebuild: ${error instanceof Error ? error.message : String(error)}`
2549
+ });
2550
+ }
2551
+ }
2552
+ await this.writeIndex(projectId, index);
2553
+ return index;
2554
+ }
2555
+ /**
2556
+ * Validates that no two fieldDefinitions share the same slug
2557
+ */
2558
+ validateFieldDefinitionSlugUniqueness(fieldDefinitions) {
2559
+ const seen = /* @__PURE__ */ new Set();
2560
+ for (const fd of fieldDefinitions) {
2561
+ if (seen.has(fd.slug)) throw new Error(`Duplicate fieldDefinition slug "${fd.slug}": each fieldDefinition within a collection must have a unique slug`);
2562
+ seen.add(fd.slug);
2563
+ }
2564
+ }
2565
+ };
2566
+
2165
2567
  //#endregion
2166
2568
  //#region src/service/EntryService.ts
2167
2569
  /**
2168
2570
  * Service that manages CRUD functionality for Entry files on disk
2169
2571
  */
2170
2572
  var EntryService = class extends AbstractCrudService {
2573
+ coreVersion;
2171
2574
  jsonFileService;
2172
2575
  gitService;
2173
2576
  collectionService;
2174
- constructor(options, logService, jsonFileService, gitService, collectionService) {
2577
+ constructor(coreVersion, options, logService, jsonFileService, gitService, collectionService) {
2175
2578
  super(serviceTypeSchema.enum.Entry, options, logService);
2579
+ this.coreVersion = coreVersion;
2176
2580
  this.jsonFileService = jsonFileService;
2177
2581
  this.gitService = gitService;
2178
2582
  this.collectionService = collectionService;
@@ -2192,11 +2596,12 @@ var EntryService = class extends AbstractCrudService {
2192
2596
  const entryFile = {
2193
2597
  objectType: "entry",
2194
2598
  id,
2599
+ coreVersion: this.coreVersion,
2195
2600
  values: props.values,
2196
2601
  created: datetime(),
2197
2602
  updated: null
2198
2603
  };
2199
- const entry = await this.toEntry(props.projectId, props.collectionId, entryFile);
2604
+ const entry = this.toEntry(entryFile);
2200
2605
  getCreateEntrySchemaFromFieldDefinitions(collection.fieldDefinitions).parse(props);
2201
2606
  await this.jsonFileService.create(entryFile, entryFilePath, entryFileSchema);
2202
2607
  await this.gitService.add(projectPath, [entryFilePath]);
@@ -2219,13 +2624,20 @@ var EntryService = class extends AbstractCrudService {
2219
2624
  readEntrySchema.parse(props);
2220
2625
  if (!props.commitHash) {
2221
2626
  const entryFile = await this.jsonFileService.read(pathTo.entryFile(props.projectId, props.collectionId, props.id), entryFileSchema);
2222
- return this.toEntry(props.projectId, props.collectionId, entryFile);
2627
+ return this.toEntry(entryFile);
2223
2628
  } else {
2224
2629
  const entryFile = this.migrate(JSON.parse(await this.gitService.getFileContentAtCommit(pathTo.project(props.projectId), pathTo.entryFile(props.projectId, props.collectionId, props.id), props.commitHash)));
2225
- return this.toEntry(props.projectId, props.collectionId, entryFile);
2630
+ return this.toEntry(entryFile);
2226
2631
  }
2227
2632
  }
2228
2633
  /**
2634
+ * Returns the commit history of an Entry
2635
+ */
2636
+ async history(props) {
2637
+ entryHistorySchema.parse(props);
2638
+ return this.gitService.log(pathTo.project(props.projectId), { filePath: pathTo.entryFile(props.projectId, props.collectionId, props.id) });
2639
+ }
2640
+ /**
2229
2641
  * Updates an Entry of given Collection with new Values and shared Values
2230
2642
  */
2231
2643
  async update(props) {
@@ -2245,7 +2657,7 @@ var EntryService = class extends AbstractCrudService {
2245
2657
  values: props.values,
2246
2658
  updated: datetime()
2247
2659
  };
2248
- const entry = await this.toEntry(props.projectId, props.collectionId, entryFile);
2660
+ const entry = this.toEntry(entryFile);
2249
2661
  getUpdateEntrySchemaFromFieldDefinitions(collection.fieldDefinitions).parse(props);
2250
2662
  await this.jsonFileService.update(entryFile, entryFilePath, entryFileSchema);
2251
2663
  await this.gitService.add(projectPath, [entryFilePath]);
@@ -2311,17 +2723,14 @@ var EntryService = class extends AbstractCrudService {
2311
2723
  * Migrates an potentially outdated Entry file to the current schema
2312
2724
  */
2313
2725
  migrate(potentiallyOutdatedEntryFile) {
2314
- return entryFileSchema.parse(potentiallyOutdatedEntryFile);
2726
+ const migrated = applyMigrations(migrateEntrySchema.parse(potentiallyOutdatedEntryFile), entryMigrations, this.coreVersion);
2727
+ return entryFileSchema.parse(migrated);
2315
2728
  }
2316
2729
  /**
2317
2730
  * Creates an Entry from given EntryFile by resolving it's Values
2318
2731
  */
2319
- async toEntry(projectId, collectionId, entryFile) {
2320
- const history = await this.gitService.log(pathTo.project(projectId), { filePath: pathTo.entryFile(projectId, collectionId, entryFile.id) });
2321
- return {
2322
- ...entryFile,
2323
- history
2324
- };
2732
+ toEntry(entryFile) {
2733
+ return { ...entryFile };
2325
2734
  }
2326
2735
  };
2327
2736
 
@@ -2350,10 +2759,11 @@ var GitTagService = class extends AbstractCrudService {
2350
2759
  id
2351
2760
  ];
2352
2761
  if (props.hash) args = [...args, props.hash];
2762
+ const fullMessage = `${this.serializeTagMessage(props.message)}\n\n${this.tagMessageToTrailers(props.message).join("\n")}`;
2353
2763
  args = [
2354
2764
  ...args,
2355
2765
  "-m",
2356
- props.message
2766
+ fullMessage
2357
2767
  ];
2358
2768
  await this.git(props.path, args);
2359
2769
  return await this.read({
@@ -2412,27 +2822,33 @@ var GitTagService = class extends AbstractCrudService {
2412
2822
  async list(props) {
2413
2823
  listGitTagsSchema.parse(props);
2414
2824
  let args = ["tag", "--list"];
2825
+ const format = [
2826
+ "%(refname:short)",
2827
+ "%(trailers:key=Type,valueonly)",
2828
+ "%(trailers:key=Version,valueonly)",
2829
+ "%(trailers:key=Core-Version,valueonly)",
2830
+ "%(*authorname)",
2831
+ "%(*authoremail)",
2832
+ "%(*authordate:iso-strict)"
2833
+ ].join("|");
2415
2834
  args = [
2416
2835
  ...args,
2417
2836
  "--sort=-*authordate",
2418
- "--format=%(refname:short)|%(subject)|%(*authorname)|%(*authoremail)|%(*authordate:iso-strict)"
2837
+ `--format=${format}`
2419
2838
  ];
2420
- const gitTags = (await this.git(props.path, args)).stdout.split("\n").filter((line) => {
2839
+ const gitTags = (await this.git(props.path, args)).stdout.replace(/\n\|/g, "|").split("\n").filter((line) => {
2421
2840
  return line.trim() !== "";
2422
2841
  }).map((line) => {
2423
2842
  const lineArray = line.split("|");
2424
- if (lineArray[3]?.startsWith("<") && lineArray[3]?.endsWith(">")) {
2425
- lineArray[3] = lineArray[3].slice(1, -1);
2426
- lineArray[3] = lineArray[3].slice(0, -1);
2427
- }
2843
+ if (lineArray[5]?.startsWith("<") && lineArray[5]?.endsWith(">")) lineArray[5] = lineArray[5].slice(1, -1);
2428
2844
  return {
2429
2845
  id: lineArray[0],
2430
- message: lineArray[1],
2846
+ message: this.parseTagTrailers(lineArray[1]?.trim(), lineArray[2]?.trim(), lineArray[3]?.trim()),
2431
2847
  author: {
2432
- name: lineArray[2],
2433
- email: lineArray[3]
2848
+ name: lineArray[4],
2849
+ email: lineArray[5]
2434
2850
  },
2435
- datetime: datetime(lineArray[4])
2851
+ datetime: datetime(lineArray[6])
2436
2852
  };
2437
2853
  }).filter(this.isGitTag.bind(this));
2438
2854
  return {
@@ -2455,6 +2871,43 @@ var GitTagService = class extends AbstractCrudService {
2455
2871
  return (await this.list({ path: props.path })).total;
2456
2872
  }
2457
2873
  /**
2874
+ * Serializes a GitTagMessage into a human-readable subject line
2875
+ */
2876
+ serializeTagMessage(message) {
2877
+ return `${message.type.charAt(0).toUpperCase() + message.type.slice(1)} ${message.type === "upgrade" ? message.coreVersion : message.version}`;
2878
+ }
2879
+ /**
2880
+ * Converts a GitTagMessage into git trailer lines
2881
+ */
2882
+ tagMessageToTrailers(message) {
2883
+ const trailers = [`Type: ${message.type}`];
2884
+ if (message.type === "upgrade") trailers.push(`Core-Version: ${message.coreVersion}`);
2885
+ else trailers.push(`Version: ${message.version}`);
2886
+ return trailers;
2887
+ }
2888
+ /**
2889
+ * Parses git trailer values back into a GitTagMessage
2890
+ */
2891
+ parseTagTrailers(type, version, coreVersion) {
2892
+ switch (type) {
2893
+ case "upgrade": return gitTagMessageSchema.parse({
2894
+ type,
2895
+ coreVersion
2896
+ });
2897
+ case "release":
2898
+ case "preview": return gitTagMessageSchema.parse({
2899
+ type,
2900
+ version
2901
+ });
2902
+ default:
2903
+ this.logService.warn({
2904
+ source: "core",
2905
+ message: `Tag with ID "${type}" has an invalid or missing Type trailer and will be ignored`
2906
+ });
2907
+ return null;
2908
+ }
2909
+ }
2910
+ /**
2458
2911
  * Type guard for GitTag
2459
2912
  *
2460
2913
  * @param obj The object to check
@@ -2744,9 +3197,16 @@ var GitService = class {
2744
3197
  gitMessageSchema.parse(message);
2745
3198
  const user = await this.userService.get();
2746
3199
  if (!user) throw new NoCurrentUserError();
3200
+ const subject = `${message.method.charAt(0).toUpperCase() + message.method.slice(1)} ${message.reference.objectType} ${message.reference.id}`;
3201
+ const trailers = [
3202
+ `Method: ${message.method}`,
3203
+ `Object-Type: ${message.reference.objectType}`,
3204
+ `Object-Id: ${message.reference.id}`
3205
+ ];
3206
+ if (message.reference.collectionId) trailers.push(`Collection-Id: ${message.reference.collectionId}`);
2747
3207
  const args = [
2748
3208
  "commit",
2749
- `--message=${JSON.stringify(message)}`,
3209
+ `--message=${`${subject}\n\n${trailers.join("\n")}`}`,
2750
3210
  `--author=${user.name} <${user.email}>`
2751
3211
  ];
2752
3212
  await this.git(path, args);
@@ -2766,33 +3226,52 @@ var GitService = class {
2766
3226
  let args = ["log"];
2767
3227
  if (options?.between?.from) args = [...args, `${options.between.from}..${options.between.to || "HEAD"}`];
2768
3228
  if (options?.limit) args = [...args, `--max-count=${options.limit}`];
2769
- args = [...args, "--format=%H|%s|%an|%ae|%aI|%D"];
3229
+ const format = [
3230
+ "%H",
3231
+ "%(trailers:key=Method,valueonly)",
3232
+ "%(trailers:key=Object-Type,valueonly)",
3233
+ "%(trailers:key=Object-Id,valueonly)",
3234
+ "%(trailers:key=Collection-Id,valueonly)",
3235
+ "%an",
3236
+ "%ae",
3237
+ "%aI",
3238
+ "%D"
3239
+ ].join("|");
3240
+ args = [...args, `--format=${format}`];
2770
3241
  if (options?.filePath) args = [
2771
3242
  ...args,
2772
3243
  "--",
2773
3244
  options.filePath
2774
3245
  ];
2775
- const noEmptyLinesArr = (await this.git(path, args)).stdout.split("\n").filter((line) => {
3246
+ const noEmptyLinesArr = (await this.git(path, args)).stdout.replace(/\n\|/g, "|").split("\n").filter((line) => {
2776
3247
  return line.trim() !== "";
2777
3248
  });
2778
3249
  return (await Promise.all(noEmptyLinesArr.map(async (line) => {
2779
3250
  const lineArray = line.split("|");
2780
- const tagId = this.refNameToTagName(lineArray[5] || "");
3251
+ const tagId = this.refNameToTagName(lineArray[8]?.trim() || "");
2781
3252
  const tag = tagId ? await this.tags.read({
2782
3253
  path,
2783
3254
  id: tagId
2784
3255
  }) : null;
3256
+ const collectionId = lineArray[4]?.trim();
2785
3257
  return {
2786
3258
  hash: lineArray[0],
2787
- message: JSON.parse(lineArray[1] || ""),
3259
+ message: {
3260
+ method: lineArray[1]?.trim(),
3261
+ reference: {
3262
+ objectType: lineArray[2]?.trim(),
3263
+ id: lineArray[3]?.trim(),
3264
+ ...collectionId ? { collectionId } : {}
3265
+ }
3266
+ },
2788
3267
  author: {
2789
- name: lineArray[2],
2790
- email: lineArray[3]
3268
+ name: lineArray[5],
3269
+ email: lineArray[6]
2791
3270
  },
2792
- datetime: datetime(lineArray[4]),
3271
+ datetime: datetime(lineArray[7]),
2793
3272
  tag
2794
3273
  };
2795
- }))).filter(this.isGitCommit.bind(this));
3274
+ }))).filter((obj) => this.isGitCommit(obj));
2796
3275
  }
2797
3276
  /**
2798
3277
  * Retrieves the content of a file at a specific commit
@@ -2806,6 +3285,35 @@ var GitService = class {
2806
3285
  };
2807
3286
  return (await this.git(path, args, { processCallback: setEncoding })).stdout;
2808
3287
  }
3288
+ /**
3289
+ * Lists directory entries at a specific commit
3290
+ *
3291
+ * Useful for discovering what files/folders existed at a past commit,
3292
+ * e.g. to detect deleted collections when comparing branches.
3293
+ *
3294
+ * @see https://git-scm.com/docs/git-ls-tree
3295
+ *
3296
+ * @param path Path to the repository
3297
+ * @param treePath Relative path within the repository to list
3298
+ * @param commitRef Commit hash, branch name, or other git ref
3299
+ */
3300
+ async listTreeAtCommit(path, treePath, commitRef) {
3301
+ const args = [
3302
+ "ls-tree",
3303
+ "--name-only",
3304
+ commitRef,
3305
+ `${treePath.replace(`${path}${Path.sep}`, "").split("\\").join("/")}/`
3306
+ ];
3307
+ try {
3308
+ return (await this.git(path, args)).stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").map((entry) => {
3309
+ const parts = entry.split("/");
3310
+ return parts[parts.length - 1] || entry;
3311
+ });
3312
+ } catch (error) {
3313
+ if (error instanceof GitError) return [];
3314
+ throw error;
3315
+ }
3316
+ }
2809
3317
  refNameToTagName(refName) {
2810
3318
  const tagName = refName.replace("tag: ", "").trim();
2811
3319
  if (tagName === "" || uuidSchema.safeParse(tagName).success === false) return null;
@@ -3174,7 +3682,6 @@ var ProjectService = class extends AbstractCrudService {
3174
3682
  created: datetime(),
3175
3683
  updated: null,
3176
3684
  coreVersion: this.coreVersion,
3177
- status: "todo",
3178
3685
  version: "0.0.1"
3179
3686
  };
3180
3687
  const projectPath = pathTo.project(id);
@@ -3228,11 +3735,23 @@ var ProjectService = class extends AbstractCrudService {
3228
3735
  const projectFile = await this.jsonFileService.read(pathTo.projectFile(props.id), projectFileSchema);
3229
3736
  return await this.toProject(projectFile);
3230
3737
  } else {
3231
- const projectFile = this.migrate(migrateProjectSchema.parse(JSON.parse(await this.gitService.getFileContentAtCommit(pathTo.project(props.id), pathTo.projectFile(props.id), props.commitHash))));
3738
+ const projectFile = this.migrate(JSON.parse(await this.gitService.getFileContentAtCommit(pathTo.project(props.id), pathTo.projectFile(props.id), props.commitHash)));
3232
3739
  return await this.toProject(projectFile);
3233
3740
  }
3234
3741
  }
3235
3742
  /**
3743
+ * Returns the commit history of a Project
3744
+ */
3745
+ async history(props) {
3746
+ projectHistorySchema.parse(props);
3747
+ const projectPath = pathTo.project(props.id);
3748
+ const fullHistory = await this.gitService.log(projectPath);
3749
+ return {
3750
+ history: await this.gitService.log(projectPath, { filePath: pathTo.projectFile(props.id) }),
3751
+ fullHistory
3752
+ };
3753
+ }
3754
+ /**
3236
3755
  * Updates given Project
3237
3756
  */
3238
3757
  async update(props) {
@@ -3265,7 +3784,7 @@ var ProjectService = class extends AbstractCrudService {
3265
3784
  const projectPath = pathTo.project(props.id);
3266
3785
  const projectFilePath = pathTo.projectFile(props.id);
3267
3786
  if (await this.gitService.branches.current(projectPath) !== projectBranchSchema.enum.work) await this.gitService.branches.switch(projectPath, projectBranchSchema.enum.work);
3268
- const currentProjectFile = migrateProjectSchema.parse(await this.jsonFileService.unsafeRead(projectFilePath));
3787
+ const currentProjectFile = await this.jsonFileService.unsafeRead(projectFilePath);
3269
3788
  if (Semver.gt(currentProjectFile.coreVersion, this.coreVersion)) throw new ProjectUpgradeError(`The Projects Core version "${currentProjectFile.coreVersion}" is higher than the current Core version "${this.coreVersion}".`);
3270
3789
  if (Semver.eq(currentProjectFile.coreVersion, this.coreVersion) && props.force !== true) throw new ProjectUpgradeError(`The Projects Core version "${currentProjectFile.coreVersion}" is already up to date.`);
3271
3790
  const assetReferences = await this.listReferences("asset", props.id);
@@ -3302,7 +3821,10 @@ var ProjectService = class extends AbstractCrudService {
3302
3821
  });
3303
3822
  await this.gitService.tags.create({
3304
3823
  path: projectPath,
3305
- message: `Upgraded Project to Core version ${migratedProjectFile.coreVersion}`
3824
+ message: {
3825
+ type: "upgrade",
3826
+ coreVersion: migratedProjectFile.coreVersion
3827
+ }
3306
3828
  });
3307
3829
  await this.gitService.branches.delete(projectPath, upgradeBranchName, true);
3308
3830
  this.logService.info({
@@ -3408,7 +3930,7 @@ var ProjectService = class extends AbstractCrudService {
3408
3930
  return (await Promise.all(projectReferences.map(async (reference) => {
3409
3931
  const json = await this.jsonFileService.unsafeRead(pathTo.projectFile(reference.id));
3410
3932
  const projectFile = migrateProjectSchema.parse(json);
3411
- if (projectFile.coreVersion !== this.coreVersion) return projectFile;
3933
+ if (projectFile.coreVersion !== this.coreVersion) return this.migrate(projectFile);
3412
3934
  return null;
3413
3935
  }))).filter(isNotEmpty);
3414
3936
  }
@@ -3440,9 +3962,9 @@ var ProjectService = class extends AbstractCrudService {
3440
3962
  /**
3441
3963
  * Migrates an potentially outdated Project file to the current schema
3442
3964
  */
3443
- migrate(props) {
3444
- props.coreVersion = this.coreVersion;
3445
- return projectFileSchema.parse(props);
3965
+ migrate(potentiallyOutdatedFile) {
3966
+ const migrated = applyMigrations(migrateProjectSchema.parse(potentiallyOutdatedFile), projectMigrations, this.coreVersion);
3967
+ return projectFileSchema.parse(migrated);
3446
3968
  }
3447
3969
  /**
3448
3970
  * Creates a Project from given ProjectFile
@@ -3451,13 +3973,9 @@ var ProjectService = class extends AbstractCrudService {
3451
3973
  const projectPath = pathTo.project(projectFile.id);
3452
3974
  let remoteOriginUrl = null;
3453
3975
  if (await this.gitService.remotes.hasOrigin(projectPath)) remoteOriginUrl = await this.gitService.remotes.getOriginUrl(projectPath);
3454
- const fullHistory = await this.gitService.log(pathTo.project(projectFile.id));
3455
- const history = await this.gitService.log(pathTo.project(projectFile.id), { filePath: pathTo.projectFile(projectFile.id) });
3456
3976
  return {
3457
3977
  ...projectFile,
3458
- remoteOriginUrl,
3459
- history,
3460
- fullHistory
3978
+ remoteOriginUrl
3461
3979
  };
3462
3980
  }
3463
3981
  /**
@@ -3487,7 +4005,8 @@ var ProjectService = class extends AbstractCrudService {
3487
4005
  "!/.gitattributes",
3488
4006
  "!/**/.gitkeep",
3489
4007
  "",
3490
- "# elek.io related ignores"
4008
+ "# elek.io related ignores",
4009
+ "collections/index.json"
3491
4010
  ].join(Os.EOL));
3492
4011
  }
3493
4012
  async upgradeObjectFile(projectId, objectType, reference, collectionId) {
@@ -3553,6 +4072,665 @@ var ProjectService = class extends AbstractCrudService {
3553
4072
  }
3554
4073
  };
3555
4074
 
4075
+ //#endregion
4076
+ //#region src/service/ReleaseService.ts
4077
+ /**
4078
+ * Service that manages Release functionality
4079
+ *
4080
+ * A release diffs the current `work` branch against the `production` branch
4081
+ * to determine what changed, computes a semver bump, and merges work into production.
4082
+ */
4083
+ var ReleaseService = class extends AbstractCrudService {
4084
+ gitService;
4085
+ jsonFileService;
4086
+ projectService;
4087
+ constructor(options, logService, gitService, jsonFileService, projectService) {
4088
+ super(serviceTypeSchema.enum.Release, options, logService);
4089
+ this.gitService = gitService;
4090
+ this.jsonFileService = jsonFileService;
4091
+ this.projectService = projectService;
4092
+ }
4093
+ /**
4094
+ * Prepares a release by diffing the current `work` branch against `production`.
4095
+ *
4096
+ * Returns a read-only summary of all changes and the computed next version.
4097
+ * If there are no changes, the next version and bump will be null.
4098
+ */
4099
+ async prepare(props) {
4100
+ prepareReleaseSchema.parse(props);
4101
+ const projectPath = pathTo.project(props.projectId);
4102
+ const currentBranch = await this.gitService.branches.current(projectPath);
4103
+ if (currentBranch !== projectBranchSchema.enum.work) throw new Error(`Not on work branch (currently on "${currentBranch}")`);
4104
+ const project = await this.projectService.read({ id: props.projectId });
4105
+ const currentVersion = project.version;
4106
+ const productionRef = projectBranchSchema.enum.production;
4107
+ const productionProject = await this.getProjectAtRef(props.projectId, projectPath, productionRef);
4108
+ const projectDiff = this.diffProject(project, productionProject);
4109
+ const currentCollections = await this.getCollectionsAtRef(props.projectId, projectPath, projectBranchSchema.enum.work);
4110
+ const productionCollections = await this.getCollectionsAtRef(props.projectId, projectPath, productionRef);
4111
+ const collectionDiff = this.diffCollections(currentCollections, productionCollections);
4112
+ const currentAssets = await this.getAssetsAtRef(props.projectId, projectPath, projectBranchSchema.enum.work);
4113
+ const productionAssets = await this.getAssetsAtRef(props.projectId, projectPath, productionRef);
4114
+ const assetDiff = this.diffAssets(currentAssets, productionAssets);
4115
+ const allCollectionIds = new Set([...currentCollections.map((c) => c.id), ...productionCollections.map((c) => c.id)]);
4116
+ const entryDiff = await this.diffEntries(props.projectId, projectPath, allCollectionIds, productionRef);
4117
+ let finalBump = null;
4118
+ for (const bump of [
4119
+ projectDiff.bump,
4120
+ collectionDiff.bump,
4121
+ assetDiff.bump,
4122
+ entryDiff.bump
4123
+ ]) if (bump) finalBump = finalBump ? this.higherBump(finalBump, bump) : bump;
4124
+ if (!finalBump) {
4125
+ if (await this.hasCommitsBetween(projectPath, productionRef, projectBranchSchema.enum.work)) finalBump = "patch";
4126
+ }
4127
+ const nextVersion = finalBump ? Semver.inc(currentVersion, finalBump) : null;
4128
+ return {
4129
+ project,
4130
+ bump: finalBump,
4131
+ currentVersion,
4132
+ nextVersion,
4133
+ projectChanges: projectDiff.projectChanges,
4134
+ collectionChanges: collectionDiff.collectionChanges,
4135
+ fieldChanges: collectionDiff.fieldChanges,
4136
+ assetChanges: assetDiff.assetChanges,
4137
+ entryChanges: entryDiff.entryChanges
4138
+ };
4139
+ }
4140
+ /**
4141
+ * Creates a release by:
4142
+ * 1. Recomputing the diff (stateless)
4143
+ * 2. Merging `work` into `production`
4144
+ * 3. Updating the project version on `production`
4145
+ * 4. Tagging on `production`
4146
+ * 5. Merging `production` back into `work` (fast-forward to sync the version commit)
4147
+ * 6. Switching back to `work`
4148
+ */
4149
+ async create(props) {
4150
+ createReleaseSchema.parse(props);
4151
+ const projectPath = pathTo.project(props.projectId);
4152
+ const projectFilePath = pathTo.projectFile(props.projectId);
4153
+ const diff = await this.prepare(props);
4154
+ if (!diff.bump || !diff.nextVersion) throw new Error("Cannot create a release: no changes detected since the last full release");
4155
+ const nextVersion = diff.nextVersion;
4156
+ try {
4157
+ await this.gitService.branches.switch(projectPath, projectBranchSchema.enum.production);
4158
+ await this.gitService.merge(projectPath, projectBranchSchema.enum.work);
4159
+ const updatedProjectFile = {
4160
+ ...diff.project,
4161
+ version: nextVersion,
4162
+ updated: datetime()
4163
+ };
4164
+ await this.jsonFileService.update(updatedProjectFile, projectFilePath, projectFileSchema);
4165
+ await this.gitService.add(projectPath, [projectFilePath]);
4166
+ await this.gitService.commit(projectPath, {
4167
+ method: "release",
4168
+ reference: {
4169
+ objectType: "project",
4170
+ id: props.projectId
4171
+ }
4172
+ });
4173
+ await this.gitService.tags.create({
4174
+ path: projectPath,
4175
+ message: {
4176
+ type: "release",
4177
+ version: nextVersion
4178
+ }
4179
+ });
4180
+ await this.gitService.branches.switch(projectPath, projectBranchSchema.enum.work);
4181
+ await this.gitService.merge(projectPath, projectBranchSchema.enum.production);
4182
+ } catch (error) {
4183
+ await this.gitService.branches.switch(projectPath, projectBranchSchema.enum.work).catch(() => {});
4184
+ throw error;
4185
+ }
4186
+ this.logService.info({
4187
+ source: "core",
4188
+ message: `Released version ${nextVersion} (${diff.bump} bump)`
4189
+ });
4190
+ return {
4191
+ version: nextVersion,
4192
+ diff
4193
+ };
4194
+ }
4195
+ /**
4196
+ * Creates a preview release by:
4197
+ * 1. Recomputing the diff (stateless)
4198
+ * 2. Computing the preview version (e.g. 1.1.0-preview.3)
4199
+ * 3. Updating the project version on `work`
4200
+ * 4. Tagging on `work` (no merge into production)
4201
+ *
4202
+ * Preview releases are snapshots of the current work state.
4203
+ * They don't promote to production — only full releases do.
4204
+ */
4205
+ async createPreview(props) {
4206
+ createPreviewReleaseSchema.parse(props);
4207
+ const projectPath = pathTo.project(props.projectId);
4208
+ const projectFilePath = pathTo.projectFile(props.projectId);
4209
+ const diff = await this.prepare(props);
4210
+ if (!diff.bump || !diff.nextVersion) throw new Error("Cannot create a preview release: no changes detected since the last full release");
4211
+ const previewNumber = await this.countPreviewsSinceLastRelease(projectPath, diff.nextVersion);
4212
+ const previewVersion = `${diff.nextVersion}-preview.${previewNumber + 1}`;
4213
+ try {
4214
+ const updatedProjectFile = {
4215
+ ...diff.project,
4216
+ version: previewVersion,
4217
+ updated: datetime()
4218
+ };
4219
+ await this.jsonFileService.update(updatedProjectFile, projectFilePath, projectFileSchema);
4220
+ await this.gitService.add(projectPath, [projectFilePath]);
4221
+ await this.gitService.commit(projectPath, {
4222
+ method: "release",
4223
+ reference: {
4224
+ objectType: "project",
4225
+ id: props.projectId
4226
+ }
4227
+ });
4228
+ await this.gitService.tags.create({
4229
+ path: projectPath,
4230
+ message: {
4231
+ type: "preview",
4232
+ version: previewVersion
4233
+ }
4234
+ });
4235
+ } catch (error) {
4236
+ await this.gitService.branches.switch(projectPath, projectBranchSchema.enum.work).catch(() => {});
4237
+ throw error;
4238
+ }
4239
+ this.logService.info({
4240
+ source: "core",
4241
+ message: `Preview released version ${previewVersion} (${diff.bump} bump)`
4242
+ });
4243
+ return {
4244
+ version: previewVersion,
4245
+ diff
4246
+ };
4247
+ }
4248
+ /**
4249
+ * Reads the project file as it exists at a given git ref
4250
+ */
4251
+ async getProjectAtRef(projectId, projectPath, ref) {
4252
+ try {
4253
+ const content = await this.gitService.getFileContentAtCommit(projectPath, pathTo.projectFile(projectId), ref);
4254
+ return projectFileSchema.parse(JSON.parse(content));
4255
+ } catch {
4256
+ return null;
4257
+ }
4258
+ }
4259
+ /**
4260
+ * Reads asset metadata files as they exist at a given git ref
4261
+ */
4262
+ async getAssetsAtRef(projectId, projectPath, ref) {
4263
+ const assetsPath = pathTo.assets(projectId);
4264
+ const fileNames = await this.gitService.listTreeAtCommit(projectPath, assetsPath, ref);
4265
+ const assets = [];
4266
+ for (const fileName of fileNames) {
4267
+ if (!fileName.endsWith(".json")) continue;
4268
+ const assetId = fileName.replace(".json", "");
4269
+ const assetFilePath = pathTo.assetFile(projectId, assetId);
4270
+ try {
4271
+ const content = await this.gitService.getFileContentAtCommit(projectPath, assetFilePath, ref);
4272
+ const assetFile = assetFileSchema.parse(JSON.parse(content));
4273
+ assets.push(assetFile);
4274
+ } catch {
4275
+ this.logService.debug({
4276
+ source: "core",
4277
+ message: `Skipping asset "${fileName}" at ref "${ref}" during release diff`
4278
+ });
4279
+ }
4280
+ }
4281
+ return assets;
4282
+ }
4283
+ /**
4284
+ * Reads entry files for a single collection as they exist at a given git ref
4285
+ */
4286
+ async getEntriesAtRef(projectId, projectPath, collectionId, ref) {
4287
+ const entriesPath = pathTo.entries(projectId, collectionId);
4288
+ const fileNames = await this.gitService.listTreeAtCommit(projectPath, entriesPath, ref);
4289
+ const entries = [];
4290
+ for (const fileName of fileNames) {
4291
+ if (!fileName.endsWith(".json") || fileName === "collection.json") continue;
4292
+ const entryId = fileName.replace(".json", "");
4293
+ const entryFilePath = pathTo.entryFile(projectId, collectionId, entryId);
4294
+ try {
4295
+ const content = await this.gitService.getFileContentAtCommit(projectPath, entryFilePath, ref);
4296
+ const entryFile = entryFileSchema.parse(JSON.parse(content));
4297
+ entries.push(entryFile);
4298
+ } catch {
4299
+ this.logService.debug({
4300
+ source: "core",
4301
+ message: `Skipping entry "${fileName}" in collection "${collectionId}" at ref "${ref}" during release diff`
4302
+ });
4303
+ }
4304
+ }
4305
+ return entries;
4306
+ }
4307
+ /**
4308
+ * Reads collections as they exist at a given git ref (branch or commit)
4309
+ */
4310
+ async getCollectionsAtRef(projectId, projectPath, ref) {
4311
+ const collectionsPath = pathTo.collections(projectId);
4312
+ const folderNames = await this.gitService.listTreeAtCommit(projectPath, collectionsPath, ref);
4313
+ const collections = [];
4314
+ for (const folderName of folderNames) {
4315
+ const collectionFilePath = pathTo.collectionFile(projectId, folderName);
4316
+ try {
4317
+ const content = await this.gitService.getFileContentAtCommit(projectPath, collectionFilePath, ref);
4318
+ const collectionFile = collectionFileSchema.parse(JSON.parse(content));
4319
+ collections.push(collectionFile);
4320
+ } catch {
4321
+ this.logService.debug({
4322
+ source: "core",
4323
+ message: `Skipping folder "${folderName}" at ref "${ref}" during release diff`
4324
+ });
4325
+ }
4326
+ }
4327
+ return collections;
4328
+ }
4329
+ /**
4330
+ * Checks if there are any commits between two refs
4331
+ */
4332
+ async hasCommitsBetween(projectPath, from, to) {
4333
+ try {
4334
+ return (await this.gitService.log(projectPath, { between: {
4335
+ from,
4336
+ to
4337
+ } })).length > 0;
4338
+ } catch {
4339
+ return true;
4340
+ }
4341
+ }
4342
+ /**
4343
+ * Diffs two sets of collections and returns all changes with the computed bump level.
4344
+ *
4345
+ * Always collects all changes so they can be displayed to the user.
4346
+ */
4347
+ diffCollections(currentCollections, productionCollections) {
4348
+ const collectionChanges = [];
4349
+ const fieldChanges = [];
4350
+ let highestBump = null;
4351
+ const currentById = new Map(currentCollections.map((c) => [c.id, c]));
4352
+ const productionById = new Map(productionCollections.map((c) => [c.id, c]));
4353
+ for (const [id] of productionById) if (!currentById.has(id)) {
4354
+ collectionChanges.push({
4355
+ collectionId: id,
4356
+ changeType: "deleted",
4357
+ bump: "major"
4358
+ });
4359
+ highestBump = "major";
4360
+ }
4361
+ for (const [id] of currentById) if (!productionById.has(id)) {
4362
+ collectionChanges.push({
4363
+ collectionId: id,
4364
+ changeType: "added",
4365
+ bump: "minor"
4366
+ });
4367
+ highestBump = this.higherBump(highestBump, "minor");
4368
+ }
4369
+ for (const [id, currentCollection] of currentById) {
4370
+ const productionCollection = productionById.get(id);
4371
+ if (!productionCollection) continue;
4372
+ const changes = this.diffFieldDefinitions(id, currentCollection.fieldDefinitions, productionCollection.fieldDefinitions);
4373
+ fieldChanges.push(...changes);
4374
+ for (const change of changes) highestBump = this.higherBump(highestBump, change.bump);
4375
+ }
4376
+ return {
4377
+ bump: highestBump,
4378
+ collectionChanges,
4379
+ fieldChanges
4380
+ };
4381
+ }
4382
+ /**
4383
+ * Diffs the project file between current and production.
4384
+ *
4385
+ * Skips immutable/system-managed fields (id, objectType, created, updated, version, coreVersion).
4386
+ */
4387
+ diffProject(current, production) {
4388
+ const projectChanges = [];
4389
+ if (!production) return {
4390
+ bump: null,
4391
+ projectChanges
4392
+ };
4393
+ let highestBump = null;
4394
+ if (current.settings.language.default !== production.settings.language.default) {
4395
+ projectChanges.push({
4396
+ changeType: "defaultLanguageChanged",
4397
+ bump: "major"
4398
+ });
4399
+ highestBump = "major";
4400
+ }
4401
+ const currentSupported = new Set(current.settings.language.supported);
4402
+ const productionSupported = new Set(production.settings.language.supported);
4403
+ for (const lang of productionSupported) if (!currentSupported.has(lang)) {
4404
+ projectChanges.push({
4405
+ changeType: "supportedLanguageRemoved",
4406
+ bump: "major"
4407
+ });
4408
+ highestBump = "major";
4409
+ break;
4410
+ }
4411
+ for (const lang of currentSupported) if (!productionSupported.has(lang)) {
4412
+ projectChanges.push({
4413
+ changeType: "supportedLanguageAdded",
4414
+ bump: "minor"
4415
+ });
4416
+ highestBump = this.higherBump(highestBump, "minor");
4417
+ break;
4418
+ }
4419
+ if (current.name !== production.name) {
4420
+ projectChanges.push({
4421
+ changeType: "nameChanged",
4422
+ bump: "patch"
4423
+ });
4424
+ highestBump = this.higherBump(highestBump, "patch");
4425
+ }
4426
+ if (current.description !== production.description) {
4427
+ projectChanges.push({
4428
+ changeType: "descriptionChanged",
4429
+ bump: "patch"
4430
+ });
4431
+ highestBump = this.higherBump(highestBump, "patch");
4432
+ }
4433
+ return {
4434
+ bump: highestBump,
4435
+ projectChanges
4436
+ };
4437
+ }
4438
+ /**
4439
+ * Diffs two sets of assets and returns all changes with the computed bump level.
4440
+ */
4441
+ diffAssets(currentAssets, productionAssets) {
4442
+ const assetChanges = [];
4443
+ let highestBump = null;
4444
+ const currentById = new Map(currentAssets.map((a) => [a.id, a]));
4445
+ const productionById = new Map(productionAssets.map((a) => [a.id, a]));
4446
+ for (const [id] of productionById) if (!currentById.has(id)) {
4447
+ assetChanges.push({
4448
+ assetId: id,
4449
+ changeType: "deleted",
4450
+ bump: "major"
4451
+ });
4452
+ highestBump = "major";
4453
+ }
4454
+ for (const [id] of currentById) if (!productionById.has(id)) {
4455
+ assetChanges.push({
4456
+ assetId: id,
4457
+ changeType: "added",
4458
+ bump: "minor"
4459
+ });
4460
+ highestBump = this.higherBump(highestBump, "minor");
4461
+ }
4462
+ for (const [id, current] of currentById) {
4463
+ const production = productionById.get(id);
4464
+ if (!production) continue;
4465
+ if (current.extension !== production.extension || current.mimeType !== production.mimeType || current.size !== production.size) {
4466
+ assetChanges.push({
4467
+ assetId: id,
4468
+ changeType: "binaryChanged",
4469
+ bump: "patch"
4470
+ });
4471
+ highestBump = this.higherBump(highestBump, "patch");
4472
+ }
4473
+ if (current.name !== production.name || current.description !== production.description) {
4474
+ assetChanges.push({
4475
+ assetId: id,
4476
+ changeType: "metadataChanged",
4477
+ bump: "patch"
4478
+ });
4479
+ highestBump = this.higherBump(highestBump, "patch");
4480
+ }
4481
+ }
4482
+ return {
4483
+ bump: highestBump,
4484
+ assetChanges
4485
+ };
4486
+ }
4487
+ /**
4488
+ * Diffs entries across all collections between current and production.
4489
+ */
4490
+ async diffEntries(projectId, projectPath, allCollectionIds, productionRef) {
4491
+ const entryChanges = [];
4492
+ let highestBump = null;
4493
+ for (const collectionId of allCollectionIds) {
4494
+ const currentEntries = await this.getEntriesAtRef(projectId, projectPath, collectionId, projectBranchSchema.enum.work);
4495
+ const productionEntries = await this.getEntriesAtRef(projectId, projectPath, collectionId, productionRef);
4496
+ const currentById = new Map(currentEntries.map((e) => [e.id, e]));
4497
+ const productionById = new Map(productionEntries.map((e) => [e.id, e]));
4498
+ for (const [id] of productionById) if (!currentById.has(id)) {
4499
+ entryChanges.push({
4500
+ collectionId,
4501
+ entryId: id,
4502
+ changeType: "deleted",
4503
+ bump: "major"
4504
+ });
4505
+ highestBump = "major";
4506
+ }
4507
+ for (const [id] of currentById) if (!productionById.has(id)) {
4508
+ entryChanges.push({
4509
+ collectionId,
4510
+ entryId: id,
4511
+ changeType: "added",
4512
+ bump: "minor"
4513
+ });
4514
+ highestBump = this.higherBump(highestBump, "minor");
4515
+ }
4516
+ for (const [id, current] of currentById) {
4517
+ const production = productionById.get(id);
4518
+ if (!production) continue;
4519
+ if (!isDeepStrictEqual(current.values, production.values)) {
4520
+ entryChanges.push({
4521
+ collectionId,
4522
+ entryId: id,
4523
+ changeType: "modified",
4524
+ bump: "patch"
4525
+ });
4526
+ highestBump = this.higherBump(highestBump, "patch");
4527
+ }
4528
+ }
4529
+ }
4530
+ return {
4531
+ bump: highestBump,
4532
+ entryChanges
4533
+ };
4534
+ }
4535
+ /**
4536
+ * Diffs field definitions of a single collection.
4537
+ *
4538
+ * Matches fields by UUID and classifies each change.
4539
+ * Always collects all changes so they can be displayed to the user.
4540
+ */
4541
+ diffFieldDefinitions(collectionId, currentFields, productionFields) {
4542
+ const changes = [];
4543
+ const currentById = new Map(currentFields.map((f) => [f.id, f]));
4544
+ const productionById = new Map(productionFields.map((f) => [f.id, f]));
4545
+ for (const [id, field] of productionById) if (!currentById.has(id)) changes.push({
4546
+ collectionId,
4547
+ fieldId: id,
4548
+ fieldSlug: field.slug,
4549
+ changeType: "deleted",
4550
+ bump: "major"
4551
+ });
4552
+ for (const [id, field] of currentById) if (!productionById.has(id)) changes.push({
4553
+ collectionId,
4554
+ fieldId: id,
4555
+ fieldSlug: field.slug,
4556
+ changeType: "added",
4557
+ bump: "minor"
4558
+ });
4559
+ for (const [id, currentField] of currentById) {
4560
+ const productionField = productionById.get(id);
4561
+ if (!productionField) continue;
4562
+ const fieldChanges = this.diffSingleField(collectionId, currentField, productionField);
4563
+ changes.push(...fieldChanges);
4564
+ }
4565
+ return changes;
4566
+ }
4567
+ /**
4568
+ * Compares two versions of the same field definition and returns all detected changes.
4569
+ *
4570
+ * Collects every change on the field so the full diff can be shown to the user.
4571
+ */
4572
+ diffSingleField(collectionId, current, production) {
4573
+ const changes = [];
4574
+ const base = {
4575
+ collectionId,
4576
+ fieldId: current.id,
4577
+ fieldSlug: current.slug
4578
+ };
4579
+ if (current.valueType !== production.valueType) changes.push({
4580
+ ...base,
4581
+ changeType: "valueTypeChanged",
4582
+ bump: "major"
4583
+ });
4584
+ if (current.fieldType !== production.fieldType) changes.push({
4585
+ ...base,
4586
+ changeType: "fieldTypeChanged",
4587
+ bump: "major"
4588
+ });
4589
+ if (current.slug !== production.slug) changes.push({
4590
+ ...base,
4591
+ changeType: "slugChanged",
4592
+ bump: "major"
4593
+ });
4594
+ if (this.isMinMaxTightened(current, production)) changes.push({
4595
+ ...base,
4596
+ changeType: "minMaxTightened",
4597
+ bump: "major"
4598
+ });
4599
+ if (production.isRequired === true && current.isRequired === false) changes.push({
4600
+ ...base,
4601
+ changeType: "isRequiredToNotRequired",
4602
+ bump: "major"
4603
+ });
4604
+ if (production.isUnique === true && current.isUnique === false) changes.push({
4605
+ ...base,
4606
+ changeType: "isUniqueToNotUnique",
4607
+ bump: "major"
4608
+ });
4609
+ if (current.fieldType === "entry" && production.fieldType === "entry") {
4610
+ if (!isDeepStrictEqual([...current.ofCollections].sort(), [...production.ofCollections].sort())) changes.push({
4611
+ ...base,
4612
+ changeType: "ofCollectionsChanged",
4613
+ bump: "major"
4614
+ });
4615
+ }
4616
+ if (production.isRequired === false && current.isRequired === true) changes.push({
4617
+ ...base,
4618
+ changeType: "isNotRequiredToRequired",
4619
+ bump: "minor"
4620
+ });
4621
+ if (production.isUnique === false && current.isUnique === true) changes.push({
4622
+ ...base,
4623
+ changeType: "isNotUniqueToUnique",
4624
+ bump: "minor"
4625
+ });
4626
+ if (this.isMinMaxLoosened(current, production)) changes.push({
4627
+ ...base,
4628
+ changeType: "minMaxLoosened",
4629
+ bump: "patch"
4630
+ });
4631
+ if (!isDeepStrictEqual(current.label, production.label)) changes.push({
4632
+ ...base,
4633
+ changeType: "labelChanged",
4634
+ bump: "patch"
4635
+ });
4636
+ if (!isDeepStrictEqual(current.description, production.description)) changes.push({
4637
+ ...base,
4638
+ changeType: "descriptionChanged",
4639
+ bump: "patch"
4640
+ });
4641
+ if ("defaultValue" in current && "defaultValue" in production && !isDeepStrictEqual(current.defaultValue, production.defaultValue)) changes.push({
4642
+ ...base,
4643
+ changeType: "defaultValueChanged",
4644
+ bump: "patch"
4645
+ });
4646
+ if (current.inputWidth !== production.inputWidth) changes.push({
4647
+ ...base,
4648
+ changeType: "inputWidthChanged",
4649
+ bump: "patch"
4650
+ });
4651
+ if (current.isDisabled !== production.isDisabled) changes.push({
4652
+ ...base,
4653
+ changeType: "isDisabledChanged",
4654
+ bump: "patch"
4655
+ });
4656
+ return changes;
4657
+ }
4658
+ /**
4659
+ * Checks if min/max constraints have been tightened.
4660
+ *
4661
+ * Tightening means: new min > old min, or new max < old max.
4662
+ * A null value means no constraint (unbounded).
4663
+ */
4664
+ isMinMaxTightened(current, production) {
4665
+ const currentMin = this.getMinMax(current, "min");
4666
+ const productionMin = this.getMinMax(production, "min");
4667
+ const currentMax = this.getMinMax(current, "max");
4668
+ const productionMax = this.getMinMax(production, "max");
4669
+ if (currentMin !== null && productionMin === null) return true;
4670
+ if (currentMin !== null && productionMin !== null && currentMin > productionMin) return true;
4671
+ if (currentMax !== null && productionMax === null) return true;
4672
+ if (currentMax !== null && productionMax !== null && currentMax < productionMax) return true;
4673
+ return false;
4674
+ }
4675
+ /**
4676
+ * Checks if min/max constraints have been loosened.
4677
+ *
4678
+ * Loosening means: new min < old min, or new max > old max.
4679
+ */
4680
+ isMinMaxLoosened(current, production) {
4681
+ const currentMin = this.getMinMax(current, "min");
4682
+ const productionMin = this.getMinMax(production, "min");
4683
+ const currentMax = this.getMinMax(current, "max");
4684
+ const productionMax = this.getMinMax(production, "max");
4685
+ if (currentMin === null && productionMin !== null) return true;
4686
+ if (currentMin !== null && productionMin !== null && currentMin < productionMin) return true;
4687
+ if (currentMax === null && productionMax !== null) return true;
4688
+ if (currentMax !== null && productionMax !== null && currentMax > productionMax) return true;
4689
+ return false;
4690
+ }
4691
+ /**
4692
+ * Safely extracts min or max from a field definition (not all types have it)
4693
+ */
4694
+ getMinMax(field, prop) {
4695
+ switch (field.fieldType) {
4696
+ case "text":
4697
+ case "textarea":
4698
+ case "number":
4699
+ case "range":
4700
+ case "asset":
4701
+ case "entry": return field[prop];
4702
+ default: return null;
4703
+ }
4704
+ }
4705
+ /**
4706
+ * Counts existing preview tags for a given base version since the last full release.
4707
+ */
4708
+ async countPreviewsSinceLastRelease(projectPath, baseVersion) {
4709
+ const tags = await this.gitService.tags.list({ path: projectPath });
4710
+ let count = 0;
4711
+ for (const tag of tags.list) {
4712
+ if (tag.message.type === "upgrade") continue;
4713
+ if (tag.message.type === "release") break;
4714
+ if (tag.message.type === "preview") {
4715
+ if (tag.message.version.split("-")[0] === baseVersion) count++;
4716
+ }
4717
+ }
4718
+ return count;
4719
+ }
4720
+ /**
4721
+ * Returns the higher of two bumps (major > minor > patch)
4722
+ */
4723
+ higherBump(a, b) {
4724
+ const order = {
4725
+ patch: 0,
4726
+ minor: 1,
4727
+ major: 2
4728
+ };
4729
+ if (a === null) return b;
4730
+ return order[a] >= order[b] ? a : b;
4731
+ }
4732
+ };
4733
+
3556
4734
  //#endregion
3557
4735
  //#region src/service/UserService.ts
3558
4736
  /**
@@ -3588,7 +4766,7 @@ var UserService = class {
3588
4766
  setUserSchema.parse(props);
3589
4767
  const userFilePath = pathTo.userFile;
3590
4768
  const userFile = { ...props };
3591
- if (userFile.userType === UserTypeSchema.enum.cloud) {}
4769
+ if (userFile.userType === userTypeSchema.enum.cloud) {}
3592
4770
  await this.jsonFileService.update(userFile, userFilePath, userFileSchema);
3593
4771
  this.logService.debug({
3594
4772
  source: "core",
@@ -3616,6 +4794,7 @@ var ElekIoCore = class {
3616
4794
  projectService;
3617
4795
  collectionService;
3618
4796
  entryService;
4797
+ releaseService;
3619
4798
  localApi;
3620
4799
  constructor(props) {
3621
4800
  this.coreVersion = package_default.version;
@@ -3628,10 +4807,11 @@ var ElekIoCore = class {
3628
4807
  this.jsonFileService = new JsonFileService(this.options, this.logService);
3629
4808
  this.userService = new UserService(this.logService, this.jsonFileService);
3630
4809
  this.gitService = new GitService(this.options, this.logService, this.userService);
3631
- this.assetService = new AssetService(this.options, this.logService, this.jsonFileService, this.gitService);
3632
- this.collectionService = new CollectionService(this.options, this.logService, this.jsonFileService, this.gitService);
3633
- this.entryService = new EntryService(this.options, this.logService, this.jsonFileService, this.gitService, this.collectionService);
4810
+ this.assetService = new AssetService(this.coreVersion, this.options, this.logService, this.jsonFileService, this.gitService);
4811
+ this.collectionService = new CollectionService(this.coreVersion, this.options, this.logService, this.jsonFileService, this.gitService);
4812
+ this.entryService = new EntryService(this.coreVersion, this.options, this.logService, this.jsonFileService, this.gitService, this.collectionService);
3634
4813
  this.projectService = new ProjectService(this.coreVersion, this.options, this.logService, this.jsonFileService, this.gitService, this.assetService, this.collectionService, this.entryService);
4814
+ this.releaseService = new ReleaseService(this.options, this.logService, this.gitService, this.jsonFileService, this.projectService);
3635
4815
  this.localApi = new LocalApi(this.logService, this.projectService, this.collectionService, this.entryService, this.assetService);
3636
4816
  this.logService.info({
3637
4817
  source: "core",
@@ -3691,6 +4871,12 @@ var ElekIoCore = class {
3691
4871
  return this.entryService;
3692
4872
  }
3693
4873
  /**
4874
+ * Prepare and create releases
4875
+ */
4876
+ get releases() {
4877
+ return this.releaseService;
4878
+ }
4879
+ /**
3694
4880
  * Allows starting and stopping a REST API
3695
4881
  * to allow developers to read local Project data
3696
4882
  */
@@ -3705,23 +4891,23 @@ var ElekIoCore = class {
3705
4891
  * Generates a flat Zod object schema from collection field definitions
3706
4892
  * for use with Astro's `parseData` validation.
3707
4893
  *
3708
- * Each key is the field definition ID (UUID) and each value schema
4894
+ * Each key is the field definition slug and each value schema
3709
4895
  * is the translatable content schema for that field type.
3710
4896
  */
3711
4897
  function buildEntryValuesSchema(fieldDefinitions) {
3712
4898
  const shape = {};
3713
4899
  for (const fieldDef of fieldDefinitions) switch (fieldDef.valueType) {
3714
- case ValueTypeSchema.enum.string:
3715
- shape[fieldDef.id] = getTranslatableStringValueContentSchemaFromFieldDefinition(fieldDef);
4900
+ case valueTypeSchema.enum.string:
4901
+ shape[fieldDef.slug] = getTranslatableStringValueContentSchemaFromFieldDefinition(fieldDef);
3716
4902
  break;
3717
- case ValueTypeSchema.enum.number:
3718
- shape[fieldDef.id] = getTranslatableNumberValueContentSchemaFromFieldDefinition(fieldDef);
4903
+ case valueTypeSchema.enum.number:
4904
+ shape[fieldDef.slug] = getTranslatableNumberValueContentSchemaFromFieldDefinition(fieldDef);
3719
4905
  break;
3720
- case ValueTypeSchema.enum.boolean:
3721
- shape[fieldDef.id] = getTranslatableBooleanValueContentSchemaFromFieldDefinition();
4906
+ case valueTypeSchema.enum.boolean:
4907
+ shape[fieldDef.slug] = getTranslatableBooleanValueContentSchemaFromFieldDefinition();
3722
4908
  break;
3723
- case ValueTypeSchema.enum.reference:
3724
- shape[fieldDef.id] = getTranslatableReferenceValueContentSchemaFromFieldDefinition(fieldDef);
4909
+ case valueTypeSchema.enum.reference:
4910
+ shape[fieldDef.slug] = getTranslatableReferenceValueContentSchemaFromFieldDefinition(fieldDef);
3725
4911
  break;
3726
4912
  }
3727
4913
  return z.object(shape);
@@ -3740,7 +4926,7 @@ function buildEntryValuesTypeString(fieldDefinitions) {
3740
4926
  `export type Entry = {`,
3741
4927
  fieldDefinitions.map((fieldDef) => {
3742
4928
  const tsType = valueTypeToTsType(fieldDef.valueType);
3743
- return ` "${fieldDef.id}": Partial<Record<SupportedLanguage, ${tsType}>>`;
4929
+ return ` "${fieldDef.slug}": Partial<Record<SupportedLanguage, ${tsType}>>`;
3744
4930
  }).join(";\n") + ";",
3745
4931
  `};`
3746
4932
  ].join("\n");
@@ -3758,13 +4944,13 @@ function valueTypeToTsType(valueType) {
3758
4944
  //#endregion
3759
4945
  //#region src/astro/transform.ts
3760
4946
  /**
3761
- * Transforms an elek.io Entry's values array into a flat object
3762
- * keyed by field definition ID. Each value's translatable content
4947
+ * Transforms an elek.io Entry's values record into a flat object
4948
+ * keyed by field definition slug. Each value's translatable content
3763
4949
  * is preserved as-is.
3764
4950
  */
3765
4951
  function transformEntryValues(values) {
3766
4952
  const result = {};
3767
- for (const value of values) result[value.fieldDefinitionId] = value.content;
4953
+ for (const [slug, value] of Object.entries(values)) result[slug] = value.content;
3768
4954
  return result;
3769
4955
  }
3770
4956
 
@@ -3856,9 +5042,13 @@ function elekEntries(props) {
3856
5042
  return {
3857
5043
  name: "elek-entries",
3858
5044
  createSchema: async () => {
5045
+ const resolvedId = await core.collections.resolveCollectionId({
5046
+ projectId: props.projectId,
5047
+ idOrSlug: props.collectionIdOrSlug
5048
+ });
3859
5049
  const collection = await core.collections.read({
3860
5050
  projectId: props.projectId,
3861
- id: props.collectionId
5051
+ id: resolvedId
3862
5052
  });
3863
5053
  return {
3864
5054
  schema: buildEntryValuesSchema(collection.fieldDefinitions),
@@ -3866,11 +5056,15 @@ function elekEntries(props) {
3866
5056
  };
3867
5057
  },
3868
5058
  load: async (context) => {
3869
- context.logger.info(`Loading elek.io Entries of Collection "${props.collectionId}" and Project "${props.projectId}"`);
5059
+ const resolvedCollectionId = await core.collections.resolveCollectionId({
5060
+ projectId: props.projectId,
5061
+ idOrSlug: props.collectionIdOrSlug
5062
+ });
5063
+ context.logger.info(`Loading elek.io Entries of Collection "${props.collectionIdOrSlug}" and Project "${props.projectId}"`);
3870
5064
  context.store.clear();
3871
5065
  const { list: entries, total } = await core.entries.list({
3872
5066
  projectId: props.projectId,
3873
- collectionId: props.collectionId,
5067
+ collectionId: resolvedCollectionId,
3874
5068
  limit: 0
3875
5069
  });
3876
5070
  if (total === 0) context.logger.warn("No Entries found");