@momentumcms/server-analog 0.5.9 → 0.5.11
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 +15 -0
- package/index.cjs +610 -132
- package/index.js +610 -132
- package/package.json +33 -44
package/index.js
CHANGED
|
@@ -145,7 +145,7 @@ function s3StorageAdapter(options) {
|
|
|
145
145
|
baseUrl,
|
|
146
146
|
forcePathStyle = false,
|
|
147
147
|
acl = "private",
|
|
148
|
-
presignedUrlExpiry =
|
|
148
|
+
presignedUrlExpiry = 900
|
|
149
149
|
} = options;
|
|
150
150
|
let client = null;
|
|
151
151
|
async function getClient() {
|
|
@@ -1854,6 +1854,21 @@ var VersionOperationsImpl = class {
|
|
|
1854
1854
|
});
|
|
1855
1855
|
}
|
|
1856
1856
|
}
|
|
1857
|
+
if (!this.context.overrideAccess && hasFieldAccessControl(this.collectionConfig.fields)) {
|
|
1858
|
+
for (let i = 0; i < docs.length; i++) {
|
|
1859
|
+
const version = docs[i].version;
|
|
1860
|
+
if (version && typeof version === "object") {
|
|
1861
|
+
const versionRecord = version;
|
|
1862
|
+
const filtered = await filterReadableFields(
|
|
1863
|
+
this.collectionConfig.fields,
|
|
1864
|
+
versionRecord,
|
|
1865
|
+
this.buildRequestContext()
|
|
1866
|
+
);
|
|
1867
|
+
const filteredAsT = filtered;
|
|
1868
|
+
docs[i] = { ...docs[i], version: filteredAsT };
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1857
1872
|
const countOptions = {
|
|
1858
1873
|
includeAutosave: options?.includeAutosave,
|
|
1859
1874
|
status: options?.status
|
|
@@ -1883,9 +1898,21 @@ var VersionOperationsImpl = class {
|
|
|
1883
1898
|
if (parsedVersion === null) {
|
|
1884
1899
|
return null;
|
|
1885
1900
|
}
|
|
1901
|
+
let filteredVersion = parsedVersion;
|
|
1902
|
+
if (!this.context.overrideAccess && hasFieldAccessControl(this.collectionConfig.fields)) {
|
|
1903
|
+
if (filteredVersion && typeof filteredVersion === "object") {
|
|
1904
|
+
const versionRecord = filteredVersion;
|
|
1905
|
+
const filtered = await filterReadableFields(
|
|
1906
|
+
this.collectionConfig.fields,
|
|
1907
|
+
versionRecord,
|
|
1908
|
+
this.buildRequestContext()
|
|
1909
|
+
);
|
|
1910
|
+
filteredVersion = filtered;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1886
1913
|
return {
|
|
1887
1914
|
...version,
|
|
1888
|
-
version:
|
|
1915
|
+
version: filteredVersion
|
|
1889
1916
|
};
|
|
1890
1917
|
}
|
|
1891
1918
|
async restore(options) {
|
|
@@ -2107,6 +2134,415 @@ var VersionOperationsImpl = class {
|
|
|
2107
2134
|
}
|
|
2108
2135
|
};
|
|
2109
2136
|
|
|
2137
|
+
// libs/server-core/src/lib/where-clause.ts
|
|
2138
|
+
var OPERATOR_MAP = {
|
|
2139
|
+
equals: "$eq",
|
|
2140
|
+
gt: "$gt",
|
|
2141
|
+
gte: "$gte",
|
|
2142
|
+
lt: "$lt",
|
|
2143
|
+
lte: "$lte",
|
|
2144
|
+
not_equals: "$ne",
|
|
2145
|
+
like: "$like",
|
|
2146
|
+
contains: "$contains",
|
|
2147
|
+
in: "$in",
|
|
2148
|
+
not_in: "$nin",
|
|
2149
|
+
exists: "$exists"
|
|
2150
|
+
};
|
|
2151
|
+
var MAX_WHERE_CONDITIONS = 20;
|
|
2152
|
+
var MAX_JOINS = 5;
|
|
2153
|
+
var MAX_WHERE_NESTING_DEPTH = 5;
|
|
2154
|
+
var MAX_PAGE_LIMIT = 1e3;
|
|
2155
|
+
var MAX_PAGE = 1e6;
|
|
2156
|
+
var VALID_OPERATORS = new Set(Object.keys(OPERATOR_MAP));
|
|
2157
|
+
function countWhereConditions(where, depth = 0) {
|
|
2158
|
+
if (depth > MAX_WHERE_NESTING_DEPTH) {
|
|
2159
|
+
throw new ValidationError([
|
|
2160
|
+
{
|
|
2161
|
+
field: "where",
|
|
2162
|
+
message: `Where clause nesting depth exceeds maximum of ${MAX_WHERE_NESTING_DEPTH} levels.`
|
|
2163
|
+
}
|
|
2164
|
+
]);
|
|
2165
|
+
}
|
|
2166
|
+
let count = 0;
|
|
2167
|
+
for (const [key, value] of Object.entries(where)) {
|
|
2168
|
+
if ((key === "and" || key === "or") && Array.isArray(value)) {
|
|
2169
|
+
for (const sub of value) {
|
|
2170
|
+
if (typeof sub === "object" && sub !== null) {
|
|
2171
|
+
count += countWhereConditions(sub, depth + 1);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
} else {
|
|
2175
|
+
count++;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return count;
|
|
2179
|
+
}
|
|
2180
|
+
function flattenWhereClause(where) {
|
|
2181
|
+
if (!where)
|
|
2182
|
+
return {};
|
|
2183
|
+
const fieldCount = countWhereConditions(where);
|
|
2184
|
+
if (fieldCount > MAX_WHERE_CONDITIONS) {
|
|
2185
|
+
throw new ValidationError([
|
|
2186
|
+
{
|
|
2187
|
+
field: "where",
|
|
2188
|
+
message: `Where clause exceeds maximum of ${MAX_WHERE_CONDITIONS} conditions (got ${fieldCount}).`
|
|
2189
|
+
}
|
|
2190
|
+
]);
|
|
2191
|
+
}
|
|
2192
|
+
return flattenWhereRecursive(where, 0);
|
|
2193
|
+
}
|
|
2194
|
+
function flattenWhereRecursive(where, depth) {
|
|
2195
|
+
if (depth > MAX_WHERE_NESTING_DEPTH) {
|
|
2196
|
+
throw new ValidationError([
|
|
2197
|
+
{
|
|
2198
|
+
field: "where",
|
|
2199
|
+
message: `Where clause nesting depth exceeds maximum of ${MAX_WHERE_NESTING_DEPTH} levels.`
|
|
2200
|
+
}
|
|
2201
|
+
]);
|
|
2202
|
+
}
|
|
2203
|
+
const result = {};
|
|
2204
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
2205
|
+
if (field === "and" || field === "or") {
|
|
2206
|
+
if (!Array.isArray(condition)) {
|
|
2207
|
+
throw new ValidationError([
|
|
2208
|
+
{
|
|
2209
|
+
field,
|
|
2210
|
+
message: `The "${field}" operator requires an array of conditions.`
|
|
2211
|
+
}
|
|
2212
|
+
]);
|
|
2213
|
+
}
|
|
2214
|
+
const internalKey = field === "and" ? "$and" : "$or";
|
|
2215
|
+
result[internalKey] = condition.filter((sub) => typeof sub === "object" && sub !== null).map((sub) => {
|
|
2216
|
+
if ("$join" in sub)
|
|
2217
|
+
return sub;
|
|
2218
|
+
return flattenWhereRecursive(sub, depth + 1);
|
|
2219
|
+
});
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
if (typeof condition !== "object" || condition === null) {
|
|
2223
|
+
result[field] = condition;
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
const condObj = condition;
|
|
2227
|
+
const ops = {};
|
|
2228
|
+
let hasOp = false;
|
|
2229
|
+
for (const [userOp, internalOp] of Object.entries(OPERATOR_MAP)) {
|
|
2230
|
+
if (userOp in condObj) {
|
|
2231
|
+
let value = condObj[userOp];
|
|
2232
|
+
if (internalOp === "$exists" && typeof value === "string") {
|
|
2233
|
+
value = value === "true";
|
|
2234
|
+
}
|
|
2235
|
+
if (typeof value === "string") {
|
|
2236
|
+
value = value.replace(/\0/g, "");
|
|
2237
|
+
}
|
|
2238
|
+
if (Array.isArray(value)) {
|
|
2239
|
+
value = value.map(
|
|
2240
|
+
(item) => typeof item === "string" ? item.replace(/\0/g, "") : item
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
ops[internalOp] = value;
|
|
2244
|
+
hasOp = true;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
for (const key of Object.keys(condObj)) {
|
|
2248
|
+
if (!VALID_OPERATORS.has(key)) {
|
|
2249
|
+
throw new ValidationError([
|
|
2250
|
+
{
|
|
2251
|
+
field,
|
|
2252
|
+
message: `Unknown operator "${key}". Valid operators: ${[...VALID_OPERATORS].sort().join(", ")}`
|
|
2253
|
+
}
|
|
2254
|
+
]);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (hasOp) {
|
|
2258
|
+
result[field] = ops;
|
|
2259
|
+
} else {
|
|
2260
|
+
result[field] = condition;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return result;
|
|
2264
|
+
}
|
|
2265
|
+
function extractRelationshipJoins(where, fields, allCollections) {
|
|
2266
|
+
if (!where)
|
|
2267
|
+
return { cleanedWhere: void 0, joins: [], allJoins: [] };
|
|
2268
|
+
const dataFields = flattenDataFields(fields);
|
|
2269
|
+
const fieldMap = new Map(dataFields.map((f) => [f.name, f]));
|
|
2270
|
+
const joins = [];
|
|
2271
|
+
const allJoins = [];
|
|
2272
|
+
const cleanedWhere = {};
|
|
2273
|
+
for (const [key, condition] of Object.entries(where)) {
|
|
2274
|
+
if (key === "and" || key === "or") {
|
|
2275
|
+
if (Array.isArray(condition)) {
|
|
2276
|
+
const cleanedArray = [];
|
|
2277
|
+
for (const sub of condition) {
|
|
2278
|
+
if (typeof sub === "object" && sub !== null) {
|
|
2279
|
+
const {
|
|
2280
|
+
cleanedWhere: subCleaned,
|
|
2281
|
+
joins: subTopJoins,
|
|
2282
|
+
allJoins: subAllJoins
|
|
2283
|
+
} = extractRelationshipJoins(
|
|
2284
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- WhereClause sub-object
|
|
2285
|
+
sub,
|
|
2286
|
+
fields,
|
|
2287
|
+
allCollections
|
|
2288
|
+
);
|
|
2289
|
+
if (subCleaned)
|
|
2290
|
+
cleanedArray.push(subCleaned);
|
|
2291
|
+
for (const join2 of subTopJoins) {
|
|
2292
|
+
cleanedArray.push({ ["$join"]: join2 });
|
|
2293
|
+
}
|
|
2294
|
+
allJoins.push(...subAllJoins);
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (cleanedArray.length > 0) {
|
|
2298
|
+
cleanedWhere[key] = cleanedArray;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
let field = fieldMap.get(key);
|
|
2304
|
+
if (!field && key.includes(".")) {
|
|
2305
|
+
const [rootKey, ...subPath] = key.split(".");
|
|
2306
|
+
const rootField = fieldMap.get(rootKey);
|
|
2307
|
+
if (rootField && rootField.type === "relationship" && subPath.length > 0 && condition !== null) {
|
|
2308
|
+
let nested = condition;
|
|
2309
|
+
for (let i = subPath.length - 1; i >= 0; i--) {
|
|
2310
|
+
nested = { [subPath[i]]: nested };
|
|
2311
|
+
}
|
|
2312
|
+
field = rootField;
|
|
2313
|
+
const {
|
|
2314
|
+
cleanedWhere: subCleaned,
|
|
2315
|
+
joins: subJoins,
|
|
2316
|
+
allJoins: subAllJoins
|
|
2317
|
+
} = extractRelationshipJoins(
|
|
2318
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- reconstructed nested where
|
|
2319
|
+
{ [rootKey]: nested },
|
|
2320
|
+
fields,
|
|
2321
|
+
allCollections
|
|
2322
|
+
);
|
|
2323
|
+
if (subCleaned)
|
|
2324
|
+
Object.assign(cleanedWhere, subCleaned);
|
|
2325
|
+
joins.push(...subJoins);
|
|
2326
|
+
allJoins.push(...subAllJoins);
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (!field || field.type !== "relationship" || typeof condition !== "object" || condition === null) {
|
|
2331
|
+
cleanedWhere[key] = condition;
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
const condObj = condition;
|
|
2335
|
+
const condKeys = Object.keys(condObj);
|
|
2336
|
+
const hasOperatorKeys = condKeys.some((k) => VALID_OPERATORS.has(k));
|
|
2337
|
+
const hasNonOperatorKeys = condKeys.some((k) => !VALID_OPERATORS.has(k));
|
|
2338
|
+
if (hasOperatorKeys && hasNonOperatorKeys) {
|
|
2339
|
+
throw new ValidationError([
|
|
2340
|
+
{
|
|
2341
|
+
field: key,
|
|
2342
|
+
message: `Cannot mix operators and sub-field references on relationship field "${key}". Use either operators (e.g. { equals: 'id' }) or sub-field queries (e.g. { name: { equals: 'value' } }), not both.`
|
|
2343
|
+
}
|
|
2344
|
+
]);
|
|
2345
|
+
}
|
|
2346
|
+
if (!hasNonOperatorKeys) {
|
|
2347
|
+
cleanedWhere[key] = condition;
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
const relField = field;
|
|
2351
|
+
let targetSlug;
|
|
2352
|
+
try {
|
|
2353
|
+
const targetConfig = relField.collection();
|
|
2354
|
+
if (targetConfig && typeof targetConfig === "object" && "slug" in targetConfig) {
|
|
2355
|
+
targetSlug = targetConfig.slug;
|
|
2356
|
+
}
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
if (!targetSlug) {
|
|
2360
|
+
throw new ValidationError([
|
|
2361
|
+
{
|
|
2362
|
+
field: key,
|
|
2363
|
+
message: `Cannot resolve target collection for relationship field "${key}".`
|
|
2364
|
+
}
|
|
2365
|
+
]);
|
|
2366
|
+
}
|
|
2367
|
+
const targetCollection = allCollections.find((c) => c.slug === targetSlug);
|
|
2368
|
+
if (!targetCollection) {
|
|
2369
|
+
throw new ValidationError([
|
|
2370
|
+
{
|
|
2371
|
+
field: key,
|
|
2372
|
+
message: `Target collection "${targetSlug}" not found for relationship field "${key}".`
|
|
2373
|
+
}
|
|
2374
|
+
]);
|
|
2375
|
+
}
|
|
2376
|
+
const subWhere = condition;
|
|
2377
|
+
const flattenedConditions = flattenWhereClause(subWhere);
|
|
2378
|
+
const targetTable = targetCollection.dbName ?? targetCollection.slug;
|
|
2379
|
+
const joinSpec = {
|
|
2380
|
+
targetTable,
|
|
2381
|
+
localField: key,
|
|
2382
|
+
targetField: "id",
|
|
2383
|
+
conditions: flattenedConditions,
|
|
2384
|
+
rawWhere: subWhere
|
|
2385
|
+
};
|
|
2386
|
+
joins.push(joinSpec);
|
|
2387
|
+
allJoins.push(joinSpec);
|
|
2388
|
+
}
|
|
2389
|
+
return {
|
|
2390
|
+
cleanedWhere: Object.keys(cleanedWhere).length > 0 ? cleanedWhere : void 0,
|
|
2391
|
+
joins,
|
|
2392
|
+
allJoins
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
var SYSTEM_QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "_status"]);
|
|
2396
|
+
async function validateWhereFields(where, fields, req) {
|
|
2397
|
+
if (!where)
|
|
2398
|
+
return;
|
|
2399
|
+
const dataFields = flattenDataFields(fields);
|
|
2400
|
+
const fieldMap = new Map(dataFields.map((f) => [f.name, f]));
|
|
2401
|
+
for (const fieldName of Object.keys(where)) {
|
|
2402
|
+
if (fieldName === "and" || fieldName === "or") {
|
|
2403
|
+
const subs = where[fieldName];
|
|
2404
|
+
if (Array.isArray(subs)) {
|
|
2405
|
+
for (const sub of subs) {
|
|
2406
|
+
if (typeof sub === "object" && sub !== null) {
|
|
2407
|
+
await validateWhereFields(sub, fields, req);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
const baseName = fieldName.includes(".") ? fieldName.split(".")[0] : fieldName;
|
|
2414
|
+
if (SYSTEM_QUERYABLE_FIELDS.has(baseName))
|
|
2415
|
+
continue;
|
|
2416
|
+
const field = fieldMap.get(baseName);
|
|
2417
|
+
if (!field) {
|
|
2418
|
+
throw new ValidationError([{ field: fieldName, message: `Unknown field: ${baseName}` }]);
|
|
2419
|
+
}
|
|
2420
|
+
if (!field.access?.read)
|
|
2421
|
+
continue;
|
|
2422
|
+
const allowed = await Promise.resolve(field.access.read({ req }));
|
|
2423
|
+
if (!allowed) {
|
|
2424
|
+
throw new AccessDeniedError("read", baseName);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
async function validateSortField(sort, fields, req) {
|
|
2429
|
+
if (!sort)
|
|
2430
|
+
return;
|
|
2431
|
+
const fieldName = sort.startsWith("-") ? sort.slice(1) : sort;
|
|
2432
|
+
const baseName = fieldName.includes(".") ? fieldName.split(".")[0] : fieldName;
|
|
2433
|
+
const dataFields = flattenDataFields(fields);
|
|
2434
|
+
const field = dataFields.find((f) => f.name === baseName);
|
|
2435
|
+
if (!field?.access?.read)
|
|
2436
|
+
return;
|
|
2437
|
+
const allowed = await Promise.resolve(field.access.read({ req }));
|
|
2438
|
+
if (!allowed) {
|
|
2439
|
+
throw new AccessDeniedError("read", baseName);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// libs/server-core/src/lib/api-utils.ts
|
|
2444
|
+
function deepEqual(a, b) {
|
|
2445
|
+
if (a === b)
|
|
2446
|
+
return true;
|
|
2447
|
+
if (a == null || b == null)
|
|
2448
|
+
return false;
|
|
2449
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
2450
|
+
return false;
|
|
2451
|
+
if (Array.isArray(a)) {
|
|
2452
|
+
if (!Array.isArray(b) || a.length !== b.length)
|
|
2453
|
+
return false;
|
|
2454
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
2455
|
+
}
|
|
2456
|
+
if (Array.isArray(b))
|
|
2457
|
+
return false;
|
|
2458
|
+
const aKeys = Object.keys(a);
|
|
2459
|
+
const bKeys = Object.keys(b);
|
|
2460
|
+
if (aKeys.length !== bKeys.length)
|
|
2461
|
+
return false;
|
|
2462
|
+
const aRec = a;
|
|
2463
|
+
const bRec = b;
|
|
2464
|
+
return aKeys.every(
|
|
2465
|
+
(key) => Object.prototype.hasOwnProperty.call(bRec, key) && deepEqual(aRec[key], bRec[key])
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
function stripTransientKeys(data) {
|
|
2469
|
+
const result = {};
|
|
2470
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2471
|
+
if (!key.startsWith("_")) {
|
|
2472
|
+
result[key] = value;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return result;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// libs/server-core/src/lib/collection-access.ts
|
|
2479
|
+
async function checkSingleCollectionAdminAccess(collection, user) {
|
|
2480
|
+
const adminFn = collection.access?.admin;
|
|
2481
|
+
if (!adminFn) {
|
|
2482
|
+
return !!user;
|
|
2483
|
+
}
|
|
2484
|
+
const accessArgs = {
|
|
2485
|
+
req: { user }
|
|
2486
|
+
};
|
|
2487
|
+
return Promise.resolve(adminFn(accessArgs));
|
|
2488
|
+
}
|
|
2489
|
+
async function checkAccessFunction(accessFn, user, defaultIfUndefined) {
|
|
2490
|
+
if (!accessFn) {
|
|
2491
|
+
return defaultIfUndefined;
|
|
2492
|
+
}
|
|
2493
|
+
const accessArgs = {
|
|
2494
|
+
req: { user }
|
|
2495
|
+
};
|
|
2496
|
+
return Promise.resolve(accessFn(accessArgs));
|
|
2497
|
+
}
|
|
2498
|
+
async function getCollectionPermissions(config, user) {
|
|
2499
|
+
const results = await Promise.all(
|
|
2500
|
+
config.collections.map(async (collection) => {
|
|
2501
|
+
const isManaged = collection.managed === true;
|
|
2502
|
+
const [canAccess, canCreate, canRead, canUpdate, canDelete] = await Promise.all([
|
|
2503
|
+
checkSingleCollectionAdminAccess(collection, user),
|
|
2504
|
+
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.create, user, !!user),
|
|
2505
|
+
checkAccessFunction(collection.access?.read, user, true),
|
|
2506
|
+
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.update, user, !!user),
|
|
2507
|
+
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.delete, user, !!user)
|
|
2508
|
+
]);
|
|
2509
|
+
return {
|
|
2510
|
+
slug: collection.slug,
|
|
2511
|
+
canAccess,
|
|
2512
|
+
canCreate,
|
|
2513
|
+
canRead,
|
|
2514
|
+
canUpdate,
|
|
2515
|
+
canDelete,
|
|
2516
|
+
...isManaged ? { managed: true } : {}
|
|
2517
|
+
};
|
|
2518
|
+
})
|
|
2519
|
+
);
|
|
2520
|
+
return results;
|
|
2521
|
+
}
|
|
2522
|
+
var ACCESS_OPS = ["read", "create", "update", "delete"];
|
|
2523
|
+
var ACCESS_DEFAULTS = {
|
|
2524
|
+
read: "public (anyone)",
|
|
2525
|
+
create: "any authenticated user",
|
|
2526
|
+
update: "any authenticated user",
|
|
2527
|
+
delete: "any authenticated user"
|
|
2528
|
+
};
|
|
2529
|
+
function warnInsecureDefaults(collections) {
|
|
2530
|
+
const logger = createLogger("Security");
|
|
2531
|
+
const warnings = [];
|
|
2532
|
+
for (const collection of collections) {
|
|
2533
|
+
if (collection.managed)
|
|
2534
|
+
continue;
|
|
2535
|
+
const missing = ACCESS_OPS.filter((op) => !collection.access?.[op]);
|
|
2536
|
+
if (missing.length > 0) {
|
|
2537
|
+
const details = missing.map((op) => `${op} (${ACCESS_DEFAULTS[op]})`).join(", ");
|
|
2538
|
+
const msg = `Collection "${collection.slug}" has no explicit access control for: ${details}. Define access functions to restrict.`;
|
|
2539
|
+
logger.warn(msg);
|
|
2540
|
+
warnings.push(msg);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return warnings;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2110
2546
|
// libs/server-core/src/lib/momentum-api.ts
|
|
2111
2547
|
var momentumApiInstance = null;
|
|
2112
2548
|
function initializeMomentumAPI(config) {
|
|
@@ -2114,6 +2550,7 @@ function initializeMomentumAPI(config) {
|
|
|
2114
2550
|
createLogger("API").warn("Already initialized, returning existing instance");
|
|
2115
2551
|
return momentumApiInstance;
|
|
2116
2552
|
}
|
|
2553
|
+
warnInsecureDefaults(config.collections);
|
|
2117
2554
|
momentumApiInstance = new MomentumAPIImpl(config);
|
|
2118
2555
|
return momentumApiInstance;
|
|
2119
2556
|
}
|
|
@@ -2171,70 +2608,6 @@ var MomentumAPIImpl = class _MomentumAPIImpl {
|
|
|
2171
2608
|
return { ...this.context };
|
|
2172
2609
|
}
|
|
2173
2610
|
};
|
|
2174
|
-
function deepEqual(a, b) {
|
|
2175
|
-
if (a === b)
|
|
2176
|
-
return true;
|
|
2177
|
-
if (a == null || b == null)
|
|
2178
|
-
return false;
|
|
2179
|
-
if (typeof a !== "object" || typeof b !== "object")
|
|
2180
|
-
return false;
|
|
2181
|
-
if (Array.isArray(a)) {
|
|
2182
|
-
if (!Array.isArray(b) || a.length !== b.length)
|
|
2183
|
-
return false;
|
|
2184
|
-
return a.every((item, i) => deepEqual(item, b[i]));
|
|
2185
|
-
}
|
|
2186
|
-
if (Array.isArray(b))
|
|
2187
|
-
return false;
|
|
2188
|
-
const aKeys = Object.keys(a);
|
|
2189
|
-
const bKeys = Object.keys(b);
|
|
2190
|
-
if (aKeys.length !== bKeys.length)
|
|
2191
|
-
return false;
|
|
2192
|
-
const aRec = a;
|
|
2193
|
-
const bRec = b;
|
|
2194
|
-
return aKeys.every(
|
|
2195
|
-
(key) => Object.prototype.hasOwnProperty.call(bRec, key) && deepEqual(aRec[key], bRec[key])
|
|
2196
|
-
);
|
|
2197
|
-
}
|
|
2198
|
-
function stripTransientKeys(data) {
|
|
2199
|
-
const result = {};
|
|
2200
|
-
for (const [key, value] of Object.entries(data)) {
|
|
2201
|
-
if (!key.startsWith("_")) {
|
|
2202
|
-
result[key] = value;
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
return result;
|
|
2206
|
-
}
|
|
2207
|
-
var COMPARISON_OPS = ["gt", "gte", "lt", "lte"];
|
|
2208
|
-
function flattenWhereClause(where) {
|
|
2209
|
-
if (!where)
|
|
2210
|
-
return {};
|
|
2211
|
-
const result = {};
|
|
2212
|
-
for (const [field, condition] of Object.entries(where)) {
|
|
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;
|
|
2232
|
-
} else {
|
|
2233
|
-
result[field] = condition;
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
return result;
|
|
2237
|
-
}
|
|
2238
2611
|
var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
2239
2612
|
constructor(slug2, collectionConfig, adapter, context, allCollections = []) {
|
|
2240
2613
|
this.slug = slug2;
|
|
@@ -2245,11 +2618,37 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2245
2618
|
}
|
|
2246
2619
|
async find(options = {}) {
|
|
2247
2620
|
await this.checkAccess("read");
|
|
2621
|
+
if (!this.context.overrideAccess) {
|
|
2622
|
+
await validateWhereFields(
|
|
2623
|
+
options.where,
|
|
2624
|
+
this.collectionConfig.fields,
|
|
2625
|
+
this.buildRequestContext()
|
|
2626
|
+
);
|
|
2627
|
+
await validateSortField(
|
|
2628
|
+
options.sort,
|
|
2629
|
+
this.collectionConfig.fields,
|
|
2630
|
+
this.buildRequestContext()
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2248
2633
|
await this.runBeforeReadHooks();
|
|
2249
|
-
const
|
|
2250
|
-
const
|
|
2634
|
+
const rawLimit = options.limit ?? 10;
|
|
2635
|
+
const rawPage = options.page ?? 1;
|
|
2636
|
+
const limit = Math.max(
|
|
2637
|
+
1,
|
|
2638
|
+
Math.min(Number.isFinite(rawLimit) ? Math.floor(rawLimit) : 10, MAX_PAGE_LIMIT)
|
|
2639
|
+
);
|
|
2640
|
+
const page = Math.max(
|
|
2641
|
+
1,
|
|
2642
|
+
Math.min(Number.isFinite(rawPage) ? Math.floor(rawPage) : 1, MAX_PAGE)
|
|
2643
|
+
);
|
|
2251
2644
|
const { depth: _depth, where, withDeleted: _wd, onlyDeleted: _od, ...queryOptions } = options;
|
|
2252
|
-
const
|
|
2645
|
+
const { cleanedWhere, joins, allJoins } = extractRelationshipJoins(
|
|
2646
|
+
where,
|
|
2647
|
+
this.collectionConfig.fields,
|
|
2648
|
+
this.allCollections
|
|
2649
|
+
);
|
|
2650
|
+
await this.enforceWhereLimits(cleanedWhere, allJoins);
|
|
2651
|
+
const whereParams = flattenWhereClause(cleanedWhere);
|
|
2253
2652
|
const softDeleteField = getSoftDeleteField(this.collectionConfig);
|
|
2254
2653
|
if (softDeleteField && !options.withDeleted && !options.onlyDeleted) {
|
|
2255
2654
|
whereParams[softDeleteField] = null;
|
|
@@ -2274,6 +2673,9 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2274
2673
|
limit,
|
|
2275
2674
|
page
|
|
2276
2675
|
};
|
|
2676
|
+
if (joins.length > 0) {
|
|
2677
|
+
query["$joins"] = joins.map(({ rawWhere: _rw, ...rest }) => rest);
|
|
2678
|
+
}
|
|
2277
2679
|
const docs = await this.adapter.find(this.slug, query);
|
|
2278
2680
|
let afterHookDocs = await this.processAfterReadHooks(docs);
|
|
2279
2681
|
const MAX_RELATIONSHIP_DEPTH = 10;
|
|
@@ -2297,14 +2699,15 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2297
2699
|
);
|
|
2298
2700
|
}
|
|
2299
2701
|
const countQuery = { ...queryOptions, ...whereParams };
|
|
2702
|
+
if (joins.length > 0) {
|
|
2703
|
+
countQuery["$joins"] = joins.map(({ rawWhere: _rw, ...rest }) => rest);
|
|
2704
|
+
}
|
|
2300
2705
|
delete countQuery["limit"];
|
|
2301
2706
|
delete countQuery["page"];
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
limit: 0
|
|
2305
|
-
|
|
2306
|
-
});
|
|
2307
|
-
const totalDocs = allDocs.length;
|
|
2707
|
+
const totalDocs = this.adapter.count ? await this.adapter.count(this.slug, countQuery) : (
|
|
2708
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Adapter returns Record<string, unknown>[], safe cast to T[]
|
|
2709
|
+
(await this.adapter.find(this.slug, { ...countQuery, limit: 0 })).length
|
|
2710
|
+
);
|
|
2308
2711
|
const totalPages = Math.ceil(totalDocs / limit) || 1;
|
|
2309
2712
|
return {
|
|
2310
2713
|
docs: afterHookDocs,
|
|
@@ -2541,7 +2944,12 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2541
2944
|
async restore(id) {
|
|
2542
2945
|
const softDeleteField = getSoftDeleteField(this.collectionConfig);
|
|
2543
2946
|
if (!softDeleteField) {
|
|
2544
|
-
throw new
|
|
2947
|
+
throw new ValidationError([
|
|
2948
|
+
{
|
|
2949
|
+
field: "_softDelete",
|
|
2950
|
+
message: `Collection "${this.slug}" does not have soft delete enabled`
|
|
2951
|
+
}
|
|
2952
|
+
]);
|
|
2545
2953
|
}
|
|
2546
2954
|
const restoreAccessFn = this.collectionConfig.access?.restore;
|
|
2547
2955
|
if (restoreAccessFn) {
|
|
@@ -2599,20 +3007,43 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2599
3007
|
if (softDeleteField) {
|
|
2600
3008
|
softDeleteFilter[softDeleteField] = null;
|
|
2601
3009
|
}
|
|
3010
|
+
const defaultWhereFilter = {};
|
|
3011
|
+
if (this.collectionConfig.defaultWhere) {
|
|
3012
|
+
const constraints = this.collectionConfig.defaultWhere(this.buildRequestContext());
|
|
3013
|
+
if (constraints) {
|
|
3014
|
+
Object.assign(defaultWhereFilter, constraints);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
2602
3017
|
let docs;
|
|
2603
3018
|
if (this.adapter.search) {
|
|
2604
3019
|
docs = await this.adapter.search(this.slug, query, searchFields, { limit, page });
|
|
2605
3020
|
if (softDeleteField) {
|
|
2606
3021
|
docs = docs.filter((doc) => !doc[softDeleteField]);
|
|
2607
3022
|
}
|
|
3023
|
+
if (Object.keys(defaultWhereFilter).length > 0) {
|
|
3024
|
+
docs = docs.filter((doc) => {
|
|
3025
|
+
return Object.entries(defaultWhereFilter).every(([key, value]) => {
|
|
3026
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3027
|
+
return JSON.stringify(doc[key]) === JSON.stringify(value);
|
|
3028
|
+
}
|
|
3029
|
+
return doc[key] === value;
|
|
3030
|
+
});
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
2608
3033
|
} else {
|
|
2609
|
-
docs = await this.adapter.find(this.slug, {
|
|
3034
|
+
docs = await this.adapter.find(this.slug, {
|
|
3035
|
+
...softDeleteFilter,
|
|
3036
|
+
...defaultWhereFilter,
|
|
3037
|
+
limit,
|
|
3038
|
+
page
|
|
3039
|
+
});
|
|
2610
3040
|
}
|
|
2611
3041
|
const resolvedDocs = docs;
|
|
2612
|
-
const
|
|
3042
|
+
const afterHookDocs = await this.processAfterReadHooks(resolvedDocs);
|
|
3043
|
+
const totalDocs = afterHookDocs.length;
|
|
2613
3044
|
const totalPages = Math.max(1, Math.ceil(totalDocs / limit));
|
|
2614
3045
|
return {
|
|
2615
|
-
docs:
|
|
3046
|
+
docs: afterHookDocs,
|
|
2616
3047
|
totalDocs,
|
|
2617
3048
|
totalPages,
|
|
2618
3049
|
page,
|
|
@@ -2625,13 +3056,40 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2625
3056
|
}
|
|
2626
3057
|
async count(where, options) {
|
|
2627
3058
|
await this.checkAccess("read");
|
|
2628
|
-
|
|
3059
|
+
if (!this.context.overrideAccess) {
|
|
3060
|
+
await validateWhereFields(where, this.collectionConfig.fields, this.buildRequestContext());
|
|
3061
|
+
}
|
|
3062
|
+
const { cleanedWhere, joins, allJoins } = extractRelationshipJoins(
|
|
3063
|
+
where,
|
|
3064
|
+
this.collectionConfig.fields,
|
|
3065
|
+
this.allCollections
|
|
3066
|
+
);
|
|
3067
|
+
await this.enforceWhereLimits(cleanedWhere, allJoins);
|
|
3068
|
+
const whereParams = flattenWhereClause(cleanedWhere);
|
|
2629
3069
|
const softDeleteField = getSoftDeleteField(this.collectionConfig);
|
|
2630
3070
|
if (softDeleteField && !options?.withDeleted) {
|
|
2631
3071
|
whereParams[softDeleteField] = null;
|
|
2632
3072
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
3073
|
+
if (this.collectionConfig.defaultWhere) {
|
|
3074
|
+
const constraints = this.collectionConfig.defaultWhere(this.buildRequestContext());
|
|
3075
|
+
if (constraints) {
|
|
3076
|
+
Object.assign(whereParams, constraints);
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (hasVersionDrafts(this.collectionConfig) && !this.context.overrideAccess) {
|
|
3080
|
+
const canSeeDrafts = await this.canReadDrafts();
|
|
3081
|
+
if (!canSeeDrafts) {
|
|
3082
|
+
whereParams["_status"] = "published";
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
const query = { ...whereParams };
|
|
3086
|
+
if (joins.length > 0) {
|
|
3087
|
+
query["$joins"] = joins.map(({ rawWhere: _rw, ...rest }) => rest);
|
|
3088
|
+
}
|
|
3089
|
+
if (this.adapter.count) {
|
|
3090
|
+
return this.adapter.count(this.slug, query);
|
|
3091
|
+
}
|
|
3092
|
+
const docs = await this.adapter.find(this.slug, { ...query, limit: 0 });
|
|
2635
3093
|
return docs.length;
|
|
2636
3094
|
}
|
|
2637
3095
|
async batchCreate(items) {
|
|
@@ -2791,6 +3249,46 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
|
|
|
2791
3249
|
return false;
|
|
2792
3250
|
}
|
|
2793
3251
|
}
|
|
3252
|
+
async enforceWhereLimits(cleanedWhere, joins) {
|
|
3253
|
+
if (joins.length > MAX_JOINS) {
|
|
3254
|
+
throw new ValidationError([
|
|
3255
|
+
{
|
|
3256
|
+
field: "where",
|
|
3257
|
+
message: `Number of relationship joins (${joins.length}) exceeds maximum of ${MAX_JOINS}.`
|
|
3258
|
+
}
|
|
3259
|
+
]);
|
|
3260
|
+
}
|
|
3261
|
+
const mainCount = cleanedWhere ? countWhereConditions(cleanedWhere) : 0;
|
|
3262
|
+
const joinCount = joins.reduce((sum, j) => sum + countWhereConditions(j.rawWhere), 0);
|
|
3263
|
+
const totalConditions = mainCount + joinCount;
|
|
3264
|
+
if (totalConditions > MAX_WHERE_CONDITIONS) {
|
|
3265
|
+
throw new ValidationError([
|
|
3266
|
+
{
|
|
3267
|
+
field: "where",
|
|
3268
|
+
message: `Where clause exceeds maximum of ${MAX_WHERE_CONDITIONS} conditions (got ${totalConditions} across main query and ${joins.length} join(s)).`
|
|
3269
|
+
}
|
|
3270
|
+
]);
|
|
3271
|
+
}
|
|
3272
|
+
if (!this.context.overrideAccess) {
|
|
3273
|
+
for (const join2 of joins) {
|
|
3274
|
+
const targetCol = this.allCollections.find(
|
|
3275
|
+
(c) => (c.dbName ?? c.slug) === join2.targetTable
|
|
3276
|
+
);
|
|
3277
|
+
if (targetCol) {
|
|
3278
|
+
const collectionAccessFn = targetCol.access?.read;
|
|
3279
|
+
if (collectionAccessFn) {
|
|
3280
|
+
const allowed = await Promise.resolve(
|
|
3281
|
+
collectionAccessFn({ req: this.buildRequestContext() })
|
|
3282
|
+
);
|
|
3283
|
+
if (!allowed) {
|
|
3284
|
+
throw new AccessDeniedError("read", targetCol.slug);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
await validateWhereFields(join2.rawWhere, targetCol.fields, this.buildRequestContext());
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
2794
3292
|
buildRequestContext() {
|
|
2795
3293
|
return {
|
|
2796
3294
|
user: this.context.user
|
|
@@ -3199,7 +3697,14 @@ function createMomentumHandlers(config) {
|
|
|
3199
3697
|
});
|
|
3200
3698
|
return {
|
|
3201
3699
|
docs: result.docs,
|
|
3202
|
-
totalDocs: result.totalDocs
|
|
3700
|
+
totalDocs: result.totalDocs,
|
|
3701
|
+
totalPages: result.totalPages,
|
|
3702
|
+
page: result.page,
|
|
3703
|
+
limit: result.limit,
|
|
3704
|
+
hasNextPage: result.hasNextPage,
|
|
3705
|
+
hasPrevPage: result.hasPrevPage,
|
|
3706
|
+
nextPage: result.nextPage,
|
|
3707
|
+
prevPage: result.prevPage
|
|
3203
3708
|
};
|
|
3204
3709
|
} catch (error) {
|
|
3205
3710
|
return handleError(error);
|
|
@@ -3287,7 +3792,14 @@ function createMomentumHandlers(config) {
|
|
|
3287
3792
|
const result = await api.collection(request.collectionSlug).search(q, { fields, limit, page });
|
|
3288
3793
|
return {
|
|
3289
3794
|
docs: result.docs,
|
|
3290
|
-
totalDocs: result.totalDocs
|
|
3795
|
+
totalDocs: result.totalDocs,
|
|
3796
|
+
totalPages: result.totalPages,
|
|
3797
|
+
page: result.page,
|
|
3798
|
+
limit: result.limit,
|
|
3799
|
+
hasNextPage: result.hasNextPage,
|
|
3800
|
+
hasPrevPage: result.hasPrevPage,
|
|
3801
|
+
nextPage: result.nextPage,
|
|
3802
|
+
prevPage: result.prevPage
|
|
3291
3803
|
};
|
|
3292
3804
|
} catch (error) {
|
|
3293
3805
|
return handleError(error);
|
|
@@ -3352,51 +3864,6 @@ function handleError(error) {
|
|
|
3352
3864
|
return { error: "Unknown error", status: 500 };
|
|
3353
3865
|
}
|
|
3354
3866
|
|
|
3355
|
-
// libs/server-core/src/lib/collection-access.ts
|
|
3356
|
-
async function checkSingleCollectionAdminAccess(collection, user) {
|
|
3357
|
-
const adminFn = collection.access?.admin;
|
|
3358
|
-
if (!adminFn) {
|
|
3359
|
-
return !!user;
|
|
3360
|
-
}
|
|
3361
|
-
const accessArgs = {
|
|
3362
|
-
req: { user }
|
|
3363
|
-
};
|
|
3364
|
-
return Promise.resolve(adminFn(accessArgs));
|
|
3365
|
-
}
|
|
3366
|
-
async function checkAccessFunction(accessFn, user, defaultIfUndefined) {
|
|
3367
|
-
if (!accessFn) {
|
|
3368
|
-
return defaultIfUndefined;
|
|
3369
|
-
}
|
|
3370
|
-
const accessArgs = {
|
|
3371
|
-
req: { user }
|
|
3372
|
-
};
|
|
3373
|
-
return Promise.resolve(accessFn(accessArgs));
|
|
3374
|
-
}
|
|
3375
|
-
async function getCollectionPermissions(config, user) {
|
|
3376
|
-
const results = await Promise.all(
|
|
3377
|
-
config.collections.map(async (collection) => {
|
|
3378
|
-
const isManaged = collection.managed === true;
|
|
3379
|
-
const [canAccess, canCreate, canRead, canUpdate, canDelete] = await Promise.all([
|
|
3380
|
-
checkSingleCollectionAdminAccess(collection, user),
|
|
3381
|
-
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.create, user, !!user),
|
|
3382
|
-
checkAccessFunction(collection.access?.read, user, true),
|
|
3383
|
-
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.update, user, !!user),
|
|
3384
|
-
isManaged ? Promise.resolve(false) : checkAccessFunction(collection.access?.delete, user, !!user)
|
|
3385
|
-
]);
|
|
3386
|
-
return {
|
|
3387
|
-
slug: collection.slug,
|
|
3388
|
-
canAccess,
|
|
3389
|
-
canCreate,
|
|
3390
|
-
canRead,
|
|
3391
|
-
canUpdate,
|
|
3392
|
-
canDelete,
|
|
3393
|
-
...isManaged ? { managed: true } : {}
|
|
3394
|
-
};
|
|
3395
|
-
})
|
|
3396
|
-
);
|
|
3397
|
-
return results;
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
3867
|
// libs/server-core/src/lib/webhooks.ts
|
|
3401
3868
|
var webhookLogger = createLogger("Webhook");
|
|
3402
3869
|
|
|
@@ -3875,7 +4342,7 @@ function checkDepth(node, currentDepth, maxDepth, context) {
|
|
|
3875
4342
|
}
|
|
3876
4343
|
}
|
|
3877
4344
|
}
|
|
3878
|
-
async function executeGraphQL(schema, requestBody, context) {
|
|
4345
|
+
async function executeGraphQL(schema, requestBody, context, options) {
|
|
3879
4346
|
if (!requestBody.query) {
|
|
3880
4347
|
return {
|
|
3881
4348
|
status: 400,
|
|
@@ -3891,6 +4358,17 @@ async function executeGraphQL(schema, requestBody, context) {
|
|
|
3891
4358
|
body: { errors: [{ message: "Query parsing failed" }] }
|
|
3892
4359
|
};
|
|
3893
4360
|
}
|
|
4361
|
+
if (options?.readOnly) {
|
|
4362
|
+
const hasMutation = document.definitions.some(
|
|
4363
|
+
(def) => def.kind === "OperationDefinition" && def.operation === "mutation"
|
|
4364
|
+
);
|
|
4365
|
+
if (hasMutation) {
|
|
4366
|
+
return {
|
|
4367
|
+
status: 405,
|
|
4368
|
+
body: { errors: [{ message: "Mutations are not allowed via GET requests" }] }
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
3894
4372
|
const depthErrors = validate(schema, document, [depthLimitRule(MAX_QUERY_DEPTH)]);
|
|
3895
4373
|
if (depthErrors.length > 0) {
|
|
3896
4374
|
return {
|
|
@@ -5555,8 +6033,8 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5555
6033
|
return { error: "API key not found" };
|
|
5556
6034
|
}
|
|
5557
6035
|
if (existingKey.createdBy !== String(user.id)) {
|
|
5558
|
-
utils.setResponseStatus(event,
|
|
5559
|
-
return { error: "
|
|
6036
|
+
utils.setResponseStatus(event, 404);
|
|
6037
|
+
return { error: "API key not found" };
|
|
5560
6038
|
}
|
|
5561
6039
|
}
|
|
5562
6040
|
try {
|