@diister/quick-permission 0.9.0-beta.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +747 -0
  3. package/aggregation.ts +114 -0
  4. package/core/filtering.ts +70 -0
  5. package/core/matching.ts +82 -0
  6. package/core/merging.ts +143 -0
  7. package/dist/aggregation.d.ts +62 -0
  8. package/dist/aggregation.d.ts.map +1 -0
  9. package/dist/aggregation.js +97 -0
  10. package/dist/aggregation.js.map +1 -0
  11. package/dist/core/filtering.d.ts +35 -0
  12. package/dist/core/filtering.d.ts.map +1 -0
  13. package/dist/core/filtering.js +62 -0
  14. package/dist/core/filtering.js.map +1 -0
  15. package/dist/core/matching.d.ts +31 -0
  16. package/dist/core/matching.d.ts.map +1 -0
  17. package/dist/core/matching.js +75 -0
  18. package/dist/core/matching.js.map +1 -0
  19. package/dist/core/merging.d.ts +29 -0
  20. package/dist/core/merging.d.ts.map +1 -0
  21. package/dist/core/merging.js +124 -0
  22. package/dist/core/merging.js.map +1 -0
  23. package/dist/indirect-aggregation.d.ts +41 -0
  24. package/dist/indirect-aggregation.d.ts.map +1 -0
  25. package/dist/indirect-aggregation.js +185 -0
  26. package/dist/indirect-aggregation.js.map +1 -0
  27. package/dist/indirect-resource.d.ts +126 -0
  28. package/dist/indirect-resource.d.ts.map +1 -0
  29. package/dist/indirect-resource.js +109 -0
  30. package/dist/indirect-resource.js.map +1 -0
  31. package/dist/mod.d.ts +25 -0
  32. package/dist/mod.d.ts.map +1 -0
  33. package/dist/mod.js +25 -0
  34. package/dist/mod.js.map +1 -0
  35. package/dist/mongo-query.d.ts +38 -0
  36. package/dist/mongo-query.d.ts.map +1 -0
  37. package/dist/mongo-query.js +88 -0
  38. package/dist/mongo-query.js.map +1 -0
  39. package/dist/permission.d.ts +57 -0
  40. package/dist/permission.d.ts.map +1 -0
  41. package/dist/permission.js +60 -0
  42. package/dist/permission.js.map +1 -0
  43. package/dist/resource.d.ts +48 -0
  44. package/dist/resource.d.ts.map +1 -0
  45. package/dist/resource.js +298 -0
  46. package/dist/resource.js.map +1 -0
  47. package/dist/rules.d.ts +106 -0
  48. package/dist/rules.d.ts.map +1 -0
  49. package/dist/rules.js +183 -0
  50. package/dist/rules.js.map +1 -0
  51. package/dist/sift/core.d.ts +104 -0
  52. package/dist/sift/core.d.ts.map +1 -0
  53. package/dist/sift/core.js +248 -0
  54. package/dist/sift/core.js.map +1 -0
  55. package/dist/sift/index.d.ts +10 -0
  56. package/dist/sift/index.d.ts.map +1 -0
  57. package/dist/sift/index.js +18 -0
  58. package/dist/sift/index.js.map +1 -0
  59. package/dist/sift/operations.d.ts +87 -0
  60. package/dist/sift/operations.d.ts.map +1 -0
  61. package/dist/sift/operations.js +257 -0
  62. package/dist/sift/operations.js.map +1 -0
  63. package/dist/sift/utils.d.ts +12 -0
  64. package/dist/sift/utils.d.ts.map +1 -0
  65. package/dist/sift/utils.js +80 -0
  66. package/dist/sift/utils.js.map +1 -0
  67. package/dist/system.d.ts +113 -0
  68. package/dist/system.d.ts.map +1 -0
  69. package/dist/system.js +712 -0
  70. package/dist/system.js.map +1 -0
  71. package/dist/target.d.ts +18 -0
  72. package/dist/target.d.ts.map +1 -0
  73. package/dist/target.js +41 -0
  74. package/dist/target.js.map +1 -0
  75. package/dist/types.d.ts +345 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +10 -0
  78. package/dist/types.js.map +1 -0
  79. package/indirect-aggregation.ts +216 -0
  80. package/indirect-resource.ts +205 -0
  81. package/mod.ts +81 -0
  82. package/mongo-query.ts +94 -0
  83. package/package.json +58 -0
  84. package/permission.ts +88 -0
  85. package/resource.ts +352 -0
  86. package/rules.ts +241 -0
  87. package/sift/MIT-LICENSE.txt +20 -0
  88. package/sift/core.ts +551 -0
  89. package/sift/index.ts +62 -0
  90. package/sift/operations.ts +449 -0
  91. package/sift/utils.ts +96 -0
  92. package/system.ts +974 -0
  93. package/target.ts +84 -0
  94. package/types.ts +408 -0
