From 5806cd3743e1a64f867ab0a367e160829b005c0c Mon Sep 17 00:00:00 2001 From: Sergii Stotskyi Date: Fri, 23 Feb 2024 22:19:48 +0200 Subject: [PATCH] feat: refactors extra subpackage in casl/ability and adds AccessibleFields helper class --- packages/casl-ability/extra.d.ts | 2 +- packages/casl-ability/extra/extra.d.ts | 1 - packages/casl-ability/extra/package.json | 4 +- packages/casl-ability/index.metadata.json | 7 - packages/casl-ability/package.json | 14 +- packages/casl-ability/src/extra.ts | 182 ------------------ packages/casl-ability/src/extra/index.ts | 5 + packages/casl-ability/src/extra/packRules.ts | 61 ++++++ .../src/extra/permittedFieldsOf.ts | 69 +++++++ .../casl-ability/src/extra/rulesToFields.ts | 29 +++ .../casl-ability/src/extra/rulesToQuery.ts | 69 +++++++ packages/casl-ability/tsconfig.json | 2 +- 12 files changed, 243 insertions(+), 202 deletions(-) delete mode 100644 packages/casl-ability/extra/extra.d.ts delete mode 100644 packages/casl-ability/index.metadata.json delete mode 100644 packages/casl-ability/src/extra.ts create mode 100644 packages/casl-ability/src/extra/index.ts create mode 100644 packages/casl-ability/src/extra/packRules.ts create mode 100644 packages/casl-ability/src/extra/permittedFieldsOf.ts create mode 100644 packages/casl-ability/src/extra/rulesToFields.ts create mode 100644 packages/casl-ability/src/extra/rulesToQuery.ts diff --git a/packages/casl-ability/extra.d.ts b/packages/casl-ability/extra.d.ts index 95d6f083a..14808df98 100644 --- a/packages/casl-ability/extra.d.ts +++ b/packages/casl-ability/extra.d.ts @@ -1 +1 @@ -export * from './extra/extra'; +export * from './dist/types/extra'; diff --git a/packages/casl-ability/extra/extra.d.ts b/packages/casl-ability/extra/extra.d.ts deleted file mode 100644 index c1ab64aec..000000000 --- a/packages/casl-ability/extra/extra.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../dist/types/extra'; diff --git a/packages/casl-ability/extra/package.json b/packages/casl-ability/extra/package.json index e90eeef00..c5cfec5ca 100644 --- a/packages/casl-ability/extra/package.json +++ b/packages/casl-ability/extra/package.json @@ -1,7 +1,7 @@ { "name": "@casl/ability/extra", - "typings": "./extra.d.ts", + "typings": "../dist/types/extra/index.d.ts", "main": "../dist/umd/extra.js", "module": "../dist/es5m/extra.js", - "es2015": "../dist/es6/extra.js" + "es2015": "../dist/es6c/extra.js" } diff --git a/packages/casl-ability/index.metadata.json b/packages/casl-ability/index.metadata.json deleted file mode 100644 index b47c817b0..000000000 --- a/packages/casl-ability/index.metadata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "__symbolic": "module", - "version": 4, - "metadata": {}, - "origins": {}, - "importAs": "@casl/ability" -} \ No newline at end of file diff --git a/packages/casl-ability/package.json b/packages/casl-ability/package.json index cb1a27312..a6a97ccc2 100644 --- a/packages/casl-ability/package.json +++ b/packages/casl-ability/package.json @@ -15,9 +15,9 @@ "require": "./dist/es6c/index.js" }, "./extra": { - "types": "./dist/types/extra.d.ts", - "import": "./dist/es6m/extra.mjs", - "require": "./dist/es6c/extra.js" + "types": "./dist/types/extra/index.d.ts", + "import": "./dist/es6m/extra/index.mjs", + "require": "./dist/es6c/extra/index.js" } }, "sideEffects": false, @@ -32,16 +32,14 @@ }, "scripts": { "build.core": "dx rollup -n casl -g @ucast/mongo2js:ucast.mongo2js", - "build.extra": "dx rollup -i src/extra.ts -n casl.extra -g @ucast/mongo2js:ucast.mongo2js", - "build.types": "dx tsc && cp index.metadata.json dist/types", + "build.extra": "dx rollup -i src/extra/index.ts -n casl.extra -g @ucast/mongo2js:ucast.mongo2js", + "build.types": "dx tsc", "prebuild": "rm -rf dist/*", "build": "npm run build.types && npm run build.core && npm run build.extra", "lint": "dx eslint src/ spec/", "test": "dx jest", "prerelease": "npm run lint && npm test && NODE_ENV=production npm run build", - "release": "dx semantic-release", - "preb": "echo 'pre'", - "b": "echo 123" + "release": "dx semantic-release" }, "keywords": [ "permissions", diff --git a/packages/casl-ability/src/extra.ts b/packages/casl-ability/src/extra.ts deleted file mode 100644 index 7f909db0f..000000000 --- a/packages/casl-ability/src/extra.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { Condition, buildAnd, buildOr, CompoundCondition } from '@ucast/mongo2js'; -import { PureAbility, AnyAbility } from './PureAbility'; -import { RuleOf } from './RuleIndex'; -import { RawRule } from './RawRule'; -import { Rule } from './Rule'; -import { setByPath, wrapArray } from './utils'; -import { AnyObject, SubjectType, ExtractSubjectType } from './types'; - -export type RuleToQueryConverter = (rule: RuleOf) => R; -export interface AbilityQuery { - $or?: T[] - $and?: T[] -} - -export function rulesToQuery( - ability: T, - action: Parameters[0], - subjectType: ExtractSubjectType[1]>, - convert: RuleToQueryConverter -): AbilityQuery | null { - const query: AbilityQuery = {}; - const rules = ability.rulesFor(action, subjectType); - - for (let i = 0; i < rules.length; i++) { - const rule = rules[i]; - const op = rule.inverted ? '$and' : '$or'; - - if (!rule.conditions) { - if (rule.inverted) { - break; - } else { - delete query[op]; - return query; - } - } else { - query[op] = query[op] || []; - query[op]!.push(convert(rule)); - } - } - - return query.$or ? query : null; -} - -function ruleToAST(rule: RuleOf): Condition { - if (!rule.ast) { - throw new Error(`Ability rule "${JSON.stringify(rule)}" does not have "ast" property. So, cannot be used to generate AST`); - } - - return rule.inverted ? new CompoundCondition('not', [rule.ast]) : rule.ast; -} - -export function rulesToAST( - ability: T, - action: Parameters[0], - subjectType: ExtractSubjectType[1]>, -): Condition | null { - const query = rulesToQuery(ability, action, subjectType, ruleToAST) as AbilityQuery; - - if (query === null) { - return null; - } - - if (!query.$and) { - return query.$or ? buildOr(query.$or) : buildAnd([]); - } - - if (query.$or) { - query.$and.push(buildOr(query.$or)); - } - - return buildAnd(query.$and); -} - -export function rulesToFields>( - ability: T, - action: Parameters[0], - subjectType: ExtractSubjectType[1]>, -): AnyObject { - return ability.rulesFor(action, subjectType) - .reduce((values, rule) => { - if (rule.inverted || !rule.conditions) { - return values; - } - - return Object.keys(rule.conditions).reduce((fields, fieldName) => { - const value = rule.conditions![fieldName]; - - if (!value || (value as any).constructor !== Object) { - setByPath(fields, fieldName, value); - } - - return fields; - }, values); - }, {} as AnyObject); -} - -export type GetRuleFields> = (rule: R) => string[]; - -export interface PermittedFieldsOptions { - fieldsFrom: GetRuleFields> -} - -export function permittedFieldsOf( - ability: T, - action: Parameters[0], - subject: Parameters[1], - options: PermittedFieldsOptions -): string[] { - const subjectType = ability.detectSubjectType(subject); - const rules = ability.possibleRulesFor(action, subjectType); - const uniqueFields = new Set(); - const deleteItem = uniqueFields.delete.bind(uniqueFields); - const addItem = uniqueFields.add.bind(uniqueFields); - let i = rules.length; - - while (i--) { - const rule = rules[i]; - if (rule.matchesConditions(subject)) { - const toggle = rule.inverted ? deleteItem : addItem; - options.fieldsFrom(rule).forEach(toggle); - } - } - - return Array.from(uniqueFields); -} - -const joinIfArray = (value: string | string[]) => Array.isArray(value) ? value.join(',') : value; - -export type PackRule> = - [string, string] | - [string, string, T['conditions']] | - [string, string, T['conditions'] | 0, 1] | - [string, string, T['conditions'] | 0, 1 | 0, string] | - [string, string, T['conditions'] | 0, 1 | 0, string | 0, string]; - -export type PackSubjectType = (type: T) => string; - -export function packRules>( - rules: T[], - packSubject?: PackSubjectType -): PackRule[] { - return rules.map((rule) => { // eslint-disable-line - const packedRule: PackRule = [ - joinIfArray((rule as any).action || (rule as any).actions), - typeof packSubject === 'function' - ? wrapArray(rule.subject).map(packSubject).join(',') - : joinIfArray(rule.subject), - rule.conditions || 0, - rule.inverted ? 1 : 0, - rule.fields ? joinIfArray(rule.fields) : 0, - rule.reason || '' - ]; - - while (packedRule.length > 0 && !packedRule[packedRule.length - 1]) packedRule.pop(); - - return packedRule; - }); -} - -export type UnpackSubjectType = (type: string) => T; - -export function unpackRules>( - rules: PackRule[], - unpackSubject?: UnpackSubjectType -): T[] { - return rules.map(([action, subject, conditions, inverted, fields, reason]) => { - const subjects = subject.split(','); - const rule = { - inverted: !!inverted, - action: action.split(','), - subject: typeof unpackSubject === 'function' - ? subjects.map(unpackSubject) - : subjects - } as T; - - if (conditions) rule.conditions = conditions; - if (fields) rule.fields = fields.split(','); - if (reason) rule.reason = reason; - - return rule; - }); -} diff --git a/packages/casl-ability/src/extra/index.ts b/packages/casl-ability/src/extra/index.ts new file mode 100644 index 000000000..8ca0310dc --- /dev/null +++ b/packages/casl-ability/src/extra/index.ts @@ -0,0 +1,5 @@ +export * from './packRules'; +export * from './permittedFieldsOf'; +export * from './rulesToFields'; +export * from './rulesToQuery'; +export * from './packRules'; diff --git a/packages/casl-ability/src/extra/packRules.ts b/packages/casl-ability/src/extra/packRules.ts new file mode 100644 index 000000000..4cb10aa67 --- /dev/null +++ b/packages/casl-ability/src/extra/packRules.ts @@ -0,0 +1,61 @@ + +import { RawRule } from '../RawRule'; +import { SubjectType } from '../types'; +import { wrapArray } from '../utils'; + +const joinIfArray = (value: string | string[]) => Array.isArray(value) ? value.join(',') : value; + +export type PackRule> = + [string, string] | + [string, string, T['conditions']] | + [string, string, T['conditions'] | 0, 1] | + [string, string, T['conditions'] | 0, 1 | 0, string] | + [string, string, T['conditions'] | 0, 1 | 0, string | 0, string]; + +export type PackSubjectType = (type: T) => string; + +export function packRules>( + rules: T[], + packSubject?: PackSubjectType +): PackRule[] { + return rules.map((rule) => { // eslint-disable-line + const packedRule: PackRule = [ + joinIfArray((rule as any).action || (rule as any).actions), + typeof packSubject === 'function' + ? wrapArray(rule.subject).map(packSubject).join(',') + : joinIfArray(rule.subject), + rule.conditions || 0, + rule.inverted ? 1 : 0, + rule.fields ? joinIfArray(rule.fields) : 0, + rule.reason || '' + ]; + + while (packedRule.length > 0 && !packedRule[packedRule.length - 1]) packedRule.pop(); + + return packedRule; + }); +} + +export type UnpackSubjectType = (type: string) => T; + +export function unpackRules>( + rules: PackRule[], + unpackSubject?: UnpackSubjectType +): T[] { + return rules.map(([action, subject, conditions, inverted, fields, reason]) => { + const subjects = subject.split(','); + const rule = { + inverted: !!inverted, + action: action.split(','), + subject: typeof unpackSubject === 'function' + ? subjects.map(unpackSubject) + : subjects + } as T; + + if (conditions) rule.conditions = conditions; + if (fields) rule.fields = fields.split(','); + if (reason) rule.reason = reason; + + return rule; + }); +} diff --git a/packages/casl-ability/src/extra/permittedFieldsOf.ts b/packages/casl-ability/src/extra/permittedFieldsOf.ts new file mode 100644 index 000000000..c8f8aead4 --- /dev/null +++ b/packages/casl-ability/src/extra/permittedFieldsOf.ts @@ -0,0 +1,69 @@ +import { AnyAbility } from '../PureAbility'; +import { Rule } from '../Rule'; +import { RuleOf } from '../RuleIndex'; +import { Subject, SubjectType } from '../types'; + +export type GetRuleFields> = (rule: R) => string[]; + +export interface PermittedFieldsOptions { + fieldsFrom: GetRuleFields> +} + +export function permittedFieldsOf( + ability: T, + action: Parameters[0], + subject: Parameters[1], + options: PermittedFieldsOptions +): string[] { + const subjectType = ability.detectSubjectType(subject); + const rules = ability.possibleRulesFor(action, subjectType); + const uniqueFields = new Set(); + const deleteItem = uniqueFields.delete.bind(uniqueFields); + const addItem = uniqueFields.add.bind(uniqueFields); + let i = rules.length; + + while (i--) { + const rule = rules[i]; + if (rule.matchesConditions(subject)) { + const toggle = rule.inverted ? deleteItem : addItem; + options.fieldsFrom(rule).forEach(toggle); + } + } + + return Array.from(uniqueFields); +} + +export type GetSubjectTypeAllFieldsExtractor = (subjectType: SubjectType) => string[]; + +/** + * Helper class to make custom `accessibleFieldsBy` helper function + */ +export class AccessibleFields { + constructor( + private readonly _ability: AnyAbility, + private readonly _action: string, + private readonly _getAllFields: GetSubjectTypeAllFieldsExtractor + ) {} + + /** + * Returns accessible fields for Model type + */ + ofType(subjectType: Extract): string[] { + return permittedFieldsOf(this._ability, this._action, subjectType, { + fieldsFrom: this._getRuleFields(subjectType) + }); + } + + /** + * Returns accessible fields for particular document + */ + of(subject: Exclude): string[] { + return permittedFieldsOf(this._ability, this._action, subject, { + fieldsFrom: this._getRuleFields(this._ability.detectSubjectType(subject)) + }); + } + + private _getRuleFields(type: SubjectType): GetRuleFields> { + return (rule) => (rule.fields || this._getAllFields(type)); + } +} diff --git a/packages/casl-ability/src/extra/rulesToFields.ts b/packages/casl-ability/src/extra/rulesToFields.ts new file mode 100644 index 000000000..5955a2fae --- /dev/null +++ b/packages/casl-ability/src/extra/rulesToFields.ts @@ -0,0 +1,29 @@ +import { PureAbility } from '../PureAbility'; +import { AnyObject, ExtractSubjectType } from '../types'; +import { setByPath } from '../utils'; + +/** + * Extracts rules condition values into an object of default values + */ +export function rulesToFields>( + ability: T, + action: Parameters[0], + subjectType: ExtractSubjectType[1]>, +): AnyObject { + return ability.rulesFor(action, subjectType) + .reduce((values, rule) => { + if (rule.inverted || !rule.conditions) { + return values; + } + + return Object.keys(rule.conditions).reduce((fields, fieldName) => { + const value = rule.conditions![fieldName]; + + if (!value || (value as any).constructor !== Object) { + setByPath(fields, fieldName, value); + } + + return fields; + }, values); + }, {} as AnyObject); +} diff --git a/packages/casl-ability/src/extra/rulesToQuery.ts b/packages/casl-ability/src/extra/rulesToQuery.ts new file mode 100644 index 000000000..e785be4d7 --- /dev/null +++ b/packages/casl-ability/src/extra/rulesToQuery.ts @@ -0,0 +1,69 @@ +import { CompoundCondition, Condition, buildAnd, buildOr } from '@ucast/mongo2js'; +import { AnyAbility } from '../PureAbility'; +import { RuleOf } from '../RuleIndex'; +import { ExtractSubjectType } from '../types'; + +export type RuleToQueryConverter = (rule: RuleOf) => R; +export interface AbilityQuery { + $or?: T[] + $and?: T[] +} + +export function rulesToQuery( + ability: T, + action: Parameters[0], + subjectType: ExtractSubjectType[1]>, + convert: RuleToQueryConverter +): AbilityQuery | null { + const query: AbilityQuery = {}; + const rules = ability.rulesFor(action, subjectType); + + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + const op = rule.inverted ? '$and' : '$or'; + + if (!rule.conditions) { + if (rule.inverted) { + break; + } else { + delete query[op]; + return query; + } + } else { + query[op] = query[op] || []; + query[op]!.push(convert(rule)); + } + } + + return query.$or ? query : null; +} + +function ruleToAST(rule: RuleOf): Condition { + if (!rule.ast) { + throw new Error(`Ability rule "${JSON.stringify(rule)}" does not have "ast" property. So, cannot be used to generate AST`); + } + + return rule.inverted ? new CompoundCondition('not', [rule.ast]) : rule.ast; +} + +export function rulesToAST( + ability: T, + action: Parameters[0], + subjectType: ExtractSubjectType[1]>, +): Condition | null { + const query = rulesToQuery(ability, action, subjectType, ruleToAST) as AbilityQuery; + + if (query === null) { + return null; + } + + if (!query.$and) { + return query.$or ? buildOr(query.$or) : buildAnd([]); + } + + if (query.$or) { + query.$and.push(buildOr(query.$or)); + } + + return buildAnd(query.$and); +} diff --git a/packages/casl-ability/tsconfig.json b/packages/casl-ability/tsconfig.json index cacee0ccd..88e96d8db 100644 --- a/packages/casl-ability/tsconfig.json +++ b/packages/casl-ability/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig", "include": [ - "src/*", + "src/**/*", "spec/**/*" ] }