@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.
- package/CHANGELOG.md +34 -0
- package/index.cjs +146 -23
- package/index.js +146 -23
- 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
|
|
2224
|
-
result[field] = condition
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3416
|
-
|
|
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
|
-
|
|
3528
|
-
|
|
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
|
-
|
|
3550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2191
|
-
result[field] = condition
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3396
|
-
|
|
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
|
-
|
|
3508
|
-
|
|
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
|
-
|
|
3530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|