@momentumcms/server-express 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 +56 -0
  2. package/index.cjs +323 -156
  3. package/index.js +201 -34
  4. package/package.json +1 -1
package/index.js CHANGED
@@ -2202,8 +2202,27 @@ var MediaCollection = defineCollection({
2202
2202
  }
2203
2203
  });
2204
2204
 
2205
+ // libs/core/src/lib/migrations.ts
2206
+ function resolveMigrationMode(mode) {
2207
+ if (mode === "push" || mode === "migrate")
2208
+ return mode;
2209
+ const env = globalThis["process"]?.env?.["NODE_ENV"];
2210
+ if (env === "production")
2211
+ return "migrate";
2212
+ return "push";
2213
+ }
2214
+
2205
2215
  // libs/core/src/lib/config.ts
2206
2216
  var MIN_PASSWORD_LENGTH = 8;
2217
+ function shouldSyncSchema(config) {
2218
+ const explicit = config.db.syncSchema ?? "auto";
2219
+ if (typeof explicit === "boolean")
2220
+ return explicit;
2221
+ if (!config.migrations)
2222
+ return true;
2223
+ const mode = resolveMigrationMode(config.migrations.mode);
2224
+ return mode !== "migrate";
2225
+ }
2207
2226
 
2208
2227
  // libs/core/src/lib/seeding/seeding.types.ts
2209
2228
  var SEED_TRACKING_COLLECTION_SLUG = "_momentum_seeds";
@@ -2291,6 +2310,14 @@ function createSeedHelpers() {
2291
2310
  };
2292
2311
  }
2293
2312
 
2313
+ // libs/core/src/lib/versions/version.types.ts
2314
+ function hasVersionDrafts(collection) {
2315
+ const v = collection.versions;
2316
+ if (!v || typeof v === "boolean")
2317
+ return false;
2318
+ return !!v.drafts;
2319
+ }
2320
+
2294
2321
  // libs/server-core/src/lib/field-access.ts
