@momentumcms/core 0.1.10 → 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 +29 -0
- package/generators/generator.cjs +59 -14
- package/generators/generator.js +59 -14
- package/index.cjs +68 -3
- package/index.js +63 -3
- package/package.json +1 -1
- package/src/generators/generator.d.ts +11 -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/fields/field.types.d.ts +6 -0
- package/src/lib/migrations.d.ts +93 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.2.0 (2026-02-17)
|
|
20
|
+
|
|
21
|
+
### 🚀 Features
|
|
22
|
+
|
|
23
|
+
- add named tabs support with nested data grouping and tab UI improvements ([63ab63e](https://github.com/DonaldMurillo/momentum-cms/commit/63ab63e))
|
|
24
|
+
|
|
25
|
+
### ❤️ Thank You
|
|
26
|
+
|
|
27
|
+
- Claude Opus 4.6
|
|
28
|
+
- Donald Murillo @DonaldMurillo
|
|
29
|
+
|
|
1
30
|
## 0.1.10 (2026-02-17)
|
|
2
31
|
|
|
3
32
|
### 🩹 Fixes
|
package/generators/generator.cjs
CHANGED
|
@@ -36,12 +36,26 @@ var import_node_path = require("node:path");
|
|
|
36
36
|
var import_node_url = require("node:url");
|
|
37
37
|
|
|
38
38
|
// libs/core/src/lib/fields/field.types.ts
|
|
39
|
+
function isNamedTab(tab) {
|
|
40
|
+
return typeof tab.name === "string" && tab.name.length > 0;
|
|
41
|
+
}
|
|
39
42
|
function flattenDataFields(fields) {
|
|
40
43
|
const result = [];
|
|
41
44
|
for (const field of fields) {
|
|
42
45
|
if (field.type === "tabs") {
|
|
43
46
|
for (const tab of field.tabs) {
|
|
44
|
-
|
|
47
|
+
if (isNamedTab(tab)) {
|
|
48
|
+
const syntheticGroup = {
|
|
49
|
+
name: tab.name,
|
|
50
|
+
type: "group",
|
|
51
|
+
label: tab.label,
|
|
52
|
+
description: tab.description,
|
|
53
|
+
fields: tab.fields
|
|
54
|
+
};
|
|
55
|
+
result.push(syntheticGroup);
|
|
56
|
+
} else {
|
|
57
|
+
result.push(...flattenDataFields(tab.fields));
|
|
58
|
+
}
|
|
45
59
|
}
|
|
46
60
|
} else if (field.type === "collapsible" || field.type === "row") {
|
|
47
61
|
result.push(...flattenDataFields(field.fields));
|
|
@@ -262,11 +276,7 @@ function generateTypes(config) {
|
|
|
262
276
|
const hasTimestamps = collection.timestamps !== false;
|
|
263
277
|
for (const field of dataFields) {
|
|
264
278
|
if (field.type === "blocks") {
|
|
265
|
-
const blockResult = generateBlockTypes(
|
|
266
|
-
collection.slug,
|
|
267
|
-
field.name,
|
|
268
|
-
field
|
|
269
|
-
);
|
|
279
|
+
const blockResult = generateBlockTypes(collection.slug, field.name, field);
|
|
270
280
|
blockDeclarations.push(blockResult.declarations);
|
|
271
281
|
}
|
|
272
282
|
}
|
|
@@ -280,11 +290,7 @@ function generateTypes(config) {
|
|
|
280
290
|
const optional = field.required ? "" : "?";
|
|
281
291
|
const propName = needsQuoting2(field.name) ? safeQuote(field.name) : field.name;
|
|
282
292
|
if (field.type === "blocks") {
|
|
283
|
-
const blockResult = generateBlockTypes(
|
|
284
|
-
collection.slug,
|
|
285
|
-
field.name,
|
|
286
|
-
field
|
|
287
|
-
);
|
|
293
|
+
const blockResult = generateBlockTypes(collection.slug, field.name, field);
|
|
288
294
|
lines.push(` ${propName}${optional}: ${blockResult.unionTypeName}[];`);
|
|
289
295
|
} else {
|
|
290
296
|
const tsType = fieldTypeToTS(field);
|
|
@@ -386,6 +392,25 @@ var FIELD_ADMIN_STRIP_KEYS = /* @__PURE__ */ new Set(["condition"]);
|
|
|
386
392
|
function isRecord(value) {
|
|
387
393
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
388
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
|
+
}
|
|
389
414
|
function serializeValue(value, indent = " ") {
|
|
390
415
|
if (value === null)
|
|
391
416
|
return "null";
|
|
@@ -554,6 +579,9 @@ function serializeTabsArray(tabs, indent) {
|
|
|
554
579
|
return "[]";
|
|
555
580
|
const items = tabs.map((tab) => {
|
|
556
581
|
const parts = [];
|
|
582
|
+
if (tab.name) {
|
|
583
|
+
parts.push(`${indent} name: ${JSON.stringify(tab.name)}`);
|
|
584
|
+
}
|
|
557
585
|
parts.push(`${indent} label: ${JSON.stringify(tab.label)}`);
|
|
558
586
|
if (tab.description) {
|
|
559
587
|
parts.push(`${indent} description: ${JSON.stringify(tab.description)}`);
|
|
@@ -575,9 +603,13 @@ function serializeCollection(collection, indent = " ") {
|
|
|
575
603
|
}
|
|
576
604
|
parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
|
|
577
605
|
if (collection.admin) {
|
|
578
|
-
const adminEntries = Object.entries(collection.admin).filter(
|
|
579
|
-
(
|
|
580
|
-
|
|
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");
|
|
581
613
|
if (adminEntries.length > 0) {
|
|
582
614
|
const adminObj = Object.fromEntries(adminEntries);
|
|
583
615
|
parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
|
|
@@ -601,6 +633,9 @@ function serializeCollection(collection, indent = " ") {
|
|
|
601
633
|
if (collection.defaultSort) {
|
|
602
634
|
parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
|
|
603
635
|
}
|
|
636
|
+
if (collection.upload !== void 0) {
|
|
637
|
+
parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
|
|
638
|
+
}
|
|
604
639
|
return `{
|
|
605
640
|
${parts.join(",\n")},
|
|
606
641
|
${indent}}`;
|
|
@@ -730,6 +765,15 @@ function parseArgs(args) {
|
|
|
730
765
|
}
|
|
731
766
|
return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
|
|
732
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
|
+
}
|
|
733
777
|
async function runGenerator(options) {
|
|
734
778
|
const configPath = (0, import_node_path.resolve)(options.configPath);
|
|
735
779
|
const typesOutputPath = (0, import_node_path.resolve)(options.typesOutputPath);
|
|
@@ -749,6 +793,7 @@ async function runGenerator(options) {
|
|
|
749
793
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(configOutputPath), { recursive: true });
|
|
750
794
|
(0, import_node_fs.writeFileSync)(configOutputPath, adminConfigContent, "utf-8");
|
|
751
795
|
console.info(`Admin config generated: ${configOutputPath}`);
|
|
796
|
+
formatWithPrettier(typesOutputPath, configOutputPath);
|
|
752
797
|
} catch (error) {
|
|
753
798
|
console.error(`Error generating:`, error);
|
|
754
799
|
throw error;
|
package/generators/generator.js
CHANGED
|
@@ -5,12 +5,26 @@ import { dirname, resolve, relative } from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
// libs/core/src/lib/fields/field.types.ts
|
|
8
|
+
function isNamedTab(tab) {
|
|
9
|
+
return typeof tab.name === "string" && tab.name.length > 0;
|
|
10
|
+
}
|
|
8
11
|
function flattenDataFields(fields) {
|
|
9
12
|
const result = [];
|
|
10
13
|
for (const field of fields) {
|
|
11
14
|
if (field.type === "tabs") {
|
|
12
15
|
for (const tab of field.tabs) {
|
|
13
|
-
|
|
16
|
+
if (isNamedTab(tab)) {
|
|
17
|
+
const syntheticGroup = {
|
|
18
|
+
name: tab.name,
|
|
19
|
+
type: "group",
|
|
20
|
+
label: tab.label,
|
|
21
|
+
description: tab.description,
|
|
22
|
+
fields: tab.fields
|
|
23
|
+
};
|
|
24
|
+
result.push(syntheticGroup);
|
|
25
|
+
} else {
|
|
26
|
+
result.push(...flattenDataFields(tab.fields));
|
|
27
|
+
}
|
|
14
28
|
}
|
|
15
29
|
} else if (field.type === "collapsible" || field.type === "row") {
|
|
16
30
|
result.push(...flattenDataFields(field.fields));
|
|
@@ -231,11 +245,7 @@ function generateTypes(config) {
|
|
|
231
245
|
const hasTimestamps = collection.timestamps !== false;
|
|
232
246
|
for (const field of dataFields) {
|
|
233
247
|
if (field.type === "blocks") {
|
|
234
|
-
const blockResult = generateBlockTypes(
|
|
235
|
-
collection.slug,
|
|
236
|
-
field.name,
|
|
237
|
-
field
|
|
238
|
-
);
|
|
248
|
+
const blockResult = generateBlockTypes(collection.slug, field.name, field);
|
|
239
249
|
blockDeclarations.push(blockResult.declarations);
|
|
240
250
|
}
|
|
241
251
|
}
|
|
@@ -249,11 +259,7 @@ function generateTypes(config) {
|
|
|
249
259
|
const optional = field.required ? "" : "?";
|
|
250
260
|
const propName = needsQuoting2(field.name) ? safeQuote(field.name) : field.name;
|
|
251
261
|
if (field.type === "blocks") {
|
|
252
|
-
const blockResult = generateBlockTypes(
|
|
253
|
-
collection.slug,
|
|
254
|
-
field.name,
|
|
255
|
-
field
|
|
256
|
-
);
|
|
262
|
+
const blockResult = generateBlockTypes(collection.slug, field.name, field);
|
|
257
263
|
lines.push(` ${propName}${optional}: ${blockResult.unionTypeName}[];`);
|
|
258
264
|
} else {
|
|
259
265
|
const tsType = fieldTypeToTS(field);
|
|
@@ -355,6 +361,25 @@ var FIELD_ADMIN_STRIP_KEYS = /* @__PURE__ */ new Set(["condition"]);
|
|
|
355
361
|
function isRecord(value) {
|
|
356
362
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
357
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
|
+
}
|
|
358
383
|
function serializeValue(value, indent = " ") {
|
|
359
384
|
if (value === null)
|
|
360
385
|
return "null";
|
|
@@ -523,6 +548,9 @@ function serializeTabsArray(tabs, indent) {
|
|
|
523
548
|
return "[]";
|
|
524
549
|
const items = tabs.map((tab) => {
|
|
525
550
|
const parts = [];
|
|
551
|
+
if (tab.name) {
|
|
552
|
+
parts.push(`${indent} name: ${JSON.stringify(tab.name)}`);
|
|
553
|
+
}
|
|
526
554
|
parts.push(`${indent} label: ${JSON.stringify(tab.label)}`);
|
|
527
555
|
if (tab.description) {
|
|
528
556
|
parts.push(`${indent} description: ${JSON.stringify(tab.description)}`);
|
|
@@ -544,9 +572,13 @@ function serializeCollection(collection, indent = " ") {
|
|
|
544
572
|
}
|
|
545
573
|
parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
|
|
546
574
|
if (collection.admin) {
|
|
547
|
-
const adminEntries = Object.entries(collection.admin).filter(
|
|
548
|
-
(
|
|
549
|
-
|
|
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");
|
|
550
582
|
if (adminEntries.length > 0) {
|
|
551
583
|
const adminObj = Object.fromEntries(adminEntries);
|
|
552
584
|
parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
|
|
@@ -570,6 +602,9 @@ function serializeCollection(collection, indent = " ") {
|
|
|
570
602
|
if (collection.defaultSort) {
|
|
571
603
|
parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
|
|
572
604
|
}
|
|
605
|
+
if (collection.upload !== void 0) {
|
|
606
|
+
parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
|
|
607
|
+
}
|
|
573
608
|
return `{
|
|
574
609
|
${parts.join(",\n")},
|
|
575
610
|
${indent}}`;
|
|
@@ -699,6 +734,15 @@ function parseArgs(args) {
|
|
|
699
734
|
}
|
|
700
735
|
return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
|
|
701
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
|
+
}
|
|
702
746
|
async function runGenerator(options) {
|
|
703
747
|
const configPath = resolve(options.configPath);
|
|
704
748
|
const typesOutputPath = resolve(options.typesOutputPath);
|
|
@@ -718,6 +762,7 @@ async function runGenerator(options) {
|
|
|
718
762
|
mkdirSync(dirname(configOutputPath), { recursive: true });
|
|
719
763
|
writeFileSync(configOutputPath, adminConfigContent, "utf-8");
|
|
720
764
|
console.info(`Admin config generated: ${configOutputPath}`);
|
|
765
|
+
formatWithPrettier(typesOutputPath, configOutputPath);
|
|
721
766
|
} catch (error) {
|
|
722
767
|
console.error(`Error generating:`, error);
|
|
723
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,
|
|
@@ -53,7 +54,9 @@ __export(src_exports, {
|
|
|
53
54
|
humanizeFieldName: () => humanizeFieldName,
|
|
54
55
|
isAuthenticated: () => isAuthenticated,
|
|
55
56
|
isLayoutField: () => isLayoutField,
|
|
57
|
+
isNamedTab: () => isNamedTab,
|
|
56
58
|
isOwner: () => isOwner,
|
|
59
|
+
isUploadCollection: () => isUploadCollection,
|
|
57
60
|
json: () => json,
|
|
58
61
|
not: () => not,
|
|
59
62
|
number: () => number,
|
|
@@ -62,6 +65,8 @@ __export(src_exports, {
|
|
|
62
65
|
point: () => point,
|
|
63
66
|
radio: () => radio,
|
|
64
67
|
relationship: () => relationship,
|
|
68
|
+
resolveMigrationConfig: () => resolveMigrationConfig,
|
|
69
|
+
resolveMigrationMode: () => resolveMigrationMode,
|
|
65
70
|
richText: () => richText,
|
|
66
71
|
row: () => row,
|
|
67
72
|
select: () => select,
|
|
@@ -116,6 +121,21 @@ function getSoftDeleteField(config) {
|
|
|
116
121
|
const sdConfig = config.softDelete;
|
|
117
122
|
return sdConfig.field ?? "deletedAt";
|
|
118
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
|
+
}
|
|
119
139
|
|
|
120
140
|
// libs/core/src/lib/fields/field.types.ts
|
|
121
141
|
var LAYOUT_FIELD_TYPES = /* @__PURE__ */ new Set(["tabs", "collapsible", "row"]);
|
|
@@ -127,6 +147,9 @@ var ReferentialIntegrityError = class extends Error {
|
|
|
127
147
|
this.constraint = constraint;
|
|
128
148
|
}
|
|
129
149
|
};
|
|
150
|
+
function isNamedTab(tab) {
|
|
151
|
+
return typeof tab.name === "string" && tab.name.length > 0;
|
|
152
|
+
}
|
|
130
153
|
function isLayoutField(field) {
|
|
131
154
|
return LAYOUT_FIELD_TYPES.has(field.type);
|
|
132
155
|
}
|
|
@@ -135,7 +158,18 @@ function flattenDataFields(fields) {
|
|
|
135
158
|
for (const field of fields) {
|
|
136
159
|
if (field.type === "tabs") {
|
|
137
160
|
for (const tab of field.tabs) {
|
|
138
|
-
|
|
161
|
+
if (isNamedTab(tab)) {
|
|
162
|
+
const syntheticGroup = {
|
|
163
|
+
name: tab.name,
|
|
164
|
+
type: "group",
|
|
165
|
+
label: tab.label,
|
|
166
|
+
description: tab.description,
|
|
167
|
+
fields: tab.fields
|
|
168
|
+
};
|
|
169
|
+
result.push(syntheticGroup);
|
|
170
|
+
} else {
|
|
171
|
+
result.push(...flattenDataFields(tab.fields));
|
|
172
|
+
}
|
|
139
173
|
}
|
|
140
174
|
} else if (field.type === "collapsible" || field.type === "row") {
|
|
141
175
|
result.push(...flattenDataFields(field.fields));
|
|
@@ -415,6 +449,9 @@ var MediaCollection = defineCollection({
|
|
|
415
449
|
singular: "Media",
|
|
416
450
|
plural: "Media"
|
|
417
451
|
},
|
|
452
|
+
upload: {
|
|
453
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
454
|
+
},
|
|
418
455
|
admin: {
|
|
419
456
|
useAsTitle: "filename",
|
|
420
457
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -435,7 +472,6 @@ var MediaCollection = defineCollection({
|
|
|
435
472
|
description: "File size in bytes"
|
|
436
473
|
}),
|
|
437
474
|
text("path", {
|
|
438
|
-
required: true,
|
|
439
475
|
label: "Storage Path",
|
|
440
476
|
description: "Path/key where the file is stored",
|
|
441
477
|
admin: {
|
|
@@ -546,6 +582,29 @@ function isOwner(ownerField = "createdBy") {
|
|
|
546
582
|
};
|
|
547
583
|
}
|
|
548
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
|
+
|
|
549
608
|
// libs/core/src/lib/config.ts
|
|
550
609
|
var MIN_PASSWORD_LENGTH = 8;
|
|
551
610
|
function defineMomentumConfig(config) {
|
|
@@ -576,7 +635,8 @@ function defineMomentumConfig(config) {
|
|
|
576
635
|
level: config.logging?.level ?? "info",
|
|
577
636
|
format: config.logging?.format ?? "pretty",
|
|
578
637
|
timestamps: config.logging?.timestamps ?? true
|
|
579
|
-
}
|
|
638
|
+
},
|
|
639
|
+
migrations: resolveMigrationConfig(config.migrations)
|
|
580
640
|
};
|
|
581
641
|
}
|
|
582
642
|
function getDbAdapter(config) {
|
|
@@ -702,6 +762,7 @@ function createSeedHelpers() {
|
|
|
702
762
|
getDbAdapter,
|
|
703
763
|
getGlobals,
|
|
704
764
|
getSoftDeleteField,
|
|
765
|
+
getUploadFieldMapping,
|
|
705
766
|
group,
|
|
706
767
|
hasAllRoles,
|
|
707
768
|
hasAnyRole,
|
|
@@ -709,7 +770,9 @@ function createSeedHelpers() {
|
|
|
709
770
|
humanizeFieldName,
|
|
710
771
|
isAuthenticated,
|
|
711
772
|
isLayoutField,
|
|
773
|
+
isNamedTab,
|
|
712
774
|
isOwner,
|
|
775
|
+
isUploadCollection,
|
|
713
776
|
json,
|
|
714
777
|
not,
|
|
715
778
|
number,
|
|
@@ -718,6 +781,8 @@ function createSeedHelpers() {
|
|
|
718
781
|
point,
|
|
719
782
|
radio,
|
|
720
783
|
relationship,
|
|
784
|
+
resolveMigrationConfig,
|
|
785
|
+
resolveMigrationMode,
|
|
721
786
|
richText,
|
|
722
787
|
row,
|
|
723
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"]);
|
|
@@ -51,6 +66,9 @@ var ReferentialIntegrityError = class extends Error {
|
|
|
51
66
|
this.constraint = constraint;
|
|
52
67
|
}
|
|
53
68
|
};
|
|
69
|
+
function isNamedTab(tab) {
|
|
70
|
+
return typeof tab.name === "string" && tab.name.length > 0;
|
|
71
|
+
}
|
|
54
72
|
function isLayoutField(field) {
|
|
55
73
|
return LAYOUT_FIELD_TYPES.has(field.type);
|
|
56
74
|
}
|
|
@@ -59,7 +77,18 @@ function flattenDataFields(fields) {
|
|
|
59
77
|
for (const field of fields) {
|
|
60
78
|
if (field.type === "tabs") {
|
|
61
79
|
for (const tab of field.tabs) {
|
|
62
|
-
|
|
80
|
+
if (isNamedTab(tab)) {
|
|
81
|
+
const syntheticGroup = {
|
|
82
|
+
name: tab.name,
|
|
83
|
+
type: "group",
|
|
84
|
+
label: tab.label,
|
|
85
|
+
description: tab.description,
|
|
86
|
+
fields: tab.fields
|
|
87
|
+
};
|
|
88
|
+
result.push(syntheticGroup);
|
|
89
|
+
} else {
|
|
90
|
+
result.push(...flattenDataFields(tab.fields));
|
|
91
|
+
}
|
|
63
92
|
}
|
|
64
93
|
} else if (field.type === "collapsible" || field.type === "row") {
|
|
65
94
|
result.push(...flattenDataFields(field.fields));
|
|
@@ -339,6 +368,9 @@ var MediaCollection = defineCollection({
|
|
|
339
368
|
singular: "Media",
|
|
340
369
|
plural: "Media"
|
|
341
370
|
},
|
|
371
|
+
upload: {
|
|
372
|
+
mimeTypes: ["image/*", "application/pdf", "video/*", "audio/*"]
|
|
373
|
+
},
|
|
342
374
|
admin: {
|
|
343
375
|
useAsTitle: "filename",
|
|
344
376
|
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
@@ -359,7 +391,6 @@ var MediaCollection = defineCollection({
|
|
|
359
391
|
description: "File size in bytes"
|
|
360
392
|
}),
|
|
361
393
|
text("path", {
|
|
362
|
-
required: true,
|
|
363
394
|
label: "Storage Path",
|
|
364
395
|
description: "Path/key where the file is stored",
|
|
365
396
|
admin: {
|
|
@@ -470,6 +501,29 @@ function isOwner(ownerField = "createdBy") {
|
|
|
470
501
|
};
|
|
471
502
|
}
|
|
472
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
|
+
|
|
473
527
|
// libs/core/src/lib/config.ts
|
|
474
528
|
var MIN_PASSWORD_LENGTH = 8;
|
|
475
529
|
function defineMomentumConfig(config) {
|
|
@@ -500,7 +554,8 @@ function defineMomentumConfig(config) {
|
|
|
500
554
|
level: config.logging?.level ?? "info",
|
|
501
555
|
format: config.logging?.format ?? "pretty",
|
|
502
556
|
timestamps: config.logging?.timestamps ?? true
|
|
503
|
-
}
|
|
557
|
+
},
|
|
558
|
+
migrations: resolveMigrationConfig(config.migrations)
|
|
504
559
|
};
|
|
505
560
|
}
|
|
506
561
|
function getDbAdapter(config) {
|
|
@@ -625,6 +680,7 @@ export {
|
|
|
625
680
|
getDbAdapter,
|
|
626
681
|
getGlobals,
|
|
627
682
|
getSoftDeleteField,
|
|
683
|
+
getUploadFieldMapping,
|
|
628
684
|
group,
|
|
629
685
|
hasAllRoles,
|
|
630
686
|
hasAnyRole,
|
|
@@ -632,7 +688,9 @@ export {
|
|
|
632
688
|
humanizeFieldName,
|
|
633
689
|
isAuthenticated,
|
|
634
690
|
isLayoutField,
|
|
691
|
+
isNamedTab,
|
|
635
692
|
isOwner,
|
|
693
|
+
isUploadCollection,
|
|
636
694
|
json,
|
|
637
695
|
not,
|
|
638
696
|
number,
|
|
@@ -641,6 +699,8 @@ export {
|
|
|
641
699
|
point,
|
|
642
700
|
radio,
|
|
643
701
|
relationship,
|
|
702
|
+
resolveMigrationConfig,
|
|
703
|
+
resolveMigrationMode,
|
|
644
704
|
richText,
|
|
645
705
|
row,
|
|
646
706
|
select,
|
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@ interface FieldDefinition {
|
|
|
31
31
|
editor?: Record<string, unknown>;
|
|
32
32
|
}>;
|
|
33
33
|
tabs?: Array<{
|
|
34
|
+
name?: string;
|
|
34
35
|
label: string;
|
|
35
36
|
description?: string;
|
|
36
37
|
fields: FieldDefinition[];
|
|
@@ -90,6 +91,16 @@ interface CollectionDefinition {
|
|
|
90
91
|
defaultWhere?: unknown;
|
|
91
92
|
endpoints?: unknown[];
|
|
92
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
|
+
};
|
|
93
104
|
}
|
|
94
105
|
interface GlobalDefinition {
|
|
95
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.
|
|
@@ -239,10 +239,16 @@ export interface SlugField extends BaseField {
|
|
|
239
239
|
}
|
|
240
240
|
/** Tab definition within a tabs layout field */
|
|
241
241
|
export interface TabConfig {
|
|
242
|
+
/** When present, creates a nested data structure (like a group). Omit for layout-only tabs. */
|
|
243
|
+
name?: string;
|
|
242
244
|
label: string;
|
|
243
245
|
description?: string;
|
|
244
246
|
fields: Field[];
|
|
245
247
|
}
|
|
248
|
+
/** Type guard: returns true if the tab has a non-empty name (stores nested data). */
|
|
249
|
+
export declare function isNamedTab(tab: TabConfig): tab is TabConfig & {
|
|
250
|
+
name: string;
|
|
251
|
+
};
|
|
246
252
|
/** Tabs layout field - organizes fields into tabbed sections */
|
|
247
253
|
export interface TabsField extends BaseField {
|
|
248
254
|
type: 'tabs';
|
|
@@ -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;
|