Skip to content

Commit

Permalink
refactor(package): replaces siftjs with @ucast/mongo2js
Browse files Browse the repository at this point in the history
This allows to use AST generated by @ucast/mongo to convert the conditions to different languages and interpret them

BREAKING CHANGE: replaces siftjs with @ucast/mongo2js. This changed `MongoQuery` type and `buildMongoQueryMatcher` function parameters. Influences users who implemented custom sift operators:

  * `MongoQuery` accepted a generic type of AdditionalOperators, now it accepts an object interface and custom operators
  * `MongoQueryOperators` is renamed to `MongoQueryFieldOperators` and now accepts `Value` generic parameter
  * `buildMongoQuery` now accepts 3 optional parameters: [custom parsing instruction](https://www.npmjs.com/package/@ucast/mongo#custom-operator), [custom operator interpreters](https://www.npmjs.com/package/@ucast/js#custom-operator-interpreter) and [options for JavaScript interpreters](https://www.npmjs.com/package/@ucast/js#custom-interpreter)
  * `Ability` does not compare objects anymore, so if you rely on value to equal specific object, then you need to either change your conditions or implement custom `equal` function

  **Before**

  ```ts
  import { MongoQuery, MongoQueryOperators, buildMongoQueryMatcher } from '@casl/ability';
  import { $nor } from 'sift';

  type CustomMongoQuery = MongoQuery<{
    $customOperator: Function
  }>;
  type $eq = MongoQueryOperators['$eq'];

  const matcher = buildMongoQueryMatcher({ $nor })
  ```

  **After**

  ```ts
  import { MongoQuery, MongoQueryFieldOperators, buildMongoQueryMatcher } from '@casl/
  ability';
  import { $nor, nor } from '@ucast/mongo2js'

  type CustomMongoQuery<T> = MongoQuery<T, {
    toplevel: {
      // can be used only on document level
      $customOperator: Function
    },
    field: {
      // can be used only on field level
      $my: boolean
    }
  }>
  type $eq = MongoQueryFieldOperators['$eq']; // accepts optional `Value` generic parameter

  const matcher = buildMongoQueryMatcher({ $nor }, { nor });
        ```

Relates to #350
  • Loading branch information
stalniy committed Aug 10, 2020
1 parent bf5ef73 commit 41e53aa
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 81 deletions.
34 changes: 12 additions & 22 deletions docs-src/src/content/pages/advanced/customize-ability/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ CASL was built with extensibility in mind and this allows you to extend conditio

## Extend conditions with custom operators

Thanks to [sift.js](https://github.com/crcn/sift.js), it's possible to [define conditions in CASL](../../guide/conditions-in-depth) using MongoDB query language. Usually this is enough but sometimes you may want to add non-standard operator or one of those that are not included in CASL by default (e.g., [$nor]) or you may want to restrict possible operators.
Thanks to [ucast](https://github.com/stalniy/ucast), it's possible to [define conditions in CASL](../../guide/conditions-in-depth) using MongoDB query language. Usually this is enough but sometimes you may want to restrict possible operators, add non-standard operator or one of those that is not included in CASL by default (e.g., [$nor]).

Let's see an example how to add `$nor` operator. To do this, we will use `buildMongoQueryMatcher` helper function from `@casl/ability` package. It allows to add or override existing operators:
Let's see an example of how to add `$nor` operator. To do this, we will use `buildMongoQueryMatcher` helper function from `@casl/ability` package. It allows to add or override existing operators:

[$nor]: https://docs.mongodb.com/manual/reference/operator/query/nor/

Expand All @@ -27,17 +27,12 @@ import {
MongoQuery,
buildMongoQueryMatcher,
} from '@casl/ability';
import { $nor } from 'sift';
import { $nor, nor } from '@ucast/mongo2js';

interface QueryExtensions {
$nor?: [MongoQuery, ...MongoQuery[]]
};
type CustomMongoQuery = MongoQuery | QueryExtensions;
const conditionsMatcher = buildMongoQueryMatcher<QueryExtensions>({ $nor });
type AppAbility = Ability<Abilities, CustomMongoQuery>;
const conditionsMatcher = buildMongoQueryMatcher({ $nor }, { nor });

export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder<AppAbility>(Ability);
const { can, build } = new AbilityBuilder<Ability>(Ability);

can('read', 'Article', {
$nor: [{ private: true }, { authorId: user.id }]
Expand All @@ -49,25 +44,20 @@ export default function defineAbilityFor(user: any) {

> We use `user: any` for the purpose of ease, you should avoid this in real apps
To restrict operators you shouldn't use `buildMongoQueryMatcher`. For example, let's allow to use only `$eq` and `$in` operators:
To restrict operators, you don't need `buildMongoQueryMatcher`. For example, let's allow to use only `$eq` and `$in` operators:

```ts
import {
Ability,
AbilityBuilder,
Abilities,
MongoQueryOperators,
MongoQueryFieldOperators,
ConditionsMatcher,
} from '@casl/ability';
import { $in, $eq, Query, createQueryTester as sift } from 'sift';

type RestrictedMongoQuery = Record<PropertyKey, number | string | {
$eq?: MongoQueryOperators['$eq'],
$in?: MongoQueryOperators['$in'],
}>;
const conditionsMatcher: ConditionsMatcher<RestrictedMongoQuery> = (conditions) => {
return sift(conditions as Query, { operations: { $in, $eq } });
};
import { $in, within, $eq, eq, createFactory, BuildMongoQuery } from '@ucast/mongo2js';

type RestrictedMongoQuery<T> = BuildMongoQuery<T, Pick<MongoQueryFieldOperators, '$eq' | '$in'>>;
const conditionsMatcher: ConditionsMatcher<RestrictedMongoQuery> = createFactory({ $in, $eq }, { in: within, eq });
type AppAbility = Ability<Abilities, RestrictedMongoQuery>;

export default function defineAbilityFor(user: any) {
Expand All @@ -80,7 +70,7 @@ export default function defineAbilityFor(user: any) {
}
```

> Read [sift docs](https://github.com/crcn/sift.js#custom-operations) page to learn how to create custom operators.
> Read [@ucast/mongo2js docs](https://github.com/stalniy/ucast/tree/master/packages/mongo2js#custom-operator) page to learn how to create custom operators.
By restricting operators, you not only disallow other developers to use more complex conditions but also make your frontend bundle size smaller (thanks to bundlers with tree-shaking support).

Expand Down
10 changes: 5 additions & 5 deletions docs-src/src/content/pages/guide/conditions-in-depth/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ meta:
description: ~
---

Thanks to [sift.js](https://github.com/crcn/sift.js) `Ability` instances can match objects using [MongoDB query language](http://docs.mongodb.org/manual/reference/operator/query/).
Thanks to [ucast](https://github.com/stalniy/ucast), we can define permissions using [MongoDB query language](http://docs.mongodb.org/manual/reference/operator/query/).

If you are not familiar with MongoDB query language, don't worry, it's not required. We will go through some of its operators in this guide. But before we start, let's see how useful it may be by creating simple article scheduling logic, that is allow to read articles only if its `createdAt` is in the past:
Don't worry, if you are not familiar with MongoDB query language. We will go through some of its operators in this guide. But before we start, let's see how useful it may be by creating simple article scheduling logic, that allows to read articles only if their `createdAt` is in the past:

```js
import { defineAbility } from '@casl/ability';
Expand All @@ -31,7 +31,7 @@ export default defineAbility((can) => {
});
```

Do you feel the power it brings?
Do you see the power it brings?

## MongoDB and its query language

Expand All @@ -43,7 +43,7 @@ JavaScript is a superset of `JSON`, that's why we decided to use MongoDB query l

**You don't need to know anything about MongoDB** in order to use CASL, you need to know only subset of its query language operators.

Query is what you pass in conditions to `can` and `cannot` functions (3rd or 4th argument if you define fields). So, it's an object which defines some restrictions on a javascript object and if that restrictions are matched then the object is returned.
Query is what you pass in conditions to `can` and `cannot` functions (3rd or 4th argument if you define fields). So, it's an object which defines restrictions on a JavaScript object and if that restrictions are matched then the object is returned.

These are some examples of a query:

Expand All @@ -56,7 +56,7 @@ const queries = [
{ price: { $gte: 10, $lte: 50 } }, // (3)
{ tags: { $all: ['permission', 'casl'] } },
{ email: { $regex: /@gmail.com$/i } },
{ 'city.address': { $elemMatch: { postalCode: { $regex: /^AB/ } } } } // (4)
{ 'cities.address': { $elemMatch: { postalCode: { $regex: /^AB/ } } } } // (4)
]
```

Expand Down
24 changes: 17 additions & 7 deletions docs-src/src/content/pages/guide/install/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,24 @@ For prototyping or learning purposes, you can use the latest version with:
For production, we recommend linking to a specific version number and build to avoid unexpected breakage from newer versions:

```html
<script src="https://cdn.jsdelivr.net/npm/@casl/ability@4.0.0"></script>
```

Remember that CASL depends on [@ucast/mongo2js] which depends on [@ucast/core], [@ucast/js] and [@ucast/mongo], that's why you need to specify all these libraries before `@casl/ability`:

```html
<script src="https://cdn.jsdelivr.net/npm/@ucast/core"></script>
<script src="https://cdn.jsdelivr.net/npm/@ucast/mongo"></script>
<script src="https://cdn.jsdelivr.net/npm/@ucast/js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ucast/mongo2js"></script>
<script src="https://cdn.jsdelivr.net/npm/@casl/ability"></script>
```

[@ucast/core]: https://www.npmjs.com/package/@ucast/core
[@ucast/js]: https://www.npmjs.com/package/@ucast/js
[@ucast/mongo]: https://www.npmjs.com/package/@ucast/mongo
[@ucast/mongo2js]: https://www.npmjs.com/package/@ucast/mongo

## Download and use &lt;script&gt; tag

Simply [download CASL from CDN](https://cdn.jsdelivr.net/npm/@casl/ability) and include with a script tag. `casl` will be registered as a global variable.
Expand All @@ -106,14 +121,9 @@ All official packages has the same directory layout (except of `@casl/mongoose`

## CSP environments

Some environments, such as Google Chrome Apps, enforce Content Security Policy (CSP), which prohibits the use of `new Function()` for evaluating expressions. [sift](https://github.com/crcn/sift.js) is a package which is CASL uses internally depends on this feature to evaluate `$where` operator.

CASL doesn't imports this operator, so if you use bundlers such as [webpack] or [rollup], they will shake out this operator and it will not appear in the eventual bundle.

If you use bundler which does not support tree shaking, you have 2 options:
Some environments, such as Google Chrome Apps, enforce Content Security Policy (CSP), which prohibits the use of `new Function()` for evaluating expressions.

1. Set `CSP_ENABLED=1` environment variable, this will replace `process.env.CSP_ENABLED` with `1`. Eventually uglifier will remove this code as unreachable
2. Depending of the module system you use, replace `from 'sift'` or `require('sift')` with `from 'sift/sift.csp.min'` or `require('sift/sift.csp.min')` in the eventual bundle (e.g., using shell `sed`)
CASL doesn't use any of the prohibited functions.

## Dev build

Expand Down
2 changes: 1 addition & 1 deletion docs-src/src/content/pages/guide/intro/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ ability.can('update', anotherArticle) // false
**Pay attention** that conditions object contains the same keys as the entity we want to check. This is how CASL matches entities by conditions. In our case, it just checks that `authorId` in `Article` instance equals to `authorId` in conditions object. Conditions may have several fields, in that case all fields should match (`AND` logic).

Thanks to [sift.js](https://github.com/crcn/sift.js) `Ability` instances can match objects using [MongoDB query language](http://docs.mongodb.org/manual/reference/operator/query/).
Thanks to [ucast](https://github.com/stalniy/ucast) `Ability` instances can match objects using [MongoDB query language](http://docs.mongodb.org/manual/reference/operator/query/).

> If you are not familiar with MongoDB query language, see [CASL conditions in depth](../conditions-in-depth) for details
Expand Down
8 changes: 4 additions & 4 deletions packages/casl-ability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@
"engines": {
"npm": "^6.0.0"
},
"dependencies": {
"sift": "^13.0.0"
},
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.8.3",
Expand Down Expand Up @@ -76,5 +73,8 @@
"dist",
"*.d.ts",
"extra"
]
],
"dependencies": {
"@ucast/mongo2js": "^1.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/casl-ability/src/Ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { fieldPatternMatcher } from './matchers/field';

export class Ability<
A extends AbilityTuple = AbilityTuple,
C extends MongoQuery<any> = MongoQuery
C extends MongoQuery = MongoQuery
> extends PureAbility<A, C> {
constructor(rules?: RawRuleFrom<A, C>[], options?: AbilityOptions<A, C>) {
super(rules, {
Expand Down
105 changes: 70 additions & 35 deletions packages/casl-ability/src/matchers/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import {
createQueryTester as sift,
$eq,
eq,
$ne,
ne,
$lt,
lt,
$lte,
lte,
$gt,
gt,
$gte,
gte,
$in,
within,
$nin,
nin,
$all,
all,
$size,
size,
$regex,
regex,
$elemMatch,
$exists
} from 'sift';
import { ConditionsMatcher as Matcher } from '../types';
elemMatch,
$exists,
exists,
createFactory,
equal,
createGetter
} from '@ucast/mongo2js';
import type { MongoQuery } from '@ucast/mongo2js';
import { ConditionsMatcher } from '../types';

const defaultOperations = {
const defaultInstructions = {
$eq,
$ne,
$lt,
Expand All @@ -31,36 +47,55 @@ const defaultOperations = {
$elemMatch,
$exists,
};

type RegExpOptions<T> = { $regex: T, $options?: string };
type Primitive = Record<PropertyKey, any> | string | number | null | boolean | undefined;
export type MongoQueryOperators = {
$eq?: any,
$ne?: any,
$lt?: string | number | Date,
$lte?: string | number | Date,
$gt?: string | number | Date,
$gte?: string | number | Date,
$in?: any[],
$nin?: any[],
$all?: any[],
/** checks by array length */
$size?: number,
$regex?: RegExp | RegExpOptions<string> | RegExpOptions<RegExp>,
/** checks the shape of array item */
$elemMatch?: {
[k in Exclude<keyof MongoQueryOperators, '$elemMatch'>]?: MongoQueryOperators[k]
},
/** checks that property exists */
$exists?: boolean
const defaultInterpreters = {
eq,
ne,
lt,
lte,
gt,
gte,
in: within,
nin,
all,
size,
regex,
elemMatch,
exists,
};

export type MongoQuery<AdditionalOperators = never> =
Record<PropertyKey, MongoQueryOperators | Primitive | AdditionalOperators>;
export function buildMongoQueryMatcher<T extends object>(
operations: Record<keyof T, any>
): Matcher<MongoQuery | T> {
const options = { operations: { ...defaultOperations, ...operations } };
return conditions => sift(conditions as any, options);
interface HasToJSON {
toJSON(): unknown
}

function toPrimitive(value: unknown) {
if (value instanceof Date) {
return value.getTime();
}

if (value && typeof (value as HasToJSON).toJSON === 'function') {
return (value as HasToJSON).toJSON();
}

return value;
}
export const mongoQueryMatcher = buildMongoQueryMatcher({});

const isEqual: typeof equal = (a, b) => equal(toPrimitive(a), toPrimitive(b));
const getField = createGetter((object, field) => toPrimitive(object[field]));

type MongoQueryMatcher =
(...args: Partial<Parameters<typeof createFactory>>) => ConditionsMatcher<MongoQuery>;
export const buildMongoQueryMatcher: MongoQueryMatcher = (instructions, interpreters, options) => {
return createFactory(
{ ...defaultInstructions, ...instructions },
{ ...defaultInterpreters, ...interpreters },
{ equal: isEqual, get: getField, ...options }
);
};

export const mongoQueryMatcher = buildMongoQueryMatcher();
export type {
MongoQuery,
MongoQueryFieldOperators,
MongoQueryTopLevelOperators,
MongoQueryOperators,
} from '@ucast/mongo2js';
32 changes: 26 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 41e53aa

Please sign in to comment.