@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.
- package/LICENSE +21 -0
- package/README.md +747 -0
- package/aggregation.ts +114 -0
- package/core/filtering.ts +70 -0
- package/core/matching.ts +82 -0
- package/core/merging.ts +143 -0
- package/dist/aggregation.d.ts +62 -0
- package/dist/aggregation.d.ts.map +1 -0
- package/dist/aggregation.js +97 -0
- package/dist/aggregation.js.map +1 -0
- package/dist/core/filtering.d.ts +35 -0
- package/dist/core/filtering.d.ts.map +1 -0
- package/dist/core/filtering.js +62 -0
- package/dist/core/filtering.js.map +1 -0
- package/dist/core/matching.d.ts +31 -0
- package/dist/core/matching.d.ts.map +1 -0
- package/dist/core/matching.js +75 -0
- package/dist/core/matching.js.map +1 -0
- package/dist/core/merging.d.ts +29 -0
- package/dist/core/merging.d.ts.map +1 -0
- package/dist/core/merging.js +124 -0
- package/dist/core/merging.js.map +1 -0
- package/dist/indirect-aggregation.d.ts +41 -0
- package/dist/indirect-aggregation.d.ts.map +1 -0
- package/dist/indirect-aggregation.js +185 -0
- package/dist/indirect-aggregation.js.map +1 -0
- package/dist/indirect-resource.d.ts +126 -0
- package/dist/indirect-resource.d.ts.map +1 -0
- package/dist/indirect-resource.js +109 -0
- package/dist/indirect-resource.js.map +1 -0
- package/dist/mod.d.ts +25 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +25 -0
- package/dist/mod.js.map +1 -0
- package/dist/mongo-query.d.ts +38 -0
- package/dist/mongo-query.d.ts.map +1 -0
- package/dist/mongo-query.js +88 -0
- package/dist/mongo-query.js.map +1 -0
- package/dist/permission.d.ts +57 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +60 -0
- package/dist/permission.js.map +1 -0
- package/dist/resource.d.ts +48 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +298 -0
- package/dist/resource.js.map +1 -0
- package/dist/rules.d.ts +106 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +183 -0
- package/dist/rules.js.map +1 -0
- package/dist/sift/core.d.ts +104 -0
- package/dist/sift/core.d.ts.map +1 -0
- package/dist/sift/core.js +248 -0
- package/dist/sift/core.js.map +1 -0
- package/dist/sift/index.d.ts +10 -0
- package/dist/sift/index.d.ts.map +1 -0
- package/dist/sift/index.js +18 -0
- package/dist/sift/index.js.map +1 -0
- package/dist/sift/operations.d.ts +87 -0
- package/dist/sift/operations.d.ts.map +1 -0
- package/dist/sift/operations.js +257 -0
- package/dist/sift/operations.js.map +1 -0
- package/dist/sift/utils.d.ts +12 -0
- package/dist/sift/utils.d.ts.map +1 -0
- package/dist/sift/utils.js +80 -0
- package/dist/sift/utils.js.map +1 -0
- package/dist/system.d.ts +113 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +712 -0
- package/dist/system.js.map +1 -0
- package/dist/target.d.ts +18 -0
- package/dist/target.d.ts.map +1 -0
- package/dist/target.js +41 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +345 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/indirect-aggregation.ts +216 -0
- package/indirect-resource.ts +205 -0
- package/mod.ts +81 -0
- package/mongo-query.ts +94 -0
- package/package.json +58 -0
- package/permission.ts +88 -0
- package/resource.ts +352 -0
- package/rules.ts +241 -0
- package/sift/MIT-LICENSE.txt +20 -0
- package/sift/core.ts +551 -0
- package/sift/index.ts +62 -0
- package/sift/operations.ts +449 -0
- package/sift/utils.ts +96 -0
- package/system.ts +974 -0
- package/target.ts +84 -0
- 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"}
|