@momentumcms/core 0.1.0 → 0.1.3

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/index.js +653 -0
  3. package/package.json +31 -31
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 0.1.3 (2026-02-16)
2
+
3
+ This was a version bump only for core to align it with other projects, there were no code changes.
4
+
5
+ ## 0.1.2 (2026-02-16)
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - **release:** centralize manifestRootsToUpdate to update both source and dist ([2b8f832](https://github.com/DonaldMurillo/momentum-cms/commit/2b8f832))
10
+ - **create-app:** fix Angular SSR, Analog builds, and CJS/ESM compatibility ([28d4d0a](https://github.com/DonaldMurillo/momentum-cms/commit/28d4d0a))
11
+
12
+ ### ❤️ Thank You
13
+
14
+ - Claude Opus 4.6
15
+ - Donald Murillo @DonaldMurillo
16
+
17
+ ## 0.1.1 (2026-02-16)
18
+
19
+ This was a version bump only for core to align it with other projects, there were no code changes.
20
+
1
21
  ## 0.1.0 (2026-02-16)
2
22
 
3
23
  ### 🚀 Features
package/index.js ADDED
@@ -0,0 +1,653 @@
1
+ // libs/core/src/lib/collections/define-collection.ts
2
+ function defineCollection(config) {
3
+ const collection = {
4
+ timestamps: true,
5
+ // Enable timestamps by default
6
+ ...config
7
+ };
8
+ if (!collection.slug) {
9
+ throw new Error("Collection must have a slug");
10
+ }
11
+ if (!collection.fields || collection.fields.length === 0) {
12
+ throw new Error(`Collection "${collection.slug}" must have at least one field`);
13
+ }
14
+ if (!/^[a-z][a-z0-9-]*$/.test(collection.slug)) {
15
+ throw new Error(
16
+ `Collection slug "${collection.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
17
+ );
18
+ }
19
+ return collection;
20
+ }
21
+ function defineGlobal(config) {
22
+ if (!config.slug) {
23
+ throw new Error("Global must have a slug");
24
+ }
25
+ if (!config.fields || config.fields.length === 0) {
26
+ throw new Error(`Global "${config.slug}" must have at least one field`);
27
+ }
28
+ if (!/^[a-z][a-z0-9-]*$/.test(config.slug)) {
29
+ throw new Error(
30
+ `Global slug "${config.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
31
+ );
32
+ }
33
+ return config;
34
+ }
35
+ function getSoftDeleteField(config) {
36
+ if (!config.softDelete)
37
+ return null;
38
+ if (config.softDelete === true)
39
+ return "deletedAt";
40
+ const sdConfig = config.softDelete;
41
+ return sdConfig.field ?? "deletedAt";
42
+ }
43
+
44
+ // libs/core/src/lib/fields/field.types.ts
45
+ var LAYOUT_FIELD_TYPES = /* @__PURE__ */ new Set(["tabs", "collapsible", "row"]);
46
+ var ReferentialIntegrityError = class extends Error {
47
+ constructor(table, constraint) {
48
+ super(`Cannot delete from "${table}": referenced by foreign key constraint "${constraint}"`);
49
+ this.name = "ReferentialIntegrityError";
50
+ this.table = table;
51
+ this.constraint = constraint;
52
+ }
53
+ };
54
+ function isLayoutField(field) {
55
+ return LAYOUT_FIELD_TYPES.has(field.type);
56
+ }
57
+ function flattenDataFields(fields) {
58
+ const result = [];
59
+ for (const field of fields) {
60
+ if (field.type === "tabs") {
61
+ for (const tab of field.tabs) {
62
+ result.push(...flattenDataFields(tab.fields));
63
+ }
64
+ } else if (field.type === "collapsible" || field.type === "row") {
65
+ result.push(...flattenDataFields(field.fields));
66
+ } else {
67
+ result.push(field);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ // libs/core/src/lib/fields/field-builders.ts
74
+ function text(name, options = {}) {
75
+ return {
76
+ name,
77
+ type: "text",
78
+ ...options
79
+ };
80
+ }
81
+ function textarea(name, options = {}) {
82
+ return {
83
+ name,
84
+ type: "textarea",
85
+ ...options
86
+ };
87
+ }
88
+ function richText(name, options = {}) {
89
+ return {
90
+ name,
91
+ type: "richText",
92
+ ...options
93
+ };
94
+ }
95
+ function number(name, options = {}) {
96
+ return {
97
+ name,
98
+ type: "number",
99
+ ...options
100
+ };
101
+ }
102
+ function date(name, options = {}) {
103
+ return {
104
+ name,
105
+ type: "date",
106
+ ...options
107
+ };
108
+ }
109
+ function checkbox(name, options = {}) {
110
+ return {
111
+ name,
112
+ type: "checkbox",
113
+ ...options,
114
+ defaultValue: options.defaultValue ?? false
115
+ };
116
+ }
117
+ function select(name, options) {
118
+ return {
119
+ name,
120
+ type: "select",
121
+ ...options
122
+ };
123
+ }
124
+ function radio(name, options) {
125
+ return {
126
+ name,
127
+ type: "radio",
128
+ ...options
129
+ };
130
+ }
131
+ function email(name, options = {}) {
132
+ return {
133
+ name,
134
+ type: "email",
135
+ ...options
136
+ };
137
+ }
138
+ function password(name, options = {}) {
139
+ return {
140
+ name,
141
+ type: "password",
142
+ ...options
143
+ };
144
+ }
145
+ function upload(name, options = {}) {
146
+ return {
147
+ name,
148
+ type: "upload",
149
+ relationTo: options.relationTo ?? "media",
150
+ ...options
151
+ };
152
+ }
153
+ function relationship(name, options) {
154
+ return {
155
+ name,
156
+ type: "relationship",
157
+ ...options
158
+ };
159
+ }
160
+ function array(name, options) {
161
+ return {
162
+ name,
163
+ type: "array",
164
+ ...options
165
+ };
166
+ }
167
+ function group(name, options) {
168
+ return {
169
+ name,
170
+ type: "group",
171
+ ...options
172
+ };
173
+ }
174
+ function blocks(name, options) {
175
+ return {
176
+ name,
177
+ type: "blocks",
178
+ ...options
179
+ };
180
+ }
181
+ function json(name, options = {}) {
182
+ return {
183
+ name,
184
+ type: "json",
185
+ ...options
186
+ };
187
+ }
188
+ function point(name, options = {}) {
189
+ return {
190
+ name,
191
+ type: "point",
192
+ ...options
193
+ };
194
+ }
195
+ function slug(name, options) {
196
+ return {
197
+ name,
198
+ type: "slug",
199
+ ...options
200
+ };
201
+ }
202
+ function tabs(name, options) {
203
+ return {
204
+ name,
205
+ type: "tabs",
206
+ ...options
207
+ };
208
+ }
209
+ function collapsible(name, options) {
210
+ return {
211
+ name,
212
+ type: "collapsible",
213
+ ...options
214
+ };
215
+ }
216
+ function row(name, options) {
217
+ return {
218
+ name,
219
+ type: "row",
220
+ ...options
221
+ };
222
+ }
223
+
224
+ // libs/core/src/lib/fields/humanize-field-name.ts
225
+ function humanizeFieldName(name) {
226
+ if (!name)
227
+ return "";
228
+ return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()).trim();
229
+ }
230
+
231
+ // libs/core/src/lib/fields/field-validators.ts
232
+ var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
233
+ function validateFieldConstraints(field, value) {
234
+ if (value === null || value === void 0) {
235
+ return [];
236
+ }
237
+ const label = field.label ?? humanizeFieldName(field.name);
238
+ const errors = [];
239
+ switch (field.type) {
240
+ case "text":
241
+ case "textarea":
242
+ if (typeof value === "string") {
243
+ validateStringLength(field.name, label, value, field.minLength, field.maxLength, errors);
244
+ }
245
+ break;
246
+ case "password":
247
+ if (typeof value === "string" && field.minLength !== void 0) {
248
+ validateStringLength(field.name, label, value, field.minLength, void 0, errors);
249
+ }
250
+ break;
251
+ case "number":
252
+ if (typeof value === "number") {
253
+ if (field.min !== void 0 && value < field.min) {
254
+ errors.push({ field: field.name, message: `${label} must be at least ${field.min}` });
255
+ }
256
+ if (field.max !== void 0 && value > field.max) {
257
+ errors.push({
258
+ field: field.name,
259
+ message: `${label} must be no more than ${field.max}`
260
+ });
261
+ }
262
+ if (field.step !== void 0 && field.step > 0) {
263
+ const remainder = Math.abs(Math.round(value / field.step * 1e10) % Math.round(1e10));
264
+ if (remainder > 1) {
265
+ errors.push({
266
+ field: field.name,
267
+ message: `${label} must be a multiple of ${field.step}`
268
+ });
269
+ }
270
+ }
271
+ }
272
+ break;
273
+ case "email":
274
+ if (typeof value === "string" && value !== "" && !EMAIL_REGEX.test(value)) {
275
+ errors.push({
276
+ field: field.name,
277
+ message: `${label} must be a valid email address`
278
+ });
279
+ }
280
+ break;
281
+ case "select":
282
+ validateSelectOptions(field.name, label, value, field.options, field.hasMany, errors);
283
+ break;
284
+ case "radio":
285
+ validateSelectOptions(field.name, label, value, field.options, false, errors);
286
+ break;
287
+ case "array":
288
+ if (Array.isArray(value)) {
289
+ validateRowCount(field.name, label, value.length, field.minRows, field.maxRows, errors);
290
+ }
291
+ break;
292
+ case "blocks":
293
+ if (Array.isArray(value)) {
294
+ validateRowCount(field.name, label, value.length, field.minRows, field.maxRows, errors);
295
+ }
296
+ break;
297
+ }
298
+ return errors;
299
+ }
300
+ function validateStringLength(name, label, value, minLength, maxLength, errors) {
301
+ if (minLength !== void 0 && value.length < minLength) {
302
+ errors.push({ field: name, message: `${label} must be at least ${minLength} characters` });
303
+ }
304
+ if (maxLength !== void 0 && value.length > maxLength) {
305
+ errors.push({
306
+ field: name,
307
+ message: `${label} must be no more than ${maxLength} characters`
308
+ });
309
+ }
310
+ }
311
+ function validateSelectOptions(name, label, value, options, hasMany, errors) {
312
+ if (value === "")
313
+ return;
314
+ const validValues = new Set(options.map((o) => o.value));
315
+ if (hasMany && Array.isArray(value)) {
316
+ const allValid = value.every((v) => validValues.has(v));
317
+ if (!allValid) {
318
+ errors.push({ field: name, message: `${label} has an invalid selection` });
319
+ }
320
+ } else if (!Array.isArray(value)) {
321
+ if (!validValues.has(value)) {
322
+ errors.push({ field: name, message: `${label} has an invalid selection` });
323
+ }
324
+ }
325
+ }
326
+ function validateRowCount(name, label, count, minRows, maxRows, errors) {
327
+ if (minRows !== void 0 && count < minRows) {
328
+ errors.push({ field: name, message: `${label} requires at least ${minRows} rows` });
329
+ }
330
+ if (maxRows !== void 0 && count > maxRows) {
331
+ errors.push({ field: name, message: `${label} allows at most ${maxRows} rows` });
332
+ }
333
+ }
334
+
335
+ // libs/core/src/lib/collections/media.collection.ts
336
+ var MediaCollection = defineCollection({
337
+ slug: "media",
338
+ labels: {
339
+ singular: "Media",
340
+ plural: "Media"
341
+ },
342
+ admin: {
343
+ useAsTitle: "filename",
344
+ defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
345
+ },
346
+ fields: [
347
+ text("filename", {
348
+ required: true,
349
+ label: "Filename",
350
+ description: "Original filename of the uploaded file"
351
+ }),
352
+ text("mimeType", {
353
+ required: true,
354
+ label: "MIME Type",
355
+ description: "File MIME type (e.g., image/jpeg, application/pdf)"
356
+ }),
357
+ number("filesize", {
358
+ label: "File Size",
359
+ description: "File size in bytes"
360
+ }),
361
+ text("path", {
362
+ required: true,
363
+ label: "Storage Path",
364
+ description: "Path/key where the file is stored",
365
+ admin: {
366
+ hidden: true
367
+ }
368
+ }),
369
+ text("url", {
370
+ label: "URL",
371
+ description: "Public URL to access the file"
372
+ }),
373
+ text("alt", {
374
+ label: "Alt Text",
375
+ description: "Alternative text for accessibility"
376
+ }),
377
+ number("width", {
378
+ label: "Width",
379
+ description: "Image width in pixels (for images only)"
380
+ }),
381
+ number("height", {
382
+ label: "Height",
383
+ description: "Image height in pixels (for images only)"
384
+ }),
385
+ json("focalPoint", {
386
+ label: "Focal Point",
387
+ description: "Focal point coordinates for image cropping",
388
+ admin: {
389
+ hidden: true
390
+ }
391
+ })
392
+ ],
393
+ access: {
394
+ // Media is readable by anyone by default
395
+ read: () => true,
396
+ // Only authenticated users can create/update/delete
397
+ create: ({ req }) => !!req?.user,
398
+ update: ({ req }) => !!req?.user,
399
+ delete: ({ req }) => !!req?.user
400
+ }
401
+ });
402
+
403
+ // libs/core/src/lib/access/access-helpers.ts
404
+ function access(callback) {
405
+ return ({ req, id, data }) => {
406
+ const user = req.user;
407
+ return callback({ user, id, data });
408
+ };
409
+ }
410
+ function allowAll() {
411
+ return () => true;
412
+ }
413
+ function denyAll() {
414
+ return () => false;
415
+ }
416
+ function isAuthenticated() {
417
+ return ({ req }) => !!req.user;
418
+ }
419
+ function hasRole(role) {
420
+ return ({ req }) => req.user?.role === role;
421
+ }
422
+ function hasAnyRole(roles) {
423
+ return ({ req }) => {
424
+ const userRole = req.user?.role;
425
+ return userRole !== void 0 && roles.includes(userRole);
426
+ };
427
+ }
428
+ function hasAllRoles(roles) {
429
+ return ({ req }) => {
430
+ const userRoles = req.user?.["roles"];
431
+ if (!Array.isArray(userRoles))
432
+ return false;
433
+ return roles.every((role) => userRoles.includes(role));
434
+ };
435
+ }
436
+ function and(...fns) {
437
+ return async (args) => {
438
+ for (const fn of fns) {
439
+ const result = await fn(args);
440
+ if (!result)
441
+ return false;
442
+ }
443
+ return true;
444
+ };
445
+ }
446
+ function or(...fns) {
447
+ return async (args) => {
448
+ for (const fn of fns) {
449
+ const result = await fn(args);
450
+ if (result)
451
+ return true;
452
+ }
453
+ return false;
454
+ };
455
+ }
456
+ function not(fn) {
457
+ return async (args) => {
458
+ const result = await fn(args);
459
+ return !result;
460
+ };
461
+ }
462
+ function isOwner(ownerField = "createdBy") {
463
+ return ({ req, data }) => {
464
+ if (!req.user?.id)
465
+ return false;
466
+ const ownerId = data?.[ownerField];
467
+ if (ownerId === void 0 || ownerId === null)
468
+ return false;
469
+ return ownerId === req.user.id || String(ownerId) === String(req.user.id);
470
+ };
471
+ }
472
+
473
+ // libs/core/src/lib/config.ts
474
+ var MIN_PASSWORD_LENGTH = 8;
475
+ function defineMomentumConfig(config) {
476
+ return {
477
+ ...config,
478
+ admin: {
479
+ basePath: config.admin?.basePath ?? "/admin",
480
+ branding: config.admin?.branding ?? {},
481
+ toasts: config.admin?.toasts ?? true
482
+ },
483
+ server: {
484
+ port: config.server?.port ?? 3e3,
485
+ cors: config.server?.cors ?? {
486
+ origin: "*",
487
+ methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
488
+ headers: ["Content-Type", "Authorization", "X-API-Key"]
489
+ }
490
+ },
491
+ seeding: config.seeding ? {
492
+ ...config.seeding,
493
+ options: {
494
+ onConflict: config.seeding.options?.onConflict ?? "skip",
495
+ runOnStart: config.seeding.options?.runOnStart ?? "development",
496
+ quiet: config.seeding.options?.quiet ?? false
497
+ }
498
+ } : void 0,
499
+ logging: {
500
+ level: config.logging?.level ?? "info",
501
+ format: config.logging?.format ?? "pretty",
502
+ timestamps: config.logging?.timestamps ?? true
503
+ }
504
+ };
505
+ }
506
+ function getDbAdapter(config) {
507
+ return config.db.adapter;
508
+ }
509
+ function getCollections(config) {
510
+ return config.collections;
511
+ }
512
+ function getGlobals(config) {
513
+ return config.globals ?? [];
514
+ }
515
+
516
+ // libs/core/src/lib/seeding/seeding.types.ts
517
+ var SEED_TRACKING_COLLECTION_SLUG = "_momentum_seeds";
518
+ var SeedConflictError = class extends Error {
519
+ constructor(seedId, collection) {
520
+ super(`Seed conflict: seedId "${seedId}" already exists in collection "${collection}"`);
521
+ this.name = "SeedConflictError";
522
+ this.seedId = seedId;
523
+ this.collection = collection;
524
+ }
525
+ };
526
+ var SeedRollbackError = class extends Error {
527
+ constructor(originalError, rolledBackSeeds, rollbackFailures) {
528
+ const rollbackStatus = rollbackFailures.length > 0 ? `Rollback partially failed: ${rolledBackSeeds.length} rolled back, ${rollbackFailures.length} failed` : `Rollback successful: ${rolledBackSeeds.length} seeds removed`;
529
+ super(`Seeding failed: ${originalError.message}. ${rollbackStatus}`);
530
+ this.name = "SeedRollbackError";
531
+ this.originalError = originalError;
532
+ this.rolledBackSeeds = rolledBackSeeds;
533
+ this.rollbackFailures = rollbackFailures;
534
+ }
535
+ };
536
+
537
+ // libs/core/src/lib/seeding/seed-helpers.ts
538
+ function createSeedHelpers() {
539
+ return {
540
+ admin(seedId, data, options) {
541
+ return {
542
+ seedId,
543
+ collection: "user",
544
+ // Better Auth user table
545
+ data: {
546
+ role: "admin",
547
+ // Admin role by default
548
+ emailVerified: true,
549
+ // Admins are pre-verified
550
+ ...data
551
+ },
552
+ options
553
+ };
554
+ },
555
+ user(seedId, data, options) {
556
+ return {
557
+ seedId,
558
+ collection: "user",
559
+ // Better Auth user table
560
+ data: {
561
+ role: "user",
562
+ // Default role
563
+ emailVerified: false,
564
+ // Default not verified
565
+ ...data
566
+ },
567
+ options
568
+ };
569
+ },
570
+ authUser(seedId, data, options) {
571
+ return {
572
+ seedId,
573
+ collection: "user",
574
+ // Better Auth user table (auth-user collection with dbName: 'user')
575
+ data: {
576
+ role: "user",
577
+ emailVerified: true,
578
+ ...data
579
+ },
580
+ options: {
581
+ ...options,
582
+ useAuthSignup: true
583
+ }
584
+ };
585
+ },
586
+ collection(slug2) {
587
+ return {
588
+ create(seedId, data, options) {
589
+ return {
590
+ seedId,
591
+ collection: slug2,
592
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- User provides partial data that will be merged with defaults during seeding
593
+ data,
594
+ options
595
+ };
596
+ }
597
+ };
598
+ }
599
+ };
600
+ }
601
+ export {
602
+ LAYOUT_FIELD_TYPES,
603
+ MIN_PASSWORD_LENGTH,
604
+ MediaCollection,
605
+ ReferentialIntegrityError,
606
+ SEED_TRACKING_COLLECTION_SLUG,
607
+ SeedConflictError,
608
+ SeedRollbackError,
609
+ access,
610
+ allowAll,
611
+ and,
612
+ array,
613
+ blocks,
614
+ checkbox,
615
+ collapsible,
616
+ createSeedHelpers,
617
+ date,
618
+ defineCollection,
619
+ defineGlobal,
620
+ defineMomentumConfig,
621
+ denyAll,
622
+ email,
623
+ flattenDataFields,
624
+ getCollections,
625
+ getDbAdapter,
626
+ getGlobals,
627
+ getSoftDeleteField,
628
+ group,
629
+ hasAllRoles,
630
+ hasAnyRole,
631
+ hasRole,
632
+ humanizeFieldName,
633
+ isAuthenticated,
634
+ isLayoutField,
635
+ isOwner,
636
+ json,
637
+ not,
638
+ number,
639
+ or,
640
+ password,
641
+ point,
642
+ radio,
643
+ relationship,
644
+ richText,
645
+ row,
646
+ select,
647
+ slug,
648
+ tabs,
649
+ text,
650
+ textarea,
651
+ upload,
652
+ validateFieldConstraints
653
+ };
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
- "name": "@momentumcms/core",
3
- "version": "0.1.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/momentum-cms/momentum-cms.git",
10
- "directory": "libs/core"
11
- },
12
- "homepage": "https://github.com/momentum-cms/momentum-cms#readme",
13
- "bugs": {
14
- "url": "https://github.com/momentum-cms/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
- "type": "commonjs",
29
- "main": "./index.cjs",
30
- "types": "./src/index.d.ts",
31
- "dependencies": {}
32
- }
2
+ "name": "@momentumcms/core",
3
+ "version": "0.1.3",
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/momentum-cms/momentum-cms.git",
10
+ "directory": "libs/core"
11
+ },
12
+ "homepage": "https://github.com/momentum-cms/momentum-cms#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/momentum-cms/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
+ "dependencies": {},
31
+ "module": "./index.js"
32
+ }