├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── config ├── permissions │ └── .gitkeep └── roles.yaml ├── env.ext ├── migrations └── 20220501-permission-composite-index.js ├── package-lock.json ├── package.json ├── src ├── helpers.ts ├── index.ts └── types.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | 10 | [{package.json,*.yml,*.yaml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [Dockerfile] 15 | indent_style = tab 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | templates 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const defaultRules = { 2 | // No console statements in production 3 | 'no-console': process.env.NODE_ENV !== 'development' ? 'error' : 'off', 4 | // No debugger statements in production 5 | 'no-debugger': process.env.NODE_ENV !== 'development' ? 'error' : 'off', 6 | // It's recommended to turn off this rule on TypeScript projects 7 | 'no-undef': 'off', 8 | // Allow ts-directive comments (used to suppress TypeScript compiler errors) 9 | '@typescript-eslint/ban-ts-comment': 'off', 10 | // Allow usage of the any type (consider to enable this rule later on) 11 | '@typescript-eslint/no-explicit-any': 'off', 12 | // Allow usage of require statements (consider to enable this rule later on) 13 | '@typescript-eslint/no-var-requires': 'off', 14 | // Allow non-null assertions for now (consider to enable this rule later on) 15 | '@typescript-eslint/no-non-null-assertion': 'off', 16 | // Allow unused arguments and variables when they begin with an underscore 17 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 18 | }; 19 | 20 | module.exports = { 21 | // Stop looking for ESLint configurations in parent folders 22 | root: true, 23 | // Global variables: Browser and Node.js 24 | env: { 25 | browser: true, 26 | node: true, 27 | }, 28 | // Basic configuration for js files 29 | plugins: ['@typescript-eslint'], 30 | extends: ['eslint:recommended'], 31 | rules: defaultRules, 32 | parserOptions: { 33 | ecmaVersion: 2020, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Gerard Lamusse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Directus RBAC Sync 2 | Hooks and CLI tool for exporting and importing Directus roles and permissions. 3 | 4 | ## Usage 5 | 6 | ### CLI 7 | 8 | - `directus rbac export`: Export roles and permissions to files 9 | - `--system`: Include system collections' permissions 10 | - `directus rbac import`: Import roles and permissions from files 11 | 12 | ### Automatic Syncing 13 | 14 | Remember to set the `RBAC_SYNC_MODE` in your `.env` file. 15 | 16 | **POSSIBLE VALUES:** 17 | - `EXPORT`: Exports all changes to file upon create/update/delete-ing 18 | - `IMPORT`: Imports roles and permissions stored in files, upon starting Directus 19 | - `FULL`: Executes both EXPORT and IMPORT 20 | - `NONE`: Does nothing 21 | 22 | 23 | ## Installation 24 | 25 | - Download and extract the release archive. 26 | - Copy the `config` directory to your project root. 27 | - Copy and rename the `dist` directory to your `extensions/hooks/rbac-sync`. 28 | - If you want to override or automate syncing add the following to your `.env` 29 | ``` 30 | RBAC_SYNC_MODE=EXPORT 31 | RBAC_CONFIG_PATH=./config 32 | ``` 33 | 34 | ### Migration (optional) 35 | 36 | To ensure that no duplicate roles ever exist, you can use the following migration: 37 | - Copy the migration file from `migrations/20220501-permission-composite-index.js` to your `extensions/migrations` directory 38 | - Exceute `directus database migrate:latest` 39 | 40 | 41 | ## Config Structure 42 | 43 | All roles are stored in the `config/roles.yaml` file. 44 | 45 | All permissions are stored per collection in `config/permissions/[COLLECTION].yaml` files. 46 | 47 | Permissions are separated by `action` with roles being a group of role ids that have the same permissions. 48 | 49 | ## Why? 50 | 51 | This is very useful for syncing roles and permissions between environments and developers. 52 | 53 | The file based approach is also useful for tracking changes using git. 54 | 55 | Roles are stored with their ids, however permissions are tracked by a composite index on `collection, action, role` in order to reduce code conflicts when multiple developers generate permissions and have to merge them. 56 | 57 | 58 | # Warning 59 | 60 | As always, be careful when making changes and importing them to your database. Make a backup and test to see if this works the way you expect. 61 | -------------------------------------------------------------------------------- /config/permissions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u12206050/directus-rbac-sync/fa8a2eade4ba5be730ce39ad3857fc88b6c06486/config/permissions/.gitkeep -------------------------------------------------------------------------------- /config/roles.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u12206050/directus-rbac-sync/fa8a2eade4ba5be730ce39ad3857fc88b6c06486/config/roles.yaml -------------------------------------------------------------------------------- /env.ext: -------------------------------------------------------------------------------- 1 | #################################################################################################### 2 | ## RBAC Sync 3 | 4 | RBAC_SYNC_MODE=EXPORT 5 | RBAC_CONFIG_PATH=./config 6 | -------------------------------------------------------------------------------- /migrations/20220501-permission-composite-index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.table('directus_permissions', (table) => { 4 | table.unique(['role', 'collection', 'action']); 5 | }); 6 | }, 7 | 8 | async down(knex) { 9 | await knex.schema.table('directus_permissions', (table) => { 10 | table.dropUnique(['role', 'collection', 'action']); 11 | }); 12 | }, 13 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directus-rbac-sync", 3 | "description": "Tool for exporting and importing Directus roles and permissions.", 4 | "icon": "extension", 5 | "version": "0.6.0", 6 | "keywords": [ 7 | "directus", 8 | "directus-extension", 9 | "directus-custom-hook" 10 | ], 11 | "type": "module", 12 | "directus:extension": { 13 | "type": "hook", 14 | "path": "dist/index.js", 15 | "source": "src/index.ts", 16 | "host": "^10.0.0" 17 | }, 18 | "scripts": { 19 | "build": "directus-extension build", 20 | "dev": "directus-extension build -w --no-minify", 21 | "link": "directus-extension link" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+ssh://git@github.com/u12206050/directus-rbac-sync.git" 26 | }, 27 | "author": "Gerard Lamusse", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/u12206050/directus-rbac-sync/issues" 31 | }, 32 | "homepage": "https://github.com/u12206050/directus-rbac-sync#readme", 33 | "dependencies": { 34 | "fs-extra": "^11.1.0", 35 | "js-yaml": "^4.1.0", 36 | "lodash-es": "^4.17.21" 37 | }, 38 | "devDependencies": { 39 | "@directus/extensions-sdk": "^10.1.8", 40 | "@directus/types": "^10.0.0", 41 | "@types/fs-extra": "^11.0.2", 42 | "@types/js-yaml": "^4.0.8", 43 | "@types/knex": "^0.16.1", 44 | "@types/lodash-es": "^4.17.10", 45 | "@types/node": "^20.8.7", 46 | "typescript": "^5.2.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Permission, Role } from '@directus/types'; 2 | import fse from 'fs-extra'; 3 | import { load as fromYaml, dump as toYaml } from 'js-yaml'; 4 | import { isEmpty, isEqual } from 'lodash-es'; 5 | import path from 'path'; 6 | import { ItemsService, StoredPermission, StoredRole } from './types'; 7 | 8 | const configPath = process.env.RBAC_CONFIG_PATH || './config'; 9 | const permissionsPath = path.resolve(configPath, 'permissions'); 10 | const rolesFile = path.join(path.resolve(configPath), `roles.yaml`) 11 | 12 | // 13 | // PERMISSIONS 14 | // 15 | 16 | export async function getPermissionCollection(permissionId: string|number, permissionsService: ItemsService) { 17 | const permission = await permissionsService.readOne(permissionId, { 18 | fields: ['collection'], 19 | }) as Permission; 20 | 21 | return permission.collection 22 | } 23 | 24 | export async function listConfiguredCollections() { 25 | const allFiles = await fse.readdir(permissionsPath); 26 | const collections: string[] = []; 27 | allFiles.forEach(file => { 28 | if (file.endsWith('.yaml')) { 29 | collections.push(file.replace('.yaml', '')); 30 | } 31 | }); 32 | 33 | return collections 34 | } 35 | 36 | export async function importPermissions(collection: string, permissionsService: ItemsService) { 37 | const yamlFile = path.join(permissionsPath, `${collection}.yaml`) 38 | if (! fse.pathExists(yamlFile)) { 39 | return 0 40 | } 41 | 42 | const yamlInput = await fse.readFile(yamlFile, 'utf8') 43 | const permissions = fromYaml(yamlInput) as Array 44 | 45 | const permissionsToImport: Array = [] 46 | const updatingActions = new Set() 47 | const updatingRoles = new Set() 48 | permissions.forEach((block) => { 49 | const { action, roles, permissions, validation, presets, fields } = block 50 | if (isEmpty(roles)) { 51 | throw new Error(`Permission block ${collection}/${action} is missing roles`) 52 | } 53 | 54 | updatingActions.add(action) 55 | 56 | roles!.forEach((role) => { 57 | updatingRoles.add(role) 58 | permissionsToImport.push({ 59 | role, 60 | action, 61 | collection, 62 | permissions: permissions || null, 63 | validation: validation || null, 64 | presets: presets || null, 65 | fields: typeof fields === "string" ? [fields] : fields ?? null, 66 | }) 67 | }) 68 | }) 69 | 70 | // Delete permissions not existing more on roles that we manage 71 | await permissionsService.deleteByQuery({ 72 | filter: { 73 | collection, 74 | role: { 75 | _in: [...updatingRoles].map((role) => role === null ? permissionsService.knex.raw('NULL') : role) as Array, 76 | }, 77 | action: { 78 | _nin: [...updatingActions] as Array, 79 | }, 80 | } 81 | }, { emitEvents: false }) 82 | 83 | const queue = permissionsToImport.map(async (permission) => { 84 | const { collection, action, role } = permission 85 | const exists = await permissionsService.readByQuery({ 86 | filter: { 87 | collection, 88 | action, 89 | role: role === null ? { _null: true } : role, 90 | }, 91 | limit: 1, 92 | fields: ['id'] 93 | }, { emitEvents: false }) 94 | 95 | if (exists?.length && exists[0]?.id) { 96 | await permissionsService.updateOne(exists[0].id, permission, { emitEvents: false }) 97 | } else { 98 | await permissionsService.createOne(permission, { emitEvents: false }) 99 | } 100 | }) 101 | 102 | await Promise.all(queue) 103 | } 104 | 105 | export async function exportPermissions(collection: string, permissionsService: ItemsService) { 106 | const rows = await permissionsService.readByQuery({ 107 | filter: { 108 | collection, 109 | }, 110 | }) as Permission[] 111 | 112 | // Find matching permissions to group roles into 113 | const uniquePerms: Array<[StoredPermission, Array]> = [] 114 | rows.sort( 115 | (rowA, rowB) => rowA.action.localeCompare(rowB.action) 116 | ).forEach((row) => { 117 | const { role, action, permissions, validation, presets, fields } = row; 118 | const perm: StoredPermission = { 119 | action 120 | } 121 | if (! isEmpty(permissions)) { 122 | perm.permissions = permissions; 123 | } 124 | if (! isEmpty(validation)) { 125 | perm.validation = validation; 126 | } 127 | if (! isEmpty(presets)) { 128 | perm.presets = presets; 129 | } 130 | 131 | if (Array.isArray(fields) && fields.length) { 132 | fields.sort((a, b) => a.localeCompare(b)) 133 | perm.fields = fields.length === 1 ? fields[0] : fields 134 | } 135 | 136 | const found = uniquePerms.find((unique) => isEqual(unique[0], perm)) 137 | if (found) { 138 | if (! found[1].includes(role)) { 139 | found[1].push(role); 140 | } 141 | } else { 142 | uniquePerms.push([perm, [role]]); 143 | } 144 | }) 145 | 146 | // Add the roles to each unique permission 147 | const permissions: Array = uniquePerms.map(([perm, roles]) => { 148 | perm.roles = roles; 149 | return perm; 150 | }) 151 | 152 | let yamlOutput = toYaml(permissions, { 153 | sortKeys: true, 154 | }) 155 | 156 | const yamlFile = path.join(permissionsPath, `${collection}.yaml`) 157 | if (! yamlOutput.startsWith('[]')) { 158 | yamlOutput = yamlOutput.replace(/- action/g, '\n- action') 159 | await fse.writeFile(yamlFile, yamlOutput, 'utf8').catch(console.error); 160 | } else { 161 | const filepathDir = path.dirname(yamlFile) 162 | await fse.readdir(filepathDir).then((files) => { 163 | if (files.find((file) => file === `${collection}.yaml`)) { 164 | return fse.remove(yamlFile) 165 | } 166 | }).catch(console.error) 167 | } 168 | } 169 | 170 | // 171 | // ROLES 172 | // 173 | 174 | export async function importRoles(rolesService: ItemsService) { 175 | if (! fse.pathExists(rolesFile)) { 176 | return 0 177 | } 178 | 179 | const yamlInput = await fse.readFile(rolesFile, 'utf8') 180 | const roles = fromYaml(yamlInput) as Array 181 | 182 | const rolesToImport: Array = roles.map((block) => { 183 | const { id, name, icon, description, enforce_tfa, external_id, ip_whitelist, app_access, admin_access } = block 184 | 185 | return { 186 | id, 187 | name, 188 | icon: icon ?? 'supervised_user_circle', 189 | description: description ?? '', 190 | enforce_tfa: enforce_tfa ?? false, 191 | external_id: external_id ?? null, 192 | ip_whitelist: ip_whitelist ?? [], 193 | app_access: app_access ?? false, 194 | admin_access: admin_access ?? false, 195 | } 196 | }) 197 | 198 | return await rolesService.upsertMany(rolesToImport, { emitEvents: false }) 199 | } 200 | 201 | export async function exportRoles(rolesService: ItemsService) { 202 | const rows = await rolesService.readByQuery({ 203 | limit: -1, 204 | fields: ['id', 'name', 'icon', 'description', 'enforce_tfa', 'external_id', 'ip_whitelist', 'app_access', 'admin_access'], 205 | }) as Role[] 206 | 207 | const roles: Array = rows.map((row) => { 208 | const { id, name, icon, ...optional } = row 209 | const role: StoredRole = { 210 | id, 211 | name, 212 | icon, 213 | } 214 | 215 | // We only want to dump the optional fields if they are not falsy 216 | Object.entries(optional).forEach(([key, value]) => { 217 | if (!! value) { 218 | // @ts-ignore 219 | role[key] = value 220 | } 221 | }) 222 | 223 | return role 224 | }) 225 | 226 | let yamlOutput = toYaml(roles, { 227 | sortKeys: false, 228 | }) 229 | 230 | if (! yamlOutput.startsWith('[]')) { 231 | yamlOutput = yamlOutput.replace(/- id/g, '\n- id') 232 | 233 | await fse.writeFile(rolesFile, yamlOutput, 'utf8'); 234 | } else { 235 | await fse.remove(rolesFile) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { clearSystemCache } from "@directus/api/cache"; 2 | import { defineHook } from '@directus/extensions-sdk'; 3 | import { ActionHandler, Collection, FilterHandler, Permission } from "@directus/types"; 4 | import { 5 | exportPermissions, 6 | exportRoles, 7 | importPermissions, 8 | importRoles, 9 | listConfiguredCollections 10 | } from "./helpers"; 11 | 12 | export default defineHook(({ filter, action, init }, extCtx) => { 13 | const { services: { CollectionsService, PermissionsService, RolesService }, env, logger } = extCtx; 14 | 15 | const onCreate: ActionHandler = async ({ key }, { database, schema }) => { 16 | const permissionsService = new PermissionsService({ database, schema }); 17 | 18 | const permission = await permissionsService.readOne(key, { 19 | fields: ['id', 'collection'], 20 | }) as Permission; 21 | 22 | await exportPermissions(permission.collection, permissionsService) 23 | } 24 | 25 | const onUpdate: ActionHandler = async ({ keys }, { database, schema }) => { 26 | const permissionsService = new PermissionsService({ database, schema }); 27 | 28 | const permissions = await permissionsService.readMany(keys, { 29 | fields: ['id', 'collection'], 30 | }) as Array>; 31 | 32 | const uniqueCollections = [...new Set(permissions.map(p => p.collection))] 33 | 34 | await Promise.all(uniqueCollections.map((collection) => 35 | exportPermissions(collection, permissionsService) 36 | )) 37 | } 38 | 39 | // 40 | // Need to keep track of what collection the deleting permission is for 41 | // so that we can dump the permissions after it is deleted 42 | // 43 | const deletingPermissions: Record = {}; 44 | const beforeDelete: FilterHandler = async (keys, meta, { database, schema }) => { 45 | const permissionsService = new PermissionsService({ database, schema }); 46 | 47 | const permissions = await permissionsService.readMany(keys, { 48 | fields: ['id', 'collection'], 49 | }) as Array<{ 50 | id: number, 51 | collection: string 52 | }>; 53 | 54 | permissions.forEach(({ id, collection }) => { 55 | deletingPermissions[id] = collection; 56 | }) 57 | } 58 | 59 | const onDelete: ActionHandler = async ({ keys }, { database, schema }) => { 60 | const permissionsService = new PermissionsService({ database, schema }); 61 | 62 | const uniqueCollections = new Set() 63 | keys.forEach((key: string|number) => uniqueCollections.add(deletingPermissions[key])) 64 | 65 | await Promise.all([...uniqueCollections].map((collection) => 66 | exportPermissions(collection, permissionsService) 67 | )) 68 | 69 | keys.forEach((key: string|number) => delete deletingPermissions[key]) 70 | } 71 | 72 | const onRoleChanges: ActionHandler = ({ keys, key }, { database, schema }) => { 73 | const rolesService = new RolesService({ database, schema }); 74 | return exportRoles(rolesService) 75 | } 76 | 77 | async function syncToDb() { 78 | const { getSchema, database } = extCtx; 79 | const schema = await getSchema() 80 | const permissionsService = new PermissionsService({ database, schema }); 81 | const rolesService = new RolesService({database, schema}); 82 | 83 | // Sync roles into db 84 | logger.info('Importing roles...') 85 | await importRoles(rolesService) 86 | 87 | // Sync permissions into db 88 | logger.info('Importing permissions...'); 89 | 90 | const collections = await listConfiguredCollections() 91 | 92 | await Promise.all(collections.map(collection => 93 | importPermissions(collection, permissionsService) 94 | )) 95 | 96 | // WIP; not sure if this is best way to solve 97 | await clearSystemCache(); 98 | 99 | logger.info('RBAC imported!') 100 | } 101 | 102 | if (['EXPORT', 'FULL'].includes(env.RBAC_SYNC_MODE)) { 103 | action('roles.create', onRoleChanges) 104 | action('roles.update', onRoleChanges) 105 | action('roles.delete', onRoleChanges) 106 | 107 | 108 | action('permissions.create', onCreate) 109 | action('permissions.update', onUpdate) 110 | filter('permissions.delete', beforeDelete) 111 | action('permissions.delete', onDelete) 112 | } 113 | 114 | if (['IMPORT', 'FULL'].includes(env.RBAC_SYNC_MODE)) { 115 | setTimeout(syncToDb, 10) 116 | } 117 | 118 | init('app.before', async ({ program }) => { 119 | if (['IMPORT', 'FULL'].includes(env.RBAC_SYNC_MODE)) { 120 | await syncToDb() 121 | } 122 | }); 123 | 124 | init('cli.before', async ({ program }) => { 125 | const dbCommand = program.command('rbac'); 126 | 127 | // Only allow this command when not automatically importing 128 | dbCommand.command('import') 129 | .description('Sync configured roles and permissions from files to database') 130 | .action(async () => { 131 | if (! ['IMPORT', 'FULL'].includes(env.RBAC_SYNC_MODE)) { 132 | try { 133 | await syncToDb() 134 | process.exit(0); 135 | } catch (err: any) { 136 | logger.error(err); 137 | } 138 | } else { 139 | logger.warn('RBAC Sync is configured to automatically import roles and permissions. Skipping manual import.') 140 | } 141 | process.exit(1); 142 | }) 143 | 144 | dbCommand.command('export') 145 | .description('Sync roles and permissions from DB to file') 146 | .option('--system', 'Include system collections') 147 | .action(async ({system = false}) => { 148 | const { getSchema, database } = extCtx; 149 | 150 | logger.info('Exporting RBAC...') 151 | try { 152 | const schema = await getSchema() 153 | const collectionsService = new CollectionsService({database, schema}); 154 | const permissionsService = new PermissionsService({database, schema}); 155 | const rolesService = new RolesService({database, schema}); 156 | 157 | const collections: Collection[] = await collectionsService.readByQuery() 158 | 159 | logger.info('Exporting permissions...'); 160 | await Promise.all(collections.map(({collection}) => { 161 | if (!system && collection.startsWith('directus_')) { 162 | return 163 | } 164 | 165 | return exportPermissions(collection, permissionsService) 166 | })) 167 | 168 | logger.info('Exporting roles...'); 169 | await exportRoles(rolesService) 170 | 171 | logger.info('RBAC exported!') 172 | process.exit(0); 173 | } catch (err: any) { 174 | logger.error(err); 175 | process.exit(1); 176 | } 177 | }); 178 | }) 179 | }); 180 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Accountability, Permission, Role } from '@directus/types'; 2 | import type { Knex } from 'knex'; 3 | 4 | export type StoredPermission = Pick & 5 | Partial> & { 6 | roles?: Array; 7 | presets?: Record | null; 8 | fields?: Array | string | null; 9 | }; 10 | export type StoredRole = Partial & Pick; 11 | 12 | // 13 | // Defining used Directus types here in order to get type hinting without installing entire Directus 14 | // 15 | export type Item = Record; 16 | export type PrimaryKey = string | number; 17 | export type MutationOptions = { 18 | emitEvents?: boolean; 19 | }; 20 | export interface ItemsService { 21 | knex: Knex; 22 | accountability: Accountability | null; 23 | 24 | createOne(data: Partial, opts?: MutationOptions): Promise; 25 | createMany(data: Partial[], opts?: MutationOptions): Promise; 26 | 27 | readOne(key: PrimaryKey, query?: any, opts?: MutationOptions): Promise; 28 | readMany(keys: PrimaryKey[], query?: any, opts?: MutationOptions): Promise; 29 | readByQuery(query: any, opts?: MutationOptions): Promise; 30 | 31 | updateOne(key: PrimaryKey, data: Partial, opts?: MutationOptions): Promise; 32 | updateMany(keys: PrimaryKey[], data: Partial, opts?: MutationOptions): Promise; 33 | 34 | upsertMany(payloads: Partial[], opts?: MutationOptions): Promise; 35 | 36 | deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise; 37 | deleteMany(keys: PrimaryKey[], opts?: MutationOptions): Promise; 38 | deleteByQuery(query: any, opts?: MutationOptions): Promise; 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "lib": ["ES2022", "DOM"], 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "esModuleInterop": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noUnusedParameters": true, 16 | "alwaysStrict": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "strictBindCallApply": true, 20 | "strictPropertyInitialization": true, 21 | "resolveJsonModule": false, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "allowSyntheticDefaultImports": true, 25 | "isolatedModules": true, 26 | "rootDir": "./src" 27 | }, 28 | "include": ["./src/**/*.ts"] 29 | } 30 | --------------------------------------------------------------------------------