@momentumcms/server-analog 0.5.3 → 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 +34 -0
  2. package/index.cjs +146 -23
  3. package/index.js +146 -23
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
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
+
31
+ ## 0.5.4 (2026-03-07)
32
+
33
+ This was a version bump only for server-analog to align it with other projects, there were no code changes.
34
+
1
35
  ## 0.5.0 (2026-02-23)
2
36
 
3
37
  This was a version bump only for server-analog to align it with other projects, there were no code changes.
package/index.cjs CHANGED
@@ -1452,6 +1452,14 @@ var MediaCollection = defineCollection({
1452
1452
  }
1453
1453
  });
1454
1454
 
1455
+ // libs/core/src/lib/versions/version.types.ts
1456
+ function hasVersionDrafts(collection) {
1457
+ const v = collection.versions;
1458
+ if (!v || typeof v === "boolean")
1459
+ return false;
1460
+ return !!v.drafts;
1461
+ }
1462
+
1455
1463
  // libs/server-core/src/lib/field-access.ts
1456
1464
  function hasFieldAccessControl(fields) {
1457
1465
  for (const field of fields) {
@@ -1806,6 +1814,12 @@ var DocumentNotFoundError = class extends Error {
1806
1814
  this.name = "DocumentNotFoundError";
1807
1815
  }
1808
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
+ };
1809
1823
  var AccessDeniedError = class extends Error {
1810
1824
  constructor(operation, collection) {
1811
1825
  super(`Access denied for ${operation} on collection "${collection}"`);
@@ -2038,13 +2052,18 @@ var VersionOperationsImpl = class {
2038
2052
  }
2039
2053
  return "draft";
2040
2054
  }
2041
- async compare(versionId1, versionId2) {
2055
+ async compare(versionId1, versionId2, parentId) {
2042
2056
  await this.checkAccess("readVersions");
2043
2057
  const version1 = await this.findVersionById(versionId1);
2044
2058
  const version2 = await this.findVersionById(versionId2);
2045
2059
  if (!version1 || !version2) {
2046
2060
  throw new Error("One or both versions not found");
2047
2061
  }
2062
+ if (parentId) {
2063
+ if (version1.parent !== parentId || version2.parent !== parentId) {
2064
+ throw new Error("Version does not belong to the specified document");
2065
+ }
2066
+ }
2048
2067
  const data1 = version1.version;
2049
2068
  const data2 = version2.version;
2050
2069
  if (!isRecord(data1) || !isRecord(data2)) {
@@ -2099,6 +2118,9 @@ var VersionOperationsImpl = class {
2099
2118
  // Private Helpers
2100
2119
  // ============================================
2101
2120
  async checkAccess(operation) {
2121
+ if (this.context.overrideAccess) {
2122
+ return;
2123
+ }
2102
2124
  const accessFn = this.collectionConfig.access?.[operation];
2103
2125
  if (!accessFn) {
2104
2126
  return;
@@ -2215,13 +2237,31 @@ function stripTransientKeys(data) {
2215
2237
  }
2216
2238
  return result;
2217
2239
  }
2240
+ var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
2218
2241
  function flattenWhereClause(where) {
2219
2242
  if (!where)
2220
2243
  return {};
2221
2244
  const result = {};
2222
2245
  for (const [field, condition] of Object.entries(where)) {
2223
- if (typeof condition === "object" && condition !== null && "equals" in condition) {
2224
- 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;
2225
2265
  } else {
2226
2266
  result[field] = condition;
2227
2267
  }
@@ -2255,6 +2295,12 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2255
2295
  Object.assign(whereParams, constraints);
2256
2296
  }
2257
2297
  }
2298
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2299
+ const canSeeDrafts = await this.canReadDrafts();
2300
+ if (!canSeeDrafts) {
2301
+ whereParams["_status"] = "published";
2302
+ }
2303
+ }
2258
2304
  const query = {
2259
2305
  ...queryOptions,
2260
2306
  ...whereParams,
@@ -2310,15 +2356,24 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2310
2356
  await this.runBeforeReadHooks();
2311
2357
  const doc = await this.adapter.findById(this.slug, id);
2312
2358
  if (!doc) {
2313
- return null;
2359
+ throw new DocumentNotFoundError(this.slug, id);
2314
2360
  }
2315
2361
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2316
2362
  if (softDeleteField && !options?.withDeleted && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T is compatible with Record<string, unknown>
2317
2363
  doc[softDeleteField]) {
2318
- return null;
2364
+ throw new DocumentNotFoundError(this.slug, id);
2365
+ }
2366
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2367
+ const canSeeDrafts = await this.canReadDrafts();
2368
+ if (!canSeeDrafts) {
2369
+ const record = doc;
2370
+ if (record["_status"] !== "published") {
2371
+ throw new DraftNotVisibleError(this.slug, id);
2372
+ }
2373
+ }
2319
2374
  }
2320
2375
  if (!this.matchesDefaultWhereConstraints(doc)) {
2321
- return null;
2376
+ throw new DocumentNotFoundError(this.slug, id);
2322
2377
  }
2323
2378
  const [processed] = await this.processAfterReadHooks([doc]);
2324
2379
  const MAX_RELATIONSHIP_DEPTH = 10;
@@ -2402,6 +2457,15 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2402
2457
  if (!this.matchesDefaultWhereConstraints(originalDoc)) {
2403
2458
  throw new DocumentNotFoundError(this.slug, id);
2404
2459
  }
2460
+ if (hasVersionDrafts(this.collectionConfig) && this.adapter.createVersion) {
2461
+ const status = originalDoc["_status"] ?? "draft";
2462
+ await this.adapter.createVersion(this.slug, id, originalDoc, { status });
2463
+ const versionsConfig = this.collectionConfig.versions;
2464
+ const maxPerDoc = typeof versionsConfig === "object" && versionsConfig !== null ? versionsConfig.maxPerDoc : void 0;
2465
+ if (maxPerDoc && this.adapter.deleteVersions) {
2466
+ await this.adapter.deleteVersions(this.slug, id, maxPerDoc);
2467
+ }
2468
+ }
2405
2469
  let processedData = data;
2406
2470
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2407
2471
  if (softDeleteField && softDeleteField in processedData) {
@@ -2733,6 +2797,30 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2733
2797
  throw new AccessDeniedError(operation, this.slug);
2734
2798
  }
2735
2799
  }
2800
+ /**
2801
+ * Check if the current user can see draft documents (non-throwing).
2802
+ * Uses `access.readDrafts` if configured, otherwise falls back to `access.update`.
2803
+ */
2804
+ async canReadDrafts() {
2805
+ if (!this.context.user)
2806
+ return false;
2807
+ const readDraftsFn = this.collectionConfig.access?.readDrafts;
2808
+ if (readDraftsFn) {
2809
+ try {
2810
+ return !!await Promise.resolve(readDraftsFn({ req: this.buildRequestContext() }));
2811
+ } catch {
2812
+ return false;
2813
+ }
2814
+ }
2815
+ const updateFn = this.collectionConfig.access?.update;
2816
+ if (!updateFn)
2817
+ return true;
2818
+ try {
2819
+ return !!await Promise.resolve(updateFn({ req: this.buildRequestContext() }));
2820
+ } catch {
2821
+ return false;
2822
+ }
2823
+ }
2736
2824
  buildRequestContext() {
2737
2825
  return {
2738
2826
  user: this.context.user
@@ -3156,9 +3244,6 @@ function createMomentumHandlers(config) {
3156
3244
  const depth = typeof request.query?.["depth"] === "number" ? request.query["depth"] : void 0;
3157
3245
  const withDeleted = request.query?.["withDeleted"] === true;
3158
3246
  const doc = await api.collection(request.collectionSlug).findById(request.id, { depth, withDeleted });
3159
- if (!doc) {
3160
- return { error: "Document not found", status: 404 };
3161
- }
3162
3247
  return { doc };
3163
3248
  } catch (error) {
3164
3249
  return handleError(error);
@@ -3275,6 +3360,9 @@ function handleError(error) {
3275
3360
  if (error instanceof DocumentNotFoundError) {
3276
3361
  return { error: error.message, status: 404 };
3277
3362
  }
3363
+ if (error instanceof DraftNotVisibleError) {
3364
+ return { doc: null };
3365
+ }
3278
3366
  if (error instanceof AccessDeniedError) {
3279
3367
  return { error: error.message, status: 403 };
3280
3368
  }
@@ -3412,8 +3500,9 @@ function buildGraphQLSchema(collections) {
3412
3500
  }
3413
3501
  function getOrCreateEnum(field, parentName) {
3414
3502
  const key = `${parentName}_${field.name}`;
3415
- if (enumCache.has(key)) {
3416
- return enumCache.get(key);
3503
+ const cached = enumCache.get(key);
3504
+ if (cached) {
3505
+ return cached;
3417
3506
  }
3418
3507
  const values = {};
3419
3508
  for (const opt of field.options) {
@@ -3524,8 +3613,9 @@ function buildGraphQLSchema(collections) {
3524
3613
  }
3525
3614
  }
3526
3615
  function getOrCreateObjectType(fields, typeName) {
3527
- if (typeCache.has(typeName)) {
3528
- return typeCache.get(typeName);
3616
+ const cachedType = typeCache.get(typeName);
3617
+ if (cachedType) {
3618
+ return cachedType;
3529
3619
  }
3530
3620
  const objType = new import_graphql2.GraphQLObjectType({
3531
3621
  name: typeName,
@@ -3546,8 +3636,9 @@ function buildGraphQLSchema(collections) {
3546
3636
  }
3547
3637
  function getOrCreateInputType(fields, typeName) {
3548
3638
  const inputName = `${typeName}Input`;
3549
- if (inputCache.has(inputName)) {
3550
- return inputCache.get(inputName);
3639
+ const cachedInput = inputCache.get(inputName);
3640
+ if (cachedInput) {
3641
+ return cachedInput;
3551
3642
  }
3552
3643
  const inputType = new import_graphql2.GraphQLInputObjectType({
3553
3644
  name: inputName,
@@ -3668,7 +3759,14 @@ function buildGraphQLSchema(collections) {
3668
3759
  const api = getMomentumAPI();
3669
3760
  const ctx = buildAPIContext(context);
3670
3761
  const contextApi = Object.keys(ctx).length > 0 ? api.setContext(ctx) : api;
3671
- 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
+ }
3672
3770
  }
3673
3771
  };
3674
3772
  queryFields[plural.charAt(0).toLowerCase() + plural.slice(1)] = {
@@ -5299,7 +5397,13 @@ function createComprehensiveMomentumHandler(config) {
5299
5397
  return { docs: r.docs, totalDocs: r.totalDocs };
5300
5398
  },
5301
5399
  findById: async (slug2, id) => {
5302
- 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
+ }
5303
5407
  },
5304
5408
  count: (slug2) => contextApi.collection(slug2).count(),
5305
5409
  create: async (slug2, data) => {
@@ -5664,6 +5768,10 @@ function createComprehensiveMomentumHandler(config) {
5664
5768
  });
5665
5769
  return { doc: restored, message: "Version restored successfully" };
5666
5770
  } catch (error) {
5771
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5772
+ utils.setResponseStatus(event, 403);
5773
+ return { error: "Access denied" };
5774
+ }
5667
5775
  const message = sanitizeErrorMessage(error, "Unknown error");
5668
5776
  if (error instanceof Error && error.message.includes("mismatch")) {
5669
5777
  utils.setResponseStatus(event, 400);
@@ -5704,6 +5812,10 @@ function createComprehensiveMomentumHandler(config) {
5704
5812
  );
5705
5813
  return { differences };
5706
5814
  } catch (error) {
5815
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5816
+ utils.setResponseStatus(event, 403);
5817
+ return { error: "Access denied" };
5818
+ }
5707
5819
  utils.setResponseStatus(event, 500);
5708
5820
  return {
5709
5821
  error: "Failed to compare versions",
@@ -5731,6 +5843,10 @@ function createComprehensiveMomentumHandler(config) {
5731
5843
  }
5732
5844
  return version;
5733
5845
  } catch (error) {
5846
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5847
+ utils.setResponseStatus(event, 403);
5848
+ return { error: "Access denied" };
5849
+ }
5734
5850
  utils.setResponseStatus(event, 500);
5735
5851
  return {
5736
5852
  error: "Failed to fetch version",
@@ -5755,6 +5871,10 @@ function createComprehensiveMomentumHandler(config) {
5755
5871
  });
5756
5872
  return result;
5757
5873
  } catch (error) {
5874
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5875
+ utils.setResponseStatus(event, 403);
5876
+ return { error: "Access denied" };
5877
+ }
5758
5878
  utils.setResponseStatus(event, 500);
5759
5879
  return {
5760
5880
  error: "Failed to fetch versions",
@@ -5795,12 +5915,7 @@ function createComprehensiveMomentumHandler(config) {
5795
5915
  }
5796
5916
  } else {
5797
5917
  const contextApi = getContextualAPI(user);
5798
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5799
- if (!doc) {
5800
- utils.setResponseStatus(event, 404);
5801
- return { error: "Document not found" };
5802
- }
5803
- docRecord = doc;
5918
+ docRecord = await contextApi.collection(collectionSlug2).findById(docId);
5804
5919
  }
5805
5920
  const emailField = getEmailBuilderFieldName(collectionConfig);
5806
5921
  const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
@@ -5869,6 +5984,10 @@ function createComprehensiveMomentumHandler(config) {
5869
5984
  return { message: "Scheduled publish cancelled" };
5870
5985
  }
5871
5986
  } catch (error) {
5987
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5988
+ utils.setResponseStatus(event, 403);
5989
+ return { error: "Access denied" };
5990
+ }
5872
5991
  utils.setResponseStatus(event, 500);
5873
5992
  return {
5874
5993
  error: `Failed to ${action.replace(/-/g, " ")}`,
@@ -5910,6 +6029,10 @@ function createComprehensiveMomentumHandler(config) {
5910
6029
  const status = await versionOps.getStatus(docId);
5911
6030
  return { status };
5912
6031
  } catch (error) {
6032
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6033
+ utils.setResponseStatus(event, 403);
6034
+ return { error: "Access denied" };
6035
+ }
5913
6036
  utils.setResponseStatus(event, 500);
5914
6037
  return {
5915
6038
  error: "Failed to get status",
package/index.js CHANGED
@@ -1419,6 +1419,14 @@ var MediaCollection = defineCollection({
1419
1419
  }
1420
1420
  });
1421
1421
 
1422
+ // libs/core/src/lib/versions/version.types.ts
1423
+ function hasVersionDrafts(collection) {
1424
+ const v = collection.versions;
1425
+ if (!v || typeof v === "boolean")
1426
+ return false;
1427
+ return !!v.drafts;
1428
+ }
1429
+
1422
1430
  // libs/server-core/src/lib/field-access.ts
1423
1431
  function hasFieldAccessControl(fields) {
1424
1432
  for (const field of fields) {
@@ -1773,6 +1781,12 @@ var DocumentNotFoundError = class extends Error {
1773
1781
  this.name = "DocumentNotFoundError";
1774
1782
  }
1775
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
+ };
1776
1790
  var AccessDeniedError = class extends Error {
1777
1791
  constructor(operation, collection) {
1778
1792
  super(`Access denied for ${operation} on collection "${collection}"`);
@@ -2005,13 +2019,18 @@ var VersionOperationsImpl = class {
2005
2019
  }
2006
2020
  return "draft";
2007
2021
  }
2008
- async compare(versionId1, versionId2) {
2022
+ async compare(versionId1, versionId2, parentId) {
2009
2023
  await this.checkAccess("readVersions");
2010
2024
  const version1 = await this.findVersionById(versionId1);
2011
2025
  const version2 = await this.findVersionById(versionId2);
2012
2026
  if (!version1 || !version2) {
2013
2027
  throw new Error("One or both versions not found");
2014
2028
  }
2029
+ if (parentId) {
2030
+ if (version1.parent !== parentId || version2.parent !== parentId) {
2031
+ throw new Error("Version does not belong to the specified document");
2032
+ }
2033
+ }
2015
2034
  const data1 = version1.version;
2016
2035
  const data2 = version2.version;
2017
2036
  if (!isRecord(data1) || !isRecord(data2)) {
@@ -2066,6 +2085,9 @@ var VersionOperationsImpl = class {
2066
2085
  // Private Helpers
2067
2086
  // ============================================
2068
2087
  async checkAccess(operation) {
2088
+ if (this.context.overrideAccess) {
2089
+ return;
2090
+ }
2069
2091
  const accessFn = this.collectionConfig.access?.[operation];
2070
2092
  if (!accessFn) {
2071
2093
  return;
@@ -2182,13 +2204,31 @@ function stripTransientKeys(data) {
2182
2204
  }
2183
2205
  return result;
2184
2206
  }
2207
+ var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
2185
2208
  function flattenWhereClause(where) {
2186
2209
  if (!where)
2187
2210
  return {};
2188
2211
  const result = {};
2189
2212
  for (const [field, condition] of Object.entries(where)) {
2190
- if (typeof condition === "object" && condition !== null && "equals" in condition) {
2191
- 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;
2192
2232
  } else {
2193
2233
  result[field] = condition;
2194
2234
  }
@@ -2222,6 +2262,12 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2222
2262
  Object.assign(whereParams, constraints);
2223
2263
  }
2224
2264
  }
2265
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2266
+ const canSeeDrafts = await this.canReadDrafts();
2267
+ if (!canSeeDrafts) {
2268
+ whereParams["_status"] = "published";
2269
+ }
2270
+ }
2225
2271
  const query = {
2226
2272
  ...queryOptions,
2227
2273
  ...whereParams,
@@ -2277,15 +2323,24 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2277
2323
  await this.runBeforeReadHooks();
2278
2324
  const doc = await this.adapter.findById(this.slug, id);
2279
2325
  if (!doc) {
2280
- return null;
2326
+ throw new DocumentNotFoundError(this.slug, id);
2281
2327
  }
2282
2328
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2283
2329
  if (softDeleteField && !options?.withDeleted && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T is compatible with Record<string, unknown>
2284
2330
  doc[softDeleteField]) {
2285
- return null;
2331
+ throw new DocumentNotFoundError(this.slug, id);
2332
+ }
2333
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
2334
+ const canSeeDrafts = await this.canReadDrafts();
2335
+ if (!canSeeDrafts) {
2336
+ const record = doc;
2337
+ if (record["_status"] !== "published") {
2338
+ throw new DraftNotVisibleError(this.slug, id);
2339
+ }
2340
+ }
2286
2341
  }
2287
2342
  if (!this.matchesDefaultWhereConstraints(doc)) {
2288
- return null;
2343
+ throw new DocumentNotFoundError(this.slug, id);
2289
2344
  }
2290
2345
  const [processed] = await this.processAfterReadHooks([doc]);
2291
2346
  const MAX_RELATIONSHIP_DEPTH = 10;
@@ -2369,6 +2424,15 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2369
2424
  if (!this.matchesDefaultWhereConstraints(originalDoc)) {
2370
2425
  throw new DocumentNotFoundError(this.slug, id);
2371
2426
  }
2427
+ if (hasVersionDrafts(this.collectionConfig) && this.adapter.createVersion) {
2428
+ const status = originalDoc["_status"] ?? "draft";
2429
+ await this.adapter.createVersion(this.slug, id, originalDoc, { status });
2430
+ const versionsConfig = this.collectionConfig.versions;
2431
+ const maxPerDoc = typeof versionsConfig === "object" && versionsConfig !== null ? versionsConfig.maxPerDoc : void 0;
2432
+ if (maxPerDoc && this.adapter.deleteVersions) {
2433
+ await this.adapter.deleteVersions(this.slug, id, maxPerDoc);
2434
+ }
2435
+ }
2372
2436
  let processedData = data;
2373
2437
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
2374
2438
  if (softDeleteField && softDeleteField in processedData) {
@@ -2700,6 +2764,30 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2700
2764
  throw new AccessDeniedError(operation, this.slug);
2701
2765
  }
2702
2766
  }
2767
+ /**
2768
+ * Check if the current user can see draft documents (non-throwing).
2769
+ * Uses `access.readDrafts` if configured, otherwise falls back to `access.update`.
2770
+ */
2771
+ async canReadDrafts() {
2772
+ if (!this.context.user)
2773
+ return false;
2774
+ const readDraftsFn = this.collectionConfig.access?.readDrafts;
2775
+ if (readDraftsFn) {
2776
+ try {
2777
+ return !!await Promise.resolve(readDraftsFn({ req: this.buildRequestContext() }));
2778
+ } catch {
2779
+ return false;
2780
+ }
2781
+ }
2782
+ const updateFn = this.collectionConfig.access?.update;
2783
+ if (!updateFn)
2784
+ return true;
2785
+ try {
2786
+ return !!await Promise.resolve(updateFn({ req: this.buildRequestContext() }));
2787
+ } catch {
2788
+ return false;
2789
+ }
2790
+ }
2703
2791
  buildRequestContext() {
2704
2792
  return {
2705
2793
  user: this.context.user
@@ -3123,9 +3211,6 @@ function createMomentumHandlers(config) {
3123
3211
  const depth = typeof request.query?.["depth"] === "number" ? request.query["depth"] : void 0;
3124
3212
  const withDeleted = request.query?.["withDeleted"] === true;
3125
3213
  const doc = await api.collection(request.collectionSlug).findById(request.id, { depth, withDeleted });
3126
- if (!doc) {
3127
- return { error: "Document not found", status: 404 };
3128
- }
3129
3214
  return { doc };
3130
3215
  } catch (error) {
3131
3216
  return handleError(error);
@@ -3242,6 +3327,9 @@ function handleError(error) {
3242
3327
  if (error instanceof DocumentNotFoundError) {
3243
3328
  return { error: error.message, status: 404 };
3244
3329
  }
3330
+ if (error instanceof DraftNotVisibleError) {
3331
+ return { doc: null };
3332
+ }
3245
3333
  if (error instanceof AccessDeniedError) {
3246
3334
  return { error: error.message, status: 403 };
3247
3335
  }
@@ -3392,8 +3480,9 @@ function buildGraphQLSchema(collections) {
3392
3480
  }
3393
3481
  function getOrCreateEnum(field, parentName) {
3394
3482
  const key = `${parentName}_${field.name}`;
3395
- if (enumCache.has(key)) {
3396
- return enumCache.get(key);
3483
+ const cached = enumCache.get(key);
3484
+ if (cached) {
3485
+ return cached;
3397
3486
  }
3398
3487
  const values = {};
3399
3488
  for (const opt of field.options) {
@@ -3504,8 +3593,9 @@ function buildGraphQLSchema(collections) {
3504
3593
  }
3505
3594
  }
3506
3595
  function getOrCreateObjectType(fields, typeName) {
3507
- if (typeCache.has(typeName)) {
3508
- return typeCache.get(typeName);
3596
+ const cachedType = typeCache.get(typeName);
3597
+ if (cachedType) {
3598
+ return cachedType;
3509
3599
  }
3510
3600
  const objType = new GraphQLObjectType({
3511
3601
  name: typeName,
@@ -3526,8 +3616,9 @@ function buildGraphQLSchema(collections) {
3526
3616
  }
3527
3617
  function getOrCreateInputType(fields, typeName) {
3528
3618
  const inputName = `${typeName}Input`;
3529
- if (inputCache.has(inputName)) {
3530
- return inputCache.get(inputName);
3619
+ const cachedInput = inputCache.get(inputName);
3620
+ if (cachedInput) {
3621
+ return cachedInput;
3531
3622
  }
3532
3623
  const inputType = new GraphQLInputObjectType({
3533
3624
  name: inputName,
@@ -3648,7 +3739,14 @@ function buildGraphQLSchema(collections) {
3648
3739
  const api = getMomentumAPI();
3649
3740
  const ctx = buildAPIContext(context);
3650
3741
  const contextApi = Object.keys(ctx).length > 0 ? api.setContext(ctx) : api;
3651
- 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
+ }
3652
3750
  }
3653
3751
  };
3654
3752
  queryFields[plural.charAt(0).toLowerCase() + plural.slice(1)] = {
@@ -5279,7 +5377,13 @@ function createComprehensiveMomentumHandler(config) {
5279
5377
  return { docs: r.docs, totalDocs: r.totalDocs };
5280
5378
  },
5281
5379
  findById: async (slug2, id) => {
5282
- 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
+ }
5283
5387
  },
5284
5388
  count: (slug2) => contextApi.collection(slug2).count(),
5285
5389
  create: async (slug2, data) => {
@@ -5644,6 +5748,10 @@ function createComprehensiveMomentumHandler(config) {
5644
5748
  });
5645
5749
  return { doc: restored, message: "Version restored successfully" };
5646
5750
  } catch (error) {
5751
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5752
+ utils.setResponseStatus(event, 403);
5753
+ return { error: "Access denied" };
5754
+ }
5647
5755
  const message = sanitizeErrorMessage(error, "Unknown error");
5648
5756
  if (error instanceof Error && error.message.includes("mismatch")) {
5649
5757
  utils.setResponseStatus(event, 400);
@@ -5684,6 +5792,10 @@ function createComprehensiveMomentumHandler(config) {
5684
5792
  );
5685
5793
  return { differences };
5686
5794
  } catch (error) {
5795
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5796
+ utils.setResponseStatus(event, 403);
5797
+ return { error: "Access denied" };
5798
+ }
5687
5799
  utils.setResponseStatus(event, 500);
5688
5800
  return {
5689
5801
  error: "Failed to compare versions",
@@ -5711,6 +5823,10 @@ function createComprehensiveMomentumHandler(config) {
5711
5823
  }
5712
5824
  return version;
5713
5825
  } catch (error) {
5826
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5827
+ utils.setResponseStatus(event, 403);
5828
+ return { error: "Access denied" };
5829
+ }
5714
5830
  utils.setResponseStatus(event, 500);
5715
5831
  return {
5716
5832
  error: "Failed to fetch version",
@@ -5735,6 +5851,10 @@ function createComprehensiveMomentumHandler(config) {
5735
5851
  });
5736
5852
  return result;
5737
5853
  } catch (error) {
5854
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5855
+ utils.setResponseStatus(event, 403);
5856
+ return { error: "Access denied" };
5857
+ }
5738
5858
  utils.setResponseStatus(event, 500);
5739
5859
  return {
5740
5860
  error: "Failed to fetch versions",
@@ -5775,12 +5895,7 @@ function createComprehensiveMomentumHandler(config) {
5775
5895
  }
5776
5896
  } else {
5777
5897
  const contextApi = getContextualAPI(user);
5778
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5779
- if (!doc) {
5780
- utils.setResponseStatus(event, 404);
5781
- return { error: "Document not found" };
5782
- }
5783
- docRecord = doc;
5898
+ docRecord = await contextApi.collection(collectionSlug2).findById(docId);
5784
5899
  }
5785
5900
  const emailField = getEmailBuilderFieldName(collectionConfig);
5786
5901
  const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
@@ -5849,6 +5964,10 @@ function createComprehensiveMomentumHandler(config) {
5849
5964
  return { message: "Scheduled publish cancelled" };
5850
5965
  }
5851
5966
  } catch (error) {
5967
+ if (error instanceof Error && error.name === "AccessDeniedError") {
5968
+ utils.setResponseStatus(event, 403);
5969
+ return { error: "Access denied" };
5970
+ }
5852
5971
  utils.setResponseStatus(event, 500);
5853
5972
  return {
5854
5973
  error: `Failed to ${action.replace(/-/g, " ")}`,
@@ -5890,6 +6009,10 @@ function createComprehensiveMomentumHandler(config) {
5890
6009
  const status = await versionOps.getStatus(docId);
5891
6010
  return { status };
5892
6011
  } catch (error) {
6012
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6013
+ utils.setResponseStatus(event, 403);
6014
+ return { error: "Access denied" };
6015
+ }
5893
6016
  utils.setResponseStatus(event, 500);
5894
6017
  return {
5895
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.3",
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",