@momentumcms/core 0.5.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Core collection config, fields, hooks, and access control for Momentum CMS",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -711,7 +711,9 @@ function generateAdminConfig(config, typesRelPath) {
711
711
  }
712
712
  lines.push(`import type { ${typeImports.join(", ")} } from '${typesRelPath}';`);
713
713
  for (const plugin of pluginsWithAdminRoutes) {
714
- const imp = plugin.browserImports.adminRoutes;
714
+ const imp = plugin.browserImports?.adminRoutes;
715
+ if (!imp)
716
+ continue;
715
717
  lines.push(`import { ${imp.exportName} } from '${imp.path}';`);
716
718
  }
717
719
  lines.push("");
@@ -744,9 +746,11 @@ ${globalItems},
744
746
  }
745
747
  }
746
748
  if (pluginsWithAdminRoutes.length > 0) {
747
- const pluginItems = pluginsWithAdminRoutes.map((p) => {
748
- const imp = p.browserImports.adminRoutes;
749
- return ` { name: ${JSON.stringify(p.name)}, adminRoutes: ${imp.exportName} }`;
749
+ const pluginItems = pluginsWithAdminRoutes.flatMap((p) => {
750
+ const imp = p.browserImports?.adminRoutes;
751
+ if (!imp)
752
+ return [];
753
+ return [` { name: ${JSON.stringify(p.name)}, adminRoutes: ${imp.exportName} }`];
750
754
  }).join(",\n");
751
755
  lines.push(` plugins: [
752
756
  ${pluginItems},
@@ -680,7 +680,9 @@ function generateAdminConfig(config, typesRelPath) {
680
680
  }
681
681
  lines.push(`import type { ${typeImports.join(", ")} } from '${typesRelPath}';`);
682
682
  for (const plugin of pluginsWithAdminRoutes) {
683
- const imp = plugin.browserImports.adminRoutes;
683
+ const imp = plugin.browserImports?.adminRoutes;
684
+ if (!imp)
685
+ continue;
684
686
  lines.push(`import { ${imp.exportName} } from '${imp.path}';`);
685
687
  }
686
688
  lines.push("");
@@ -713,9 +715,11 @@ ${globalItems},
713
715
  }
714
716
  }
715
717
  if (pluginsWithAdminRoutes.length > 0) {
716
- const pluginItems = pluginsWithAdminRoutes.map((p) => {
717
- const imp = p.browserImports.adminRoutes;
718
- return ` { name: ${JSON.stringify(p.name)}, adminRoutes: ${imp.exportName} }`;
718
+ const pluginItems = pluginsWithAdminRoutes.flatMap((p) => {
719
+ const imp = p.browserImports?.adminRoutes;
720
+ if (!imp)
721
+ return [];
722
+ return [` { name: ${JSON.stringify(p.name)}, adminRoutes: ${imp.exportName} }`];
719
723
  }).join(",\n");
720
724
  lines.push(` plugins: [
721
725
  ${pluginItems},
package/src/index.cjs CHANGED
@@ -128,6 +128,8 @@ function getUploadFieldMapping(config) {
128
128
  if (!isUploadCollection(config))
129
129
  return null;
130
130
  const u = config.upload;
131
+ if (!u)
132
+ return null;
131
133
  return {
132
134
  filename: u.filenameField ?? "filename",
133
135
  mimeType: u.mimeTypeField ?? "mimeType",
@@ -586,7 +588,7 @@ function isOwner(ownerField = "createdBy") {
586
588
  function resolveMigrationMode(mode) {
587
589
  if (mode === "push" || mode === "migrate")
588
590
  return mode;
589
- const env = process.env["NODE_ENV"];
591
+ const env = globalThis["process"]?.env?.["NODE_ENV"];
590
592
  if (env === "production")
591
593
  return "migrate";
592
594
  return "push";
package/src/index.d.ts CHANGED
@@ -13,3 +13,5 @@ export * from './lib/storage';
13
13
  export * from './lib/seeding';
14
14
  export * from './lib/versions';
15
15
  export * from './lib/migrations';
16
+ export * from './lib/queue';
17
+ export * from './lib/cron';
package/src/index.js CHANGED
@@ -47,6 +47,8 @@ function getUploadFieldMapping(config) {
47
47
  if (!isUploadCollection(config))
48
48
  return null;
49
49
  const u = config.upload;
50
+ if (!u)
51
+ return null;
50
52
  return {
51
53
  filename: u.filenameField ?? "filename",
52
54
  mimeType: u.mimeTypeField ?? "mimeType",
@@ -505,7 +507,7 @@ function isOwner(ownerField = "createdBy") {
505
507
  function resolveMigrationMode(mode) {
506
508
  if (mode === "push" || mode === "migrate")
507
509
  return mode;
508
- const env = process.env["NODE_ENV"];
510
+ const env = globalThis["process"]?.env?.["NODE_ENV"];
509
511
  if (env === "production")
510
512
  return "migrate";
511
513
  return "push";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cron module for Momentum CMS
3
+ * Defines types for recurring job schedules
4
+ */
5
+ import type { JobPriority } from '../queue';
6
+ /**
7
+ * A recurring job schedule definition.
8
+ * Used by the cron plugin to periodically enqueue jobs into the queue.
9
+ */
10
+ export interface RecurringJobDefinition {
11
+ /** Unique name for this recurring schedule */
12
+ name: string;
13
+ /** Job type to enqueue (must match a registered handler in the queue plugin) */
14
+ type: string;
15
+ /** Cron expression (5-field: minute hour day-of-month month day-of-week) */
16
+ cron: string;
17
+ /** Job payload */
18
+ payload?: unknown;
19
+ /** Queue name. @default 'default' */
20
+ queue?: string;
21
+ /** Priority (0=highest, 9=lowest). @default 5 */
22
+ priority?: JobPriority;
23
+ /** Maximum retry attempts. @default 3 */
24
+ maxRetries?: number;
25
+ /** Timeout in ms. @default 30000 */
26
+ timeout?: number;
27
+ /** Whether the schedule is enabled. @default true */
28
+ enabled?: boolean;
29
+ }
@@ -20,8 +20,8 @@ export interface FieldAdminConfig {
20
20
  readOnly?: boolean;
21
21
  hidden?: boolean;
22
22
  placeholder?: string;
23
- /** For blocks fields: editor rendering mode. 'visual' enables the WYSIWYG block editor. */
24
- editor?: 'visual' | 'form';
23
+ /** Editor rendering mode. 'visual' enables WYSIWYG block editor; 'email-builder' enables email template builder; 'form-builder' enables form schema builder for json fields. */
24
+ editor?: 'visual' | 'form' | 'email-builder' | 'form-builder';
25
25
  /** Render this group field as a collapsible accordion section */
26
26
  collapsible?: boolean;
27
27
  /** Whether the collapsible section starts expanded (default: false) */
@@ -5,7 +5,7 @@
5
5
  * These live in @momentumcms/core to avoid circular dependencies.
6
6
  * Runtime implementations (PluginRunner, etc.) live in @momentumcms/plugins/core.
7
7
  */
8
- import type { CollectionConfig } from './collections';
8
+ import type { CollectionConfig, UserContext } from './collections';
9
9
  import type { MomentumConfig } from './config';
10
10
  /**
11
11
  * Descriptor for Express middleware/routes that a plugin wants auto-mounted.
@@ -127,6 +127,12 @@ export interface MomentumAPI {
127
127
  collection(slug: string): unknown;
128
128
  /** Get the current config */
129
129
  getConfig(): MomentumConfig;
130
+ /** Return a new API instance with merged context (immutable). */
131
+ setContext(ctx: {
132
+ user?: UserContext;
133
+ depth?: number;
134
+ showHiddenFields?: boolean;
135
+ }): MomentumAPI;
130
136
  }
131
137
  /**
132
138
  * A Momentum CMS plugin.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Queue module for Momentum CMS
3
+ * Defines interfaces for job queue adapters
4
+ */
5
+ /**
6
+ * Job status lifecycle:
7
+ * pending -> active -> completed
8
+ * -> failed (retries remain) -> pending (retry)
9
+ * -> dead (max retries exceeded)
10
+ */
11
+ export type JobStatus = 'pending' | 'active' | 'completed' | 'failed' | 'dead';
12
+ /**
13
+ * Job priority levels. Lower number = higher priority.
14
+ * 0 is the highest priority, 9 is the lowest.
15
+ */
16
+ export type JobPriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
17
+ /**
18
+ * Backoff strategy for job retries.
19
+ */
20
+ export interface BackoffStrategy {
21
+ /** Backoff type */
22
+ type: 'exponential' | 'linear' | 'fixed';
23
+ /** Base delay in milliseconds */
24
+ delay: number;
25
+ /** Maximum delay in milliseconds (cap for exponential). @default 300000 (5 minutes) */
26
+ maxDelay?: number;
27
+ }
28
+ /**
29
+ * Options when enqueuing a job.
30
+ */
31
+ export interface EnqueueOptions {
32
+ /** Queue name. @default 'default' */
33
+ queue?: string;
34
+ /** Priority (0=highest, 9=lowest). @default 5 */
35
+ priority?: JobPriority;
36
+ /** Delay execution until this Date (ISO string or Date). */
37
+ runAt?: string | Date;
38
+ /** Maximum retry attempts. @default 3 */
39
+ maxRetries?: number;
40
+ /** Backoff strategy for retries. @default { type: 'exponential', delay: 1000 } */
41
+ backoff?: BackoffStrategy;
42
+ /** Maximum time in ms a job can run before being considered stalled. @default 30000 */
43
+ timeout?: number;
44
+ /** Unique key for deduplication. If a pending/active job with this key exists, the new job is skipped. */
45
+ uniqueKey?: string;
46
+ /** Arbitrary metadata attached to the job (not part of the handler payload). */
47
+ metadata?: Record<string, unknown>;
48
+ }
49
+ /**
50
+ * A job record as stored/returned by the adapter.
51
+ */
52
+ export interface Job<T = unknown> {
53
+ /** Unique job ID */
54
+ id: string;
55
+ /** Job type name (matches a registered handler) */
56
+ type: string;
57
+ /** Job payload (serialized as JSON) */
58
+ payload: T;
59
+ /** Current job status */
60
+ status: JobStatus;
61
+ /** Queue name */
62
+ queue: string;
63
+ /** Priority (0-9) */
64
+ priority: JobPriority;
65
+ /** Number of attempts made */
66
+ attempts: number;
67
+ /** Maximum retry attempts */
68
+ maxRetries: number;
69
+ /** Backoff configuration */
70
+ backoff: BackoffStrategy;
71
+ /** Timeout in milliseconds */
72
+ timeout: number;
73
+ /** Unique deduplication key */
74
+ uniqueKey?: string;
75
+ /** When the job should run (null = immediately) */
76
+ runAt: string | null;
77
+ /** When the job was last started */
78
+ startedAt?: string;
79
+ /** When the job completed or failed permanently */
80
+ finishedAt?: string;
81
+ /** Last error message (if failed/dead) */
82
+ lastError?: string;
83
+ /** Arbitrary metadata */
84
+ metadata?: Record<string, unknown>;
85
+ /** ISO timestamp of creation */
86
+ createdAt: string;
87
+ /** ISO timestamp of last update */
88
+ updatedAt: string;
89
+ }
90
+ /**
91
+ * Options for fetching the next batch of jobs to process.
92
+ */
93
+ export interface FetchJobsOptions {
94
+ /** Queue name to fetch from. @default 'default' */
95
+ queue?: string;
96
+ /** Maximum number of jobs to fetch. @default 1 */
97
+ limit?: number;
98
+ }
99
+ /**
100
+ * Options for querying jobs (admin dashboard, monitoring).
101
+ */
102
+ export interface JobQueryOptions {
103
+ /** Filter by status */
104
+ status?: JobStatus;
105
+ /** Filter by queue name */
106
+ queue?: string;
107
+ /** Filter by job type */
108
+ type?: string;
109
+ /** Pagination limit. @default 50 */
110
+ limit?: number;
111
+ /** Pagination page (1-based). @default 1 */
112
+ page?: number;
113
+ }
114
+ /**
115
+ * Result of a job query.
116
+ */
117
+ export interface JobQueryResult {
118
+ jobs: Job[];
119
+ total: number;
120
+ page: number;
121
+ limit: number;
122
+ }
123
+ /**
124
+ * Queue statistics for monitoring.
125
+ */
126
+ export interface QueueStats {
127
+ /** Queue name */
128
+ queue: string;
129
+ /** Count of jobs by status */
130
+ counts: Record<JobStatus, number>;
131
+ /** Oldest pending job age in milliseconds */
132
+ oldestPendingAge?: number;
133
+ }
134
+ /**
135
+ * Queue adapter interface.
136
+ * Implement this interface to create custom queue backends (PostgreSQL, Redis, etc.).
137
+ */
138
+ export interface QueueAdapter {
139
+ /**
140
+ * Initialize the queue backend (create indexes, etc.).
141
+ * Called once during server startup after collection tables are created.
142
+ */
143
+ initialize(): Promise<void>;
144
+ /**
145
+ * Enqueue a new job.
146
+ * @param type - Job type name (matches a registered handler)
147
+ * @param payload - Job payload data
148
+ * @param options - Enqueue options
149
+ * @returns The created job record
150
+ */
151
+ enqueue(type: string, payload: unknown, options?: EnqueueOptions): Promise<Job>;
152
+ /**
153
+ * Fetch the next batch of jobs ready for processing.
154
+ * Must use atomic locking (e.g., SKIP LOCKED) to prevent double-processing.
155
+ * Jobs are returned in priority order (lowest number first), then by runAt/createdAt.
156
+ */
157
+ fetchJobs(options?: FetchJobsOptions): Promise<Job[]>;
158
+ /**
159
+ * Mark a job as completed.
160
+ */
161
+ completeJob(jobId: string): Promise<void>;
162
+ /**
163
+ * Mark a job as failed. If retries remain, schedules the next attempt.
164
+ * If max retries exceeded, moves to 'dead' status.
165
+ */
166
+ failJob(jobId: string, error: string): Promise<void>;
167
+ /**
168
+ * Query jobs for monitoring/admin UI.
169
+ */
170
+ queryJobs(options?: JobQueryOptions): Promise<JobQueryResult>;
171
+ /**
172
+ * Get statistics for one or all queues.
173
+ * @param queue - Optional queue name (all queues if omitted)
174
+ */
175
+ getStats(queue?: string): Promise<QueueStats[]>;
176
+ /**
177
+ * Get a single job by ID.
178
+ * @returns The job record, or null if not found
179
+ */
180
+ getJob(jobId: string): Promise<Job | null>;
181
+ /**
182
+ * Delete a specific job by ID.
183
+ * @returns True if deleted
184
+ */
185
+ deleteJob(jobId: string): Promise<boolean>;
186
+ /**
187
+ * Purge completed/dead jobs older than the given age.
188
+ * @param olderThanMs - Age threshold in milliseconds
189
+ * @param status - Status to purge. @default 'completed'
190
+ * @returns Number of jobs purged
191
+ */
192
+ purgeJobs(olderThanMs: number, status?: 'completed' | 'dead'): Promise<number>;
193
+ /**
194
+ * Retry a dead job (move it back to pending with reset attempts).
195
+ * @returns The updated job
196
+ */
197
+ retryJob(jobId: string): Promise<Job>;
198
+ /**
199
+ * Detect and recover stalled jobs (active jobs that exceeded their timeout).
200
+ * Moves them back to pending for retry, or to dead if maxRetries exceeded.
201
+ * @returns Number of jobs recovered
202
+ */
203
+ recoverStalledJobs(): Promise<number>;
204
+ /**
205
+ * Graceful shutdown. Releases any held resources.
206
+ */
207
+ shutdown(): Promise<void>;
208
+ }
@@ -12,8 +12,8 @@ export interface UploadedFile {
12
12
  mimeType: string;
13
13
  /** File size in bytes */
14
14
  size: number;
15
- /** File content as Buffer */
16
- buffer: Buffer;
15
+ /** File content (Uint8Array for universal compat; Node Buffer extends Uint8Array) */
16
+ buffer: Uint8Array;
17
17
  }
18
18
  /**
19
19
  * Represents a file after it has been stored.
@@ -82,5 +82,5 @@ export interface StorageAdapter {
82
82
  * @param path - The storage path/key
83
83
  * @returns The file as a Buffer, or null if not found
84
84
  */
85
- read?(path: string): Promise<Buffer | null>;
85
+ read?(path: string): Promise<Uint8Array | null>;
86
86
  }