@momentumcms/core 0.0.1

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