@via-profit/ability 2.1.0 → 3.0.1

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/dist/index.js CHANGED
@@ -70,11 +70,26 @@ class AbilityCondition extends AbilityCode_1.default {
70
70
  static in = new AbilityCondition('in');
71
71
  static not_in = new AbilityCondition('not in');
72
72
  static fromLiteral(literal) {
73
- const code = AbilityCondition[literal]?.code || null;
74
- if (code === null) {
75
- throw new AbilityError_1.AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
73
+ switch (literal) {
74
+ case 'equal':
75
+ return this.equal;
76
+ case 'not_equal':
77
+ return this.not_equal;
78
+ case 'more_than':
79
+ return this.more_than;
80
+ case 'less_than':
81
+ return this.less_than;
82
+ case 'less_or_equal':
83
+ return this.less_or_equal;
84
+ case 'more_or_equal':
85
+ return this.more_or_equal;
86
+ case 'in':
87
+ return this.in;
88
+ case 'not_in':
89
+ return this.not_in;
90
+ default:
91
+ throw new AbilityError_1.AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
76
92
  }
77
- return new AbilityCondition(code);
78
93
  }
79
94
  get literal() {
80
95
  const literal = Object.keys(AbilityCondition).find(member => {
@@ -113,6 +128,74 @@ class AbilityParserError extends Error {
113
128
  exports.AbilityParserError = AbilityParserError;
114
129
 
115
130
 
131
+ /***/ }),
132
+
133
+ /***/ 363:
134
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
135
+
136
+
137
+ var __importDefault = (this && this.__importDefault) || function (mod) {
138
+ return (mod && mod.__esModule) ? mod : { "default": mod };
139
+ };
140
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
141
+ exports.AbilityExplainPolicy = exports.AbilityExplainRuleSet = exports.AbilityExplainRule = exports.AbilityExplain = void 0;
142
+ const AbilityMatch_1 = __importDefault(__webpack_require__(909));
143
+ class AbilityExplain {
144
+ type;
145
+ children;
146
+ name;
147
+ match;
148
+ constructor(config, children = []) {
149
+ this.type = config.type;
150
+ this.children = children;
151
+ this.name = config.name;
152
+ this.match = config.match;
153
+ }
154
+ toString(indent = 0) {
155
+ const pad = ' '.repeat(indent);
156
+ const mark = this.match.code === AbilityMatch_1.default.match.code ? '✓' : '✗';
157
+ let out = `${pad}${mark} ${this.type} «${this.name}» is ${this.match.code}`;
158
+ this.children.forEach(child => {
159
+ out += '\n' + child.toString(indent + 1);
160
+ });
161
+ return out;
162
+ }
163
+ }
164
+ exports.AbilityExplain = AbilityExplain;
165
+ class AbilityExplainRule extends AbilityExplain {
166
+ constructor(rule) {
167
+ super({
168
+ type: 'rule',
169
+ match: rule.state,
170
+ name: rule.name,
171
+ });
172
+ }
173
+ }
174
+ exports.AbilityExplainRule = AbilityExplainRule;
175
+ class AbilityExplainRuleSet extends AbilityExplain {
176
+ constructor(ruleSet) {
177
+ const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
178
+ super({
179
+ type: 'ruleSet',
180
+ match: ruleSet.state,
181
+ name: ruleSet.name,
182
+ }, children);
183
+ }
184
+ }
185
+ exports.AbilityExplainRuleSet = AbilityExplainRuleSet;
186
+ class AbilityExplainPolicy extends AbilityExplain {
187
+ constructor(policy) {
188
+ const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
189
+ super({
190
+ type: 'policy',
191
+ name: policy.name,
192
+ match: policy.matchState,
193
+ }, children);
194
+ }
195
+ }
196
+ exports.AbilityExplainPolicy = AbilityExplainPolicy;
197
+
198
+
116
199
  /***/ }),
117
200
 
118
201
  /***/ 909:
@@ -148,20 +231,6 @@ exports.AbilityParser = void 0;
148
231
  const AbilityError_1 = __webpack_require__(122);
149
232
  const AbilityCondition_1 = __importDefault(__webpack_require__(261));
150
233
  class AbilityParser {
151
- /*
152
- *
153
- * readonly ['order.update']: {
154
- * readonly user: {
155
- * readonly roles: readonly string[];
156
- * readonly department: string;
157
- * };
158
- * readonly order: {
159
- * readonly estimatedArrivalAt: number;
160
- * readonly status: string;
161
- * }
162
- * }
163
- *
164
- * */
165
234
  /**
166
235
  * Sets a value in a nested object structure based on a dot/bracket notation path.
167
236
  * @param object - The target object to modify.
@@ -169,52 +238,222 @@ class AbilityParser {
169
238
  * @param value - The value to set at the specified path.
170
239
  */
171
240
  static setValueDotValue(object, path, value) {
172
- const way = path.replace(/\[/g, '.').replace(/\]/g, '').split('.');
241
+ if (!path || path.trim().length === 0) {
242
+ throw new AbilityError_1.AbilityParserError(`Invalid path provided on a [${path}]`);
243
+ }
244
+ const way = path.replace(/\[/g, '.').replace(/]/g, '').split('.');
173
245
  const last = way.pop();
174
246
  if (!last) {
175
247
  throw new AbilityError_1.AbilityParserError(`Invalid path provided on a [${path}]`);
176
248
  }
177
- way.reduce((o, k, i, kk) => {
178
- if (!o[k]) {
179
- o[k] = isFinite(Number(kk[i + 1])) ? [] : {};
249
+ const lastObj = way.reduce((acc, key, index, array) => {
250
+ const currentValue = acc[key];
251
+ const nextKey = array[index + 1];
252
+ const shouldBeArray = nextKey !== undefined && isFinite(Number(nextKey));
253
+ if (currentValue === undefined || currentValue === null) {
254
+ // Create missing property
255
+ const newValue = shouldBeArray ? [] : {};
256
+ acc[key] = newValue;
257
+ return newValue;
180
258
  }
181
- return o[k];
182
- }, object)[last] = value;
259
+ if (typeof currentValue !== 'object') {
260
+ throw new AbilityError_1.AbilityParserError(`Cannot set property '${key}' on non-object value at path: ${path}`);
261
+ }
262
+ return currentValue;
263
+ }, object);
264
+ const existingValue = lastObj[last];
265
+ if (existingValue !== undefined &&
266
+ typeof existingValue === 'object' &&
267
+ existingValue !== null &&
268
+ !Array.isArray(existingValue)) {
269
+ throw new AbilityError_1.AbilityParserError(`Cannot set primitive value on existing object at path: ${path}`);
270
+ }
271
+ lastObj[last] = value;
183
272
  }
184
273
  /**
185
274
  * Generates TypeScript type definitions based on the provided policies.
186
275
  * @param policies - An array of AbilityPolicy instances.
187
- * @param outPath - The output path for the generated type definitions.
188
- * @returns A record containing the generated type definitions.
276
+ * @returns A generated type definitions.
189
277
  */
190
- static generateTypeDefs(policies, outPath) {
191
- const record = {};
278
+ static generateTypeDefs(policies) {
279
+ // Structure to store types: { [action]: { [subjectPath]: type } }
280
+ const typeStructure = {};
281
+ // Iterate through all policies
192
282
  policies.forEach(policy => {
283
+ const action = policy.action;
284
+ // Initialize object for action if it doesn't exist
285
+ if (!typeStructure[action]) {
286
+ typeStructure[action] = {};
287
+ }
288
+ // Iterate through all ruleSets in the policy
193
289
  policy.ruleSet.forEach(ruleSet => {
290
+ // Iterate through all rules in the ruleSet
194
291
  ruleSet.rules.forEach(rule => {
195
- let value = 'any';
196
- switch (true) {
197
- case rule.condition.isEqual(AbilityCondition_1.default.not_equal):
198
- case rule.condition.isEqual(AbilityCondition_1.default.equal):
199
- value = typeof rule.resource;
200
- break;
201
- case rule.condition.isEqual(AbilityCondition_1.default.in):
202
- case rule.condition.isEqual(AbilityCondition_1.default.not_in):
203
- value = `${typeof rule.resource}[]`;
204
- break;
205
- case rule.condition.isEqual(AbilityCondition_1.default.more_or_equal):
206
- case rule.condition.isEqual(AbilityCondition_1.default.more_than):
207
- case rule.condition.isEqual(AbilityCondition_1.default.less_or_equal):
208
- case rule.condition.isEqual(AbilityCondition_1.default.less_than):
209
- value = 'number';
210
- break;
292
+ const subjectPath = rule.subject;
293
+ const existingType = typeStructure[action][subjectPath];
294
+ const ruleType = this.determineTypeFromRule(rule);
295
+ // If a type already exists for this path, create a union
296
+ if (existingType && existingType !== ruleType) {
297
+ typeStructure[action][subjectPath] = `${existingType} | ${ruleType}`;
298
+ }
299
+ else {
300
+ typeStructure[action][subjectPath] = ruleType;
211
301
  }
212
- AbilityParser.setValueDotValue(record, rule.subject, value);
213
302
  });
214
303
  });
215
304
  });
216
- console.log(JSON.stringify(record));
217
- return record;
305
+ // Transform flat structure into nested structure for easier use
306
+ const nestedStructure = this.buildNestedStructure(typeStructure);
307
+ return this.formatTypeDefinitions(nestedStructure);
308
+ }
309
+ /**
310
+ * Determines TypeScript type based on the rule
311
+ * @param rule - The rule to analyze
312
+ * @returns TypeScript type as string
313
+ */
314
+ static determineTypeFromRule(rule) {
315
+ // Numeric comparisons - always number
316
+ if (rule.condition.isEqual(AbilityCondition_1.default.more_than) ||
317
+ rule.condition.isEqual(AbilityCondition_1.default.less_than) ||
318
+ rule.condition.isEqual(AbilityCondition_1.default.more_or_equal) ||
319
+ rule.condition.isEqual(AbilityCondition_1.default.less_or_equal)) {
320
+ return 'number';
321
+ }
322
+ // Array operations
323
+ if (rule.condition.isEqual(AbilityCondition_1.default.in) ||
324
+ rule.condition.isEqual(AbilityCondition_1.default.not_in)) {
325
+ return this.getArrayType(rule.resource);
326
+ }
327
+ // Equality/Inequality operations
328
+ if (rule.condition.isEqual(AbilityCondition_1.default.equal) ||
329
+ rule.condition.isEqual(AbilityCondition_1.default.not_equal)) {
330
+ return this.getPrimitiveType(rule.resource);
331
+ }
332
+ return 'any';
333
+ }
334
+ /**
335
+ * Gets TypeScript type for array values
336
+ * @param resource - The resource value to analyze
337
+ * @returns TypeScript array type as string
338
+ */
339
+ static getArrayType(resource) {
340
+ if (Array.isArray(resource)) {
341
+ if (resource.length === 0)
342
+ return 'any[]';
343
+ // Determine types of array elements
344
+ const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
345
+ const elementType = elementTypes.size === 1
346
+ ? Array.from(elementTypes)[0]
347
+ : `(${Array.from(elementTypes).join(' | ')})`;
348
+ return `${elementType}[]`;
349
+ }
350
+ // If resource is not an array but condition is in/not_in,
351
+ // it expects an array of such elements
352
+ return `${this.getPrimitiveType(resource)}[]`;
353
+ }
354
+ /**
355
+ * Gets primitive TypeScript type for a value
356
+ * @param value - The value to analyze
357
+ * @returns TypeScript primitive type as string
358
+ */
359
+ static getPrimitiveType(value) {
360
+ if (value === null)
361
+ return 'null';
362
+ if (value === undefined)
363
+ return 'undefined';
364
+ switch (typeof value) {
365
+ case 'string':
366
+ return 'string';
367
+ case 'number':
368
+ return 'number';
369
+ case 'boolean':
370
+ return 'boolean';
371
+ case 'object':
372
+ if (Array.isArray(value)) {
373
+ return 'array'; // special marker, handled separately
374
+ }
375
+ return 'object';
376
+ default:
377
+ return 'any';
378
+ }
379
+ }
380
+ /**
381
+ * Builds nested structure from flat paths
382
+ * Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
383
+ * @param flatStructure - Flat structure with dot notation paths
384
+ * @returns Nested object structure
385
+ */
386
+ static buildNestedStructure(flatStructure) {
387
+ const result = {};
388
+ Object.entries(flatStructure).forEach(([action, paths]) => {
389
+ result[action] = {};
390
+ Object.entries(paths).forEach(([path, type]) => {
391
+ const parts = path.split('.');
392
+ let current = result[action];
393
+ // Iterate through all parts except the last one
394
+ for (let i = 0; i < parts.length - 1; i++) {
395
+ const part = parts[i];
396
+ const currentValue = current[part];
397
+ if (!currentValue || typeof currentValue !== 'object') {
398
+ const newObj = {};
399
+ current[part] = newObj;
400
+ current = newObj;
401
+ }
402
+ else {
403
+ current = currentValue;
404
+ }
405
+ }
406
+ // Set type for the last part
407
+ const lastPart = parts[parts.length - 1];
408
+ current[lastPart] = type;
409
+ });
410
+ });
411
+ return result;
412
+ }
413
+ /**
414
+ * Formats type structure into a string
415
+ * @param structure - Nested type structure
416
+ * @returns Formatted TypeScript type definition string
417
+ */
418
+ static formatTypeDefinitions(structure) {
419
+ let output = '// Automatically generated by via-profit/ability\n';
420
+ output += '// Do not edit manually\n\n';
421
+ output += 'export type Resources = {\n';
422
+ // Sort actions for stable output
423
+ const sortedActions = Object.keys(structure).sort();
424
+ sortedActions.forEach(action => {
425
+ output += ` ['${action}']: {\n`;
426
+ output += this.formatNestedObject(structure[action], 4);
427
+ output += ' };\n';
428
+ });
429
+ output += '}\n';
430
+ return output;
431
+ }
432
+ /**
433
+ * Recursively formats nested object
434
+ * @param obj - Object to format
435
+ * @param indent - Current indentation level
436
+ * @returns Formatted string
437
+ */
438
+ static formatNestedObject(obj, indent) {
439
+ const spaces = ' '.repeat(indent);
440
+ let output = '';
441
+ // Sort keys for stable output
442
+ const sortedKeys = Object.keys(obj).sort();
443
+ sortedKeys.forEach(key => {
444
+ const value = obj[key];
445
+ if (typeof value === 'object' && value !== null) {
446
+ // Nested object
447
+ output += `${spaces}readonly ${key}: {\n`;
448
+ output += this.formatNestedObject(value, indent + 2);
449
+ output += `${spaces}};\n`;
450
+ }
451
+ else {
452
+ // Primitive type
453
+ output += `${spaces}readonly ${key}: ${value};\n`;
454
+ }
455
+ });
456
+ return output;
218
457
  }
219
458
  }
220
459
  exports.AbilityParser = AbilityParser;
@@ -236,6 +475,8 @@ const AbilityRuleSet_1 = __importDefault(__webpack_require__(402));
236
475
  const AbilityMatch_1 = __importDefault(__webpack_require__(909));
237
476
  const AbilityCompare_1 = __importDefault(__webpack_require__(923));
238
477
  const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
478
+ const AbilityExplain_1 = __webpack_require__(363);
479
+ const AbilityError_1 = __webpack_require__(122);
239
480
  class AbilityPolicy {
240
481
  matchState = AbilityMatch_1.default.pending;
241
482
  /**
@@ -262,15 +503,17 @@ class AbilityPolicy {
262
503
  */
263
504
  id;
264
505
  /**
265
- * Soon
506
+ * Running the `enforce` or `resolve` method
507
+ * will select only those from all passed policies that fall under the specified action.
266
508
  */
267
509
  action;
268
510
  constructor(params) {
269
- const { name, id, action, effect } = params;
511
+ const { name, id, action, effect, compareMethod = AbilityCompare_1.default.and } = params;
270
512
  this.name = name;
271
513
  this.id = id;
272
514
  this.action = action;
273
515
  this.effect = effect;
516
+ this.compareMethod = compareMethod;
274
517
  }
275
518
  /**
276
519
  * Add rule set to the policy
@@ -304,6 +547,20 @@ class AbilityPolicy {
304
547
  }
305
548
  return this.matchState;
306
549
  }
550
+ explain() {
551
+ if (this.matchState === AbilityMatch_1.default.pending) {
552
+ throw new AbilityError_1.AbilityError('First, run the check method, then explain');
553
+ }
554
+ return new AbilityExplain_1.AbilityExplainPolicy(this);
555
+ }
556
+ /**
557
+ * Parses an array of policy configurations into an array of AbilityPolicy instances.
558
+ * @param configs - Array of policy configurations
559
+ * @returns Array of AbilityPolicy instances
560
+ */
561
+ static parseAll(configs) {
562
+ return configs.map(config => this.parse(config));
563
+ }
307
564
  /**
308
565
  * Parse the config JSON format to Policy class instance
309
566
  */
@@ -371,6 +628,7 @@ exports.AbilityResolver = void 0;
371
628
  const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
372
629
  const AbilityMatch_1 = __importDefault(__webpack_require__(909));
373
630
  const AbilityError_1 = __webpack_require__(122);
631
+ const AbilityExplain_1 = __webpack_require__(363);
374
632
  class AbilityResolver {
375
633
  policies;
376
634
  constructor(policyOrListOfPolicies) {
@@ -392,6 +650,11 @@ class AbilityResolver {
392
650
  this.policies = filteredPolicies;
393
651
  return this;
394
652
  }
653
+ resolveWithExplain(action, resource) {
654
+ return this.resolve(action, resource).policies.map(policy => {
655
+ return new AbilityExplain_1.AbilityExplainPolicy(policy);
656
+ });
657
+ }
395
658
  enforce(action, resource) {
396
659
  const resolver = this.resolve(action, resource);
397
660
  if (resolver.isDeny()) {
@@ -489,8 +752,8 @@ class AbilityRule {
489
752
  */
490
753
  constructor(params) {
491
754
  const { id, name, subject, resource, condition } = params;
492
- this.id = id;
493
- this.name = name;
755
+ this.name = name || `${JSON.stringify(subject)} ${condition.code} ${JSON.stringify(resource)}`;
756
+ this.id = id || this.name;
494
757
  this.subject = subject;
495
758
  this.resource = resource;
496
759
  this.condition = condition;
@@ -628,6 +891,62 @@ class AbilityRule {
628
891
  condition: this.condition.code,
629
892
  };
630
893
  }
894
+ static equal(subject, resource) {
895
+ return new AbilityRule({
896
+ condition: AbilityCondition_1.default.equal,
897
+ subject,
898
+ resource,
899
+ });
900
+ }
901
+ static notIn(subject, resource) {
902
+ return new AbilityRule({
903
+ condition: AbilityCondition_1.default.not_in,
904
+ subject,
905
+ resource,
906
+ });
907
+ }
908
+ static in(subject, resource) {
909
+ return new AbilityRule({
910
+ condition: AbilityCondition_1.default.in,
911
+ subject,
912
+ resource,
913
+ });
914
+ }
915
+ static notEqual(subject, resource) {
916
+ return new AbilityRule({
917
+ condition: AbilityCondition_1.default.not_equal,
918
+ subject,
919
+ resource,
920
+ });
921
+ }
922
+ static lessThan(subject, resource) {
923
+ return new AbilityRule({
924
+ condition: AbilityCondition_1.default.less_than,
925
+ subject,
926
+ resource,
927
+ });
928
+ }
929
+ static lessOrEqual(subject, resource) {
930
+ return new AbilityRule({
931
+ condition: AbilityCondition_1.default.less_or_equal,
932
+ subject,
933
+ resource,
934
+ });
935
+ }
936
+ static moreThan(subject, resource) {
937
+ return new AbilityRule({
938
+ condition: AbilityCondition_1.default.more_than,
939
+ subject,
940
+ resource,
941
+ });
942
+ }
943
+ static moreOrEqual(subject, resource) {
944
+ return new AbilityRule({
945
+ condition: AbilityCondition_1.default.more_or_equal,
946
+ subject,
947
+ resource,
948
+ });
949
+ }
631
950
  }
632
951
  exports.AbilityRule = AbilityRule;
633
952
  exports["default"] = AbilityRule;
@@ -670,8 +989,8 @@ class AbilityRuleSet {
670
989
  id;
671
990
  constructor(params) {
672
991
  const { name, id, compareMethod } = params;
673
- this.name = name;
674
- this.id = id;
992
+ this.name = name || 'No name';
993
+ this.id = id || this.name;
675
994
  this.compareMethod = compareMethod;
676
995
  }
677
996
  addRule(rule) {
@@ -727,6 +1046,16 @@ class AbilityRuleSet {
727
1046
  rules: this.rules.map(rule => rule.export()),
728
1047
  };
729
1048
  }
1049
+ static and(rules) {
1050
+ return new AbilityRuleSet({
1051
+ compareMethod: AbilityCompare_1.default.and,
1052
+ }).addRules(rules);
1053
+ }
1054
+ static or(rules) {
1055
+ return new AbilityRuleSet({
1056
+ compareMethod: AbilityCompare_1.default.or,
1057
+ }).addRules(rules);
1058
+ }
730
1059
  }
731
1060
  exports.AbilityRuleSet = AbilityRuleSet;
732
1061
  exports["default"] = AbilityRuleSet;
@@ -764,6 +1093,7 @@ __exportStar(__webpack_require__(277), exports);
764
1093
  __exportStar(__webpack_require__(668), exports);
765
1094
  __exportStar(__webpack_require__(476), exports);
766
1095
  __exportStar(__webpack_require__(402), exports);
1096
+ __exportStar(__webpack_require__(363), exports);
767
1097
 
768
1098
 
769
1099
  /***/ })
package/package.json CHANGED
@@ -1,21 +1,30 @@
1
1
  {
2
2
  "name": "@via-profit/ability",
3
3
  "support": "https://via-profit.ru",
4
- "version": "2.1.0",
4
+ "version": "3.0.1",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",
8
8
  "access",
9
9
  "via-profit"
10
- ],
10
+ ],
11
11
  "main": "./dist/index.js",
12
12
  "engines": {
13
13
  "node": ">= 17.0.0",
14
14
  "npm": ">= 8.19.3"
15
15
  },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE",
20
+ "CHANGELOG.md",
21
+ "CONTRIBUTING.md",
22
+ "SECURITY.md"
23
+ ],
16
24
  "scripts": {
17
25
  "lint": "tsc --noEmit && eslint --fix .",
18
26
  "pretty": "prettier --write ./src",
27
+ "build": "npm run build:dist",
19
28
  "build:dev": "cross-env NODE_ENV=development webpack --config ./webpack/webpack-config.ts --color --progress",
20
29
  "build:dist": "cross-env NODE_ENV=production webpack --config ./webpack/webpack-config.ts --color --progress",
21
30
  "build:playground": "cross-env NODE_ENV=development webpack --config ./webpack/webpack-config-playground.ts --color --progress",
Binary file