@web-ts-toolkit/access-router 0.44.0 → 0.45.0

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/advanced.js CHANGED
@@ -134,6 +134,12 @@ var import_utils8 = require("@web-ts-toolkit/utils");
134
134
  var import_utils = require("@web-ts-toolkit/utils");
135
135
  var DEFAULT_LIST_HARD_LIMIT = 1e3;
136
136
 
137
+ // src/helpers/sort-policy.ts
138
+ var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
139
+ function isValidFieldPath(field) {
140
+ return typeof field === "string" && fieldPathPattern.test(field);
141
+ }
142
+
137
143
  // src/helpers/index.ts
138
144
  var import_utils3 = require("@web-ts-toolkit/utils");
139
145
 
@@ -716,7 +722,9 @@ var defaultRequestComplexity = {
716
722
  maxIncludeCount: 10,
717
723
  maxSubQueryCount: 10,
718
724
  maxBulkConcurrency: 10,
719
- maxHookConcurrency: 10
725
+ maxHookConcurrency: 10,
726
+ maxCorrelatedQueries: 100,
727
+ maxCorrelatedDepth: 5
720
728
  };
721
729
  var normalizePositiveInteger = (value, fallback) => {
722
730
  return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
@@ -736,6 +744,14 @@ var resolveRequestComplexity = (options) => ({
736
744
  maxHookConcurrency: normalizePositiveInteger(
737
745
  options?.maxHookConcurrency,
738
746
  defaultRequestComplexity.maxHookConcurrency
747
+ ),
748
+ maxCorrelatedQueries: normalizePositiveInteger(
749
+ options?.maxCorrelatedQueries,
750
+ defaultRequestComplexity.maxCorrelatedQueries
751
+ ),
752
+ maxCorrelatedDepth: normalizePositiveInteger(
753
+ options?.maxCorrelatedDepth,
754
+ defaultRequestComplexity.maxCorrelatedDepth
739
755
  )
740
756
  });
741
757
  function validateRequestComplexity(value, options, scope = "request") {
@@ -1244,6 +1260,175 @@ var getGlobalOption = (key, defaultValue) => {
1244
1260
 
1245
1261
  // src/validation/common.ts
1246
1262
  var import_zod2 = require("zod");
1263
+ var import_utils10 = require("@web-ts-toolkit/utils");
1264
+
1265
+ // src/correlated-includes.ts
1266
+ var import_utils9 = require("@web-ts-toolkit/utils");
1267
+ var PARENT_MARKER_KEY = "$parent";
1268
+ var ESCAPE_WRAPPER_KEY = "$escape";
1269
+ var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
1270
+ var MARKER_FORBIDDEN_OPERATOR_KEYS = /* @__PURE__ */ new Set(["$text", "$where", "$comment"]);
1271
+ var LOGICAL_CLAUSE_KEYS = /* @__PURE__ */ new Set(["$and", "$or", "$nor"]);
1272
+ var DANGEROUS_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1273
+ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1274
+ var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
1275
+ var ownKeys = (value) => Object.keys(value);
1276
+ function isParentRefMarker(value) {
1277
+ return (0, import_utils9.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
1278
+ }
1279
+ function isEscapedParentRef(value) {
1280
+ return (0, import_utils9.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
1281
+ }
1282
+ function hasMarkerKey(value) {
1283
+ return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
1284
+ }
1285
+ function isValidParentRefPath(path) {
1286
+ if (typeof path !== "string" || path.length === 0) return false;
1287
+ const segments = path.split(".");
1288
+ return segments.every(
1289
+ (segment) => (FIELD_SEGMENT_PATTERN.test(segment) || ARRAY_INDEX_SEGMENT_PATTERN.test(segment)) && !DANGEROUS_PATH_SEGMENTS.has(segment)
1290
+ );
1291
+ }
1292
+ function isValidCorrelatedOutputPath(path) {
1293
+ return typeof path === "string" && path.length > 0 && isValidFieldPath(path) && path !== "_id" && !path.startsWith("$");
1294
+ }
1295
+ function containsParentMarkerShape(value) {
1296
+ const seen = /* @__PURE__ */ new Set();
1297
+ const visit = (current) => {
1298
+ if (isParentRefMarker(current) || isEscapedParentRef(current)) return true;
1299
+ if ((0, import_utils9.isArray)(current)) {
1300
+ if (seen.has(current)) return false;
1301
+ seen.add(current);
1302
+ return current.some(visit);
1303
+ }
1304
+ if ((0, import_utils9.isPlainObject)(current)) {
1305
+ if (seen.has(current)) return false;
1306
+ seen.add(current);
1307
+ if (hasMarkerKey(current)) return true;
1308
+ return Object.values(current).some(visit);
1309
+ }
1310
+ return false;
1311
+ };
1312
+ return visit(value);
1313
+ }
1314
+ function scanTemplateValueNode(value, path, issues) {
1315
+ if (isEscapedParentRef(value)) {
1316
+ const inner = value.$escape;
1317
+ if (!isParentRefMarker(inner) || typeof inner.$parent !== "string" || (inner.$parent?.length ?? 0) === 0) {
1318
+ issues.push({
1319
+ message: 'Invalid $escape payload: expected { "$escape": { "$parent": "<field>" } }',
1320
+ path
1321
+ });
1322
+ }
1323
+ return;
1324
+ }
1325
+ if (isParentRefMarker(value)) {
1326
+ if (!isValidParentRefPath(value.$parent)) {
1327
+ issues.push({
1328
+ message: `Invalid $parent reference path at ${path.length === 0 ? "#" : path.join(".")}`,
1329
+ path
1330
+ });
1331
+ }
1332
+ return;
1333
+ }
1334
+ if ((0, import_utils9.isPlainObject)(value)) {
1335
+ if (hasMarkerKey(value)) {
1336
+ issues.push({
1337
+ message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
1338
+ path
1339
+ });
1340
+ return;
1341
+ }
1342
+ scanTemplateObjectEntries(value, path, issues);
1343
+ return;
1344
+ }
1345
+ if ((0, import_utils9.isArray)(value)) {
1346
+ value.forEach((item, index) => scanTemplateValueNode(item, [...path, index], issues));
1347
+ }
1348
+ }
1349
+ function scanTemplateObjectEntries(obj, basePath, issues) {
1350
+ for (const [key, child] of Object.entries(obj)) {
1351
+ const childPath = [...basePath, key];
1352
+ if (key === PARENT_MARKER_KEY) {
1353
+ issues.push({ message: "$parent is never a field name in correlated templates", path: childPath });
1354
+ continue;
1355
+ }
1356
+ if (SUBQUERY_PAYLOAD_KEYS.has(key)) {
1357
+ if (containsParentMarkerShape(child)) {
1358
+ issues.push({
1359
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in subquery payloads`,
1360
+ path: childPath
1361
+ });
1362
+ }
1363
+ continue;
1364
+ }
1365
+ if (MARKER_FORBIDDEN_OPERATOR_KEYS.has(key)) {
1366
+ if (containsParentMarkerShape(child)) {
1367
+ issues.push({
1368
+ message: `Unsupported $parent reference inside ${key}: markers are not resolved in ${key} values`,
1369
+ path: childPath
1370
+ });
1371
+ }
1372
+ continue;
1373
+ }
1374
+ if (key === "$elemMatch") {
1375
+ if (isParentRefMarker(child) || isEscapedParentRef(child)) {
1376
+ issues.push({
1377
+ message: "Unsupported $parent reference as a direct $elemMatch value",
1378
+ path: childPath
1379
+ });
1380
+ continue;
1381
+ }
1382
+ scanTemplateValueNode(child, childPath, issues);
1383
+ continue;
1384
+ }
1385
+ if (LOGICAL_CLAUSE_KEYS.has(key)) {
1386
+ if (!(0, import_utils9.isArray)(child)) {
1387
+ scanTemplateValueNode(child, childPath, issues);
1388
+ continue;
1389
+ }
1390
+ child.forEach((clause, index) => {
1391
+ const clausePath = [...childPath, index];
1392
+ if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
1393
+ issues.push({
1394
+ message: `Unsupported $parent reference as a direct ${key} clause: clauses must be filter objects`,
1395
+ path: clausePath
1396
+ });
1397
+ return;
1398
+ }
1399
+ if ((0, import_utils9.isPlainObject)(clause)) {
1400
+ scanTemplateObjectEntries(clause, clausePath, issues);
1401
+ }
1402
+ });
1403
+ continue;
1404
+ }
1405
+ scanTemplateValueNode(child, childPath, issues);
1406
+ }
1407
+ }
1408
+ function validateCorrelatedFilterTemplate(filter) {
1409
+ const issues = [];
1410
+ if (!(0, import_utils9.isPlainObject)(filter)) {
1411
+ issues.push({ message: "Correlated include filter must be an object", path: [] });
1412
+ return issues;
1413
+ }
1414
+ if (isParentRefMarker(filter) || isEscapedParentRef(filter)) {
1415
+ issues.push({ message: "Correlated include filter must not itself be a $parent reference", path: [] });
1416
+ return issues;
1417
+ }
1418
+ scanTemplateObjectEntries(filter, [], issues);
1419
+ return issues;
1420
+ }
1421
+ function findArgsMarkerIssue(value, where) {
1422
+ if (value !== void 0 && containsParentMarkerShape(value)) {
1423
+ return {
1424
+ message: `Unsupported $parent reference in ${where}: references are only supported in filter and id positions`,
1425
+ path: []
1426
+ };
1427
+ }
1428
+ return null;
1429
+ }
1430
+
1431
+ // src/validation/common.ts
1247
1432
  var stringOrStringArray = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string())]);
1248
1433
  var safeIntegerString = (min, message) => import_zod2.z.string().regex(/^\d+$/, message).refine((value) => Number.isSafeInteger(Number(value)), "Expected a safe integer").refine((value) => Number(value) >= min, message);
1249
1434
  var queryBooleanString = import_zod2.z.enum(["true", "false"]);
@@ -1304,7 +1489,8 @@ var subPopulateSchema = import_zod2.z.union([
1304
1489
  select: projectionSchema.optional()
1305
1490
  }).passthrough()
1306
1491
  ]);
1307
- var includeItemSchema = import_zod2.z.object({
1492
+ var legacyIncludeItemSchema = import_zod2.z.object({
1493
+ mode: import_zod2.z.enum(["legacy"]).optional(),
1308
1494
  model: import_zod2.z.string().min(1),
1309
1495
  op: import_zod2.z.enum(["list", "read", "count"]),
1310
1496
  path: import_zod2.z.string().min(1),
@@ -1313,8 +1499,139 @@ var includeItemSchema = import_zod2.z.object({
1313
1499
  foreignField: import_zod2.z.string().min(1),
1314
1500
  args: import_zod2.z.unknown().optional().describe("Forwarded to list/read includes. Count includes ignore pagination fields so counts remain exact."),
1315
1501
  options: import_zod2.z.unknown().optional()
1316
- }).passthrough();
1317
- var includeSchema = import_zod2.z.union([includeItemSchema, import_zod2.z.array(includeItemSchema)]);
1502
+ }).passthrough().superRefine((entry, ctx) => {
1503
+ if (entry.filter !== void 0 && containsParentMarkerShape(entry.filter)) {
1504
+ ctx.addIssue({
1505
+ code: import_zod2.z.ZodIssueCode.custom,
1506
+ message: 'Legacy includes must not contain $parent markers; use a correlated include (mode: "correlated")',
1507
+ path: ["filter"]
1508
+ });
1509
+ }
1510
+ });
1511
+ var parentRefSchema = import_zod2.z.object({ $parent: import_zod2.z.string().min(1) }).strict().superRefine((value, ctx) => {
1512
+ if (!isValidParentRefPath(value.$parent)) {
1513
+ ctx.addIssue({
1514
+ code: import_zod2.z.ZodIssueCode.custom,
1515
+ message: `Invalid $parent reference path: ${value.$parent}`,
1516
+ path: ["$parent"]
1517
+ });
1518
+ }
1519
+ });
1520
+ var correlatedIdSchema = import_zod2.z.union([import_zod2.z.string().min(1), parentRefSchema]);
1521
+ var correlatedFilterSchema = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).superRefine((filter, ctx) => {
1522
+ for (const issue of validateCorrelatedFilterTemplate(filter)) {
1523
+ ctx.addIssue({
1524
+ code: import_zod2.z.ZodIssueCode.custom,
1525
+ message: issue.message,
1526
+ path: issue.path.map(String)
1527
+ });
1528
+ }
1529
+ });
1530
+ var correlatedEmptyOptionsSchema = import_zod2.z.unknown().refine(
1531
+ (value) => value === void 0 || (0, import_utils10.isPlainObject)(value) && Object.keys(value).length === 0,
1532
+ "Correlated include options must be absent or empty"
1533
+ ).optional();
1534
+ var correlatedPathSchema = import_zod2.z.string().min(1).superRefine((path, ctx) => {
1535
+ if (!isValidCorrelatedOutputPath(path)) {
1536
+ ctx.addIssue({
1537
+ code: import_zod2.z.ZodIssueCode.custom,
1538
+ message: `Invalid correlated include output path: ${path}`,
1539
+ path: []
1540
+ });
1541
+ }
1542
+ });
1543
+ var checkArgsMarkers = (select, sort, ctx) => {
1544
+ const selectIssue = findArgsMarkerIssue(select, "args.select");
1545
+ if (selectIssue) {
1546
+ ctx.addIssue({ code: import_zod2.z.ZodIssueCode.custom, message: selectIssue.message, path: ["select"] });
1547
+ }
1548
+ const sortIssue = findArgsMarkerIssue(sort, "args.sort");
1549
+ if (sortIssue) {
1550
+ ctx.addIssue({ code: import_zod2.z.ZodIssueCode.custom, message: sortIssue.message, path: ["sort"] });
1551
+ }
1552
+ };
1553
+ var correlatedReadArgsSchema = import_zod2.z.object({
1554
+ select: projectionSchema.optional(),
1555
+ sort: sortSchema.optional(),
1556
+ include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
1557
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
1558
+ var correlatedListArgsSchema = import_zod2.z.object({
1559
+ select: projectionSchema.optional(),
1560
+ sort: sortSchema.optional(),
1561
+ skip: import_zod2.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
1562
+ limit: import_zod2.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
1563
+ page: import_zod2.z.union([nonNegativeIntegerSchema, nonNegativeIntegerString]).optional(),
1564
+ pageSize: import_zod2.z.union([positiveIntegerSchema, positiveIntegerString]).optional(),
1565
+ include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
1566
+ }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
1567
+ var correlatedCountArgsSchema = import_zod2.z.unknown().refine(
1568
+ (value) => value === void 0 || (0, import_utils10.isPlainObject)(value) && Object.keys(value).length === 0,
1569
+ "Count includes accept no args"
1570
+ ).optional();
1571
+ var correlatedReadByIdSchema = import_zod2.z.object({
1572
+ mode: import_zod2.z.literal("correlated"),
1573
+ model: import_zod2.z.string().min(1),
1574
+ op: import_zod2.z.literal("read"),
1575
+ path: correlatedPathSchema,
1576
+ id: correlatedIdSchema,
1577
+ args: correlatedReadArgsSchema.optional(),
1578
+ options: correlatedEmptyOptionsSchema
1579
+ }).strict();
1580
+ var correlatedReadByFilterSchema = import_zod2.z.object({
1581
+ mode: import_zod2.z.literal("correlated"),
1582
+ model: import_zod2.z.string().min(1),
1583
+ op: import_zod2.z.literal("read"),
1584
+ path: correlatedPathSchema,
1585
+ filter: correlatedFilterSchema,
1586
+ args: correlatedReadArgsSchema.optional(),
1587
+ options: correlatedEmptyOptionsSchema
1588
+ }).strict();
1589
+ var correlatedListSchema = import_zod2.z.object({
1590
+ mode: import_zod2.z.literal("correlated"),
1591
+ model: import_zod2.z.string().min(1),
1592
+ op: import_zod2.z.literal("list"),
1593
+ path: correlatedPathSchema,
1594
+ filter: correlatedFilterSchema,
1595
+ args: correlatedListArgsSchema.optional(),
1596
+ options: correlatedEmptyOptionsSchema
1597
+ }).strict();
1598
+ var correlatedCountSchema = import_zod2.z.object({
1599
+ mode: import_zod2.z.literal("correlated"),
1600
+ model: import_zod2.z.string().min(1),
1601
+ op: import_zod2.z.literal("count"),
1602
+ path: correlatedPathSchema,
1603
+ filter: correlatedFilterSchema,
1604
+ args: correlatedCountArgsSchema,
1605
+ options: correlatedEmptyOptionsSchema
1606
+ }).strict();
1607
+ var includeItemSchema = import_zod2.z.union([
1608
+ legacyIncludeItemSchema,
1609
+ correlatedReadByIdSchema,
1610
+ correlatedReadByFilterSchema,
1611
+ correlatedListSchema,
1612
+ correlatedCountSchema
1613
+ ]);
1614
+ var correlatedNestedIncludeSchema = import_zod2.z.union([includeItemSchema, import_zod2.z.array(includeItemSchema)]);
1615
+ var rejectDuplicateCorrelatedPaths = (items, ctx) => {
1616
+ const seen = /* @__PURE__ */ new Set();
1617
+ items.forEach((item, index) => {
1618
+ if ((0, import_utils10.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
1619
+ if (seen.has(item.path)) {
1620
+ ctx.addIssue({
1621
+ code: import_zod2.z.ZodIssueCode.custom,
1622
+ message: `Duplicate correlated include output path: ${item.path}`,
1623
+ path: [index, "path"]
1624
+ });
1625
+ } else {
1626
+ seen.add(item.path);
1627
+ }
1628
+ }
1629
+ });
1630
+ };
1631
+ var includeSchema = import_zod2.z.union([
1632
+ includeItemSchema,
1633
+ import_zod2.z.array(includeItemSchema).superRefine(rejectDuplicateCorrelatedPaths)
1634
+ ]);
1318
1635
  var fieldsSchema = import_zod2.z.array(import_zod2.z.string().min(1));
1319
1636
  var taskSchema = import_zod2.z.object({
1320
1637
  type: import_zod2.z.string().min(1),
@@ -1851,7 +2168,7 @@ var listBodySchema = import_zod3.z.object({
1851
2168
  }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query"]));
1852
2169
  var countBodySchema = import_zod3.z.object({
1853
2170
  filter: objectOrArraySchema.optional()
1854
- }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options"]));
2171
+ }).passthrough().superRefine((body, ctx) => rejectKeys(body, ctx, ["query", "access", "options", "include"]));
1855
2172
  var readFilterBodySchema = import_zod3.z.object({
1856
2173
  filter: objectOrArraySchema.optional(),
1857
2174
  select: projectionSchema.optional(),