2295
2322
  function hasFieldAccessControl(fields) {
2296
2323
  for (const field of fields) {
@@ -2645,6 +2672,12 @@ var DocumentNotFoundError = class extends Error {
2645
2672
  this.name = "DocumentNotFoundError";
2646
2673
  }
2647
2674
  };
2675
+ var DraftNotVisibleError = class extends Error {
2676
+ constructor(collection, id) {
2677
+ super(`Draft "${id}" in collection "${collection}" is not visible to the current user`);
2678
+ this.name = "DraftNotVisibleError";
2679
+ }
2680
+ };
2648
2681
  var AccessDeniedError = class extends Error {
2649
2682
  constructor(operation, collection) {
2650
2683
  super(`Access denied for ${operation} on collection "${collection}"`);
@@ -2877,13 +2910,18 @@ var VersionOperationsImpl = class {
2877
2910
  }
2878
2911
  return "draft";
2879
2912
  }
2880
- async compare(versionId1, versionId2) {
2913
+ async compare(versionId1, versionId2, parentId) {
2881
2914
  await this.checkAccess("readVersions");
2882
2915
  const version1 = await this.findVersionById(versionId1);
2883
2916
  const version2 = await this.findVersionById(versionId2);
2884
2917
  if (!version1 || !version2) {
2885
2918
  throw new Error("One or both versions not found");
2886
2919
  }
2920
+ if (parentId) {
2921
+ if (version1.parent !== parentId || version2.parent !== parentId) {
2922
+ throw new Error("Version does not belong to the specified document");
2923
+ }
2924
+ }
2887
2925
  const data1 = version1.version;
2888
2926
  const data2 = version2.version;
2889
2927
  if (!isRecord(data1) || !isRecord(data2)) {
@@ -2938,6 +2976,9 @@ var VersionOperationsImpl = class {
2938
2976
  // Private Helpers
2939
2977
  // ============================================
2940
2978
  async checkAccess(operation) {
2979
+ if (this.context.overrideAccess) {
2980
+ return;
2981
+ }
2941
2982
  const accessFn = this.collectionConfig.access?.[operation];
2942
2983
  if (!accessFn) {
2943
2984
  return;
@@ -3054,13 +3095,31 @@ function stripTransientKeys(data) {
3054
3095
  }
3055
3096
  return result;
3056
3097
  }
3098
+ var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
3057
3099
  function flattenWhereClause(where) {
3058
3100
  if (!where)
3059
3101
  return {};
3060
3102
  const result = {};
3061
3103
  for (const [field, condition] of Object.entries(where)) {
3062
- if (typeof condition === "object" && condition !== null && "equals" in condition) {
3063
- result[field] = condition["equals"];
3104
+ if (typeof condition !== "object" || condition === null) {
3105
+ result[field] = condition;
3106
+ continue;
3107
+ }
3108
+ const condObj = condition;
3109
+ if ("equals" in condObj) {
3110
+ result[field] = condObj["equals"];
3111
+ continue;
3112
+ }
3113
+ const ops = {};
3114
+ let hasComparisonOp = false;
3115
+ for (const op of COMPARISON_OPS) {
3116
+ if (op in condObj) {
3117
+ ops[`$${op}`] = condObj[op];
3118
+ hasComparisonOp = true;
3119
+ }
3120
+ }
3121
+ if (hasComparisonOp) {
3122
+ result[field] = ops;
3064
3123
  } else {
3065
3124
  result[field] = condition;
3066
3125
  }
@@ -3094,6 +3153,12 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
3094
3153
  Object.assign(whereParams, constraints);
3095
3154
  }
3096
3155
  }
3156
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
3157
+ const canSeeDrafts = await this.canReadDrafts();
3158
+ if (!canSeeDrafts) {
3159
+ whereParams["_status"] = "published";
3160
+ }
3161
+ }
3097
3162
  const query = {
3098
3163
  ...queryOptions,
3099
3164
  ...whereParams,
@@ -3149,15 +3214,24 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
3149
3214
  await this.runBeforeReadHooks();
3150
3215
  const doc = await this.adapter.findById(this.slug, id);
3151
3216
  if (!doc) {
3152
- return null;
3217
+ throw new DocumentNotFoundError(this.slug, id);
3153
3218
  }
3154
3219
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
3155
3220
  if (softDeleteField && !options?.withDeleted && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- T is compatible with Record<string, unknown>
3156
3221
  doc[softDeleteField]) {
3157
- return null;
3222
+ throw new DocumentNotFoundError(this.slug, id);
3223
+ }
3224
+ if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
3225
+ const canSeeDrafts = await this.canReadDrafts();
3226
+ if (!canSeeDrafts) {
3227
+ const record = doc;
3228
+ if (record["_status"] !== "published") {
3229
+ throw new DraftNotVisibleError(this.slug, id);
3230
+ }
3231
+ }
3158
3232
  }
3159
3233
  if (!this.matchesDefaultWhereConstraints(doc)) {
3160
- return null;
3234
+ throw new DocumentNotFoundError(this.slug, id);
3161
3235
  }
3162
3236
  const [processed] = await this.processAfterReadHooks([doc]);
3163
3237
  const MAX_RELATIONSHIP_DEPTH = 10;
@@ -3241,6 +3315,15 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
3241
3315
  if (!this.matchesDefaultWhereConstraints(originalDoc)) {
3242
3316
  throw new DocumentNotFoundError(this.slug, id);
3243
3317
  }
3318
+ if (hasVersionDrafts(this.collectionConfig) && this.adapter.createVersion) {
3319
+ const status = originalDoc["_status"] ?? "draft";
3320
+ await this.adapter.createVersion(this.slug, id, originalDoc, { status });
3321
+ const versionsConfig = this.collectionConfig.versions;
3322
+ const maxPerDoc = typeof versionsConfig === "object" && versionsConfig !== null ? versionsConfig.maxPerDoc : void 0;
3323
+ if (maxPerDoc && this.adapter.deleteVersions) {
3324
+ await this.adapter.deleteVersions(this.slug, id, maxPerDoc);
3325
+ }
3326
+ }
3244
3327
  let processedData = data;
3245
3328
  const softDeleteField = getSoftDeleteField(this.collectionConfig);
3246
3329
  if (softDeleteField && softDeleteField in processedData) {
@@ -3572,6 +3655,30 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
3572
3655
  throw new AccessDeniedError(operation, this.slug);
3573
3656
  }
3574
3657
  }
3658
+ /**
3659
+ * Check if the current user can see draft documents (non-throwing).
3660
+ * Uses `access.readDrafts` if configured, otherwise falls back to `access.update`.
3661
+ */
3662
+ async canReadDrafts() {
3663
+ if (!this.context.user)
3664
+ return false;
3665
+ const readDraftsFn = this.collectionConfig.access?.readDrafts;
3666
+ if (readDraftsFn) {
3667
+ try {
3668
+ return !!await Promise.resolve(readDraftsFn({ req: this.buildRequestContext() }));
3669
+ } catch {
3670
+ return false;
3671
+ }
3672
+ }
3673
+ const updateFn = this.collectionConfig.access?.update;
3674
+ if (!updateFn)
3675
+ return true;
3676
+ try {
3677
+ return !!await Promise.resolve(updateFn({ req: this.buildRequestContext() }));
3678
+ } catch {
3679
+ return false;
3680
+ }
3681
+ }
3575
3682
  buildRequestContext() {
3576
3683
  return {
3577
3684
  user: this.context.user
@@ -3995,9 +4102,6 @@ function createMomentumHandlers(config) {
3995
4102
  const depth = typeof request.query?.["depth"] === "number" ? request.query["depth"] : void 0;
3996
4103
  const withDeleted = request.query?.["withDeleted"] === true;
3997
4104
  const doc = await api.collection(request.collectionSlug).findById(request.id, { depth, withDeleted });
3998
- if (!doc) {
3999
- return { error: "Document not found", status: 404 };
4000
- }
4001
4105
  return { doc };
4002
4106
  } catch (error) {
4003
4107
  return handleError(error);
@@ -4114,6 +4218,9 @@ function handleError(error) {
4114
4218
  if (error instanceof DocumentNotFoundError) {
4115
4219
  return { error: error.message, status: 404 };
4116
4220
  }
4221
+ if (error instanceof DraftNotVisibleError) {
4222
+ return { doc: null };
4223
+ }
4117
4224
  if (error instanceof AccessDeniedError) {
4118
4225
  return { error: error.message, status: 403 };
4119
4226
  }
@@ -4566,7 +4673,10 @@ async function sendWebhook(webhook, payload, attempt = 0) {
4566
4673
  webhookLogger.warn(`Blocked request to disallowed URL: ${webhook.url}`);
4567
4674
  return;
4568
4675
  }
4569
- const body = JSON.stringify(payload);
4676
+ const body = JSON.stringify(
4677
+ payload,
4678
+ (_key, value) => typeof value === "bigint" ? Number(value) : value
4679
+ );
4570
4680
  const maxRetries = webhook.retries ?? 0;
4571
4681
  const headers = {
4572
4682
  "Content-Type": "application/json",
@@ -4708,7 +4818,7 @@ function startPublishScheduler(adapter, collections, options) {
4708
4818
  const scheduled = await adapter.findScheduledDocuments(collection.slug, now);
4709
4819
  for (const doc of scheduled) {
4710
4820
  try {
4711
- const api = getMomentumAPI();
4821
+ const api = getMomentumAPI().setContext({ overrideAccess: true });
4712
4822
  const versionOps = api.collection(collection.slug).versions();
4713
4823
  if (!versionOps)
4714
4824
  continue;
@@ -4828,8 +4938,9 @@ function buildGraphQLSchema(collections) {
4828
4938
  }
4829
4939
  function getOrCreateEnum(field, parentName) {
4830
4940
  const key = `${parentName}_${field.name}`;
4831
- if (enumCache.has(key)) {
4832
- return enumCache.get(key);
4941
+ const cached = enumCache.get(key);
4942
+ if (cached) {
4943
+ return cached;
4833
4944
  }
4834
4945
  const values = {};
4835
4946
  for (const opt of field.options) {
@@ -4940,8 +5051,9 @@ function buildGraphQLSchema(collections) {
4940
5051
  }
4941
5052
  }
4942
5053
  function getOrCreateObjectType(fields, typeName) {
4943
- if (typeCache.has(typeName)) {
4944
- return typeCache.get(typeName);
5054
+ const cachedType = typeCache.get(typeName);
5055
+ if (cachedType) {
5056
+ return cachedType;
4945
5057
  }
4946
5058
  const objType = new GraphQLObjectType({
4947
5059
  name: typeName,
@@ -4962,8 +5074,9 @@ function buildGraphQLSchema(collections) {
4962
5074
  }
4963
5075
  function getOrCreateInputType(fields, typeName) {
4964
5076
  const inputName = `${typeName}Input`;
4965
- if (inputCache.has(inputName)) {
4966
- return inputCache.get(inputName);
5077
+ const cachedInput = inputCache.get(inputName);
5078
+ if (cachedInput) {
5079
+ return cachedInput;
4967
5080
  }
4968
5081
  const inputType = new GraphQLInputObjectType({
4969
5082
  name: inputName,
@@ -5084,7 +5197,14 @@ function buildGraphQLSchema(collections) {
5084
5197
  const api = getMomentumAPI();
5085
5198
  const ctx = buildAPIContext(context);
5086
5199
  const contextApi = Object.keys(ctx).length > 0 ? api.setContext(ctx) : api;
5087
- return contextApi.collection(col.slug).findById(args.id);
5200
+ try {
5201
+ return await contextApi.collection(col.slug).findById(args.id);
5202
+ } catch (err) {
5203
+ if (err instanceof Error && err.name === "DocumentNotFoundError") {
5204
+ return null;
5205
+ }
5206
+ throw err;
5207
+ }
5088
5208
  }
5089
5209
  };
5090
5210
  queryFields[plural.charAt(0).toLowerCase() + plural.slice(1)] = {
@@ -6375,6 +6495,22 @@ var RateLimiter = class _RateLimiter {
6375
6495
  }
6376
6496
  };
6377
6497
 
6498
+ // libs/server-core/src/lib/schema-sync.ts
6499
+ async function syncDatabaseSchema(config, log) {
6500
+ if (shouldSyncSchema(config)) {
6501
+ if (config.db.adapter.initialize) {
6502
+ log.info("Initializing database schema...");
6503
+ await config.db.adapter.initialize(config.collections);
6504
+ }
6505
+ if (config.db.adapter.initializeGlobals && config.globals && config.globals.length > 0) {
6506
+ log.info(`Initializing globals table for ${config.globals.length} global(s)...`);
6507
+ await config.db.adapter.initializeGlobals(config.globals);
6508
+ }
6509
+ } else {
6510
+ log.info("Skipping automatic schema sync (migration mode). Run migrations separately.");
6511
+ }
6512
+ }
6513
+
6378
6514
  // libs/server-core/src/lib/shared-server-utils.ts
6379
6515
  function sanitizeErrorMessage(error, fallback) {
6380
6516
  if (!(error instanceof Error))
@@ -6792,6 +6928,10 @@ function momentumApiMiddleware(config) {
6792
6928
  res.json(result);
6793
6929
  } catch (error) {
6794
6930
  const message = sanitizeErrorMessage(error, "Unknown error");
6931
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6932
+ res.status(403).json({ error: "Access denied" });
6933
+ return;
6934
+ }
6795
6935
  res.status(500).json({ error: "Failed to fetch versions", message });
6796
6936
  }
6797
6937
  });
@@ -6820,6 +6960,10 @@ function momentumApiMiddleware(config) {
6820
6960
  res.json(version);
6821
6961
  } catch (error) {
6822
6962
  const message = sanitizeErrorMessage(error, "Unknown error");
6963
+ if (error instanceof Error && error.name === "AccessDeniedError") {
6964
+ res.status(403).json({ error: "Access denied" });
6965
+ return;
6966
+ }
6823
6967
  res.status(500).json({ error: "Failed to fetch version", message });
6824
6968
  }
6825
6969
  });
@@ -6858,6 +7002,10 @@ function momentumApiMiddleware(config) {
6858
7002
  res.status(400).json({ error: "Version parent mismatch", message });
6859
7003
  return;
6860
7004
  }
7005
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7006
+ res.status(403).json({ error: "Access denied" });
7007
+ return;
7008
+ }
6861
7009
  res.status(500).json({ error: "Failed to restore version", message });
6862
7010
  }
6863
7011
  });
@@ -6879,6 +7027,10 @@ function momentumApiMiddleware(config) {
6879
7027
  res.json({ doc: published, message: "Document published successfully" });
6880
7028
  } catch (error) {
6881
7029
  const message = sanitizeErrorMessage(error, "Unknown error");
7030
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7031
+ res.status(403).json({ error: "Access denied" });
7032
+ return;
7033
+ }
6882
7034
  res.status(500).json({ error: "Failed to publish document", message });
6883
7035
  }
6884
7036
  });
@@ -6908,6 +7060,10 @@ function momentumApiMiddleware(config) {
6908
7060
  res.json(result);
6909
7061
  } catch (error) {
6910
7062
  const message = sanitizeErrorMessage(error, "Unknown error");
7063
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7064
+ res.status(403).json({ error: "Access denied" });
7065
+ return;
7066
+ }
6911
7067
  res.status(500).json({ error: "Failed to schedule publish", message });
6912
7068
  }
6913
7069
  });
@@ -6929,6 +7085,10 @@ function momentumApiMiddleware(config) {
6929
7085
  res.json({ message: "Scheduled publish cancelled" });
6930
7086
  } catch (error) {
6931
7087
  const message = sanitizeErrorMessage(error, "Unknown error");
7088
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7089
+ res.status(403).json({ error: "Access denied" });
7090
+ return;
7091
+ }
6932
7092
  res.status(500).json({ error: "Failed to cancel scheduled publish", message });
6933
7093
  }
6934
7094
  });
@@ -6950,6 +7110,10 @@ function momentumApiMiddleware(config) {
6950
7110
  res.json({ doc: unpublished, message: "Document unpublished successfully" });
6951
7111
  } catch (error) {
6952
7112
  const message = sanitizeErrorMessage(error, "Unknown error");
7113
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7114
+ res.status(403).json({ error: "Access denied" });
7115
+ return;
7116
+ }
6953
7117
  res.status(500).json({ error: "Failed to unpublish document", message });
6954
7118
  }
6955
7119
  });
@@ -6972,6 +7136,10 @@ function momentumApiMiddleware(config) {
6972
7136
  res.json({ version: draft, message: "Draft saved successfully" });
6973
7137
  } catch (error) {
6974
7138
  const message = sanitizeErrorMessage(error, "Unknown error");
7139
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7140
+ res.status(403).json({ error: "Access denied" });
7141
+ return;
7142
+ }
6975
7143
  res.status(500).json({ error: "Failed to save draft", message });
6976
7144
  }
6977
7145
  });