@@ -0,0 +1,31 @@
1
+ type TargetPath = readonly unknown[];
2
+ /**
3
+ * Match a requested target path against a pattern path.
4
+ *
5
+ * Targets are always arrays of segments. Each segment is matched against the
6
+ * corresponding pattern segment.
7
+ *
8
+ * Segment-level wildcards:
9
+ * - `"*"` matches any value in that segment.
10
+ * - `"prefix:*"` matches any string segment that starts with `prefix:`.
11
+ *
12
+ * For "any target" semantics, leave the permission's `target` undefined at the
13
+ * call site rather than passing a wildcard here.
14
+ *
15
+ * @example
16
+ * matchPath(["article:123"], ["article:*"]) // true
17
+ * matchPath(["article:123", "comment:4"], ["article:*", "*"]) // true
18
+ * matchPath(["a"], ["a", "b"]) // false (different arity)
19
+ */
20
+ export declare function matchPath(requested: TargetPath, pattern: TargetPath): boolean;
21
+ /**
22
+ * Symmetric "overlap" match: returns true if there's any concrete value that
23
+ * could satisfy both `a` and `b` simultaneously (treating both as patterns).
24
+ *
25
+ * Used by broad-match checks where the request target itself may carry
26
+ * wildcards — e.g. "do I have any badge access in this expo?" with the request
27
+ * `["expo:1", "*"]` overlapping a grant on `["expo:1", "badge:*"]`.
28
+ */
29
+ export declare function overlapPath(a: TargetPath, b: TargetPath): boolean;
30
+ export {};
31
+ //# sourceMappingURL=matching.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matching.d.ts","sourceRoot":"","sources":["../../core/matching.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG,SAAS,OAAO,EAAE,CAAC;AAErC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAG7E;AAuBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAGjE"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Match a requested target path against a pattern path.
3
+ *
4
+ * Targets are always arrays of segments. Each segment is matched against the
5
+ * corresponding pattern segment.
6
+ *
7
+ * Segment-level wildcards:
8
+ * - `"*"` matches any value in that segment.
9
+ * - `"prefix:*"` matches any string segment that starts with `prefix:`.
10
+ *
11
+ * For "any target" semantics, leave the permission's `target` undefined at the
12
+ * call site rather than passing a wildcard here.
13
+ *
14
+ * @example
15
+ * matchPath(["article:123"], ["article:*"]) // true
16
+ * matchPath(["article:123", "comment:4"], ["article:*", "*"]) // true
17
+ * matchPath(["a"], ["a", "b"]) // false (different arity)
18
+ */
19
+ export function matchPath(requested, pattern) {
20
+ if (requested.length !== pattern.length)
21
+ return false;
22
+ return requested.every((segment, i) => matchSegment(segment, pattern[i]));
23
+ }
24
+ function matchSegment(requested, pattern) {
25
+ // Full-segment wildcard
26
+ if (pattern === "*")
27
+ return true;
28
+ // Prefix wildcard on string segments: "article:*"
29
+ if (typeof pattern === "string" &&
30
+ typeof requested === "string" &&
31
+ pattern.endsWith("*")) {
32
+ return requested.startsWith(pattern.slice(0, -1));
33
+ }
34
+ // Nested array segments (rare, but supported)
35
+ if (Array.isArray(pattern) && Array.isArray(requested)) {
36
+ return matchPath(requested, pattern);
37
+ }
38
+ return requested === pattern;
39
+ }
40
+ /**
41
+ * Symmetric "overlap" match: returns true if there's any concrete value that
42
+ * could satisfy both `a` and `b` simultaneously (treating both as patterns).
43
+ *
44
+ * Used by broad-match checks where the request target itself may carry
45
+ * wildcards — e.g. "do I have any badge access in this expo?" with the request
46
+ * `["expo:1", "*"]` overlapping a grant on `["expo:1", "badge:*"]`.
47
+ */
48
+ export function overlapPath(a, b) {
49
+ if (a.length !== b.length)
50
+ return false;
51
+ return a.every((segment, i) => overlapSegment(segment, b[i]));
52
+ }
53
+ function overlapSegment(a, b) {
54
+ // Full wildcards on either side → overlap
55
+ if (a === "*" || b === "*")
56
+ return true;
57
+ if (typeof a === "string" && typeof b === "string") {
58
+ const aPrefix = a.endsWith("*") ? a.slice(0, -1) : null;
59
+ const bPrefix = b.endsWith("*") ? b.slice(0, -1) : null;
60
+ if (aPrefix !== null && bPrefix !== null) {
61
+ // Two prefix wildcards: overlap iff one prefix extends the other
62
+ return aPrefix.startsWith(bPrefix) || bPrefix.startsWith(aPrefix);
63
+ }
64
+ if (aPrefix !== null)
65
+ return b.startsWith(aPrefix);
66
+ if (bPrefix !== null)
67
+ return a.startsWith(bPrefix);
68
+ }
69
+ // Nested arrays
70
+ if (Array.isArray(a) && Array.isArray(b)) {
71
+ return overlapPath(a, b);
72
+ }
73
+ return a === b;
74
+ }
75
+ //# sourceMappingURL=matching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matching.js","sourceRoot":"","sources":["../../core/matching.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS,CAAC,SAAqB,EAAE,OAAmB;IAClE,IAAI,SAAS,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACtD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,YAAY,CAAC,SAAkB,EAAE,OAAgB;IACxD,wBAAwB;IACxB,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEjC,kDAAkD;IAClD,IACE,OAAO,OAAO,KAAK,QAAQ;QAC3B,OAAO,SAAS,KAAK,QAAQ;QAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EACrB,CAAC;QACD,OAAO,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,8CAA8C;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACvD,OAAO,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,SAAS,KAAK,OAAO,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,CAAa,EAAE,CAAa;IACtD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,cAAc,CAAC,CAAU,EAAE,CAAU;IAC5C,0CAA0C;IAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAExC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QACnD,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACxD,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAExD,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACzC,iEAAiE;YACjE,OAAO,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,gBAAgB;IAChB,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Merging utilities for combining outputs from multiple permissions
3
+ */
4
+ export interface FilterSpec {
5
+ [key: string]: boolean | FilterSpec;
6
+ }
7
+ /**
8
+ * Merge two filter specs using union strategy (more permissive)
9
+ *
10
+ * @example
11
+ * mergeFilters(
12
+ * { _id: true, name: true },
13
+ * { name: true, email: true }
14
+ * )
15
+ * // → { _id: true, name: true, email: true }
16
+ */
17
+ export declare function mergeFilters(current: FilterSpec | undefined, incoming: FilterSpec | undefined): FilterSpec;
18
+ /**
19
+ * Merge two outputs intelligently based on the key
20
+ *
21
+ * @example
22
+ * mergeOutputs(
23
+ * { filter: { _id: true }, data: { _id: 1 } },
24
+ * { filter: { name: true }, data: { name: "John" } }
25
+ * )
26
+ * // → { filter: { _id: true, name: true }, data: { _id: 1, name: "John" } }
27
+ */
28
+ export declare function mergeOutputs(current: any, incoming: any): any;
29
+ //# sourceMappingURL=merging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merging.d.ts","sourceRoot":"","sources":["../../core/merging.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,MAAM,WAAW,UAAU;IACzB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,UAAU,GAAG,SAAS,EAC/B,QAAQ,EAAE,UAAU,GAAG,SAAS,GAC/B,UAAU,CA0BZ;AAmED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,GAAG,GAAG,CAiB7D"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Merging utilities for combining outputs from multiple permissions
3
+ */
4
+ /**
5
+ * Merge two filter specs using union strategy (more permissive)
6
+ *
7
+ * @example
8
+ * mergeFilters(
9
+ * { _id: true, name: true },
10
+ * { name: true, email: true }
11
+ * )
12
+ * // → { _id: true, name: true, email: true }
13
+ */
14
+ export function mergeFilters(current, incoming) {
15
+ if (!current)
16
+ return incoming || {};
17
+ if (!incoming)
18
+ return current;
19
+ const result = { ...current };
20
+ for (const [key, value] of Object.entries(incoming)) {
21
+ if (typeof value === "boolean" && typeof result[key] === "boolean") {
22
+ // Union: if either says true, it's true
23
+ result[key] = result[key] || value;
24
+ }
25
+ else if (typeof value === "object" && typeof result[key] === "object") {
26
+ // Recursive merge for nested filters
27
+ result[key] = mergeFilters(result[key], value);
28
+ }
29
+ else if (!(key in result)) {
30
+ // New key, add it
31
+ result[key] = value;
32
+ }
33
+ else {
34
+ // Type mismatch or other case: incoming wins
35
+ result[key] = value;
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * Merge two values using the "most permissive" strategy, recursing down to
42
+ * the leaves of nested structures.
43
+ *
44
+ * Strategies (applied at each leaf):
45
+ * - filter / updateFilter / writeFilter: union via mergeFilters
46
+ * - arrays: concat + dedup (set union)
47
+ * - numbers: Math.max (largest grant wins)
48
+ * - booleans: OR (true wins)
49
+ * - Date: latest wins
50
+ * - plain objects: recurse field-by-field with the same strategy
51
+ * - everything else (string, mismatched types, null): incoming wins
52
+ *
53
+ * Recursive object handling fixes the previous `{ ...current, ...incoming }`
54
+ * shortcut which made nested merges depend on insertion order rather than
55
+ * value semantics.
56
+ */
57
+ function mergeValue(key, current, incoming) {
58
+ // Filter-style fields are unions of allowed fields, regardless of nesting depth
59
+ if (key === "filter" || key === "updateFilter" || key === "writeFilter") {
60
+ return mergeFilters(current, incoming);
61
+ }
62
+ // Arrays: union with dedup
63
+ if (Array.isArray(current) && Array.isArray(incoming)) {
64
+ return [...new Set([...current, ...incoming])];
65
+ }
66
+ // Numbers: most permissive = largest
67
+ if (typeof current === "number" && typeof incoming === "number") {
68
+ return Math.max(current, incoming);
69
+ }
70
+ // Booleans: most permissive = OR
71
+ if (typeof current === "boolean" && typeof incoming === "boolean") {
72
+ return current || incoming;
73
+ }
74
+ // Dates: most permissive = latest
75
+ if (current instanceof Date && incoming instanceof Date) {
76
+ return new Date(Math.max(current.getTime(), incoming.getTime()));
77
+ }
78
+ // Plain objects: recurse field-by-field
79
+ if (typeof current === "object" &&
80
+ typeof incoming === "object" &&
81
+ current !== null &&
82
+ incoming !== null &&
83
+ !Array.isArray(current) &&
84
+ !Array.isArray(incoming) &&
85
+ !(current instanceof Date) &&
86
+ !(incoming instanceof Date)) {
87
+ const result = { ...current };
88
+ for (const [k, v] of Object.entries(incoming)) {
89
+ result[k] = k in current ? mergeValue(k, current[k], v) : v;
90
+ }
91
+ return result;
92
+ }
93
+ // Type mismatch or primitive: incoming wins
94
+ return incoming;
95
+ }
96
+ /**
97
+ * Merge two outputs intelligently based on the key
98
+ *
99
+ * @example
100
+ * mergeOutputs(
101
+ * { filter: { _id: true }, data: { _id: 1 } },
102
+ * { filter: { name: true }, data: { name: "John" } }
103
+ * )
104
+ * // → { filter: { _id: true, name: true }, data: { _id: 1, name: "John" } }
105
+ */
106
+ export function mergeOutputs(current, incoming) {
107
+ if (!current)
108
+ return incoming;
109
+ if (!incoming)
110
+ return current;
111
+ const result = { ...current };
112
+ for (const [key, value] of Object.entries(incoming)) {
113
+ if (!(key in result)) {
114
+ // New key, add it
115
+ result[key] = value;
116
+ }
117
+ else {
118
+ // Existing key, merge intelligently
119
+ result[key] = mergeValue(key, result[key], value);
120
+ }
121
+ }
122
+ return result;
123
+ }
124
+ //# sourceMappingURL=merging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merging.js","sourceRoot":"","sources":["../../core/merging.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Orchestrateur : transforme une liste de grants + ressources indirectes
3
+ * déclarées en un pipeline d'aggregation MongoDB qui pushe les
4
+ * contraintes de jointure en DB.
5
+ *
6
+ * Algorithme :
7
+ * 1. Identifier les indirect resources réellement référencées par les
8
+ * `with` keys des grants.
9
+ * 2. Transitivement pull les indirect resources parentes (chaînage).
10
+ * 3. Trier topologiquement (parent avant enfant) pour que les `$lookup`
11
+ * enfants puissent référencer l'alias produit par leur parent.
12
+ * 4. Émettre un `$lookup` par indirect resource (dédup par `id`).
13
+ * 5. Émettre un `$match` final OR-isé cross-grant par indirect resource,
14
+ * AND-é entre indirect resources distinctes.
15
+ */
16
+ import type { Grant } from "./types.ts";
17
+ import type { IndirectResource } from "./indirect-resource.ts";
18
+ export type AggregationStage = Record<string, unknown>;
19
+ export type MongoFilter = Record<string, unknown>;
20
+ /**
21
+ * Construit le pipeline complet à partir du `baseFilter` (les contraintes
22
+ * find-style classiques agrégées par `aggregateConstraints`) et de la
23
+ * liste des grants matchés.
24
+ *
25
+ * Retourne :
26
+ * - `null` quand aucune indirect resource ne contribue effectivement
27
+ * de filtre — soit aucun grant ne les référence, soit la sémantique
28
+ * "any wins" s'applique (au moins un grant ne référence pas
29
+ * l'indirect → le set complet est admissible). Dans ce cas le
30
+ * consommateur retombe sur le mode find classique avec `constraints`.
31
+ * - Un array de stages sinon (mode aggregation pipeline).
32
+ *
33
+ * La sémantique "any wins" est essentielle pour la composition
34
+ * cross-grant : un admin global avec un grant "open" sur la même perm
35
+ * doit court-circuiter le filtre que d'autres grants tenteraient
36
+ * d'imposer via une indirect resource — exactement comme
37
+ * `aggregateConstraints` traite `undefined` comme "any wins" pour les
38
+ * constraints find-style.
39
+ */
40
+ export declare function buildAggregationStages(baseFilter: MongoFilter, grants: readonly Grant[], declaredIndirect: readonly IndirectResource[]): AggregationStage[] | null;
41
+ //# sourceMappingURL=indirect-aggregation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indirect-aggregation.d.ts","sourceRoot":"","sources":["../indirect-aggregation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,WAAW,EACvB,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,gBAAgB,EAAE,SAAS,gBAAgB,EAAE,GAC5C,gBAAgB,EAAE,GAAG,IAAI,CA6E3B"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Orchestrateur : transforme une liste de grants + ressources indirectes
3
+ * déclarées en un pipeline d'aggregation MongoDB qui pushe les
4
+ * contraintes de jointure en DB.
5
+ *
6
+ * Algorithme :
7
+ * 1. Identifier les indirect resources réellement référencées par les
8
+ * `with` keys des grants.
9
+ * 2. Transitivement pull les indirect resources parentes (chaînage).
10
+ * 3. Trier topologiquement (parent avant enfant) pour que les `$lookup`
11
+ * enfants puissent référencer l'alias produit par leur parent.
12
+ * 4. Émettre un `$lookup` par indirect resource (dédup par `id`).
13
+ * 5. Émettre un `$match` final OR-isé cross-grant par indirect resource,
14
+ * AND-é entre indirect resources distinctes.
15
+ */
16
+ import { validateSpec } from "./mongo-query.js";
17
+ /**
18
+ * Construit le pipeline complet à partir du `baseFilter` (les contraintes
19
+ * find-style classiques agrégées par `aggregateConstraints`) et de la
20
+ * liste des grants matchés.
21
+ *
22
+ * Retourne :
23
+ * - `null` quand aucune indirect resource ne contribue effectivement
24
+ * de filtre — soit aucun grant ne les référence, soit la sémantique
25
+ * "any wins" s'applique (au moins un grant ne référence pas
26
+ * l'indirect → le set complet est admissible). Dans ce cas le
27
+ * consommateur retombe sur le mode find classique avec `constraints`.
28
+ * - Un array de stages sinon (mode aggregation pipeline).
29
+ *
30
+ * La sémantique "any wins" est essentielle pour la composition
31
+ * cross-grant : un admin global avec un grant "open" sur la même perm
32
+ * doit court-circuiter le filtre que d'autres grants tenteraient
33
+ * d'imposer via une indirect resource — exactement comme
34
+ * `aggregateConstraints` traite `undefined` comme "any wins" pour les
35
+ * constraints find-style.
36
+ */
37
+ export function buildAggregationStages(baseFilter, grants, declaredIndirect) {
38
+ const indirectById = new Map();
39
+ for (const ir of declaredIndirect)
40
+ indirectById.set(ir.id, ir);
41
+ // Indirects référencées par AU MOINS un grant.
42
+ const referenced = new Set();
43
+ for (const g of grants) {
44
+ if (!g.with)
45
+ continue;
46
+ for (const id of Object.keys(g.with)) {
47
+ if (indirectById.has(id))
48
+ referenced.add(id);
49
+ }
50
+ }
51
+ if (referenced.size === 0)
52
+ return null;
53
+ // Sémantique "any wins" : une indirect ne contribue de filtre que si
54
+ // TOUS les grants matchés portent une condition pour elle. Sinon le
55
+ // grant sans condition ouvre la porte au set complet → on n'a pas le
56
+ // droit de filtrer sur cette indirect.
57
+ const effective = new Set();
58
+ for (const id of referenced) {
59
+ if (grants.every((g) => g.with?.[id] !== undefined)) {
60
+ effective.add(id);
61
+ }
62
+ }
63
+ if (effective.size === 0)
64
+ return null;
65
+ // Pull in chained parents (transitivement) pour les indirects effectives.
66
+ const required = new Set(effective);
67
+ for (const id of effective) {
68
+ walkDeps(indirectById.get(id), required, indirectById);
69
+ }
70
+ // Topological order: parent before child.
71
+ const ordered = topoSort([...required].map((id) => indirectById.get(id)));
72
+ const stages = [{ $match: baseFilter }];
73
+ for (const ir of ordered) {
74
+ stages.push(buildLookupStage(ir));
75
+ }
76
+ // Per-indirect cross-grant OR; cross-indirect AND.
77
+ const matchClauses = [];
78
+ for (const irId of effective) {
79
+ const ir = indirectById.get(irId);
80
+ const perGrantSpecs = [];
81
+ for (const g of grants) {
82
+ const spec = g.with?.[irId];
83
+ if (!spec)
84
+ continue;
85
+ // Security: same whitelist as `match` rule for direct resources —
86
+ // grants may come from untrusted providers, the spec ends up
87
+ // pushed to Mongo as-is. Reject dangerous operators ($where,
88
+ // $expr, $function, etc.) before they reach the DB.
89
+ validateSpec(spec);
90
+ perGrantSpecs.push(spec);
91
+ }
92
+ // `effective` guarantees all grants have a spec, so perGrantSpecs is
93
+ // non-empty here.
94
+ const path = lookupAlias(ir);
95
+ const elemMatches = perGrantSpecs.map((spec) => ({
96
+ [path]: { $elemMatch: { ...staticTypeFilter(ir), ...spec } },
97
+ }));
98
+ matchClauses.push(elemMatches.length === 1 ? elemMatches[0] : { $or: elemMatches });
99
+ }
100
+ if (matchClauses.length > 0) {
101
+ stages.push({
102
+ $match: matchClauses.length === 1 ? matchClauses[0] : { $and: matchClauses },
103
+ });
104
+ }
105
+ return stages;
106
+ }
107
+ // ─── Helpers ──────────────────────────────────────────────────────────
108
+ function walkDeps(ir, acc, byId) {
109
+ if (ir.from.kind === "indirect") {
110
+ const parent = byId.get(ir.from.id);
111
+ if (parent && !acc.has(parent.id)) {
112
+ acc.add(parent.id);
113
+ walkDeps(parent, acc, byId);
114
+ }
115
+ }
116
+ }
117
+ function topoSort(items) {
118
+ const sorted = [];
119
+ const remaining = new Set(items.map((i) => i.id));
120
+ const byId = new Map(items.map((i) => [i.id, i]));
121
+ while (remaining.size > 0) {
122
+ let progressed = false;
123
+ for (const id of [...remaining]) {
124
+ const ir = byId.get(id);
125
+ const parentId = ir.from.kind === "indirect" ? ir.from.id : null;
126
+ const parentInSet = parentId !== null && remaining.has(parentId);
127
+ if (!parentInSet) {
128
+ sorted.push(ir);
129
+ remaining.delete(id);
130
+ progressed = true;
131
+ }
132
+ }
133
+ if (!progressed) {
134
+ throw new Error("Cycle detected in indirect resource chain");
135
+ }
136
+ }
137
+ return sorted;
138
+ }
139
+ function lookupAlias(ir) {
140
+ return `_${ir.id}`;
141
+ }
142
+ function staticTypeFilter(ir) {
143
+ return ir.to?._type ? { _type: ir.to._type } : {};
144
+ }
145
+ function buildLookupStage(ir) {
146
+ const isChained = ir.from.kind === "indirect";
147
+ // `<self>` est un placeholder neutre — le consommateur (mongodbee
148
+ // adapter) substitue le nom de la collection courante côté driver.
149
+ const fromCollection = ir.on.foreignCollection ?? "<self>";
150
+ const alias = lookupAlias(ir);
151
+ if (!isChained) {
152
+ const subPipeline = ir.to?._type
153
+ ? [{ $match: { _type: ir.to._type } }]
154
+ : undefined;
155
+ return {
156
+ $lookup: {
157
+ from: fromCollection,
158
+ localField: ir.on.localField,
159
+ foreignField: ir.on.foreignField,
160
+ ...(subPipeline && { pipeline: subPipeline }),
161
+ as: alias,
162
+ },
163
+ };
164
+ }
165
+ // Chained: lookup uses $let + $expr to dereference the parent alias.
166
+ const parentAlias = lookupAlias(ir.from);
167
+ return {
168
+ $lookup: {
169
+ from: fromCollection,
170
+ let: {
171
+ src: { $arrayElemAt: [`$${parentAlias}.${ir.on.localField}`, 0] },
172
+ },
173
+ pipeline: [
174
+ {
175
+ $match: {
176
+ $expr: { $eq: [`$${ir.on.foreignField}`, "$$src"] },
177
+ ...(ir.to?._type && { _type: ir.to._type }),
178
+ },
179
+ },
180
+ ],
181
+ as: alias,
182
+ },
183
+ };
184
+ }
185
+ //# sourceMappingURL=indirect-aggregation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indirect-aggregation.js","sourceRoot":"","sources":["../indirect-aggregation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `indirectResource` — déclaration d'une ressource atteignable via une
3
+ * jointure (DB-pushdown via aggregation pipeline). Pendant de `resource()`
4
+ * pour les cas où la donnée filtrante vit dans une autre collection (ou
5
+ * dans la même via un foreign field).
6
+ *
7
+ * Cas d'usage typique :
8
+ * - lister les `participant` qui ont une `org_membership` matching org=X
9
+ * - lister les `participant` dont l'`user` lié appartient à entreprise=Y
10
+ * - chaîner sur plusieurs niveaux (`participant → user → entreprise_member`)
11
+ *
12
+ * La méthode `.match()` retourne une `Rule` sentinelle (kind="indirect-match")
13
+ * que l'orchestrateur de `system.ts` détecte pour générer le pipeline
14
+ * d'aggregation correspondant — voir `indirect-aggregation.ts`.
15
+ *
16
+ * À la différence des ressources directes :
17
+ * - Pas de `fetch` ni de `dedupKey` : aucune donnée chargée à l'évaluation
18
+ * - Pas de rule `filter`/`include`/`require*` — uniquement `match` pour
19
+ * contribuer une contrainte au pipeline généré.
20
+ */
21
+ import type { FetchCtx, Rule } from "./types.ts";
22
+ /**
23
+ * Identité de jointure entre une ressource source et la cible jointe.
24
+ *
25
+ * - `localField` : nom du champ sur le doc source.
26
+ * - `foreignField` : nom du champ sur le doc cible.
27
+ * - `foreignCollection` : si absent, jointure dans la même collection
28
+ * (self-lookup) — utile pour les multi-collections mongodbee où le
29
+ * discriminator de type vit dans un champ `_type`. Si présent,
30
+ * jointure vers une collection externe (équivalent `externalLookup`).
31
+ */
32
+ export interface IndirectResourceJoin {
33
+ readonly localField: string;
34
+ readonly foreignField: string;
35
+ readonly foreignCollection?: string;
36
+ }
37
+ /**
38
+ * Ressource indirecte. Identifie une cible joignable et expose des rules
39
+ * pour contribuer aux contraintes du pipeline généré.
40
+ *
41
+ * - `from` accepte une `Resource` directe (l'objet retourné par
42
+ * `resource({...})`, sans champ `kind` — on le marque "direct" par
43
+ * défaut) OU une autre `IndirectResource` (qui a `kind: "indirect"`)
44
+ * pour chaîner sur plusieurs niveaux.
45
+ * - `to._type` discrimine le type cible quand la jointure vise une
46
+ * collection multi-types (mongodbee). **Barrière de sécurité** : un
47
+ * grant ne peut pas adresser un autre `_type` via le `with`.
48
+ * - `cardinality` indique si on attend N résultats (`many`) ou 1
49
+ * (`one`) — utilisé pour optimiser le pipeline généré ($unwind potentiel).
50
+ */
51
+ export interface IndirectResource {
52
+ readonly id: string;
53
+ /** Marker discriminant les ressources directes des indirectes. */
54
+ readonly kind: "indirect";
55
+ /** Normalisé au factory à partir du `from` du spec (Resource → direct). */
56
+ readonly from: {
57
+ readonly id: string;
58
+ readonly kind: "direct" | "indirect";
59
+ };
60
+ readonly on: IndirectResourceJoin;
61
+ readonly to?: {
62
+ readonly _type?: string;
63
+ };
64
+ readonly cardinality: "one" | "many";
65
+ /**
66
+ * Fetcher opt-in pour le mode concrete (per-doc check). Quand défini,
67
+ * la rule `match()` évalue effectivement la condition en mode concrete
68
+ * (au lieu d'être une sentinelle no-op). Reçoit la valeur de la source
69
+ * (déjà fetchée par l'engine) et retourne le tableau de docs joints.
70
+ *
71
+ * Toujours retourner un array, même pour `cardinality: "one"` — la
72
+ * sémantique du match utilise `$elemMatch`-like (au moins un match).
73
+ *
74
+ * Si absent, l'indirect reste sentinelle (legacy) : ne contribue qu'au
75
+ * pipeline en cap-mode.
76
+ */
77
+ readonly fetcher?: (sourceDoc: unknown, ctx: FetchCtx) => readonly unknown[] | Promise<readonly unknown[]>;
78
+ /** Calcule la cache key pour un target (cf. `Resource.cacheKeyForTarget`). */
79
+ cacheKeyForTarget(target: readonly unknown[]): string;
80
+ /**
81
+ * Crée une rule qui déclare l'utilisation de cette resource indirecte
82
+ * dans la permission.
83
+ *
84
+ * - En cap-mode : sentinelle, détectée par `system.ts` qui collecte
85
+ * l'indirect pour générer le pipeline (cf. `buildAggregationStages`).
86
+ * - En concrete-mode + `fetcher` défini : fetche les docs joints et
87
+ * évalue `grant.with[id]` contre eux (au moins un doc match).
88
+ * Sans `fetcher`, sentinelle aussi (no-op).
89
+ */
90
+ match(): Rule;
91
+ }
92
+ interface IndirectResourceSpec {
93
+ readonly id: string;
94
+ /**
95
+ * `Resource` (sans champ `kind`) ou `IndirectResource` (avec
96
+ * `kind: "indirect"`). Le factory normalise en stockant un
97
+ * `{ id, kind }` minimal pour la résolution du graphe.
98
+ */
99
+ readonly from: {
100
+ readonly id: string;
101
+ readonly kind?: "direct" | "indirect";
102
+ };
103
+ readonly on: IndirectResourceJoin;
104
+ readonly to?: {
105
+ readonly _type?: string;
106
+ };
107
+ readonly cardinality: "one" | "many";
108
+ readonly fetch?: (sourceDoc: unknown, ctx: FetchCtx) => readonly unknown[] | Promise<readonly unknown[]>;
109
+ }
110
+ /**
111
+ * Sentinelle dans `Rule.descriptor` pour qu'`system.ts` reconnaisse
112
+ * cette rule comme "indirect" et collecte la resource correspondante.
113
+ *
114
+ * Le champ est typé `unknown` côté `RuleDescriptor` parce que
115
+ * `[extra: string]: unknown` est la signature catch-all des descriptors —
116
+ * on cast à l'extraction.
117
+ */
118
+ export declare const INDIRECT_RULE_KIND = "indirect-match";
119
+ export declare function indirectResource(spec: IndirectResourceSpec): IndirectResource;
120
+ /**
121
+ * Extraction utilitaire pour `system.ts` : récupère la `IndirectResource`
122
+ * portée par une rule sentinelle, ou `null` si la rule n'en est pas une.
123
+ */
124
+ export declare function extractIndirectResource(rule: Rule): IndirectResource | null;
125
+ export {};
126
+ //# sourceMappingURL=indirect-resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indirect-resource.d.ts","sourceRoot":"","sources":["../indirect-resource.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAA;KAAE,CAAC;IAC7E,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CACjB,SAAS,EAAE,OAAO,EAClB,GAAG,EAAE,QAAQ,KACV,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IACtD,8EAA8E;IAC9E,iBAAiB,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAAC;IACtD;;;;;;;;;OASG;IACH,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;KAAE,CAAC;IAC9E,QAAQ,CAAC,EAAE,EAAE,oBAAoB,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,CACf,SAAS,EAAE,OAAO,EAClB,GAAG,EAAE,QAAQ,KACV,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,mBAAmB,CAAC;AAEnD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,gBAAgB,CAuE7E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,gBAAgB,GAAG,IAAI,CAK3E"}