@momentumcms/core 0.2.0 → 0.4.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/package.json CHANGED
@@ -1,35 +1,44 @@
1
1
  {
2
- "name": "@momentumcms/core",
3
- "version": "0.2.0",
4
- "description": "Core collection config, fields, hooks, and access control for Momentum CMS",
5
- "license": "MIT",
6
- "author": "Momentum CMS Contributors",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/DonaldMurillo/momentum-cms.git",
10
- "directory": "libs/core"
11
- },
12
- "homepage": "https://github.com/DonaldMurillo/momentum-cms#readme",
13
- "bugs": {
14
- "url": "https://github.com/DonaldMurillo/momentum-cms/issues"
15
- },
16
- "keywords": [
17
- "cms",
18
- "headless-cms",
19
- "angular",
20
- "momentum-cms",
21
- "collections",
22
- "fields",
23
- "content-management"
24
- ],
25
- "engines": {
26
- "node": ">=18"
27
- },
28
- "main": "./index.cjs",
29
- "types": "./src/index.d.ts",
30
- "bin": {
31
- "momentum-generate": "./generators/generator.cjs"
32
- },
33
- "dependencies": {},
34
- "module": "./index.js"
35
- }
2
+ "name": "@momentumcms/core",
3
+ "version": "0.4.0",
4
+ "description": "Core collection config, fields, hooks, and access control for Momentum CMS",
5
+ "license": "MIT",
6
+ "author": "Momentum CMS Contributors",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/DonaldMurillo/momentum-cms.git",
10
+ "directory": "libs/core"
11
+ },
12
+ "homepage": "https://github.com/DonaldMurillo/momentum-cms#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/DonaldMurillo/momentum-cms/issues"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "headless-cms",
19
+ "angular",
20
+ "momentum-cms",
21
+ "collections",
22
+ "fields",
23
+ "content-management"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "main": "./src/index.cjs",
29
+ "module": "./src/index.js",
30
+ "types": "./src/index.d.ts",
31
+ "schematics": "./schematics/collection.json",
32
+ "bin": {
33
+ "momentum-generate": "./src/generators/generator.cjs"
34
+ },
35
+ "peerDependencies": {
36
+ "@angular-devkit/schematics": ">=17.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "@angular-devkit/schematics": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "dependencies": {}
44
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3
+ "schematics": {
4
+ "types": {
5
+ "description": "Generate TypeScript types and browser-safe admin config from momentum.config.ts",
6
+ "factory": "./types/index#generateTypes",
7
+ "schema": "./types/schema.json"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // libs/core/schematics/types/index.ts
21
+ var types_exports = {};
22
+ __export(types_exports, {
23
+ generateTypes: () => generateTypes
24
+ });
25
+ module.exports = __toCommonJS(types_exports);
26
+ var import_node_child_process = require("node:child_process");
27
+ function generateTypes(options) {
28
+ return (_tree, context) => {
29
+ const configPath = options.configPath || "src/momentum.config.ts";
30
+ const typesOutput = options.typesOutput || "src/generated/momentum.types.ts";
31
+ const configOutput = options.configOutput || "src/generated/momentum.config.ts";
32
+ context.logger.info("Generating Momentum CMS types and admin config...");
33
+ context.logger.info(` Config: ${configPath}`);
34
+ context.logger.info(` Types: ${typesOutput}`);
35
+ context.logger.info(` Admin: ${configOutput}`);
36
+ try {
37
+ (0, import_node_child_process.execFileSync)(
38
+ "npx",
39
+ ["momentum-generate", configPath, "--types", typesOutput, "--config", configOutput],
40
+ { stdio: "inherit", shell: true }
41
+ );
42
+ context.logger.info("Types and admin config generated successfully.");
43
+ } catch (error) {
44
+ context.logger.error("Failed to generate types. Is @momentumcms/core installed?");
45
+ throw error;
46
+ }
47
+ return _tree;
48
+ };
49
+ }
50
+ // Annotate the CommonJS export names for ESM import in node:
51
+ 0 && (module.exports = {
52
+ generateTypes
53
+ });
@@ -0,0 +1,28 @@
1
+ // libs/core/schematics/types/index.ts
2
+ import { execFileSync } from "node:child_process";
3
+ function generateTypes(options) {
4
+ return (_tree, context) => {
5
+ const configPath = options.configPath || "src/momentum.config.ts";
6
+ const typesOutput = options.typesOutput || "src/generated/momentum.types.ts";
7
+ const configOutput = options.configOutput || "src/generated/momentum.config.ts";
8
+ context.logger.info("Generating Momentum CMS types and admin config...");
9
+ context.logger.info(` Config: ${configPath}`);
10
+ context.logger.info(` Types: ${typesOutput}`);
11
+ context.logger.info(` Admin: ${configOutput}`);
12
+ try {
13
+ execFileSync(
14
+ "npx",
15
+ ["momentum-generate", configPath, "--types", typesOutput, "--config", configOutput],
16
+ { stdio: "inherit", shell: true }
17
+ );
18
+ context.logger.info("Types and admin config generated successfully.");
19
+ } catch (error) {
20
+ context.logger.error("Failed to generate types. Is @momentumcms/core installed?");
21
+ throw error;
22
+ }
23
+ return _tree;
24
+ };
25
+ }
26
+ export {
27
+ generateTypes
28
+ };
@@ -0,0 +1,5 @@
1
+ export interface TypesSchema {
2
+ configPath: string;
3
+ typesOutput: string;
4
+ configOutput: string;
5
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "MomentumGenerateTypes",
4
+ "title": "Momentum CMS Type Generation",
5
+ "description": "Generate TypeScript types and browser-safe admin config from momentum.config.ts",
6
+ "type": "object",
7
+ "properties": {
8
+ "configPath": {
9
+ "type": "string",
10
+ "description": "Path to momentum.config.ts",
11
+ "default": "src/momentum.config.ts",
12
+ "x-prompt": "Path to your momentum.config.ts file?"
13
+ },
14
+ "typesOutput": {
15
+ "type": "string",
16
+ "description": "Output path for generated TypeScript types",
17
+ "default": "src/generated/momentum.types.ts"
18
+ },
19
+ "configOutput": {
20
+ "type": "string",
21
+ "description": "Output path for generated browser-safe admin config",
22
+ "default": "src/generated/momentum.config.ts"
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -392,6 +392,44 @@ 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 collectFieldNames(fields) {
396
+ const names = [];
397
+ for (const field of fields) {
398
+ if (field.type === "tabs" && field.tabs) {
399
+ for (const tab of field.tabs) {
400
+ if (tab.name) {
401
+ names.push(tab.name);
402
+ } else {
403
+ names.push(...collectFieldNames(tab.fields));
404
+ }
405
+ }
406
+ } else if ((field.type === "collapsible" || field.type === "row") && field.fields) {
407
+ names.push(...collectFieldNames(field.fields));
408
+ } else {
409
+ names.push(field.name);
410
+ }
411
+ }
412
+ return names;
413
+ }
414
+ function previewFunctionToTemplate(fn, fields) {
415
+ try {
416
+ const sentinel = "__MCMS_FIELD_";
417
+ const mockDoc = {};
418
+ for (const name of collectFieldNames(fields)) {
419
+ mockDoc[name] = `${sentinel}${name}__`;
420
+ }
421
+ const result = fn(mockDoc);
422
+ if (typeof result !== "string")
423
+ return true;
424
+ const template = result.replace(
425
+ new RegExp(`${sentinel}(\\w+)__`, "g"),
426
+ (_match, fieldName) => `{${fieldName}}`
427
+ );
428
+ return template;
429
+ } catch {
430
+ return true;
431
+ }
432
+ }
395
433
  function serializeValue(value, indent = " ") {
396
434
  if (value === null)
397
435
  return "null";
@@ -584,9 +622,13 @@ function serializeCollection(collection, indent = " ") {
584
622
  }
585
623
  parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
586
624
  if (collection.admin) {
587
- const adminEntries = Object.entries(collection.admin).filter(
588
- ([, v]) => v !== void 0 && typeof v !== "function"
589
- );
625
+ const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
626
+ if (k === "preview" && typeof v === "function") {
627
+ const fn = v;
628
+ return [k, previewFunctionToTemplate(fn, collection.fields)];
629
+ }
630
+ return [k, v];
631
+ }).filter(([, v]) => typeof v !== "function");
590
632
  if (adminEntries.length > 0) {
591
633
  const adminObj = Object.fromEntries(adminEntries);
592
634
  parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
@@ -610,6 +652,9 @@ function serializeCollection(collection, indent = " ") {
610
652
  if (collection.defaultSort) {
611
653
  parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
612
654
  }
655
+ if (collection.upload !== void 0) {
656
+ parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
657
+ }
613
658
  return `{
614
659
  ${parts.join(",\n")},
615
660
  ${indent}}`;
@@ -739,6 +784,15 @@ function parseArgs(args) {
739
784
  }
740
785
  return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
741
786
  }
787
+ function formatWithPrettier(...filePaths) {
788
+ try {
789
+ (0, import_node_child_process.execFileSync)("npx", ["prettier", "--write", ...filePaths], {
790
+ stdio: "pipe"
791
+ });
792
+ } catch {
793
+ console.warn("prettier not available \u2014 skipping formatting of generated files");
794
+ }
795
+ }
742
796
  async function runGenerator(options) {
743
797
  const configPath = (0, import_node_path.resolve)(options.configPath);
744
798
  const typesOutputPath = (0, import_node_path.resolve)(options.typesOutputPath);
@@ -758,6 +812,7 @@ async function runGenerator(options) {
758
812
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(configOutputPath), { recursive: true });
759
813
  (0, import_node_fs.writeFileSync)(configOutputPath, adminConfigContent, "utf-8");
760
814
  console.info(`Admin config generated: ${configOutputPath}`);
815
+ formatWithPrettier(typesOutputPath, configOutputPath);
761
816
  } catch (error) {
762
817
  console.error(`Error generating:`, error);
763
818
  throw error;
@@ -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;
@@ -361,6 +361,44 @@ 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 collectFieldNames(fields) {
365
+ const names = [];
366
+ for (const field of fields) {
367
+ if (field.type === "tabs" && field.tabs) {
368
+ for (const tab of field.tabs) {
369
+ if (tab.name) {
370
+ names.push(tab.name);
371
+ } else {
372
+ names.push(...collectFieldNames(tab.fields));
373
+ }
374
+ }
375
+ } else if ((field.type === "collapsible" || field.type === "row") && field.fields) {
376
+ names.push(...collectFieldNames(field.fields));
377
+ } else {
378
+ names.push(field.name);
379
+ }
380
+ }
381
+ return names;
382
+ }
383
+ function previewFunctionToTemplate(fn, fields) {
384
+ try {
385
+ const sentinel = "__MCMS_FIELD_";
386
+ const mockDoc = {};
387
+ for (const name of collectFieldNames(fields)) {
388
+ mockDoc[name] = `${sentinel}${name}__`;
389
+ }
390
+ const result = fn(mockDoc);
391
+ if (typeof result !== "string")
392
+ return true;
393
+ const template = result.replace(
394
+ new RegExp(`${sentinel}(\\w+)__`, "g"),
395
+ (_match, fieldName) => `{${fieldName}}`
396
+ );
397
+ return template;
398
+ } catch {
399
+ return true;
400
+ }
401
+ }
364
402
  function serializeValue(value, indent = " ") {
365
403
  if (value === null)
366
404
  return "null";
@@ -553,9 +591,13 @@ function serializeCollection(collection, indent = " ") {
553
591
  }
554
592
  parts.push(`${indent} fields: ${serializeFieldsArray(collection.fields, indent + " ")}`);
555
593
  if (collection.admin) {
556
- const adminEntries = Object.entries(collection.admin).filter(
557
- ([, v]) => v !== void 0 && typeof v !== "function"
558
- );
594
+ const adminEntries = Object.entries(collection.admin).filter(([, v]) => v !== void 0).map(([k, v]) => {
595
+ if (k === "preview" && typeof v === "function") {
596
+ const fn = v;
597
+ return [k, previewFunctionToTemplate(fn, collection.fields)];
598
+ }
599
+ return [k, v];
600
+ }).filter(([, v]) => typeof v !== "function");
559
601
  if (adminEntries.length > 0) {
560
602
  const adminObj = Object.fromEntries(adminEntries);
561
603
  parts.push(`${indent} admin: ${serializeValue(adminObj, indent + " ")}`);
@@ -579,6 +621,9 @@ function serializeCollection(collection, indent = " ") {
579
621
  if (collection.defaultSort) {
580
622
  parts.push(`${indent} defaultSort: ${JSON.stringify(collection.defaultSort)}`);
581
623
  }
624
+ if (collection.upload !== void 0) {
625
+ parts.push(`${indent} upload: ${serializeValue(collection.upload, indent + " ")}`);
626
+ }
582
627
  return `{
583
628
  ${parts.join(",\n")},
584
629
  ${indent}}`;
@@ -708,6 +753,15 @@ function parseArgs(args) {
708
753
  }
709
754
  return { configPath, typesOutputPath, configOutputPath, watch: watchMode };
710
755
  }
756
+ function formatWithPrettier(...filePaths) {
757
+ try {
758
+ execFileSync("npx", ["prettier", "--write", ...filePaths], {
759
+ stdio: "pipe"
760
+ });
761
+ } catch {
762
+ console.warn("prettier not available \u2014 skipping formatting of generated files");
763
+ }
764
+ }
711
765
  async function runGenerator(options) {
712
766
  const configPath = resolve(options.configPath);
713
767
  const typesOutputPath = resolve(options.typesOutputPath);
@@ -727,6 +781,7 @@ async function runGenerator(options) {
727
781
  mkdirSync(dirname(configOutputPath), { recursive: true });
728
782
  writeFileSync(configOutputPath, adminConfigContent, "utf-8");
729
783
  console.info(`Admin config generated: ${configOutputPath}`);
784
+ formatWithPrettier(typesOutputPath, configOutputPath);
730
785
  } catch (error) {
731
786
  console.error(`Error generating:`, error);
732
787
  throw error;
@@ -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/src/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from './lib/plugins';
12
12
  export * from './lib/storage';
13
13
  export * from './lib/seeding';
14
14
  export * from './lib/versions';
15
+ export * from './lib/migrations';
@@ -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,
@@ -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';
@@ -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;
@@ -17,8 +17,14 @@ export interface PluginMiddlewareDescriptor {
17
17
  path: string;
18
18
  /** Express Router or middleware function. Typed as unknown to avoid Express dependency in core. */
19
19
  handler: unknown;
20
- /** Where to mount relative to collection CRUD routes. @default 'before-api' */
21
- position?: 'before-api' | 'after-api';
20
+ /**
21
+ * Where to mount relative to collection CRUD routes.
22
+ * - 'before-api': Before collection CRUD routes (under /api)
23
+ * - 'after-api': After collection CRUD routes (under /api)
24
+ * - 'root': Mounted at the application root (not under /api)
25
+ * @default 'before-api'
26
+ */
27
+ position?: 'before-api' | 'after-api' | 'root';
22
28
  }
23
29
  /**
24
30
  * Descriptor for Angular DI providers that a plugin wants auto-registered during SSR.
package/CHANGELOG.md DELETED
@@ -1,117 +0,0 @@
1
- ## 0.2.0 (2026-02-17)
2
-
3
- ### 🚀 Features
4
-
5
- - add named tabs support with nested data grouping and tab UI improvements ([63ab63e](https://github.com/DonaldMurillo/momentum-cms/commit/63ab63e))
6
-
7
- ### ❤️ Thank You
8
-
9
- - Claude Opus 4.6
10
- - Donald Murillo @DonaldMurillo
11
-
12
- ## 0.1.10 (2026-02-17)
13
-
14
- ### 🩹 Fixes
15
-
16
- - **create-momentum-app:** add shell option to execFileSync for Windows ([#28](https://github.com/DonaldMurillo/momentum-cms/pull/28))
17
-
18
- ### ❤️ Thank You
19
-
20
- - Claude Opus 4.6
21
- - Donald Murillo @DonaldMurillo
22
-
23
- ## 0.1.9 (2026-02-16)
24
-
25
- This was a version bump only for core to align it with other projects, there were no code changes.
26
-
27
- ## 0.1.8 (2026-02-16)
28
-
29
- ### 🩹 Fixes
30
-
31
- - correct repository URLs and add GitHub link to CLI ([#26](https://github.com/DonaldMurillo/momentum-cms/pull/26))
32
-
33
- ### ❤️ Thank You
34
-
35
- - Claude Opus 4.6
36
- - Donald Murillo @DonaldMurillo
37
-
38
- ## 0.1.7 (2026-02-16)
39
-
40
- ### 🩹 Fixes
41
-
42
- - correct repository URLs and add GitHub link to CLI output ([f7e96bb](https://github.com/DonaldMurillo/momentum-cms/commit/f7e96bb))
43
-
44
- ### ❤️ Thank You
45
-
46
- - Claude Opus 4.6
47
- - Donald Murillo @DonaldMurillo
48
-
49
- ## 0.1.6 (2026-02-16)
50
-
51
- This was a version bump only for core to align it with other projects, there were no code changes.
52
-
53
- ## 0.1.5 (2026-02-16)
54
-
55
- ### 🚀 Features
56
-
57
- - **create-app:** add landing page, fix setup flow, theme detection, type generator, Playwright E2E ([5e0f4ed](https://github.com/DonaldMurillo/momentum-cms/commit/5e0f4ed))
58
-
59
- ### ❤️ Thank You
60
-
61
- - Claude Opus 4.6
62
- - Donald Murillo @DonaldMurillo
63
-
64
- ## 0.1.4 (2026-02-16)
65
-
66
- This was a version bump only for core to align it with other projects, there were no code changes.
67
-
68
- ## 0.1.3 (2026-02-16)
69
-
70
- This was a version bump only for core to align it with other projects, there were no code changes.
71
-
72
- ## 0.1.2 (2026-02-16)
73
-
74
- ### 🩹 Fixes
75
-
76
- - **release:** centralize manifestRootsToUpdate to update both source and dist ([2b8f832](https://github.com/DonaldMurillo/momentum-cms/commit/2b8f832))
77
- - **create-app:** fix Angular SSR, Analog builds, and CJS/ESM compatibility ([28d4d0a](https://github.com/DonaldMurillo/momentum-cms/commit/28d4d0a))
78
-
79
- ### ❤️ Thank You
80
-
81
- - Claude Opus 4.6
82
- - Donald Murillo @DonaldMurillo
83
-
84
- ## 0.1.1 (2026-02-16)
85
-
86
- This was a version bump only for core to align it with other projects, there were no code changes.
87
-
88
- ## 0.1.0 (2026-02-16)
89
-
90
- ### 🚀 Features
91
-
92
- - implement soft deletes with full stack support ([#22](https://github.com/DonaldMurillo/momentum-cms/pull/22))
93
- - add tracking rules, content performance, and block analytics ([#21](https://github.com/DonaldMurillo/momentum-cms/pull/21))
94
- - implement globals (singleton collections) with full stack support ([#20](https://github.com/DonaldMurillo/momentum-cms/pull/20))
95
- - visual block editor & auth-gated admin mode ([#18](https://github.com/DonaldMurillo/momentum-cms/pull/18))
96
- - Add display formatting and complex field rendering ([#14](https://github.com/DonaldMurillo/momentum-cms/pull/14))
97
- - UI polish fixes and database-level FK constraints for relationship integrity ([#13](https://github.com/DonaldMurillo/momentum-cms/pull/13))
98
- - Add document versioning and drafts system ([#5](https://github.com/DonaldMurillo/momentum-cms/pull/5))
99
- - Add seeding feature with idempotent data initialization ([#1](https://github.com/DonaldMurillo/momentum-cms/pull/1))
100
- - Add typed access control helper functions ([980d8d0](https://github.com/DonaldMurillo/momentum-cms/commit/980d8d0))
101
- - Add type-safe Momentum API with signal support ([aee6c02](https://github.com/DonaldMurillo/momentum-cms/commit/aee6c02))
102
- - Implement admin UI with API integration and SSR hydration ([9ed7b2b](https://github.com/DonaldMurillo/momentum-cms/commit/9ed7b2b))
103
- - Initialize Momentum CMS foundation ([f64f581](https://github.com/DonaldMurillo/momentum-cms/commit/f64f581))
104
-
105
- ### 🩹 Fixes
106
-
107
- - resolve CUD toast interceptor issues ([#17](https://github.com/DonaldMurillo/momentum-cms/pull/17), [#1](https://github.com/DonaldMurillo/momentum-cms/issues/1), [#2](https://github.com/DonaldMurillo/momentum-cms/issues/2), [#3](https://github.com/DonaldMurillo/momentum-cms/issues/3), [#4](https://github.com/DonaldMurillo/momentum-cms/issues/4))
108
- - address 7 critical and high-severity security and validation bugs ([#12](https://github.com/DonaldMurillo/momentum-cms/pull/12))
109
- - address security vulnerabilities from code review ([#9](https://github.com/DonaldMurillo/momentum-cms/pull/9))
110
- - address security and reliability issues from code review ([#7](https://github.com/DonaldMurillo/momentum-cms/pull/7))
111
-
112
- ### ❤️ Thank You
113
-
114
- - Claude Haiku 4.5
115
- - Claude Opus 4.5
116
- - Claude Opus 4.6
117
- - Donald Murillo @DonaldMurillo
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024-present Momentum CMS Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/README.md DELETED
@@ -1,11 +0,0 @@
1
- # core
2
-
3
- This library was generated with [Nx](https://nx.dev).
4
-
5
- ## Building
6
-
7
- Run `nx build core` to build the library.
8
-
9
- ## Running unit tests
10
-
11
- Run `nx test core` to execute the unit tests via [Jest](https://jestjs.io).