@momentumcms/server-analog 0.5.4 → 0.5.5

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 (4) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/index.cjs +73 -17
  3. package/index.js +73 -17
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.5.5 (2026-03-09)
2
+
3
+ ### 🚀 Features
4
+
5
+ - extract syncDatabaseSchema + fix findById breaking change ([#52](https://github.com/DonaldMurillo/momentum-cms/pull/52))
6
+ - swappable admin pages & layout slots with security hardening ([#51](https://github.com/DonaldMurillo/momentum-cms/pull/51))
7
+ - NestJS server adapter + E2E stabilization (0.5.2) ([#48](https://github.com/DonaldMurillo/momentum-cms/pull/48))
8
+ - S3, auth plugin wiring, redirects, and E2E tooling ([#41](https://github.com/DonaldMurillo/momentum-cms/pull/41))
9
+ - client-side page view tracking and content performance improvements ([#39](https://github.com/DonaldMurillo/momentum-cms/pull/39))
10
+ - blocks showcase with articles, pages, and UI fixes ([#36](https://github.com/DonaldMurillo/momentum-cms/pull/36))
11
+ - add named tabs support with nested data grouping and UI improvements ([#30](https://github.com/DonaldMurillo/momentum-cms/pull/30))
12
+ - Implement admin UI with API integration and SSR hydration ([9ed7b2bd](https://github.com/DonaldMurillo/momentum-cms/commit/9ed7b2bd))
13
+ - Initialize Momentum CMS foundation ([f64f5817](https://github.com/DonaldMurillo/momentum-cms/commit/f64f5817))
14
+
15
+ ### 🩹 Fixes
16
+
17
+ - Analog versioning access control + E2E improvements ([#54](https://github.com/DonaldMurillo/momentum-cms/pull/54))
18
+ - add public access to form-builder npm publish config ([#46](https://github.com/DonaldMurillo/momentum-cms/pull/46))
19
+ - Resolve non-null assertion bugs and CLAUDE.md violations ([#44](https://github.com/DonaldMurillo/momentum-cms/pull/44))
20
+ - **create-momentum-app:** add shell option to execFileSync for Windows ([#28](https://github.com/DonaldMurillo/momentum-cms/pull/28))
21
+ - correct repository URLs and add GitHub link to CLI ([#26](https://github.com/DonaldMurillo/momentum-cms/pull/26))
22
+ - 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))
23
+
24
+ ### ❤️ Thank You
25
+
26
+ - Claude Haiku 4.5
27
+ - Claude Opus 4.5
28
+ - Claude Opus 4.6
29
+ - Donald Murillo @DonaldMurillo
30
+
1
31
  ## 0.5.4 (2026-03-07)
2
32
 
3
33
  This was a version bump only for server-analog to align it with other projects, there were no code changes.
package/index.cjs CHANGED
@@ -1814,6 +1814,12 @@ var DocumentNotFoundError = class extends Error {
1814
1814
  this.name = "DocumentNotFoundError";
1815
1815
  }
1816
1816
  };
1817
+ var DraftNotVisibleError = class extends Error {
1818
+ constructor(collection, id) {
1819
+ super(`Draft "${id}" in collection "${collection}" is not visible to the current user`);
1820
+ this.name = "DraftNotVisibleError";
1821
+ }
1822
+ };
1817
1823
  var AccessDeniedError = class extends Error {
1818
1824
  constructor(operation, collection) {
1819
1825
  super(`Access denied for ${operation} on collection "${collection}"`);
@@ -2231,13 +2237,31 @@ function stripTransientKeys(data) {
2231
2237
  }
2232
2238
  return result;
2233
2239
  }
2240
+ var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
2234
2241
  function flattenWhereClause(where) {
2235
2242
  if (!where)
2236
2243
  return {};
2237
2244
  const result = {};
2238
2245
  for (const [field, condition] of Object.entries(where)) {
2239
- if (typeof condition === "object" && condition !== null && "equals" in condition) {
2240
- result[field] = condition["equals"];
2246
+ if (typeof condition !== "object" || condition === null) {
2247
+ result[field] = condition;
2248
+ continue;
2249
+ }
2250
+ const condObj = condition;
2251
+ if ("equals" in condObj) {
2252
+ result[field] = condObj["equals"];
2253
+ continue;
2254
+ }
2255
+ const ops = {};
2256
+ let hasComparisonOp = false;
2257
+ for (const op of COMPARISON_OPS) {
2258
+ if (op in condObj) {
2259
+ ops[`$${op}`] = condObj[op];
2260
+ hasComparisonOp = true;
2261
+ }
2262
+ }
2263
+ if (hasComparisonOp) {
2264
+ result[field] = ops;
2241
2265
  } else {
2242
2266
  result[field] = condition;
2243
2267
  }
@@ -2332,24 +2356,24 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2332
2356
  await this.runBeforeReadHooks();
2333
2357
  const doc = await this.adapter.findById(this.slug, id);
2334
2358
  if (!doc) {
2335
- return null;
2359
+ throw new DocumentNotFoundError(this.slug, id);
2336
2360
  }
2337
2361
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2338
2362
  if (softDeleteField && !options?.withDeleted && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T is compatible with Record<string, unknown>
2339
2363
  doc[softDeleteField]) {
2340
- return null;
2364
+ throw new DocumentNotFoundError(this.slug, id);
2341
2365
  }
2342
2366
  if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2343
2367
  const canSeeDrafts = await this.canReadDrafts();
2344
2368
  if (!canSeeDrafts) {
2345
2369
  const record = doc;
2346
2370
  if (record["_status"] !== "published") {
2347
- return null;
2371
+ throw new DraftNotVisibleError(this.slug, id);
2348
2372
  }
2349
2373
  }
2350
2374
  }
2351
2375
  if (!this.matchesDefaultWhereConstraints(doc)) {
2352
- return null;
2376
+ throw new DocumentNotFoundError(this.slug, id);
2353
2377
  }
2354
2378
  const [processed] = await this.processAfterReadHooks([doc]);
2355
2379
  const MAX_RELATIONSHIP_DEPTH = 10;
@@ -3220,9 +3244,6 @@ function createMomentumHandlers(config) {
3220
3244
  const depth = typeof request.query?.["depth"] === "number" ? request.query["depth"] : void 0;
3221
3245
  const withDeleted = request.query?.["withDeleted"] === true;
3222
3246
  const doc = await api.collection(request.collectionSlug).findById(request.id, { depth, withDeleted });
3223
- if (!doc) {
3224
- return { error: "Document not found", status: 404 };
3225
- }
3226
3247
  return { doc };
3227
3248
  } catch (error) {
3228
3249
  return handleError(error);
@@ -3339,6 +3360,9 @@ function handleError(error) {
3339
3360
  if (error instanceof DocumentNotFoundError) {
3340
3361
  return { error: error.message, status: 404 };
3341
3362
  }
3363
+ if (error instanceof DraftNotVisibleError) {
3364
+ return { doc: null };
3365
+ }
3342
3366
  if (error instanceof AccessDeniedError) {
3343
3367
  return { error: error.message, status: 403 };
3344
3368
  }
@@ -3735,7 +3759,14 @@ function buildGraphQLSchema(collections) {
3735
3759
  const api = getMomentumAPI();
3736
3760
  const ctx = buildAPIContext(context);
3737
3761
  const contextApi = Object.keys(ctx).length > 0 ? api.setContext(ctx) : api;
3738
- return contextApi.collection(col.slug).findById(args.id);
3762
+ try {
3763
+ return await contextApi.collection(col.slug).findById(args.id);
3764
+ } catch (err) {
3765
+ if (err instanceof Error && err.name === "DocumentNotFoundError") {
3766
+ return null;
3767
+ }
3768
+ throw err;
3769
+ }
3739
3770
  }
3740
3771
  };
3741
3772
  queryFields[plural.charAt(0).toLowerCase() + plural.slice(1)] = {
@@ -5366,7 +5397,13 @@ function createComprehensiveMomentumHandler(config) {
5366
5397
  return { docs: r.docs, totalDocs: r.totalDocs };
5367
5398
  },
5368
5399
  findById: async (slug2, id) => {
5369
- return await contextApi.collection(slug2).findById(id);
5400
+ try {
5401
+ return await contextApi.collection(slug2).findById(id);
5402
+ } catch (err) {
5403
+ if (err instanceof Error && err.name === "DocumentNotFoundError")
5404
+ return null;
5405
+ throw err;
5406
+ }
5370
5407
  },
5371
5408
  count: (slug2) => contextApi.collection(slug2).count(),
5372
5409
  create: async (slug2, data) => {
@@ -5731,6 +5768,10 @@ function createComprehensiveMomentumHandler(config) {
5731
5768
  });
5732
5769
  return { doc: restored, message: "Version restored successfully" };
5733
5770
  } catch (error) {
5771
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5772
+ utils.setResponseStatus(event, 403);
5773
+ return { error: "Access denied" };
5774
+ }
5734
5775
  const message = sanitizeErrorMessage(error, "Unknown error");
5735
5776
  if (error instanceof Error && error.message.includes("mismatch")) {
5736
5777
  utils.setResponseStatus(event, 400);
@@ -5771,6 +5812,10 @@ function createComprehensiveMomentumHandler(config) {
5771
5812
  );
5772
5813
  return { differences };
5773
5814
  } catch (error) {
5815
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5816
+ utils.setResponseStatus(event, 403);
5817
+ return { error: "Access denied" };
5818
+ }
5774
5819
  utils.setResponseStatus(event, 500);
5775
5820
  return {
5776
5821
  error: "Failed to compare versions",
@@ -5798,6 +5843,10 @@ function createComprehensiveMomentumHandler(config) {
5798
5843
  }
5799
5844
  return version;
5800
5845
  } catch (error) {
5846
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5847
+ utils.setResponseStatus(event, 403);
5848
+ return { error: "Access denied" };
5849
+ }
5801
5850
  utils.setResponseStatus(event, 500);
5802
5851
  return {
5803
5852
  error: "Failed to fetch version",
@@ -5822,6 +5871,10 @@ function createComprehensiveMomentumHandler(config) {
5822
5871
  });
5823
5872
  return result;
5824
5873
  } catch (error) {
5874
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5875
+ utils.setResponseStatus(event, 403);
5876
+ return { error: "Access denied" };
5877
+ }
5825
5878
  utils.setResponseStatus(event, 500);
5826
5879
  return {
5827
5880
  error: "Failed to fetch versions",
@@ -5862,12 +5915,7 @@ function createComprehensiveMomentumHandler(config) {
5862
5915
  }
5863
5916
  } else {
5864
5917
  const contextApi = getContextualAPI(user);
5865
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5866
- if (!doc) {
5867
- utils.setResponseStatus(event, 404);
5868
- return { error: "Document not found" };
5869
- }
5870
- docRecord = doc;
5918
+ docRecord = await contextApi.collection(collectionSlug2).findById(docId);
5871
5919
  }
5872
5920
  const emailField = getEmailBuilderFieldName(collectionConfig);
5873
5921
  const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
@@ -5936,6 +5984,10 @@ function createComprehensiveMomentumHandler(config) {
5936
5984
  return { message: "Scheduled publish cancelled" };
5937
5985
  }
5938
5986
  } catch (error) {
5987
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5988
+ utils.setResponseStatus(event, 403);
5989
+ return { error: "Access denied" };
5990
+ }
5939
5991
  utils.setResponseStatus(event, 500);
5940
5992
  return {
5941
5993
  error: `Failed to ${action.replace(/-/g, " ")}`,
@@ -5977,6 +6029,10 @@ function createComprehensiveMomentumHandler(config) {
5977
6029
  const status = await versionOps.getStatus(docId);
5978
6030
  return { status };
5979
6031
  } catch (error) {
6032
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6033
+ utils.setResponseStatus(event, 403);
6034
+ return { error: "Access denied" };
6035
+ }
5980
6036
  utils.setResponseStatus(event, 500);
5981
6037
  return {
5982
6038
  error: "Failed to get status",
package/index.js CHANGED
@@ -1781,6 +1781,12 @@ var DocumentNotFoundError = class extends Error {
1781
1781
  this.name = "DocumentNotFoundError";
1782
1782
  }
1783
1783
  };
1784
+ var DraftNotVisibleError = class extends Error {
1785
+ constructor(collection, id) {
1786
+ super(`Draft "${id}" in collection "${collection}" is not visible to the current user`);
1787
+ this.name = "DraftNotVisibleError";
1788
+ }
1789
+ };
1784
1790
  var AccessDeniedError = class extends Error {
1785
1791
  constructor(operation, collection) {
1786
1792
  super(`Access denied for ${operation} on collection "${collection}"`);
@@ -2198,13 +2204,31 @@ function stripTransientKeys(data) {
2198
2204
  }
2199
2205
  return result;
2200
2206
  }
2207
+ var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
2201
2208
  function flattenWhereClause(where) {
2202
2209
  if (!where)
2203
2210
  return {};
2204
2211
  const result = {};
2205
2212
  for (const [field, condition] of Object.entries(where)) {
2206
- if (typeof condition === "object" && condition !== null && "equals" in condition) {
2207
- result[field] = condition["equals"];
2213
+ if (typeof condition !== "object" || condition === null) {
2214
+ result[field] = condition;
2215
+ continue;
2216
+ }
2217
+ const condObj = condition;
2218
+ if ("equals" in condObj) {
2219
+ result[field] = condObj["equals"];
2220
+ continue;
2221
+ }
2222
+ const ops = {};
2223
+ let hasComparisonOp = false;
2224
+ for (const op of COMPARISON_OPS) {
2225
+ if (op in condObj) {
2226
+ ops[`$${op}`] = condObj[op];
2227
+ hasComparisonOp = true;
2228
+ }
2229
+ }
2230
+ if (hasComparisonOp) {
2231
+ result[field] = ops;
2208
2232
  } else {
2209
2233
  result[field] = condition;
2210
2234
  }
@@ -2299,24 +2323,24 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2299
2323
  await this.runBeforeReadHooks();
2300
2324
  const doc = await this.adapter.findById(this.slug, id);
2301
2325
  if (!doc) {
2302
- return null;
2326
+ throw new DocumentNotFoundError(this.slug, id);
2303
2327
  }
2304
2328
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2305
2329
  if (softDeleteField && !options?.withDeleted && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T is compatible with Record<string, unknown>
2306
2330
  doc[softDeleteField]) {
2307
- return null;
2331
+ throw new DocumentNotFoundError(this.slug, id);
2308
2332
  }
2309
2333
  if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2310
2334
  const canSeeDrafts = await this.canReadDrafts();
2311
2335
  if (!canSeeDrafts) {
2312
2336
  const record = doc;
2313
2337
  if (record["_status"] !== "published") {
2314
- return null;
2338
+ throw new DraftNotVisibleError(this.slug, id);
2315
2339
  }
2316
2340
  }
2317
2341
  }
2318
2342
  if (!this.matchesDefaultWhereConstraints(doc)) {
2319
- return null;
2343
+ throw new DocumentNotFoundError(this.slug, id);
2320
2344
  }
2321
2345
  const [processed] = await this.processAfterReadHooks([doc]);
2322
2346
  const MAX_RELATIONSHIP_DEPTH = 10;
@@ -3187,9 +3211,6 @@ function createMomentumHandlers(config) {
3187
3211
  const depth = typeof request.query?.["depth"] === "number" ? request.query["depth"] : void 0;
3188
3212
  const withDeleted = request.query?.["withDeleted"] === true;
3189
3213
  const doc = await api.collection(request.collectionSlug).findById(request.id, { depth, withDeleted });
3190
- if (!doc) {
3191
- return { error: "Document not found", status: 404 };
3192
- }
3193
3214
  return { doc };
3194
3215
  } catch (error) {
3195
3216
  return handleError(error);
@@ -3306,6 +3327,9 @@ function handleError(error) {
3306
3327
  if (error instanceof DocumentNotFoundError) {
3307
3328
  return { error: error.message, status: 404 };
3308
3329
  }
3330
+ if (error instanceof DraftNotVisibleError) {
3331
+ return { doc: null };
3332
+ }
3309
3333
  if (error instanceof AccessDeniedError) {
3310
3334
  return { error: error.message, status: 403 };
3311
3335
  }
@@ -3715,7 +3739,14 @@ function buildGraphQLSchema(collections) {
3715
3739
  const api = getMomentumAPI();
3716
3740
  const ctx = buildAPIContext(context);
3717
3741
  const contextApi = Object.keys(ctx).length > 0 ? api.setContext(ctx) : api;
3718
- return contextApi.collection(col.slug).findById(args.id);
3742
+ try {
3743
+ return await contextApi.collection(col.slug).findById(args.id);
3744
+ } catch (err) {
3745
+ if (err instanceof Error && err.name === "DocumentNotFoundError") {
3746
+ return null;
3747
+ }
3748
+ throw err;
3749
+ }
3719
3750
  }
3720
3751
  };
3721
3752
  queryFields[plural.charAt(0).toLowerCase() + plural.slice(1)] = {
@@ -5346,7 +5377,13 @@ function createComprehensiveMomentumHandler(config) {
5346
5377
  return { docs: r.docs, totalDocs: r.totalDocs };
5347
5378
  },
5348
5379
  findById: async (slug2, id) => {
5349
- return await contextApi.collection(slug2).findById(id);
5380
+ try {
5381
+ return await contextApi.collection(slug2).findById(id);
5382
+ } catch (err) {
5383
+ if (err instanceof Error && err.name === "DocumentNotFoundError")
5384
+ return null;
5385
+ throw err;
5386
+ }
5350
5387
  },
5351
5388
  count: (slug2) => contextApi.collection(slug2).count(),
5352
5389
  create: async (slug2, data) => {
@@ -5711,6 +5748,10 @@ function createComprehensiveMomentumHandler(config) {
5711
5748
  });
5712
5749
  return { doc: restored, message: "Version restored successfully" };
5713
5750
  } catch (error) {
5751
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5752
+ utils.setResponseStatus(event, 403);
5753
+ return { error: "Access denied" };
5754
+ }
5714
5755
  const message = sanitizeErrorMessage(error, "Unknown error");
5715
5756
  if (error instanceof Error && error.message.includes("mismatch")) {
5716
5757
  utils.setResponseStatus(event, 400);
@@ -5751,6 +5792,10 @@ function createComprehensiveMomentumHandler(config) {
5751
5792
  );
5752
5793
  return { differences };
5753
5794
  } catch (error) {
5795
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5796
+ utils.setResponseStatus(event, 403);
5797
+ return { error: "Access denied" };
5798
+ }
5754
5799
  utils.setResponseStatus(event, 500);
5755
5800
  return {
5756
5801
  error: "Failed to compare versions",
@@ -5778,6 +5823,10 @@ function createComprehensiveMomentumHandler(config) {
5778
5823
  }
5779
5824
  return version;
5780
5825
  } catch (error) {
5826
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5827
+ utils.setResponseStatus(event, 403);
5828
+ return { error: "Access denied" };
5829
+ }
5781
5830
  utils.setResponseStatus(event, 500);
5782
5831
  return {
5783
5832
  error: "Failed to fetch version",
@@ -5802,6 +5851,10 @@ function createComprehensiveMomentumHandler(config) {
5802
5851
  });
5803
5852
  return result;
5804
5853
  } catch (error) {
5854
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5855
+ utils.setResponseStatus(event, 403);
5856
+ return { error: "Access denied" };
5857
+ }
5805
5858
  utils.setResponseStatus(event, 500);
5806
5859
  return {
5807
5860
  error: "Failed to fetch versions",
@@ -5842,12 +5895,7 @@ function createComprehensiveMomentumHandler(config) {
5842
5895
  }
5843
5896
  } else {
5844
5897
  const contextApi = getContextualAPI(user);
5845
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5846
- if (!doc) {
5847
- utils.setResponseStatus(event, 404);
5848
- return { error: "Document not found" };
5849
- }
5850
- docRecord = doc;
5898
+ docRecord = await contextApi.collection(collectionSlug2).findById(docId);
5851
5899
  }
5852
5900
  const emailField = getEmailBuilderFieldName(collectionConfig);
5853
5901
  const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
@@ -5916,6 +5964,10 @@ function createComprehensiveMomentumHandler(config) {
5916
5964
  return { message: "Scheduled publish cancelled" };
5917
5965
  }
5918
5966
  } catch (error) {
5967
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5968
+ utils.setResponseStatus(event, 403);
5969
+ return { error: "Access denied" };
5970
+ }
5919
5971
  utils.setResponseStatus(event, 500);
5920
5972
  return {
5921
5973
  error: `Failed to ${action.replace(/-/g, " ")}`,
@@ -5957,6 +6009,10 @@ function createComprehensiveMomentumHandler(config) {
5957
6009
  const status = await versionOps.getStatus(docId);
5958
6010
  return { status };
5959
6011
  } catch (error) {
6012
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6013
+ utils.setResponseStatus(event, 403);
6014
+ return { error: "Access denied" };
6015
+ }
5960
6016
  utils.setResponseStatus(event, 500);
5961
6017
  return {
5962
6018
  error: "Failed to get status",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/server-analog",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Nitro/h3 adapter for Momentum CMS with Analog.js support",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",