├── .codeclimate.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── docs.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .renovaterc ├── BACKERS.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs-src ├── .env ├── .env.production ├── .gitignore ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── app-icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ ├── fonts │ │ ├── StardosStencil-Bold.woff2 │ │ └── StardosStencil-Regular.woff2 │ ├── global.css │ ├── manifest.json │ ├── versions.txt │ └── web-root │ │ ├── 404.html │ │ ├── google4f1edd737abc76a4.html │ │ └── robots.txt ├── rollup.config.js ├── src │ ├── app.js │ ├── assets │ │ ├── casl-shield.png │ │ ├── icons │ │ │ ├── menu.png │ │ │ └── search.svg │ │ ├── payment-options │ │ │ ├── liqpay-qrcode.png │ │ │ ├── liqpay.svg │ │ │ ├── monobank-qrcode.svg │ │ │ └── monobank.png │ │ └── shield.png │ ├── bootstrap.js │ ├── components │ │ ├── App.js │ │ ├── AppFooter.js │ │ ├── AppHeader.js │ │ ├── AppLink.js │ │ ├── AppMenu.js │ │ ├── AppNotification.js │ │ ├── AppRoot.js │ │ ├── ArticleDetails.js │ │ ├── GithubButton.js │ │ ├── HomePage.js │ │ ├── I18nElement.js │ │ ├── LangPicker.js │ │ ├── MenuDrawer.js │ │ ├── OldVersionAlert.js │ │ ├── OneTimeDonations.js │ │ ├── Page.js │ │ ├── PageNav.js │ │ ├── PagesByCategories.js │ │ ├── QuickSearch.js │ │ └── VersionsSelect.js │ ├── config │ │ ├── app.js │ │ ├── menu.yml │ │ ├── routes.yml │ │ └── search.js │ ├── content │ │ ├── app │ │ │ └── en.yml │ │ └── pages │ │ │ ├── advanced │ │ │ ├── ability-inheritance │ │ │ │ └── en.md │ │ │ ├── ability-to-database-query │ │ │ │ └── en.md │ │ │ ├── customize-ability │ │ │ │ └── en.md │ │ │ ├── debugging-testing │ │ │ │ └── en.md │ │ │ └── typescript │ │ │ │ ├── casl-abilitybuilder-conditions-hints.png │ │ │ │ ├── casl-abilitybuilder-fields-hints.png │ │ │ │ ├── casl-abilitybuilder.png │ │ │ │ ├── casl-action-hints.png │ │ │ │ ├── casl-class-subject-with-name.png │ │ │ │ ├── casl-class-subject.png │ │ │ │ ├── casl-discriminated-class-subject.png │ │ │ │ ├── casl-subject-hints.png │ │ │ │ ├── casl-tagged-union-subject.png │ │ │ │ └── en.md │ │ │ ├── api │ │ │ ├── casl-ability-extra │ │ │ │ └── en.md │ │ │ └── casl-ability │ │ │ │ └── en.md │ │ │ ├── cookbook │ │ │ ├── cache-rules │ │ │ │ └── en.md │ │ │ ├── claim-authorization │ │ │ │ └── en.md │ │ │ ├── intro │ │ │ │ └── en.md │ │ │ ├── less-confusing-can-api │ │ │ │ └── en.md │ │ │ ├── roles-with-persisted-permissions │ │ │ │ └── en.md │ │ │ └── roles-with-static-permissions │ │ │ │ └── en.md │ │ │ ├── guide │ │ │ ├── conditions-in-depth │ │ │ │ └── en.md │ │ │ ├── define-aliases │ │ │ │ └── en.md │ │ │ ├── define-rules │ │ │ │ └── en.md │ │ │ ├── install │ │ │ │ └── en.md │ │ │ ├── intro │ │ │ │ └── en.md │ │ │ ├── restricting-fields │ │ │ │ └── en.md │ │ │ └── subject-type-detection │ │ │ │ └── en.md │ │ │ ├── notfound │ │ │ └── en.md │ │ │ ├── package │ │ │ ├── casl-angular │ │ │ │ └── en.md │ │ │ ├── casl-aurelia │ │ │ │ └── en.md │ │ │ ├── casl-mongoose │ │ │ │ └── en.md │ │ │ ├── casl-prisma │ │ │ │ └── en.md │ │ │ ├── casl-react │ │ │ │ └── en.md │ │ │ └── casl-vue │ │ │ │ └── en.md │ │ │ └── support-casljs │ │ │ └── en.md │ ├── directives │ │ └── i18n.js │ ├── hooks │ │ ├── scrollToSection.js │ │ └── watchMedia.js │ ├── partials │ │ └── caslFeatures.js │ ├── serviceWorker.js │ ├── services │ │ ├── ContentType.js │ │ ├── content.js │ │ ├── error.js │ │ ├── http.js │ │ ├── i18n.js │ │ ├── meta.js │ │ ├── pageController.js │ │ ├── querystring.js │ │ ├── router.js │ │ ├── utils.js │ │ └── version.js │ └── styles │ │ ├── alert.js │ │ ├── btn.js │ │ ├── code.js │ │ ├── grid.js │ │ ├── index.js │ │ ├── md.js │ │ └── page.js └── tools │ ├── SearchIndex.js │ ├── appEnvVars.js │ ├── contentParser.js │ ├── index.html.js │ ├── mdImage.js │ ├── mdLink.js │ ├── mdTableContainer.js │ ├── prerender.js │ ├── sitemap.xml.js │ ├── stop-words │ ├── en.txt │ ├── ru.txt │ └── ua.txt │ └── workbox.config.js ├── git-hooks ├── .gitignore └── pre-commit ├── package-lock.json ├── package.json ├── packages ├── casl-ability │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── extra.d.ts │ ├── extra │ │ └── package.json │ ├── index.d.ts │ ├── package.json │ ├── spec │ │ ├── ability.spec.js │ │ ├── builder.spec.js │ │ ├── error.spec.ts │ │ ├── pack_rules.spec.ts │ │ ├── permitted_fields.spec.js │ │ ├── rulesToAST.spec.js │ │ ├── rulesToQuery.spec.js │ │ ├── rules_to_fields.spec.js │ │ ├── spec_helper.js │ │ ├── subject_helper.spec.ts │ │ └── types │ │ │ ├── Ability.spec.ts │ │ │ └── AbilityBuilder.spec.ts │ ├── src │ │ ├── Ability.ts │ │ ├── AbilityBuilder.ts │ │ ├── ForbiddenError.ts │ │ ├── PureAbility.ts │ │ ├── RawRule.ts │ │ ├── Rule.ts │ │ ├── RuleIndex.ts │ │ ├── extra │ │ │ ├── index.ts │ │ │ ├── packRules.ts │ │ │ ├── permittedFieldsOf.ts │ │ │ ├── rulesToFields.ts │ │ │ └── rulesToQuery.ts │ │ ├── hkt.ts │ │ ├── index.ts │ │ ├── matchers │ │ │ ├── conditions.ts │ │ │ └── field.ts │ │ ├── structures │ │ │ └── LinkedItem.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── casl-angular │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── jest.config.js │ ├── package.json │ ├── spec │ │ ├── AbilityService.spec.ts │ │ ├── AbilityServiceSignal.spec.ts │ │ ├── pipes.e2e.spec.ts │ │ └── spec_helper.ts │ ├── src │ │ ├── AbilityService.ts │ │ ├── AbilityServiceSignal.ts │ │ ├── pipes.ts │ │ └── public.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tsconfig.types.json ├── casl-aurelia │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── spec │ │ ├── .eslintrc │ │ ├── plugin.spec.js │ │ └── spec_helper.js │ ├── src │ │ ├── index.ts │ │ └── value-converter │ │ │ └── can.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── casl-mongoose │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── spec │ │ ├── accessibleBy.spec.ts │ │ ├── accessibleFieldsBy.spec.ts │ │ ├── accessible_fields.spec.ts │ │ └── accessible_records.spec.ts │ ├── src │ │ ├── accessibleBy.ts │ │ ├── accessibleFieldsBy.ts │ │ ├── index.ts │ │ └── plugins │ │ │ ├── accessible_fields.ts │ │ │ └── accessible_records.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── casl-prisma │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── runtime.d.ts │ ├── runtime.js │ ├── schema.prisma │ ├── spec │ │ ├── AppAbility.ts │ │ ├── PrismaAbility.spec.ts │ │ ├── accessibleBy.spec.ts │ │ └── prismaQuery.spec.ts │ ├── src │ │ ├── accessibleByFactory.ts │ │ ├── createAbilityFactory.ts │ │ ├── errors │ │ │ └── ParsingQueryError.ts │ │ ├── index.ts │ │ ├── prisma │ │ │ ├── PrismaQueryParser.ts │ │ │ ├── interpretPrismaQuery.ts │ │ │ └── prismaQuery.ts │ │ ├── prismaClientBoundTypes.ts │ │ └── runtime.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── casl-react │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── spec │ │ ├── Can.spec.tsx │ │ ├── factory.spec.tsx │ │ └── useAbility.spec.ts │ ├── src │ │ ├── Can.ts │ │ ├── factory.ts │ │ ├── hooks │ │ │ └── useAbility.ts │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── casl-vue │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── spec │ │ ├── can.spec.js │ │ ├── hooks.spec.js │ │ └── plugin.spec.js │ ├── src │ │ ├── component │ │ │ └── can.ts │ │ ├── index.ts │ │ ├── plugin.ts │ │ ├── reactiveAbility.ts │ │ └── useAbility.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── dx │ ├── bin │ ├── dx.js │ ├── release-packages.sh │ └── semantic-release │ ├── config │ ├── babel.config.mjs │ ├── eslint.config.mjs │ ├── jest.chai.config.js │ ├── jest.config.js │ ├── lintstaged.js │ ├── rollup.config.mjs │ └── semantic-release.js │ ├── lib │ ├── dx.js │ ├── spawn.js │ └── spec_helper.js │ ├── package.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | eslint: 4 | enabled: false 5 | config: 6 | extensions: [.js, .ts] 7 | checks: 8 | import/extensions: 9 | enabled: false 10 | import/no-extraneous-dependencies: 11 | enabled: false 12 | no-console: 13 | enabled: false 14 | method-complexity: 15 | config: 16 | threshold: 8 17 | 18 | exclude_patterns: 19 | - docs/**/* 20 | - docs-src/**/* 21 | - "**/spec/**/*" 22 | - "**/*.d.ts" 23 | - tools/**/* 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: sstotskyi 5 | open_collective: casljs 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 1. Ability configuration (rules, detectSubjectType, etc) 23 | 2. How do you check abilities 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Interactive example** (optional, but highly desirable) 29 | provide a link to the example from http://repl.it/, https://codesandbox.io/ or similar, so we can quickly test and provide feedback. Otherwise 30 | 31 | **CASL Version** 32 | 33 | 34 | 35 | `@casl/ability` - v 36 | `@casl/vue` - v 37 | `@casl/react` - v 38 | `@casl/angular` - v 39 | `@casl/aurelia` - v 40 | 41 | **Environment**: 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | **Is your feature request related to a problem? Please describe.** 18 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | 20 | **Describe the solution you'd like** 21 | A clear and concise description of what you want to happen. 22 | 23 | **Describe alternatives you've considered** (optional) 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | 26 | **Additional context** (optional) 27 | Add any other context or screenshots about the feature request here. 28 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [master, next, docs-fix] 6 | paths: 7 | - docs-src/**/* 8 | - .github/workflows/docs.yml 9 | - packages/casl-mongoose/README.md 10 | - packages/casl-react/README.md 11 | - packages/casl-vue/README.md 12 | - packages/casl-angular/README.md 13 | - packages/casl-aurelia/README.md 14 | - packages/casl-prisma/README.md 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | env: 20 | CASL_VERSION: v6 21 | SITEMAP_WEBSITE: https://casl.js.org 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Cache dependencies 25 | uses: actions/cache@v2 26 | env: 27 | cache-name: casl-docs-deps 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('docs-src/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-build-${{ env.cache-name }}- 33 | ${{ runner.os }}-build- 34 | ${{ runner.os }}- 35 | - name: Install deps 36 | working-directory: docs-src 37 | run: npm install 38 | - name: Build documentation 39 | working-directory: docs-src 40 | env: 41 | NODE_ENV: production 42 | LIT_APP_PUBLIC_PATH: "/${{ env.CASL_VERSION }}" 43 | LIT_APP_CASL_VERSION: "${{ env.CASL_VERSION }}" 44 | run: LIT_APP_COMMIT_HASH=$(git rev-parse --short "$GITHUB_SHA") npm run build 45 | - name: Build sitemap 46 | working-directory: docs-src 47 | run: npm run build.sitemap 48 | - name: Prerender 49 | working-directory: docs-src 50 | run: npm run prerender 51 | - name: Deploy 52 | env: 53 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 54 | GITHUB_ACTOR: docs 55 | run: | 56 | CASL_DOCS_ROOT="./${CASL_VERSION}" 57 | CURRENT_BRANCH=$(git branch --show-current) 58 | cat <<- EOF > $HOME/.netrc 59 | machine github.com 60 | login $GITHUB_ACTOR 61 | password $GITHUB_TOKEN 62 | machine api.github.com 63 | login $GITHUB_ACTOR 64 | password $GITHUB_TOKEN 65 | EOF 66 | chmod 600 $HOME/.netrc 67 | git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" 68 | git config --global user.name "$GITHUB_ACTOR" 69 | git fetch origin gh-pages 70 | git checkout -f gh-pages 71 | rm -rf $CASL_DOCS_ROOT 72 | mv docs-src/dist $CASL_DOCS_ROOT 73 | 74 | if [ "$CURRENT_BRANCH" = "master" ]; then 75 | cp $CASL_DOCS_ROOT/web-root/* $CASL_DOCS_ROOT/versions.txt $CASL_DOCS_ROOT/index.html . 76 | fi 77 | 78 | git add . 79 | git commit -m "chore(release): deploy ${CASL_DOCS_ROOT}" 80 | git push --set-upstream origin gh-pages 81 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - .github/workflows/main.yml 8 | - packages/**/*.{js,ts,json} 9 | - '*.{json,js}' 10 | - .eslintrc 11 | - tools/**/* 12 | pull_request: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node: 20 | - 18 # 2025-04-30 21 | - 20 # 2026-05-26 22 | - latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | ref: ${{ github.sha }} 27 | fetch-depth: 2 28 | - uses: pnpm/action-setup@v2 29 | with: 30 | version: 9.15.2 31 | run_install: false 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | - name: Cache dependencies 38 | uses: actions/cache@v3 39 | env: 40 | cache-name: casl-deps 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-build-${{ env.cache-name }}- 46 | ${{ runner.os }}-build- 47 | ${{ runner.os }}- 48 | - name: Install dependencies 49 | run: pnpm install 50 | - name: Get base sha 51 | id: base-commit 52 | run: | 53 | echo "sha=${{ github.event.pull_request.base.sha || github.event.before }}" >> $GITHUB_OUTPUT 54 | - name: Build 55 | run: | 56 | if [[ "${{ github.event_name }}" != "pull_request" || $(git diff --name-only HEAD^1 HEAD | grep casl-) = "" ]]; then 57 | pnpm run -r build 58 | else 59 | pnpm run -r --filter '[${{ steps.base-commit.outputs.sha }}]...' build 60 | fi 61 | - name: lint 62 | run: | 63 | if [[ "${{ github.event_name }}" != "pull_request" || $(git diff --name-only HEAD^1 HEAD | grep casl-) = "" ]]; then 64 | pnpm run -r lint 65 | else 66 | pnpm run -r --filter '[${{ steps.base-commit.outputs.sha }}]' lint 67 | fi 68 | - name: test 69 | run: | 70 | if [[ "${{ github.event_name }}" != "pull_request" || $(git diff --name-only HEAD^1 HEAD | grep casl-) = "" ]]; then 71 | pnpm run -r test --coverage 72 | else 73 | pnpm run -r --filter '...[${{ steps.base-commit.outputs.sha }}]' test --coverage 74 | fi 75 | - name: submit coverage 76 | env: 77 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 78 | run: pnpm run coverage.submit 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | coverage.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | dist/ 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | #vscode 60 | .vscode 61 | -------------------------------------------------------------------------------- /.renovaterc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "monorepo:angular" 5 | ], 6 | "ignoreDeps": [ 7 | "semantic-release", 8 | "@semantic-release/git", 9 | "@semantic-release/changelog" 10 | ], 11 | "ignorePaths": ["**/docs-src/**"], 12 | "pinVersions": false, 13 | "separatePatchReleases": false, 14 | "ignoreUnstable": true, 15 | "automerge": true, 16 | "automergeType": "branch-push", 17 | "lockFileMaintenance": { 18 | "enabled": true 19 | }, 20 | "peerDependencies": { 21 | "versionStrategy": "widen" 22 | }, 23 | "packageRules": [ 24 | { 25 | "sourceUrlPrefixes": [ 26 | "https://github.com/babel/babel" 27 | ], 28 | "groupName": "babel monorepo" 29 | }, 30 | { 31 | "packageNames": [ 32 | "chai", 33 | "chai-spies" 34 | ], 35 | "groupName": "chai" 36 | }, 37 | { 38 | "packagePatterns": ["^karma"], 39 | "groupName": "karma" 40 | }, 41 | { 42 | "packagePatterns": ["^eslint"], 43 | "groupName": "eslint" 44 | }, 45 | { 46 | "packagePatterns": ["^rollup"], 47 | "groupName": "rollup" 48 | }, 49 | { 50 | "packagePatterns": ["lerna"], 51 | "groupName": "lerna" 52 | }, 53 | { 54 | "packagePatterns": ["jest"], 55 | "groupName": "jest" 56 | }, 57 | { 58 | "packageNames": [ 59 | "@vue/test-utils", 60 | "vue", 61 | "vue-template-compiler" 62 | ], 63 | "groupName": "vue" 64 | }, 65 | { 66 | "packageNames": [ 67 | "react", 68 | "react-test-renderer", 69 | "@types/react", 70 | "@testing-library/react-hooks" 71 | ], 72 | "groupName": "react" 73 | }, 74 | { 75 | "packagePatterns": ["^aurelia-"], 76 | "groupName": "aurelia" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Sponsors & Backers 2 | 3 | CASL.js is an MIT-licensed open source project. It's an independent project with its ongoing development made possible entirely thanks to the support of [my wife](https://github.com/Olena-Stotska), who allows me to spend our free time on Open Source :). 4 | Maintaining and developing Open Source project takes considerable amount of time to: 5 | 6 | * read, analyze and prioritize issues 7 | * answer questions in support chat and stackoverflow 8 | * create articles and examples of how to use CASL 9 | 10 | So, I am currently exploring the possibility of working on CASL fulltime. 11 | 12 | If you run a business and is using CASL in a revenue-generating product, it would make business sense to sponsor CASL development: 13 | it ensures the project that your product relies on stays healthy and actively maintained. It can also help your exposure in the JavaScript community and makes it easier to attract JavaScript developers. 14 | 15 | If you'd like to help and see how CASL evolves, please consider: 16 | 17 | - [Become a backer or sponsor on OpenCollective](https://opencollective.com/casljs). 18 | 19 | ## Platinum Sponsors 20 | 21 | [Become the first platinum sponsor](https://opencollective.com/casljs/contribute/platinum-sponsors-13746/checkout) 22 | 23 | ## Gold Sponsors 24 | 25 | [Become the first gold sponsor](https://opencollective.com/casljs/contribute/gold-sponsors-13747/checkout) 26 | 27 | ## Silver Sponsors 28 | 29 | [Become the first silver sponsor](https://opencollective.com/casljs/contribute/silver-sponsors-13745/checkout) 30 | 31 | ## Bronze Sponsors 32 | 33 | [Become the first bronze sponsor](https://opencollective.com/casljs/contribute/bronze-sponsors-13741/checkout) 34 | 35 | ## Generous Backers 36 | 37 | [Become the first backer](https://opencollective.com/casljs/contribute/backer-13740/checkout) 38 | 39 | 40 | ## OpenCollective Backers 41 | 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs-src/.env: -------------------------------------------------------------------------------- 1 | LIT_APP_DIST_FOLDER=dist 2 | LIT_APP_PUBLIC_PATH=/ 3 | LIT_APP_SUPPORTED_LANGS=en 4 | LIT_APP_REPO_URL=https://github.com/stalniy/casl 5 | LIT_APP_TITLE=CASL.js 6 | LIT_APP_CASL_VERSION=vnext 7 | SHARETHIS_SRC=//platform-api.sharethis.com/js/sharethis.js#property=5a81a42dd4d59e0012e89863 8 | -------------------------------------------------------------------------------- /docs-src/.env.production: -------------------------------------------------------------------------------- 1 | LIT_APP_GA_ID=UA-19088556-6 2 | -------------------------------------------------------------------------------- /docs-src/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | !www/favicon.ico 3 | www/ 4 | 5 | *~ 6 | *.sw[mnpcod] 7 | *.log 8 | *.lock 9 | *.tmp 10 | *.tmp.* 11 | log.txt 12 | *.sublime-project 13 | *.sublime-workspace 14 | 15 | .stencil/ 16 | .idea/ 17 | .vscode/ 18 | .sass-cache/ 19 | .versions/ 20 | node_modules/ 21 | $RECYCLE.BIN/ 22 | 23 | .DS_Store 24 | Thumbs.db 25 | UserInterfaceState.xcuserstate 26 | -------------------------------------------------------------------------------- /docs-src/babel.config.js: -------------------------------------------------------------------------------- 1 | const CONFIG = { 2 | default: { 3 | plugins: [ 4 | '@babel/plugin-proposal-class-properties', 5 | ['@babel/plugin-proposal-unicode-property-regex', { useUnicodeFlag: false }], 6 | ], 7 | }, 8 | es5: { 9 | presets: [ 10 | ['@babel/preset-env', { 11 | loose: true, 12 | useBuiltIns: false, 13 | targets: 'defaults', 14 | }] 15 | ], 16 | }, 17 | test: { 18 | presets: [ 19 | ['@babel/preset-env', { 20 | loose: true, 21 | targets: { 22 | node: '10' 23 | } 24 | }] 25 | ], 26 | } 27 | }; 28 | 29 | function config(name) { 30 | if (name === 'default' || !CONFIG[name]) { 31 | return CONFIG.default; 32 | } 33 | 34 | const { presets = [], plugins = [] } = CONFIG[name]; 35 | 36 | return { 37 | presets: presets.concat(CONFIG.default.presets || []), 38 | plugins: plugins.concat(CONFIG.default.plugins || []), 39 | }; 40 | } 41 | 42 | module.exports = (api) => { 43 | let format; 44 | api.caller(caller => format = caller.output || process.env.NODE_ENV); 45 | api.cache.using(() => format); 46 | 47 | return config(format); 48 | }; 49 | -------------------------------------------------------------------------------- /docs-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casl-docs", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "CASL docs", 6 | "author": "Sergii Stotskyi ", 7 | "scripts": { 8 | "build": "rm -rf dist/* && rollup -c rollup.config.js", 9 | "build.watch": "npm run build -- --watch", 10 | "build.sitemap": "node tools/sitemap.xml.js", 11 | "prerender": "node tools/prerender.js", 12 | "lint": "../node_modules/.bin/eslint --fix --ext .js,.ts src", 13 | "serve": "history-server dist" 14 | }, 15 | "dependencies": { 16 | "@curi/router": "^2.1.0", 17 | "@hickory/browser": "^2.1.0", 18 | "@webcomponents/webcomponentsjs": "^2.4.2", 19 | "github-buttons": "^2.7.0", 20 | "lit-element": "^2.2.1", 21 | "lit-html": "^1.2.1", 22 | "lit-translate": "^1.1.22", 23 | "menu-drawer.js": "^1.3.0", 24 | "minisearch": "^3.0.0" 25 | }, 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@babel/core": "^7.8.4", 29 | "@babel/plugin-proposal-class-properties": "^7.8.3", 30 | "@babel/plugin-proposal-unicode-property-regex": "^7.8.8", 31 | "@babel/preset-env": "^7.8.4", 32 | "@rollup/plugin-babel": "^5.0.2", 33 | "@rollup/plugin-commonjs": "^19.0.0", 34 | "@rollup/plugin-html": "^0.2.0", 35 | "@rollup/plugin-node-resolve": "^13.0.0", 36 | "@rollup/plugin-replace": "^2.3.2", 37 | "@rollup/plugin-url": "^6.0.0", 38 | "@rollup/pluginutils": "^4.0.0", 39 | "@sindresorhus/slugify": "1.1.2", 40 | "core-js": "^3.6.4", 41 | "dotenv-flow": "^3.1.0", 42 | "gray-matter": "^4.0.2", 43 | "highlight.js": "^11.0.0", 44 | "history-server": "^1.3.1", 45 | "image-size": "^1.0.0", 46 | "js-yaml": "^4.0.0", 47 | "markdown-it-highlightjs": "^3.1.0", 48 | "markdown-it-include": "^2.0.0", 49 | "puppeteer": "^13.0.0", 50 | "regenerator-runtime": "^0.13.3", 51 | "rollup": "^2.35.1", 52 | "rollup-plugin-content": "0.7.1", 53 | "rollup-plugin-copy": "^3.3.0", 54 | "rollup-plugin-legacy-bundle": "0.2.1", 55 | "rollup-plugin-minify-html-literals": "^1.2.3", 56 | "rollup-plugin-terser": "^7.0.0", 57 | "rollup-plugin-workbox": "^6.0.0", 58 | "xyaml-webpack-loader": "0.7.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs-src/public/app-icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/android-chrome-256x256.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/apple-touch-icon.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/favicon.ico -------------------------------------------------------------------------------- /docs-src/public/app-icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/app-icons/mstile-150x150.png -------------------------------------------------------------------------------- /docs-src/public/app-icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs-src/public/fonts/StardosStencil-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/fonts/StardosStencil-Bold.woff2 -------------------------------------------------------------------------------- /docs-src/public/fonts/StardosStencil-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/public/fonts/StardosStencil-Regular.woff2 -------------------------------------------------------------------------------- /docs-src/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | line-height: 1.5; 5 | color: #202428; 6 | min-width: 320px; 7 | } 8 | 9 | body, 10 | a, 11 | button, 12 | input { 13 | font-size: 16px; 14 | font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 15 | } 16 | 17 | /* latin */ 18 | @font-face { 19 | font-family: 'Stardos Stencil'; 20 | font-style: normal; 21 | font-weight: 400; 22 | font-display: swap; 23 | src: local('Stardos Stencil Regular'), local('StardosStencil-Regular'), url("~@/fonts/StardosStencil-Regular.woff2") format('woff2'); 24 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 25 | } 26 | /* latin */ 27 | @font-face { 28 | font-family: 'Stardos Stencil'; 29 | font-style: normal; 30 | font-weight: 700; 31 | font-display: swap; 32 | src: local('Stardos Stencil Bold'), local('StardosStencil-Bold'), url("~@/fonts/StardosStencil-Bold.woff2") format('woff2'); 33 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 34 | } 35 | -------------------------------------------------------------------------------- /docs-src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CASL", 3 | "short_name": "CASL", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [ 7 | { 8 | "src": "app-icons/android-chrome-192x192.png", 9 | "sizes": "192x192", 10 | "type": "image/png" 11 | }, 12 | { 13 | "src": "app-icons/android-chrome-256x256.png", 14 | "sizes": "256x256", 15 | "type": "image/png" 16 | } 17 | ], 18 | "background_color": "#ffffff", 19 | "theme_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /docs-src/public/versions.txt: -------------------------------------------------------------------------------- 1 | { "number": "v4" } 2 | { "number": "v5" } 3 | { "number": "v6" } 4 | -------------------------------------------------------------------------------- /docs-src/public/web-root/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CASL.js 6 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs-src/public/web-root/google4f1edd737abc76a4.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google4f1edd737abc76a4.html 2 | -------------------------------------------------------------------------------- /docs-src/public/web-root/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Host: casl.js.org 3 | Sitemap: https://casl.js.org/v4/sitemap.xml 4 | Sitemap: https://casl.js.org/v5/sitemap.xml 5 | -------------------------------------------------------------------------------- /docs-src/src/app.js: -------------------------------------------------------------------------------- 1 | import 'core-js/modules/web.immediate'; 2 | import App from './components/App'; 3 | import AppRoot from './components/AppRoot'; 4 | import AppFooter from './components/AppFooter'; 5 | import AppHeader from './components/AppHeader'; 6 | import AppLink from './components/AppLink'; 7 | import AppMenu from './components/AppMenu'; 8 | import ArticleDetails from './components/ArticleDetails'; 9 | import Page from './components/Page'; 10 | import PageNav from './components/PageNav'; 11 | import HomePage from './components/HomePage'; 12 | import GithubButton from './components/GithubButton'; 13 | import QuickSearch from './components/QuickSearch'; 14 | import LangPicker from './components/LangPicker'; 15 | import OneTimeDonations from './components/OneTimeDonations'; 16 | import PagesByCategories from './components/PagesByCategories'; 17 | import AppNotification from './components/AppNotification'; 18 | import MenuDrawer from './components/MenuDrawer'; 19 | import VersionsSelect from './components/VersionsSelect'; 20 | import OldVersionAlert from './components/OldVersionAlert'; 21 | import { locale, setLocale, defaultLocale } from './services/i18n'; 22 | import router from './services/router'; 23 | import { setRouteMeta } from './services/meta'; 24 | 25 | const components = [ 26 | App, 27 | AppRoot, 28 | AppFooter, 29 | AppHeader, 30 | AppLink, 31 | AppMenu, 32 | ArticleDetails, 33 | Page, 34 | HomePage, 35 | PageNav, 36 | QuickSearch, 37 | LangPicker, 38 | GithubButton, 39 | PagesByCategories, 40 | OneTimeDonations, 41 | AppNotification, 42 | MenuDrawer, 43 | VersionsSelect, 44 | OldVersionAlert, 45 | ]; 46 | 47 | export function bootstrap(selector) { 48 | const app = document.querySelector(selector); 49 | components.forEach(c => customElements.define(c.cName, c)); 50 | router.observe(async (route) => { 51 | const lang = route.response.params.lang || defaultLocale; 52 | 53 | if (locale() !== lang) { 54 | await setLocale(lang); 55 | app.ready = true; 56 | } 57 | 58 | setRouteMeta(route); 59 | }); 60 | 61 | return app; 62 | } 63 | -------------------------------------------------------------------------------- /docs-src/src/assets/casl-shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/assets/casl-shield.png -------------------------------------------------------------------------------- /docs-src/src/assets/icons/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/assets/icons/menu.png -------------------------------------------------------------------------------- /docs-src/src/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs-src/src/assets/payment-options/liqpay-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/assets/payment-options/liqpay-qrcode.png -------------------------------------------------------------------------------- /docs-src/src/assets/payment-options/monobank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/assets/payment-options/monobank.png -------------------------------------------------------------------------------- /docs-src/src/assets/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/assets/shield.png -------------------------------------------------------------------------------- /docs-src/src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { bootstrap } from './app'; 2 | import { register } from './serviceWorker'; 3 | 4 | window.__isAppExecuted__ = true; 5 | const app = bootstrap('casl-docs'); 6 | register({ 7 | onUpdate(worker) { 8 | app.notify('updateAvailable', { 9 | onClick() { 10 | worker.postMessage({ type: 'SKIP_WAITING' }); 11 | } 12 | }); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /docs-src/src/components/AppNotification.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { ut } from '../directives/i18n'; 3 | 4 | export default class AppNotification extends LitElement { 5 | static cName = 'app-notification'; 6 | static properties = { 7 | type: { type: String }, 8 | message: { type: String } 9 | }; 10 | 11 | constructor() { 12 | super(); 13 | this.type = 'info'; 14 | this.message = ''; 15 | } 16 | 17 | render() { 18 | return html`${ut(this.message)}`; 19 | } 20 | } 21 | 22 | AppNotification.styles = css` 23 | :host { 24 | display: block; 25 | background: rgb(29, 31, 33); 26 | border-radius: 7px; 27 | padding: 1rem; 28 | color: #fff; 29 | cursor: pointer; 30 | } 31 | 32 | a { 33 | color: inherit; 34 | } 35 | `; 36 | -------------------------------------------------------------------------------- /docs-src/src/components/AppRoot.js: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit-element'; 2 | import gridCss from '../styles/grid'; 3 | 4 | export default class AppRoot extends LitElement { 5 | static cName = 'app-root'; 6 | static properties = { 7 | theme: { type: String }, 8 | layout: { type: String }, 9 | menu: { type: Object }, 10 | }; 11 | 12 | constructor() { 13 | super(); 14 | this.theme = 'default'; 15 | this.menu = null; 16 | this.layout = ''; 17 | } 18 | 19 | render() { 20 | return html` 21 | 22 |
23 | 28 |
29 |
30 | 31 | `; 32 | } 33 | } 34 | 35 | AppRoot.styles = [ 36 | gridCss, 37 | css` 38 | :host { 39 | display: block; 40 | } 41 | 42 | app-header { 43 | position: relative; 44 | position: sticky; 45 | top: 0; 46 | z-index: 10; 47 | background: rgba(255, 255, 255, 0.9); 48 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px 0px; 49 | } 50 | 51 | .row > main, 52 | .col-1 > main { 53 | min-width: 0; 54 | padding-left: 10px; 55 | padding-right: 10px; 56 | } 57 | 58 | .aside, 59 | main { 60 | padding-bottom: 30px; 61 | } 62 | 63 | aside { 64 | display: none; 65 | } 66 | 67 | @media (min-width: 768px) { 68 | .aside { 69 | position: sticky; 70 | top: 54px; 71 | height: calc(100vh - 132px); 72 | overflow-y: auto; 73 | padding-top: 2rem; 74 | } 75 | 76 | .row > aside { 77 | display: block; 78 | flex-basis: 260px; 79 | max-width: 260px; 80 | min-width: 200px; 81 | padding-left: 20px; 82 | box-shadow: rgba(0, 0, 0, 0.1) 1px -1px 2px 0px; 83 | } 84 | 85 | .row > main { 86 | flex-basis: 80%; 87 | margin: 0 auto; 88 | max-width: 800px; 89 | } 90 | } 91 | ` 92 | ]; 93 | -------------------------------------------------------------------------------- /docs-src/src/components/ArticleDetails.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { t, d } from '../directives/i18n'; 3 | 4 | export default class ArticleDetails extends LitElement { 5 | static cName = 'app-article-details'; 6 | static properties = { 7 | article: { type: Object, attribute: false }, 8 | category: { type: String }, 9 | }; 10 | 11 | constructor() { 12 | super(); 13 | this.article = null; 14 | this.category = ''; 15 | } 16 | 17 | render() { 18 | const { article } = this; 19 | const category = this.category || article.categories[0]; 20 | 21 | return html` 22 | 25 | 26 | ${t('article.author')} 27 | 28 | 29 | 30 | 31 | ${article.commentsCount || 0} 32 | 33 | ${t('article.readMore')} 34 | 35 | `; 36 | } 37 | } 38 | 39 | ArticleDetails.styles = [ 40 | css` 41 | :host { 42 | margin-top: 10px; 43 | color: var(--app-article-details-color, #999); 44 | font-size: 11px; 45 | } 46 | 47 | :host > * { 48 | margin-right: 10px; 49 | } 50 | 51 | app-link { 52 | margin-right: 10px; 53 | color: var(--app-link-active-color); 54 | } 55 | 56 | app-link > [class^="icon-"] { 57 | margin-right: 5px; 58 | } 59 | ` 60 | ]; 61 | -------------------------------------------------------------------------------- /docs-src/src/components/GithubButton.js: -------------------------------------------------------------------------------- 1 | import { LitElement } from 'lit-element'; 2 | import { render } from 'github-buttons'; 3 | import config from '../config/app'; 4 | 5 | export default class GithubButton extends LitElement { 6 | static cName = 'github-button'; 7 | static properties = { 8 | href: { type: String }, 9 | size: { type: String }, 10 | theme: { type: String }, 11 | showCount: { type: Boolean }, 12 | text: { type: String } 13 | }; 14 | 15 | constructor() { 16 | super(); 17 | 18 | this.href = config.repoURL; 19 | this.size = undefined; 20 | this.theme = 'light'; 21 | this.showCount = true; 22 | this.text = undefined; 23 | } 24 | 25 | _collectOptions() { 26 | return { 27 | href: this.href, 28 | 'data-size': this.size, 29 | 'data-color-scheme': this.theme, 30 | 'data-show-count': this.showCount, 31 | 'data-text': this.text 32 | }; 33 | } 34 | 35 | update() { 36 | render(this._collectOptions(), (el) => { 37 | if (this.shadowRoot.firstChild) { 38 | this.shadowRoot.replaceChild(el, this.shadowRoot.firstChild); 39 | } else { 40 | this.shadowRoot.appendChild(el); 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs-src/src/components/I18nElement.js: -------------------------------------------------------------------------------- 1 | import { LitElement } from 'lit-element'; 2 | import { listenForLangChanged, locale } from '../services/i18n'; 3 | 4 | export default class I18nElement extends LitElement { 5 | constructor() { 6 | super(); 7 | this._unwatchLang = null; 8 | this._locale = locale(); 9 | } 10 | 11 | connectedCallback() { 12 | super.connectedCallback(); 13 | this._unwatchLang = listenForLangChanged((lang) => { 14 | this._locale = lang; 15 | this.reload().then(() => this.requestUpdate()); 16 | }); 17 | } 18 | 19 | disconnectedCallback() { 20 | this._unwatchLang(); 21 | super.disconnectedCallback(); 22 | } 23 | 24 | reload() { 25 | return Promise.reject(new Error( 26 | `${this.constructor.cName} should implement "reload" method` 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs-src/src/components/LangPicker.js: -------------------------------------------------------------------------------- 1 | import { html, LitElement } from 'lit-element'; 2 | import router from '../services/router'; 3 | 4 | export default class LangPicker extends LitElement { 5 | static cName = 'app-lang-picker'; 6 | 7 | render() { 8 | return html` 9 | 13 | `; 14 | } 15 | 16 | _changeLang(event) { 17 | const lang = event.target.value; 18 | const current = router.current().response; 19 | 20 | router.navigate({ 21 | url: router.url({ 22 | name: current.name, 23 | params: { ...current.params, lang }, 24 | query: current.location.query, 25 | hash: current.location.hash, 26 | }) 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs-src/src/components/OldVersionAlert.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { ut } from '../directives/i18n'; 3 | import { alertCss, mdCss } from '../styles'; 4 | import { getCurrentVersion, genCurrentUrlForVersion, fetchVersions } from '../services/version'; 5 | import router from '../services/router'; 6 | 7 | export default class OldVersionAlert extends LitElement { 8 | static cName = 'old-version-alert'; 9 | 10 | constructor() { 11 | super(); 12 | this._versions = []; 13 | this._currentVersion = getCurrentVersion() || 'unknown'; 14 | } 15 | 16 | async connectedCallback() { 17 | super.connectedCallback(); 18 | this._unwatchRouter = router.observe(() => this.requestUpdate()); 19 | this._versions = await fetchVersions(); 20 | this.requestUpdate(); 21 | } 22 | 23 | disconnectedCallback() { 24 | super.disconnectedCallback(); 25 | 26 | if (this._unwatchRouter) { 27 | this._unwatchRouter(); 28 | } 29 | } 30 | 31 | render() { 32 | const latestVersion = this._versions[this._versions.length - 1]; 33 | 34 | if (!latestVersion || latestVersion.number === this._currentVersion) { 35 | return html``; 36 | } 37 | 38 | return html` 39 |
40 | ${ut('oldVersionAlert', { 41 | latestVersion: latestVersion.number, 42 | currentVersion: this._currentVersion, 43 | latestVersionUrl: genCurrentUrlForVersion(latestVersion.number), 44 | })} 45 |
46 | `; 47 | } 48 | } 49 | 50 | OldVersionAlert.styles = [ 51 | mdCss, 52 | alertCss, 53 | css` 54 | a { 55 | color: inherit; 56 | } 57 | `, 58 | ]; 59 | -------------------------------------------------------------------------------- /docs-src/src/components/OneTimeDonations.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { t } from '../directives/i18n'; 3 | import liqpayIcon from '../assets/payment-options/liqpay.svg'; 4 | import liqpayQrCode from '../assets/payment-options/liqpay-qrcode.png'; 5 | import monoIcon from '../assets/payment-options/monobank.png'; 6 | import monoQrCode from '../assets/payment-options/monobank-qrcode.svg'; 7 | 8 | const PAYMENT_OPTIONS = { 9 | liqpay: { 10 | icon: liqpayIcon, 11 | image: liqpayQrCode 12 | }, 13 | mono: { 14 | icon: monoIcon, 15 | image: monoQrCode, 16 | } 17 | }; 18 | const PAYMENT_NAMES = Object.keys(PAYMENT_OPTIONS); 19 | 20 | function renderPaymentOption(name) { 21 | const option = PAYMENT_OPTIONS[name]; 22 | 23 | if (!option) { 24 | console.warn(`Cannot find configuration for ${name} payment option`); 25 | return null; 26 | } 27 | 28 | let content; 29 | 30 | if (option.image) { 31 | content = html``; 32 | } 33 | 34 | return html`
${content}
`; 35 | } 36 | 37 | export default class OneTimeDonations extends LitElement { 38 | static cName = 'one-time-donations'; 39 | static properties = { 40 | selected: { type: String } 41 | }; 42 | 43 | constructor() { 44 | super(); 45 | this.selected = ''; 46 | } 47 | 48 | connectedCallback() { 49 | super.connectedCallback(); 50 | 51 | if (PAYMENT_NAMES.length === 1) { 52 | this.selected = PAYMENT_NAMES[0]; 53 | } 54 | } 55 | 56 | _setSelected({ target }) { 57 | if (target.tagName !== 'IMG') { 58 | this.selected = ''; 59 | return; 60 | } 61 | 62 | this.selected = target.getAttribute('data-name'); 63 | } 64 | 65 | render() { 66 | return html` 67 |
68 | ${PAYMENT_NAMES.map(name => html` 69 | ${t(`payment.${name}`)} 70 | `)} 71 |
72 | ${this.selected ? renderPaymentOption(this.selected) : ''} 73 | `; 74 | } 75 | } 76 | 77 | OneTimeDonations.styles = css` 78 | .options { 79 | margin: 20px 0; 80 | } 81 | 82 | .options img { 83 | margin: 10px; 84 | cursor: pointer 85 | } 86 | 87 | .selected { 88 | text-align: center; 89 | } 90 | `; 91 | -------------------------------------------------------------------------------- /docs-src/src/components/Page.js: -------------------------------------------------------------------------------- 1 | import { html, css } from 'lit-element'; 2 | import { unsafeHTML } from 'lit-html/directives/unsafe-html'; 3 | import { mdCss, pageCss, codeCss } from '../styles'; 4 | import I18nElement from './I18nElement'; 5 | import { interpolate, locale } from '../services/i18n'; 6 | import content from '../services/content'; 7 | import { setPageMeta } from '../services/meta'; 8 | import { tryToNavigateElement, scrollToSectionIn } from '../hooks/scrollToSection'; 9 | 10 | function renderContent(page, vars) { 11 | return unsafeHTML(interpolate(page.content, vars)); 12 | } 13 | 14 | export default class Page extends I18nElement { 15 | static cName = 'app-page'; 16 | static properties = { 17 | type: { type: String }, 18 | name: { type: String }, 19 | vars: { type: Object, attribute: false }, 20 | content: { type: Function, attribute: false }, 21 | nav: { type: Array }, 22 | _page: { type: Object }, 23 | }; 24 | 25 | constructor() { 26 | super(); 27 | 28 | this._page = null; 29 | this.nav = []; 30 | this.name = null; 31 | this.vars = {}; 32 | this.type = 'page'; 33 | this.content = renderContent; 34 | } 35 | 36 | connectedCallback() { 37 | super.connectedCallback(); 38 | this.shadowRoot.addEventListener('click', (event) => { 39 | tryToNavigateElement(this.shadowRoot, event.target); 40 | }, false); 41 | } 42 | 43 | async updated(changed) { 44 | if (this._page === null || changed.has('name') || changed.has('type')) { 45 | await this.reload(); 46 | } 47 | } 48 | 49 | async reload() { 50 | this._page = await content(this.type).load(locale(), this.name); 51 | setPageMeta(this._page); 52 | await this.updateComplete; 53 | scrollToSectionIn(this.shadowRoot); 54 | } 55 | 56 | _renderNav() { 57 | const [prev, next] = this.nav; 58 | return html` 59 | 60 | `; 61 | } 62 | 63 | render() { 64 | if (!this._page) { 65 | return html``; 66 | } 67 | 68 | return html` 69 |
70 |

${interpolate(this._page.title)}

71 | 72 |
${this.content(this._page, this.vars)}
73 |
74 | ${this.nav && this.nav.length ? this._renderNav() : ''} 75 | `; 76 | } 77 | } 78 | 79 | Page.styles = [ 80 | pageCss, 81 | mdCss, 82 | codeCss, 83 | css` 84 | :host { 85 | display: block; 86 | } 87 | 88 | app-page-nav { 89 | margin-top: 20px; 90 | } 91 | ` 92 | ]; 93 | -------------------------------------------------------------------------------- /docs-src/src/components/PageNav.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | 3 | export default class PageNav extends LitElement { 4 | static cName = 'app-page-nav'; 5 | static properties = { 6 | next: { type: Object }, 7 | prev: { type: Object }, 8 | pageType: { type: String } 9 | }; 10 | 11 | constructor() { 12 | super(); 13 | this.next = null; 14 | this.prev = null; 15 | this.pageType = 'page'; 16 | } 17 | 18 | _linkTo(type) { 19 | const page = this[type]; 20 | 21 | if (!page) { 22 | return ''; 23 | } 24 | 25 | return html` 26 | 27 | ${page.title} 28 | 29 | `; 30 | } 31 | 32 | render() { 33 | return html` 34 | ${this._linkTo('prev')} 35 | ${this._linkTo('next')} 36 | `; 37 | } 38 | } 39 | 40 | PageNav.styles = css` 41 | :host { 42 | display: block; 43 | } 44 | 45 | :host:after { 46 | display: table; 47 | clear: both; 48 | content: ''; 49 | } 50 | 51 | app-link { 52 | color: #81a2be; 53 | text-decoration: none; 54 | } 55 | 56 | app-link:hover { 57 | border-bottom-color: transparent; 58 | } 59 | 60 | .next { 61 | float: right; 62 | margin-left: 30px; 63 | } 64 | 65 | .next:after, 66 | .prev:before { 67 | display: inline-block; 68 | vertical-align: middle; 69 | content: '⇢'; 70 | } 71 | 72 | .prev:before { 73 | content: '⇠'; 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /docs-src/src/components/PagesByCategories.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { t } from '../directives/i18n'; 3 | 4 | export default class PagesByCategories extends LitElement { 5 | static cName = 'pages-by-categories'; 6 | static properties = { 7 | items: { type: Object }, 8 | type: { type: String }, 9 | categories: { type: Array } 10 | }; 11 | 12 | constructor() { 13 | super(); 14 | this.items = null; 15 | this.type = 'page'; 16 | this.categories = null; 17 | } 18 | 19 | render() { 20 | const categories = this.categories || Object.keys(this.items); 21 | return html` 22 | 32 | `; 33 | } 34 | } 35 | 36 | PagesByCategories.styles = css` 37 | :host { 38 | display: block; 39 | } 40 | 41 | ul { 42 | list-style-type: none; 43 | margin: 0; 44 | padding: 0; 45 | } 46 | 47 | nav > h3:first-child { 48 | margin-top: 0; 49 | } 50 | 51 | li { 52 | margin-top: 8px; 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /docs-src/src/components/VersionsSelect.js: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element'; 2 | import { getCurrentVersion, fetchVersions, genCurrentUrlForVersion } from '../services/version'; 3 | 4 | function updateVersion(event) { 5 | window.location.href = genCurrentUrlForVersion(event.target.value); 6 | } 7 | 8 | export default class VersionsSelect extends LitElement { 9 | static cName = 'versions-select'; 10 | 11 | constructor() { 12 | super(); 13 | 14 | this._versions = []; 15 | this._currentVersion = getCurrentVersion(); 16 | 17 | if (this._currentVersion) { 18 | this._versions.push({ number: this._currentVersion }); 19 | } 20 | } 21 | 22 | async connectedCallback() { 23 | super.connectedCallback(); 24 | const versions = await fetchVersions(); 25 | this._versions = versions.slice(0).reverse(); 26 | this.requestUpdate(); 27 | } 28 | 29 | render() { 30 | return html` 31 | 36 | `; 37 | } 38 | } 39 | 40 | VersionsSelect.styles = css` 41 | :host { 42 | display: inline-block; 43 | } 44 | 45 | select { 46 | display: block; 47 | font-size: 16px; 48 | font-weight: 700; 49 | color: rgb(68, 68, 68); 50 | line-height: 1.3; 51 | padding-left: 0.5em; 52 | padding-right: 1.1em; 53 | box-sizing: border-box; 54 | margin: 0; 55 | -moz-appearance: none; 56 | -webkit-appearance: none; 57 | appearance: none; 58 | background-color: transparent; 59 | background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23444444%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); 60 | background-repeat: no-repeat; 61 | /* arrow icon position (1em from the right, 50% vertical) , then gradient position*/ 62 | background-position: right .5em top 50%; 63 | background-size: .5em auto; 64 | border: 0; 65 | cursor: pointer; 66 | } 67 | 68 | /* Hide arrow icon in IE browsers */ 69 | select::-ms-expand { 70 | display: none; 71 | } 72 | 73 | select:focus { 74 | outline: none; 75 | } 76 | `; 77 | -------------------------------------------------------------------------------- /docs-src/src/config/app.js: -------------------------------------------------------------------------------- 1 | export default { 2 | repoURL: process.env.REPO_URL, 3 | baseUrl: process.env.BASE_URL, 4 | }; 5 | -------------------------------------------------------------------------------- /docs-src/src/config/menu.yml: -------------------------------------------------------------------------------- 1 | items: 2 | - name: learn 3 | route: false 4 | children: 5 | - heading: docs 6 | - name: guide 7 | page: guide/intro 8 | - name: api 9 | - name: examples 10 | url: https://github.com/stalniy/casl-examples 11 | - name: cookbook 12 | page: cookbook/intro 13 | - name: ecosystem 14 | route: false 15 | children: 16 | - heading: packages 17 | - name: pkg-prisma 18 | page: package/casl-prisma 19 | - name: pkg-mongoose 20 | page: package/casl-mongoose 21 | - name: pkg-angular 22 | page: package/casl-angular 23 | - name: pkg-react 24 | page: package/casl-react 25 | - name: pkg-vue 26 | page: package/casl-vue 27 | - name: pkg-aurelia 28 | page: package/casl-aurelia 29 | - heading: help 30 | - name: questions 31 | url: https://stackoverflow.com/questions/tagged/casl 32 | - name: chat 33 | url: https://github.com/stalniy/casl/discussions 34 | - heading: news 35 | - name: blog 36 | url: https://sergiy-stotskiy.medium.com 37 | - name: support 38 | 39 | footer: 40 | - icon: github 41 | url: https://github.com/stalniy/casl 42 | - icon: twitter 43 | url: https://twitter.com/sergiy_stotskiy 44 | - icon: medium 45 | url: https://sergiy-stotskiy.medium.com 46 | -------------------------------------------------------------------------------- /docs-src/src/config/routes.yml: -------------------------------------------------------------------------------- 1 | routes: 2 | - name: home 3 | path: :lang 4 | restrictions: 5 | lang: en 6 | controller: Home 7 | sitemap: 8 | priority: 1 9 | changefreq: yearly 10 | provider: langs 11 | children: 12 | - name: api 13 | path: api/:id? 14 | restrictions: 15 | id: '[\w/-]+' 16 | controller: Page 17 | meta: 18 | categories: [api] 19 | ignoreIdPrefix: true 20 | sitemap: 21 | priority: 0.7 22 | changefreq: monthly 23 | provider: pages 24 | - name: support 25 | path: support-casljs 26 | controller: Page 27 | sitemap: 28 | priority: 1 29 | changefreq: yearly 30 | provider: route 31 | - name: page 32 | path: :id 33 | restrictions: 34 | id: '[\w/-]+' 35 | controller: Page 36 | meta: 37 | encode: false 38 | categories: [guide, advanced, package, cookbook] 39 | sitemap: 40 | priority: 1 41 | changefreq: monthly 42 | provider: pages 43 | -------------------------------------------------------------------------------- /docs-src/src/config/search.js: -------------------------------------------------------------------------------- 1 | function extractField(object, fieldName) { 2 | switch (fieldName) { 3 | case 'summary': 4 | return object.meta ? object.meta.description : null; 5 | case 'headings': 6 | return object[fieldName].map(h => h.title).join(' '); 7 | default: 8 | return object[fieldName]; 9 | } 10 | } 11 | 12 | export default { 13 | extractField, 14 | fields: ['title', 'headings', 'summary'], 15 | searchOptions: { 16 | boost: { 17 | title: 2, 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/ability-inheritance/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ability inheritance 3 | categories: [advanced] 4 | order: 27 5 | hidden: true 6 | meta: 7 | keywords: ~ 8 | description: ~ 9 | --- 10 | 11 | TODO 12 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder-conditions-hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder-conditions-hints.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder-fields-hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder-fields-hints.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-abilitybuilder.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-action-hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-action-hints.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-class-subject-with-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-class-subject-with-name.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-class-subject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-class-subject.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-discriminated-class-subject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-discriminated-class-subject.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-subject-hints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-subject-hints.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/advanced/typescript/casl-tagged-union-subject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stalniy/casl/a94cdcbbb06de91082dcd8bd42594758403a8ff6/docs-src/src/content/pages/advanced/typescript/casl-tagged-union-subject.png -------------------------------------------------------------------------------- /docs-src/src/content/pages/cookbook/less-confusing-can-api/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Less confusing can API 3 | categories: [cookbook] 4 | order: 15 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | ## The issue 11 | 12 | CASL uses `can` and `cannot` function names to both define and check permissions. For some of you, it may look confusing and you would like to be more explicit and not rely on the execution context. 13 | 14 | Example: 15 | 16 | ```ts 17 | import { defineAbility } from '@casl/ability'; 18 | 19 | // define abilities 20 | const ability = defineAbility((can, cannot) => { 21 | can('read', 'Post'); 22 | cannot('read', 'Post', { private: true }); 23 | }); 24 | 25 | // check abilities 26 | ability.can('read', 'Post'); 27 | ``` 28 | 29 | **The main disadvantage** is that you need to remember the context and differences between function signatures of [`can` that defines ability](../../guide/intro) and `can` that checks it. 30 | 31 | ## The solution 32 | 33 | What we can do is to use different variables' names, so the example above looks this: 34 | 35 | ```ts 36 | import { defineAbility } from '@casl/ability'; 37 | 38 | // define abilities 39 | const ability = defineAbility((allow, forbid) => { 40 | allow('read', 'Post'); 41 | forbid('read', 'Post', { private: true }); 42 | }); 43 | 44 | // check abilities 45 | ability.can('read', 'Post'); 46 | ``` 47 | 48 | The same example using pure `AbilityBuilder` can be written in similar way using [object-destructuring]: 49 | 50 | ```ts 51 | import { AbilityBuilder, createMongoAbility } from '@casl/ability'; 52 | 53 | // define abilities 54 | const { can: allow, cannot: forbid, build } = new AbilityBuilder(createMongoAbility); 55 | 56 | allow('read', 'Post'); 57 | forbid('read', 'Post', { private: true }); 58 | 59 | const ability = build(); 60 | 61 | // check abilities 62 | ability.can('read', 'Post'); 63 | ``` 64 | 65 | **The main advantage** is that everybody clearly sees that `allow` and `can` are different methods and potentially may have different signatures (because they different!). 66 | 67 | > See [Define rules](../../guide/define-rules) for other ways to define ability. 68 | 69 | ## When to avoid 70 | 71 | This approach really makes it easy to work with CASL for junior developers and those who prefers explicit code over conventions or context. If contextual code doesn't bother you, you can avoid this. 72 | 73 | [object-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assigning_to_new_variable_names 74 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/notfound/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not Found 3 | hidden: true 4 | meta: 5 | keywords: ~ 6 | description: ~ 7 | --- 8 | 9 | Such page does not exist. 10 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-angular/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL Angular 3 | categories: [package] 4 | order: 115 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | @include: packages/casl-angular/README.md -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-aurelia/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL Aurelia 3 | categories: [package] 4 | order: 130 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | @include: packages/casl-aurelia/README.md 11 | 12 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-mongoose/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL Mongoose 3 | categories: [package] 4 | order: 110 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | @include: packages/casl-mongoose/README.md 11 | 12 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-prisma/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL Prisma 3 | categories: [package] 4 | order: 105 5 | meta: 6 | keywords: prisma authorization, permission management, casl 7 | description: > 8 | Prisma authorization using CASL permission management library. 9 | Test permissions in runtime and get accessible record using Prisma Where conditions 10 | --- 11 | 12 | @include: packages/casl-prisma/README.md 13 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-react/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL React 3 | categories: [package] 4 | order: 120 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | @include: packages/casl-react/README.md 11 | -------------------------------------------------------------------------------- /docs-src/src/content/pages/package/casl-vue/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CASL Vue 3 | categories: [package] 4 | order: 125 5 | meta: 6 | keywords: ~ 7 | description: ~ 8 | --- 9 | 10 | @include: packages/casl-vue/README.md -------------------------------------------------------------------------------- /docs-src/src/content/pages/support-casljs/en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Support CASL.js Development 3 | categories: [support] 4 | hidden: true 5 | meta: 6 | keywords: casl.js, financial support, opencollective, funding, recurring pledges 7 | description: Isomorphic Authorization JavaScript library 8 | --- 9 | 10 | CASL.js is an MIT licensed open source project and completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. You can support CASL.js development via the following methods: 11 | 12 | ## One-time Donations 13 | 14 | We accept donations through these channels: 15 | 16 | 17 | 18 | ## Recurring Pledges 19 | 20 | Recurring pledges come with exclusive perks, e.g. having your name listed in the CASL GitHub repository, have your company logo placed on this website, personal consultation or project support through live chat or screen-sharing. 21 | 22 | Get more details about different options to 23 | * [become a backer or sponsor via OpenCollective](https://opencollective.com/casljs) 24 | * [become a backer or sponsor via Patreon](https://patreon.com/sstotskyi) 25 | 26 | ## Why is it important for you? 27 | 28 | If you run a business and are using CASL.js in a revenue-generating product, it makes business sense to sponsor CASL.js development: it ensures the project that your product relies on stays healthy and actively maintained. It can also help your exposure in the JavaScript community and makes it easier to attract developers. 29 | 30 | If you are an individual user and have enjoyed the productivity of using CASL, consider donating as a sign of appreciation - like [buying me coffee](https://opencollective.com/casljs/donate/details) once in a while :) 31 | -------------------------------------------------------------------------------- /docs-src/src/directives/i18n.js: -------------------------------------------------------------------------------- 1 | import { directive } from 'lit-html'; 2 | import { unsafeHTML } from 'lit-html/directives/unsafe-html'; 3 | import { langChanged, get, translate } from 'lit-translate'; 4 | import { d as formatDate } from '../services/i18n'; 5 | 6 | export const d = directive((date, format) => (part) => { 7 | const value = formatDate(date, format); 8 | 9 | if (part.value !== value) { 10 | part.setValue(value); 11 | } 12 | }); 13 | 14 | export const t = translate; 15 | 16 | export const ut = directive((key, values) => langChanged(() => unsafeHTML(get(key, values)))); 17 | -------------------------------------------------------------------------------- /docs-src/src/hooks/scrollToSection.js: -------------------------------------------------------------------------------- 1 | import router from '../services/router'; 2 | 3 | function scrollToElement(root, id) { 4 | const element = root.getElementById(id); 5 | 6 | if (!element) { 7 | return; 8 | } 9 | 10 | const headerHeight = 85; 11 | element.scrollIntoView(true); 12 | document.documentElement.scrollTop -= headerHeight; 13 | } 14 | 15 | function closest(startNode, tagName) { 16 | let current = startNode; 17 | const maxIterations = 3; 18 | let i = 0; 19 | 20 | while (current && i < maxIterations) { 21 | if (current.tagName === tagName) { 22 | return current; 23 | } 24 | 25 | current = current.parentNode; 26 | i++; 27 | } 28 | 29 | return null; 30 | } 31 | 32 | export function tryToNavigateElement(root, target) { 33 | let hash; 34 | 35 | if (target.tagName[0] === 'H' && target.id) { 36 | hash = target.id; 37 | } else { 38 | const clickedTarget = closest(target, 'A'); 39 | const hashIndex = clickedTarget ? clickedTarget.href.indexOf('#') : -1; 40 | 41 | if (hashIndex !== -1) { 42 | scrollToElement(root, clickedTarget.href.slice(hashIndex + 1)); 43 | } 44 | } 45 | 46 | if (hash) { 47 | const { location } = router.current().response; 48 | const url = `${location.pathname}${window.location.search}#${hash}`; 49 | router.navigate({ url }); 50 | scrollToElement(root, hash); 51 | } 52 | } 53 | 54 | export function scrollToSectionIn(root) { 55 | const { hash } = router.current().response.location; 56 | 57 | if (hash) { 58 | scrollToElement(root, hash); 59 | } else { 60 | window.scroll(0, 0); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs-src/src/hooks/watchMedia.js: -------------------------------------------------------------------------------- 1 | const viewport = window.visualViewport || window; 2 | 3 | export default function watchMedia(query, onUpdate) { 4 | const ql = window.matchMedia(query); 5 | const notify = () => onUpdate(ql.matches); 6 | 7 | viewport.addEventListener('resize', notify); 8 | notify(); 9 | 10 | return () => viewport.removeEventListener('resize', notify); 11 | } 12 | -------------------------------------------------------------------------------- /docs-src/src/partials/caslFeatures.js: -------------------------------------------------------------------------------- 1 | import { html, css, unsafeCSS } from 'lit-element'; 2 | import { t, ut } from '../directives/i18n'; 3 | 4 | const features = [ 5 | 'isomorphic', 6 | 'versatile', 7 | 'declarative', 8 | 'typesafe', 9 | 'treeshakable' 10 | ]; 11 | 12 | function renderFeature(feature) { 13 | return html` 14 |
15 |

${t(`features.${feature}.title`)}

16 |

${ut(`features.${feature}.description`)}

17 |
18 | `; 19 | } 20 | 21 | const template = () => html` 22 |
${features.map(renderFeature)}
23 | `; 24 | 25 | template.styles = [ 26 | css` 27 | .features { 28 | padding: 1rem 0; 29 | display: -ms-grid; 30 | display: grid; 31 | justify-content: center; 32 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 33 | -ms-grid-columns: ${unsafeCSS(features.map(() => 'minmax(200px, 1fr)').join(' '))}; 34 | } 35 | 36 | .feature { 37 | padding: 1rem; 38 | } 39 | 40 | .feature h3 { 41 | font-size: 1.4rem; 42 | } 43 | 44 | .feature p:last-child { 45 | margin-bottom: 0; 46 | } 47 | ` 48 | ]; 49 | 50 | export default template; 51 | -------------------------------------------------------------------------------- /docs-src/src/services/content.js: -------------------------------------------------------------------------------- 1 | import ContentType from './ContentType'; 2 | import * as pagesDetails from '../content/pages.pages'; 3 | 4 | const contentTypes = { 5 | page: new ContentType(pagesDetails), 6 | }; 7 | 8 | export default (type) => { 9 | const contentLoader = contentTypes[type]; 10 | 11 | if (!contentLoader) { 12 | throw new TypeError(`Unknown content loader "${type}".`); 13 | } 14 | 15 | return contentLoader; 16 | }; 17 | -------------------------------------------------------------------------------- /docs-src/src/services/error.js: -------------------------------------------------------------------------------- 1 | export function notFoundError(message) { 2 | return Object.assign(new Error(message), { 3 | code: 'NOT_FOUND' 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /docs-src/src/services/http.js: -------------------------------------------------------------------------------- 1 | import config from '../config/app'; 2 | 3 | const identity = x => x; 4 | const FORMATS = { 5 | json: JSON, 6 | raw: { 7 | parse: identity, 8 | stringify: identity, 9 | }, 10 | txtArrayJSON: { 11 | parse(value) { 12 | const values = value.trim().replace(/[\r\n]+/g, ','); 13 | return JSON.parse(`[${values}]`); 14 | }, 15 | stringify() { 16 | throw new Error('"txtArrayJSON" format is not serializable'); 17 | }, 18 | }, 19 | }; 20 | 21 | function http(url, options = {}) { 22 | const format = FORMATS[options.format || 'json']; 23 | 24 | return new Promise((resolve, reject) => { 25 | const xhr = new XMLHttpRequest(); 26 | 27 | xhr.open(options.method || 'GET', url); 28 | 29 | if (options.headers) { 30 | Object.keys(options.headers).forEach((name) => { 31 | xhr.setRequestHeader(name, options.headers[name]); 32 | }); 33 | } 34 | 35 | xhr.onload = () => resolve({ 36 | status: xhr.status, 37 | headers: { 38 | 'content-type': xhr.getResponseHeader('Content-Type'), 39 | }, 40 | body: format.parse(xhr.responseText), 41 | }); 42 | xhr.ontimeout = xhr.onerror = reject; // eslint-disable-line no-multi-assign 43 | xhr.send(options.data ? format.stringify(options.data) : null); 44 | }); 45 | } 46 | 47 | const inflightRequests = Object.create(null); 48 | export function fetch(rawUrl, options = {}) { 49 | const url = options.absoluteUrl ? rawUrl : config.baseUrl + rawUrl; 50 | const method = options.method || 'GET'; 51 | 52 | if (method !== 'GET') { 53 | return http(url, options); 54 | } 55 | 56 | inflightRequests[url] = inflightRequests[url] || http(url, options); 57 | 58 | if (options.cache === true) { 59 | return inflightRequests[url]; 60 | } 61 | 62 | return inflightRequests[url] 63 | .then((response) => { 64 | delete inflightRequests[url]; 65 | return response; 66 | }) 67 | .catch((error) => { 68 | delete inflightRequests[url]; 69 | return Promise.reject(error); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /docs-src/src/services/i18n.js: -------------------------------------------------------------------------------- 1 | import { registerTranslateConfig, use, get, listenForLangChanged } from 'lit-translate'; 2 | import { memoize } from './utils'; 3 | import { fetch } from './http'; 4 | import { pages as langUrls } from '../content/app.i18n'; 5 | 6 | function lookup(path, config) { 7 | const keys = path.split('.'); 8 | let pointer = config.strings; 9 | 10 | for (let i = 0; i < keys.length; i++) { 11 | const key = keys[i]; 12 | 13 | if (!pointer || !pointer[key]) { 14 | return undefined; 15 | } 16 | 17 | pointer = pointer[key]; 18 | } 19 | 20 | return pointer; 21 | } 22 | 23 | function missingKey(path) { 24 | console.warn(`missing i18n key: ${path}`); // eslint-disable-line no-console 25 | return path; 26 | } 27 | 28 | const VAR_REGEXP = /%\{(\w+)\}/g; 29 | 30 | export function interpolate(string, vars) { 31 | return string.replace(VAR_REGEXP, (_, varName) => vars[varName]); 32 | } 33 | 34 | const i18n = registerTranslateConfig({ 35 | async loader(lang) { 36 | const response = await fetch(langUrls[lang].default); 37 | return response.body; 38 | }, 39 | lookup, 40 | interpolate, 41 | empty: missingKey, 42 | }); 43 | 44 | const dateTime = memoize((locale, format) => new Intl.DateTimeFormat(locale, get(`dateTimeFormats.${format}`))); 45 | 46 | export const LOCALES = process.env.SUPPORTED_LANGS; 47 | 48 | export const defaultLocale = LOCALES[0]; 49 | 50 | export const locale = () => i18n.lang; 51 | 52 | export const t = get; 53 | 54 | export const translationExists = key => !!lookup(key, i18n); 55 | 56 | export function setLocale(lang) { 57 | if (!LOCALES.includes(lang)) { 58 | throw new Error(`Locale ${lang} is not supported. Supported: ${LOCALES.join(', ')}`); 59 | } 60 | 61 | return use(lang); 62 | } 63 | 64 | export function d(date, format = 'default') { 65 | const actualDate = typeof date === 'string' ? new Date(date) : date; 66 | return dateTime(i18n.lang, format).format(actualDate); 67 | } 68 | 69 | export { 70 | listenForLangChanged 71 | }; 72 | -------------------------------------------------------------------------------- /docs-src/src/services/meta.js: -------------------------------------------------------------------------------- 1 | import { t, translationExists } from './i18n'; 2 | 3 | export function setTitle(title) { 4 | const prefix = title ? `${title} - ` : ''; 5 | document.title = prefix + t('name'); 6 | } 7 | 8 | function getMetaTag(name) { 9 | let meta = document.head.querySelector(`meta[name="${name}"]`); 10 | 11 | if (!meta) { 12 | meta = document.createElement('meta'); 13 | meta.setAttribute('name', name); 14 | document.head.appendChild(meta); 15 | } 16 | 17 | return meta; 18 | } 19 | 20 | export function setMeta(name, content) { 21 | if (typeof name === 'object') { 22 | Object.keys(name).forEach(key => setMeta(key, name[key])); 23 | return; 24 | } 25 | 26 | const defaultValue = t(`meta.${name}`); 27 | const value = Array.isArray(content) 28 | ? content.concat(defaultValue).join(', ') 29 | : content || defaultValue; 30 | 31 | getMetaTag(name).setAttribute('content', value.replace(/[\n\r]+/g, ' ')); 32 | } 33 | 34 | export function setRouteMeta({ response }) { 35 | const html = document.documentElement; 36 | 37 | if (html.lang !== response.params.lang) { 38 | html.lang = response.params.lang; 39 | } 40 | 41 | const prefix = `meta.${response.name}`; 42 | 43 | if (translationExists(prefix)) { 44 | setTitle(t(`${prefix}.title`)); 45 | setMeta('keywords', t(`${prefix}.keywords`)); 46 | setMeta('description', t(`${prefix}.description`)); 47 | } else { 48 | setTitle(); 49 | setMeta('keywords'); 50 | setMeta('description'); 51 | } 52 | } 53 | 54 | export function setPageMeta(page) { 55 | const meta = page.meta || {}; 56 | 57 | setTitle(page.title); 58 | setMeta('keywords', meta.keywords || ''); 59 | setMeta('description', meta.description || ''); 60 | } 61 | -------------------------------------------------------------------------------- /docs-src/src/services/pageController.js: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-element'; 2 | import content from './content'; 3 | 4 | function respondWithError(error) { 5 | if (error.code === 'NOT_FOUND') { 6 | return { 7 | body: html`` 8 | }; 9 | } 10 | 11 | throw error; 12 | } 13 | 14 | async function loadFirstPage(loader, params) { 15 | if (!params.categories) { 16 | return loader.at(params.lang, 0); 17 | } 18 | 19 | const byCategories = await loader.byCategories(params.lang, params.categories); 20 | const firstCategory = params.categories.find(category => byCategories[category].length); 21 | 22 | return byCategories[firstCategory][0]; 23 | } 24 | 25 | const identity = x => x; 26 | export const loadPages = (transformParams = identity) => async (match) => { 27 | const vars = transformParams(match); 28 | const loader = content('page'); 29 | 30 | if (!vars.id) { 31 | const firstPage = await loadFirstPage(loader, vars); 32 | vars.redirectTo = firstPage.id; 33 | } else if (vars.id.endsWith('/')) { 34 | vars.redirectTo = vars.id.slice(0, -1); 35 | } else { 36 | [vars.page, vars.byCategories, vars.nav] = await Promise.all([ 37 | loader.load(vars.lang, vars.id), 38 | vars.categories.length ? loader.byCategories(vars.lang, vars.categories) : null, 39 | loader.getNearestFor(vars.lang, vars.id, vars.categories) 40 | ]); 41 | } 42 | 43 | return vars; 44 | }; 45 | 46 | export const respondWithPage = render => ({ match, error, resolved }) => { 47 | if (error) { 48 | return respondWithError(error); 49 | } if (resolved.redirectTo) { 50 | return { 51 | redirect: { 52 | name: 'page', 53 | params: { 54 | id: resolved.redirectTo, 55 | lang: match.params.lang, 56 | } 57 | } 58 | }; 59 | } 60 | 61 | return { body: render(resolved, match.params) }; 62 | }; 63 | 64 | export const renderPage = respondWithPage(vars => ({ 65 | main: html` 66 | 67 | `, 68 | sidebar: vars.byCategories 69 | ? html`` 70 | : null 71 | })); 72 | -------------------------------------------------------------------------------- /docs-src/src/services/querystring.js: -------------------------------------------------------------------------------- 1 | export function parse(querystring) { 2 | return querystring 3 | ? JSON.parse(`{"${querystring.replace(/&/g, '","').replace(/=/g, '":"')}"}`) 4 | : {}; 5 | } 6 | 7 | export function stringify(querystring) { 8 | if (!querystring) { 9 | return ''; 10 | } 11 | 12 | return Object.keys(querystring) 13 | .reduce((qs, key) => { 14 | qs.push(`${key}=${querystring[key]}`); 15 | return qs; 16 | }, []) 17 | .join('&'); 18 | } 19 | -------------------------------------------------------------------------------- /docs-src/src/services/utils.js: -------------------------------------------------------------------------------- 1 | const json = (...args) => JSON.stringify(args); 2 | 3 | export function memoize(fn, generateKey = json) { 4 | const cache = new Map(); 5 | const memoized = function (...args) { 6 | const key = generateKey(...args); 7 | 8 | if (!cache.has(key)) { 9 | cache.set(key, fn.apply(this, args)); 10 | } 11 | 12 | return cache.get(key); 13 | }; 14 | memoized.cache = cache; 15 | 16 | return memoized; 17 | } 18 | 19 | export function debounce(fn, timeout) { 20 | let timerId; 21 | return function (...args) { 22 | clearTimeout(timerId); 23 | timerId = setTimeout(() => fn.apply(this, args), timeout); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /docs-src/src/services/version.js: -------------------------------------------------------------------------------- 1 | import { fetch } from './http'; 2 | 3 | export function getCurrentVersion() { 4 | return process.env.CASL_VERSION || null; 5 | } 6 | 7 | export async function fetchVersions() { 8 | const response = await fetch('/versions.txt', { format: 'txtArrayJSON', cache: true }); 9 | return response.body; 10 | } 11 | 12 | export function genCurrentUrlForVersion(version) { 13 | return window.location.href 14 | .replace(`/${getCurrentVersion()}/`, `/${version}/`); 15 | } 16 | -------------------------------------------------------------------------------- /docs-src/src/styles/alert.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | /* other alert styles is in md.js */ 5 | 6 | .alert-warning { 7 | border-left-color: #856404; 8 | background-color: #fff3cd; 9 | } 10 | 11 | .alert-warning:before { 12 | background: #856404; 13 | content: 'w'; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /docs-src/src/styles/btn.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | .btn { 5 | display: inline-block; 6 | outline: 0; 7 | text-decoration: none; 8 | background-color: transparent; 9 | border: 1px solid #877e87; 10 | border-radius: 1rem; 11 | padding: .375rem 1.5rem; 12 | font-weight: 700; 13 | appearance: none; 14 | -webkit-appearance: none; 15 | -moz-appearance: none; 16 | transition: 17 | color .2s cubic-bezier(.08,.52,.52,1), 18 | background .2s cubic-bezier(.08,.52,.52,1), 19 | border-color .2s cubic-bezier(.08,.52,.52,1); 20 | cursor: pointer; 21 | color: #444; 22 | } 23 | 24 | .btn:hover { 25 | background-color: #202428; 26 | border-color: #202428; 27 | color: #fff; 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /docs-src/src/styles/code.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | .hljs, 5 | code[data-filename] { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 1rem; 9 | background: #1d1f21; 10 | border-radius: 7px; 11 | box-shadow: rgba(0, 0, 0, 0.55) 0px 11px 11px 0px; 12 | font-size: 0.8rem; 13 | color: #c5c8c6; 14 | } 15 | 16 | .hljs::selection, 17 | .hljs span::selection, 18 | code[data-filename]::selection, 19 | code[data-filename] span::selection { 20 | background: #373b41; 21 | } 22 | 23 | code[data-filename] { 24 | position: relative; 25 | padding-top: 22px; 26 | } 27 | 28 | code[data-filename]:before { 29 | position: absolute; 30 | top: 0; 31 | right: 0; 32 | font-size: 0.7rem; 33 | content: attr(data-filename); 34 | padding: 2px 6px; 35 | border-radius: 0 0 0 7px; 36 | border-left: 1px solid #c5c8c6; 37 | border-bottom: 1px solid #c5c8c6; 38 | color: #fff; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-name { 43 | color: #f0c674; 44 | } 45 | 46 | .hljs-comment { 47 | color: #707880; 48 | } 49 | 50 | .hljs-meta, 51 | .hljs-meta .hljs-keyword { 52 | color: #f0c674; 53 | } 54 | 55 | .hljs-number, 56 | .hljs-symbol, 57 | .hljs-literal, 58 | .hljs-deletion, 59 | .hljs-link { 60 | color: #cc6666 61 | } 62 | 63 | .hljs-string, 64 | .hljs-doctag, 65 | .hljs-addition, 66 | .hljs-regexp, 67 | .hljs-selector-attr, 68 | .hljs-selector-pseudo { 69 | color: #b5bd68; 70 | } 71 | 72 | .hljs-attribute, 73 | .hljs-code, 74 | .hljs-selector-id { 75 | color: #b294bb; 76 | } 77 | 78 | .hljs-keyword, 79 | .hljs-selector-tag, 80 | .hljs-bullet, 81 | .hljs-tag { 82 | color: #81a2be; 83 | } 84 | 85 | .hljs-subst, 86 | .hljs-variable, 87 | .hljs-template-tag, 88 | .hljs-template-variable { 89 | color: #8abeb7; 90 | } 91 | 92 | .hljs-type, 93 | .hljs-built_in, 94 | .hljs-builtin-name, 95 | .hljs-quote, 96 | .hljs-section, 97 | .hljs-selector-class { 98 | color: #de935f; 99 | } 100 | 101 | .hljs-emphasis { 102 | font-style: italic; 103 | } 104 | 105 | .hljs-strong { 106 | font-weight: bold; 107 | } 108 | 109 | @media (min-width: 768px) { 110 | code[data-filename] { 111 | padding-top: 1rem; 112 | } 113 | 114 | code[data-filename]:before { 115 | font-size: inherit; 116 | opacity: 0.5; 117 | transition: opacity .5s; 118 | } 119 | 120 | code[data-filename]:hover:before { 121 | opacity: 1; 122 | } 123 | } 124 | `; 125 | -------------------------------------------------------------------------------- /docs-src/src/styles/grid.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | .row { 5 | display: flex; 6 | } 7 | 8 | .row.wrap { 9 | flex-wrap: wrap; 10 | } 11 | 12 | .row.align-center { 13 | align-items: center; 14 | } 15 | 16 | .row.align-start { 17 | align-items: start; 18 | } 19 | 20 | .col { 21 | flex-grow: 1; 22 | flex-basis: 0; 23 | max-width: 100%; 24 | } 25 | 26 | .col-fixed { 27 | flex-grow: 0; 28 | flex-basis: auto; 29 | } 30 | 31 | @media (min-width: 768px) { 32 | .container { 33 | margin: auto; 34 | max-width: 1200px; 35 | } 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /docs-src/src/styles/index.js: -------------------------------------------------------------------------------- 1 | export { default as gridCss } from './grid'; 2 | export { default as mdCss } from './md'; 3 | export { default as pageCss } from './page'; 4 | export { default as btnCss } from './btn'; 5 | export { default as codeCss } from './code'; 6 | export { default as alertCss } from './alert'; 7 | -------------------------------------------------------------------------------- /docs-src/src/styles/md.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | .md pre { 5 | overflow: auto; 6 | } 7 | 8 | .md a, 9 | .md app-link { 10 | color: #81a2be; 11 | text-decoration: underline; 12 | border-bottom: 0; 13 | } 14 | 15 | .md a:hover, 16 | .md app-link:hover { 17 | text-decoration: none; 18 | border-bottom: 0; 19 | } 20 | 21 | .md code:not([class]) { 22 | color: rgb(222, 147, 95);; 23 | background: #f8f8f8; 24 | padding: 2px 5px; 25 | margin: 0 2px; 26 | border-radius: 2px; 27 | white-space: nowrap; 28 | font-family: "Roboto Mono", Monaco, courier, monospace; 29 | } 30 | 31 | .md blockquote, 32 | .alert { 33 | padding: 0.8rem 1rem; 34 | margin: 0; 35 | border-left: 4px solid #81a2be; 36 | background-color: #f8f8f8; 37 | position: relative; 38 | border-bottom-right-radius: 2px; 39 | border-top-right-radius: 2px; 40 | } 41 | 42 | .md blockquote:before, 43 | .alert:before { 44 | position: absolute; 45 | top: 0.8rem; 46 | left: -12px; 47 | color: #fff; 48 | background: #81a2be; 49 | width: 20px; 50 | height: 20px; 51 | border-radius: 100%; 52 | text-align: center; 53 | line-height: 20px; 54 | font-weight: bold; 55 | font-size: 14px; 56 | content: 'i'; 57 | } 58 | 59 | .md blockquote > p:first-child, 60 | .alert > p:first-child { 61 | margin-top: 0; 62 | } 63 | 64 | .md blockquote > p:last-child, 65 | .alert > p:last-child { 66 | margin-bottom: 0; 67 | } 68 | 69 | .md blockquote + blockquote, 70 | .alert + .alert { 71 | margin-top: 20px; 72 | } 73 | 74 | .md table { 75 | border-collapse: collapse; 76 | width: 100%; 77 | } 78 | 79 | .md .responsive { 80 | width: 100%; 81 | overflow-x: auto; 82 | } 83 | 84 | .md th, 85 | .md td { 86 | border: 1px solid #c6cbd1; 87 | padding: 6px 13px; 88 | } 89 | 90 | .md tr { 91 | border-top: 1px solid #c6cbd1; 92 | } 93 | 94 | .md .editor { 95 | width: 100%; 96 | height: 500px; 97 | border: 0; 98 | border-radius: 4px; 99 | overflow: hidden; 100 | } 101 | 102 | .md h3::before { 103 | margin-left: -15px; 104 | margin-right: 5px; 105 | content: '#'; 106 | color: #81a2be; 107 | } 108 | `; 109 | -------------------------------------------------------------------------------- /docs-src/src/styles/page.js: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element'; 2 | 3 | export default css` 4 | h1 { 5 | margin: 2rem 0 1rem; 6 | font-size: 2rem; 7 | } 8 | 9 | h2 { 10 | padding-bottom: 0.3rem; 11 | border-bottom: 1px solid #ddd; 12 | } 13 | 14 | h1, h2, h3, h4, h5 { 15 | font-weight: normal; 16 | cursor: pointer; 17 | } 18 | 19 | .description { 20 | margin-top: 10px; 21 | color: #333; 22 | padding-left: 5px; 23 | } 24 | 25 | .description img { 26 | max-width: 100%; 27 | height: auto; 28 | } 29 | 30 | .description > h1 { 31 | display: none; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /docs-src/tools/SearchIndex.js: -------------------------------------------------------------------------------- 1 | import MiniSearch from 'minisearch'; 2 | import fs from 'fs'; 3 | import searchOptions from '../src/config/search'; 4 | 5 | function createIndexFor(lang) { 6 | const stopWordsPath = `${__dirname}/tools/stop-words/${lang}.txt`; 7 | const stopWords = new Set(fs.readFileSync(stopWordsPath, 'utf8').trim().split('\n')); 8 | 9 | return new MiniSearch({ 10 | ...searchOptions, 11 | processTerm(rawTerm) { 12 | const term = rawTerm.toLowerCase(); 13 | 14 | if (stopWords.has(term)) { 15 | return null; 16 | } 17 | 18 | return term; 19 | }, 20 | }); 21 | } 22 | 23 | export class SearchIndex { 24 | static factory(options) { 25 | return () => new SearchIndex(options); 26 | } 27 | 28 | constructor() { 29 | this._indexes = {}; 30 | this.exportAs = 'searchIndexes'; 31 | } 32 | 33 | add(item, { lang }) { 34 | if (item.hidden) { 35 | return; 36 | } 37 | 38 | this._indexes[lang] = this._indexes[lang] || createIndexFor(lang); 39 | this._indexes[lang].add(item); 40 | } 41 | 42 | toJSON() { 43 | return Object.keys(this._indexes).reduce((results, lang) => { 44 | results[lang] = this._indexes[lang].toJSON(); 45 | return results; 46 | }, {}); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs-src/tools/appEnvVars.js: -------------------------------------------------------------------------------- 1 | export default function getAppEnvVars(env, prefix = 'LIT_APP_') { 2 | return Object.keys(env).reduce((appEnvs, key) => { 3 | if (key.startsWith(prefix)) { 4 | appEnvs[`process.env.${key.slice(prefix.length)}`] = JSON.stringify(env[key]); 5 | } 6 | return appEnvs; 7 | }, {}); 8 | } 9 | -------------------------------------------------------------------------------- /docs-src/tools/contentParser.js: -------------------------------------------------------------------------------- 1 | import { parse, getOrCreateMdParser } from 'xyaml-webpack-loader/parser'; 2 | import matter from 'gray-matter'; 3 | import slugify from '@sindresorhus/slugify'; 4 | 5 | export const markdownOptions = { 6 | use: { 7 | 'markdown-it-highlightjs': {}, 8 | 'markdown-it-headinganchor': { 9 | anchorClass: 'h-link', 10 | slugify, 11 | }, 12 | 'markdown-it-include': { 13 | root: `${__dirname}/..`, 14 | bracesAreOptional: true, 15 | includeRe: /@include:(\s+[\w._/-]+)/ 16 | }, 17 | [`${__dirname}/tools/mdLink`]: { 18 | external: { 19 | target: '_blank', 20 | rel: 'noopener nofollow' 21 | }, 22 | local: { 23 | tagName: 'app-link' 24 | } 25 | }, 26 | [`${__dirname}/tools/mdImage`]: { 27 | size: 'auto', 28 | srcRoot: `${process.env.LIT_APP_PUBLIC_PATH || ''}/images` 29 | }, 30 | [`${__dirname}/tools/mdTableContainer`]: {} 31 | } 32 | }; 33 | 34 | const xyamlOptions = { markdown: markdownOptions }; 35 | 36 | export const parsexYaml = content => parse(content, xyamlOptions); 37 | 38 | const grayMatterOptions = { 39 | language: 'xyaml', 40 | engines: { xyaml: parsexYaml } 41 | }; 42 | 43 | export function parseFrontMatter(source, context) { 44 | const file = matter(source, grayMatterOptions); 45 | const parser = getOrCreateMdParser(markdownOptions); 46 | const content = parser.render(file.content, context).trim(); 47 | const headings = content.match(/]*>(?:.+?)<\/h\1>/g) || []; 48 | 49 | return { 50 | ...file.data, 51 | content, 52 | headings: headings.map((h) => { 53 | const idIndex = h.indexOf(' id="'); 54 | const id = idIndex === -1 ? null : h.slice(idIndex + 5, h.indexOf('"', idIndex + 6)); 55 | 56 | return { 57 | id, 58 | title: h.replace(/<[^>]+>/g, ' ').trim(), 59 | }; 60 | }), 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /docs-src/tools/mdImage.js: -------------------------------------------------------------------------------- 1 | const { basename, dirname } = require('path'); 2 | const getImageSize = require('image-size'); 3 | 4 | function replaceLocalSrcWithUrl(src, options) { 5 | const root = options.srcRoot || '/images'; 6 | return `${root}/${basename(src)}`; 7 | } 8 | 9 | module.exports = function image(md, options = {}) { 10 | const normalizeSrc = options.normalizeSrc || replaceLocalSrcWithUrl; 11 | const renderImage = md.renderer.rules.image; 12 | 13 | md.renderer.rules.image = (tokens, idx, params, env, self) => { 14 | const token = tokens[idx]; 15 | const srcIndex = token.attrIndex('src'); 16 | const srcAttr = token.attrs[srcIndex]; 17 | 18 | if (srcAttr && srcAttr[1][0] === '.') { 19 | if (options.size === 'auto' && env.file) { 20 | const imagePath = `${dirname(env.file.path)}/${srcAttr[1]}`; 21 | const size = getImageSize(imagePath); 22 | 23 | token.attrSet('width', size.width); 24 | token.attrSet('height', size.height); 25 | } 26 | 27 | srcAttr[1] = normalizeSrc(srcAttr[1], options); 28 | } 29 | 30 | return renderImage(tokens, idx, params, env, self); 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /docs-src/tools/mdLink.js: -------------------------------------------------------------------------------- 1 | const { normalize: normalizePath, dirname } = require('path'); 2 | const fs = require('fs'); 3 | 4 | function isExternalUrl(url) { 5 | return url.startsWith('https://') || url.startsWith('http://'); 6 | } 7 | 8 | function isLocalUrl(url) { 9 | return url.startsWith('/') || url.startsWith('../') || url.startsWith('./'); 10 | } 11 | 12 | function buildInAppLinkAttrs(token, ctx) { 13 | const attrs = token.attrs.slice(0); 14 | 15 | attrs[ctx.hrefIndex][0] = 'to'; 16 | attrs[ctx.hrefIndex][1] = 'page'; 17 | attrs.push(['params', JSON.stringify({ id: ctx.page })]); 18 | 19 | if (ctx.hash) { 20 | attrs.push(['hash', ctx.hash]); 21 | } 22 | 23 | return attrs; 24 | } 25 | 26 | function toCustomLink(token, hrefIndex, options, env) { 27 | let page = token.attrs[hrefIndex][1]; 28 | const hashIndex = page.indexOf('#'); 29 | let hash = null; 30 | 31 | if (hashIndex !== -1) { 32 | hash = page.slice(hashIndex + 1); 33 | page = page.slice(0, hashIndex); 34 | } 35 | 36 | if (page.startsWith('/')) { 37 | page = page.slice(1); 38 | } else { 39 | const filePath = `${dirname(env.file.path)}/${page}`; 40 | 41 | if (!fs.existsSync(filePath)) { 42 | console.warn(`Unable to locate file by path ${page} in ${env.relativePath}`); 43 | } 44 | 45 | page = normalizePath(`${dirname(env.relativePath)}/${page}`); 46 | } 47 | 48 | const attrs = options.attrs || buildInAppLinkAttrs; 49 | 50 | return Object.create(token, { 51 | attrs: { value: attrs(token, { hrefIndex, page, hash }, env) }, 52 | tag: { value: options.tagName } 53 | }); 54 | } 55 | 56 | module.exports = (md, config = {}) => { 57 | let isInAppLink = false; 58 | const isInAppUrl = config.isInAppUrl || isLocalUrl; 59 | 60 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => { 61 | const token = tokens[idx]; 62 | const hrefIndex = token.attrIndex('href'); 63 | 64 | if (hrefIndex >= 0) { 65 | const hrefToken = token.attrs[hrefIndex]; 66 | 67 | if (isExternalUrl(hrefToken[1])) { 68 | Object.keys(config.external).forEach((key) => { 69 | token.attrSet(key, config.external[key]); 70 | }); 71 | } else if (isInAppUrl(hrefToken[1]) && config.local) { 72 | tokens[idx] = toCustomLink(token, hrefIndex, config.local, env); 73 | isInAppLink = true; 74 | } 75 | } 76 | return self.renderToken(tokens, idx, options); 77 | }; 78 | 79 | md.renderer.rules.link_close = (tokens, idx, options, env, self) => { 80 | const token = tokens[idx]; 81 | if (isInAppLink) { 82 | token.tag = config.local.tagName; 83 | isInAppLink = false; 84 | } 85 | return self.renderToken(tokens, idx, options); 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /docs-src/tools/mdTableContainer.js: -------------------------------------------------------------------------------- 1 | module.exports = (md, config = {}) => { 2 | const cssClass = config.cssClass || 'responsive'; 3 | 4 | md.renderer.rules.table_open = (tokens, idx, options, env, self) => `
${self.renderToken(tokens, idx, options)}`; 5 | 6 | md.renderer.rules.table_close = (tokens, idx, options, env, self) => `${self.renderToken(tokens, idx, options)}
`; 7 | }; 8 | -------------------------------------------------------------------------------- /docs-src/tools/workbox.config.js: -------------------------------------------------------------------------------- 1 | const prependBaseUrl = baseUrl => async manifest => ({ 2 | warnings: [], 3 | manifest: manifest.map((entry) => { 4 | entry.url = `${baseUrl}/${entry.url}`; 5 | return entry; 6 | }) 7 | }); 8 | const route = (url, handler, options) => ({ 9 | urlPattern: new RegExp(url.replace(/\//g, '\\/')), 10 | handler, 11 | options 12 | }); 13 | 14 | export default (DEST, PUBLIC_PATH) => ({ 15 | swDest: `${DEST}/sw.js`, 16 | cleanupOutdatedCaches: true, 17 | inlineWorkboxRuntime: process.env.NODE_ENV === 'production', 18 | maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, 19 | globDirectory: DEST, 20 | globPatterns: [ 21 | 'assets/*.json', 22 | 'app-icons/*', 23 | 'fonts/*', 24 | 'manifest.json', 25 | 'index.html', 26 | '*.js', 27 | '*.{png,jpeg}' 28 | ], 29 | navigateFallback: `${PUBLIC_PATH}/index.html`, 30 | runtimeCaching: [ 31 | route(`${PUBLIC_PATH}/images/`, 'StaleWhileRevalidate', { 32 | cacheName: 'images', 33 | expiration: { 34 | maxEntries: 100 35 | } 36 | }), 37 | route(`${PUBLIC_PATH}/@webcomponents/`, 'CacheFirst', { 38 | cacheName: 'polyfills' 39 | }) 40 | ], 41 | manifestTransforms: [ 42 | prependBaseUrl(PUBLIC_PATH) 43 | ] 44 | }); 45 | -------------------------------------------------------------------------------- /git-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | packages/dx/bin/dx.js lint-staged 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "casl-monorepo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "build.docs": "cd docs-src && NODE_ENV=production npm run build", 7 | "coverage.submit": "codecov", 8 | "prepare": "cd packages/dx && bin/dx.js install" 9 | }, 10 | "devDependencies": { 11 | "codecov": "^3.8.3" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/stalniy/casl.git" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/casl-ability/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-ability/extra.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types/extra'; 2 | -------------------------------------------------------------------------------- /packages/casl-ability/extra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/ability/extra", 3 | "typings": "../dist/types/extra/index.d.ts", 4 | "main": "../dist/umd/extra/index.js", 5 | "module": "../dist/es5m/extra/index.js", 6 | "es2015": "../dist/es6c/extra/index.js" 7 | } 8 | -------------------------------------------------------------------------------- /packages/casl-ability/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types'; 2 | -------------------------------------------------------------------------------- /packages/casl-ability/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/ability", 3 | "version": "6.7.4", 4 | "description": "CASL is an isomorphic authorization JavaScript library which restricts what resources a given user is allowed to access", 5 | "funding": "https://github.com/stalniy/casl/blob/master/BACKERS.md", 6 | "main": "dist/es6c/index.js", 7 | "module": "dist/es5m/index.js", 8 | "es2015": "dist/es6m/index.mjs", 9 | "legacy": "dist/umd/index.js", 10 | "typings": "./index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/types/index.d.ts", 14 | "import": "./dist/es6m/index.mjs", 15 | "require": "./dist/es6c/index.js" 16 | }, 17 | "./extra": { 18 | "types": "./dist/types/extra/index.d.ts", 19 | "import": "./dist/es6m/extra/index.mjs", 20 | "require": "./dist/es6c/extra/index.js" 21 | } 22 | }, 23 | "sideEffects": false, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/stalniy/casl.git", 27 | "directory": "packages/casl-ability" 28 | }, 29 | "homepage": "https://casl.js.org", 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "scripts": { 34 | "build.core": "dx rollup -n casl -g @ucast/mongo2js:ucast.mongo2js", 35 | "build.extra": "dx rollup -i src/extra/index.ts -n casl.extra -g @ucast/mongo2js:ucast.mongo2js", 36 | "build.types": "dx tsc", 37 | "build.prepare": "rm -rf dist/* && npm run build.types", 38 | "build": "npm run build.prepare && npm run build.core && npm run build.extra", 39 | "lint": "dx eslint src/ spec/", 40 | "test": "dx jest", 41 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 42 | "release": "npm run release.prepare && dx semantic-release" 43 | }, 44 | "keywords": [ 45 | "permissions", 46 | "authorization", 47 | "acl", 48 | "abac", 49 | "rbac", 50 | "ibac", 51 | "cancan" 52 | ], 53 | "author": "Sergii Stotskyi ", 54 | "license": "MIT", 55 | "devDependencies": { 56 | "@casl/dx": "workspace:^1.0.0", 57 | "@types/jest": "^29.0.0", 58 | "@types/node": "^22.0.0", 59 | "chai": "^4.1.0", 60 | "chai-spies": "^1.0.0", 61 | "expect-type": "^0.15.0" 62 | }, 63 | "files": [ 64 | "dist", 65 | "*.d.ts", 66 | "extra" 67 | ], 68 | "dependencies": { 69 | "@ucast/mongo2js": "^1.3.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/casl-ability/spec/rulesToAST.spec.js: -------------------------------------------------------------------------------- 1 | import { defineAbility } from '../src' 2 | import { rulesToAST } from '../src/extra' 3 | import './spec_helper' 4 | 5 | describe('rulesToAST', () => { 6 | it('returns empty "and" `Condition` if there are no conditions in `Ability`', () => { 7 | const ability = defineAbility(can => can('read', 'Post')) 8 | const ast = rulesToAST(ability, 'read', 'Post') 9 | 10 | expect(ast.operator).to.equal('and') 11 | expect(ast.value).to.be.an('array').that.is.empty 12 | }) 13 | 14 | it('returns `null` if there is no ability to do an action', () => { 15 | const ability = defineAbility(can => can('read', 'Post')) 16 | const ast = rulesToAST(ability, 'update', 'Post') 17 | 18 | expect(ast).to.be.null 19 | }) 20 | 21 | it('returns only "or" `Condition` if there are no forbidden rules', () => { 22 | const ability = defineAbility((can) => { 23 | can('read', 'Post', { author: 1 }) 24 | can('read', 'Post', { private: false }) 25 | }) 26 | const ast = rulesToAST(ability, 'read', 'Post') 27 | 28 | expect(ast).to.deep.equal({ 29 | operator: 'or', 30 | value: [ 31 | { operator: 'eq', field: 'private', value: false }, 32 | { operator: 'eq', field: 'author', value: 1 }, 33 | ] 34 | }) 35 | }) 36 | 37 | it('returns "and" condition that includes "or" if there are forbidden and regular rules', () => { 38 | const ability = defineAbility((can, cannot) => { 39 | can('read', 'Post', { author: 1 }) 40 | can('read', 'Post', { sharedWith: 1 }) 41 | cannot('read', 'Post', { private: true }) 42 | }) 43 | const ast = rulesToAST(ability, 'read', 'Post') 44 | 45 | expect(ast).to.deep.equal({ 46 | operator: 'and', 47 | value: [ 48 | { 49 | operator: 'not', 50 | value: [ 51 | { operator: 'eq', field: 'private', value: true } 52 | ] 53 | }, 54 | { 55 | operator: 'or', 56 | value: [ 57 | { operator: 'eq', field: 'sharedWith', value: 1 }, 58 | { operator: 'eq', field: 'author', value: 1 }, 59 | ] 60 | } 61 | ] 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /packages/casl-ability/spec/rules_to_fields.spec.js: -------------------------------------------------------------------------------- 1 | import { rulesToFields } from '../src/extra' 2 | import { defineAbility, PureAbility } from '../src' 3 | import './spec_helper' 4 | 5 | describe('rulesToFields', () => { 6 | it('returns an empty object for an empty `Ability` instance', () => { 7 | const object = rulesToFields(new PureAbility(), 'read', 'Post') 8 | 9 | expect(object).to.be.an('object').and.empty 10 | }) 11 | 12 | it('returns an empty object if `Ability` contains only inverted rules', () => { 13 | const ability = defineAbility((_, cannot) => { 14 | cannot('read', 'Post', { id: 5 }) 15 | cannot('read', 'Post', { private: true }) 16 | }) 17 | const object = rulesToFields(ability, 'read', 'Post') 18 | 19 | expect(object).to.be.an('object').and.empty 20 | }) 21 | 22 | it('returns an empty object for `Ability` instance with rules without conditions', () => { 23 | const ability = defineAbility(can => can('read', 'Post')) 24 | const object = rulesToFields(ability, 'read', 'Post') 25 | 26 | expect(object).to.be.an('object').and.empty 27 | }) 28 | 29 | it('extracts field values from direct rule conditions', () => { 30 | const ability = defineAbility((can) => { 31 | can('read', 'Post', { id: 5 }) 32 | can('read', 'Post', { private: true }) 33 | }) 34 | const object = rulesToFields(ability, 'read', 'Post') 35 | 36 | expect(object).to.deep.equal({ id: 5, private: true }) 37 | }) 38 | 39 | it('correctly sets values for fields declared with `dot notation`', () => { 40 | const ability = defineAbility((can) => { 41 | can('read', 'Post', { id: 5 }) 42 | can('read', 'Post', { 'state.private': true }) 43 | }) 44 | const object = rulesToFields(ability, 'read', 'Post') 45 | 46 | expect(object).to.deep.equal({ 47 | id: 5, 48 | state: { 49 | private: true 50 | } 51 | }) 52 | }) 53 | 54 | it('skips plain object values (i.e., mongo query expressions)', () => { 55 | const ability = defineAbility((can) => { 56 | can('read', 'Post', { state: { $in: ['draft', 'review'] } }) 57 | can('read', 'Post', { private: true }) 58 | }) 59 | const object = rulesToFields(ability, 'read', 'Post') 60 | 61 | expect(object).to.deep.equal({ private: true }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /packages/casl-ability/spec/spec_helper.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import spies from 'chai-spies' 3 | import '@casl/dx/lib/spec_helper' 4 | 5 | chai.use(spies) 6 | 7 | chai.Assertion.addMethod('allow', function assertAbility(action, subject, field) { 8 | const subjectRepresantation = prettifyObject(subject) 9 | this.assert( 10 | this._obj.can(action, subject, field), 11 | `expected ability to allow ${action} on ${subjectRepresantation}`, 12 | `expected ability to not allow ${action} on ${subjectRepresantation}` 13 | ) 14 | }) 15 | 16 | function prettifyObject(object) { 17 | if (!object || typeof object === 'string') { 18 | return object 19 | } 20 | 21 | if (typeof object === 'function') { 22 | return object.name 23 | } 24 | 25 | const attrs = JSON.stringify(object) 26 | return `${object.constructor.name} { ${attrs[0] === '{' ? attrs.slice(1, -1) : attrs} }` 27 | } 28 | 29 | export class Post { 30 | constructor(attrs) { 31 | Object.assign(this, attrs) 32 | } 33 | } 34 | 35 | export function ruleToObject(rule) { 36 | const fields = ['action', 'subject', 'conditions', 'fields', 'inverted', 'reason'] 37 | return fields.reduce((object, field) => { 38 | if (rule[field]) { 39 | object[field] = rule[field] 40 | } 41 | return object 42 | }, {}) 43 | } 44 | -------------------------------------------------------------------------------- /packages/casl-ability/spec/subject_helper.spec.ts: -------------------------------------------------------------------------------- 1 | import { subject, detectSubjectType } from '../src' 2 | 3 | describe('`subject` helper', () => { 4 | it('defines subject type for an object', () => { 5 | const object = subject('Article', {}) 6 | expect(detectSubjectType(object)).toBe('Article') 7 | }) 8 | 9 | it('throws exception when trying to redefine subject type', () => { 10 | const object = subject('Article', {}) 11 | expect(() => subject('User', object)).toThrow(Error) 12 | }) 13 | 14 | it('does not throw if subject type of an object equals to provided subject type', () => { 15 | const object = subject('Article', {}) 16 | expect(() => subject('Article', object)).not.toThrow(Error) 17 | }) 18 | 19 | it('ignores falsy subjects', () => { 20 | // @ts-expect-error 21 | expect(() => subject('Test', null)).not.toThrow(Error) 22 | // @ts-expect-error 23 | expect(() => subject('Test', undefined)).not.toThrow(Error) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/casl-ability/src/Ability.ts: -------------------------------------------------------------------------------- 1 | import { PureAbility, AbilityOptions, AbilityOptionsOf } from './PureAbility'; 2 | import { RawRuleFrom } from './RawRule'; 3 | import { AbilityTuple } from './types'; 4 | import { MongoQuery, mongoQueryMatcher } from './matchers/conditions'; 5 | import { fieldPatternMatcher } from './matchers/field'; 6 | import { Public, RawRuleOf } from './RuleIndex'; 7 | 8 | /** 9 | * @deprecated use createMongoAbility function instead and MongoAbility interface. 10 | * In the next major version PureAbility will be renamed to Ability and this class will be removed 11 | */ 12 | export class Ability< 13 | A extends AbilityTuple = AbilityTuple, 14 | C extends MongoQuery = MongoQuery 15 | > extends PureAbility { 16 | constructor(rules: RawRuleFrom[] = [], options: AbilityOptions = {}) { 17 | super(rules, { 18 | conditionsMatcher: mongoQueryMatcher, 19 | fieldMatcher: fieldPatternMatcher, 20 | ...options, 21 | }); 22 | } 23 | } 24 | 25 | export interface AnyMongoAbility extends Public> {} 26 | export interface MongoAbility< 27 | A extends AbilityTuple = AbilityTuple, 28 | C extends MongoQuery = MongoQuery 29 | > extends PureAbility {} 30 | 31 | /** 32 | * Creates Ability with MongoDB conditions matcher 33 | */ 34 | export function createMongoAbility< 35 | T extends AnyMongoAbility = MongoAbility 36 | >(rules?: RawRuleOf[], options?: AbilityOptionsOf): T; 37 | export function createMongoAbility< 38 | A extends AbilityTuple = AbilityTuple, 39 | C extends MongoQuery = MongoQuery 40 | >(rules?: RawRuleFrom[], options?: AbilityOptions): MongoAbility; 41 | export function createMongoAbility(rules: any[] = [], options = {}): AnyMongoAbility { 42 | return new PureAbility(rules, { 43 | conditionsMatcher: mongoQueryMatcher, 44 | fieldMatcher: fieldPatternMatcher, 45 | ...options, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/casl-ability/src/ForbiddenError.ts: -------------------------------------------------------------------------------- 1 | import { AnyAbility } from './PureAbility'; 2 | import { Normalize, Subject } from './types'; 3 | import { Generics } from './RuleIndex'; 4 | import { getSubjectTypeName } from './utils'; 5 | 6 | export type GetErrorMessage = (error: ForbiddenError) => string; 7 | /** @deprecated will be removed in the next major release */ 8 | export const getDefaultErrorMessage: GetErrorMessage = error => `Cannot execute "${error.action}" on "${error.subjectType}"`; 9 | 10 | const NativeError = function NError(this: Error, message: string) { 11 | this.message = message; 12 | } as unknown as new (message: string) => Error; 13 | 14 | NativeError.prototype = Object.create(Error.prototype); 15 | 16 | export class ForbiddenError extends NativeError { 17 | public readonly ability!: T; 18 | public action!: Normalize['abilities']>[0]; 19 | public subject!: Generics['abilities'][1]; 20 | public field?: string; 21 | public subjectType!: string; 22 | 23 | static _defaultErrorMessage = getDefaultErrorMessage; 24 | 25 | static setDefaultMessage(messageOrFn: string | GetErrorMessage) { 26 | this._defaultErrorMessage = typeof messageOrFn === 'string' ? () => messageOrFn : messageOrFn; 27 | } 28 | 29 | static from(ability: U): ForbiddenError { 30 | return new this(ability); 31 | } 32 | 33 | constructor(ability: T) { 34 | super(''); 35 | this.ability = ability; 36 | 37 | if (typeof Error.captureStackTrace === 'function') { 38 | this.name = 'ForbiddenError'; 39 | Error.captureStackTrace(this, this.constructor); 40 | } 41 | } 42 | 43 | setMessage(message: string): this { 44 | this.message = message; 45 | return this; 46 | } 47 | 48 | throwUnlessCan(...args: Parameters): void; 49 | throwUnlessCan(action: string, subject?: Subject, field?: string): void { 50 | const error = (this as any).unlessCan(action, subject, field); 51 | if (error) throw error; 52 | } 53 | 54 | unlessCan(...args: Parameters): this | undefined; 55 | unlessCan(action: string, subject?: Subject, field?: string): this | undefined { 56 | const rule = this.ability.relevantRuleFor(action, subject, field); 57 | 58 | if (rule && !rule.inverted) { 59 | return; 60 | } 61 | 62 | this.action = action; 63 | this.subject = subject; 64 | this.subjectType = getSubjectTypeName(this.ability.detectSubjectType(subject)); 65 | this.field = field; 66 | 67 | const reason = rule ? rule.reason : ''; 68 | this.message = this.message || reason || (this.constructor as any)._defaultErrorMessage(this); 69 | return this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/casl-ability/src/PureAbility.ts: -------------------------------------------------------------------------------- 1 | import { RuleIndex, RuleIndexOptions, RuleIndexOptionsOf, Public, RawRuleOf } from './RuleIndex'; 2 | import { Abilities, AbilityTuple, CanParameters, Subject } from './types'; 3 | import { Rule } from './Rule'; 4 | 5 | export interface AbilityOptions 6 | extends RuleIndexOptions {} 7 | export interface AnyAbility extends Public> {} 8 | export interface AbilityOptionsOf extends RuleIndexOptionsOf {} 9 | 10 | export type AbilityClass = new ( 11 | rules?: RawRuleOf[], 12 | options?: AbilityOptionsOf 13 | ) => T; 14 | 15 | export type CreateAbility = ( 16 | rules?: RawRuleOf[], 17 | options?: AbilityOptionsOf 18 | ) => T; 19 | 20 | export class PureAbility< 21 | A extends Abilities = AbilityTuple, 22 | Conditions = unknown 23 | > extends RuleIndex { 24 | can(...args: CanParameters): boolean; 25 | can(action: string, subject?: Subject, field?: string): boolean { 26 | const rule = (this as PrimitiveAbility).relevantRuleFor(action, subject, field); 27 | return !!rule && !rule.inverted; 28 | } 29 | 30 | relevantRuleFor(...args: CanParameters): Rule | null; 31 | relevantRuleFor(action: string, subject?: Subject, field?: string): Rule | null { 32 | const subjectType = this.detectSubjectType(subject); 33 | const rules = (this as any).rulesFor(action, subjectType, field); 34 | 35 | for (let i = 0, length = rules.length; i < length; i++) { 36 | if (rules[i].matchesConditions(subject)) { 37 | return rules[i]; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | 44 | cannot(...args: CanParameters): boolean; 45 | cannot(action: string, subject?: Subject, field?: string): boolean { 46 | return !(this as PrimitiveAbility).can(action, subject, field); 47 | } 48 | } 49 | 50 | /** 51 | * helper interface that helps to emit js methods that have static parameters 52 | */ 53 | interface PrimitiveAbility { 54 | can(action: string, subject?: Subject, field?: string): boolean; 55 | relevantRuleFor(action: string, subject?: Subject, field?: string): Rule | null 56 | } 57 | -------------------------------------------------------------------------------- /packages/casl-ability/src/RawRule.ts: -------------------------------------------------------------------------------- 1 | import { SubjectType, AbilityTypes, AbilityTupleType, Abilities, ToAbilityTypes } from './types'; 2 | 3 | interface BaseRawRule { 4 | fields?: string | string[] 5 | conditions?: Conditions 6 | /** indicates that rule forbids something (i.e., has inverted logic) */ 7 | inverted?: boolean 8 | /** explains the reason of why rule does not allow to do something */ 9 | reason?: string 10 | } 11 | 12 | export interface ClaimRawRule extends BaseRawRule { 13 | action: A | A[] 14 | subject?: undefined 15 | } 16 | 17 | export interface SubjectRawRule extends BaseRawRule { 18 | action: A | A[] 19 | subject: S | S[] 20 | } 21 | 22 | type DefineRule>> = 23 | T extends AbilityTupleType ? SubjectRawRule : Else; 24 | 25 | export type RawRule = DefineRule; 26 | export type RawRuleFrom = RawRule, C>; 27 | -------------------------------------------------------------------------------- /packages/casl-ability/src/extra/index.ts: -------------------------------------------------------------------------------- 1 | export * from './packRules'; 2 | export * from './permittedFieldsOf'; 3 | export * from './rulesToFields'; 4 | export * from './rulesToQuery'; 5 | -------------------------------------------------------------------------------- /packages/casl-ability/src/extra/packRules.ts: -------------------------------------------------------------------------------- 1 | 2 | import { RawRule } from '../RawRule'; 3 | import { SubjectType } from '../types'; 4 | import { wrapArray } from '../utils'; 5 | 6 | const joinIfArray = (value: string | string[]) => Array.isArray(value) ? value.join(',') : value; 7 | 8 | export type PackRule> = 9 | [string, string] | 10 | [string, string, T['conditions']] | 11 | [string, string, T['conditions'] | 0, 1] | 12 | [string, string, T['conditions'] | 0, 1 | 0, string] | 13 | [string, string, T['conditions'] | 0, 1 | 0, string | 0, string]; 14 | 15 | export type PackSubjectType = (type: T) => string; 16 | 17 | export function packRules>( 18 | rules: T[], 19 | packSubject?: PackSubjectType 20 | ): PackRule[] { 21 | return rules.map((rule) => { 22 | const packedRule: PackRule = [ 23 | joinIfArray((rule as any).action || (rule as any).actions), 24 | typeof packSubject === 'function' 25 | ? wrapArray(rule.subject).map(packSubject).join(',') 26 | : joinIfArray(rule.subject), 27 | rule.conditions || 0, 28 | rule.inverted ? 1 : 0, 29 | rule.fields ? joinIfArray(rule.fields) : 0, 30 | rule.reason || '' 31 | ]; 32 | 33 | while (packedRule.length > 0 && !packedRule[packedRule.length - 1]) packedRule.pop(); 34 | 35 | return packedRule; 36 | }); 37 | } 38 | 39 | export type UnpackSubjectType = (type: string) => T; 40 | 41 | export function unpackRules>( 42 | rules: PackRule[], 43 | unpackSubject?: UnpackSubjectType 44 | ): T[] { 45 | return rules.map(([action, subject, conditions, inverted, fields, reason]) => { 46 | const subjects = subject.split(','); 47 | const rule = { 48 | inverted: !!inverted, 49 | action: action.split(','), 50 | subject: typeof unpackSubject === 'function' 51 | ? subjects.map(unpackSubject) 52 | : subjects 53 | } as T; 54 | 55 | if (conditions) rule.conditions = conditions; 56 | if (fields) rule.fields = fields.split(','); 57 | if (reason) rule.reason = reason; 58 | 59 | return rule; 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /packages/casl-ability/src/extra/permittedFieldsOf.ts: -------------------------------------------------------------------------------- 1 | import { AnyAbility } from '../PureAbility'; 2 | import { Rule } from '../Rule'; 3 | import { RuleOf } from '../RuleIndex'; 4 | import { Subject, SubjectType } from '../types'; 5 | 6 | export type GetRuleFields> = (rule: R) => string[]; 7 | 8 | export interface PermittedFieldsOptions { 9 | fieldsFrom: GetRuleFields> 10 | } 11 | 12 | export function permittedFieldsOf( 13 | ability: T, 14 | action: Parameters[0], 15 | subject: Parameters[1], 16 | options: PermittedFieldsOptions 17 | ): string[] { 18 | const subjectType = ability.detectSubjectType(subject); 19 | const rules = ability.possibleRulesFor(action, subjectType); 20 | const uniqueFields = new Set(); 21 | const deleteItem = uniqueFields.delete.bind(uniqueFields); 22 | const addItem = uniqueFields.add.bind(uniqueFields); 23 | let i = rules.length; 24 | 25 | while (i--) { 26 | const rule = rules[i]; 27 | if (rule.matchesConditions(subject)) { 28 | const toggle = rule.inverted ? deleteItem : addItem; 29 | options.fieldsFrom(rule).forEach(toggle); 30 | } 31 | } 32 | 33 | return Array.from(uniqueFields); 34 | } 35 | 36 | export type GetSubjectTypeAllFieldsExtractor = (subjectType: SubjectType) => string[]; 37 | 38 | /** 39 | * Helper class to make custom `accessibleFieldsBy` helper function 40 | */ 41 | export class AccessibleFields { 42 | constructor( 43 | private readonly _ability: AnyAbility, 44 | private readonly _action: string, 45 | private readonly _getAllFields: GetSubjectTypeAllFieldsExtractor 46 | ) {} 47 | 48 | /** 49 | * Returns accessible fields for Model type 50 | */ 51 | ofType(subjectType: Extract): string[] { 52 | return permittedFieldsOf(this._ability, this._action, subjectType, { 53 | fieldsFrom: this._getRuleFields(subjectType) 54 | }); 55 | } 56 | 57 | /** 58 | * Returns accessible fields for particular document 59 | */ 60 | of(subject: Exclude): string[] { 61 | return permittedFieldsOf(this._ability, this._action, subject, { 62 | fieldsFrom: this._getRuleFields(this._ability.detectSubjectType(subject)) 63 | }); 64 | } 65 | 66 | private _getRuleFields(type: SubjectType): GetRuleFields> { 67 | return (rule) => (rule.fields || this._getAllFields(type)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/casl-ability/src/extra/rulesToFields.ts: -------------------------------------------------------------------------------- 1 | import { PureAbility } from '../PureAbility'; 2 | import { AnyObject, ExtractSubjectType } from '../types'; 3 | import { setByPath } from '../utils'; 4 | 5 | /** 6 | * Extracts rules condition values into an object of default values 7 | */ 8 | export function rulesToFields>( 9 | ability: T, 10 | action: Parameters[0], 11 | subjectType: ExtractSubjectType[1]>, 12 | ): AnyObject { 13 | return ability.rulesFor(action, subjectType) 14 | .reduce((values, rule) => { 15 | if (rule.inverted || !rule.conditions) { 16 | return values; 17 | } 18 | 19 | return Object.keys(rule.conditions).reduce((fields, fieldName) => { 20 | const value = rule.conditions![fieldName]; 21 | 22 | if (!value || (value as any).constructor !== Object) { 23 | setByPath(fields, fieldName, value); 24 | } 25 | 26 | return fields; 27 | }, values); 28 | }, {} as AnyObject); 29 | } 30 | -------------------------------------------------------------------------------- /packages/casl-ability/src/extra/rulesToQuery.ts: -------------------------------------------------------------------------------- 1 | import { CompoundCondition, Condition, buildAnd, buildOr } from '@ucast/mongo2js'; 2 | import { AnyAbility } from '../PureAbility'; 3 | import { Generics, RuleOf } from '../RuleIndex'; 4 | import { ExtractSubjectType } from '../types'; 5 | 6 | export type RuleToQueryConverter = (rule: RuleOf) => R; 7 | export interface AbilityQuery { 8 | $or?: T[] 9 | $and?: T[] 10 | } 11 | 12 | export function rulesToQuery( 13 | ability: T, 14 | action: Parameters[0], 15 | subjectType: ExtractSubjectType[1]>, 16 | convert: RuleToQueryConverter 17 | ): AbilityQuery | null { 18 | const $and: Generics['conditions'][] = []; 19 | const $or: Generics['conditions'][] = []; 20 | const rules = ability.rulesFor(action, subjectType); 21 | 22 | for (let i = 0; i < rules.length; i++) { 23 | const rule = rules[i]; 24 | const list = rule.inverted ? $and : $or; 25 | 26 | if (!rule.conditions) { 27 | if (rule.inverted) { 28 | // stop if inverted rule without fields and conditions 29 | // Example: 30 | // can('read', 'Post', { id: 2 }) 31 | // cannot('read', "Post") 32 | // can('read', 'Post', { id: 5 }) 33 | break; 34 | } else { 35 | // if it allows reading all types then remove previous conditions 36 | // Example: 37 | // can('read', 'Post', { id: 1 }) 38 | // can('read', 'Post') 39 | // cannot('read', 'Post', { status: 'draft' }) 40 | return $and.length ? { $and } : {}; 41 | } 42 | } else { 43 | list.push(convert(rule)); 44 | } 45 | } 46 | 47 | // if there are no regular conditions and the where no rule without condition 48 | // then user is not allowed to perform this action on this subject type 49 | if (!$or.length) return null; 50 | return $and.length ? { $or, $and } : { $or }; 51 | } 52 | 53 | function ruleToAST(rule: RuleOf): Condition { 54 | if (!rule.ast) { 55 | throw new Error(`Ability rule "${JSON.stringify(rule)}" does not have "ast" property. So, cannot be used to generate AST`); 56 | } 57 | 58 | return rule.inverted ? new CompoundCondition('not', [rule.ast]) : rule.ast; 59 | } 60 | 61 | export function rulesToAST( 62 | ability: T, 63 | action: Parameters[0], 64 | subjectType: ExtractSubjectType[1]>, 65 | ): Condition | null { 66 | const query = rulesToQuery(ability, action, subjectType, ruleToAST) as AbilityQuery; 67 | 68 | if (query === null) { 69 | return null; 70 | } 71 | 72 | if (!query.$and) { 73 | return query.$or ? buildOr(query.$or) : buildAnd([]); 74 | } 75 | 76 | if (query.$or) { 77 | query.$and.push(buildOr(query.$or)); 78 | } 79 | 80 | return buildAnd(query.$and); 81 | } 82 | -------------------------------------------------------------------------------- /packages/casl-ability/src/hkt.ts: -------------------------------------------------------------------------------- 1 | export declare const ɵvalue: unique symbol; 2 | export type Container = { [ɵvalue]?: T }; 3 | export type Unpack> = Exclude; 4 | 5 | export interface Generic { 6 | 0: T1 7 | 1: T2 8 | 2: T3 9 | 3: T4 10 | } 11 | export interface GenericFactory< 12 | T1 = unknown, 13 | T2 = unknown, 14 | T3 = unknown, 15 | T4 = unknown 16 | > extends Generic { 17 | produce: unknown 18 | } 19 | export type ProduceGeneric = T extends Container 20 | ? (Unpack & Generic)['produce'] 21 | : T; 22 | -------------------------------------------------------------------------------- /packages/casl-ability/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Ability'; 2 | export * from './PureAbility'; 3 | export * from './AbilityBuilder'; 4 | export * from './ForbiddenError'; 5 | export * from './RawRule'; 6 | export * from './hkt'; 7 | export * from './matchers/conditions'; 8 | export * from './matchers/field'; 9 | export type { 10 | SubjectClass, 11 | SubjectType, 12 | Subject, 13 | AbilityTuple, 14 | Abilities, 15 | Normalize, 16 | IfString, 17 | AbilityParameters, 18 | CanParameters, 19 | ExtractSubjectType, 20 | InferSubjects, 21 | ForcedSubject, 22 | MatchConditions, 23 | ConditionsMatcher, 24 | MatchField, 25 | FieldMatcher, 26 | } from './types'; 27 | export type { 28 | Generics, 29 | RuleOf, 30 | RawRuleOf, 31 | UpdateEvent, 32 | EventHandler, 33 | Unsubscribe 34 | } from './RuleIndex'; 35 | export * as hkt from './hkt'; 36 | export { 37 | setSubjectType as subject, 38 | detectSubjectType, 39 | createAliasResolver, 40 | wrapArray 41 | } from './utils'; 42 | -------------------------------------------------------------------------------- /packages/casl-ability/src/matchers/conditions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $all, 3 | $elemMatch, 4 | $eq, 5 | $exists, 6 | $gt, 7 | $gte, 8 | $in, 9 | $lt, 10 | $lte, 11 | $ne, 12 | $nin, 13 | $options, 14 | $regex, 15 | $size, 16 | all, 17 | and, 18 | BuildMongoQuery, 19 | createFactory, 20 | DefaultOperators, 21 | elemMatch, 22 | eq, 23 | exists, 24 | gt, 25 | gte, 26 | lt, 27 | lte, 28 | ne, 29 | nin, 30 | regex, 31 | size, 32 | within 33 | } from '@ucast/mongo2js'; 34 | import { Container, GenericFactory } from '../hkt'; 35 | import { AnyObject, ConditionsMatcher } from '../types'; 36 | 37 | const defaultInstructions = { 38 | $eq, 39 | $ne, 40 | $lt, 41 | $lte, 42 | $gt, 43 | $gte, 44 | $in, 45 | $nin, 46 | $all, 47 | $size, 48 | $regex, 49 | $options, 50 | $elemMatch, 51 | $exists, 52 | }; 53 | const defaultInterpreters = { 54 | eq, 55 | ne, 56 | lt, 57 | lte, 58 | gt, 59 | gte, 60 | in: within, 61 | nin, 62 | all, 63 | size, 64 | regex, 65 | elemMatch, 66 | exists, 67 | and, 68 | }; 69 | 70 | interface MongoQueryFactory extends GenericFactory { 71 | produce: MongoQuery 72 | } 73 | 74 | type MergeUnion = { [K in Keys]: T[K] }; 75 | export type MongoQuery = BuildMongoQuery, { 76 | toplevel: {}, 77 | field: Pick>['field'], keyof typeof defaultInstructions> 78 | }> & Container; 79 | 80 | type MongoQueryMatcherFactory = 81 | (...args: Partial>) => ConditionsMatcher; 82 | export const buildMongoQueryMatcher = ((instructions, interpreters, options) => createFactory( 83 | { ...defaultInstructions, ...instructions }, 84 | { ...defaultInterpreters, ...interpreters }, 85 | options 86 | )) as MongoQueryMatcherFactory; 87 | 88 | export const mongoQueryMatcher = createFactory(defaultInstructions, defaultInterpreters); 89 | export type { 90 | MongoQueryFieldOperators, MongoQueryOperators, MongoQueryTopLevelOperators 91 | } from '@ucast/mongo2js'; 92 | 93 | -------------------------------------------------------------------------------- /packages/casl-ability/src/matchers/field.ts: -------------------------------------------------------------------------------- 1 | import { FieldMatcher } from '../types'; 2 | 3 | const REGEXP_SPECIAL_CHARS = /[-/\\^$+?.()|[\]{}]/g; 4 | const REGEXP_ANY = /\.?\*+\.?/g; 5 | const REGEXP_STARS = /\*+/; 6 | const REGEXP_DOT = /\./g; 7 | 8 | function detectRegexpPattern(match: string, index: number, string: string): string { 9 | const quantifier = string[0] === '*' || match[0] === '.' && match[match.length - 1] === '.' 10 | ? '+' 11 | : '*'; 12 | const matcher = match.indexOf('**') === -1 ? '[^.]' : '.'; 13 | const pattern = match.replace(REGEXP_DOT, '\\$&') 14 | .replace(REGEXP_STARS, matcher + quantifier); 15 | 16 | return index + match.length === string.length ? `(?:${pattern})?` : pattern; 17 | } 18 | 19 | function escapeRegexp(match: string, index: number, string: string): string { 20 | if (match === '.' && (string[index - 1] === '*' || string[index + 1] === '*')) { 21 | return match; 22 | } 23 | 24 | return `\\${match}`; 25 | } 26 | 27 | function createPattern(fields: string[]) { 28 | const patterns = fields.map(field => field 29 | .replace(REGEXP_SPECIAL_CHARS, escapeRegexp) 30 | .replace(REGEXP_ANY, detectRegexpPattern)); 31 | const pattern = patterns.length > 1 ? `(?:${patterns.join('|')})` : patterns[0]; 32 | 33 | return new RegExp(`^${pattern}$`); 34 | } 35 | 36 | export const fieldPatternMatcher: FieldMatcher = (fields) => { 37 | let pattern: RegExp | null; 38 | 39 | return (field) => { 40 | if (typeof pattern === 'undefined') { 41 | pattern = fields.every(f => f.indexOf('*') === -1) 42 | ? null 43 | : createPattern(fields); 44 | } 45 | 46 | return pattern === null 47 | ? fields.indexOf(field) !== -1 48 | : pattern.test(field); 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /packages/casl-ability/src/structures/LinkedItem.ts: -------------------------------------------------------------------------------- 1 | export interface LinkedItem { 2 | next: LinkedItem | null 3 | prev: LinkedItem | null 4 | readonly value: T 5 | } 6 | 7 | export function linkedItem(value: T, prev: LinkedItem['prev']) { 8 | const item = { value, prev, next: null }; 9 | 10 | if (prev) { 11 | prev.next = item; 12 | } 13 | 14 | return item; 15 | } 16 | 17 | export function unlinkItem(item: LinkedItem) { 18 | if (item.next) { 19 | item.next.prev = item.prev; 20 | } 21 | 22 | if (item.prev) { 23 | item.prev.next = item.next; 24 | } 25 | 26 | item.next = item.prev = null; 27 | } 28 | 29 | export const cloneLinkedItem = >(item: T): T => ({ 30 | value: item.value, 31 | prev: item.prev, 32 | next: item.next, 33 | } as T); 34 | -------------------------------------------------------------------------------- /packages/casl-ability/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "spec/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/casl-ability/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/**/*", 5 | "spec/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/casl-angular/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-angular/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types'; 2 | -------------------------------------------------------------------------------- /packages/casl-angular/jest.config.js: -------------------------------------------------------------------------------- 1 | const config = require('@casl/dx/config/jest.config'); 2 | 3 | module.exports = { 4 | ...config, 5 | transform: { 6 | '^.+\\.(ts|js|mjs|html|svg)$': 'jest-preset-angular', 7 | }, 8 | testEnvironment: "jsdom", 9 | preset: 'jest-preset-angular', 10 | setupFilesAfterEnv: [ 11 | 'zone.js', 12 | 'zone.js/testing' 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /packages/casl-angular/spec/AbilityService.spec.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core' 2 | import { TestBed } from '@angular/core/testing' 3 | import { createMongoAbility, MongoAbility, PureAbility } from '@casl/ability' 4 | import { firstValueFrom } from 'rxjs' 5 | import { AbilityService } from '../src/public' 6 | import './spec_helper' 7 | 8 | describe('AbilityService', () => { 9 | it('provides ability through `ability$` Observable', async () => { 10 | const { abilityService, ability } = setup() 11 | const providedAility = await firstValueFrom(abilityService.ability$) 12 | 13 | expect(providedAility).toBe(ability) 14 | }) 15 | 16 | it('emits ability instance when its rules are updated', () => { 17 | const { abilityService, ability } = setup() 18 | const listener = jest.fn() 19 | 20 | abilityService.ability$.subscribe(listener) 21 | ability.update([{ action: 'manage', subject: 'all' }]) 22 | ability.update([]) 23 | 24 | expect(listener).toHaveBeenCalledTimes(3) // initial emit + 2 updates 25 | expect(listener).toHaveBeenCalledWith(ability) 26 | }) 27 | 28 | function setup() { 29 | const ability = createMongoAbility() 30 | TestBed.configureTestingModule({ 31 | providers: [ 32 | AbilityService, 33 | { provide: PureAbility, useValue: ability } 34 | ] 35 | }) 36 | 37 | return { 38 | ability, 39 | abilityService: TestBed.inject(AbilityService as Type>) 40 | } 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /packages/casl-angular/spec/AbilityServiceSignal.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, DebugElement, inject, Input, Predicate } from '@angular/core' 2 | import { TestBed } from '@angular/core/testing' 3 | import { createMongoAbility, PureAbility } from '@casl/ability' 4 | import { AbilityServiceSignal } from '../src/public' 5 | import './spec_helper' 6 | import { createComponent } from './spec_helper' 7 | 8 | describe('AbilityServiceSignal', () => { 9 | it('allows to use its `can` method in template', async () => { 10 | setup() 11 | const fixture = createComponent(App, { role: 'admin' }) 12 | const deleteButton: Predicate = (d) => d.nativeElement.textContent?.trim() === 'Delete' 13 | expect(fixture.debugElement.query(deleteButton)).toBeTruthy() 14 | 15 | Object.assign(fixture.componentInstance, { role: 'user' }) 16 | fixture.detectChanges() 17 | expect(fixture.debugElement.query(deleteButton)).toBeFalsy() 18 | }) 19 | 20 | function setup() { 21 | TestBed.configureTestingModule({ 22 | providers: [ 23 | AbilityServiceSignal, 24 | { provide: PureAbility, useValue: createMongoAbility() } 25 | ] 26 | }) 27 | } 28 | 29 | @Component({ 30 | selector: 'pfa-app', 31 | standalone: true, 32 | template: ` 33 |

An article

34 | 35 | @if (can('delete', 'Article')) { 36 | 37 | } 38 | ` 39 | }) 40 | class App { 41 | @Input() 42 | set role(value: 'admin' | 'user') { 43 | if (value === 'admin') { 44 | this.abilityService.update([{ action: 'manage', subject: 'all' }]) 45 | } else { 46 | this.abilityService.update([{ action: 'read', subject: 'all' }]) 47 | } 48 | } 49 | 50 | private readonly abilityService = inject(AbilityServiceSignal) 51 | protected can = this.abilityService.can 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /packages/casl-angular/spec/pipes.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { PureAbility } from '@casl/ability' 2 | import { TestBed } from '@angular/core/testing' 3 | import { createApp, createComponent, configureTestingModule, Post } from './spec_helper' 4 | 5 | const AppWithAblePipe = createApp('{{ \'read\' | able: post }}') 6 | const AppWithAblePurePipe = createApp('{{ \'read\' | ablePure: post | async }}') 7 | 8 | describe('Ability pipes', () => { 9 | let fixture 10 | let ability 11 | let post 12 | 13 | afterEach(() => { 14 | if (fixture) { 15 | fixture.destroy() 16 | } 17 | }) 18 | 19 | describe('module', () => { 20 | it('provides deprecated impure `can` pipe', () => { 21 | configureTestingModule([AppWithAblePurePipe]) 22 | fixture = createComponent(AppWithAblePurePipe) 23 | expect(fixture.nativeElement.textContent).toBe('false') 24 | }) 25 | 26 | it('provides impure `able` pipe', () => { 27 | configureTestingModule([AppWithAblePipe]) 28 | fixture = createComponent(AppWithAblePipe) 29 | expect(fixture.nativeElement.textContent).toBe('false') 30 | }) 31 | }) 32 | 33 | describe('`able` pipe', () => { 34 | behavesLikeAbilityPipe(AppWithAblePipe) 35 | }) 36 | 37 | describe('`ablePure` pipe', () => { 38 | behavesLikeAbilityPipe(AppWithAblePurePipe) 39 | }) 40 | 41 | function behavesLikeAbilityPipe(App) { 42 | beforeEach(() => { 43 | configureTestingModule([App]) 44 | ability = TestBed.inject(PureAbility) 45 | post = new Post({ author: 'me' }) 46 | }) 47 | 48 | it('updates template when `ability` is updated', () => { 49 | fixture = createComponent(App, { post }) 50 | ability.update([{ subject: Post.name, action: 'read' }]) 51 | fixture.detectChanges() 52 | 53 | expect(fixture.nativeElement.textContent).toBe('true') 54 | }) 55 | 56 | describe('when abilities depends on object attribute', () => { 57 | beforeEach(() => { 58 | ability.update([{ subject: Post.name, action: 'read', conditions: { author: 'me' } }]) 59 | fixture = createComponent(App, { post }) 60 | fixture.detectChanges() 61 | }) 62 | 63 | it('returns `true` if object attribute equals to specified value', () => { 64 | expect(fixture.nativeElement.textContent).toBe('true') 65 | }) 66 | 67 | if (App !== AppWithAblePurePipe) { 68 | it('updates template when object attribute is changed', () => { 69 | post.author = 'not me' 70 | fixture.detectChanges() 71 | 72 | expect(fixture.nativeElement.textContent).toBe('false') 73 | }) 74 | } 75 | }) 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /packages/casl-angular/spec/spec_helper.ts: -------------------------------------------------------------------------------- 1 | import { Component, Type } from '@angular/core' 2 | import { ComponentFixture, TestBed } from '@angular/core/testing' 3 | import { 4 | BrowserDynamicTestingModule, 5 | platformBrowserDynamicTesting 6 | } from '@angular/platform-browser-dynamic/testing' 7 | import { createMongoAbility, PureAbility } from '@casl/ability' 8 | import { AblePipe, AblePurePipe } from '../src/public' 9 | 10 | TestBed.initTestEnvironment( 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting() 13 | ) 14 | 15 | let appIndex = 0 16 | export const createApp = (template) => { 17 | @Component({ 18 | selector: `app-ability-${++appIndex}`, 19 | template, 20 | inputs: ['post'], 21 | standalone: false, 22 | }) 23 | class App {} 24 | 25 | return App 26 | } 27 | 28 | export class Post { 29 | constructor(attrs) { 30 | Object.assign(this, attrs) 31 | } 32 | } 33 | 34 | export function createComponent>( 35 | ComponentType: T, 36 | inputs?: Record 37 | ): ComponentFixture> { 38 | const cmp = TestBed.createComponent(ComponentType) as ComponentFixture> 39 | Object.assign(cmp.componentInstance, inputs) 40 | cmp.detectChanges() 41 | 42 | return cmp 43 | } 44 | 45 | export function configureTestingModule(declarations: Type[] = []) { 46 | TestBed.configureTestingModule({ 47 | imports: [AblePipe, AblePurePipe], 48 | declarations, 49 | providers: [ 50 | { provide: PureAbility, useFactory: () => createMongoAbility() } 51 | ] 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /packages/casl-angular/src/AbilityService.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { AnyAbility, PureAbility } from '@casl/ability'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AbilityService { 7 | readonly ability$: Observable; 8 | 9 | constructor(@Inject(PureAbility) ability: T) { 10 | this.ability$ = new Observable((observer) => { 11 | observer.next(ability); 12 | return ability.on('updated', () => observer.next(ability)); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/casl-angular/src/AbilityServiceSignal.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable, OnDestroy, signal } from "@angular/core"; 2 | import { AnyAbility, PureAbility, RawRuleOf } from "@casl/ability"; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class AbilityServiceSignal implements OnDestroy { 6 | private readonly _rules = signal[]>([]); 7 | private readonly _ability = inject(PureAbility) as unknown as T; 8 | private readonly _disposeAbilitySubscription: () => void; 9 | 10 | constructor() { 11 | this._disposeAbilitySubscription = this._ability.on('updated', (event) => { 12 | this._rules.set(event.rules as any); 13 | }); 14 | } 15 | 16 | ngOnDestroy(): void { 17 | this._disposeAbilitySubscription(); 18 | } 19 | 20 | can = (...args: Parameters): boolean => { 21 | this._rules(); // generate side effect for angular to track changes in this signal 22 | return this._ability.can(...args); 23 | }; 24 | 25 | cannot = (...args: Parameters): boolean => { 26 | return !this.can(...args); 27 | }; 28 | 29 | update(rules: T['rules']): void { 30 | this._ability.update(rules); 31 | } 32 | } -------------------------------------------------------------------------------- /packages/casl-angular/src/pipes.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, Inject, PipeTransform } from '@angular/core'; 2 | import { PureAbility, AnyAbility } from '@casl/ability'; 3 | import { Observable } from 'rxjs'; 4 | 5 | /** 6 | * @deprecated use AbilityService instead 7 | */ 8 | @Pipe({ name: 'able', pure: false, standalone: true }) 9 | export class AblePipe implements PipeTransform { 10 | private _ability: T; 11 | 12 | constructor(@Inject(PureAbility) ability: T) { 13 | this._ability = ability; 14 | } 15 | 16 | transform(...args: Parameters): boolean { 17 | return this._ability.can(...args); 18 | } 19 | } 20 | 21 | /** 22 | * @deprecated use AbilityService instead 23 | */ 24 | @Pipe({ name: 'ablePure', standalone: true }) 25 | export class AblePurePipe implements PipeTransform { 26 | private _ability: T; 27 | 28 | constructor(@Inject(PureAbility) ability: T) { 29 | this._ability = ability; 30 | } 31 | 32 | transform(...args: Parameters): Observable { 33 | return new Observable((s) => { 34 | const emit = () => s.next(this._ability.can(...args)); 35 | emit(); 36 | return this._ability.on('updated', emit); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/casl-angular/src/public.ts: -------------------------------------------------------------------------------- 1 | export * from './pipes'; 2 | export * from './AbilityService'; 3 | export * from './AbilityServiceSignal'; 4 | -------------------------------------------------------------------------------- /packages/casl-angular/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/public.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/casl-angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "inlineSources": true, 5 | "inlineSourceMap": true, 6 | "declaration": false, 7 | "module": "es2020", 8 | "target": "es2017", 9 | "skipLibCheck": true, 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "allowJs": true, 15 | "importHelpers": true, 16 | "noEmitHelpers": true, 17 | "lib": [ 18 | "es2019", 19 | "dom" 20 | ] 21 | }, 22 | "include": [ 23 | "src/*", 24 | "spec/*" 25 | ], 26 | "angularCompilerOptions": { 27 | "enableIvy": true, 28 | "skipMetadataEmit": true, 29 | "flatModuleOutFile": "index.js", 30 | "flatModuleId": "@casl/angular" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/casl-angular/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "types": ["jest"] 6 | }, 7 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/casl-angular/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "dist/types" 6 | }, 7 | "angularCompilerOptions": { 8 | "skipMetadataEmit": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/casl-aurelia/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-aurelia/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types/index'; 2 | -------------------------------------------------------------------------------- /packages/casl-aurelia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/aurelia", 3 | "version": "1.3.1", 4 | "description": "Aurelia plugin for CASL which makes it easy to add permissions in any Aurelia apps", 5 | "main": "dist/umd/index.js", 6 | "module": "dist/es5m/index.js", 7 | "es2015": "dist/es6m/index.mjs", 8 | "typings": "dist/types/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/types/index.d.ts", 12 | "import": "./dist/es6m/index.mjs", 13 | "require": "./dist/umd/index.js" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/stalniy/casl.git", 19 | "directory": "packages/casl-aurelia" 20 | }, 21 | "homepage": "https://casl.js.org", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build.prepare": "rm -rf dist/* && npm run build.types", 27 | "build": "npm run build.prepare && BUILD_TYPES=es5m,es6m,umd dx rollup -n casl.au -g aurelia-framework:au,@casl/ability:casl", 28 | "build.types": "dx tsc", 29 | "lint": "dx eslint src/", 30 | "test": "dx jest --env jsdom --config ../dx/config/jest.chai.config.js", 31 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 32 | "release": "npm run release.prepare && dx semantic-release" 33 | }, 34 | "keywords": [ 35 | "casl", 36 | "aurelia", 37 | "authorization", 38 | "acl", 39 | "permissions" 40 | ], 41 | "author": "Sergii Stotskyi ", 42 | "license": "MIT", 43 | "peerDependencies": { 44 | "@casl/ability": "^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", 45 | "aurelia-framework": "^1.3.1" 46 | }, 47 | "devDependencies": { 48 | "@casl/ability": "^6.0.0", 49 | "@casl/dx": "workspace:^1.0.0", 50 | "@types/jest": "^29.0.0", 51 | "aurelia-bootstrapper": "^2.3.0", 52 | "aurelia-framework": "^1.3.1", 53 | "aurelia-loader-nodejs": "^1.0.1", 54 | "aurelia-pal-browser": "^1.8.0", 55 | "aurelia-polyfills": "^1.3.0", 56 | "aurelia-testing": "^1.0.0-beta.4.0.0", 57 | "chai": "^4.1.0", 58 | "chai-spies": "^1.0.0" 59 | }, 60 | "files": [ 61 | "dist", 62 | "*.d.ts" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /packages/casl-aurelia/spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/casl-aurelia/spec/spec_helper.js: -------------------------------------------------------------------------------- 1 | import 'aurelia-polyfills' 2 | import { Options } from 'aurelia-loader-nodejs' 3 | import { join } from 'path' 4 | 5 | process.browser = true 6 | Options.relativeToDir = join(__dirname, '..', 'src') 7 | 8 | -------------------------------------------------------------------------------- /packages/casl-aurelia/src/index.ts: -------------------------------------------------------------------------------- 1 | import { FrameworkConfiguration } from 'aurelia-framework'; 2 | import { PureAbility, AnyAbility } from '@casl/ability'; 3 | import { CanValueConverter, AbleValueConverter } from './value-converter/can'; 4 | 5 | export { CanValueConverter, AbleValueConverter } from './value-converter/can'; 6 | 7 | export function configure(config: FrameworkConfiguration, defaultAbility?: AnyAbility) { 8 | if (defaultAbility && defaultAbility instanceof PureAbility) { 9 | config.container.registerInstance(PureAbility, defaultAbility); 10 | } 11 | 12 | config.globalResources([ 13 | CanValueConverter, 14 | AbleValueConverter 15 | ]); 16 | } 17 | -------------------------------------------------------------------------------- /packages/casl-aurelia/src/value-converter/can.ts: -------------------------------------------------------------------------------- 1 | import { signalBindings } from 'aurelia-framework'; 2 | import { PureAbility, AnyAbility } from '@casl/ability'; 3 | 4 | const ABILITY_CHANGED_SIGNAL = 'caslAbilityChanged'; 5 | const HAS_AU_SUBSCRIPTION = new WeakMap(); 6 | 7 | class AbilityValueConverter { 8 | static inject = [PureAbility]; 9 | 10 | public readonly signals = [ABILITY_CHANGED_SIGNAL]; 11 | protected readonly _ability!: T; 12 | 13 | constructor(ability: T) { 14 | this._ability = ability; 15 | } 16 | 17 | can(...args: Parameters): boolean { 18 | if (!HAS_AU_SUBSCRIPTION.has(this._ability)) { 19 | this._ability.on('updated', () => signalBindings(ABILITY_CHANGED_SIGNAL)); 20 | HAS_AU_SUBSCRIPTION.set(this._ability, true); 21 | } 22 | 23 | return this._ability.can(...args as [any, any?, any?]); 24 | } 25 | } 26 | 27 | /** @deprecated use "able" value converter instead */ 28 | export class CanValueConverter extends AbilityValueConverter { 29 | static $resource = { 30 | name: 'can', 31 | type: 'valueConverter' 32 | }; 33 | 34 | toView( 35 | subject: Parameters[1], 36 | action: Parameters[0], 37 | field?: string 38 | ): boolean { 39 | return (this as any).can(action, subject, field); 40 | } 41 | } 42 | 43 | export class AbleValueConverter extends AbilityValueConverter { 44 | static $resource = { 45 | name: 'able', 46 | type: 'valueConverter' 47 | }; 48 | 49 | toView(...args: Parameters): boolean { 50 | return this.can(...args); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/casl-aurelia/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "spec/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/casl-aurelia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "compilerOptions": { 8 | "outDir": "dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/casl-mongoose/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-mongoose/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/mongoose", 3 | "version": "8.0.3", 4 | "description": "Allows to query accessible records from MongoDB based on CASL rules", 5 | "main": "dist/es6c/index.js", 6 | "es2015": "dist/es6m/index.mjs", 7 | "typings": "dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/types/index.d.ts", 11 | "import": "./dist/es6m/index.mjs", 12 | "require": "./dist/es6c/index.js" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/stalniy/casl.git", 18 | "directory": "packages/casl-mongoose" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "homepage": "https://casl.js.org", 24 | "scripts": { 25 | "build.prepare": "rm -rf dist/* && npm run build.types", 26 | "build": "npm run build.prepare && BUILD_TYPES=es6m,es6c dx rollup -e @casl/ability/extra,@casl/ability,mongoose", 27 | "build.types": "dx tsc", 28 | "lint": "dx eslint src/ spec/", 29 | "test": "dx jest", 30 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 31 | "release": "npm run release.prepare && dx semantic-release" 32 | }, 33 | "keywords": [ 34 | "casl", 35 | "mongo", 36 | "authorization", 37 | "acl", 38 | "permissions" 39 | ], 40 | "author": "Sergii Stotskyi ", 41 | "license": "MIT", 42 | "peerDependencies": { 43 | "@casl/ability": "^6.7.0", 44 | "mongoose": "^6.0.13 || ^7.0.0 || ^8.0.0" 45 | }, 46 | "devDependencies": { 47 | "@casl/ability": "^6.0.0", 48 | "@casl/dx": "workspace:^1.0.0", 49 | "@types/jest": "^29.0.0", 50 | "chai": "^4.1.0", 51 | "chai-spies": "^1.0.0", 52 | "mongoose": "^8.0.0" 53 | }, 54 | "files": [ 55 | "dist", 56 | "*.d.ts", 57 | "index.js" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /packages/casl-mongoose/spec/accessibleFieldsBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { MongoAbility, createMongoAbility, defineAbility } from "@casl/ability" 2 | import { accessibleFieldsBy } from "../src" 3 | import mongoose from "mongoose" 4 | 5 | describe('accessibleFieldsBy', () => { 6 | type AppAbility = MongoAbility<[string, Post | mongoose.Model | 'Post']> 7 | interface Post { 8 | _id: string; 9 | title: string; 10 | state: string; 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-redeclare 14 | const Post = mongoose.model('Post', new mongoose.Schema({ 15 | title: String, 16 | state: String, 17 | })) 18 | 19 | describe('when subject type is a mongoose model', () => { 20 | testWithSubjectType(Post, Post) 21 | }) 22 | 23 | describe('when subject type is a mongoose model name', () => { 24 | testWithSubjectType('Post', Post) 25 | }) 26 | 27 | function testWithSubjectType(type: mongoose.Model | 'Post', Model: mongoose.Model) { 28 | it('returns empty array for empty `Ability` instance', () => { 29 | const fields = accessibleFieldsBy(createMongoAbility()).ofType(type) 30 | 31 | expect(fields).toBeInstanceOf(Array) 32 | expect(fields).toHaveLength(0) 33 | }) 34 | 35 | it('returns all fields for model if ability does not have restrictions on rules', () => { 36 | const ability = defineAbility(can => can('read', type)) 37 | 38 | expect(accessibleFieldsBy(ability).ofType(type).sort()) 39 | .toEqual(['_id', '__v', 'title', 'state'].sort()) 40 | }) 41 | 42 | it('returns fields for `read` action by default', () => { 43 | const ability = defineAbility(can => can('read', type, ['title', 'state'])) 44 | 45 | expect(accessibleFieldsBy(ability).ofType(type)).toEqual(['title', 'state']) 46 | }) 47 | 48 | it('returns fields for an action specified as 2nd parameter', () => { 49 | const ability = defineAbility(can => can('update', type, ['title', 'state'])) 50 | 51 | expect(accessibleFieldsBy(ability, 'update').ofType(type)).toEqual(['title', 'state']) 52 | }) 53 | 54 | it('returns fields permitted for the instance when called on model instance', () => { 55 | const ability = defineAbility((can) => { 56 | can('update', type, ['title', 'state'], { state: 'draft' }) 57 | can('update', type, ['title'], { state: 'public' }) 58 | }) 59 | const post = new Model({ state: 'public' }) 60 | 61 | expect(accessibleFieldsBy(ability, 'update').of(post)).toEqual(['title']) 62 | }) 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /packages/casl-mongoose/src/accessibleBy.ts: -------------------------------------------------------------------------------- 1 | import { AnyMongoAbility, Generics, SubjectType, Abilities, AbilityTuple, ExtractSubjectType } from '@casl/ability'; 2 | import { rulesToQuery } from '@casl/ability/extra'; 3 | 4 | function convertToMongoQuery(rule: AnyMongoAbility['rules'][number]) { 5 | const conditions = rule.conditions!; 6 | return rule.inverted ? { $nor: [conditions] } : conditions; 7 | } 8 | 9 | export const EMPTY_RESULT_QUERY = { $expr: { $eq: [0, 1] } }; 10 | export class AccessibleRecords { 11 | constructor( 12 | private readonly _ability: AnyMongoAbility, 13 | private readonly _action: string 14 | ) {} 15 | 16 | /** 17 | * In case action is not allowed, it returns `{ $expr: { $eq: [0, 1] } }` 18 | */ 19 | ofType(subjectType: T): Record { 20 | const query = rulesToQuery(this._ability, this._action, subjectType, convertToMongoQuery); 21 | return query === null ? EMPTY_RESULT_QUERY : query as Record; 22 | } 23 | } 24 | 25 | type SubjectTypes = T extends AbilityTuple 26 | ? ExtractSubjectType 27 | : never; 28 | 29 | /** 30 | * Returns accessible records Mongo query per record type (i.e., entity type) based on provided Ability and action. 31 | */ 32 | export function accessibleBy( 33 | ability: T, 34 | action: Parameters[0] = 'read' 35 | ): AccessibleRecords['abilities']>> { 36 | return new AccessibleRecords(ability, action); 37 | } 38 | -------------------------------------------------------------------------------- /packages/casl-mongoose/src/accessibleFieldsBy.ts: -------------------------------------------------------------------------------- 1 | import { AnyMongoAbility, Generics } from "@casl/ability"; 2 | import { AccessibleFields, GetSubjectTypeAllFieldsExtractor } from "@casl/ability/extra"; 3 | import mongoose from 'mongoose'; 4 | 5 | const getSubjectTypeAllFieldsExtractor: GetSubjectTypeAllFieldsExtractor = (type) => { 6 | const Model = typeof type === 'string' ? mongoose.models[type] : type; 7 | if (!Model) throw new Error(`Unknown mongoose model "${type}"`); 8 | return 'schema' in Model ? Object.keys((Model.schema as any).paths) : []; 9 | }; 10 | 11 | export function accessibleFieldsBy( 12 | ability: T, 13 | action: Parameters[0] = 'read' 14 | ): AccessibleFields['abilities'], unknown[]>[1]> { 15 | return new AccessibleFields(ability, action, getSubjectTypeAllFieldsExtractor); 16 | } 17 | -------------------------------------------------------------------------------- /packages/casl-mongoose/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AccessibleFieldDocumentMethods, AccessibleFieldsModel } from './plugins/accessible_fields'; 2 | import { AccessibleRecordModel, AccessibleRecordQueryHelpers } from './plugins/accessible_records'; 3 | 4 | export interface AccessibleModel< 5 | T, 6 | TQueryHelpers = unknown, 7 | TMethods = unknown, 8 | TVirtuals = unknown 9 | > 10 | extends 11 | AccessibleRecordModel, TVirtuals>, 12 | AccessibleFieldsModel, 16 | TVirtuals 17 | >, TMethods, TVirtuals> 18 | {} 19 | 20 | export { accessibleRecordsPlugin } from './plugins/accessible_records'; 21 | export type { AccessibleRecordModel } from './plugins/accessible_records'; 22 | export { getSchemaPaths, accessibleFieldsPlugin } from './plugins/accessible_fields'; 23 | export type { 24 | AccessibleFieldsModel, 25 | AccessibleFieldsDocument, 26 | AccessibleFieldsOptions 27 | } from './plugins/accessible_fields'; 28 | 29 | export { accessibleBy } from './accessibleBy'; 30 | export type { AccessibleRecords } from './accessibleBy'; 31 | 32 | export { accessibleFieldsBy } from './accessibleFieldsBy'; 33 | -------------------------------------------------------------------------------- /packages/casl-mongoose/src/plugins/accessible_records.ts: -------------------------------------------------------------------------------- 1 | import { AnyMongoAbility, Generics, Normalize } from '@casl/ability'; 2 | import type { Document as Doc, HydratedDocument, Model, Query, QueryWithHelpers, Schema } from 'mongoose'; 3 | import { accessibleBy } from '../accessibleBy'; 4 | 5 | function accessibleRecords( 6 | baseQuery: Query, 7 | ability: T, 8 | action?: Normalize['abilities']>[0] 9 | ): QueryWithHelpers { 10 | const subjectType = ability.detectSubjectType({ 11 | constructor: baseQuery.model 12 | }); 13 | 14 | if (!subjectType) { 15 | throw new TypeError(`Cannot detect subject type of "${baseQuery.model.modelName}" to return accessible records`); 16 | } 17 | 18 | const query = accessibleBy(ability, action).ofType(subjectType); 19 | 20 | return baseQuery.and([query]); 21 | } 22 | 23 | type GetAccessibleRecords = ( 24 | ability: U, 25 | action?: Normalize['abilities']>[0] 26 | ) => QueryWithHelpers>; 27 | 28 | export type AccessibleRecordQueryHelpers = { 29 | /** @deprecated use accessibleBy helper instead */ 30 | accessibleBy: GetAccessibleRecords< 31 | HydratedDocument, 32 | TQueryHelpers, 33 | TMethods, 34 | TVirtuals 35 | > 36 | }; 37 | export interface AccessibleRecordModel< 38 | T, 39 | TQueryHelpers = {}, 40 | TMethods = {}, 41 | TVirtuals = {} 42 | > extends Model, 44 | TMethods, 45 | TVirtuals> { 46 | /** @deprecated use accessibleBy helper instead */ 47 | accessibleBy: GetAccessibleRecords< 48 | HydratedDocument, 49 | TQueryHelpers, 50 | TMethods, 51 | TVirtuals 52 | > 53 | } 54 | 55 | function modelAccessibleBy(this: Model, ability: AnyMongoAbility, action?: string) { 56 | return accessibleRecords(this.where(), ability, action); 57 | } 58 | 59 | function queryAccessibleBy( 60 | this: Query, 61 | ability: AnyMongoAbility, 62 | action?: string 63 | ) { 64 | return accessibleRecords(this, ability, action); 65 | } 66 | 67 | export function accessibleRecordsPlugin(schema: Schema): void { 68 | (schema.query as Record).accessibleBy = queryAccessibleBy; 69 | schema.statics.accessibleBy = modelAccessibleBy; 70 | } 71 | -------------------------------------------------------------------------------- /packages/casl-mongoose/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "spec/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/casl-mongoose/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "compilerOptions": { 8 | "outDir": "dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/casl-prisma/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2021 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-prisma/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/prisma", 3 | "version": "1.5.1", 4 | "description": "Allows to query accessible records using Prisma client based on CASL rules", 5 | "main": "dist/es6c/index.js", 6 | "es2015": "dist/es6m/index.mjs", 7 | "typings": "dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/types/index.d.ts", 11 | "import": "./dist/es6m/index.mjs", 12 | "require": "./dist/es6c/index.js" 13 | }, 14 | "./runtime": { 15 | "types": "./dist/types/runtime.d.ts", 16 | "import": "./dist/es6m/runtime.mjs", 17 | "require": "./dist/es6c/runtime.js" 18 | } 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/stalniy/casl.git", 23 | "directory": "packages/casl-prisma" 24 | }, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "homepage": "https://casl.js.org", 29 | "scripts": { 30 | "build.prepare": "rm -rf dist/* && npm run build.types && npm run build.runtime", 31 | "build.runtime": "BUILD_TYPES=es6m,es6c dx rollup -i src/runtime.ts -e @casl/ability/extra,@casl/ability,@prisma/client,@ucast/core,@ucast/js", 32 | "build": "npm run build.prepare && BUILD_TYPES=es6m,es6c dx rollup -e @casl/ability/extra,@casl/ability,@prisma/client,@ucast/core,@ucast/js,./runtime", 33 | "build.types": "dx tsc", 34 | "lint": "dx eslint src/ spec/", 35 | "test": "dx jest", 36 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 37 | "release": "npm run release.prepare && dx semantic-release", 38 | "prepare": "prisma generate" 39 | }, 40 | "keywords": [ 41 | "casl", 42 | "sql", 43 | "authorization", 44 | "acl", 45 | "permissions" 46 | ], 47 | "author": "Sergii Stotskyi ", 48 | "license": "MIT", 49 | "peerDependencies": { 50 | "@casl/ability": "^5.3.0 || ^6.0.0", 51 | "@prisma/client": "^2.14.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" 52 | }, 53 | "devDependencies": { 54 | "@casl/ability": "^6.0.0", 55 | "@casl/dx": "workspace:^1.0.0", 56 | "@prisma/client": "^6.0.0", 57 | "@types/jest": "^29.0.0", 58 | "prisma": "^6.0.0" 59 | }, 60 | "files": [ 61 | "dist", 62 | "*.d.ts", 63 | "runtime.js" 64 | ], 65 | "dependencies": { 66 | "@ucast/core": "^1.10.0", 67 | "@ucast/js": "^3.0.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/casl-prisma/runtime.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types/runtime'; 2 | -------------------------------------------------------------------------------- /packages/casl-prisma/runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/es6c/runtime'); 2 | -------------------------------------------------------------------------------- /packages/casl-prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id Int @id 12 | firstName String 13 | lastName String 14 | age Int 15 | verified Boolean? 16 | posts Post[] 17 | } 18 | 19 | model Post { 20 | id Int @id 21 | title String 22 | author User @relation(fields: [authorId], references: [id]) 23 | authorId Int 24 | } 25 | -------------------------------------------------------------------------------- /packages/casl-prisma/spec/AppAbility.ts: -------------------------------------------------------------------------------- 1 | import { PureAbility } from '@casl/ability' 2 | import { User, Post } from '@prisma/client' 3 | import { PrismaQuery, Subjects } from '../src' 4 | 5 | export type AppAbility = PureAbility<[string, 'all' | Subjects<{ 6 | User: User, 7 | Post: Post 8 | }>], PrismaQuery> 9 | -------------------------------------------------------------------------------- /packages/casl-prisma/spec/accessibleBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenError } from '@casl/ability' 2 | import { accessibleBy, createPrismaAbility } from '../src' 3 | import { AppAbility } from './AppAbility' 4 | 5 | describe('accessibleBy', () => { 6 | const ability = createPrismaAbility([ 7 | { 8 | action: 'read', 9 | subject: 'Post', 10 | conditions: { 11 | id: 1 12 | } 13 | }, 14 | { 15 | inverted: true, 16 | action: 'read', 17 | subject: 'Post', 18 | conditions: { 19 | title: { startsWith: '[WIP]:' } 20 | } 21 | }, 22 | ]) 23 | 24 | it('throws `ForbiddenError` if ability does not allow to execute action', () => { 25 | expect(() => accessibleBy(ability, 'update').Post).toThrow(ForbiddenError as unknown as Error) 26 | }) 27 | 28 | it('wraps inverted rules in `NOT` operator', () => { 29 | const query = accessibleBy(ability).Post 30 | 31 | expect(query.AND).toEqual([{ 32 | NOT: { 33 | title: { startsWith: '[WIP]:' } 34 | } 35 | }]) 36 | }) 37 | 38 | it('wraps regular rules in OR and inverted ones in AND', () => { 39 | const query = accessibleBy(ability).Post 40 | 41 | expect(query).toEqual({ 42 | AND: [{ 43 | NOT: { 44 | title: { startsWith: '[WIP]:' } 45 | } 46 | }], 47 | OR: [{ 48 | id: 1 49 | }] 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/accessibleByFactory.ts: -------------------------------------------------------------------------------- 1 | import { rulesToQuery } from '@casl/ability/extra'; 2 | import { AnyAbility, ForbiddenError, PureAbility } from '@casl/ability'; 3 | 4 | function convertToPrismaQuery(rule: AnyAbility['rules'][number]) { 5 | return rule.inverted ? { NOT: rule.conditions } : rule.conditions; 6 | } 7 | 8 | const proxyHandlers: ProxyHandler<{ _ability: AnyAbility, _action: string }> = { 9 | get(target, subjectType) { 10 | const query = rulesToQuery(target._ability, target._action, subjectType, convertToPrismaQuery); 11 | 12 | if (query === null) { 13 | const error = ForbiddenError.from(target._ability) 14 | .setMessage(`It's not allowed to run "${target._action}" on "${subjectType as string}"`); 15 | error.action = target._action; 16 | error.subjectType = error.subject = subjectType as string; 17 | throw error; 18 | } 19 | 20 | const prismaQuery = Object.create(null); 21 | 22 | if (query.$or) { 23 | prismaQuery.OR = query.$or; 24 | } 25 | 26 | if (query.$and) { 27 | prismaQuery.AND = query.$and; 28 | } 29 | 30 | return prismaQuery; 31 | } 32 | }; 33 | 34 | export const createAccessibleByFactory = < 35 | TResult extends Record, 36 | TPrismaQuery 37 | >() => { 38 | return function accessibleBy(ability: PureAbility, action = 'read'): TResult { 39 | return new Proxy({ 40 | _ability: ability, 41 | _action: action 42 | }, proxyHandlers) as unknown as TResult; 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/createAbilityFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbilityOptions, 3 | AbilityOptionsOf, 4 | AbilityTuple, 5 | fieldPatternMatcher, 6 | PureAbility, 7 | RawRuleFrom, 8 | RawRuleOf 9 | } from '@casl/ability'; 10 | import { prismaQuery } from './prisma/prismaQuery'; 11 | 12 | export function createAbilityFactory< 13 | TModelName extends string, 14 | TPrismaQuery extends Record 15 | >() { 16 | function createAbility< 17 | T extends PureAbility 18 | >(rules?: RawRuleOf[], options?: AbilityOptionsOf): T; 19 | function createAbility< 20 | A extends AbilityTuple = [string, TModelName], 21 | C extends TPrismaQuery = TPrismaQuery 22 | >( 23 | rules?: RawRuleFrom[], 24 | options?: AbilityOptions 25 | ): PureAbility; 26 | function createAbility(rules: any[] = [], options = {}): PureAbility { 27 | return new PureAbility(rules, { 28 | ...options, 29 | conditionsMatcher: prismaQuery, 30 | fieldMatcher: fieldPatternMatcher, 31 | }); 32 | } 33 | 34 | return createAbility; 35 | } 36 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/errors/ParsingQueryError.ts: -------------------------------------------------------------------------------- 1 | export class ParsingQueryError extends Error { 2 | static invalidArgument(operatorName: string, value: unknown, expectValueType: string) { 3 | const valueType = `${typeof value}(${JSON.stringify(value, null, 2)})`; 4 | return new this( 5 | `"${operatorName}" expects to receive ${expectValueType} but instead got "${valueType}"` 6 | ); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AbilityOptions, AbilityTuple, fieldPatternMatcher, PureAbility, RawRuleFrom } from '@casl/ability'; 2 | import { createAbilityFactory, createAccessibleByFactory, prismaQuery } from './runtime'; 3 | import type { WhereInputPerModel, ModelName, PrismaQuery } from './prismaClientBoundTypes'; 4 | 5 | export type { PrismaQuery, WhereInput } from './prismaClientBoundTypes'; 6 | export type { Model, Subjects } from './runtime'; 7 | export { prismaQuery, ParsingQueryError } from './runtime'; 8 | 9 | const createPrismaAbility = createAbilityFactory(); 10 | const accessibleBy = createAccessibleByFactory(); 11 | 12 | export { 13 | createPrismaAbility, 14 | accessibleBy, 15 | }; 16 | 17 | /** 18 | * Uses conditional type to support union distribution 19 | */ 20 | type ExtendedAbilityTuple = T extends AbilityTuple 21 | ? [T[0], 'all' | T[1]] 22 | : never; 23 | 24 | /** 25 | * @deprecated use createPrismaAbility instead 26 | */ 27 | export class PrismaAbility< 28 | A extends AbilityTuple = [string, ModelName], 29 | C extends PrismaQuery = PrismaQuery 30 | > extends PureAbility, C> { 31 | constructor( 32 | rules?: RawRuleFrom, C>[], 33 | options?: AbilityOptions, C> 34 | ) { 35 | super(rules, { 36 | conditionsMatcher: prismaQuery, 37 | fieldMatcher: fieldPatternMatcher, 38 | ...options, 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/prisma/prismaQuery.ts: -------------------------------------------------------------------------------- 1 | import { AnyInterpreter, createTranslatorFactory } from '@ucast/core'; 2 | import { ForcedSubject } from '@casl/ability'; 3 | import { PrismaQueryParser } from './PrismaQueryParser'; 4 | import { interpretPrismaQuery } from './interpretPrismaQuery'; 5 | 6 | const parser = new PrismaQueryParser(); 7 | export const prismaQuery = createTranslatorFactory( 8 | parser.parse, 9 | interpretPrismaQuery as AnyInterpreter 10 | ); 11 | 12 | export type Model = T & ForcedSubject; 13 | export type Subjects>>> = 14 | | keyof T 15 | | { [K in keyof T]: Model }[keyof T]; 16 | 17 | /** 18 | * Extracts Prisma model name from given object and possible list of all subjects 19 | */ 20 | export type ExtractModelName< 21 | TObject, 22 | TModelName extends string 23 | > = TObject extends { kind: TModelName } 24 | ? TObject['kind'] 25 | : TObject extends ForcedSubject 26 | ? TObject['__caslSubjectType__'] 27 | : TObject extends { __typename: TModelName } 28 | ? TObject['__typename'] 29 | : TModelName; 30 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/prismaClientBoundTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma, PrismaClient } from '@prisma/client'; 2 | import type { hkt } from '@casl/ability'; 3 | import type { ExtractModelName, Model } from './prisma/prismaQuery'; 4 | 5 | export type ModelName = Prisma.ModelName; 6 | 7 | type ModelWhereInput = { 8 | [K in Prisma.ModelName]: Uncapitalize extends keyof PrismaClient 9 | ? Extract]['findFirst']>[0], { where?: any }>['where'] 10 | : never 11 | }; 12 | 13 | export type WhereInput = Extract< 14 | ModelWhereInput[TModelName], 15 | Record 16 | >; 17 | 18 | interface PrismaQueryTypeFactory extends hkt.GenericFactory { 19 | produce: WhereInput> 20 | } 21 | 22 | type PrismaModel = Model, string>; 23 | export type PrismaQuery = 24 | WhereInput> & hkt.Container; 25 | 26 | export type WhereInputPerModel = { 27 | [K in ModelName]: WhereInput; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/casl-prisma/src/runtime.ts: -------------------------------------------------------------------------------- 1 | export { prismaQuery } from './prisma/prismaQuery'; 2 | export type { Model, Subjects, ExtractModelName } from './prisma/prismaQuery'; 3 | export { createAccessibleByFactory } from './accessibleByFactory'; 4 | export { createAbilityFactory } from './createAbilityFactory'; 5 | export { ParsingQueryError } from './errors/ParsingQueryError'; 6 | -------------------------------------------------------------------------------- /packages/casl-prisma/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "spec/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/casl-prisma/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "compilerOptions": { 8 | "outDir": "dist/types" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/casl-react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-react/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/types'; 2 | -------------------------------------------------------------------------------- /packages/casl-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/react", 3 | "version": "5.0.0", 4 | "description": "React component for CASL which makes it easy to add permissions in any React application", 5 | "main": "dist/umd/index.js", 6 | "module": "dist/es5m/index.js", 7 | "es2015": "dist/es6m/index.mjs", 8 | "typings": "./index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/types/index.d.ts", 12 | "import": "./dist/es6m/index.mjs", 13 | "require": "./dist/umd/index.js" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/stalniy/casl.git", 19 | "directory": "packages/casl-react" 20 | }, 21 | "homepage": "https://casl.js.org", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build.prepare": "rm -rf dist/* && npm run build.types", 27 | "build": "npm run build.prepare && BUILD_TYPES=es5m,es6m,umd dx rollup -n casl.react -g react:React,prop-types:React.PropTypes,@casl/ability:casl", 28 | "build.types": "dx tsc", 29 | "lint": "dx eslint src/ spec/", 30 | "test": "dx jest --env jsdom", 31 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 32 | "release": "npm run release.prepare && dx semantic-release" 33 | }, 34 | "keywords": [ 35 | "casl", 36 | "react", 37 | "authorization", 38 | "acl", 39 | "permissions" 40 | ], 41 | "author": "Sergii Stotskyi ", 42 | "license": "MIT", 43 | "peerDependencies": { 44 | "@casl/ability": "^4.0.0 || ^5.1.0 || ^6.0.0", 45 | "react": "^17.0.0 || ^18.0.0 || ^19.0.0" 46 | }, 47 | "devDependencies": { 48 | "@casl/ability": "^6.0.0", 49 | "@casl/dx": "workspace:^1.0.0", 50 | "@testing-library/dom": "^10.4.0", 51 | "@testing-library/react": "^16.1.0", 52 | "@types/jest": "^29.0.0", 53 | "@types/node": "^22.0.0", 54 | "@types/react": "^19.0.0", 55 | "chai": "^4.1.0", 56 | "chai-spies": "^1.0.0", 57 | "react": "^19.0.0", 58 | "react-dom": "^19.0.0" 59 | }, 60 | "files": [ 61 | "dist", 62 | "*.d.ts" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /packages/casl-react/spec/factory.spec.tsx: -------------------------------------------------------------------------------- 1 | import { createMongoAbility, defineAbility, MongoAbility } from '@casl/ability' 2 | import React, { createContext } from 'react' 3 | import { render, screen } from '@testing-library/react' 4 | import { BoundCanProps, createContextualCan } from '../src' 5 | 6 | describe('`createContextualCan`', () => { 7 | let ability: MongoAbility 8 | let AbilityContext: React.Context 9 | let ContextualCan: React.FunctionComponent> 10 | 11 | beforeEach(() => { 12 | ability = defineAbility(can => can('read', 'Post')) 13 | AbilityContext = createContext(createMongoAbility()) 14 | ContextualCan = createContextualCan(AbilityContext.Consumer) 15 | }) 16 | 17 | it('allows to override `Ability` instance by passing it in props', () => { 18 | render(I see it) 19 | 20 | expect(screen.queryByText('I see it')).toBeTruthy() 21 | }) 22 | 23 | it('expects `Ability` instance to be provided by context Provider', () => { 24 | render( 25 | I see it 26 | ) 27 | 28 | expect(screen.queryByText('I see it')).toBeTruthy() 29 | }) 30 | 31 | it('should not render anything if ability does not have rules', () => { 32 | render(I see it) 33 | expect(screen.queryByText('I see it')).not.toBeTruthy() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/casl-react/spec/useAbility.spec.ts: -------------------------------------------------------------------------------- 1 | import { createMongoAbility, MongoAbility } from '@casl/ability' 2 | import { act, renderHook } from '@testing-library/react' 3 | import { createContext } from 'react' 4 | import { useAbility } from '../src' 5 | 6 | describe('`useAbility` hook', () => { 7 | let ability: MongoAbility 8 | let AbilityContext: React.Context 9 | 10 | beforeEach(() => { 11 | ability = createMongoAbility() 12 | AbilityContext = createContext(ability) 13 | }) 14 | 15 | it('provides an `Ability` instance from context', () => { 16 | const { result } = renderHook(() => useAbility(AbilityContext)) 17 | expect(result.current).toBe(ability) 18 | }) 19 | 20 | it('triggers re-render when `Ability` rules are changed', () => { 21 | const component = jest.fn(() => useAbility(AbilityContext)) 22 | 23 | renderHook(component) 24 | act(() => { 25 | ability.update([{ action: 'read', subject: 'Post' }]) 26 | }) 27 | 28 | expect(component).toHaveBeenCalledTimes(2) 29 | }) 30 | 31 | it('subscribes to `Ability` instance only once', () => { 32 | jest.spyOn(ability, 'on') 33 | const { rerender } = renderHook(() => useAbility(AbilityContext)) 34 | 35 | act(() => { 36 | rerender() 37 | rerender() 38 | }) 39 | 40 | expect(ability.on).toHaveBeenCalledTimes(1) 41 | }) 42 | 43 | it('unsubscribes from `Ability` when component is destroyed', () => { 44 | const component = jest.fn(() => useAbility(AbilityContext)) 45 | const { unmount } = renderHook(component) 46 | 47 | act(() => { 48 | unmount() 49 | ability.update([{ action: 'read', subject: 'Post' }]) 50 | }) 51 | 52 | expect(component).toHaveBeenCalledTimes(1) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /packages/casl-react/src/factory.ts: -------------------------------------------------------------------------------- 1 | import { AnyAbility } from '@casl/ability'; 2 | import { Consumer, FunctionComponent, createElement } from 'react'; 3 | import { BoundCanProps, Can } from './Can'; 4 | 5 | export function createContextualCan( 6 | Getter: Consumer 7 | ): FunctionComponent> { 8 | return (props: BoundCanProps) => createElement(Getter, { 9 | children: (ability: T) => 10 | createElement(Can, { ...props, ability: props.ability || ability } as any), 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/casl-react/src/hooks/useAbility.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AnyAbility } from '@casl/ability'; 3 | 4 | export function useAbility(context: React.Context): T { 5 | const ability = React.useContext(context); 6 | const [rules, setRules] = React.useState(); 7 | 8 | React.useEffect(() => ability.on('updated', (event) => { 9 | if (event.rules !== rules) { 10 | setRules(event.rules); 11 | } 12 | }), []); 13 | 14 | return ability; 15 | } 16 | -------------------------------------------------------------------------------- /packages/casl-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Can'; 2 | export * from './factory'; 3 | export * from './hooks/useAbility'; 4 | -------------------------------------------------------------------------------- /packages/casl-react/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "exclude": [ 8 | "spec/**/*" 9 | ], 10 | "compilerOptions": { 11 | "outDir": "dist/types", 12 | "allowSyntheticDefaultImports": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/casl-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "compilerOptions": { 8 | "jsx": "react", 9 | "outDir": "dist/types", 10 | "allowSyntheticDefaultImports": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/casl-vue/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Sergii Stotskyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/casl-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/vue", 3 | "version": "2.2.2", 4 | "description": "Vue plugin for CASL which makes it easy to add permissions in any Vue application", 5 | "main": "dist/umd/index.js", 6 | "module": "dist/es5m/index.js", 7 | "es2015": "dist/es6m/index.mjs", 8 | "typings": "dist/types/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/types/index.d.ts", 12 | "import": "./dist/es6m/index.mjs", 13 | "require": "./dist/umd/index.js" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/stalniy/casl.git", 19 | "directory": "packages/casl-vue" 20 | }, 21 | "homepage": "https://casl.js.org", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build.prepare": "rm -rf dist/* && npm run build.types", 27 | "build": "npm run build.prepare && BUILD_TYPES=es5m,es6m,umd dx rollup -n casl.vue -g vue:Vue,@casl/ability:casl", 28 | "build.types": "dx tsc -p tsconfig.build.json", 29 | "lint": "dx eslint src/ spec/", 30 | "test": "dx jest --env jsdom --config ../dx/config/jest.chai.config.js", 31 | "release.prepare": "npm run lint && npm test && NODE_ENV=production npm run build", 32 | "release": "npm run release.prepare && dx semantic-release" 33 | }, 34 | "keywords": [ 35 | "casl", 36 | "vue", 37 | "authorization", 38 | "acl", 39 | "permissions" 40 | ], 41 | "author": "Sergii Stotskyi ", 42 | "license": "MIT", 43 | "peerDependencies": { 44 | "@casl/ability": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0", 45 | "vue": "^3.0.0" 46 | }, 47 | "devDependencies": { 48 | "@casl/ability": "^6.0.0", 49 | "@casl/dx": "workspace:^1.0.0", 50 | "@types/jest": "^29.0.0", 51 | "chai": "^4.1.0", 52 | "chai-spies": "^1.0.0", 53 | "vue": "^3.2.45" 54 | }, 55 | "files": [ 56 | "dist", 57 | "*.d.ts" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /packages/casl-vue/spec/hooks.spec.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import { Ability } from '@casl/ability' 3 | import { provideAbility, useAbility } from '../src' 4 | 5 | describe('Vue hooks', () => { 6 | let ability 7 | let root 8 | const Child = { 9 | setup() { 10 | try { 11 | useAbility() 12 | return () => 'Provided' 13 | } catch (e) { 14 | return () => e.message 15 | } 16 | } 17 | } 18 | 19 | beforeEach(() => { 20 | ability = new Ability() 21 | root = document.createElement('div') 22 | }) 23 | 24 | describe('provideAbility', () => { 25 | it('provides reactive `Ability` instance', () => { 26 | createApp({ 27 | setup() { 28 | provideAbility(ability) 29 | return () => h(Child) 30 | } 31 | }).mount(root) 32 | 33 | expect(root.innerHTML).to.equal('Provided') 34 | }) 35 | }) 36 | 37 | describe('`useAbility`', () => { 38 | it('throws if `Ability` instance has not been provided', () => { 39 | const app = createApp({ 40 | setup: () => () => h(Child) 41 | }) 42 | 43 | app.config.warnHandler = () => {} 44 | app.mount(root) 45 | 46 | expect(root.innerHTML).to.contain('Cannot inject Ability instance') 47 | }) 48 | 49 | it('allows to use `can` and `cannot` directly', () => { 50 | const app = createApp({ 51 | setup() { 52 | provideAbility(ability) 53 | return () => h({ 54 | setup() { 55 | const { can, cannot } = useAbility() 56 | return () => { 57 | return h('div', can('read', 'Post').toString() + cannot('read', 'Post').toString()) 58 | } 59 | } 60 | }) 61 | } 62 | }) 63 | 64 | app.mount(root) 65 | expect(root.firstElementChild.innerHTML).to.equal('falsetrue') 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /packages/casl-vue/spec/plugin.spec.js: -------------------------------------------------------------------------------- 1 | import { createApp, defineComponent, nextTick } from 'vue' 2 | import { defineAbility } from '@casl/ability' 3 | import { abilitiesPlugin, ABILITY_TOKEN, Can } from '../src' 4 | 5 | describe('Abilities plugin', () => { 6 | let ability 7 | let vm 8 | let app 9 | let appRoot 10 | const App = defineComponent({ 11 | inject: { 12 | ability: { from: ABILITY_TOKEN } 13 | }, 14 | render() { 15 | return this.ability.can('read', 'Post') ? 'Yes' : 'No' 16 | } 17 | }) 18 | 19 | beforeEach(() => { 20 | ability = defineAbility(can => can('read', 'Post')) 21 | appRoot = window.document.createElement('div') 22 | }) 23 | 24 | it('throws if `Ability` instance is not passed', () => { 25 | expect(() => createApp().use(abilitiesPlugin)) 26 | .to.throw(/Please provide an Ability instance/) 27 | expect(() => createApp().use(abilitiesPlugin, {})) 28 | .to.throw(/Please provide an Ability instance/) 29 | }) 30 | 31 | describe('by default', () => { 32 | beforeEach(() => { 33 | app = createApp(App) 34 | .use(abilitiesPlugin, ability) 35 | vm = app.mount(appRoot) 36 | }) 37 | 38 | it('does not define global `$ability` and `$can` if `useGlobalProperties` is falsy', () => { 39 | expect(vm.$ability).not.to.exist 40 | expect(vm.$can).not.to.exist 41 | }) 42 | 43 | it('does not provide `Can` component', () => { 44 | expect(app.component(Can.name)).not.to.exist 45 | }) 46 | 47 | it('provides `Ability` instance', () => { 48 | expect(vm.ability).to.equal(ability) 49 | }) 50 | 51 | it('provides reactive `Ability` instance', async () => { 52 | expect(appRoot.innerHTML).to.equal('Yes') 53 | 54 | ability.update([]) 55 | await nextTick() 56 | 57 | expect(appRoot.innerHTML).to.equal('No') 58 | }) 59 | }) 60 | 61 | describe('when `useGlobalProperties` is true', () => { 62 | beforeEach(() => { 63 | vm = createApp(App) 64 | .use(abilitiesPlugin, ability, { 65 | useGlobalProperties: true 66 | }) 67 | .mount(appRoot) 68 | }) 69 | 70 | it('defines `$can` and `$ability` for all components', () => { 71 | expect(vm.$can('read', 'Post')).to.be.true 72 | expect(vm.$ability.can('read', 'Post')).to.be.true 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /packages/casl-vue/src/component/can.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abilities, 3 | AbilityTuple, 4 | AnyAbility, 5 | Generics, 6 | IfString, 7 | MongoAbility, 8 | SubjectType 9 | } from '@casl/ability'; 10 | import { ComponentCustomProperties, defineComponent } from 'vue'; 11 | import { useAbility } from '../useAbility'; 12 | 13 | type AbilityCanProps< 14 | T extends Abilities, 15 | Else = IfString 16 | > = T extends AbilityTuple 17 | ? { do: T[0], on: T[1], field?: string } | 18 | { I: T[0], a: Extract, field?: string } | 19 | { I: T[0], an: Extract, field?: string } | 20 | { I: T[0], this: Exclude, field?: string } 21 | : Else; 22 | 23 | export type CanProps = AbilityCanProps['abilities']> & { 24 | not?: boolean, 25 | passThrough?: boolean 26 | }; 27 | 28 | type VueAbility = ComponentCustomProperties extends { $ability: AnyAbility } 29 | ? ComponentCustomProperties['$ability'] 30 | : MongoAbility; 31 | 32 | function detectSubjectProp(props: Record) { 33 | if (props.a !== undefined) { 34 | return 'a'; 35 | } 36 | 37 | if (props.this !== undefined) { 38 | return 'this'; 39 | } 40 | 41 | if (props.an !== undefined) { 42 | return 'an'; 43 | } 44 | 45 | return ''; 46 | } 47 | 48 | export const Can = defineComponent>({ 49 | name: 'Can', 50 | props: { 51 | I: String, 52 | do: String, 53 | a: [String, Function], 54 | an: [String, Function], 55 | this: [String, Function, Object], 56 | on: [String, Function, Object], 57 | not: Boolean, 58 | passThrough: Boolean, 59 | field: String 60 | } as any, 61 | setup(props, { slots }) { 62 | const $props = props as Record; 63 | let actionProp = 'do'; 64 | let subjectProp = 'on'; 65 | 66 | if ($props[actionProp] === undefined) { 67 | actionProp = 'I'; 68 | subjectProp = detectSubjectProp(props); 69 | } 70 | 71 | if (!$props[actionProp]) { 72 | throw new Error('Neither `I` nor `do` prop was passed in '); 73 | } 74 | 75 | if (!slots.default) { 76 | throw new Error('Expects to receive default slot'); 77 | } 78 | 79 | const ability = useAbility(); 80 | 81 | return () => { 82 | const isAllowed = ability.can($props[actionProp], $props[subjectProp], $props.field); 83 | const canRender = props.not ? !isAllowed : isAllowed; 84 | 85 | if (!props.passThrough) { 86 | return canRender ? slots.default!() : null; 87 | } 88 | 89 | return slots.default!({ 90 | allowed: canRender, 91 | ability, 92 | }); 93 | }; 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /packages/casl-vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Can } from './component/can'; 2 | export type { CanProps } from './component/can'; 3 | export { abilitiesPlugin } from './plugin'; 4 | export type { AbilityPluginOptions } from './plugin'; 5 | export { useAbility, provideAbility, ABILITY_TOKEN } from './useAbility'; 6 | -------------------------------------------------------------------------------- /packages/casl-vue/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { AnyAbility, PureAbility } from '@casl/ability'; 3 | import { ABILITY_TOKEN } from './useAbility'; 4 | import { reactiveAbility } from './reactiveAbility'; 5 | 6 | export interface AbilityPluginOptions { 7 | useGlobalProperties?: boolean 8 | } 9 | 10 | export function abilitiesPlugin(app: App, ability: AnyAbility, options?: AbilityPluginOptions) { 11 | if (!ability || !(ability instanceof PureAbility)) { 12 | throw new Error('Please provide an Ability instance to abilitiesPlugin plugin'); 13 | } 14 | 15 | app.provide(ABILITY_TOKEN, reactiveAbility(ability)); 16 | 17 | if (options && options.useGlobalProperties) { 18 | app.config.globalProperties.$ability = ability; 19 | app.config.globalProperties.$can = ability.can.bind(ability); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/casl-vue/src/reactiveAbility.ts: -------------------------------------------------------------------------------- 1 | import { AnyAbility, SubjectType } from '@casl/ability'; 2 | import { ref } from 'vue'; 3 | 4 | export function reactiveAbility(ability: AnyAbility) { 5 | if (Object.hasOwn(ability, 'possibleRulesFor')) { 6 | return ability; 7 | } 8 | 9 | const watcher = ref(true); 10 | ability.on('updated', () => { 11 | watcher.value = !watcher.value; 12 | }); 13 | 14 | const possibleRulesFor = ability.possibleRulesFor.bind(ability); 15 | ability.possibleRulesFor = (action: string, subject: SubjectType) => { 16 | watcher.value = watcher.value; // eslint-disable-line 17 | return possibleRulesFor(action, subject); 18 | }; 19 | ability.can = ability.can.bind(ability); 20 | ability.cannot = ability.cannot.bind(ability); 21 | 22 | return ability; 23 | } 24 | -------------------------------------------------------------------------------- /packages/casl-vue/src/useAbility.ts: -------------------------------------------------------------------------------- 1 | import type { AnyAbility, MongoAbility } from '@casl/ability'; 2 | import { inject, InjectionKey, provide } from 'vue'; 3 | import { reactiveAbility } from './reactiveAbility'; 4 | 5 | export const ABILITY_TOKEN: InjectionKey = Symbol('ability'); 6 | 7 | export function useAbility(): T { 8 | const ability = inject(ABILITY_TOKEN); 9 | 10 | if (!ability) { 11 | throw new Error('Cannot inject Ability instance because it was not provided'); 12 | } 13 | 14 | return ability; 15 | } 16 | 17 | export function provideAbility(ability: AnyAbility) { 18 | provide(ABILITY_TOKEN, reactiveAbility(ability)); 19 | } 20 | -------------------------------------------------------------------------------- /packages/casl-vue/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "spec/**/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/casl-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "src/*", 5 | "spec/*" 6 | ], 7 | "compilerOptions": { 8 | "outDir": "dist/types", 9 | "types": ["vue"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/dx/bin/dx.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const dx = require('../lib/dx'); 4 | 5 | dx(process.argv.slice(2)); 6 | -------------------------------------------------------------------------------- /packages/dx/bin/release-packages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | release_packages() { 4 | changed_paths=$1; 5 | preview_branch=$2; 6 | 7 | echo "Releasing packages with the next args (branch: ${preview_branch:-master}): $changed_paths" 8 | 9 | if [ "$changed_paths" = "" ];then 10 | cat <<______HERE__ 11 | Usage: 12 | release-packages "packages/casl-ability" 13 | release-packages ' 14 | packages/casl-ability 15 | packages/casl-angular 16 | ' 17 | ______HERE__ 18 | return 1; 19 | fi 20 | 21 | changed_packages="$(echo "$changed_paths" | grep 'packages/' | grep -v packages/dx)"; 22 | 23 | if [ "$changed_packages" = "" ]; then 24 | echo "No packages to release" >> $GITHUB_STEP_SUMMARY; 25 | echo -e "Changed files:\n${changed_paths}" >> $GITHUB_STEP_SUMMARY 26 | return 0; 27 | fi 28 | 29 | pnpm_options='' 30 | for path in $changed_packages; do 31 | pnpm_options="${pnpm_options} --filter ./${path}" 32 | done 33 | 34 | release_options="" 35 | if [ "$preview_branch" != "" ]; then 36 | release_options=" --dry-run --verify-conditions false --no-ci --branches master,$preview_branch" 37 | fi 38 | echo "running: pnpm run -r $pnpm_options release $release_options" >> $GITHUB_STEP_SUMMARY 39 | pnpm run -r $pnpm_options release $release_options 40 | } 41 | 42 | extract_package_versions() { 43 | changed_paths=$1; 44 | changed_packages="$(echo "$changed_paths" | grep 'packages/' | grep -v packages/dx)"; 45 | 46 | released_packages=(); 47 | for path in $changed_packages; do 48 | package_name=$(grep '"name":' $path/package.json | cut -d : -f 2 | cut -d '"' -f 2); 49 | package_version=$(grep '"version":' $path/package.json | cut -d : -f 2 | cut -d '"' -f 2); 50 | released_packages[${#released_packages[@]}]="${package_name}@${package_version}"; 51 | done 52 | 53 | echo "🚀 Released in ${released_packages[@]}" >> $GITHUB_STEP_SUMMARY 54 | echo "${released_packages[@]}" 55 | } 56 | -------------------------------------------------------------------------------- /packages/dx/bin/semantic-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 4 | const git = require('semantic-release/lib/git'); 5 | 6 | // overwrite semantic release verifyAuth function to be ignored in dry-run mode 7 | const verifyAuth = git.verifyAuth; 8 | git.verifyAuth = async (...args) => { 9 | if (process.argv.includes('--dry-run')) return; 10 | return verifyAuth(...args); 11 | } 12 | 13 | require('semantic-release/bin/semantic-release.js'); 14 | -------------------------------------------------------------------------------- /packages/dx/config/babel.config.mjs: -------------------------------------------------------------------------------- 1 | const CONFIG = { 2 | default: { 3 | plugins: [ 4 | ['@babel/plugin-transform-typescript', { 5 | allowDeclareFields: false 6 | }], 7 | ['@babel/plugin-proposal-class-properties', { 8 | loose: true 9 | }], 10 | ], 11 | }, 12 | es6: { 13 | plugins: [ 14 | ['@babel/plugin-proposal-object-rest-spread', { 15 | loose: true, 16 | useBuiltIns: true 17 | }] 18 | ] 19 | }, 20 | es5: { 21 | presets: [ 22 | ['@babel/preset-env', { 23 | modules: false, 24 | loose: true, 25 | targets: { 26 | browsers: ['last 3 versions'] 27 | } 28 | }], 29 | ], 30 | }, 31 | test: { 32 | presets: [ 33 | ['@babel/preset-env', { 34 | loose: true, 35 | targets: { 36 | node: '10' 37 | } 38 | }] 39 | ], 40 | } 41 | }; 42 | 43 | export default function config(name) { 44 | if (name === 'default' || !CONFIG[name]) { 45 | return CONFIG.default; 46 | } 47 | 48 | const { presets = [], plugins = [] } = CONFIG[name]; 49 | 50 | return { 51 | presets: presets.concat(CONFIG.default.presets || []), 52 | plugins: plugins.concat(CONFIG.default.plugins || []), 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/dx/config/jest.chai.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./jest.config'); 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | setupFilesAfterEnv: [ 6 | `${__dirname}/../lib/spec_helper.js` 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /packages/dx/config/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: process.cwd(), 3 | coverageDirectory: './coverage', 4 | testEnvironment: 'node', 5 | coverageReporters: [ 6 | 'lcov', 7 | 'text' 8 | ], 9 | collectCoverageFrom: [ 10 | '/src/**/*.{ts,js}' 11 | ], 12 | testMatch: [ 13 | '/spec/**/*.spec.{ts,tsx,js}' 14 | ], 15 | transform: { 16 | '^.+\\.[t|j]sx?$': 'ts-jest', 17 | '^.+\\.mjs$': 'ts-jest', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/dx/config/lintstaged.js: -------------------------------------------------------------------------------- 1 | const fsPath = require('path'); 2 | 3 | const dx = fsPath.join(__dirname, '..', 'bin', 'dx.js'); 4 | 5 | module.exports = { 6 | '**/*.ts': [ 7 | `${dx} eslint --fix` 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /packages/dx/config/semantic-release.js: -------------------------------------------------------------------------------- 1 | const parser = require('git-log-parser'); // eslint-disable-line 2 | 3 | // this is hack which allows to use semantic-release for monorepo 4 | // https://github.com/semantic-release/semantic-release/issues/193#issuecomment-578436666 5 | parser.parse = (parse => (config, options) => { 6 | if (Array.isArray(config._)) { 7 | config._.push(options.cwd); 8 | } else if (config._) { 9 | config._ = [config._, options.cwd]; 10 | } else { 11 | config._ = options.cwd; 12 | } 13 | return parse(config, options); 14 | })(parser.parse); 15 | 16 | module.exports = { 17 | tagFormat: `${process.env.npm_package_name}@\${version}`, 18 | branches: [ 19 | 'master', 20 | { name: 'next', channel: 'next', prerelease: true }, 21 | { name: 'alpha', channel: 'alpha', prerelease: true }, 22 | `${process.env.npm_package_name}@+([0-9])?(.{+([0-9]),x}).x` 23 | ], 24 | plugins: [ 25 | ['@semantic-release/commit-analyzer', { 26 | releaseRules: [ 27 | { type: 'chore', scope: 'deps', release: 'patch' }, 28 | { type: 'docs', scope: 'README', release: 'patch' }, 29 | { type: 'refactor', release: 'patch' }, 30 | ] 31 | }], 32 | '@semantic-release/release-notes-generator', 33 | ['@semantic-release/changelog', { 34 | changelogTitle: '# Change Log\n\nAll notable changes to this project will be documented in this file.' 35 | }], 36 | '@semantic-release/npm', 37 | ['@semantic-release/git', { 38 | message: `chore(release): ${process.env.npm_package_name}@\${nextRelease.version} [skip ci]` 39 | }], 40 | ["@semantic-release/github", { 41 | releasedLabels: false, 42 | successComment: false, 43 | }] 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /packages/dx/lib/spawn.js: -------------------------------------------------------------------------------- 1 | const child = require('child_process'); 2 | 3 | function spawn(cli, args, options = {}) { 4 | return child.spawn(cli, args, { 5 | cwd: options.cwd || process.cwd(), 6 | stdio: 'inherit', 7 | env: { 8 | ...process.env, 9 | ...options.env, 10 | FORCE_COLOR: '1', 11 | }, 12 | }); 13 | } 14 | 15 | function spawnAndExit(cli, args, options) { 16 | const cliProcess = spawn(cli, args, options); 17 | cliProcess.once('exit', exitCode => process.exit(exitCode || 0)); 18 | return cliProcess; 19 | } 20 | 21 | module.exports = { 22 | spawn, 23 | spawnAndExit, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/dx/lib/spec_helper.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof require === 'function' && typeof module !== 'undefined') { 3 | require('chai').use(require('chai-spies')); // eslint-disable-line 4 | factory(require('chai'), global); // eslint-disable-line 5 | } else if (typeof window === 'object') { 6 | window.global = window; // eslint-disable-line 7 | factory(window.chai, window); // eslint-disable-line 8 | } 9 | }((chai, global) => { 10 | global.expect = chai.expect; 11 | global.spy = chai.spy; 12 | })); 13 | -------------------------------------------------------------------------------- /packages/dx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@casl/dx", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Private dev env for CASL", 6 | "author": "Sergii Stotskyi ", 7 | "license": "MIT", 8 | "bin": { 9 | "dx": "bin/dx.js" 10 | }, 11 | "dependencies": { 12 | "@babel/core": "^7.14.0", 13 | "@babel/plugin-proposal-class-properties": "^7.13.0", 14 | "@babel/plugin-proposal-object-rest-spread": "^7.13.8", 15 | "@babel/plugin-transform-typescript": "^7.13.0", 16 | "@babel/preset-env": "^7.14.1", 17 | "@eslint/js": "^9.17.0", 18 | "@rollup/plugin-babel": "^6.0.0", 19 | "@rollup/plugin-node-resolve": "^16.0.0", 20 | "@rollup/plugin-terser": "^0.4.3", 21 | "@semantic-release/changelog": "^5.0.1", 22 | "@semantic-release/git": "^9.0.0", 23 | "eslint": "^9.0.0", 24 | "eslint-plugin-import": "^2.31.0", 25 | "globals": "^16.0.0", 26 | "jest": "^29.0.0", 27 | "lint-staged": "^15.0.0", 28 | "rollup": "^4.0.0", 29 | "rollup-plugin-sourcemaps": "^0.6.3", 30 | "semantic-release": "^17.4.2", 31 | "ts-jest": "^29.0.0", 32 | "typescript": "~5.5.0", 33 | "typescript-eslint": "^8.19.0" 34 | }, 35 | "devDependencies": { 36 | "@stylistic/eslint-plugin-js": "^4.0.0", 37 | "@stylistic/eslint-plugin-ts": "^4.0.0", 38 | "@types/jest": "^29.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/dx/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": [ 4 | "bin/*", 5 | "config/*", 6 | "lib/*" 7 | ], 8 | "compilerOptions": { 9 | "allowJs": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "noErrorTruncation": true, 9 | "allowJs": true, 10 | "esModuleInterop": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------