-
Notifications
You must be signed in to change notification settings - Fork 2
/
Able.ts
181 lines (172 loc) · 5.9 KB
/
Able.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
export namespace Able {
/**
* A list of resolved or unresolved abilities.
*/
export type AbilitySet = string[];
/**
* A map of ability aliases, where a single alias (the key) maps to a set of
* abilities (value). This map can be resolved recursively.
*
* Example value:
*
* ```js
* { "foo": ["bar"], "bar": ["baz", "bam"], "xxx": ["yyy"] }
* ```
*
* With the above example value, given a list of abilities `["foo"]`, the
* resolved abilities will be `["foo", "bar", "baz", "bam"]`.
*/
export interface GroupDefinition { [key: string]: AbilitySet|string|null|undefined; }
/**
* A map of values extracted or to be applied in a list of abilities. A
* `ValueMap` of `{ foo: "bar", baz: ["1", "2"] }` equals to list of abilities
* `["?foo=bar", "?baz[]=1", "?baz[]=2"]`.
*/
export interface ValueMap { [key: string]: string|string[]; }
/**
* Resolve a set of abilities with a group definition, returning a set of
* abilities based on all abilities and references to groups in `abilities`.
*
* Unlike `.resolve`, this function does not apply or resolve values.
*
* ```ts
* const definition = { foo: ["bar"] };
* const abilities = ["foo", "bam"];
* Able.flatten(definition, abilities);
* // ["foo", "bam", "bar"]
* ```
*
* @param definition
* @param abilities
*/
export function flatten(definition: GroupDefinition, abilities: AbilitySet): AbilitySet {
abilities = abilities.slice();
for (const ability of abilities) {
const members = arr(definition[ability]);
for (const member of members) {
if (!abilities.includes(member)) {
abilities.push(member);
}
}
}
return abilities;
}
/**
* Extract values from a resolved set of abilities and return the values as a
* map with the remaining abilities.
*
* Example:
*
* ```ts
* const abilities = ["foo", "bam", "?foo=1", "?x[]=3"];
* Able.extractValues(abilities);
* // [ { foo: '1', x: [ '3' ] }, [ 'foo', 'bam' ] ]
* ```
*
* @param abilities
*/
export function extractValues(abilities: AbilitySet): [ValueMap, AbilitySet] {
const values: ValueMap = {};
const remainder: string[] = [];
for (const ability of abilities) {
if (ability[0] === "?") {
const [key, value] = ability.substr(1).split("=", 2);
if (key[key.length - 2] === "[" && key[key.length - 1] === "]") {
const arrKey = key.substr(0, key.length - 2);
if (!(values[arrKey] instanceof Array)) {
values[arrKey] = [];
}
if (typeof value !== "undefined") {
(values[arrKey] as string[]).push(value);
}
} else {
values[key] = typeof value === "undefined" ? "" : value;
}
} else {
remainder.push(ability);
}
}
return [values, remainder];
}
/**
* Replacing template abilities with a given set of values applied. Template
* abilities with missing values are removed.
*
* Example:
*
* ```ts
* const abilities = ["article:{articleId}:read", "post:{postId}:read"]
* const values = { articleId: ["1", "2"] }
* Able.applyValues(abilities, values);
* // [ 'article:1:read', 'article:2:read' ]
* ```
*
* @param abilities
* @param values
*/
export function applyValues(abilities: AbilitySet, values: ValueMap): AbilitySet {
const REGEX = /\{([^}]+)\}/g;
return abilities.reduce((outerAbilitiesAcc, ability) => {
const match = ability.match(REGEX);
if (!match) {
return outerAbilitiesAcc.concat([ability]);
}
return outerAbilitiesAcc.concat(match
.map((k) => k.substr(1, k.length - 2))
.reduce((abilitiesAcc, k) =>
abilitiesAcc.reduce((acc, innerAbility) =>
acc.concat(arr(values[k]).map((v) => innerAbility.replace(`{${k}}`, v))), [] as string[]), [ability]));
}, [] as string[]);
}
/**
* Flatten abilities, and extract and apply embedded values.
*
* ```ts
* const definition = { writer: ["article:{articleId}:write"] };
* const abilities = ["writer", "?articleId[]=4"];
* Able.resolve(definition, abilities);
* // [ 'writer', 'article:4:write' ]
* ```
*
* @param definition
* @param abilities
*/
export function resolve(definition: GroupDefinition, abilities: AbilitySet): AbilitySet {
const flattened = Able.flatten(definition, abilities);
const [extractedValues, extractedAbilities] = Able.extractValues(flattened);
return Able.applyValues(extractedAbilities, extractedValues);
}
/**
* Compare a set of resolved abilities with a set of required abilities, and
* return all abilities in `requiredAbilities` missing in `abilities`. Returns
* an empty array if `abilities` contains all abilities from
* `requiredAbilities`.
* @param abilities
* @param requiredAbilities
*/
export function getMissingAbilities(abilities: AbilitySet, requiredAbilities: AbilitySet): AbilitySet {
return requiredAbilities.filter((ability) => !abilities.includes(ability));
}
/**
* Similar to `getMissingAbilities`, but returns `true` if there are no
* missing abilities. In effect, this function returns whether the user has
* access to something that requires a set of abilities. Only returns `true`
* if `appliedAbilities` include all abilities in `requiredAbilities`.
* @param appliedAbilities
* @param requiredAbilities
*/
export function canAccess(appliedAbilities: AbilitySet, requiredAbilities: AbilitySet): boolean {
return this.getMissingAbilities(appliedAbilities, requiredAbilities).length === 0;
}
}
function arr(valueOrValues?: string|string[]|null): string[] {
if (valueOrValues === "") {
return [""];
} else if (typeof valueOrValues === "undefined" || valueOrValues === null) {
return [];
} else if (typeof valueOrValues === "string") {
return [valueOrValues];
} else {
return valueOrValues;
}
}