@momentumcms/core 0.2.0 → 0.3.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.
- package/CHANGELOG.md +18 -0
- package/generators/generator.cjs +39 -3
- package/generators/generator.js +39 -3
- package/index.cjs +51 -2
- package/index.js +47 -2
- package/package.json +1 -1
- package/src/generators/generator.d.ts +10 -0
- package/src/index.d.ts +1 -0
- package/src/lib/collections/collection.types.d.ts +26 -2
- package/src/lib/collections/define-collection.d.ts +19 -0
- package/src/lib/collections/index.d.ts +2 -2
- package/src/lib/config.d.ts +41 -0
- package/src/lib/migrations.d.ts +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 0.3.0 (2026-02-20)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- implement Payload-style migration CLI workflow with clone-test-apply safety ([#35](https://github.com/DonaldMurillo/momentum-cms/pull/35))
|
|
6
|
+
- add named tabs support with nested data grouping and UI improvements ([#30](https://github.com/DonaldMurillo/momentum-cms/pull/30))
|
|
7
|
+
|
|
8
|
+
### 🩹 Fixes
|
|
9
|
+
|
|
10
|
+
- fix nav highlighting and resolve pre-existing E2E test failures ([#34](https://github.com/DonaldMurillo/momentum-cms/pull/34))
|
|
11
|
+
- add auth guard and MIME validation to PATCH upload route; fix pagination with client-side filtering ([#32](https://github.com/DonaldMurillo/momentum-cms/pull/32))
|
|
12
|
+
|
|
13
|
+
### ❤️ Thank You
|
|
14
|
+
|
|
15
|
+
- Claude Haiku 4.5
|
|
16
|
+
- Claude Opus 4.6
|
|
17
|
+
- Donald Murillo @DonaldMurillo
|
|
18
|
+
|
|
1
19
|
## 0.2.0 (2026-02-17)
|
|
2
20
|
|
|
3
21
|
### 🚀 Features
|
package/generators/generator.cjs
CHANGED
|
@@ -392,6 +392,25 @@ var FIELD_ADMIN_STRIP_KEYS = /* @__PURE__ */ new Set(["condition"]);
|
|
|
392
392
|
function isRecord(value) {
|
|
393
393
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
394
394
|
}
|
|
395
|
+
function previewFunctionToTemplate(fn, fields) {
|
|
396
|
+
try {
|
|
397
|
+
const sentinel = "__MCMS_FIELD_";
|
|
398
|
+
const mockDoc = {};
|
|
399
|
+
for (const field of fields) {
|
|
400
|
+
mockDoc[field.name] = `${sentinel}${field.name}__`;
|
|
401
|
+
}
|
|
402
|
+
const result = fn(mockDoc);
|
|
403
|
+
if (typeof result !== "string")
|
|
404
|
+
return true;
|
|
405
|
+
const template = result.replace(
|
|
406
|
+
new RegExp(`${sentinel}(\\w+)__`, "g"),
|
|
407
|
+
(_match, fieldName) => `{${fieldName}}`
|
|
408
|
+
);
|
|
409
|
+
return template;
|
|
410
|
+
} catch {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
395
414
|
function serializeValue(value, indent = " ") {
|
|
396
415
|
if (value === null)
|
|
397
416
|
return "null";
|
|
@@ -584,9 +603,13 @@ function serializeCollection(collection, indent = " ") {
|
|
|
584
603
|
}
|
|
585
604
|
parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
|
|
586
605
|
if (collection.admin) {
|
|
587
|
-
const adminEntries = Object.entries(collection.admin).filter(
|
|
588
|
-
(
|
|
589
|
-
|
|
606
|
+
const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
|
|
607
|
+
if (k === "preview" && typeof v === "function") {
|
|
608
|
+
const fn = v;
|
|
609
|
+
return [k, previewFunctionToTemplate(fn, collection.fields)];
|
|
610
|
+
}
|
|
611
|
+
return [k, v];
|
|
612
|
+
}).filter(([, v]) => typeof v !== "function");
|
|
590
613
|
if (adminEntries.length > 0) {
|
|
591
614
|
const adminObj = Object.fromEntries(adminEntries);
|
|
592
615
|
parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
|
|
@@ -610,6 +633,9 @@ function serializeCollection(collection, indent = " ") {
|
|
|
610
633
|
if (collection.defaultSort) {
|
|
611
634
|
parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
|
|
612
635
|
}
|
|
636
|
+
if (collection.upload !== void 0) {
|
|
637
|
+
parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
|
|
638
|
+
}
|
|
613
639
|
return `{
|
|
614
640
|
${parts.join(",\n")},
|
|
615
641
|
${indent}}`;
|
|
@@ -739,6 +765,15 @@ function parseArgs(args) {
|
|
|
739
765
|
}
|
|
740
766
|
return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
|
|
741
767
|
}
|
|
768
|
+
function formatWithPrettier(...filePaths) {
|
|
769
|
+
try {
|
|
770
|
+
(0, import_node_child_process.execFileSync)("npx", ["prettier", "--write", ...filePaths], {
|
|
771
|
+
stdio: "pipe"
|
|
772
|
+
});
|
|
773
|
+
} catch {
|
|
774
|
+
console.warn("prettier not available \u2014 skipping formatting of generated files");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
742
777
|
async function runGenerator(options) {
|
|
743
778
|
const configPath = (0, import_node_path.resolve)(options.configPath);
|
|
744
779
|
const typesOutputPath = (0, import_node_path.resolve)(options.typesOutputPath);
|
|
@@ -758,6 +793,7 @@ async function runGenerator(options) {
|
|
|
758
793
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(configOutputPath), { recursive: true });
|
|
759
794
|
(0, import_node_fs.writeFileSync)(configOutputPath, adminConfigContent, "utf-8");
|
|
760
795
|
console.info(`Admin config generated: ${configOutputPath}`);
|
|
796
|
+
formatWithPrettier(typesOutputPath, configOutputPath);
|
|
761
797
|
} catch (error) {
|
|
762
798
|
console.error(`Error generating:`, error);
|
|
763
799
|
throw error;
|
package/generators/generator.js
CHANGED
|
@@ -361,6 +361,25 @@ var FIELD_ADMIN_STRIP_KEYS = /* @__PURE__ */ new Set(["condition"]);
|
|
|
361
361
|
function isRecord(value) {
|
|
362
362
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
363
363
|
}
|
|
364
|
+
function previewFunctionToTemplate(fn, fields) {
|
|
365
|
+
try {
|
|
366
|
+
const sentinel = "__MCMS_FIELD_";
|
|
367
|
+
const mockDoc = {};
|
|
368
|
+
for (const field of fields) {
|
|
369
|
+
mockDoc[field.name] = `${sentinel}${field.name}__`;
|
|
370
|
+
}
|
|
371
|
+
const result = fn(mockDoc);
|
|
372
|
+
if (typeof result !== "string")
|
|
373
|
+
return true;
|
|
374
|
+
const template = result.replace(
|
|
375
|
+
new RegExp(`${sentinel}(\\w+)__`, "g"),
|
|
376
|
+
(_match, fieldName) => `{${fieldName}}`
|
|
377
|
+
);
|
|
378
|
+
return template;
|
|
379
|
+
} catch {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
364
383
|
function serializeValue(value, indent = " ") {
|
|
365
384
|
if (value === null)
|
|
366
385
|
return "null";
|
|
@@ -553,9 +572,13 @@ function serializeCollection(collection, indent = " ") {
|
|
|
553
572
|
}
|
|
554
573
|
parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
|
|
555
574
|
if (collection.admin) {
|
|
556
|
-
const adminEntries = Object.entries(collection.admin).filter(
|
|
557
|
-
(
|
|
558
|
-
|
|
575
|
+
const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
|
|
576
|
+
if (k === "preview" && typeof v === "function") {
|
|
577
|
+
const fn = v;
|
|
578
|
+
return [k, previewFunctionToTemplate(fn, collection.fields)];
|
|
579
|
+
}
|
|
580
|
+
return [k, v];
|
|
581
|
+
}).filter(([, v]) => typeof v !== "function");
|
|
559
582
|
if (adminEntries.length > 0) {
|
|
560
583
|
const adminObj = Object.fromEntries(adminEntries);
|
|
561
584
|
parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
|
|
@@ -579,6 +602,9 @@ function serializeCollection(collection, indent = " ") {
|
|
|
579
602
|
if (collection.defaultSort) {
|
|
580
603
|
parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
|
|
581
604
|
}
|
|
605
|
+
if (collection.upload !== void 0) {
|
|
606
|
+
parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
|
|
607
|
+
}
|
|
582
608
|
return `{
|
|
583
609
|
${parts.join(",\n")},
|
|
584
610
|
${indent}}`;
|
|
@@ -708,6 +734,15 @@ function parseArgs(args) {
|
|
|
708
734
|
}
|
|
709
735
|
return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
|
|
710
736
|
}
|
|
737
|
+
function formatWithPrettier(...filePaths) {
|
|
738
|
+
try {
|
|
739
|
+
execFileSync("npx", ["prettier", "--write", ...filePaths], {
|
|
740
|
+
stdio: "pipe"
|
|
741
|
+
});
|
|
742
|
+
} catch {
|
|
743
|
+
console.warn("prettier not available \u2014 skipping formatting of generated files");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
711
746
|
async function runGenerator(options) {
|
|
712
747
|
const configPath = resolve(options.configPath);
|
|
713
748
|
const typesOutputPath = resolve(options.typesOutputPath);
|
|
@@ -727,6 +762,7 @@ async function runGenerator(options) {
|
|
|
727
762
|
mkdirSync(dirname(configOutputPath), { recursive: true });
|
|
728
763
|
writeFileSync(configOutputPath, adminConfigContent, "utf-8");
|
|
729
764
|
console.info(`Admin config generated: ${configOutputPath}`);
|
|
765
|
+
formatWithPrettier(typesOutputPath, configOutputPath);
|
|
730
766
|
} catch (error) {
|
|
731
767
|
console.error(`Error generating:`, error);
|
|
732
768
|
throw error;
|
package/index.cjs
CHANGED
|
@@ -46,6 +46,7 @@ __export(src_exports, {
|
|
|
46
46
|
getDbAdapter: () => getDbAdapter,
|
|
47
47
|
getGlobals: () => getGlobals,
|
|
48
48
|
getSoftDeleteField: () => getSoftDeleteField,
|
|
49
|
+
getUploadFieldMapping: () => getUploadFieldMapping,
|
|
49
50
|
group: () => group,
|
|
50
51
|
hasAllRoles: () => hasAllRoles,
|
|
51
52
|
hasAnyRole: () => hasAnyRole,
|
|
@@ -55,6 +56,7 @@ __export(src_exports, {
|
|
|
55
56
|
isLayoutField: () => isLayoutField,
|
|
56
57
|
isNamedTab: () => isNamedTab,
|
|
57
58
|
isOwner: () => isOwner,
|
|
59
|
+
isUploadCollection: () => isUploadCollection,
|
|
58
60
|
json: () => json,
|
|
59
61
|
not: () => not,
|
|
60
62
|
number: () => number,
|
|
@@ -63,6 +65,8 @@ __export(src_exports, {
|
|
|
63
65
|
point: () => point,
|
|
64
66
|
radio: () => radio,
|
|
65
67
|
relationship: () => relationship,
|
|
68
|
+
resolveMigrationConfig: () => resolveMigrationConfig,
|
|
69
|
+
resolveMigrationMode: () => resolveMigrationMode,
|
|
66
70
|
richText: () => richText,
|
|
67
71
|
row: () => row,
|
|
68
72
|
select: () => select,
|
|
@@ -117,6 +121,21 @@ function getSoftDeleteField(config) {
|
|
|
117
121
|
const sdConfig = config.softDelete;
|
|
118
122
|
return sdConfig.field ?? "deletedAt";
|
|
119
123
|
}
|
|
124
|
+
function isUploadCollection(config) {
|
|
125
|
+
return config.upload != null;
|
|
126
|
+
}
|
|
127
|
+
function getUploadFieldMapping(config) {
|
|
128
|
+
if (!isUploadCollection(config))
|
|
129
|
+
return null;
|
|
130
|
+
const u = config.upload;
|
|
131
|
+
return {
|
|
132
|
+
filename: u.filenameField ?? "filename",
|
|
133
|
+
mimeType: u.mimeTypeField ?? "mimeType",
|
|
134
|
+
filesize: u.filesizeField ?? "filesize",
|
|
135
|
+
path: u.pathField ?? "path",
|
|
136
|
+
url: u.urlField ?? "url"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
120
139
|
|
|
121
140
|
// libs/core/src/lib/fields/field.types.ts
|
|
122
141
|
var LAYOUT_FIELD_TYPES = /* @__PURE__ */ new Set(["tabs", "collapsible", "row"]);
|
|
@@ -430,6 +449,9 @@ var MediaCollection = defineCollection({
|
|
|
430
449
|
singular: "Media",
|
|
431
450
|
plural: "Media"
|
|
432
451
|
},
|
|
452
|
+
upload: {
|
|
453
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
454
|
+
},
|
|
433
455
|
admin: {
|
|
434
456
|
useAsTitle: "filename",
|
|
435
457
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -450,7 +472,6 @@ var MediaCollection = defineCollection({
|
|
|
450
472
|
description: "File size in bytes"
|
|
451
473
|
}),
|
|
452
474
|
text("path", {
|
|
453
|
-
required: true,
|
|
454
475
|
label: "Storage Path",
|
|
455
476
|
description: "Path/key where the file is stored",
|
|
456
477
|
admin: {
|
|
@@ -561,6 +582,29 @@ function isOwner(ownerField = "createdBy") {
|
|
|
561
582
|
};
|
|
562
583
|
}
|
|
563
584
|
|
|
585
|
+
// libs/core/src/lib/migrations.ts
|
|
586
|
+
function resolveMigrationMode(mode) {
|
|
587
|
+
if (mode === "push" || mode === "migrate")
|
|
588
|
+
return mode;
|
|
589
|
+
const env = process.env["NODE_ENV"];
|
|
590
|
+
if (env === "production")
|
|
591
|
+
return "migrate";
|
|
592
|
+
return "push";
|
|
593
|
+
}
|
|
594
|
+
function resolveMigrationConfig(config) {
|
|
595
|
+
if (!config)
|
|
596
|
+
return void 0;
|
|
597
|
+
const mode = resolveMigrationMode(config.mode);
|
|
598
|
+
return {
|
|
599
|
+
...config,
|
|
600
|
+
directory: config.directory ?? "./migrations",
|
|
601
|
+
mode,
|
|
602
|
+
cloneTest: config.cloneTest ?? mode === "migrate",
|
|
603
|
+
dangerDetection: config.dangerDetection ?? true,
|
|
604
|
+
autoApply: config.autoApply ?? mode === "push"
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
564
608
|
// libs/core/src/lib/config.ts
|
|
565
609
|
var MIN_PASSWORD_LENGTH = 8;
|
|
566
610
|
function defineMomentumConfig(config) {
|
|
@@ -591,7 +635,8 @@ function defineMomentumConfig(config) {
|
|
|
591
635
|
level: config.logging?.level ?? "info",
|
|
592
636
|
format: config.logging?.format ?? "pretty",
|
|
593
637
|
timestamps: config.logging?.timestamps ?? true
|
|
594
|
-
}
|
|
638
|
+
},
|
|
639
|
+
migrations: resolveMigrationConfig(config.migrations)
|
|
595
640
|
};
|
|
596
641
|
}
|
|
597
642
|
function getDbAdapter(config) {
|
|
@@ -717,6 +762,7 @@ function createSeedHelpers() {
|
|
|
717
762
|
getDbAdapter,
|
|
718
763
|
getGlobals,
|
|
719
764
|
getSoftDeleteField,
|
|
765
|
+
getUploadFieldMapping,
|
|
720
766
|
group,
|
|
721
767
|
hasAllRoles,
|
|
722
768
|
hasAnyRole,
|
|
@@ -726,6 +772,7 @@ function createSeedHelpers() {
|
|
|
726
772
|
isLayoutField,
|
|
727
773
|
isNamedTab,
|
|
728
774
|
isOwner,
|
|
775
|
+
isUploadCollection,
|
|
729
776
|
json,
|
|
730
777
|
not,
|
|
731
778
|
number,
|
|
@@ -734,6 +781,8 @@ function createSeedHelpers() {
|
|
|
734
781
|
point,
|
|
735
782
|
radio,
|
|
736
783
|
relationship,
|
|
784
|
+
resolveMigrationConfig,
|
|
785
|
+
resolveMigrationMode,
|
|
737
786
|
richText,
|
|
738
787
|
row,
|
|
739
788
|
select,
|
package/index.js
CHANGED
|
@@ -40,6 +40,21 @@ function getSoftDeleteField(config) {
|
|
|
40
40
|
const sdConfig = config.softDelete;
|
|
41
41
|
return sdConfig.field ?? "deletedAt";
|
|
42
42
|
}
|
|
43
|
+
function isUploadCollection(config) {
|
|
44
|
+
return config.upload != null;
|
|
45
|
+
}
|
|
46
|
+
function getUploadFieldMapping(config) {
|
|
47
|
+
if (!isUploadCollection(config))
|
|
48
|
+
return null;
|
|
49
|
+
const u = config.upload;
|
|
50
|
+
return {
|
|
51
|
+
filename: u.filenameField ?? "filename",
|
|
52
|
+
mimeType: u.mimeTypeField ?? "mimeType",
|
|
53
|
+
filesize: u.filesizeField ?? "filesize",
|
|
54
|
+
path: u.pathField ?? "path",
|
|
55
|
+
url: u.urlField ?? "url"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
43
58
|
|
|
44
59
|
// libs/core/src/lib/fields/field.types.ts
|
|
45
60
|
var LAYOUT_FIELD_TYPES = /* @__PURE__ */ new Set(["tabs", "collapsible", "row"]);
|
|
@@ -353,6 +368,9 @@ var MediaCollection = defineCollection({
|
|
|
353
368
|
singular: "Media",
|
|
354
369
|
plural: "Media"
|
|
355
370
|
},
|
|
371
|
+
upload: {
|
|
372
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
373
|
+
},
|
|
356
374
|
admin: {
|
|
357
375
|
useAsTitle: "filename",
|
|
358
376
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -373,7 +391,6 @@ var MediaCollection = defineCollection({
|
|
|
373
391
|
description: "File size in bytes"
|
|
374
392
|
}),
|
|
375
393
|
text("path", {
|
|
376
|
-
required: true,
|
|
377
394
|
label: "Storage Path",
|
|
378
395
|
description: "Path/key where the file is stored",
|
|
379
396
|
admin: {
|
|
@@ -484,6 +501,29 @@ function isOwner(ownerField = "createdBy") {
|
|
|
484
501
|
};
|
|
485
502
|
}
|
|
486
503
|
|
|
504
|
+
// libs/core/src/lib/migrations.ts
|
|
505
|
+
function resolveMigrationMode(mode) {
|
|
506
|
+
if (mode === "push" || mode === "migrate")
|
|
507
|
+
return mode;
|
|
508
|
+
const env = process.env["NODE_ENV"];
|
|
509
|
+
if (env === "production")
|
|
510
|
+
return "migrate";
|
|
511
|
+
return "push";
|
|
512
|
+
}
|
|
513
|
+
function resolveMigrationConfig(config) {
|
|
514
|
+
if (!config)
|
|
515
|
+
return void 0;
|
|
516
|
+
const mode = resolveMigrationMode(config.mode);
|
|
517
|
+
return {
|
|
518
|
+
...config,
|
|
519
|
+
directory: config.directory ?? "./migrations",
|
|
520
|
+
mode,
|
|
521
|
+
cloneTest: config.cloneTest ?? mode === "migrate",
|
|
522
|
+
dangerDetection: config.dangerDetection ?? true,
|
|
523
|
+
autoApply: config.autoApply ?? mode === "push"
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
487
527
|
// libs/core/src/lib/config.ts
|
|
488
528
|
var MIN_PASSWORD_LENGTH = 8;
|
|
489
529
|
function defineMomentumConfig(config) {
|
|
@@ -514,7 +554,8 @@ function defineMomentumConfig(config) {
|
|
|
514
554
|
level: config.logging?.level ?? "info",
|
|
515
555
|
format: config.logging?.format ?? "pretty",
|
|
516
556
|
timestamps: config.logging?.timestamps ?? true
|
|
517
|
-
}
|
|
557
|
+
},
|
|
558
|
+
migrations: resolveMigrationConfig(config.migrations)
|
|
518
559
|
};
|
|
519
560
|
}
|
|
520
561
|
function getDbAdapter(config) {
|
|
@@ -639,6 +680,7 @@ export {
|
|
|
639
680
|
getDbAdapter,
|
|
640
681
|
getGlobals,
|
|
641
682
|
getSoftDeleteField,
|
|
683
|
+
getUploadFieldMapping,
|
|
642
684
|
group,
|
|
643
685
|
hasAllRoles,
|
|
644
686
|
hasAnyRole,
|
|
@@ -648,6 +690,7 @@ export {
|
|
|
648
690
|
isLayoutField,
|
|
649
691
|
isNamedTab,
|
|
650
692
|
isOwner,
|
|
693
|
+
isUploadCollection,
|
|
651
694
|
json,
|
|
652
695
|
not,
|
|
653
696
|
number,
|
|
@@ -656,6 +699,8 @@ export {
|
|
|
656
699
|
point,
|
|
657
700
|
radio,
|
|
658
701
|
relationship,
|
|
702
|
+
resolveMigrationConfig,
|
|
703
|
+
resolveMigrationMode,
|
|
659
704
|
richText,
|
|
660
705
|
row,
|
|
661
706
|
select,
|
package/package.json
CHANGED
|
@@ -91,6 +91,16 @@ interface CollectionDefinition {
|
|
|
91
91
|
defaultWhere?: unknown;
|
|
92
92
|
endpoints?: unknown[];
|
|
93
93
|
webhooks?: unknown[];
|
|
94
|
+
upload?: {
|
|
95
|
+
mimeTypes?: string[];
|
|
96
|
+
maxFileSize?: number;
|
|
97
|
+
directory?: string;
|
|
98
|
+
filenameField?: string;
|
|
99
|
+
mimeTypeField?: string;
|
|
100
|
+
filesizeField?: string;
|
|
101
|
+
pathField?: string;
|
|
102
|
+
urlField?: string;
|
|
103
|
+
};
|
|
94
104
|
}
|
|
95
105
|
interface GlobalDefinition {
|
|
96
106
|
slug: string;
|
package/src/index.d.ts
CHANGED
|
@@ -74,8 +74,8 @@ export interface AdminConfig {
|
|
|
74
74
|
description?: string;
|
|
75
75
|
/** Hide from admin navigation */
|
|
76
76
|
hidden?: boolean;
|
|
77
|
-
/** Enable preview mode */
|
|
78
|
-
preview?: boolean | ((doc: Record<string, unknown>) => string);
|
|
77
|
+
/** Enable preview mode. String values are URL templates with {fieldName} placeholders. */
|
|
78
|
+
preview?: boolean | string | ((doc: Record<string, unknown>) => string);
|
|
79
79
|
/** Custom action buttons displayed in the collection list header (alongside Create button) */
|
|
80
80
|
headerActions?: Array<{
|
|
81
81
|
id: string;
|
|
@@ -120,6 +120,24 @@ export interface SoftDeleteConfig {
|
|
|
120
120
|
/** Auto-purge soft-deleted records after this many days. Undefined means never purge. */
|
|
121
121
|
retentionDays?: number;
|
|
122
122
|
}
|
|
123
|
+
export interface UploadCollectionConfig {
|
|
124
|
+
/** Allowed MIME types for uploads to this collection (e.g., ['image/*', 'application/pdf']) */
|
|
125
|
+
mimeTypes?: string[];
|
|
126
|
+
/** Maximum file size in bytes (overrides global storage.maxFileSize) */
|
|
127
|
+
maxFileSize?: number;
|
|
128
|
+
/** Subdirectory within the upload dir for this collection's files */
|
|
129
|
+
directory?: string;
|
|
130
|
+
/** Field name for the original filename. @default 'filename' */
|
|
131
|
+
filenameField?: string;
|
|
132
|
+
/** Field name for the MIME type. @default 'mimeType' */
|
|
133
|
+
mimeTypeField?: string;
|
|
134
|
+
/** Field name for the file size in bytes. @default 'filesize' */
|
|
135
|
+
filesizeField?: string;
|
|
136
|
+
/** Field name for the storage path. @default 'path' */
|
|
137
|
+
pathField?: string;
|
|
138
|
+
/** Field name for the public URL. @default 'url' */
|
|
139
|
+
urlField?: string;
|
|
140
|
+
}
|
|
123
141
|
export interface TimestampsConfig {
|
|
124
142
|
/** Add createdAt field */
|
|
125
143
|
createdAt?: boolean;
|
|
@@ -188,6 +206,12 @@ export interface CollectionConfig {
|
|
|
188
206
|
endpoints?: EndpointConfig[];
|
|
189
207
|
/** Webhook subscriptions for this collection */
|
|
190
208
|
webhooks?: WebhookConfig[];
|
|
209
|
+
/**
|
|
210
|
+
* Upload configuration. When present, this collection becomes an "upload collection"
|
|
211
|
+
* where POST /api/{slug} accepts multipart/form-data and auto-populates file metadata fields.
|
|
212
|
+
* Similar to Payload CMS's upload collection pattern.
|
|
213
|
+
*/
|
|
214
|
+
upload?: UploadCollectionConfig;
|
|
191
215
|
}
|
|
192
216
|
/** Events that trigger webhooks. */
|
|
193
217
|
export type WebhookEvent = 'afterChange' | 'afterDelete' | 'afterCreate' | 'afterUpdate';
|
|
@@ -40,6 +40,25 @@ export declare function defineGlobal(config: GlobalConfig): GlobalConfig;
|
|
|
40
40
|
* @returns The deletedAt field name, or null if soft delete is not enabled
|
|
41
41
|
*/
|
|
42
42
|
export declare function getSoftDeleteField(config: CollectionConfig): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Check whether a collection is configured as an upload collection.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isUploadCollection(config: CollectionConfig): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Resolved field name mapping for upload collection metadata fields.
|
|
49
|
+
*/
|
|
50
|
+
export interface UploadFieldMapping {
|
|
51
|
+
filename: string;
|
|
52
|
+
mimeType: string;
|
|
53
|
+
filesize: string;
|
|
54
|
+
path: string;
|
|
55
|
+
url: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get the resolved upload field mapping for an upload collection.
|
|
59
|
+
* Returns field names used for auto-populating file metadata, or null for non-upload collections.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getUploadFieldMapping(config: CollectionConfig): UploadFieldMapping | null;
|
|
43
62
|
/**
|
|
44
63
|
* Helper type to extract the document type from a collection
|
|
45
64
|
* Useful for typing API responses and database queries
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * from './collection.types';
|
|
2
|
-
export { defineCollection, defineGlobal, getSoftDeleteField } from './define-collection';
|
|
3
|
-
export type { InferDocumentType } from './define-collection';
|
|
2
|
+
export { defineCollection, defineGlobal, getSoftDeleteField, isUploadCollection, getUploadFieldMapping, } from './define-collection';
|
|
3
|
+
export type { InferDocumentType, UploadFieldMapping } from './define-collection';
|
|
4
4
|
export { MediaCollection } from './media.collection';
|
|
5
5
|
export type { MediaDocument } from './media.collection';
|
package/src/lib/config.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SeedingConfig, SeedingOptions } from './seeding';
|
|
|
3
3
|
import type { DocumentVersion, DocumentStatus, VersionQueryOptions, VersionCountOptions, CreateVersionOptions } from './versions';
|
|
4
4
|
import type { MomentumPlugin, PluginAdminRouteDescriptor } from './plugins';
|
|
5
5
|
import type { StorageAdapter } from './storage';
|
|
6
|
+
import type { MigrationConfig, ResolvedMigrationConfig } from './migrations';
|
|
6
7
|
/**
|
|
7
8
|
* Minimum password length for user accounts.
|
|
8
9
|
* Shared across seeding, user sync hooks, and setup middleware.
|
|
@@ -13,6 +14,12 @@ export declare const MIN_PASSWORD_LENGTH = 8;
|
|
|
13
14
|
* This is a placeholder type - actual adapters implement this interface.
|
|
14
15
|
*/
|
|
15
16
|
export interface DatabaseAdapter {
|
|
17
|
+
/**
|
|
18
|
+
* Database dialect identifier.
|
|
19
|
+
* Set by the adapter factory (e.g., postgresAdapter sets 'postgresql', sqliteAdapter sets 'sqlite').
|
|
20
|
+
* Used by the migration CLI to select the correct introspection and SQL generation strategy.
|
|
21
|
+
*/
|
|
22
|
+
dialect?: 'postgresql' | 'sqlite';
|
|
16
23
|
find(collection: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
17
24
|
findById(collection: string, id: string): Promise<Record<string, unknown> | null>;
|
|
18
25
|
create(collection: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -148,6 +155,34 @@ export interface DatabaseAdapter {
|
|
|
148
155
|
* @returns The full global record after update
|
|
149
156
|
*/
|
|
150
157
|
updateGlobal?(slug: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
158
|
+
/**
|
|
159
|
+
* Introspect the current database schema.
|
|
160
|
+
* Returns table/column metadata for diffing against collection config.
|
|
161
|
+
*/
|
|
162
|
+
introspect?(): Promise<Record<string, unknown>>;
|
|
163
|
+
/**
|
|
164
|
+
* Execute a raw SQL statement (DDL or DML).
|
|
165
|
+
* Returns the number of affected rows.
|
|
166
|
+
*/
|
|
167
|
+
executeRaw?(sql: string, params?: unknown[]): Promise<number>;
|
|
168
|
+
/**
|
|
169
|
+
* Execute a raw SQL query and return rows.
|
|
170
|
+
*/
|
|
171
|
+
queryRaw?<T extends Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
172
|
+
/**
|
|
173
|
+
* Clone the database for migration testing.
|
|
174
|
+
* Returns a connection string or path for the clone.
|
|
175
|
+
*/
|
|
176
|
+
cloneDatabase?(targetName: string): Promise<string>;
|
|
177
|
+
/**
|
|
178
|
+
* Drop a cloned database.
|
|
179
|
+
*/
|
|
180
|
+
dropClone?(targetName: string): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Acquire an advisory lock for migration safety.
|
|
183
|
+
* Returns a release function.
|
|
184
|
+
*/
|
|
185
|
+
acquireMigrationLock?(): Promise<() => Promise<void>>;
|
|
151
186
|
}
|
|
152
187
|
/**
|
|
153
188
|
* Database configuration options.
|
|
@@ -327,6 +362,11 @@ export interface MomentumConfig {
|
|
|
327
362
|
* Plugins run in array order during init/ready, reverse during shutdown.
|
|
328
363
|
*/
|
|
329
364
|
plugins?: MomentumPlugin[];
|
|
365
|
+
/**
|
|
366
|
+
* Migration system configuration.
|
|
367
|
+
* When set, enables the schema migration system.
|
|
368
|
+
*/
|
|
369
|
+
migrations?: MigrationConfig;
|
|
330
370
|
}
|
|
331
371
|
/**
|
|
332
372
|
* Resolved seeding options with defaults applied.
|
|
@@ -346,6 +386,7 @@ export interface ResolvedMomentumConfig extends MomentumConfig {
|
|
|
346
386
|
server: Required<ServerConfig>;
|
|
347
387
|
seeding?: ResolvedSeedingConfig;
|
|
348
388
|
logging: ResolvedLoggingConfig;
|
|
389
|
+
migrations?: ResolvedMigrationConfig;
|
|
349
390
|
}
|
|
350
391
|
/**
|
|
351
392
|
* Defines Momentum CMS configuration.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Types for Momentum CMS
|
|
3
|
+
*
|
|
4
|
+
* Universal (browser + server) types for the migration system.
|
|
5
|
+
* Server-only implementation lives in @momentumcms/migrations.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for the migration system.
|
|
9
|
+
* Added to MomentumConfig.migrations when migrations are enabled.
|
|
10
|
+
*/
|
|
11
|
+
export interface MigrationConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Directory where migration files are stored.
|
|
14
|
+
* Relative to the app root.
|
|
15
|
+
* @default './migrations'
|
|
16
|
+
*/
|
|
17
|
+
directory?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Database operation mode.
|
|
20
|
+
* - 'push': Dev mode — direct schema sync, no migration files
|
|
21
|
+
* - 'migrate': Production mode — migration files required
|
|
22
|
+
* - 'auto': 'push' in development, 'migrate' in production
|
|
23
|
+
* @default 'auto'
|
|
24
|
+
*/
|
|
25
|
+
mode?: 'push' | 'migrate' | 'auto';
|
|
26
|
+
/**
|
|
27
|
+
* Enable clone-test-apply safety pipeline before applying migrations.
|
|
28
|
+
* When enabled, migrations are first tested on a database clone.
|
|
29
|
+
* @default true in migrate mode
|
|
30
|
+
*/
|
|
31
|
+
cloneTest?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Enable dangerous operation detection and warnings.
|
|
34
|
+
* @default true
|
|
35
|
+
*/
|
|
36
|
+
dangerDetection?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Automatically apply pending migrations on server start.
|
|
39
|
+
* Only applies in 'push' mode. Migrate mode always requires explicit CLI commands.
|
|
40
|
+
* @default true in push mode
|
|
41
|
+
*/
|
|
42
|
+
autoApply?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Status of an individual applied migration.
|
|
46
|
+
*/
|
|
47
|
+
export interface MigrationStatus {
|
|
48
|
+
/** Migration filename (without extension) */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Batch number (migrations applied together share a batch) */
|
|
51
|
+
batch: number;
|
|
52
|
+
/** When the migration was applied (ISO string) */
|
|
53
|
+
appliedAt: string;
|
|
54
|
+
/** SHA-256 checksum of the migration file */
|
|
55
|
+
checksum: string;
|
|
56
|
+
/** How long the migration took to execute (ms) */
|
|
57
|
+
executionMs: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Overall migration system status (browser-safe for admin UI).
|
|
61
|
+
*/
|
|
62
|
+
export interface MigrationSystemStatus {
|
|
63
|
+
/** Whether the database schema matches the collection config */
|
|
64
|
+
inSync: boolean;
|
|
65
|
+
/** Number of pending (unapplied) migrations */
|
|
66
|
+
pending: number;
|
|
67
|
+
/** List of applied migrations */
|
|
68
|
+
applied: MigrationStatus[];
|
|
69
|
+
/** Current migration mode */
|
|
70
|
+
mode: 'push' | 'migrate';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Severity levels for dangerous operation warnings.
|
|
74
|
+
*/
|
|
75
|
+
export type DangerSeverity = 'warning' | 'destructive' | 'irreversible';
|
|
76
|
+
/**
|
|
77
|
+
* Resolved migration config with defaults applied.
|
|
78
|
+
*/
|
|
79
|
+
export interface ResolvedMigrationConfig extends MigrationConfig {
|
|
80
|
+
directory: string;
|
|
81
|
+
mode: 'push' | 'migrate';
|
|
82
|
+
cloneTest: boolean;
|
|
83
|
+
dangerDetection: boolean;
|
|
84
|
+
autoApply: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the effective migration mode from config and environment.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveMigrationMode(mode: MigrationConfig['mode']): 'push' | 'migrate';
|
|
90
|
+
/**
|
|
91
|
+
* Resolve migration config with defaults applied.
|
|
92
|
+
*/
|
|
93
|
+
export declare function resolveMigrationConfig(config: MigrationConfig | undefined): ResolvedMigrationConfig | undefined;
|