@@ -6997,10 +7165,15 @@ function momentumApiMiddleware(config) {
6997
7165
  });
6998
7166
  return;
6999
7167
  }
7000
- const differences = await versionOps.compare(versionId1, versionId2);
7168
+ const parentId = req.params["id"];
7169
+ const differences = await versionOps.compare(versionId1, versionId2, parentId);
7001
7170
  res.json({ differences });
7002
7171
  } catch (error) {
7003
7172
  const message = sanitizeErrorMessage(error, "Unknown error");
7173
+ if (error instanceof Error && error.name === "AccessDeniedError") {
7174
+ res.status(403).json({ error: "Access denied" });
7175
+ return;
7176
+ }
7004
7177
  res.status(500).json({ error: "Failed to compare versions", message });
7005
7178
  }
7006
7179
  });
@@ -7053,12 +7226,7 @@ function momentumApiMiddleware(config) {
7053
7226
  } else {
7054
7227
  const api = getMomentumAPI();
7055
7228
  const contextApi = user ? api.setContext({ user }) : api;
7056
- const dbDoc = await contextApi.collection(slug2).findById(id);
7057
- if (!dbDoc) {
7058
- res.status(404).json({ error: "Document not found" });
7059
- return;
7060
- }
7061
- doc = dbDoc;
7229
+ doc = await contextApi.collection(slug2).findById(id);
7062
7230
  }
