├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .fossa.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATES │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yaml └── workflows │ ├── main.yaml │ └── semgrep.yaml ├── .gitignore ├── .npm-upgrade.json ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── NOTICE.txt ├── README.md ├── package-lock.json ├── package.json ├── src ├── constants │ ├── default-schema-version.ts │ ├── index.ts │ ├── keyword.ts │ ├── language-name.ts │ ├── schema-version.ts │ └── single-indentation.ts ├── documentation │ ├── concepts.ts │ └── index.ts ├── index.ts ├── inner-utils │ ├── assert-never.ts │ └── insecure-random-string.ts ├── samples │ ├── custom-roles.json │ ├── entitlements.json │ ├── expenses.json │ ├── gdrive.json │ ├── generic.json │ ├── github.json │ ├── index.ts │ ├── iot.json │ └── slack.json ├── theme │ ├── index.ts │ ├── supported-themes │ │ └── openfga-dark.ts │ ├── theme.typings.ts │ └── utils.ts ├── tools │ ├── index.ts │ ├── monaco │ │ ├── index.ts │ │ ├── language-definition.ts │ │ ├── providers │ │ │ ├── code-actions.ts │ │ │ ├── completion.ts │ │ │ └── hover-actions.ts │ │ ├── register-dsl.ts │ │ ├── theme.ts │ │ ├── typings.ts │ │ └── validate.ts │ └── prism │ │ ├── index.ts │ │ └── language-definition.ts ├── utilities │ └── graphs │ │ ├── graph.typings.ts │ │ ├── index.ts │ │ ├── model-graph.ts │ │ └── related-users-graph.ts └── validator │ ├── default-regex.ts │ ├── index.ts │ └── ulid-regex.ts ├── tests ├── data │ ├── index.ts │ ├── model-validation.ts │ └── test-models.ts ├── dsl-validation.test.ts ├── jest.config.js └── tsconfig.spec.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,ts}] 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/parser/grammar.ts 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "prettier", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/recommended", 13 | "plugin:import/typescript" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2021, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "@typescript-eslint" 22 | ], 23 | "ignorePatterns": ["src/parser/grammar.ts"], 24 | "rules": { 25 | "no-case-declarations": "off", 26 | "linebreak-style": [ 27 | "error", 28 | "unix" 29 | ], 30 | "@typescript-eslint/ban-ts-comment": "off", 31 | "@typescript-eslint/no-explicit-any": "warn", 32 | "quotes": [ 33 | "error", 34 | "double" 35 | ], 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | "max-len": [ 41 | "warn", 42 | { 43 | "code": 120 44 | } 45 | ], 46 | "object-curly-spacing": [ 47 | "error", 48 | "always" 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /.fossa.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | 3 | server: https://app.fossa.com 4 | 5 | project: 6 | id: github.com/openfga/syntax-transformer 7 | name: github.com/openfga/syntax-transformer 8 | link: openfga.dev 9 | url: github.com/openfga/syntax-transformer 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @openfga/frontend @openfga/dx 2 | README.md @openfga/product @openfga/community @openfga/dx 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report an issue 3 | about: Create a bug report about an existing issue. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Please do not report security vulnerabilities here**. See the [Responsible Disclosure Program](https://github.com/openfga/.github/blob/main/SECURITY.md). 11 | 12 | **Thank you in advance for helping us to improve this library!** Please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. 13 | 14 | By submitting an issue to this repository, you agree to the terms within the [OpenFGA Code of Conduct](https://github.com/openfga/.github/blob/main/CODE_OF_CONDUCT.md). 15 | 16 | ### Description 17 | 18 | > Provide a clear and concise description of the issue, including what you expected to happen. 19 | 20 | ### Reproduction 21 | 22 | > Detail the steps taken to reproduce this error, what was expected, and whether this issue can be reproduced consistently or if it is intermittent. 23 | > 24 | > Where applicable, please include: 25 | > 26 | > - Code sample to reproduce the issue 27 | > - Log files (redact/remove sensitive information) 28 | > - Application settings (redact/remove sensitive information) 29 | > - Screenshots 30 | 31 | ### Environment 32 | 33 | > Please provide the following: 34 | 35 | - **Version of this library used:** 36 | - **Version of the platform or framework used, if applicable:** 37 | - **Other relevant versions (language, server software, OS, browser):** 38 | - **Other modules/plugins/libraries that might be involved:** 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest new functionality for this project. 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Please do not report security vulnerabilities here**. See the [Responsible Disclosure Program](https://github.com/openfga/.github/blob/main/SECURITY.md). 11 | 12 | **Thank you in advance for helping us to improve this library!** Please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. 13 | 14 | By submitting an issue to this repository, you agree to the terms within the [OpenFGA Code of Conduct](https://github.com/openfga/.github/blob/main/CODE_OF_CONDUCT.md). 15 | 16 | ### Describe the problem you'd like to have solved 17 | 18 | > A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | 20 | ### Describe the ideal solution 21 | 22 | > A clear and concise description of what you want to happen. 23 | 24 | ## Alternatives and current workarounds 25 | 26 | > A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 27 | 28 | ### Additional context 29 | 30 | > Add any other context or screenshots about the feature request here. 31 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | dependencies: 9 | dependency-type: "production" 10 | patterns: 11 | - "*" 12 | dev-dependencies: 13 | dependency-type: "development" 14 | patterns: 15 | - "*" 16 | exclude-patterns: 17 | - "eslint" 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | groups: 23 | dependencies: 24 | patterns: 25 | - "*" 26 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Publish 2 | 3 | on: 4 | merge_group: 5 | push: 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x, 18.x, 20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | submodules: recursive 24 | 25 | - name: Set up node 26 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | registry-url: 'https://registry.npmjs.org' 30 | always-auth: false 31 | cache: 'npm' 32 | 33 | - name: Update npm 34 | if: startsWith(matrix.node-version, '18.') || startsWith(matrix.node-version, '20.') 35 | run: npm i -g npm 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Build 41 | run: npm run build 42 | 43 | test: 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 48 | with: 49 | submodules: recursive 50 | 51 | - name: Set up node 52 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 53 | with: 54 | node-version: '20' 55 | cache: 'npm' 56 | 57 | - name: Update npm 58 | run: npm i -g npm 59 | 60 | - name: Install dependencies 61 | run: npm ci 62 | 63 | - name: Audit dependencies 64 | run: npm audit 65 | 66 | - name: Check for circular dependencies 67 | run: npx madge --circular . --extensions ts,js 68 | 69 | # Run Prettier 70 | - name: Run Prettier 71 | run: npm run format:check 72 | 73 | # Run ESLint 74 | - name: Run eslint 75 | run: npm run lint 76 | 77 | - name: Run tests 78 | run: npm test 79 | 80 | publish: 81 | runs-on: ubuntu-latest 82 | if: startsWith(github.ref, 'refs/tags/v') 83 | needs: [build, test] 84 | permissions: 85 | contents: read 86 | id-token: write 87 | 88 | steps: 89 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 90 | with: 91 | submodules: recursive 92 | 93 | - name: Set up node 94 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 95 | with: 96 | node-version: '20' 97 | registry-url: 'https://registry.npmjs.org' 98 | scope: '@openfga' 99 | always-auth: false 100 | cache: 'npm' 101 | 102 | - name: Update npm 103 | run: npm i -g npm 104 | 105 | - name: Install dependencies 106 | run: npm ci 107 | 108 | - name: Publish to npm 109 | run: npm publish 110 | env: 111 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 112 | 113 | create-release: 114 | runs-on: ubuntu-latest 115 | if: startsWith(github.ref, 'refs/tags/v') 116 | needs: publish 117 | permissions: 118 | contents: write 119 | 120 | steps: 121 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 122 | 123 | - uses: Roang-zero1/github-create-release-action@57eb9bdce7a964e48788b9e78b5ac766cb684803 # v3.0.1 124 | with: 125 | version_regex: ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ 126 | env: 127 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 128 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yaml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | semgrep: 8 | name: Scan 9 | runs-on: ubuntu-latest 10 | container: 11 | image: returntocorp/semgrep 12 | if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 15 | - run: semgrep ci --no-suppress-errors 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | .DS_Store 6 | VERSION.txt 7 | tests/coverage 8 | .idea/ 9 | .vscode/ 10 | .dccache 11 | 12 | .env 13 | credentials.json 14 | 15 | src/parser/grammar.ts 16 | -------------------------------------------------------------------------------- /.npm-upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "eslint": { 4 | "versions": "9.*.*", 5 | "reason": "breaking changes" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tests/__snapshots__/* 2 | src/parser/grammar.ts 3 | *.ne -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "bracketSpacing": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.2.0 Beta 11 (Frontend Utils) 4 | ### [0.2.0-beta.11](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.10...v0.2.0-beta.11) (2024-09-06) 5 | 6 | - chore: update dependencies 7 | 8 | ## v0.2.0 Beta 10 (Frontend Utils) 9 | ### [0.2.0-beta.10](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.9...v0.2.0-beta.10) (2024-08-23) 10 | 11 | - fix: code completions for schema 1.2 (#273) 12 | - chore: update dependencies 13 | 14 | ## v0.2.0 Beta 9 (Frontend Utils) 15 | ### [0.2.0-beta.9](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.8...v0.2.0-beta.9) (2024-04-12) 16 | 17 | - fix: correct syntax highlighting for identifiers with dashes and underscores 18 | 19 | ## v0.2.0 Beta 8 (Frontend Utils) 20 | ### [0.2.0-beta.8](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.7...v0.2.0-beta.8) (2024-04-12) 21 | 22 | - feat: support modular models for prism 23 | - fix: handle case when an error other than language error is thrown in validateDSL 24 | 25 | ## v0.2.0 Beta 7 (Frontend Utils) 26 | ### [0.2.0-beta.7](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.6...v0.2.0-beta.7) (2024-04-09) 27 | 28 | - feat: add modular model support for monaco 29 | 30 | ## v0.2.0 Beta 6 (Frontend Utils) 31 | ### [0.2.0-beta.6](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.5...v0.2.0-beta.6) (2024-01-29) 32 | 33 | - fix(monaco/validate): handle metadata being undefined 34 | 35 | ## v0.2.0 Beta 5 (Frontend Utils) 36 | ### [0.2.0-beta.5](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.4...v0.2.0-beta.5) (2024-01-23) 37 | 38 | - feat: correctly tokenize comments (#197) 39 | - chore: update dependencies 40 | 41 | ## v0.2.0 Beta 4 (Frontend Utils) 42 | ### [0.2.0-beta.4](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.3...v0.2.0-beta.4) (2024-01-03) 43 | 44 | - chore: update dependencies 45 | 46 | ## v0.2.0 Beta 3 (Frontend Utils) 47 | ### [0.2.0-beta.3](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.2...v0.2.0-beta.3) (2023-11-20) 48 | 49 | - feat: add conditions tokens for prism 50 | - fix: monaco grammar for conditions 51 | - fix: monaco grammar for type restrictions 52 | 53 | ## v0.2.0 Beta 2 (Frontend Utils) 54 | ### [0.2.0-beta.2](https://github.com/openfga/frontend-utils/compare/v0.2.0-beta.1...v0.2.0-beta.2) (2023-11-03) 55 | 56 | - fix: bump dependencies 57 | - brings in improvements to monaco alerts and support for conditions 58 | 59 | ## v0.2.0 Beta 1 (Frontend Utils) 60 | ### [0.2.0-beta.1](https://github.com/openfga/frontend-utils/releases/tag/v0.2.0-beta.1) (2023-10-09) 61 | 62 | Removed: (BREAKING) 63 | - Syntax Transformer has been moved to [Language](https://github.com/openfga/language/tree/main/pkg/js) 64 | - Package has been renamed to `@openfga/frontend-utils`: this is meant for use by the VS Code extension, the FGA Playground, the docs and UI frameworks building OpenFGA modeling tooling. 65 | 66 | Added: 67 | - Uses the new language based syntax transformer to power modeling and validation 68 | 69 | ## v0.1.6 (Syntax Transformer) 70 | ### [0.1.6](https://github.com/openfga/syntax-transformer/compare/v0.1.5...v0.1.6) (2023-09-21) 71 | 72 | - fix: exporting language 73 | 74 | ## v0.1.5 (Syntax Transformer) 75 | ### [0.1.5](https://github.com/openfga/syntax-transformer/compare/v0.1.4...v0.1.5) (2023-09-11) 76 | 77 | - fix: exporting language 78 | 79 | ## v0.1.4 (Syntax Transformer) 80 | ### [0.1.4](https://github.com/openfga/syntax-transformer/compare/v0.1.3...v0.1.4) (2023-09-11) 81 | 82 | - feat: export the language 83 | - chore(deps): upgrade dependencies 84 | 85 | ## v0.1.3 (Syntax Transformer) 86 | ### [0.1.3](https://github.com/openfga/syntax-transformer/compare/v0.1.2...v0.1.3) (2023-06-26) 87 | 88 | - feat(validation): Allow self-referencing type restrictions 89 | - fix(validation): Prevent invalid model that may introduce infinite loop 90 | - chore(deps): upgrade dependencies 91 | 92 | ## v0.1.2 (Syntax Transformer) 93 | ### [0.1.2](https://github.com/openfga/syntax-transformer/compare/v0.1.1...v0.1.2) (2023-04-21) 94 | 95 | - chore(ci): update permissions and publish provenance data 96 | - chore(deps): upgrade dependencies 97 | 98 | ## v0.1.1 (Syntax Transformer) 99 | ### [0.1.1](https://github.com/openfga/syntax-transformer/compare/v0.1.0...v0.1.1) (2023-04-03) 100 | 101 | - fix(validation): raise error if schema is not specified in DSL (openfga/syntax-transformer#127) 102 | 103 | ## v0.1.0 (Syntax Transformer) 104 | ### [0.1.0](https://github.com/openfga/syntax-transformer/compare/v0.0.14...v0.1.0) (2023-03-31) 105 | 106 | - feat!: default to schema v1.1 (openfga/syntax-transformer#122) 107 | - chore: upgrade dependencies (openfga/syntax-transformer#121) 108 | 109 | ## v0.0.14 (Syntax Transformer) 110 | ### [0.0.14](https://github.com/openfga/syntax-transformer/compare/v0.0.13...v0.0.14) (2023-02-16) 111 | 112 | - fix(validation): allow ttu relation as long as one of the child has such relation. fixes openfga/syntax-transformer#113 113 | - fix(syntax highlighting): regex lookbehind error on Safari. fixes openfga/syntax-transformer#116 114 | - chore: upgrade dependencies 115 | 116 | ## v0.0.13 (Syntax Transformer) 117 | ### [0.0.13](https://github.com/openfga/syntax-transformer/compare/v0.0.12...v0.0.13) (2023-01-25) 118 | 119 | - fix: add yargs to list of dependencies. fixes openfga/syntax-transformer#111 120 | 121 | ## v0.0.12 (Syntax Transformer) 122 | ### [0.0.12](https://github.com/openfga/syntax-transformer/compare/v0.0.11...v0.0.12) (2023-01-23) 123 | 124 | #### Changes 125 | - feat: add a simple cli to transform models, run it by: 126 | ```sh 127 | npx @openfga/syntax-transformer transform --from=json --inputFile=test.json 128 | npx @openfga/syntax-transformer transform --from=dsl --inputFile=test.openfga 129 | ``` 130 | - chore(deps): upgrade dependencies 131 | 132 | ## v0.0.1 (Syntax Transformer)1 133 | ### [0.0.11](https://github.com/openfga/syntax-transformer/compare/v0.0.10...v0.0.11) (2023-01-10) 134 | 135 | Please note: All additional undocumented functionality should be considered unstable and may be removed at any moment. 136 | 137 | #### Changes 138 | - feat: export graph for use in the FGA Playground 139 | - feat: export sample authorization models 140 | - chore(deps): upgrade dependencies 141 | 142 | ## v0.0.1 (Syntax Transformer)0 143 | ### [0.0.10](https://github.com/openfga/syntax-transformer/compare/v0.0.9...v0.0.10) (2022-12-15) 144 | 145 | #### Changes 146 | - chore(deps): upgrade `@openfga/sdk` to `v0.2.0` and update other deps 147 | - chore: revert `package-lock.json` to version `2` because it was breaking snyk 148 | 149 | ## v0.0.9 (Syntax Transformer) 150 | ### [0.0.9](https://github.com/openfga/syntax-transformer/compare/v0.0.8...v0.0.9) (2022-12-02) 151 | 152 | #### Changes 153 | - feat(editor-support): expose auto-completion for monaco (openfga/syntax-transformer#90) 154 | - feat(editor-support): expose syntax highlighting and tokenization for monaco and prism (openfga/syntax-transformer#90) 155 | 156 | #### Chore 157 | - chore(deps): bump dev dependencies 158 | 159 | ## v0.0.8 (Syntax Transformer) 160 | ### [0.0.8](https://github.com/openfga/syntax-transformer/compare/v0.0.7...v0.0.8) (2022-11-16) 161 | 162 | #### Changes 163 | - feat: model 1.1 removing 'as self' (openfga/syntax-transformer#94) 164 | - feat: model 1.1 allowing wildcard restriction in allowable types (openfga/syntax-transformer#95) 165 | 166 | #### Chore 167 | - chore: update indent-dsl to use keyword constant (openfga/syntax-transformer#91) 168 | 169 | ## v0.0.7 (Syntax Transformer) 170 | 171 | ### [0.0.7](https://github.com/openfga/syntax-transformer/compare/v0.0.6...v0.0.7) (2022-11-02) 172 | 173 | #### Changes 174 | - feat: add indentDSL to improve checkDSL parsing reliability (#68) 175 | - feat: validation rule for type and relation name (#78) 176 | - feat: initial support schema 1.1 (#67, #73, #75) 177 | 178 | #### Fixes 179 | - fix(parse-dsl): fix issue with infinite loops triggered when parsing some models #76 180 | 181 | #### Chore 182 | - chore(deps): bump dependencies 183 | 184 | ## v0.0.6 (Syntax Transformer) 185 | 186 | ### [0.0.6](https://github.com/openfga/syntax-transformer/compare/v0.0.5...v0.0.6) (2022-10-11) 187 | 188 | #### Changes 189 | - fix(check-dsl): allow same relation in computedUserset of from clause (openfga/syntax-transformer#70) 190 | - chore(deps): bump dependencies 191 | 192 | ## v0.0.5 (Syntax Transformer) 193 | 194 | ### [0.0.5](https://github.com/openfga/syntax-transformer/compare/v0.0.4...v0.0.5) (2022-09-29) 195 | 196 | #### Changes 197 | - fix(grammar): compile beforehand (openfga/syntax-transformer#52) 198 | - fix(grammar): overhaul response returned by parser (openfga/syntax-transformer#52) 199 | - fix(parse-dsl): support types with no relations in the dsl (openfga/syntax-transformer#52) 200 | - fix(check-dsl): support types with no relations in the dsl (openfga/syntax-transformer#57) 201 | - chore(deps): bump dependencies 202 | 203 | ## v0.0.4 (Syntax Transformer) 204 | 205 | ### [0.0.4](https://github.com/openfga/syntax-transformer/compare/v0.0.3...v0.0.4) (2022-08-16) 206 | 207 | #### Changes 208 | - fix(check-dsl): incorrect parsing of relations starting with `as` 209 | - chore(deps): bump dependencies 210 | 211 | ## v0.0.3 (Syntax Transformer) 212 | 213 | ### [0.0.3](https://github.com/openfga/syntax-transformer/compare/v0.0.2...v0.0.3) (2022-06-29) 214 | 215 | #### Changes 216 | - feat(reporters): add some reporters that can be used to validate the model, and show errors found 217 | 218 | ## v0.0.2 (Syntax Transformer) 219 | 220 | ### [0.0.2](https://github.com/openfga/syntax-transformer/compare/v0.0.1...v0.0.2) (2022-06-15) 221 | 222 | #### Changes 223 | - chore(ci): fix publishing to npm 224 | 225 | ## v0.0.1 (Syntax Transformer) 226 | 227 | ### [0.0.1](https://github.com/openfga/syntax-transformer/releases/tag/v0.0.1) (2022-06-15) 228 | 229 | Internal Release 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | 180 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | OpenFGA Syntax Transformer 3 | github.com/openfga/syntax-transformer 4 | 5 | Copyright 2022 Okta, Inc. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- 16 | 17 | 18 | 19 | 20 | The following 3rd-party software packages may be used by or distributed with OpenFGA Syntax Transformer. Certain licenses and notices may appear in other parts of the product in accordance with the applicable license requirements. 21 | 22 | The Okta product that this document references does not necessarily use all the open source software packages referred to below and may also only use portions of a given package. In addition, Okta makes available to customers certain complementary, unmodified open source software packages that facilitate customers’ use of the OpenFGA Syntax Transformer product. 23 | 24 | 25 | 26 | 27 | _________________________________________________________________________________ 28 | 29 | ================================================================================ 30 | 31 | Dependencies 32 | 33 | ================================================================================ 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- 38 | commander (2.20.3) 39 | -------------------------------------------------------------------------------- 40 | 41 | * Declared Licenses * 42 | MIT 43 | 44 | (The MIT License) 45 | 46 | Copyright (c) 2011 TJ Holowaychuk 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of this software and associated documentation files (the 50 | 'Software'), to deal in the Software without restriction, including 51 | without limitation the rights to use, copy, modify, merge, publish, 52 | distribute, sublicense, and/or sell copies of the Software, and to 53 | permit persons to whom the Software is furnished to do so, subject to 54 | the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be 57 | included in all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 62 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 63 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 64 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 65 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- 70 | discontinuous-range (1.0.0) 71 | -------------------------------------------------------------------------------- 72 | 73 | * Declared Licenses * 74 | MIT 75 | 76 | The MIT License (MIT) 77 | 78 | Copyright (c) 2014 David Tudury 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy 81 | of this software and associated documentation files (the "Software"), to deal 82 | in the Software without restriction, including without limitation the rights 83 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 84 | copies of the Software, and to permit persons to whom the Software is 85 | furnished to do so, subject to the following conditions: 86 | 87 | The above copyright notice and this permission notice shall be included in all 88 | copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 91 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 92 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 93 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 94 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 95 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 96 | SOFTWARE. 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- 101 | lodash (4.17.21) 102 | -------------------------------------------------------------------------------- 103 | 104 | * Declared Licenses * 105 | No licenses found 106 | 107 | * Other Licenses * 108 | MIT, CC0-1.0 109 | 110 | 111 | Copyright OpenJS Foundation and other contributors 112 | 113 | Based on Underscore.js, copyright Jeremy Ashkenas, 114 | DocumentCloud and Investigative Reporters & Editors 115 | 116 | This software consists of voluntary contributions made by many 117 | individuals. For exact contribution history, see the revision history 118 | available at https://github.com/lodash/lodash 119 | 120 | The following license applies to all parts of this software except as 121 | documented below: 122 | 123 | ==== 124 | 125 | Permission is hereby granted, free of charge, to any person obtaining 126 | a copy of this software and associated documentation files (the 127 | "Software"), to deal in the Software without restriction, including 128 | without limitation the rights to use, copy, modify, merge, publish, 129 | distribute, sublicense, and/or sell copies of the Software, and to 130 | permit persons to whom the Software is furnished to do so, subject to 131 | the following conditions: 132 | 133 | The above copyright notice and this permission notice shall be 134 | included in all copies or substantial portions of the Software. 135 | 136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 137 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 138 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 139 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 140 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 141 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 142 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 143 | 144 | ==== 145 | 146 | Copyright and related rights for sample code are waived via CC0. Sample 147 | code is defined as all source code displayed within the prose of the 148 | documentation. 149 | 150 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 151 | 152 | ==== 153 | 154 | Files located in the node_modules and vendor directories are externally 155 | maintained libraries used by this software which have their own 156 | licenses; we recommend you read them, as their terms may differ from the 157 | terms above. 158 | 159 | 160 | 161 | CC0 1.0 Universal<> 162 | Statement of Purpose 163 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 164 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 165 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 166 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 167 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 168 | ii. moral rights retained by the original author(s) and/or performer(s); 169 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 170 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 171 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 172 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 173 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 174 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 175 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 176 | 4. Limitations and Disclaimers. 177 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 178 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 179 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 180 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 181 | 182 | 183 | -------------------------------------------------------------------------------- 184 | moo (0.5.1) 185 | -------------------------------------------------------------------------------- 186 | 187 | * Declared Licenses * 188 | BSD-3-Clause 189 | 190 | BSD 3-Clause License 191 | 192 | Copyright (c) 2017, Tim Radvan (tjvr) 193 | All rights reserved. 194 | 195 | Redistribution and use in source and binary forms, with or without 196 | modification, are permitted provided that the following conditions are met: 197 | 198 | * Redistributions of source code must retain the above copyright notice, this 199 | list of conditions and the following disclaimer. 200 | 201 | * Redistributions in binary form must reproduce the above copyright notice, 202 | this list of conditions and the following disclaimer in the documentation 203 | and/or other materials provided with the distribution. 204 | 205 | * Neither the name of the copyright holder nor the names of its 206 | contributors may be used to endorse or promote products derived from 207 | this software without specific prior written permission. 208 | 209 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 210 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 211 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 212 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 213 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 214 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 215 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 216 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 217 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 218 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- 223 | nearley (2.20.1) 224 | -------------------------------------------------------------------------------- 225 | 226 | * Declared Licenses * 227 | MIT 228 | 229 | The MIT License (MIT) 230 | 231 | Copyright (c) 2014, 2015, 2016, 2017, 2018, 2019 Kartik Chandra, Tim Radvan 232 | 233 | Permission is hereby granted, free of charge, to any person obtaining a copy 234 | of this software and associated documentation files (the "Software"), to deal 235 | in the Software without restriction, including without limitation the rights 236 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 237 | copies of the Software, and to permit persons to whom the Software is 238 | furnished to do so, subject to the following conditions: 239 | 240 | The above copyright notice and this permission notice shall be included in all 241 | copies or substantial portions of the Software. 242 | 243 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 244 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 245 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 246 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 247 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 248 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 249 | SOFTWARE. 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- 254 | railroad-diagrams (1.0.0) 255 | -------------------------------------------------------------------------------- 256 | 257 | * Declared Licenses * 258 | CC0-1.0 259 | 260 | CC0 1.0 Universal<> 261 | Statement of Purpose 262 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 263 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 264 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 265 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 266 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 267 | ii. moral rights retained by the original author(s) and/or performer(s); 268 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 269 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 270 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 271 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 272 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 273 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 274 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 275 | 4. Limitations and Disclaimers. 276 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 277 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 278 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 279 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 280 | 281 | 282 | -------------------------------------------------------------------------------- 283 | randexp (0.4.6) 284 | -------------------------------------------------------------------------------- 285 | 286 | * Declared Licenses * 287 | MIT 288 | 289 | Copyright (C) 2011 by Roly Fentanes 290 | 291 | Permission is hereby granted, free of charge, to any person obtaining a copy 292 | of this software and associated documentation files (the "Software"), to deal 293 | in the Software without restriction, including without limitation the rights 294 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 295 | copies of the Software, and to permit persons to whom the Software is 296 | furnished to do so, subject to the following conditions: 297 | 298 | The above copyright notice and this permission notice shall be included in 299 | all copies or substantial portions of the Software. 300 | 301 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 302 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 303 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 304 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 305 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 306 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 307 | THE SOFTWARE. 308 | 309 | 310 | 311 | -------------------------------------------------------------------------------- 312 | ret (0.1.15) 313 | -------------------------------------------------------------------------------- 314 | 315 | * Declared Licenses * 316 | MIT 317 | 318 | Copyright (C) 2011 by Roly Fentanes 319 | 320 | Permission is hereby granted, free of charge, to any person obtaining a copy 321 | of this software and associated documentation files (the "Software"), to deal 322 | in the Software without restriction, including without limitation the rights 323 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 324 | copies of the Software, and to permit persons to whom the Software is 325 | furnished to do so, subject to the following conditions: 326 | 327 | The above copyright notice and this permission notice shall be included in 328 | all copies or substantial portions of the Software. 329 | 330 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 331 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 332 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 333 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 334 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 335 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 336 | THE SOFTWARE. 337 | 338 | 339 | 340 | 341 | ================================================================================ 342 | Licenses 343 | 344 | ================================================================================ 345 | 346 | * MIT * 347 | 348 | (The MIT License) 349 | 350 | Copyright (c) 2011 TJ Holowaychuk 351 | 352 | Permission is hereby granted, free of charge, to any person obtaining 353 | a copy of this software and associated documentation files (the 354 | 'Software'), to deal in the Software without restriction, including 355 | without limitation the rights to use, copy, modify, merge, publish, 356 | distribute, sublicense, and/or sell copies of the Software, and to 357 | permit persons to whom the Software is furnished to do so, subject to 358 | the following conditions: 359 | 360 | The above copyright notice and this permission notice shall be 361 | included in all copies or substantial portions of the Software. 362 | 363 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 364 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 365 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 366 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 367 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 368 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 369 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 370 | 371 | 372 | * BSD-3-Clause * 373 | 374 | BSD 3-Clause License 375 | 376 | Copyright (c) 2017, Tim Radvan (tjvr) 377 | All rights reserved. 378 | 379 | Redistribution and use in source and binary forms, with or without 380 | modification, are permitted provided that the following conditions are met: 381 | 382 | * Redistributions of source code must retain the above copyright notice, this 383 | list of conditions and the following disclaimer. 384 | 385 | * Redistributions in binary form must reproduce the above copyright notice, 386 | this list of conditions and the following disclaimer in the documentation 387 | and/or other materials provided with the distribution. 388 | 389 | * Neither the name of the copyright holder nor the names of its 390 | contributors may be used to endorse or promote products derived from 391 | this software without specific prior written permission. 392 | 393 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 394 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 395 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 396 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 397 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 398 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 399 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 400 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 401 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 402 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 403 | 404 | 405 | * CC0-1.0 * 406 | 407 | Creative Commons Legal Code 408 | CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. 409 | Statement of Purpose 410 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 411 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 412 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 413 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 414 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 415 | ii. moral rights retained by the original author(s) and/or performer(s); 416 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 417 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 418 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work; 419 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 420 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 421 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 422 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 423 | 4. Limitations and Disclaimers. 424 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 425 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 426 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 427 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 428 | 429 | * MIT * 430 | 431 | Copyright (C) 2011 by Roly Fentanes 432 | 433 | Permission is hereby granted, free of charge, to any person obtaining a copy 434 | of this software and associated documentation files (the "Software"), to deal 435 | in the Software without restriction, including without limitation the rights 436 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 437 | copies of the Software, and to permit persons to whom the Software is 438 | furnished to do so, subject to the following conditions: 439 | 440 | The above copyright notice and this permission notice shall be included in 441 | all copies or substantial portions of the Software. 442 | 443 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 444 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 445 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 446 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 447 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 448 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 449 | THE SOFTWARE. 450 | 451 | 452 | * MIT * 453 | 454 | Copyright OpenJS Foundation and other contributors 455 | 456 | Based on Underscore.js, copyright Jeremy Ashkenas, 457 | DocumentCloud and Investigative Reporters & Editors 458 | 459 | This software consists of voluntary contributions made by many 460 | individuals. For exact contribution history, see the revision history 461 | available at https://github.com/lodash/lodash 462 | 463 | The following license applies to all parts of this software except as 464 | documented below: 465 | 466 | ==== 467 | 468 | Permission is hereby granted, free of charge, to any person obtaining 469 | a copy of this software and associated documentation files (the 470 | "Software"), to deal in the Software without restriction, including 471 | without limitation the rights to use, copy, modify, merge, publish, 472 | distribute, sublicense, and/or sell copies of the Software, and to 473 | permit persons to whom the Software is furnished to do so, subject to 474 | the following conditions: 475 | 476 | The above copyright notice and this permission notice shall be 477 | included in all copies or substantial portions of the Software. 478 | 479 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 480 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 481 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 482 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 483 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 484 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 485 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 486 | 487 | ==== 488 | 489 | Copyright and related rights for sample code are waived via CC0. Sample 490 | code is defined as all source code displayed within the prose of the 491 | documentation. 492 | 493 | CC0: http://creativecommons.org/publicdomain/zero/1.0/ 494 | 495 | ==== 496 | 497 | Files located in the node_modules and vendor directories are externally 498 | maintained libraries used by this software which have their own 499 | licenses; we recommend you read them, as their terms may differ from the 500 | terms above. 501 | 502 | 503 | * MIT * 504 | 505 | The MIT License (MIT) 506 | 507 | Copyright (c) 2014 David Tudury 508 | 509 | Permission is hereby granted, free of charge, to any person obtaining a copy 510 | of this software and associated documentation files (the "Software"), to deal 511 | in the Software without restriction, including without limitation the rights 512 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 513 | copies of the Software, and to permit persons to whom the Software is 514 | furnished to do so, subject to the following conditions: 515 | 516 | The above copyright notice and this permission notice shall be included in all 517 | copies or substantial portions of the Software. 518 | 519 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 520 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 521 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 522 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 523 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 524 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 525 | SOFTWARE. 526 | 527 | 528 | * MIT * 529 | 530 | The MIT License (MIT) 531 | 532 | Copyright (c) 2014, 2015, 2016, 2017, 2018, 2019 Kartik Chandra, Tim Radvan 533 | 534 | Permission is hereby granted, free of charge, to any person obtaining a copy 535 | of this software and associated documentation files (the "Software"), to deal 536 | in the Software without restriction, including without limitation the rights 537 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 538 | copies of the Software, and to permit persons to whom the Software is 539 | furnished to do so, subject to the following conditions: 540 | 541 | The above copyright notice and this permission notice shall be included in all 542 | copies or substantial portions of the Software. 543 | 544 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 545 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 546 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 547 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 548 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 549 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 550 | SOFTWARE. 551 | 552 | 553 | 554 | -------------------------------------------------------------------------------- 555 | -------------------------------------------------------------------------------- 556 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenFGA Frontend Utils 2 | 3 | Exposes helpful utilities for building authoring experiences of OpenFGA Models. 4 | 5 | Currently used in the OpenFGA Docs and the FGA Playground to provide theming, model validation and diagnostics and graphic capabilities. 6 | 7 | [![npm](https://img.shields.io/npm/v/@openfga/frontend-utils.svg?style=flat)](https://www.npmjs.com/package/@openfga/frontend-utils) 8 | [![Release](https://img.shields.io/github/v/release/openfga/frontend-utils?sort=semver&color=green)](https://github.com/openfga/frontend-utils/releases) 9 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE) 10 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B4989%2Fgithub.com%2Fopenfga%2Ffrontend-utils.svg?type=shield)](https://app.fossa.com/reports/fb48e89d-655d-4656-8c7d-4eaa77e19e72) 11 | [![Join our community](https://img.shields.io/badge/slack-cncf_%23openfga-40abb8.svg?logo=slack)](https://openfga.dev/community) 12 | [![Twitter](https://img.shields.io/twitter/follow/openfga?color=%23179CF0&logo=twitter&style=flat-square "@openfga on Twitter")](https://twitter.com/openfga) 13 | 14 | ## Table of Contents 15 | 16 | - [About OpenFGA](#about-openfga) 17 | - [Resources](#resources) 18 | - [Syntax Transformer & CLI](#syntax-transformer--cli) 19 | - [Installation](#installation) 20 | - [Features](#features) 21 | - [Usage](#usage) 22 | - [Contributing](#contributing) 23 | - [Author](#author) 24 | - [License](#license) 25 | 26 | ## About OpenFGA 27 | 28 | [OpenFGA](https://openfga.dev) is an open source Fine-Grained Authorization solution inspired by [Google's Zanzibar paper](https://research.google/pubs/pub48190/). It was created by the FGA team at [Auth0](https://auth0.com) based on [Auth0 Fine-Grained Authorization (FGA)](https://fga.dev), available under [a permissive license (Apache-2)](https://github.com/openfga/rfcs/blob/main/LICENSE) and welcomes community contributions. 29 | 30 | OpenFGA is designed to make it easy for application builders to model their permission layer, and to add and integrate fine-grained authorization into their applications. OpenFGA’s design is optimized for reliability and low latency at a high scale. 31 | 32 | It allows in-memory data storage for quick development, as well as pluggable database modules - with initial support for PostgreSQL. 33 | 34 | It offers an [HTTP API](https://openfga.dev/api/service) and a [gRPC API](https://buf.build/openfga/api/file/main:openfga/v1/openfga_service.proto). It has SDKs for [Node.js/JavaScript](https://www.npmjs.com/package/@openfga/sdk), [GoLang](https://github.com/openfga/go-sdk), [Python](https://github.com/openfga/python-sdk) and [.NET](https://www.nuget.org/packages/OpenFga.Sdk). Look in our [Community section](https://github.com/openfga/community#community-projects) for third-party SDKs and tools. 35 | 36 | More SDKs and integrations such as Rego are planned for the future. 37 | 38 | ## Resources 39 | 40 | - [OpenFGA Documentation](https://openfga.dev/docs) 41 | - [OpenFGA API Documentation](https://openfga.dev/api) 42 | - [Twitter](https://twitter.com/openfga) 43 | - [OpenFGA Community](https://openfga.dev/community) 44 | - [Zanzibar Academy](https://zanzibar.academy) 45 | - [Google's Zanzibar Paper (2019)](https://research.google/pubs/pub48190/) 46 | 47 | ## Syntax Transformer & CLI 48 | 49 | The Syntax Transformer has a new home in the [language repo](https://github.com/openfga/language). 50 | 51 | The CLI can now be found at https://github.com/openfga/cli. 52 | 53 | ## Installation 54 | 55 | ```bash 56 | npm install --save @openfga/frontend-utils 57 | ``` 58 | 59 | ## Features 60 | 61 | - Theming (for Monaco and Prism) 62 | - Graphing 63 | - Diagnostics (for Monaco and VS Code) 64 | - Snippets (for Monaco and VS Code) 65 | - Hover suggestions (for Monaco and VS Code) 66 | 67 | ## Usage 68 | 69 | TBD 70 | 71 | ## Contributing 72 | 73 | See [CONTRIBUTING](https://github.com/openfga/.github/blob/main/CONTRIBUTING.md). 74 | 75 | ## Author 76 | [OpenFGA](https://openfga.dev) team 77 | 78 | ## License 79 | [Apache-2.0](./LICENSE) 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openfga/frontend-utils", 3 | "version": "0.2.0-beta.11", 4 | "description": "", 5 | "license": "Apache-2.0", 6 | "main": "./dist/index.js", 7 | "typings": "./dist/index.d.ts", 8 | "scripts": { 9 | "build:ts": "rm -rf dist/ && tsc --outDir dist/", 10 | "build": "npm run build:ts", 11 | "build:npx": "tsc --build tsconfig.json", 12 | "clean": "rm -r dist/", 13 | "prepublishOnly": "npm run build", 14 | "test": "jest --config ./tests/jest.config.js", 15 | "typecheck": "tsc --skipLibCheck", 16 | "lint": "eslint -c .eslintrc.js --ext .ts", 17 | "lint:fix": "npm run lint -- --fix", 18 | "format:check": "prettier --check {src,tests}/**", 19 | "format:fix": "prettier --write {src,tests}/**" 20 | }, 21 | "keywords": [ 22 | "openfga", 23 | "authorization", 24 | "fga", 25 | "fine-grained-authorization", 26 | "rebac", 27 | "zanzibar", 28 | "vscode", 29 | "monaco", 30 | "prism", 31 | "language", 32 | "theme" 33 | ], 34 | "author": "OpenFGA", 35 | "dependencies": { 36 | "@openfga/syntax-transformer": "^0.2.0-beta.21" 37 | }, 38 | "devDependencies": { 39 | "@openfga/sdk": "^0.7.0", 40 | "@types/jest": "^29.5.12", 41 | "@types/node": "^22.5.4", 42 | "@types/prismjs": "^1.26.4", 43 | "@typescript-eslint/eslint-plugin": "^8.4.0", 44 | "@typescript-eslint/parser": "^8.4.0", 45 | "eslint": "^8.57.0", 46 | "eslint-config-prettier": "^9.1.0", 47 | "eslint-plugin-import": "^2.30.0", 48 | "jest": "^29.7.0", 49 | "monaco-editor": "0.52.0", 50 | "prettier": "^3.3.3", 51 | "ts-jest": "^29.2.5", 52 | "typescript": "^5.5.4" 53 | }, 54 | "files": [ 55 | "README.md", 56 | "CHANGELOG.md", 57 | "LICENSE", 58 | "dist" 59 | ], 60 | "repository": { 61 | "type": "git", 62 | "url": "git://github.com:openfga/frontend-utils.git" 63 | }, 64 | "bugs": { 65 | "url": "https://github.com/openfga/frontend-utils/issues" 66 | }, 67 | "homepage": "https://github.com/openfga/frontend-utils#readme", 68 | "publishConfig": { 69 | "access": "public", 70 | "provenance": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/constants/default-schema-version.ts: -------------------------------------------------------------------------------- 1 | import { SchemaVersion } from "./schema-version"; 2 | 3 | export const DEFAULT_SCHEMA_VERSION = SchemaVersion.OneDotOne; 4 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { Keyword } from "./keyword"; 2 | import { SchemaVersion } from "./schema-version"; 3 | import { LANGUAGE_NAME } from "./language-name"; 4 | import { DEFAULT_SCHEMA_VERSION } from "./default-schema-version"; 5 | 6 | export const enums = { 7 | Keyword, 8 | SchemaVersion, 9 | }; 10 | 11 | export { LANGUAGE_NAME, DEFAULT_SCHEMA_VERSION }; 12 | 13 | const constants = { 14 | LANGUAGE_NAME, 15 | DEFAULT_SCHEMA_VERSION, 16 | enums, 17 | }; 18 | 19 | export default constants; 20 | -------------------------------------------------------------------------------- /src/constants/keyword.ts: -------------------------------------------------------------------------------- 1 | export enum Keyword { 2 | TYPE = "type", 3 | RELATIONS = "relations", 4 | SELF = "self", 5 | DEFINE = "define", 6 | AS = "as", 7 | OR = "or", 8 | AND = "and", 9 | FROM = "from", 10 | WITH = "with", 11 | BUT_NOT = "but not", 12 | MODEL = "model", 13 | SCHEMA = "schema", 14 | MODULE = "module", 15 | EXTEND = "extend", 16 | CONDITION = "condition", 17 | } 18 | 19 | export enum ReservedKeywords { 20 | THIS = "this", 21 | } 22 | -------------------------------------------------------------------------------- /src/constants/language-name.ts: -------------------------------------------------------------------------------- 1 | export const LANGUAGE_NAME = "dsl.openfga"; 2 | -------------------------------------------------------------------------------- /src/constants/schema-version.ts: -------------------------------------------------------------------------------- 1 | export enum SchemaVersion { 2 | OneDotZero = "1.0", 3 | OneDotOne = "1.1", 4 | OneDotTwo = "1.2", 5 | } 6 | -------------------------------------------------------------------------------- /src/constants/single-indentation.ts: -------------------------------------------------------------------------------- 1 | export const SINGLE_INDENTATION = " "; 2 | -------------------------------------------------------------------------------- /src/documentation/concepts.ts: -------------------------------------------------------------------------------- 1 | import OpenFGAParser from "@openfga/syntax-transformer/dist/gen/OpenFGAParser"; 2 | 3 | // Lookup and return the corresponding literal from the parser, without quotes 4 | 5 | function getSymbol(symbol: number): string { 6 | return OpenFGAParser.literalNames[symbol]!.replace(/'/g, ""); 7 | } 8 | 9 | export type DocumentationMap = Partial>; 10 | 11 | export const defaultDocumentationMap: DocumentationMap = { 12 | [getSymbol(OpenFGAParser.TYPE)]: { 13 | summary: `A type or grouping of objects that have similar characteristics. For example: 14 | - workspace 15 | - repository 16 | - organization 17 | - document`, 18 | link: "https://openfga.dev/docs/concepts#what-is-a-type", 19 | }, 20 | [getSymbol(OpenFGAParser.RELATIONS)]: { 21 | summary: 22 | "A **relation** defines the possible relationship between an [object](https://openfga.dev/docs/concepts#what-is-an-object) and a [user](https://openfga.dev/docs/concepts#what-is-a-user).", 23 | link: "https://openfga.dev/docs/concepts#what-is-a-relation", 24 | }, 25 | [getSymbol(OpenFGAParser.AND)]: { 26 | summary: 27 | "The intersection operator used to indicate that a relationship exists if the user is in all the sets of users.", 28 | link: "https://openfga.dev/docs/configuration-language#the-intersection-operator", 29 | }, 30 | [getSymbol(OpenFGAParser.OR)]: { 31 | summary: 32 | "The union operator is used to indicate that a relationship exists if the user is in any of the sets of users", 33 | link: "https://openfga.dev/docs/configuration-language#the-union-operator", 34 | }, 35 | [getSymbol(OpenFGAParser.BUT_NOT)]: { 36 | summary: 37 | "The exclusion operator is used to indicate that a relationship exists if the user is in the base userset, but not in the excluded userset.", 38 | link: "https://openfga.dev/docs/configuration-language#the-exclusion-operator", 39 | }, 40 | [getSymbol(OpenFGAParser.FROM)]: { 41 | summary: "Allows referencing relations on related objects.", 42 | link: "https://openfga.dev/docs/configuration-language#referencing-relations-on-related-objects", 43 | }, 44 | [getSymbol(OpenFGAParser.SCHEMA)]: { 45 | summary: 46 | "Defines the schema version to be used, with currently only support for '1.1'. Note that the 1.0 schema is deprecated.", 47 | link: "https://openfga.dev/docs/modeling/migrating/migrating-schema-1-1", 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/documentation/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfga/frontend-utils/4aab9b454c2fd79d147c0decce7a19c77c98d6ed/src/documentation/index.ts -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import constants from "./constants"; 2 | import validator from "./validator"; 3 | export * as theming from "./theme"; 4 | export * as tools from "./tools"; 5 | export * as graphBuilder from "./utilities/graphs"; 6 | import sampleAuthorizationModels from "./samples"; 7 | 8 | export { sampleAuthorizationModels, constants, validator }; 9 | -------------------------------------------------------------------------------- /src/inner-utils/assert-never.ts: -------------------------------------------------------------------------------- 1 | export function assertNever(value: never): never { 2 | throw new Error(`Assertion failed. Unexpected value: '${value}'`); 3 | } 4 | -------------------------------------------------------------------------------- /src/inner-utils/insecure-random-string.ts: -------------------------------------------------------------------------------- 1 | // Source: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript/8084248#8084248 2 | export const insecureRandomString = () => (Math.random() + 1).toString(36); 3 | -------------------------------------------------------------------------------- /src/samples/custom-roles.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "team", 10 | "relations": { 11 | "member": { 12 | "this": {} 13 | } 14 | }, 15 | "metadata": { 16 | "relations": { 17 | "member": { 18 | "directly_related_user_types": [ 19 | { 20 | "type": "user" 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | }, 27 | { 28 | "type": "role", 29 | "relations": { 30 | "assignee": { 31 | "this": {} 32 | } 33 | }, 34 | "metadata": { 35 | "relations": { 36 | "assignee": { 37 | "directly_related_user_types": [ 38 | { 39 | "type": "user" 40 | }, 41 | { 42 | "type": "team", 43 | "object": "member" 44 | }, 45 | { 46 | "type": "org", 47 | "object": "member" 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | }, 54 | { 55 | "type": "org", 56 | "relations": { 57 | "asset_category_creator": { 58 | "union": { 59 | "child": [ 60 | { 61 | "this": {} 62 | }, 63 | { 64 | "computedUserset": { 65 | "object": "", 66 | "relation": "owner" 67 | } 68 | } 69 | ] 70 | } 71 | }, 72 | "asset_commenter": { 73 | "union": { 74 | "child": [ 75 | { 76 | "this": {} 77 | }, 78 | { 79 | "computedUserset": { 80 | "object": "", 81 | "relation": "asset_editor" 82 | } 83 | } 84 | ] 85 | } 86 | }, 87 | "asset_creator": { 88 | "union": { 89 | "child": [ 90 | { 91 | "this": {} 92 | }, 93 | { 94 | "computedUserset": { 95 | "object": "", 96 | "relation": "owner" 97 | } 98 | } 99 | ] 100 | } 101 | }, 102 | "asset_editor": { 103 | "union": { 104 | "child": [ 105 | { 106 | "this": {} 107 | }, 108 | { 109 | "computedUserset": { 110 | "object": "", 111 | "relation": "owner" 112 | } 113 | } 114 | ] 115 | } 116 | }, 117 | "asset_viewer": { 118 | "union": { 119 | "child": [ 120 | { 121 | "this": {} 122 | }, 123 | { 124 | "computedUserset": { 125 | "object": "", 126 | "relation": "asset_commenter" 127 | } 128 | } 129 | ] 130 | } 131 | }, 132 | "member": { 133 | "union": { 134 | "child": [ 135 | { 136 | "this": {} 137 | }, 138 | { 139 | "computedUserset": { 140 | "object": "", 141 | "relation": "owner" 142 | } 143 | } 144 | ] 145 | } 146 | }, 147 | "owner": { 148 | "this": {} 149 | }, 150 | "role_assigner": { 151 | "union": { 152 | "child": [ 153 | { 154 | "this": {} 155 | }, 156 | { 157 | "computedUserset": { 158 | "object": "", 159 | "relation": "owner" 160 | } 161 | } 162 | ] 163 | } 164 | }, 165 | "role_creator": { 166 | "union": { 167 | "child": [ 168 | { 169 | "this": {} 170 | }, 171 | { 172 | "computedUserset": { 173 | "object": "", 174 | "relation": "owner" 175 | } 176 | } 177 | ] 178 | } 179 | }, 180 | "team_assigner": { 181 | "union": { 182 | "child": [ 183 | { 184 | "this": {} 185 | }, 186 | { 187 | "computedUserset": { 188 | "object": "", 189 | "relation": "owner" 190 | } 191 | } 192 | ] 193 | } 194 | }, 195 | "team_creator": { 196 | "union": { 197 | "child": [ 198 | { 199 | "this": {} 200 | }, 201 | { 202 | "computedUserset": { 203 | "object": "", 204 | "relation": "owner" 205 | } 206 | } 207 | ] 208 | } 209 | } 210 | }, 211 | "metadata": { 212 | "relations": { 213 | "asset_category_creator": { 214 | "directly_related_user_types": [ 215 | { 216 | "type": "role", 217 | "object": "assignee" 218 | } 219 | ] 220 | }, 221 | "asset_commenter": { 222 | "directly_related_user_types": [ 223 | { 224 | "type": "role", 225 | "object": "assignee" 226 | } 227 | ] 228 | }, 229 | "asset_creator": { 230 | "directly_related_user_types": [ 231 | { 232 | "type": "role", 233 | "object": "assignee" 234 | } 235 | ] 236 | }, 237 | "asset_editor": { 238 | "directly_related_user_types": [ 239 | { 240 | "type": "role", 241 | "object": "assignee" 242 | } 243 | ] 244 | }, 245 | "asset_viewer": { 246 | "directly_related_user_types": [ 247 | { 248 | "type": "role", 249 | "object": "assignee" 250 | } 251 | ] 252 | }, 253 | "member": { 254 | "directly_related_user_types": [ 255 | { 256 | "type": "user" 257 | } 258 | ] 259 | }, 260 | "owner": { 261 | "directly_related_user_types": [ 262 | { 263 | "type": "user" 264 | } 265 | ] 266 | }, 267 | "role_assigner": { 268 | "directly_related_user_types": [ 269 | { 270 | "type": "role", 271 | "object": "assignee" 272 | } 273 | ] 274 | }, 275 | "role_creator": { 276 | "directly_related_user_types": [ 277 | { 278 | "type": "role", 279 | "object": "assignee" 280 | } 281 | ] 282 | }, 283 | "team_assigner": { 284 | "directly_related_user_types": [ 285 | { 286 | "type": "role", 287 | "object": "assignee" 288 | } 289 | ] 290 | }, 291 | "team_creator": { 292 | "directly_related_user_types": [ 293 | { 294 | "type": "role", 295 | "object": "assignee" 296 | } 297 | ] 298 | } 299 | } 300 | } 301 | }, 302 | { 303 | "type": "asset-category", 304 | "relations": { 305 | "asset_creator": { 306 | "union": { 307 | "child": [ 308 | { 309 | "this": {} 310 | }, 311 | { 312 | "tupleToUserset": { 313 | "tupleset": { 314 | "object": "", 315 | "relation": "org" 316 | }, 317 | "computedUserset": { 318 | "object": "", 319 | "relation": "asset_creator" 320 | } 321 | } 322 | } 323 | ] 324 | } 325 | }, 326 | "commenter": { 327 | "union": { 328 | "child": [ 329 | { 330 | "this": {} 331 | }, 332 | { 333 | "computedUserset": { 334 | "object": "", 335 | "relation": "editor" 336 | } 337 | }, 338 | { 339 | "tupleToUserset": { 340 | "tupleset": { 341 | "object": "", 342 | "relation": "org" 343 | }, 344 | "computedUserset": { 345 | "object": "", 346 | "relation": "asset_commenter" 347 | } 348 | } 349 | } 350 | ] 351 | } 352 | }, 353 | "editor": { 354 | "union": { 355 | "child": [ 356 | { 357 | "this": {} 358 | }, 359 | { 360 | "tupleToUserset": { 361 | "tupleset": { 362 | "object": "", 363 | "relation": "org" 364 | }, 365 | "computedUserset": { 366 | "object": "", 367 | "relation": "asset_editor" 368 | } 369 | } 370 | } 371 | ] 372 | } 373 | }, 374 | "org": { 375 | "this": {} 376 | }, 377 | "viewer": { 378 | "union": { 379 | "child": [ 380 | { 381 | "this": {} 382 | }, 383 | { 384 | "computedUserset": { 385 | "object": "", 386 | "relation": "commenter" 387 | } 388 | }, 389 | { 390 | "tupleToUserset": { 391 | "tupleset": { 392 | "object": "", 393 | "relation": "org" 394 | }, 395 | "computedUserset": { 396 | "object": "", 397 | "relation": "asset_viewer" 398 | } 399 | } 400 | } 401 | ] 402 | } 403 | } 404 | }, 405 | "metadata": { 406 | "relations": { 407 | "asset_creator": { 408 | "directly_related_user_types": [ 409 | { 410 | "type": "role", 411 | "object": "assignee" 412 | } 413 | ] 414 | }, 415 | "commenter": { 416 | "directly_related_user_types": [ 417 | { 418 | "type": "role", 419 | "object": "assignee" 420 | } 421 | ] 422 | }, 423 | "editor": { 424 | "directly_related_user_types": [ 425 | { 426 | "type": "role", 427 | "object": "assignee" 428 | } 429 | ] 430 | }, 431 | "org": { 432 | "directly_related_user_types": [ 433 | { 434 | "type": "org" 435 | } 436 | ] 437 | }, 438 | "viewer": { 439 | "directly_related_user_types": [ 440 | { 441 | "type": "role", 442 | "object": "assignee" 443 | } 444 | ] 445 | } 446 | } 447 | } 448 | }, 449 | { 450 | "type": "asset", 451 | "relations": { 452 | "category": { 453 | "this": {} 454 | }, 455 | "comment": { 456 | "union": { 457 | "child": [ 458 | { 459 | "this": {} 460 | }, 461 | { 462 | "computedUserset": { 463 | "object": "", 464 | "relation": "edit" 465 | } 466 | }, 467 | { 468 | "tupleToUserset": { 469 | "tupleset": { 470 | "object": "", 471 | "relation": "category" 472 | }, 473 | "computedUserset": { 474 | "object": "", 475 | "relation": "commenter" 476 | } 477 | } 478 | } 479 | ] 480 | } 481 | }, 482 | "edit": { 483 | "union": { 484 | "child": [ 485 | { 486 | "this": {} 487 | }, 488 | { 489 | "tupleToUserset": { 490 | "tupleset": { 491 | "object": "", 492 | "relation": "category" 493 | }, 494 | "computedUserset": { 495 | "object": "", 496 | "relation": "editor" 497 | } 498 | } 499 | } 500 | ] 501 | } 502 | }, 503 | "view": { 504 | "union": { 505 | "child": [ 506 | { 507 | "this": {} 508 | }, 509 | { 510 | "computedUserset": { 511 | "object": "", 512 | "relation": "comment" 513 | } 514 | }, 515 | { 516 | "tupleToUserset": { 517 | "tupleset": { 518 | "object": "", 519 | "relation": "category" 520 | }, 521 | "computedUserset": { 522 | "object": "", 523 | "relation": "viewer" 524 | } 525 | } 526 | } 527 | ] 528 | } 529 | } 530 | }, 531 | "metadata": { 532 | "relations": { 533 | "category": { 534 | "directly_related_user_types": [ 535 | { 536 | "type": "asset-category" 537 | } 538 | ] 539 | }, 540 | "comment": { 541 | "directly_related_user_types": [ 542 | { 543 | "type": "role", 544 | "object": "assignee" 545 | } 546 | ] 547 | }, 548 | "edit": { 549 | "directly_related_user_types": [ 550 | { 551 | "type": "role", 552 | "object": "assignee" 553 | } 554 | ] 555 | }, 556 | "view": { 557 | "directly_related_user_types": [ 558 | { 559 | "type": "role", 560 | "object": "assignee" 561 | } 562 | ] 563 | } 564 | } 565 | } 566 | } 567 | ] 568 | } 569 | -------------------------------------------------------------------------------- /src/samples/entitlements.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "plan", 10 | "relations": { 11 | "subscriber": { 12 | "this": {} 13 | }, 14 | "subscriber_member": { 15 | "tupleToUserset": { 16 | "tupleset": { 17 | "object": "", 18 | "relation": "subscriber" 19 | }, 20 | "computedUserset": { 21 | "object": "", 22 | "relation": "member" 23 | } 24 | } 25 | } 26 | }, 27 | "metadata": { 28 | "relations": { 29 | "subscriber": { 30 | "directly_related_user_types": [ 31 | { 32 | "type": "organization" 33 | } 34 | ] 35 | }, 36 | "subscriber_member": { 37 | "directly_related_user_types": [] 38 | } 39 | } 40 | } 41 | }, 42 | { 43 | "type": "organization", 44 | "relations": { 45 | "member": { 46 | "this": {} 47 | } 48 | }, 49 | "metadata": { 50 | "relations": { 51 | "member": { 52 | "directly_related_user_types": [ 53 | { 54 | "type": "user" 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "type": "feature", 63 | "relations": { 64 | "can_access": { 65 | "tupleToUserset": { 66 | "tupleset": { 67 | "object": "", 68 | "relation": "associated_plan" 69 | }, 70 | "computedUserset": { 71 | "object": "", 72 | "relation": "subscriber_member" 73 | } 74 | } 75 | }, 76 | "associated_plan": { 77 | "this": {} 78 | } 79 | }, 80 | "metadata": { 81 | "relations": { 82 | "can_access": { 83 | "directly_related_user_types": [] 84 | }, 85 | "associated_plan": { 86 | "directly_related_user_types": [ 87 | { 88 | "type": "plan" 89 | } 90 | ] 91 | } 92 | } 93 | } 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /src/samples/expenses.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "employee", 6 | "relations": { 7 | "manager": { 8 | "this": {} 9 | }, 10 | "can_manage": { 11 | "union": { 12 | "child": [ 13 | { 14 | "computedUserset": { 15 | "relation": "manager" 16 | } 17 | }, 18 | { 19 | "tupleToUserset": { 20 | "tupleset": { 21 | "object": "", 22 | "relation": "manager" 23 | }, 24 | "computedUserset": { 25 | "object": "", 26 | "relation": "can_manage" 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | } 33 | }, 34 | "metadata": { 35 | "relations": { 36 | "manager": { 37 | "directly_related_user_types": [ 38 | { 39 | "type": "employee" 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | }, 46 | { 47 | "type": "report", 48 | "relations": { 49 | "submitter": { 50 | "this": {} 51 | }, 52 | "approver": { 53 | "tupleToUserset": { 54 | "tupleset": { 55 | "object": "", 56 | "relation": "submitter" 57 | }, 58 | "computedUserset": { 59 | "object": "", 60 | "relation": "can_manage" 61 | } 62 | } 63 | } 64 | }, 65 | "metadata": { 66 | "relations": { 67 | "submitter": { 68 | "directly_related_user_types": [ 69 | { 70 | "type": "employee" 71 | } 72 | ] 73 | }, 74 | "approver": { 75 | "directly_related_user_types": [] 76 | } 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /src/samples/gdrive.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "group", 10 | "relations": { 11 | "member": { 12 | "this": {} 13 | } 14 | }, 15 | "metadata": { 16 | "relations": { 17 | "member": { 18 | "directly_related_user_types": [ 19 | { 20 | "type": "user" 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | }, 27 | { 28 | "type": "folder", 29 | "relations": { 30 | "can_create_file": { 31 | "computedUserset": { 32 | "object": "", 33 | "relation": "owner" 34 | } 35 | }, 36 | "owner": { 37 | "this": {} 38 | }, 39 | "parent": { 40 | "this": {} 41 | }, 42 | "viewer": { 43 | "union": { 44 | "child": [ 45 | { 46 | "this": {} 47 | }, 48 | { 49 | "computedUserset": { 50 | "object": "", 51 | "relation": "owner" 52 | } 53 | }, 54 | { 55 | "tupleToUserset": { 56 | "tupleset": { 57 | "object": "", 58 | "relation": "parent" 59 | }, 60 | "computedUserset": { 61 | "object": "", 62 | "relation": "viewer" 63 | } 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | }, 70 | "metadata": { 71 | "relations": { 72 | "can_create_file": { "directly_related_user_types": [] }, 73 | "owner": { 74 | "directly_related_user_types": [ 75 | { 76 | "type": "user" 77 | } 78 | ] 79 | }, 80 | "parent": { 81 | "directly_related_user_types": [ 82 | { 83 | "type": "folder" 84 | } 85 | ] 86 | }, 87 | "viewer": { 88 | "directly_related_user_types": [ 89 | { 90 | "type": "user" 91 | }, 92 | { 93 | "type": "user", 94 | "wildcard": {} 95 | }, 96 | { 97 | "type": "group", 98 | "relation": "member" 99 | } 100 | ] 101 | } 102 | } 103 | } 104 | }, 105 | { 106 | "type": "doc", 107 | "relations": { 108 | "can_change_owner": { 109 | "computedUserset": { 110 | "object": "", 111 | "relation": "owner" 112 | } 113 | }, 114 | "owner": { 115 | "this": {} 116 | }, 117 | "parent": { 118 | "this": {} 119 | }, 120 | "can_read": { 121 | "union": { 122 | "child": [ 123 | { 124 | "computedUserset": { 125 | "object": "", 126 | "relation": "viewer" 127 | } 128 | }, 129 | { 130 | "computedUserset": { 131 | "object": "", 132 | "relation": "owner" 133 | } 134 | }, 135 | { 136 | "tupleToUserset": { 137 | "tupleset": { 138 | "object": "", 139 | "relation": "parent" 140 | }, 141 | "computedUserset": { 142 | "object": "", 143 | "relation": "viewer" 144 | } 145 | } 146 | } 147 | ] 148 | } 149 | }, 150 | "can_share": { 151 | "union": { 152 | "child": [ 153 | { 154 | "computedUserset": { 155 | "object": "", 156 | "relation": "owner" 157 | } 158 | }, 159 | { 160 | "tupleToUserset": { 161 | "tupleset": { 162 | "object": "", 163 | "relation": "parent" 164 | }, 165 | "computedUserset": { 166 | "object": "", 167 | "relation": "owner" 168 | } 169 | } 170 | } 171 | ] 172 | } 173 | }, 174 | "viewer": { 175 | "this": {} 176 | }, 177 | "can_write": { 178 | "union": { 179 | "child": [ 180 | { 181 | "computedUserset": { 182 | "object": "", 183 | "relation": "owner" 184 | } 185 | }, 186 | { 187 | "tupleToUserset": { 188 | "tupleset": { 189 | "object": "", 190 | "relation": "parent" 191 | }, 192 | "computedUserset": { 193 | "object": "", 194 | "relation": "owner" 195 | } 196 | } 197 | } 198 | ] 199 | } 200 | } 201 | }, 202 | "metadata": { 203 | "relations": { 204 | "can_change_owner": { "directly_related_user_types": [] }, 205 | "owner": { 206 | "directly_related_user_types": [ 207 | { 208 | "type": "user" 209 | } 210 | ] 211 | }, 212 | "parent": { 213 | "directly_related_user_types": [ 214 | { 215 | "type": "folder" 216 | } 217 | ] 218 | }, 219 | "can_read": { "directly_related_user_types": [] }, 220 | "can_share": { "directly_related_user_types": [] }, 221 | "viewer": { 222 | "directly_related_user_types": [ 223 | { 224 | "type": "user" 225 | }, 226 | { 227 | "type": "user", 228 | "wildcard": {} 229 | }, 230 | { 231 | "type": "group", 232 | "relation": "member" 233 | } 234 | ] 235 | }, 236 | "can_write": { "directly_related_user_types": [] } 237 | } 238 | } 239 | } 240 | ] 241 | } 242 | -------------------------------------------------------------------------------- /src/samples/generic.json: -------------------------------------------------------------------------------- 1 | { 2 | "type_definitions": [ 3 | { 4 | "type": "user", 5 | "relations": {} 6 | }, 7 | { 8 | "type": "group", 9 | "relations": { 10 | "member": { 11 | "this": {} 12 | } 13 | }, 14 | "metadata": { 15 | "relations": { 16 | "member": { 17 | "directly_related_user_types": [ 18 | { 19 | "type": "user" 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | }, 26 | { 27 | "type": "resource", 28 | "relations": { 29 | "can_edit": { 30 | "computedUserset": { 31 | "object": "", 32 | "relation": "editor" 33 | } 34 | }, 35 | "can_view": { 36 | "union": { 37 | "child": [ 38 | { 39 | "computedUserset": { 40 | "object": "", 41 | "relation": "viewer" 42 | } 43 | }, 44 | { 45 | "computedUserset": { 46 | "object": "", 47 | "relation": "editor" 48 | } 49 | } 50 | ] 51 | } 52 | }, 53 | "editor": { 54 | "this": {} 55 | }, 56 | "viewer": { 57 | "this": {} 58 | } 59 | }, 60 | "metadata": { 61 | "relations": { 62 | "can_edit": { 63 | "directly_related_user_types": [] 64 | }, 65 | "can_view": { 66 | "directly_related_user_types": [] 67 | }, 68 | "editor": { 69 | "directly_related_user_types": [ 70 | { 71 | "type": "user" 72 | }, 73 | { 74 | "type": "group", 75 | "relation": "member" 76 | } 77 | ] 78 | }, 79 | "viewer": { 80 | "directly_related_user_types": [ 81 | { 82 | "type": "user" 83 | }, 84 | { 85 | "type": "group", 86 | "relation": "member" 87 | } 88 | ] 89 | } 90 | } 91 | } 92 | } 93 | ], 94 | "schema_version": "1.1" 95 | } 96 | -------------------------------------------------------------------------------- /src/samples/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "team", 10 | "relations": { 11 | "member": { 12 | "this": {} 13 | } 14 | }, 15 | "metadata": { 16 | "relations": { 17 | "member": { 18 | "directly_related_user_types": [ 19 | { 20 | "type": "user" 21 | }, 22 | { 23 | "type": "team", 24 | "relation": "member" 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | }, 31 | { 32 | "type": "repo", 33 | "relations": { 34 | "admin": { 35 | "union": { 36 | "child": [ 37 | { 38 | "this": {} 39 | }, 40 | { 41 | "tupleToUserset": { 42 | "tupleset": { 43 | "object": "", 44 | "relation": "owner" 45 | }, 46 | "computedUserset": { 47 | "object": "", 48 | "relation": "repo_admin" 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | }, 55 | "maintainer": { 56 | "union": { 57 | "child": [ 58 | { 59 | "this": {} 60 | }, 61 | { 62 | "computedUserset": { 63 | "object": "", 64 | "relation": "admin" 65 | } 66 | } 67 | ] 68 | } 69 | }, 70 | "owner": { 71 | "this": {} 72 | }, 73 | "reader": { 74 | "union": { 75 | "child": [ 76 | { 77 | "this": {} 78 | }, 79 | { 80 | "computedUserset": { 81 | "object": "", 82 | "relation": "triager" 83 | } 84 | }, 85 | { 86 | "tupleToUserset": { 87 | "tupleset": { 88 | "object": "", 89 | "relation": "owner" 90 | }, 91 | "computedUserset": { 92 | "object": "", 93 | "relation": "repo_reader" 94 | } 95 | } 96 | } 97 | ] 98 | } 99 | }, 100 | "triager": { 101 | "union": { 102 | "child": [ 103 | { 104 | "this": {} 105 | }, 106 | { 107 | "computedUserset": { 108 | "object": "", 109 | "relation": "writer" 110 | } 111 | } 112 | ] 113 | } 114 | }, 115 | "writer": { 116 | "union": { 117 | "child": [ 118 | { 119 | "this": {} 120 | }, 121 | { 122 | "computedUserset": { 123 | "object": "", 124 | "relation": "maintainer" 125 | } 126 | }, 127 | { 128 | "tupleToUserset": { 129 | "tupleset": { 130 | "object": "", 131 | "relation": "owner" 132 | }, 133 | "computedUserset": { 134 | "object": "", 135 | "relation": "repo_writer" 136 | } 137 | } 138 | } 139 | ] 140 | } 141 | } 142 | }, 143 | "metadata": { 144 | "relations": { 145 | "admin": { 146 | "directly_related_user_types": [ 147 | { 148 | "type": "user" 149 | }, 150 | { 151 | "type": "team", 152 | "relation": "member" 153 | } 154 | ] 155 | }, 156 | "maintainer": { 157 | "directly_related_user_types": [ 158 | { 159 | "type": "user" 160 | }, 161 | { 162 | "type": "team", 163 | "relation": "member" 164 | } 165 | ] 166 | }, 167 | "owner": { 168 | "directly_related_user_types": [ 169 | { 170 | "type": "organization" 171 | } 172 | ] 173 | }, 174 | "reader": { 175 | "directly_related_user_types": [ 176 | { 177 | "type": "user" 178 | }, 179 | { 180 | "type": "team", 181 | "relation": "member" 182 | } 183 | ] 184 | }, 185 | "triager": { 186 | "directly_related_user_types": [ 187 | { 188 | "type": "user" 189 | }, 190 | { 191 | "type": "team", 192 | "relation": "member" 193 | } 194 | ] 195 | }, 196 | "writer": { 197 | "directly_related_user_types": [ 198 | { 199 | "type": "user" 200 | }, 201 | { 202 | "type": "team", 203 | "relation": "member" 204 | } 205 | ] 206 | } 207 | } 208 | } 209 | }, 210 | { 211 | "type": "organization", 212 | "relations": { 213 | "member": { 214 | "union": { 215 | "child": [ 216 | { 217 | "this": {} 218 | }, 219 | { 220 | "computedUserset": { 221 | "object": "", 222 | "relation": "owner" 223 | } 224 | } 225 | ] 226 | } 227 | }, 228 | "owner": { 229 | "this": {} 230 | }, 231 | "repo_admin": { 232 | "this": {} 233 | }, 234 | "repo_reader": { 235 | "this": {} 236 | }, 237 | "repo_writer": { 238 | "this": {} 239 | } 240 | }, 241 | "metadata": { 242 | "relations": { 243 | "member": { 244 | "directly_related_user_types": [ 245 | { 246 | "type": "user" 247 | } 248 | ] 249 | }, 250 | "owner": { 251 | "directly_related_user_types": [ 252 | { 253 | "type": "user" 254 | } 255 | ] 256 | }, 257 | "repo_admin": { 258 | "directly_related_user_types": [ 259 | { 260 | "type": "user" 261 | }, 262 | { 263 | "type": "organization", 264 | "relation": "member" 265 | } 266 | ] 267 | }, 268 | "repo_reader": { 269 | "directly_related_user_types": [ 270 | { 271 | "type": "user" 272 | }, 273 | { 274 | "type": "organization", 275 | "relation": "member" 276 | } 277 | ] 278 | }, 279 | "repo_writer": { 280 | "directly_related_user_types": [ 281 | { 282 | "type": "user" 283 | }, 284 | { 285 | "type": "organization", 286 | "relation": "member" 287 | } 288 | ] 289 | } 290 | } 291 | } 292 | } 293 | ] 294 | } 295 | -------------------------------------------------------------------------------- /src/samples/index.ts: -------------------------------------------------------------------------------- 1 | import type { AuthorizationModel as ApiAuthorizationModel, TypeDefinition } from "@openfga/sdk"; 2 | 3 | type AuthorizationModel = { 4 | schema_version: Required; 5 | type_definitions: TypeDefinition[]; 6 | }; 7 | 8 | const entitlements = import("./entitlements.json") as unknown as Promise; 9 | const expenses = import("./expenses.json") as any as Promise; 10 | const gdrive = import("./gdrive.json") as unknown as Promise; 11 | const generic = import("./generic.json") as unknown as Promise; 12 | const github = import("./github.json") as unknown as Promise; 13 | const iot = import("./iot.json") as Promise; 14 | const slack = import("./slack.json") as unknown as Promise; 15 | const customRoles = import("./custom-roles.json") as unknown as Promise; 16 | 17 | const sampleAuthorizationModels: Record>>> = { 18 | entitlements, 19 | expenses, 20 | gdrive, 21 | generic, 22 | github, 23 | iot, 24 | slack, 25 | customRoles, 26 | }; 27 | 28 | export default sampleAuthorizationModels; 29 | -------------------------------------------------------------------------------- /src/samples/iot.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "device_group", 10 | "relations": { 11 | "it_admin": { 12 | "this": {} 13 | }, 14 | "security_guard": { 15 | "this": {} 16 | } 17 | }, 18 | "metadata": { 19 | "relations": { 20 | "it_admin": { 21 | "directly_related_user_types": [ 22 | { 23 | "type": "user" 24 | } 25 | ] 26 | }, 27 | "security_guard": { 28 | "directly_related_user_types": [ 29 | { 30 | "type": "user" 31 | } 32 | ] 33 | } 34 | } 35 | } 36 | }, 37 | { 38 | "type": "device", 39 | "relations": { 40 | "can_rename_device": { 41 | "computedUserset": { 42 | "object": "", 43 | "relation": "it_admin" 44 | } 45 | }, 46 | "it_admin": { 47 | "this": {} 48 | }, 49 | "can_view_live_video": { 50 | "union": { 51 | "child": [ 52 | { 53 | "computedUserset": { 54 | "object": "", 55 | "relation": "it_admin" 56 | } 57 | }, 58 | { 59 | "computedUserset": { 60 | "object": "", 61 | "relation": "security_guard" 62 | } 63 | } 64 | ] 65 | } 66 | }, 67 | "can_view_recorded_video": { 68 | "union": { 69 | "child": [ 70 | { 71 | "computedUserset": { 72 | "object": "", 73 | "relation": "it_admin" 74 | } 75 | }, 76 | { 77 | "computedUserset": { 78 | "object": "", 79 | "relation": "security_guard" 80 | } 81 | } 82 | ] 83 | } 84 | }, 85 | "security_guard": { 86 | "this": {} 87 | } 88 | }, 89 | "metadata": { 90 | "relations": { 91 | "can_rename_device": { 92 | "directly_related_user_types": [] 93 | }, 94 | "it_admin": { 95 | "directly_related_user_types": [ 96 | { 97 | "type": "user" 98 | }, 99 | { 100 | "type": "device_group", 101 | "relation": "it_admin" 102 | } 103 | ] 104 | }, 105 | "can_view_live_video": { 106 | "directly_related_user_types": [] 107 | }, 108 | "can_view_recorded_video": { 109 | "directly_related_user_types": [] 110 | }, 111 | "security_guard": { 112 | "directly_related_user_types": [ 113 | { 114 | "type": "user" 115 | }, 116 | { 117 | "type": "device_group", 118 | "relation": "security_guard" 119 | } 120 | ] 121 | } 122 | } 123 | } 124 | } 125 | ] 126 | } 127 | -------------------------------------------------------------------------------- /src/samples/slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.1", 3 | "type_definitions": [ 4 | { 5 | "type": "user", 6 | "relations": {} 7 | }, 8 | { 9 | "type": "workspace", 10 | "relations": { 11 | "channels_admin": { 12 | "union": { 13 | "child": [ 14 | { 15 | "this": {} 16 | }, 17 | { 18 | "computedUserset": { 19 | "object": "", 20 | "relation": "legacy_admin" 21 | } 22 | } 23 | ] 24 | } 25 | }, 26 | "guest": { 27 | "this": {} 28 | }, 29 | "legacy_admin": { 30 | "this": {} 31 | }, 32 | "member": { 33 | "union": { 34 | "child": [ 35 | { 36 | "this": {} 37 | }, 38 | { 39 | "computedUserset": { 40 | "object": "", 41 | "relation": "legacy_admin" 42 | } 43 | }, 44 | { 45 | "computedUserset": { 46 | "object": "", 47 | "relation": "channels_admin" 48 | } 49 | } 50 | ] 51 | } 52 | } 53 | }, 54 | "metadata": { 55 | "relations": { 56 | "channels_admin": { 57 | "directly_related_user_types": [ 58 | { 59 | "type": "user" 60 | } 61 | ] 62 | }, 63 | "guest": { 64 | "directly_related_user_types": [ 65 | { 66 | "type": "user" 67 | } 68 | ] 69 | }, 70 | "legacy_admin": { 71 | "directly_related_user_types": [ 72 | { 73 | "type": "user" 74 | } 75 | ] 76 | }, 77 | "member": { 78 | "directly_related_user_types": [ 79 | { 80 | "type": "user" 81 | } 82 | ] 83 | } 84 | } 85 | } 86 | }, 87 | { 88 | "type": "channel", 89 | "relations": { 90 | "commenter": { 91 | "union": { 92 | "child": [ 93 | { 94 | "this": {} 95 | }, 96 | { 97 | "computedUserset": { 98 | "object": "", 99 | "relation": "writer" 100 | } 101 | } 102 | ] 103 | } 104 | }, 105 | "parent_workspace": { 106 | "this": {} 107 | }, 108 | "writer": { 109 | "this": {} 110 | } 111 | }, 112 | "metadata": { 113 | "relations": { 114 | "commenter": { 115 | "directly_related_user_types": [ 116 | { 117 | "type": "user" 118 | } 119 | ] 120 | }, 121 | "parent_workspace": { 122 | "directly_related_user_types": [ 123 | { 124 | "type": "workspace" 125 | } 126 | ] 127 | }, 128 | "writer": { 129 | "directly_related_user_types": [ 130 | { 131 | "type": "user" 132 | } 133 | ] 134 | } 135 | } 136 | } 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { openfgaDark } from "./supported-themes/openfga-dark"; 2 | import { OpenFgaThemeConfiguration, SupportedTheme } from "./theme.typings"; 3 | 4 | export { 5 | OpenFgaThemeConfiguration, 6 | SupportedTheme, 7 | OpenFgaDslThemeToken, 8 | OpenFgaDslThemeTokenType, 9 | } from "./theme.typings"; 10 | 11 | export const supportedThemes: Record = { 12 | [SupportedTheme.OpenFgaDark]: openfgaDark, 13 | }; 14 | -------------------------------------------------------------------------------- /src/theme/supported-themes/openfga-dark.ts: -------------------------------------------------------------------------------- 1 | import { OpenFgaDslThemeTokenType, OpenFgaThemeConfiguration } from "../theme.typings"; 2 | 3 | export const openfgaDark: OpenFgaThemeConfiguration = { 4 | name: "openfga-dark", 5 | baseTheme: "vs-dark", 6 | background: { 7 | color: "#141517", 8 | }, 9 | colors: { 10 | [OpenFgaDslThemeTokenType.DEFAULT]: "#FFFFFF", 11 | [OpenFgaDslThemeTokenType.COMMENT]: "#737981", 12 | [OpenFgaDslThemeTokenType.KEYWORD]: "#AAAAAA", 13 | [OpenFgaDslThemeTokenType.MODULE]: "#79ED83", 14 | [OpenFgaDslThemeTokenType.EXTEND]: "#79ED83", 15 | [OpenFgaDslThemeTokenType.TYPE]: "#79ED83", 16 | [OpenFgaDslThemeTokenType.RELATION]: "#20F1F5", 17 | [OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE]: "#CEEC93", 18 | [OpenFgaDslThemeTokenType.CONDITION]: "#79ED83", 19 | [OpenFgaDslThemeTokenType.CONDITION_PARAM]: "#20F1F5", 20 | [OpenFgaDslThemeTokenType.CONDITION_PARAM_TYPE]: "#AAAAAA", 21 | }, 22 | styles: {}, 23 | }; 24 | -------------------------------------------------------------------------------- /src/theme/theme.typings.ts: -------------------------------------------------------------------------------- 1 | export enum OpenFgaDslThemeTokenType { 2 | DEFAULT = "default", 3 | COMMENT = "comment", 4 | KEYWORD = "keyword", 5 | EXTEND = "extend", 6 | TYPE = "type", 7 | MODULE = "module", 8 | RELATION = "relation", 9 | DIRECTLY_ASSIGNABLE = "directly-assignable", 10 | CONDITION = "condition", 11 | CONDITION_PARAM = "condition-param", 12 | CONDITION_PARAM_TYPE = "condition-param-type", 13 | } 14 | 15 | export enum OpenFgaDslThemeToken { 16 | COMMENT = "comment", 17 | DELIMITER_BRACKET_RELATION_DEFINITION = "delimiter.bracket.relation-definition", 18 | DELIMITER_BRACKET_TYPE_RESTRICTIONS = "delimiter.bracket.type-restrictions", 19 | DELIMITER_BRACKET_CONDITION_EXPRESSION = "delimiter.bracket.condition-expression", 20 | DELIMITER_COLON_TYPE_RESTRICTIONS = "delimiter.colon.type-restrictions", 21 | DELIMITER_COMMA_TYPE_RESTRICTIONS = "delimiter.comma.type-restrictions", 22 | DELIMITER_COLON_CONDITION_PARAM = "delimiter.colon.condition", 23 | DELIMITER_COMMA_CONDITION_PARAM = "delimiter.comma.condition", 24 | DELIMITER_DEFINE_COLON = "delimiter.colon.define", 25 | DELIMITER_HASHTAG_TYPE_RESTRICTIONS = "delimiter.hashtag.type-restrictions", 26 | KEYWORD_AS = "keyword.as", 27 | KEYWORD_DEFINE = "keyword.define", 28 | KEYWORD_FROM = "keyword.from", 29 | KEYWORD_MODEL = "keyword.model", 30 | KEYWORD_RELATIONS = "keyword.relations", 31 | KEYWORD_SCHEMA = "keyword.schema", 32 | KEYWORD_MODULE = "keyword.module", 33 | KEYWORD_EXTEND = "keyword.extend", 34 | KEYWORD_SELF = "keyword.self", 35 | KEYWORD_TYPE = "keyword.type", 36 | KEYWORD_CONDITION = "keyword.condition", 37 | KEYWORD_WITH = "keyword.with", 38 | OPERATOR_AND = "keyword.operator.word.intersection", 39 | OPERATOR_BUT_NOT = "keyword.operator.word.exclusion", 40 | OPERATOR_OR = "keyword.operator.word.union", 41 | VALUE_CONDITION = "entity.name.function.condition", 42 | VALUE_RELATION_COMPUTED = "computed.relation.value", 43 | VALUE_RELATION_NAME = "entity.name.function.member.relation.name", 44 | VALUE_RELATION_TUPLE_TO_USERSET_COMPUTED = "computed.tupletouserset.relation.value", 45 | VALUE_RELATION_TUPLE_TO_USERSET_TUPLESET = "tupleset.tupletouserset.relation.value", 46 | VALUE_MODULE = "module.value", 47 | VALUE_SCHEMA = "schema.value", 48 | VALUE_TYPE_NAME = "support.class.type.name.value", 49 | VALUE_TYPE_RESTRICTIONS_RELATION = "variable.parameter.type-restrictions.relation.value", 50 | VALUE_TYPE_RESTRICTIONS_TYPE = "variable.parameter.type-restrictions.type.value", 51 | VALUE_TYPE_RESTRICTIONS_WILDCARD = "variable.parameter.type-restrictions.wildcard.value", 52 | CONDITION_PARAM = "variable.parameter.name.condition", 53 | CONDITION_PARAM_TYPE = "variable.parameter.type.condition", 54 | CONDITION_SYMBOL = "identifier.symbol.condition", 55 | } 56 | 57 | export interface OpenFgaThemeConfiguration { 58 | name: string; 59 | baseTheme?: "vs" | "vs-dark" | "hc-black" | "hc-light"; 60 | colors: Record; 61 | rawColorOverrides?: Partial>; 62 | styles?: Partial>; 63 | rawStylesOverrides?: Partial>; 64 | background: { 65 | color: string; 66 | }; 67 | } 68 | 69 | export enum SupportedTheme { 70 | OpenFgaDark = "openfga-dark", 71 | } 72 | -------------------------------------------------------------------------------- /src/theme/utils.ts: -------------------------------------------------------------------------------- 1 | import { OpenFgaDslThemeToken, OpenFgaDslThemeTokenType, OpenFgaThemeConfiguration } from "./theme.typings"; 2 | 3 | const tokenTypeMap: Record = { 4 | [OpenFgaDslThemeToken.COMMENT]: OpenFgaDslThemeTokenType.COMMENT, 5 | [OpenFgaDslThemeToken.DELIMITER_BRACKET_RELATION_DEFINITION]: OpenFgaDslThemeTokenType.DEFAULT, 6 | [OpenFgaDslThemeToken.DELIMITER_BRACKET_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 7 | [OpenFgaDslThemeToken.DELIMITER_BRACKET_CONDITION_EXPRESSION]: OpenFgaDslThemeTokenType.DEFAULT, 8 | [OpenFgaDslThemeToken.DELIMITER_COLON_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 9 | [OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 10 | [OpenFgaDslThemeToken.DELIMITER_COLON_CONDITION_PARAM]: OpenFgaDslThemeTokenType.DEFAULT, 11 | [OpenFgaDslThemeToken.DELIMITER_COMMA_CONDITION_PARAM]: OpenFgaDslThemeTokenType.DEFAULT, 12 | [OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON]: OpenFgaDslThemeTokenType.DEFAULT, 13 | [OpenFgaDslThemeToken.DELIMITER_HASHTAG_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 14 | [OpenFgaDslThemeToken.KEYWORD_AS]: OpenFgaDslThemeTokenType.KEYWORD, 15 | [OpenFgaDslThemeToken.KEYWORD_DEFINE]: OpenFgaDslThemeTokenType.KEYWORD, 16 | [OpenFgaDslThemeToken.KEYWORD_FROM]: OpenFgaDslThemeTokenType.KEYWORD, 17 | [OpenFgaDslThemeToken.KEYWORD_MODEL]: OpenFgaDslThemeTokenType.KEYWORD, 18 | [OpenFgaDslThemeToken.KEYWORD_MODULE]: OpenFgaDslThemeTokenType.KEYWORD, 19 | [OpenFgaDslThemeToken.KEYWORD_EXTEND]: OpenFgaDslThemeTokenType.KEYWORD, 20 | [OpenFgaDslThemeToken.KEYWORD_RELATIONS]: OpenFgaDslThemeTokenType.KEYWORD, 21 | [OpenFgaDslThemeToken.KEYWORD_SCHEMA]: OpenFgaDslThemeTokenType.KEYWORD, 22 | [OpenFgaDslThemeToken.KEYWORD_SELF]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 23 | [OpenFgaDslThemeToken.KEYWORD_TYPE]: OpenFgaDslThemeTokenType.KEYWORD, 24 | [OpenFgaDslThemeToken.OPERATOR_AND]: OpenFgaDslThemeTokenType.KEYWORD, 25 | [OpenFgaDslThemeToken.OPERATOR_BUT_NOT]: OpenFgaDslThemeTokenType.KEYWORD, 26 | [OpenFgaDslThemeToken.OPERATOR_OR]: OpenFgaDslThemeTokenType.KEYWORD, 27 | [OpenFgaDslThemeToken.KEYWORD_CONDITION]: OpenFgaDslThemeTokenType.KEYWORD, 28 | [OpenFgaDslThemeToken.KEYWORD_WITH]: OpenFgaDslThemeTokenType.KEYWORD, 29 | [OpenFgaDslThemeToken.VALUE_CONDITION]: OpenFgaDslThemeTokenType.CONDITION, 30 | [OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED]: OpenFgaDslThemeTokenType.DEFAULT, 31 | [OpenFgaDslThemeToken.VALUE_RELATION_NAME]: OpenFgaDslThemeTokenType.RELATION, 32 | [OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_COMPUTED]: OpenFgaDslThemeTokenType.DEFAULT, 33 | [OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_TUPLESET]: OpenFgaDslThemeTokenType.DEFAULT, 34 | [OpenFgaDslThemeToken.VALUE_MODULE]: OpenFgaDslThemeTokenType.TYPE, 35 | [OpenFgaDslThemeToken.VALUE_SCHEMA]: OpenFgaDslThemeTokenType.DEFAULT, 36 | [OpenFgaDslThemeToken.VALUE_TYPE_NAME]: OpenFgaDslThemeTokenType.TYPE, 37 | [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_RELATION]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 38 | [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_TYPE]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 39 | [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_WILDCARD]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, 40 | [OpenFgaDslThemeToken.CONDITION_PARAM]: OpenFgaDslThemeTokenType.CONDITION_PARAM, 41 | [OpenFgaDslThemeToken.CONDITION_PARAM_TYPE]: OpenFgaDslThemeTokenType.CONDITION_PARAM_TYPE, 42 | [OpenFgaDslThemeToken.CONDITION_SYMBOL]: OpenFgaDslThemeTokenType.DEFAULT, 43 | }; 44 | 45 | export function getThemeTokenStyle( 46 | token: OpenFgaDslThemeToken, 47 | themeConfig: OpenFgaThemeConfiguration, 48 | ): { 49 | foreground?: string; 50 | fontStyle?: string; 51 | } { 52 | return { 53 | foreground: 54 | themeConfig.rawColorOverrides?.[token] || 55 | themeConfig.colors[tokenTypeMap[token || OpenFgaDslThemeTokenType.DEFAULT]], 56 | fontStyle: 57 | themeConfig.rawStylesOverrides?.[token] || 58 | themeConfig.styles?.[tokenTypeMap[token || OpenFgaDslThemeTokenType.DEFAULT]], 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export { MonacoExtensions } from "./monaco"; 2 | export { PrismExtensions } from "./prism"; 3 | -------------------------------------------------------------------------------- /src/tools/monaco/index.ts: -------------------------------------------------------------------------------- 1 | import { LANGUAGE_NAME } from "../../constants"; 2 | import * as languageDefinition from "./language-definition"; 3 | import { registerDSL } from "./register-dsl"; 4 | export type { DocumentationMap } from "./providers/hover-actions"; 5 | import { buildMonacoTheme, monacoThemes } from "./theme"; 6 | import { validateDSL } from "./validate"; 7 | 8 | export const MonacoExtensions = { 9 | LANGUAGE_NAME, 10 | registerDSL, 11 | monacoThemes, 12 | validateDSL, 13 | languageDefinition, 14 | buildMonacoTheme, 15 | }; 16 | -------------------------------------------------------------------------------- /src/tools/monaco/language-definition.ts: -------------------------------------------------------------------------------- 1 | import type * as MonacoEditor from "monaco-editor"; 2 | 3 | import { Keyword } from "../../constants/keyword"; 4 | import { LANGUAGE_NAME } from "../../constants"; 5 | import { OpenFgaDslThemeToken } from "../../theme"; 6 | 7 | // Source for the code below: 8 | // https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/python/python.ts 9 | 10 | // This is exported as a function so that we can keep referencing monaco as just a type rather than loading it directly, 11 | // thus keeping it as a dev dependency for those who do not want to use it 12 | export function getLanguageConfiguration(monaco: typeof MonacoEditor): MonacoEditor.languages.LanguageConfiguration { 13 | return { 14 | comments: { 15 | lineComment: "#", 16 | }, 17 | brackets: [ 18 | ["[", "]"], 19 | ["(", ")"], 20 | ["{", "}"], 21 | ], 22 | autoClosingPairs: [ 23 | { open: "[", close: "]" }, 24 | { open: "(", close: ")" }, 25 | { open: "{", close: "}" }, 26 | ], 27 | surroundingPairs: [ 28 | { open: "[", close: "]" }, 29 | { open: "(", close: ")" }, 30 | ], 31 | onEnterRules: [ 32 | { 33 | beforeText: new RegExp("^\\s*(?:type|relations|model|define).*?:\\s*$"), 34 | action: { indentAction: monaco.languages.IndentAction.Indent }, 35 | }, 36 | ], 37 | folding: { 38 | offSide: true, 39 | markers: { 40 | start: new RegExp("^\\s*#region\\b"), 41 | end: new RegExp("^\\s*#endregion\\b"), 42 | }, 43 | }, 44 | }; 45 | } 46 | 47 | export const language = { 48 | defaultToken: "", 49 | tokenPostfix: `.${LANGUAGE_NAME}`, 50 | 51 | keywords: [], 52 | operators: [], 53 | 54 | identifiers: new RegExp(/(?!self)(?:\w|-[a-zA-Z])*/), 55 | 56 | brackets: [ 57 | { open: "[", close: "]", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_TYPE_RESTRICTIONS }, 58 | { open: "(", close: ")", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_RELATION_DEFINITION }, 59 | { open: "{", close: "}", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_CONDITION_EXPRESSION }, 60 | ], 61 | 62 | tokenizer: { 63 | root: [ 64 | { include: "@comment" }, 65 | { include: "@whitespace" }, 66 | 67 | [new RegExp(/(\[)/), "@brackets", "@restrictions"], 68 | 69 | [new RegExp(/(\{)/), "@brackets", "@cel_symbols"], 70 | 71 | [new RegExp(/[{}[\]()]/), "@brackets"], 72 | 73 | [ 74 | new RegExp(/(schema)(\s+)(\d\.\d)/), 75 | [OpenFgaDslThemeToken.KEYWORD_SCHEMA, "@whitespace", OpenFgaDslThemeToken.VALUE_SCHEMA], 76 | ], 77 | [ 78 | new RegExp(/(module)(\s+)(@identifiers)/), 79 | [OpenFgaDslThemeToken.KEYWORD_MODULE, "@whitespace", OpenFgaDslThemeToken.VALUE_MODULE], 80 | ], 81 | [ 82 | new RegExp(/(extend)(\s+)(type)(\s+)(@identifiers)/), 83 | [ 84 | OpenFgaDslThemeToken.KEYWORD_EXTEND, 85 | "@whitespace", 86 | OpenFgaDslThemeToken.KEYWORD_TYPE, 87 | "@whitespace", 88 | OpenFgaDslThemeToken.VALUE_TYPE_NAME, 89 | ], 90 | ], 91 | [ 92 | new RegExp(/(type)(\s+)(@identifiers)/), 93 | [OpenFgaDslThemeToken.KEYWORD_TYPE, "@whitespace", OpenFgaDslThemeToken.VALUE_TYPE_NAME], 94 | ], 95 | [ 96 | new RegExp(/(define)(\s+)(@identifiers)/), 97 | [OpenFgaDslThemeToken.KEYWORD_DEFINE, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_NAME], 98 | ], 99 | [ 100 | new RegExp(/(or)(\s+)(@identifiers)/), 101 | [OpenFgaDslThemeToken.OPERATOR_OR, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], 102 | ], 103 | [ 104 | new RegExp(/(and)(\s+)(@identifiers)/), 105 | [OpenFgaDslThemeToken.OPERATOR_AND, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], 106 | ], 107 | [ 108 | new RegExp(/(but not)(\s+)(@identifiers)/), 109 | [OpenFgaDslThemeToken.OPERATOR_BUT_NOT, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], 110 | ], 111 | [ 112 | new RegExp(/(as)(\s+)(@identifiers)/), 113 | [OpenFgaDslThemeToken.KEYWORD_AS, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], 114 | ], 115 | [ 116 | new RegExp(/(:)(\s+)(@identifiers)/), 117 | [OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], 118 | ], 119 | [ 120 | new RegExp(/(@identifiers)(\s+)(from)(\s+)(@identifiers)/), 121 | [ 122 | OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_COMPUTED, 123 | "@whitespace", 124 | OpenFgaDslThemeToken.KEYWORD_FROM, 125 | "@whitespace", 126 | OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_TUPLESET, 127 | ], 128 | ], 129 | [ 130 | new RegExp(/(@identifiers)(\s*)(:)(\s*)(@identifiers)(<@identifiers>)(,?)(\s*)/), 131 | [ 132 | OpenFgaDslThemeToken.CONDITION_PARAM, 133 | "@whitespace", 134 | OpenFgaDslThemeToken.DELIMITER_COLON_CONDITION_PARAM, 135 | "@whitespace", 136 | OpenFgaDslThemeToken.CONDITION_PARAM_TYPE, 137 | OpenFgaDslThemeToken.CONDITION_PARAM_TYPE, 138 | OpenFgaDslThemeToken.DELIMITER_COMMA_CONDITION_PARAM, 139 | "@whitespace", 140 | ], 141 | ], 142 | [ 143 | new RegExp(/(@identifiers)(\s*)(:)(\s*)(@identifiers)(,?)(\s*)/), 144 | [ 145 | OpenFgaDslThemeToken.CONDITION_PARAM, 146 | "@whitespace", 147 | OpenFgaDslThemeToken.DELIMITER_COLON_CONDITION_PARAM, 148 | "@whitespace", 149 | OpenFgaDslThemeToken.CONDITION_PARAM_TYPE, 150 | OpenFgaDslThemeToken.DELIMITER_COMMA_CONDITION_PARAM, 151 | "@whitespace", 152 | ], 153 | ], 154 | [ 155 | new RegExp(/(condition)(\s*)(@identifiers)(\s*)(\()/), 156 | [ 157 | OpenFgaDslThemeToken.KEYWORD_CONDITION, 158 | "@whitespace", 159 | OpenFgaDslThemeToken.VALUE_CONDITION, 160 | "@whitespace", 161 | "@brackets", 162 | ], 163 | ], 164 | 165 | [":", OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON], 166 | [",", OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS], 167 | [Keyword.BUT_NOT, OpenFgaDslThemeToken.OPERATOR_BUT_NOT], 168 | [Keyword.SELF, OpenFgaDslThemeToken.KEYWORD_SELF], 169 | [ 170 | new RegExp(/@identifiers/), 171 | { 172 | cases: { 173 | [Keyword.AND]: OpenFgaDslThemeToken.OPERATOR_AND, 174 | [Keyword.OR]: OpenFgaDslThemeToken.OPERATOR_OR, 175 | [Keyword.TYPE]: OpenFgaDslThemeToken.KEYWORD_TYPE, 176 | [Keyword.RELATIONS]: OpenFgaDslThemeToken.KEYWORD_RELATIONS, 177 | [Keyword.DEFINE]: OpenFgaDslThemeToken.KEYWORD_DEFINE, 178 | [Keyword.FROM]: OpenFgaDslThemeToken.KEYWORD_FROM, 179 | [Keyword.WITH]: OpenFgaDslThemeToken.KEYWORD_WITH, 180 | [Keyword.CONDITION]: OpenFgaDslThemeToken.KEYWORD_CONDITION, 181 | [Keyword.AS]: OpenFgaDslThemeToken.KEYWORD_AS, 182 | [Keyword.MODEL]: OpenFgaDslThemeToken.KEYWORD_MODEL, 183 | [Keyword.SCHEMA]: { token: OpenFgaDslThemeToken.KEYWORD_SCHEMA }, 184 | // TODO: This should be "identifier", however because tupleset was not properly 185 | // detected with the rules above, this is the quickiest hacky fix we can do to 186 | // get it out there for people to use 187 | "@default": OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_TUPLESET, 188 | }, 189 | }, 190 | ], 191 | ], 192 | 193 | cel_symbols: [ 194 | [/"/, "string", "@string_double"], 195 | [/'/, "string", "@string_single"], 196 | [/(\w|-[a-zA-Z])+/, OpenFgaDslThemeToken.CONDITION_SYMBOL], 197 | [/(<|<=|>=|>|=|!=|&&|\|\||\.|null|in)/, OpenFgaDslThemeToken.CONDITION_SYMBOL], 198 | [/\[|\]|\(|\)/, "@brackets"], 199 | [/\}/, "@brackets", "@pop"], 200 | ], 201 | 202 | string_double: [ 203 | [/[^\\"]+/, "string"], 204 | [/\\./, "string.escape.invalid"], 205 | [/"/, "string", "@pop"], 206 | ], 207 | 208 | string_single: [ 209 | [/[^\\']+/, "string"], 210 | [/\\./, "string.escape.invalid"], 211 | [/'/, "string", "@pop"], 212 | ], 213 | 214 | restrictions: [ 215 | { include: "@whitespace" }, 216 | [new RegExp(`${Keyword.WITH}`), OpenFgaDslThemeToken.KEYWORD_WITH], 217 | [/(\w|-[a-zA-Z])+/, OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_TYPE], 218 | [ 219 | /(:)(\*)/, 220 | [OpenFgaDslThemeToken.DELIMITER_COLON_TYPE_RESTRICTIONS, OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_WILDCARD], 221 | ], 222 | [/#/, OpenFgaDslThemeToken.DELIMITER_HASHTAG_TYPE_RESTRICTIONS], 223 | [/,/, OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS], 224 | [/\]/, "@brackets", "@pop"], 225 | ], 226 | 227 | // Deal with white space, including comments 228 | whitespace: [[new RegExp(/\s+/), "white"]], 229 | comment: [ 230 | [new RegExp(/\s+(#.*)/), OpenFgaDslThemeToken.COMMENT], 231 | [new RegExp(/^\s*(#.*)/), OpenFgaDslThemeToken.COMMENT], 232 | ], 233 | }, 234 | }; 235 | -------------------------------------------------------------------------------- /src/tools/monaco/providers/code-actions.ts: -------------------------------------------------------------------------------- 1 | import { errors } from "@openfga/syntax-transformer"; 2 | import type { editor, languages } from "monaco-editor"; 3 | 4 | import { Keyword } from "../../../constants/keyword"; 5 | import { SchemaVersion } from "../../../constants/schema-version"; 6 | import { SINGLE_INDENTATION } from "../../../constants/single-indentation"; 7 | import { Marker, MonacoEditor } from "../typings"; 8 | 9 | type ValidationError = errors.ValidationError; 10 | interface CodeActionInput { 11 | markerRange: MonacoEditor.Range; 12 | model: editor.ITextModel; 13 | marker: editor.IMarkerData & Marker; 14 | text: string; 15 | schemaVersion: SchemaVersion; 16 | } 17 | 18 | interface CodeActionResult { 19 | title: string; 20 | text: string; 21 | startColumn?: number; 22 | startLineNumber?: number; 23 | } 24 | 25 | const errorFixesByErrorCodeAndSchema: Partial< 26 | Record< 27 | ValidationError, 28 | Record< 29 | SchemaVersion, 30 | (markerData: { relation?: string } & Omit) => CodeActionResult 31 | > 32 | > 33 | > = { 34 | [errors.ValidationError.MissingDefinition]: { 35 | [SchemaVersion.OneDotZero]: ({ model, marker, relation }) => { 36 | const lineContent = model.getLineContent(marker.startLineNumber); 37 | return { 38 | startColumn: 0, 39 | title: `Fix: add definition for \`${relation}\`.`, 40 | text: `${SINGLE_INDENTATION}${SINGLE_INDENTATION}${Keyword.DEFINE} ${relation} ${Keyword.AS} ${Keyword.SELF}\n${lineContent}`, 41 | }; 42 | }, 43 | [SchemaVersion.OneDotOne]: ({ model, marker, relation }) => { 44 | const lineContent = model.getLineContent(marker.startLineNumber); 45 | return { 46 | startColumn: 0, 47 | title: `Fix: add definition for \`${relation}\`.`, 48 | text: `${SINGLE_INDENTATION}${SINGLE_INDENTATION}${Keyword.DEFINE} ${relation}: [typeName]\n${lineContent}`, 49 | }; 50 | }, 51 | [SchemaVersion.OneDotTwo]: ({ model, marker, relation }) => { 52 | const lineContent = model.getLineContent(marker.startLineNumber); 53 | return { 54 | startColumn: 0, 55 | title: `Fix: add definition for \`${relation}\`.`, 56 | text: `${SINGLE_INDENTATION}${SINGLE_INDENTATION}${Keyword.DEFINE} ${relation}: [typeName]\n${lineContent}`, 57 | }; 58 | }, 59 | }, 60 | [errors.ValidationError.SelfError]: { 61 | [SchemaVersion.OneDotZero]: ({ text }) => ({ 62 | title: `Fix: replace \`${text}\` by \`self\`.`, 63 | text: Keyword.SELF, 64 | }), 65 | [SchemaVersion.OneDotOne]: ({ text }) => ({ 66 | title: `Fix: replace \`${text}\` with type restrictions.`, 67 | text: "[typeName]", 68 | }), 69 | [SchemaVersion.OneDotTwo]: ({ text }) => ({ 70 | title: `Fix: replace \`${text}\` with type restrictions.`, 71 | text: "[typeName]", 72 | }), 73 | }, 74 | [errors.ValidationError.DuplicatedError]: { 75 | [SchemaVersion.OneDotZero]: ({ model, marker, markerRange, text }) => ({ 76 | startLineNumber: markerRange.startLineNumber - 1, 77 | startColumn: model.getLineContent(marker.startLineNumber - 1).length + 1, 78 | title: `Fix: remove duplicated \`${text}\`.`, 79 | text: "", 80 | }), 81 | [SchemaVersion.OneDotOne]: ({ model, marker, markerRange, text }) => ({ 82 | startLineNumber: markerRange.startLineNumber - 1, 83 | startColumn: model.getLineContent(marker.startLineNumber - 1).length + 1, 84 | title: `Fix: remove duplicated \`${text}\`.`, 85 | text: "", 86 | }), 87 | [SchemaVersion.OneDotTwo]: ({ model, marker, markerRange, text }) => ({ 88 | startLineNumber: markerRange.startLineNumber - 1, 89 | startColumn: model.getLineContent(marker.startLineNumber - 1).length + 1, 90 | title: `Fix: remove duplicated \`${text}\`.`, 91 | text: "", 92 | }), 93 | }, 94 | }; 95 | 96 | function getCodeActionForError({ markerRange, model, marker, text, schemaVersion }: CodeActionInput) { 97 | const { error, relation } = marker.extraInformation || {}; 98 | const fixContent = errorFixesByErrorCodeAndSchema[error as ValidationError]?.[schemaVersion]?.({ 99 | model, 100 | marker, 101 | markerRange, 102 | text, 103 | relation, 104 | }); 105 | 106 | if (!fixContent) { 107 | return; 108 | } 109 | return { 110 | title: fixContent?.title, 111 | diagnostics: [marker], 112 | edit: { 113 | edits: [ 114 | { 115 | textEdit: { 116 | range: markerRange, 117 | text: fixContent.text, 118 | }, 119 | resource: model.uri, 120 | versionId: undefined, 121 | }, 122 | ], 123 | }, 124 | kind: "quickfix", 125 | }; 126 | } 127 | 128 | export const provideCodeActions = 129 | (monaco: typeof MonacoEditor, schemaVersion: SchemaVersion) => 130 | ( 131 | model: editor.ITextModel, 132 | range: MonacoEditor.Range, 133 | context: languages.CodeActionContext & { markers: Marker[] }, 134 | ): languages.ProviderResult => { 135 | const codeActions: languages.CodeAction[] = []; 136 | 137 | context.markers 138 | .map((marker: Marker) => { 139 | const startOffset = model.getOffsetAt({ column: marker.startColumn, lineNumber: marker.startLineNumber }); 140 | const endOffset = model.getOffsetAt({ column: marker.endColumn, lineNumber: marker.endLineNumber }); 141 | const text = model.getValue().substr(startOffset, endOffset - startOffset); 142 | 143 | const markerRange = new monaco.Range( 144 | marker.startLineNumber, 145 | marker.startColumn, 146 | marker.endLineNumber, 147 | marker.endColumn, 148 | ); 149 | 150 | const action = getCodeActionForError({ markerRange, model, marker, text, schemaVersion }); 151 | if (action) { 152 | codeActions.push(action); 153 | } 154 | }) 155 | .filter((action) => action); 156 | 157 | return { 158 | actions: codeActions, 159 | // eslint-disable-next-line @typescript-eslint/no-empty-function 160 | dispose() {}, 161 | }; 162 | }; 163 | -------------------------------------------------------------------------------- /src/tools/monaco/providers/completion.ts: -------------------------------------------------------------------------------- 1 | import type { AuthorizationModel as ApiAuthorizationModel } from "@openfga/sdk"; 2 | import { transformer } from "@openfga/syntax-transformer"; 3 | import type { editor, languages, Position } from "monaco-editor"; 4 | 5 | import { Keyword } from "../../../constants/keyword"; 6 | import { SINGLE_INDENTATION } from "../../../constants/single-indentation"; 7 | import { assertNever } from "../../../inner-utils/assert-never"; 8 | import { SchemaVersion } from "../../../constants/schema-version"; 9 | import { MonacoEditor } from "../typings"; 10 | 11 | type AuthorizationModel = Required>; 12 | 13 | export type CompletionExtraOptions = { 14 | samples?: Record & { 15 | entitlements?: AuthorizationModel; 16 | expenses?: AuthorizationModel; 17 | gdrive?: AuthorizationModel; 18 | generic?: AuthorizationModel; 19 | github?: AuthorizationModel; 20 | iot?: AuthorizationModel; 21 | slack?: AuthorizationModel; 22 | customRoles?: AuthorizationModel; 23 | }; 24 | }; 25 | 26 | function getSuggestions( 27 | monaco: typeof MonacoEditor, 28 | range: MonacoEditor.IRange | languages.CompletionItemRanges, 29 | schemaVersion: SchemaVersion, 30 | samples: CompletionExtraOptions["samples"] = {}, 31 | ) { 32 | const suggestions: languages.CompletionItem[] = []; 33 | ["entitlements", "expenses", "gdrive", "generic", "github", "iot", "slack", "customRoles"].forEach((key) => { 34 | const sampleModel = samples?.[key]; 35 | if (sampleModel) { 36 | suggestions.push({ 37 | label: `sample-${key}`, 38 | kind: monaco.languages.CompletionItemKind.Struct, 39 | insertText: transformer.transformJSONToDSL( 40 | schemaVersion === SchemaVersion.OneDotOne || schemaVersion === SchemaVersion.OneDotTwo 41 | ? sampleModel 42 | : { 43 | schema_version: SchemaVersion.OneDotZero, 44 | type_definitions: sampleModel.type_definitions.map((typeDef) => ({ 45 | type: typeDef.type, 46 | relations: typeDef.relations, 47 | })), 48 | }, 49 | ), 50 | range, 51 | }); 52 | } 53 | }); 54 | return suggestions; 55 | } 56 | 57 | const provideCompletionItemsOneDotOne = 58 | (monaco: typeof MonacoEditor, completionExtraOptions: CompletionExtraOptions = {}) => 59 | (model: editor.ITextModel, position: Position): languages.CompletionList => { 60 | let suggestions: languages.CompletionItem[] = []; 61 | const word = model.getWordUntilPosition(position); 62 | const range = { 63 | startLineNumber: position.lineNumber, 64 | endLineNumber: position.lineNumber, 65 | startColumn: word.startColumn, 66 | endColumn: word.endColumn, 67 | }; 68 | 69 | if (position.column === 2) { 70 | suggestions = [ 71 | { 72 | label: Keyword.TYPE, 73 | kind: monaco.languages.CompletionItemKind.Function, 74 | // eslint-disable-next-line no-template-curly-in-string 75 | insertText: `${Keyword.TYPE} \${1:typeName} 76 | ${SINGLE_INDENTATION}${Keyword.RELATIONS} 77 | ${SINGLE_INDENTATION}${SINGLE_INDENTATION}${Keyword.DEFINE} \${2:relationName}: [\${3:typeName}]`, 78 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 79 | range, 80 | }, 81 | { 82 | label: "type_group", 83 | kind: monaco.languages.CompletionItemKind.Function, 84 | // eslint-disable-next-line no-template-curly-in-string 85 | insertText: `${Keyword.TYPE} \${1:group} 86 | ${SINGLE_INDENTATION}${Keyword.RELATIONS} 87 | ${SINGLE_INDENTATION}${SINGLE_INDENTATION}${Keyword.DEFINE} \${2:member}: [\${3:user, group#member}]`, 88 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 89 | range, 90 | }, 91 | { 92 | label: Keyword.TYPE, 93 | kind: monaco.languages.CompletionItemKind.Keyword, 94 | insertText: Keyword.TYPE, 95 | range, 96 | }, 97 | { 98 | label: Keyword.MODEL, 99 | kind: monaco.languages.CompletionItemKind.Function, 100 | // eslint-disable-next-line no-template-curly-in-string 101 | insertText: `${Keyword.MODEL} 102 | ${SINGLE_INDENTATION}${Keyword.SCHEMA} \${1:1.1}`, 103 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 104 | range, 105 | }, 106 | { 107 | label: Keyword.MODEL, 108 | kind: monaco.languages.CompletionItemKind.Keyword, 109 | insertText: Keyword.TYPE, 110 | range, 111 | }, 112 | { 113 | label: Keyword.CONDITION, 114 | kind: monaco.languages.CompletionItemKind.Function, 115 | // eslint-disable-next-line no-template-curly-in-string 116 | insertText: `${Keyword.CONDITION} \${1:conditionName}(\${2:parameterName}: \${3:string}) { 117 | \${4} 118 | }`, 119 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 120 | range, 121 | }, 122 | ]; 123 | } else if (position.column === 4) { 124 | suggestions = [ 125 | { 126 | label: Keyword.RELATIONS, 127 | kind: monaco.languages.CompletionItemKind.Keyword, 128 | insertText: Keyword.RELATIONS, 129 | range, 130 | }, 131 | ]; 132 | } else if (position.column > 6) { 133 | suggestions = [ 134 | { 135 | label: Keyword.OR, 136 | kind: monaco.languages.CompletionItemKind.Keyword, 137 | insertText: Keyword.OR, 138 | range, 139 | }, 140 | { 141 | label: Keyword.AND, 142 | kind: monaco.languages.CompletionItemKind.Keyword, 143 | insertText: Keyword.AND, 144 | range, 145 | }, 146 | { 147 | label: Keyword.BUT_NOT, 148 | kind: monaco.languages.CompletionItemKind.Keyword, 149 | insertText: Keyword.BUT_NOT, 150 | range, 151 | }, 152 | { 153 | label: Keyword.FROM, 154 | kind: monaco.languages.CompletionItemKind.Function, 155 | // eslint-disable-next-line no-template-curly-in-string 156 | insertText: `\${1:relation1} ${Keyword.FROM} \${1:relation2}`, 157 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 158 | range, 159 | }, 160 | { 161 | label: Keyword.FROM, 162 | kind: monaco.languages.CompletionItemKind.Keyword, 163 | insertText: Keyword.FROM, 164 | range, 165 | }, 166 | { 167 | label: Keyword.CONDITION, 168 | kind: monaco.languages.CompletionItemKind.Keyword, 169 | insertText: Keyword.CONDITION, 170 | range, 171 | }, 172 | ]; 173 | } else if (position.column === 6) { 174 | suggestions = [ 175 | { 176 | label: "define-assignable", 177 | kind: monaco.languages.CompletionItemKind.Function, 178 | // eslint-disable-next-line no-template-curly-in-string 179 | insertText: "define ${1:relationName}: [${2:typeName}]", 180 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 181 | range, 182 | }, 183 | { 184 | label: "define-from-other-relation", 185 | kind: monaco.languages.CompletionItemKind.Function, 186 | // eslint-disable-next-line no-template-curly-in-string 187 | insertText: "define ${1:relationName}: ${2:otherRelationName}", 188 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 189 | range, 190 | }, 191 | { 192 | label: "define-from-other-relation-assignable", 193 | kind: monaco.languages.CompletionItemKind.Function, 194 | // eslint-disable-next-line no-template-curly-in-string 195 | insertText: "define ${1:relationName}: [${2:typeName}] or ${3:otherRelationName}", 196 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 197 | range, 198 | }, 199 | { 200 | label: "define-from-object", 201 | kind: monaco.languages.CompletionItemKind.Function, 202 | // eslint-disable-next-line no-template-curly-in-string 203 | insertText: "define ${1:relationName}: ${2:relationInRelatedObject} from ${3:relationInThisType}}", 204 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, 205 | range, 206 | }, 207 | { 208 | label: "define", 209 | kind: monaco.languages.CompletionItemKind.Keyword, 210 | insertText: "define", 211 | range, 212 | }, 213 | ]; 214 | } else { 215 | suggestions = getSuggestions(monaco, range, SchemaVersion.OneDotOne, completionExtraOptions.samples); 216 | } 217 | 218 | return { 219 | suggestions, 220 | }; 221 | }; 222 | 223 | export const provideCompletionItems = 224 | ( 225 | monaco: typeof MonacoEditor, 226 | schemaVersion = SchemaVersion.OneDotZero, 227 | completionExtraOptions: CompletionExtraOptions = {}, 228 | ) => 229 | (model: editor.ITextModel, position: Position): languages.ProviderResult => { 230 | switch (schemaVersion) { 231 | case SchemaVersion.OneDotTwo: 232 | case SchemaVersion.OneDotOne: 233 | return provideCompletionItemsOneDotOne(monaco, completionExtraOptions)(model, position); 234 | case SchemaVersion.OneDotZero: 235 | throw new Error("unsupported schema version"); 236 | default: 237 | assertNever(schemaVersion); 238 | } 239 | }; 240 | -------------------------------------------------------------------------------- /src/tools/monaco/providers/hover-actions.ts: -------------------------------------------------------------------------------- 1 | import { Keyword } from "../../../constants/keyword"; 2 | import { defaultDocumentationMap } from "../../../documentation/concepts"; 3 | import { MonacoEditor } from "../typings"; 4 | import type { editor, languages, Position } from "monaco-editor"; 5 | 6 | export type DocumentationMap = Partial>; 7 | 8 | function getDocumentation(keyword: Keyword, documentationMap: DocumentationMap): { value: string }[] | undefined { 9 | const definition = documentationMap[keyword]; 10 | if (!definition) { 11 | return undefined; 12 | } 13 | 14 | const { link, summary } = definition; 15 | const documentation = [ 16 | { value: "**Documentation**" }, 17 | { 18 | value: summary, 19 | }, 20 | ]; 21 | if (link) { 22 | documentation.push({ value: `[Learn more](${link})` }); 23 | } 24 | return documentation; 25 | } 26 | 27 | export const providerHover = 28 | (monaco: typeof MonacoEditor, documentationMap = defaultDocumentationMap) => 29 | (model: editor.ITextModel, position: Position): languages.Hover | undefined => { 30 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 31 | const wordMeta = model.getWordAtPosition(position)!; 32 | 33 | if (!wordMeta) { 34 | return; 35 | } 36 | 37 | const { startColumn, endColumn, word } = wordMeta; 38 | 39 | const contents = getDocumentation(word as Keyword, documentationMap as any); 40 | if (!contents) { 41 | return; 42 | } 43 | return { 44 | range: new monaco.Range(position.lineNumber, startColumn, position.lineNumber, endColumn), 45 | contents, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /src/tools/monaco/register-dsl.ts: -------------------------------------------------------------------------------- 1 | import { LANGUAGE_NAME } from "../../constants"; 2 | import { SchemaVersion } from "../../constants/schema-version"; 3 | import { getLanguageConfiguration, language } from "./language-definition"; 4 | import { DocumentationMap, providerHover } from "./providers/hover-actions"; 5 | import { provideCompletionItems } from "./providers/completion"; 6 | import { provideCodeActions } from "./providers/code-actions"; 7 | import { MonacoEditor } from "./typings"; 8 | 9 | export interface RegisterDslOverrides { 10 | documentationMap: DocumentationMap; 11 | } 12 | 13 | export const registerDSL = ( 14 | monaco: typeof MonacoEditor, 15 | schemaVersion = SchemaVersion.OneDotZero, 16 | overrides: RegisterDslOverrides, 17 | ) => { 18 | const isLanguageRegistered = !!monaco.languages 19 | .getLanguages() 20 | .find((language: MonacoEditor.languages.ILanguageExtensionPoint) => language.id === LANGUAGE_NAME); 21 | 22 | if (isLanguageRegistered) { 23 | return; 24 | } 25 | 26 | monaco.languages.register({ id: LANGUAGE_NAME }); 27 | 28 | monaco.languages.setLanguageConfiguration(LANGUAGE_NAME, getLanguageConfiguration(monaco)); 29 | 30 | monaco.languages.setMonarchTokensProvider(LANGUAGE_NAME, language); 31 | 32 | monaco.languages.registerHoverProvider(LANGUAGE_NAME, { 33 | provideHover: providerHover(monaco, overrides.documentationMap), 34 | }); 35 | 36 | monaco.languages.registerCompletionItemProvider(LANGUAGE_NAME, { 37 | provideCompletionItems: provideCompletionItems(monaco, schemaVersion), 38 | }); 39 | 40 | monaco.languages.registerCodeActionProvider(LANGUAGE_NAME, { 41 | provideCodeActions: provideCodeActions(monaco, schemaVersion), 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/tools/monaco/theme.ts: -------------------------------------------------------------------------------- 1 | import type { editor } from "monaco-editor"; 2 | 3 | import { LANGUAGE_NAME } from "../../constants"; 4 | import { OpenFgaDslThemeToken, OpenFgaThemeConfiguration, SupportedTheme, supportedThemes } from "../../theme"; 5 | import { getThemeTokenStyle } from "../../theme/utils"; 6 | 7 | function buildMonacoTheme(themeConfig: OpenFgaThemeConfiguration): editor.IStandaloneThemeData { 8 | return { 9 | base: themeConfig.baseTheme || "vs", 10 | inherit: true, 11 | colors: { 12 | "editor.background": themeConfig.background.color, 13 | }, 14 | rules: Object.values(OpenFgaDslThemeToken).map((token) => { 15 | const style = getThemeTokenStyle(token, themeConfig); 16 | return { 17 | token: `${token}.${LANGUAGE_NAME}`, 18 | ...style, 19 | }; 20 | }), 21 | }; 22 | } 23 | 24 | const monacoThemes: Record = {}; 25 | Object.values(SupportedTheme).forEach((themeName) => { 26 | monacoThemes[themeName] = buildMonacoTheme(supportedThemes[themeName]); 27 | }); 28 | 29 | export { monacoThemes, buildMonacoTheme }; 30 | -------------------------------------------------------------------------------- /src/tools/monaco/typings.ts: -------------------------------------------------------------------------------- 1 | export type * as MonacoEditor from "monaco-editor"; 2 | 3 | import type { editor } from "monaco-editor"; 4 | import { errors } from "@openfga/syntax-transformer"; 5 | 6 | type ValidationError = errors.ValidationError; 7 | 8 | export interface Marker extends editor.IMarkerData { 9 | severity: number; 10 | startColumn: number; 11 | endColumn: number; 12 | startLineNumber: number; 13 | endLineNumber: number; 14 | message: string; 15 | source: string; 16 | extraInformation?: { error?: ValidationError; typeName?: string; relation?: string }; 17 | } 18 | -------------------------------------------------------------------------------- /src/tools/monaco/validate.ts: -------------------------------------------------------------------------------- 1 | import type * as MonacoEditor from "monaco-editor"; 2 | 3 | import { Marker } from "./typings"; 4 | import { DSLSyntaxSingleError, ModelValidationSingleError } from "@openfga/syntax-transformer/dist/errors"; 5 | import { errors, validator } from "@openfga/syntax-transformer"; 6 | export function validateDSL(monaco: typeof MonacoEditor, dsl: string): Marker[] { 7 | const markers: Marker[] = []; 8 | try { 9 | validator.validateDSL(dsl); 10 | } catch (err) { 11 | if (!(err as errors.BaseMultiError).errors) { 12 | return markers; 13 | } 14 | 15 | for (const singleErr of (err as errors.BaseMultiError).errors) { 16 | let source; 17 | if (singleErr instanceof DSLSyntaxSingleError) { 18 | source = "SyntaxError"; 19 | } else if (singleErr instanceof ModelValidationSingleError) { 20 | source = "ModelValidationError"; 21 | } else { 22 | throw new Error("Unhandled Exception: " + JSON.stringify(singleErr, null, 4)); 23 | } 24 | 25 | const extraInformation: Marker["extraInformation"] = {}; 26 | const errorMetadata = singleErr.metadata; 27 | if (errorMetadata) { 28 | if ("errorType" in errorMetadata) { 29 | extraInformation.error = errorMetadata.errorType; 30 | } 31 | ["typeName", "relation"].forEach((field) => { 32 | if (field in errorMetadata) { 33 | (extraInformation as any)[field] = (errorMetadata as any)[field]; 34 | } 35 | }); 36 | } 37 | 38 | // monaco starts range of line & col from position 1 39 | // https://microsoft.github.io/monaco-editor/typedoc/classes/Selection.html#startLineNumber 40 | markers.push({ 41 | message: singleErr.msg, 42 | severity: monaco.MarkerSeverity.Error, 43 | startColumn: (singleErr?.column?.start || 0) + 1, 44 | endColumn: (singleErr?.column?.end || 0) + 1, 45 | startLineNumber: (singleErr?.line?.start || 0) + 1, 46 | endLineNumber: (singleErr?.line?.end || 0) + 1, 47 | source, 48 | extraInformation, 49 | }); 50 | } 51 | } 52 | 53 | return markers; 54 | } 55 | -------------------------------------------------------------------------------- /src/tools/prism/index.ts: -------------------------------------------------------------------------------- 1 | import { languageDefinition } from "./language-definition"; 2 | import { LANGUAGE_NAME } from "../../constants"; 3 | 4 | export const PrismExtensions = { 5 | LANGUAGE_NAME, 6 | languageDefinition, 7 | }; 8 | -------------------------------------------------------------------------------- /src/tools/prism/language-definition.ts: -------------------------------------------------------------------------------- 1 | import { OpenFgaDslThemeTokenType } from "../../theme/theme.typings"; 2 | 3 | export const languageDefinition = { 4 | [OpenFgaDslThemeTokenType.MODULE]: { 5 | pattern: /(module\s+)[\w_-]+/i, 6 | lookbehind: true, 7 | }, 8 | [OpenFgaDslThemeTokenType.TYPE]: { 9 | pattern: /(\btype\s+)[\w_-]+/i, 10 | lookbehind: true, 11 | }, 12 | [OpenFgaDslThemeTokenType.EXTEND]: { 13 | pattern: /(\bextend type\s+)[\w_-]+/i, 14 | lookbehind: true, 15 | }, 16 | [OpenFgaDslThemeTokenType.RELATION]: { 17 | pattern: /(\bdefine\s+)[\w_-]+/i, 18 | lookbehind: true, 19 | }, 20 | [OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE]: /\[.*]|self/, 21 | [OpenFgaDslThemeTokenType.CONDITION]: { 22 | pattern: /(\bcondition\s+)[\w_-]+/i, 23 | lookbehind: true, 24 | }, 25 | "condition-params": { 26 | pattern: /\(.*\)\s*{/, 27 | inside: { 28 | "condition-param": /\b([\w_-]+)\s*:/i, 29 | "condition-param-type": /\b(string|int|map|uint|list|timestamp|bool|duration|double|ipaddress)\b/, 30 | }, 31 | }, 32 | [OpenFgaDslThemeTokenType.COMMENT]: { 33 | pattern: /(^\s*|\s+)#.*/, 34 | }, 35 | [OpenFgaDslThemeTokenType.KEYWORD]: { 36 | pattern: /\b(type|relations|define|and|or|but not|from|as|model|schema|condition|module|extend)\b/, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/utilities/graphs/graph.typings.ts: -------------------------------------------------------------------------------- 1 | export enum GraphNodeGroup { 2 | AssignableRelation = "assignable-relation", 3 | NonassignableRelation = "nonassignable-relation", 4 | Type = "type", 5 | StoreName = "store-name", 6 | Default = "default", 7 | Check = "check", 8 | } 9 | 10 | export enum GraphEdgeGroup { 11 | StoreToType = "store-to-type", 12 | TypeToRelation = "type-to-relation", 13 | RelationToRelation = "relation-to-relation", 14 | AssignableSourceToRelation = "assignable-sourcee-to-relation", 15 | Default = "default", 16 | } 17 | 18 | export interface GraphNode { 19 | id: string; 20 | label: string; 21 | type?: string; 22 | group?: GraphNodeGroup; 23 | isActive?: boolean; 24 | } 25 | 26 | export interface GraphEdge { 27 | to: string; 28 | from: string; 29 | label?: string; 30 | group: GraphEdgeGroup; 31 | dashes?: boolean; 32 | isActive?: boolean; 33 | } 34 | 35 | export interface GraphDefinition { 36 | nodes: GraphNode[]; 37 | edges: GraphEdge[]; 38 | } 39 | 40 | export type ResolutionTree = Record< 41 | string, 42 | { 43 | inActivePath?: boolean; 44 | parents: Record; 45 | } 46 | >; 47 | 48 | export enum RelationType { 49 | DirectUsers = "Direct Users", 50 | ComputedUserset = "Related From Same Object", 51 | TupleToUserset = "Related From Related Objects", 52 | } 53 | -------------------------------------------------------------------------------- /src/utilities/graphs/index.ts: -------------------------------------------------------------------------------- 1 | export { GraphDefinition, GraphEdge, GraphNode, GraphNodeGroup, GraphEdgeGroup, ResolutionTree } from "./graph.typings"; 2 | export { TreeBuilder } from "./related-users-graph"; 3 | export { AuthorizationModelGraphBuilder } from "./model-graph"; 4 | -------------------------------------------------------------------------------- /src/utilities/graphs/model-graph.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationModel, ObjectRelation, RelationMetadata, TypeDefinition, Userset } from "@openfga/sdk"; 2 | import { GraphDefinition, GraphEdgeGroup, GraphNodeGroup } from "./graph.typings"; 3 | 4 | export type TypeGraphOpts = { showAssignable?: boolean }; 5 | 6 | export class AuthorizationModelGraphBuilder { 7 | private _graph: GraphDefinition = { nodes: [], edges: [] }; 8 | 9 | constructor( 10 | private authorizationModel: AuthorizationModel, 11 | private store?: { name?: string; id?: string }, 12 | ) { 13 | this.buildGraph(); 14 | } 15 | 16 | private static getStoreId(storeName: string) { 17 | return `store|${storeName}`; 18 | } 19 | 20 | private static getTypeId(typeId: string) { 21 | return `type|${typeId}`; 22 | } 23 | 24 | private static getRelationId(typeId: string, relationKey: string) { 25 | return `${typeId}.relation|${relationKey}`; 26 | } 27 | 28 | private buildGraph() { 29 | const storeName = this.store?.name || this.store?.id || "Store"; 30 | const rootId = AuthorizationModelGraphBuilder.getStoreId(storeName); 31 | const authorizationModelGraph: GraphDefinition = { 32 | nodes: [{ id: rootId, label: storeName, group: GraphNodeGroup.StoreName }], 33 | edges: [], 34 | }; 35 | 36 | this.authorizationModel.type_definitions?.forEach((typeDef) => { 37 | const graph = this.getTypeGraph(typeDef, authorizationModelGraph); 38 | authorizationModelGraph.nodes = authorizationModelGraph.nodes.concat(graph.nodes); 39 | authorizationModelGraph.edges = authorizationModelGraph.edges.concat(graph.edges); 40 | }); 41 | 42 | this._graph = authorizationModelGraph; 43 | } 44 | 45 | // A relation definition has self if `this` is in the relation definition 46 | private checkIfRelationAssignable(relationDef: Userset): boolean { 47 | return !!( 48 | relationDef.this || 49 | relationDef.difference?.base.this || 50 | relationDef.difference?.subtract.this || 51 | (relationDef.intersection?.child || []).some((child) => this.checkIfRelationAssignable(child)) || 52 | (relationDef.union?.child || []).some((child) => this.checkIfRelationAssignable(child)) 53 | ); 54 | } 55 | 56 | // Get the sources that can be assignable to a relation 57 | private getAssignableSourcesForRelation( 58 | relationDef: Userset, 59 | relationMetadata: RelationMetadata, 60 | ): { 61 | types: string[]; 62 | relations: string[]; 63 | conditions: string[]; 64 | publicTypes: string[]; 65 | isAssignable: boolean; 66 | } { 67 | const assignableSources: { 68 | types: string[]; 69 | relations: string[]; 70 | conditions: string[]; 71 | publicTypes: string[]; 72 | isAssignable: boolean; 73 | } = { types: [], relations: [], conditions: [], publicTypes: [], isAssignable: false }; 74 | 75 | // If this is not used anywhere, then it's not assignable 76 | if (!this.checkIfRelationAssignable(relationDef)) { 77 | return assignableSources; 78 | } 79 | 80 | const assignable = relationMetadata.directly_related_user_types; 81 | assignable?.forEach((relationRef) => { 82 | // TODO: wildcard and conditions 83 | if (!(relationRef.relation || relationRef.wildcard || (relationRef as any).condition)) { 84 | return; 85 | } 86 | 87 | // TODO: Mark relations as assignable once supported 88 | if (relationRef.relation) { 89 | assignableSources.relations.push( 90 | AuthorizationModelGraphBuilder.getRelationId(relationRef.type, relationRef.relation), 91 | ); 92 | return; 93 | } 94 | 95 | assignableSources.isAssignable = true; 96 | assignableSources.types.push(AuthorizationModelGraphBuilder.getTypeId(relationRef.type)); 97 | }); 98 | 99 | return assignableSources; 100 | } 101 | 102 | private addRelationToRelationEdge( 103 | typeGraph: GraphDefinition, 104 | typeId: string, 105 | fromRelationKey: string, 106 | toRelation: ObjectRelation, 107 | ): void { 108 | typeGraph.edges.push({ 109 | from: AuthorizationModelGraphBuilder.getRelationId(typeId, fromRelationKey), 110 | to: AuthorizationModelGraphBuilder.getRelationId(typeId, toRelation.relation!), 111 | group: GraphEdgeGroup.RelationToRelation, 112 | dashes: true, 113 | }); 114 | } 115 | 116 | private getTypeGraph( 117 | typeDef: TypeDefinition, 118 | authorizationModelGraph: GraphDefinition, 119 | { showAssignable }: TypeGraphOpts = {}, 120 | ): GraphDefinition { 121 | const typeId = AuthorizationModelGraphBuilder.getTypeId(typeDef.type); 122 | const typeGraph: GraphDefinition = { 123 | nodes: [{ id: typeId, label: typeDef.type, group: GraphNodeGroup.Type }], 124 | edges: [{ from: authorizationModelGraph.nodes[0].id, to: typeId, group: GraphEdgeGroup.StoreToType }], 125 | }; 126 | 127 | const relationDefs = typeDef?.relations || {}; 128 | 129 | Object.keys(relationDefs).forEach((relationKey: string) => { 130 | const relationId = AuthorizationModelGraphBuilder.getRelationId(typeId, relationKey); 131 | 132 | const relationDef = relationDefs[relationKey] || {}; 133 | const assignableSources = this.getAssignableSourcesForRelation( 134 | relationDef, 135 | typeDef.metadata?.relations?.[relationKey] || {}, 136 | ); 137 | const isAssignable = assignableSources.isAssignable; 138 | 139 | // If a relation definition does not have this, then we call it a `permission`, e.g. not directly assignable 140 | typeGraph.nodes.push({ 141 | id: relationId, 142 | label: relationKey, 143 | group: isAssignable ? GraphNodeGroup.AssignableRelation : GraphNodeGroup.NonassignableRelation, 144 | }); 145 | 146 | if (showAssignable) { 147 | // TODO: Support assignable relations and wildcards, and conditionals 148 | assignableSources.types.forEach((assignableSource) => { 149 | typeGraph.edges.push({ 150 | from: AuthorizationModelGraphBuilder.getTypeId(assignableSource), 151 | to: relationId, 152 | group: GraphEdgeGroup.AssignableSourceToRelation, 153 | }); 154 | }); 155 | } 156 | 157 | // TODO: Support - 1. AND, 2. BUT NOT, 3. Nested relations, 4. Tuple to Userset 158 | typeGraph.edges.push({ from: typeId, to: relationId, group: GraphEdgeGroup.TypeToRelation }); 159 | if (relationDef.computedUserset) { 160 | this.addRelationToRelationEdge(typeGraph, typeId, relationKey, relationDef.computedUserset); 161 | } else { 162 | (relationDef.union?.child || []).forEach((child) => { 163 | if (child.computedUserset) { 164 | this.addRelationToRelationEdge(typeGraph, typeId, relationKey, child.computedUserset); 165 | } 166 | }); 167 | } 168 | }); 169 | 170 | return typeGraph; 171 | } 172 | 173 | get graph(): GraphDefinition { 174 | return this._graph; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/utilities/graphs/related-users-graph.ts: -------------------------------------------------------------------------------- 1 | import type { ExpandResponse, Node, OpenFgaApi, TupleKey } from "@openfga/sdk"; 2 | import { GraphDefinition, GraphEdgeGroup, RelationType, ResolutionTree } from "./graph.typings"; 3 | 4 | /** 5 | * Taking a string representation of a user (object or userset), returns the proper object fields 6 | * 7 | * @param {string} userString - a string in the form of: `:`, `:#`, `:*` 8 | * 9 | * @typedef {Object} UserComponents 10 | * @property {string} object - a string in the form of: `:` 11 | * @property {string} relation - the relation part of a userset 12 | * @property {boolean} isWildcard 13 | * 14 | * @return {UserComponents} - the object and relation components of a user 15 | */ 16 | const getUserComponents = (userString: string): { isWildcard: boolean; object: string; relation?: string } => { 17 | const objectRelation = userString.split("#"); 18 | const isWildcard = objectRelation[1] === "*" || userString === "*"; 19 | 20 | return { 21 | object: objectRelation[0], 22 | relation: isWildcard ? undefined : objectRelation[1], 23 | isWildcard, 24 | }; 25 | }; 26 | 27 | export class TreeBuilder { 28 | private currentTree?: ResolutionTree; 29 | private readonly expandedTuples: Record = {}; 30 | 31 | constructor( 32 | private readonly openFgaApi: Pick, 33 | private readonly capturedTuple: Required>, 34 | private readonly storeId: string, 35 | private readonly existingTree?: ResolutionTree, 36 | private readonly authorizationModelId?: string, 37 | ) { 38 | if (existingTree) { 39 | this.currentTree = existingTree; 40 | } 41 | } 42 | 43 | private addParent(user: string, parent: string, type?: RelationType, inActivePath?: boolean): void { 44 | if (!this.currentTree) return; 45 | if (!this.currentTree[user]) { 46 | this.currentTree[user] = { parents: {} }; 47 | } 48 | this.currentTree[user].parents[parent] = { inActivePath, type }; 49 | 50 | if (parent === `${this.capturedTuple.object}#${this.capturedTuple.relation}`) { 51 | if (!this.currentTree[this.capturedTuple.object]) { 52 | this.currentTree[this.capturedTuple.object] = { parents: {}, inActivePath: true }; 53 | } 54 | this.addParent( 55 | `${this.capturedTuple.object}#${this.capturedTuple.relation}`, 56 | this.capturedTuple.object, 57 | undefined, 58 | true, 59 | ); 60 | } 61 | } 62 | 63 | private async expandTuple(tuple: Pick): Promise { 64 | return this.openFgaApi.expand(this.storeId, { 65 | tuple_key: { 66 | relation: tuple.relation, 67 | object: tuple.object, 68 | }, 69 | authorization_model_id: this.authorizationModelId, 70 | }); 71 | } 72 | 73 | private async walkDirectUser(node: Node, user: string): Promise { 74 | const tupleKey = getUserComponents(user); 75 | 76 | this.addParent(user, node.name!, RelationType.DirectUsers); 77 | 78 | // this is a relation that can be broken down further, e.g. github-org:auth0#member 79 | if (tupleKey.relation) { 80 | await this.walk(tupleKey); 81 | } 82 | } 83 | 84 | private async walkDirectUsers(node: Node): Promise { 85 | const users = node?.leaf?.users?.users || []; 86 | 87 | await Promise.all(users.map(async (user) => this.walkDirectUser(node, user))); 88 | } 89 | 90 | private async walkComputedUserSet(node: Node, computedUserSet: string, viaTupleToUserset?: boolean): Promise { 91 | if (!computedUserSet || computedUserSet.split(":").length !== 2) { 92 | return; 93 | } 94 | 95 | this.addParent( 96 | computedUserSet, 97 | node.name!, 98 | viaTupleToUserset ? RelationType.TupleToUserset : RelationType.ComputedUserset, 99 | ); 100 | 101 | const tupleKey = getUserComponents(computedUserSet); 102 | 103 | await this.walk(tupleKey); 104 | } 105 | 106 | private async walkTupleToUserset(node: Node): Promise { 107 | const tupleToUsersetTupleset = node.leaf?.tupleToUserset?.tupleset; 108 | 109 | if (!tupleToUsersetTupleset) { 110 | return; 111 | } 112 | 113 | const tupleKey = getUserComponents(tupleToUsersetTupleset); 114 | 115 | // this.addParent(tupleToUsersetTupleset, node.name, RelationType.TupleToUserset); 116 | 117 | await this.walk(tupleKey); 118 | } 119 | 120 | private getNodeType(node: Node): RelationType | undefined { 121 | if (node?.leaf?.computed?.userset) { 122 | return RelationType.ComputedUserset; 123 | } 124 | if (node?.leaf?.tupleToUserset?.computed) { 125 | return RelationType.TupleToUserset; 126 | } 127 | if (!node?.union?.nodes?.length) { 128 | return RelationType.DirectUsers; 129 | } 130 | } 131 | 132 | private async walkNode(node: Node): Promise { 133 | const promises: Promise[] = []; 134 | 135 | const type = this.getNodeType(node); 136 | 137 | switch (type) { 138 | case RelationType.DirectUsers: 139 | promises.push(this.walkDirectUsers(node)); 140 | break; 141 | case RelationType.ComputedUserset: 142 | const computedUserSet = node.leaf?.computed?.userset; 143 | 144 | promises.push(this.walkComputedUserSet(node, computedUserSet!)); 145 | break; 146 | case RelationType.TupleToUserset: 147 | promises.push(this.walkTupleToUserset(node)); 148 | 149 | const computedList = node.leaf?.tupleToUserset?.computed || []; 150 | 151 | promises.push(...computedList.map(async (computed) => this.walkComputedUserSet(node, computed.userset!, true))); 152 | break; 153 | // Union 154 | default: 155 | const childNodes = node.union?.nodes || []; 156 | 157 | promises.push(...childNodes.map(async (childNode) => this.walkNode(childNode))); 158 | } 159 | 160 | await Promise.all(promises); 161 | } 162 | 163 | private async walk(currentTuple: Pick & Partial>): Promise { 164 | const currentTupleKey = `${currentTuple.object}#${currentTuple.relation}`; 165 | 166 | if (!currentTuple.relation || this.expandedTuples[currentTupleKey]) { 167 | return; 168 | } 169 | if (this.currentTree && !this.currentTree[currentTupleKey]) { 170 | this.currentTree[currentTupleKey] = { parents: {} }; 171 | } 172 | 173 | this.expandedTuples[currentTupleKey] = true; 174 | const data = await this.expandTuple(currentTuple as Pick); 175 | const rootNode = data.tree?.root; 176 | 177 | await this.walkNode(rootNode!); 178 | } 179 | 180 | // This is a workaround for the tree generation function generating nodes the have no parents 181 | // If we find one, clear it - logic while drawing the graph will deal with hiding nodes linked to it 182 | private deleteHangingNodes(): void { 183 | const { tree = {} } = this; 184 | 185 | Object.keys(tree).forEach((nodeName) => { 186 | const { parents } = tree[nodeName]; 187 | 188 | if (!Object.keys(parents).length && nodeName !== this.capturedTuple.object) { 189 | // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 190 | delete tree[nodeName]; 191 | } 192 | }); 193 | } 194 | 195 | get tree(): ResolutionTree | undefined { 196 | return this.currentTree; 197 | } 198 | 199 | async buildTree(): Promise { 200 | if (this.tree) { 201 | return; 202 | } 203 | 204 | this.currentTree = {}; 205 | await this.walk(this.capturedTuple); 206 | } 207 | 208 | fillActivePath(targetUser: string): ResolutionTree { 209 | const { tree = {} } = this; 210 | const { object: targetObject } = this.capturedTuple; 211 | 212 | // Clone the tree 213 | const fullTreeObject: ResolutionTree = JSON.parse(JSON.stringify(tree)); 214 | 215 | let nextPaths = ["*", targetUser]; 216 | 217 | const traversedPaths: Record = {}; 218 | 219 | while (nextPaths.length) { 220 | const nextPathsObject: Record = {}; 221 | 222 | nextPaths.forEach((nextPath) => { 223 | if (traversedPaths[nextPath]) { 224 | return; 225 | } else { 226 | traversedPaths[nextPath] = true; 227 | } 228 | if (!fullTreeObject[nextPath]) { 229 | return; 230 | } 231 | 232 | if (fullTreeObject[nextPath].parents[targetObject]) { 233 | // if one of the parents is the actual object we are looking for, delete all other parents 234 | // this prevents looping cases where we find the object and still tru to keep going 235 | Object.keys(fullTreeObject[nextPath].parents).forEach((parent) => { 236 | if (parent !== targetObject) { 237 | delete fullTreeObject[nextPath].parents[parent]; 238 | } 239 | }); 240 | } 241 | 242 | Object.keys(fullTreeObject[nextPath].parents).forEach((parent) => { 243 | if (parent === nextPath) { 244 | return; 245 | } 246 | 247 | fullTreeObject[nextPath].parents[parent].inActivePath = true; 248 | nextPathsObject[parent] = true; 249 | }); 250 | }); 251 | 252 | nextPaths = Object.keys(nextPathsObject); 253 | } 254 | 255 | return fullTreeObject; 256 | } 257 | 258 | buildGraph(targetUser?: string, onlyInActivePath?: boolean): GraphDefinition { 259 | const { capturedTuple } = this; 260 | let { tree = {} } = this; 261 | 262 | const hasUser = !!targetUser; 263 | const graph: GraphDefinition = { 264 | nodes: [], 265 | edges: [], 266 | }; 267 | let shouldHideNode = false; 268 | 269 | this.deleteHangingNodes(); 270 | 271 | if (targetUser) { 272 | tree = this.fillActivePath(targetUser); 273 | } 274 | 275 | Object.keys(tree).forEach((nodeKey) => { 276 | const node = tree[nodeKey]; 277 | const [object, relation] = nodeKey.split("#"); 278 | let hasFoundParentInActivePath: boolean | undefined; 279 | 280 | Object.keys(node.parents).forEach((parentKey) => { 281 | const parentNode = node.parents[parentKey]; 282 | 283 | // If the parent node does not exist in the tree, ignore the whole node 284 | if (!tree[parentKey]) { 285 | shouldHideNode = true; 286 | 287 | return; 288 | } 289 | 290 | if (onlyInActivePath && !parentNode.inActivePath) { 291 | return; 292 | } 293 | 294 | hasFoundParentInActivePath = true; 295 | 296 | const type = parentNode.type?.replace(/\s/gu, ""); 297 | 298 | if (type) { 299 | graph.nodes.push({ 300 | id: `${parentKey}.${type}.${nodeKey}`, 301 | label: parentNode.type as string, 302 | }); 303 | 304 | graph.edges.push({ 305 | to: `${parentKey}.${type}.${nodeKey}`, 306 | from: parentKey, 307 | isActive: parentNode.inActivePath, 308 | group: GraphEdgeGroup.Default, 309 | }); 310 | } 311 | 312 | graph.edges.push({ 313 | to: nodeKey, 314 | from: type ? `${parentKey}.${type}.${nodeKey}` : parentKey, 315 | label: parentKey === capturedTuple.object ? `${relation} from` : "", 316 | isActive: parentNode.inActivePath, 317 | group: GraphEdgeGroup.Default, 318 | }); 319 | }); 320 | 321 | const isUserNode = nodeKey === targetUser || (nodeKey === "*" && hasUser); 322 | const isObjectNode = nodeKey === capturedTuple.object; 323 | 324 | // Only add the node if: 325 | // 1- It is the main object we are checking for OR 326 | // 2- It has a parent in the "active" path for the relationship OR 327 | // 3- We are not drawing only the active oath or force hiding the node 328 | if (isObjectNode || hasFoundParentInActivePath || !(onlyInActivePath || shouldHideNode)) { 329 | const isActive = isObjectNode || isUserNode; 330 | 331 | graph.nodes.push({ 332 | id: nodeKey, 333 | label: object === "*" && hasUser ? `${targetUser} via everyone (*)` : nodeKey, 334 | isActive, 335 | }); 336 | } 337 | }); 338 | 339 | return graph; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/validator/default-regex.ts: -------------------------------------------------------------------------------- 1 | // These rules should match the rules as specified in 2 | // https://github.com/openfga/api/blob/main/openfga/v1/openfga.proto 3 | 4 | // eslint-disable-next-line no-useless-escape 5 | export const defaultTypeRule = "^[^:#@\\s]{1,254}$"; 6 | // eslint-disable-next-line no-useless-escape 7 | export const defaultRelationRule = "^[^:#@\\s]{1,50}$"; 8 | -------------------------------------------------------------------------------- /src/validator/index.ts: -------------------------------------------------------------------------------- 1 | import { ulidValidate } from "./ulid-regex"; 2 | 3 | const validator = { 4 | ulidValidate, 5 | }; 6 | 7 | export default validator; 8 | -------------------------------------------------------------------------------- /src/validator/ulid-regex.ts: -------------------------------------------------------------------------------- 1 | export const ulidPattern = /[0-9A-HJKMNP-TV-Z]{26}/; 2 | export const ulidValidate = (id: string) => ulidPattern.test(id); 3 | -------------------------------------------------------------------------------- /tests/data/index.ts: -------------------------------------------------------------------------------- 1 | export { testModels } from "./test-models"; 2 | -------------------------------------------------------------------------------- /tests/data/test-models.ts: -------------------------------------------------------------------------------- 1 | import type { WriteAuthorizationModelRequest } from "@openfga/sdk"; 2 | 3 | export const testModels: { name: string; json: WriteAuthorizationModelRequest; friendly: string }[] = [ 4 | { 5 | name: "one type with no relations", 6 | json: { 7 | schema_version: "1.0", 8 | type_definitions: [ 9 | { 10 | type: "document", 11 | relations: {}, 12 | }, 13 | ], 14 | }, 15 | friendly: `model 16 | schema 1.0 17 | type document\n`, 18 | }, 19 | { 20 | name: "one type with no relations and another with one relation", 21 | json: { 22 | schema_version: "1.0", 23 | type_definitions: [ 24 | { 25 | type: "group", 26 | relations: {}, 27 | }, 28 | { 29 | type: "document", 30 | relations: { 31 | viewer: { 32 | this: {}, 33 | }, 34 | editor: { 35 | this: {}, 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | friendly: 42 | "model\n schema 1.0\ntype group\ntype document\n relations\n define viewer as self\n define editor as self\n", 43 | }, 44 | { 45 | name: "simple model", 46 | json: { 47 | schema_version: "1.0", 48 | type_definitions: [ 49 | { 50 | type: "document", 51 | relations: { 52 | viewer: { 53 | this: {}, 54 | }, 55 | editor: { 56 | this: {}, 57 | }, 58 | }, 59 | }, 60 | ], 61 | }, 62 | friendly: "model\n schema 1.0\ntype document\n relations\n define viewer as self\n define editor as self\n", 63 | }, 64 | { 65 | name: "multiple types", 66 | json: { 67 | schema_version: "1.0", 68 | type_definitions: [ 69 | { 70 | type: "folder", 71 | relations: { 72 | editor: { 73 | this: {}, 74 | }, 75 | }, 76 | }, 77 | { 78 | type: "document", 79 | relations: { 80 | parent: { 81 | this: {}, 82 | }, 83 | editor: { 84 | union: { 85 | child: [ 86 | { 87 | this: {}, 88 | }, 89 | { 90 | tupleToUserset: { 91 | tupleset: { 92 | object: "", 93 | relation: "parent", 94 | }, 95 | computedUserset: { 96 | object: "", 97 | relation: "editor", 98 | }, 99 | }, 100 | }, 101 | ], 102 | }, 103 | }, 104 | }, 105 | }, 106 | ], 107 | }, 108 | friendly: `model 109 | schema 1.0 110 | type folder 111 | relations 112 | define editor as self 113 | type document 114 | relations 115 | define parent as self 116 | define editor as self or editor from parent 117 | `, 118 | }, 119 | { 120 | name: "difference", 121 | json: { 122 | schema_version: "1.0", 123 | type_definitions: [ 124 | { 125 | type: "document", 126 | relations: { 127 | blocked: { 128 | this: {}, 129 | }, 130 | editor: { 131 | difference: { 132 | base: { 133 | this: {}, 134 | }, 135 | subtract: { 136 | computedUserset: { 137 | object: "", 138 | relation: "blocked", 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | }, 145 | { 146 | type: "team", 147 | relations: { 148 | member: { 149 | this: {}, 150 | }, 151 | }, 152 | }, 153 | ], 154 | }, 155 | friendly: `model 156 | schema 1.0 157 | type document 158 | relations 159 | define blocked as self 160 | define editor as self but not blocked 161 | type team 162 | relations 163 | define member as self 164 | `, 165 | }, 166 | { 167 | name: "intersection", 168 | json: { 169 | schema_version: "1.0", 170 | type_definitions: [ 171 | { 172 | type: "document", 173 | relations: { 174 | owner: { 175 | this: {}, 176 | }, 177 | writer: { 178 | this: {}, 179 | }, 180 | can_write: { 181 | computedUserset: { 182 | object: "", 183 | relation: "writer", 184 | }, 185 | }, 186 | can_delete: { 187 | intersection: { 188 | child: [ 189 | { 190 | computedUserset: { 191 | object: "", 192 | relation: "writer", 193 | }, 194 | }, 195 | { 196 | tupleToUserset: { 197 | tupleset: { 198 | object: "", 199 | relation: "owner", 200 | }, 201 | computedUserset: { 202 | object: "", 203 | relation: "member", 204 | }, 205 | }, 206 | }, 207 | ], 208 | }, 209 | }, 210 | }, 211 | }, 212 | { 213 | type: "organization", 214 | relations: { 215 | member: { 216 | this: {}, 217 | }, 218 | }, 219 | }, 220 | ], 221 | }, 222 | friendly: `model 223 | schema 1.0 224 | type document 225 | relations 226 | define owner as self 227 | define writer as self 228 | define can_write as writer 229 | define can_delete as writer and member from owner 230 | type organization 231 | relations 232 | define member as self 233 | `, 234 | }, 235 | { 236 | name: "union for 1.1", 237 | json: { 238 | schema_version: "1.1", 239 | type_definitions: [ 240 | { 241 | type: "user", 242 | relations: {}, 243 | }, 244 | { 245 | type: "document", 246 | relations: { 247 | owner: { 248 | this: {}, 249 | }, 250 | writer: { 251 | union: { 252 | child: [ 253 | { 254 | computedUserset: { 255 | object: "", 256 | relation: "owner", 257 | }, 258 | }, 259 | { 260 | this: {}, 261 | }, 262 | ], 263 | }, 264 | }, 265 | reader: { 266 | union: { 267 | child: [ 268 | { 269 | computedUserset: { 270 | object: "", 271 | relation: "owner", 272 | }, 273 | }, 274 | { 275 | this: {}, 276 | }, 277 | { 278 | computedUserset: { 279 | object: "", 280 | relation: "writer", 281 | }, 282 | }, 283 | ], 284 | }, 285 | }, 286 | can_write: { 287 | computedUserset: { 288 | object: "", 289 | relation: "writer", 290 | }, 291 | }, 292 | can_delete: { 293 | intersection: { 294 | child: [ 295 | { 296 | computedUserset: { 297 | object: "", 298 | relation: "writer", 299 | }, 300 | }, 301 | { 302 | tupleToUserset: { 303 | tupleset: { 304 | object: "", 305 | relation: "owner", 306 | }, 307 | computedUserset: { 308 | object: "", 309 | relation: "member", 310 | }, 311 | }, 312 | }, 313 | ], 314 | }, 315 | }, 316 | }, 317 | metadata: { 318 | relations: { 319 | owner: { directly_related_user_types: [{ type: "user" }] }, 320 | writer: { directly_related_user_types: [{ type: "user" }] }, 321 | reader: { directly_related_user_types: [{ type: "user" }] }, 322 | can_write: { directly_related_user_types: [] }, 323 | can_delete: { directly_related_user_types: [] }, 324 | }, 325 | }, 326 | }, 327 | { 328 | type: "organization", 329 | relations: { 330 | member: { 331 | this: {}, 332 | }, 333 | }, 334 | metadata: { 335 | relations: { 336 | member: { directly_related_user_types: [{ type: "user" }] }, 337 | }, 338 | }, 339 | }, 340 | ], 341 | }, 342 | friendly: `model 343 | schema 1.1 344 | type user 345 | type document 346 | relations 347 | define owner: [user] 348 | define writer: owner or [user] 349 | define reader: owner or [user] or writer 350 | define can_write: writer 351 | define can_delete: writer and member from owner 352 | type organization 353 | relations 354 | define member: [user] 355 | `, 356 | }, 357 | { 358 | name: "intersection for 1.1", 359 | json: { 360 | schema_version: "1.1", 361 | type_definitions: [ 362 | { 363 | type: "user", 364 | relations: {}, 365 | }, 366 | { 367 | type: "document", 368 | relations: { 369 | allowed: { 370 | this: {}, 371 | }, 372 | member: { 373 | this: {}, 374 | }, 375 | writer: { 376 | intersection: { 377 | child: [ 378 | { 379 | computedUserset: { 380 | object: "", 381 | relation: "member", 382 | }, 383 | }, 384 | { 385 | this: {}, 386 | }, 387 | ], 388 | }, 389 | }, 390 | reader: { 391 | intersection: { 392 | child: [ 393 | { 394 | computedUserset: { 395 | object: "", 396 | relation: "member", 397 | }, 398 | }, 399 | { 400 | this: {}, 401 | }, 402 | { 403 | computedUserset: { 404 | object: "", 405 | relation: "allowed", 406 | }, 407 | }, 408 | ], 409 | }, 410 | }, 411 | }, 412 | metadata: { 413 | relations: { 414 | allowed: { directly_related_user_types: [{ type: "user" }] }, 415 | member: { directly_related_user_types: [{ type: "user" }] }, 416 | writer: { directly_related_user_types: [{ type: "user" }] }, 417 | reader: { directly_related_user_types: [{ type: "user" }] }, 418 | }, 419 | }, 420 | }, 421 | ], 422 | }, 423 | friendly: `model 424 | schema 1.1 425 | type user 426 | type document 427 | relations 428 | define allowed: [user] 429 | define member: [user] 430 | define writer: member and [user] 431 | define reader: member and [user] and allowed 432 | `, 433 | }, 434 | { 435 | name: "difference for model 1.1", 436 | json: { 437 | schema_version: "1.1", 438 | type_definitions: [ 439 | { 440 | type: "document", 441 | relations: { 442 | blocked: { 443 | this: {}, 444 | }, 445 | editor: { 446 | difference: { 447 | base: { 448 | this: {}, 449 | }, 450 | subtract: { 451 | computedUserset: { 452 | object: "", 453 | relation: "blocked", 454 | }, 455 | }, 456 | }, 457 | }, 458 | }, 459 | metadata: { 460 | relations: { 461 | blocked: { directly_related_user_types: [{ type: "user" }] }, 462 | editor: { directly_related_user_types: [{ type: "user" }] }, 463 | }, 464 | }, 465 | }, 466 | { 467 | type: "user", 468 | relations: {}, 469 | }, 470 | ], 471 | }, 472 | friendly: `model 473 | schema 1.1 474 | type document 475 | relations 476 | define blocked: [user] 477 | define editor: [user] but not blocked 478 | type user 479 | `, 480 | }, 481 | { 482 | name: "relations-starting-with-as", 483 | json: { 484 | schema_version: "1.0", 485 | type_definitions: [ 486 | { type: "org", relations: { member: { this: {} } } }, 487 | { 488 | type: "feature", 489 | relations: { 490 | associated_plan: { this: {} }, 491 | access: { 492 | tupleToUserset: { 493 | tupleset: { object: "", relation: "associated_plan" }, 494 | computedUserset: { object: "", relation: "subscriber_member" }, 495 | }, 496 | }, 497 | }, 498 | }, 499 | { 500 | type: "plan", 501 | relations: { 502 | subscriber: { this: {} }, 503 | subscriber_member: { 504 | tupleToUserset: { 505 | tupleset: { object: "", relation: "subscriber" }, 506 | computedUserset: { object: "", relation: "member" }, 507 | }, 508 | }, 509 | }, 510 | }, 511 | { 512 | type: "permission", 513 | relations: { 514 | access_feature: { 515 | tupleToUserset: { 516 | tupleset: { 517 | object: "", 518 | relation: "associated_feature", 519 | }, 520 | computedUserset: { object: "", relation: "access" }, 521 | }, 522 | }, 523 | associated_feature: { this: {} }, 524 | }, 525 | }, 526 | ], 527 | }, 528 | friendly: `model 529 | schema 1.0 530 | type org 531 | relations 532 | define member as self 533 | type feature 534 | relations 535 | define associated_plan as self 536 | define access as subscriber_member from associated_plan 537 | type plan 538 | relations 539 | define subscriber as self 540 | define subscriber_member as member from subscriber 541 | type permission 542 | relations 543 | define access_feature as access from associated_feature 544 | define associated_feature as self 545 | `, 546 | }, 547 | { 548 | name: "one type with one relation that supports one type", 549 | json: { 550 | schema_version: "1.1", 551 | type_definitions: [ 552 | { 553 | type: "document", 554 | relations: { 555 | viewer: { 556 | this: {}, 557 | }, 558 | }, 559 | metadata: { 560 | relations: { 561 | viewer: { directly_related_user_types: [{ type: "team", relation: "member" }] }, 562 | }, 563 | }, 564 | }, 565 | ], 566 | }, 567 | friendly: `model 568 | schema 1.1 569 | type document 570 | relations 571 | define viewer: [team#member] 572 | `, 573 | }, 574 | { 575 | name: "one type with one relation that supports two types", 576 | json: { 577 | schema_version: "1.1", 578 | type_definitions: [ 579 | { 580 | type: "document", 581 | relations: { 582 | viewer: { 583 | this: {}, 584 | }, 585 | }, 586 | metadata: { 587 | relations: { 588 | viewer: { directly_related_user_types: [{ type: "user" }, { type: "group" }] }, 589 | }, 590 | }, 591 | }, 592 | ], 593 | }, 594 | friendly: `model 595 | schema 1.1 596 | type document 597 | relations 598 | define viewer: [user,group] 599 | `, 600 | }, 601 | { 602 | name: "wildcard restriction conversion", 603 | json: { 604 | schema_version: "1.1", 605 | type_definitions: [ 606 | { 607 | type: "document", 608 | relations: { 609 | viewer: { 610 | this: {}, 611 | }, 612 | }, 613 | metadata: { 614 | relations: { 615 | viewer: { 616 | directly_related_user_types: [{ type: "user" }, { type: "user", wildcard: {} }, { type: "group" }], 617 | }, 618 | }, 619 | }, 620 | }, 621 | ], 622 | }, 623 | friendly: `model 624 | schema 1.1 625 | type document 626 | relations 627 | define viewer: [user,user:*,group] 628 | `, 629 | }, 630 | ]; 631 | -------------------------------------------------------------------------------- /tests/dsl-validation.test.ts: -------------------------------------------------------------------------------- 1 | import { tools } from "../src"; 2 | import { MonacoEditor } from "../src/tools/monaco/typings"; 3 | 4 | const { MonacoExtensions } = tools; 5 | const { validateDSL } = MonacoExtensions; 6 | 7 | const MonacoErrorSeverityShim = { MarkerSeverity: { Error: 8 } } as typeof MonacoEditor; 8 | 9 | // @ts-ignore 10 | import { validation_cases } from "./data/model-validation"; 11 | 12 | describe("DSL validation", () => { 13 | validation_cases.forEach((testCase) => { 14 | it(`should validate ${testCase.name}`, () => { 15 | const result = validateDSL(MonacoErrorSeverityShim, testCase.friendly).map((error: any) => { 16 | delete error.extraInformation; 17 | delete error.message; 18 | delete error.source; 19 | return error; 20 | }); 21 | 22 | // TODO: Current language does not expose this information 23 | const expectedErrors = testCase.expectedError.map((expectedError: any) => { 24 | delete expectedError.extraInformation; 25 | delete expectedError.message; 26 | delete expectedError.source; 27 | return expectedError; 28 | }); 29 | 30 | expect(result).toEqual(expectedErrors); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | preset: "ts-jest", 4 | coveragePathIgnorePatterns: ["/node_modules/", "/tests/"], 5 | moduleFileExtensions: ["js", "d.ts", "ts", "json"], 6 | }; 7 | -------------------------------------------------------------------------------- /tests/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"], 5 | "noEmit": true, 6 | "rootDir": "../" 7 | }, 8 | "include": ["../**/*.ts", "./*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "resolveJsonModule": true, 9 | "noImplicitAny": true, 10 | "strict": true, 11 | "outDir": "dist", 12 | "rootDir": "src", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ] 16 | }, 17 | "exclude": [ 18 | "dist", 19 | "node_modules", 20 | "**/*.test.ts", 21 | "tests" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------