7063
7231
  const emailField = getEmailBuilderFieldName(collectionConfig);
7064
7232
  const html = emailField ? await renderEmailPreviewHTML(doc, emailField) : renderPreviewHTML({ doc, collection: collectionConfig });
@@ -7251,7 +7419,13 @@ function momentumApiMiddleware(config) {
7251
7419
  return { docs: r.docs, totalDocs: r.totalDocs };
7252
7420
  },
7253
7421
  findById: async (slug2, id) => {
7254
- return await ctxApi.collection(slug2).findById(id);
7422
+ try {
7423
+ return await ctxApi.collection(slug2).findById(id);
7424
+ } catch (err) {
7425
+ if (err instanceof Error && err.name === "DocumentNotFoundError")
7426
+ return null;
7427
+ throw err;
7428
+ }
7255
7429
  },
7256
7430
  count: (slug2) => ctxApi.collection(slug2).count(),
7257
7431
  create: async (slug2, data) => {
@@ -8649,14 +8823,7 @@ function initializeMomentum(config, options = {}) {
8649
8823
  }
8650
8824
  }
8651
8825
  setPluginMiddleware(pluginMiddleware2);
8652
- if (config.db.adapter.initialize) {
8653
- log.info("Initializing database schema...");
8654
- await config.db.adapter.initialize(config.collections);
8655
- }
8656
- if (config.db.adapter.initializeGlobals && config.globals && config.globals.length > 0) {
8657
- log.info(`Initializing globals table for ${config.globals.length} global(s)...`);
8658
- await config.db.adapter.initializeGlobals(config.globals);
8659
- }
8826
+ await syncDatabaseSchema(config, log);
8660
8827
  log.info("Initializing API...");
8661
8828
  const api = initializeMomentumAPI(config);
8662
8829
  const runOnStart = config.seeding?.options?.runOnStart ?? "development";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/server-express",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Express adapter for Momentum CMS with Angular SSR support",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",