├── .env-example
├── .eslintrc.cjs
├── .github
└── workflows
│ └── push.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .releaserc
├── LICENSE
├── README.md
├── commitlint.config.cjs
├── package.json
├── src
├── database.ts
├── index.ts
├── property.ts
├── resource.ts
└── utils
│ ├── convert-filter.ts
│ ├── create-validation-error.ts
│ └── index.ts
├── tsconfig.json
├── types
├── database.d.ts
├── database.spec.d.ts
├── hooks.spec.d.ts
├── property.d.ts
├── property.spec.d.ts
├── resource.d.ts
├── resource.spec.d.ts
└── utils
│ ├── available-test-models.d.ts
│ ├── convert-filter.d.ts
│ └── create-validation-error.d.ts
└── yarn.lock
/.env-example:
--------------------------------------------------------------------------------
1 | NODE_ENV=test
2 | POSTGRES_USER="adminjs"
3 | POSTGRES_PASSWORD="adminjs"
4 | POSTGRES_DATABASE="adminjs-sequelize-test"
5 | POSTGRES_HOST
6 | POSTGRES_PORT
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es2020: true,
4 | },
5 | extends: [
6 | 'airbnb',
7 | 'plugin:react/recommended',
8 | 'plugin:@typescript-eslint/recommended',
9 | ],
10 | parser: '@typescript-eslint/parser',
11 | parserOptions: {
12 | ecmaFeatures: {
13 | jsx: true,
14 | },
15 | ecmaVersion: 20,
16 | sourceType: 'module',
17 | },
18 | plugins: ['react', '@typescript-eslint'],
19 | rules: {
20 | semi: ['error', 'always'],
21 | 'no-unused-vars': 'off',
22 | 'import/extensions': 'off',
23 | 'import/no-unresolved': 'off',
24 | 'react/jsx-filename-extension': 'off',
25 | indent: ['error', 2],
26 | 'linebreak-style': ['error', 'unix'],
27 | 'object-curly-newline': 'off',
28 | '@typescript-eslint/no-explicit-any': 'off',
29 | },
30 | overrides: [
31 | {
32 | files: ['*.tsx'],
33 | rules: {
34 | 'react/prop-types': 'off',
35 | 'react/jsx-props-no-spreading': 'off',
36 | 'import/no-extraneous-dependencies': 'off',
37 | },
38 | },
39 | {
40 | files: ['./src/**/*.spec.ts', 'spec/*.ts'],
41 | rules: {
42 | 'no-unused-expressions': 'off',
43 | 'prefer-arrow-callback': 'off',
44 | 'func-names': 'off',
45 | 'import/no-extraneous-dependencies': 'off',
46 | },
47 | },
48 | ],
49 | };
50 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 | on: [push, pull_request]
3 | jobs:
4 | setup:
5 | name: setup
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Dump GitHub context
9 | env:
10 | GITHUB_CONTEXT: ${{ toJson(github) }}
11 | run: echo "$GITHUB_CONTEXT"
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | - name: Setup
15 | uses: actions/setup-node@v2
16 | with:
17 | node-version: '18.x'
18 | - uses: actions/cache@v2
19 | id: yarn-cache
20 | with:
21 | path: node_modules
22 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
23 | restore-keys: |
24 | ${{ runner.os }}-node_modules-
25 | - name: Install
26 | if: steps.yarn-cache.outputs.cache-hit != 'true'
27 | run: yarn install
28 |
29 | test:
30 | name: Test
31 | runs-on: ubuntu-latest
32 | needs: setup
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v2
36 | - name: Setup
37 | uses: actions/setup-node@v2
38 | with:
39 | node-version: '18.x'
40 | - uses: actions/cache@v2
41 | id: yarn-cache
42 | with:
43 | path: node_modules
44 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
45 | restore-keys: |
46 | ${{ runner.os }}-node_modules-
47 | - name: Install
48 | if: steps.yarn-cache.outputs.cache-hit != 'true'
49 | run: yarn install
50 | - name: Lint
51 | run: yarn lint
52 | - name: Build
53 | run: yarn build
54 | - name: Release
55 | if: github.event_name == 'push'
56 | env:
57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
60 | run: yarn release
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 | out
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and not Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | # Stores VSCode versions used for testing VSCode extensions
109 | .vscode-test
110 |
111 | # yarn v2
112 | .yarn/cache
113 | .yarn/unplugged
114 | .yarn/build-state.yml
115 | .yarn/install-state.gz
116 | .pnp.*
117 |
118 | lib
119 | .vscode
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example-app
2 | migrations
3 | models
4 | .github
5 | ./src/
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | access=public
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "+([0-9])?(.{+([0-9]),x}).x",
4 | "master",
5 | "next",
6 | "next-major",
7 | {
8 | "name": "beta",
9 | "prerelease": true
10 | }
11 | ],
12 | "plugins": [
13 | "@semantic-release/commit-analyzer",
14 | "@semantic-release/release-notes-generator",
15 | "@semantic-release/npm",
16 | "@semantic-release/github",
17 | "@semantic-release/git",
18 | [
19 | "semantic-release-slack-bot",
20 | {
21 | "notifyOnSuccess": true,
22 | "notifyOnFail": false
23 | }
24 | ]
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 SoftwareBrothers.co
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## adminjs-sequelizejs
2 |
3 | This is an official [AdminJS](https://github.com/SoftwareBrothers/adminjs) adapter which integrates [sequelize ORM](http://docs.sequelizejs.com/) into AdminJS.
4 |
5 | ## Usage
6 |
7 | The plugin can be registered using standard `AdminJS.registerAdapter` method.
8 |
9 | ```javascript
10 | const AdminJS = require('adminjs')
11 | const AdminJSSequelize = require('@adminjs/sequelize')
12 |
13 | AdminJS.registerAdapter(AdminJSSequelize)
14 | ```
15 |
16 | More options can be found in [AdminJS](https://github.com/SoftwareBrothers/adminjs) repository.
17 |
18 | ## Testing
19 |
20 | Integration tests require running database. Database connection data are given in `config/config.js`. Make sure you have following env variables set: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_PORT, POSTGRES_DATABASE, POSTGRES_HOST. Take a look at `config/config.js` to see default values.
21 |
22 |
23 | Than you will have to create database and run migrations
24 |
25 | ```
26 | npm run sequelize db:create
27 | npm run sequelize db:migrate
28 | ```
29 |
30 | ## License
31 |
32 | AdminJS is copyrighted © 2023 rst.software. It is a free software, and may be redistributed under the terms specified in the [LICENSE](LICENSE.md) file.
33 |
34 | ## About rst.software
35 |
36 |
37 |
38 | We’re an open, friendly team that helps clients from all over the world to transform their businesses and create astonishing products.
39 |
40 | * We are available for [hire](https://www.rst.software/estimate-your-project).
41 | * If you want to work for us - check out the [career page](https://www.rst.software/join-us).
42 |
--------------------------------------------------------------------------------
/commitlint.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@commitlint/config-conventional',
4 | ],
5 | };
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@adminjs/sequelize",
3 | "version": "4.1.1",
4 | "description": "Sequelize adapter for AdminJS",
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "import": "./lib/index.js",
9 | "types": "./lib/index.d.ts"
10 | }
11 | },
12 | "scripts": {
13 | "build": "tsc",
14 | "dev": "tsc --watch",
15 | "check:all": "yarn build && yarn test && yarn lint",
16 | "sequelize": "sequelize",
17 | "lint": "eslint ./src/**/*.ts",
18 | "release": "semantic-release"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/SoftwareBrothers/adminjs-sequelizejs.git"
23 | },
24 | "author": "Wojciech Krysiak",
25 | "license": "MIT",
26 | "peerDependencies": {
27 | "adminjs": "^7.0.0",
28 | "sequelize": ">=6"
29 | },
30 | "husky": {
31 | "hooks": {
32 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
33 | }
34 | },
35 | "devDependencies": {
36 | "@commitlint/cli": "^17.4.4",
37 | "@commitlint/config-conventional": "^17.4.4",
38 | "@semantic-release/git": "^10.0.1",
39 | "@types/chai": "^4.3.4",
40 | "@types/mocha": "^10.0.1",
41 | "@types/node": "^18.15.3",
42 | "@types/sinon": "^10.0.13",
43 | "@types/sinon-chai": "^3.2.9",
44 | "@typescript-eslint/eslint-plugin": "^5.55.0",
45 | "@typescript-eslint/parser": "^5.55.0",
46 | "adminjs": "^7.0.0",
47 | "chai": "^4.3.7",
48 | "eslint": "^8.36.0",
49 | "eslint-config-airbnb": "^19.0.4",
50 | "eslint-plugin-import": "^2.27.5",
51 | "eslint-plugin-jsx-a11y": "^6.7.1",
52 | "eslint-plugin-react": "^7.32.2",
53 | "eslint-plugin-react-hooks": "^4.6.0",
54 | "husky": "^4.2.5",
55 | "mocha": "^10.2.0",
56 | "nyc": "^15.1.0",
57 | "pg": "^8.10.0",
58 | "semantic-release": "^20.1.3",
59 | "semantic-release-slack-bot": "^4.0.0",
60 | "sequelize": "^6.29.3",
61 | "sequelize-cli": "^6.6.0",
62 | "sinon": "^15.0.2",
63 | "sinon-chai": "^3.7.0",
64 | "ts-node": "^10.9.1",
65 | "typescript": "^4.9.5"
66 | },
67 | "dependencies": {
68 | "escape-regexp": "0.0.1"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/database.ts:
--------------------------------------------------------------------------------
1 | import { BaseDatabase, BaseResource } from 'adminjs';
2 | import { Sequelize } from 'sequelize';
3 |
4 | import Resource from './resource.js';
5 |
6 | class Database extends BaseDatabase {
7 | private sequelize: Sequelize;
8 |
9 | static isAdapterFor(database: any): boolean {
10 | return (database.sequelize
11 | && database.sequelize.constructor.name === 'Sequelize')
12 | || database.constructor.name === 'Sequelize';
13 | }
14 |
15 | constructor(database: any) {
16 | super(database);
17 | this.sequelize = database.sequelize || database;
18 | }
19 |
20 | resources(): Array {
21 | return Object.keys(this.sequelize.models).map((key) => (
22 | new Resource(this.sequelize.models[key])
23 | ));
24 | }
25 | }
26 |
27 | export default Database;
28 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module @adminjs/sequelize
3 | * @subcategory Adapters
4 | * @section modules
5 | *
6 | * @description
7 | * ### A Sequelize database adapter for AdminJS.
8 | *
9 | * #### Installation
10 | *
11 | * To install the adapter run
12 | *
13 | * ```
14 | * yarn add @adminjs/sequelize
15 | * ```
16 | *
17 | * ### Usage
18 | *
19 | * In order to use it in your project register the adapter first:
20 | *
21 | * ```javascript
22 | * const AdminJS = require('adminjs')
23 | * const AdminJSSequelize = require('@adminjs/sequelize')
24 | *
25 | * AdminJS.registerAdapter(AdminJSSequelize)
26 | * ```
27 | *
28 | * ### Passing an entire database
29 | *
30 | * Sequelize generates folder in your app called `./models` and there is an `index.js` file.
31 | * You can require it and pass to {@link AdminJSOptions} like this:
32 | *
33 | * ```javascript
34 | * const db = require('../models');
35 | * const AdminJS = new AdminJS({
36 | * databases: [db],
37 | * //... other AdminJSOptions
38 | * })
39 | * //...
40 | * ```
41 | *
42 | * ### Passing each resource
43 | *
44 | * Also you can pass a single resource and adjust it to your needs via {@link ResourceOptions}.
45 | *
46 | * So let say you have a model called `vendor` and there is a `vendor.js` file in your `./models`.
47 | * Within this file there is
48 | *
49 | * ```javascript
50 | * //...
51 | * sequelize.define('vendor', //...
52 | * //...
53 | * ```
54 | *
55 | * In order to pass it directly, run this code:
56 | *
57 | * ```javascript
58 | * const db = require('../models');
59 | * const AdminJS = new AdminJS({
60 | * databases: [db], // you can still load an entire database and adjust just one resource
61 | * resources: [{
62 | * resource: db.vendor,
63 | * options: {
64 | * //...
65 | * }
66 | * }]
67 | * })
68 | * //...
69 | * ```
70 | *
71 | *
72 | */
73 |
74 | /**
75 | * Implementation of {@link BaseDatabase} for Sequelize Adapter
76 | *
77 | * @memberof module:@adminjs/sequelize
78 | * @type {typeof BaseDatabase}
79 | * @static
80 | */
81 | import Database from './database.js';
82 |
83 | /**
84 | * Implementation of {@link BaseResource} for Sequelize Adapter
85 | *
86 | * @memberof module:@adminjs/sequelize
87 | * @type {typeof BaseResource}
88 | * @static
89 | */
90 | import Resource from './resource.js';
91 |
92 | export { default as Database } from './database.js';
93 | export { default as Resource } from './resource.js';
94 | export * from './utils/index.js';
95 |
96 | export default { Database, Resource };
97 |
--------------------------------------------------------------------------------
/src/property.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | import { BaseProperty, PropertyType } from 'adminjs';
3 | import { ModelAttributeColumnOptions } from 'sequelize';
4 |
5 | const TYPES_MAPPING = [
6 | ['STRING', 'string'],
7 | ['TEXT', 'string'],
8 | ['INTEGER', 'number'],
9 | ['BIGINT', 'number'],
10 | ['FLOAT', 'float'],
11 | ['REAL', 'float'],
12 | ['DOUBLE', 'float'],
13 | ['DECIMAL', 'float'],
14 | ['DATE', 'datetime'],
15 | ['DATEONLY', 'date'],
16 | ['ENUM', 'string'],
17 | ['ARRAY', 'array'],
18 | ['JSON', 'object'],
19 | ['JSONB', 'object'],
20 | ['BLOB', 'string'],
21 | ['UUID', 'string'],
22 | ['CIDR', 'string'],
23 | ['INET', 'string'],
24 | ['MACADDR', 'string'],
25 | ['RANGE', 'string'],
26 | ['GEOMETRY', 'string'],
27 | ['BOOLEAN', 'boolean'],
28 | ];
29 |
30 | class Property extends BaseProperty {
31 | private sequelizePath: ModelAttributeColumnOptions;
32 |
33 | private fieldName: string;
34 |
35 | constructor(sequelizePath: ModelAttributeColumnOptions) {
36 | const { fieldName } = sequelizePath as any;
37 | super({ path: fieldName });
38 | this.fieldName = fieldName;
39 | this.sequelizePath = sequelizePath;
40 | }
41 |
42 | name(): string {
43 | return this.fieldName;
44 | }
45 |
46 | isEditable(): boolean {
47 | if ((this.sequelizePath as any)._autoGenerated) {
48 | return false;
49 | }
50 | if (this.sequelizePath.autoIncrement) {
51 | return false;
52 | }
53 | if (this.isId()) {
54 | return false;
55 | }
56 | return true;
57 | }
58 |
59 | isVisible(): boolean {
60 | // fields containing password are hidden by default
61 | return !this.name().match('password');
62 | }
63 |
64 | isId(): boolean {
65 | return !!this.sequelizePath.primaryKey;
66 | }
67 |
68 | reference(): string | null {
69 | if (this.isArray()) {
70 | return null;
71 | }
72 |
73 | if (this.sequelizePath.references === 'string') {
74 | return this.sequelizePath.references as string;
75 | } if (this.sequelizePath.references && typeof this.sequelizePath.references !== 'string') {
76 | // Sequelize v7
77 | if ((this.sequelizePath.references as any).table?.tableName) {
78 | return (this.sequelizePath.references as any).table?.tableName as string;
79 | }
80 | // Sequelize v4+
81 | if (this.sequelizePath.references.model && typeof this.sequelizePath.references.model !== 'string') {
82 | return (this.sequelizePath.references?.model as any)?.tableName as string;
83 | }
84 | return this.sequelizePath.references?.model as string;
85 | }
86 | return null;
87 | }
88 |
89 | availableValues(): Array | null {
90 | return this.sequelizePath.values && this.sequelizePath.values.length
91 | ? this.sequelizePath.values as Array
92 | : null;
93 | }
94 |
95 | isArray(): boolean {
96 | return this.sequelizePath.type.constructor.name === 'ARRAY';
97 | }
98 |
99 | /**
100 | * @returns {PropertyType}
101 | */
102 | type(): PropertyType {
103 | let sequelizeType = this.sequelizePath.type;
104 |
105 | if (this.isArray()) {
106 | sequelizeType = (sequelizeType as any).type;
107 | }
108 |
109 | const key = TYPES_MAPPING.find((element) => (
110 | sequelizeType.constructor.name === element[0]
111 | ));
112 |
113 | if (this.reference()) {
114 | return 'reference' as PropertyType;
115 | }
116 |
117 | const type = key && key[1];
118 | return (type || 'string') as PropertyType;
119 | }
120 |
121 | isSortable(): boolean {
122 | return this.type() !== 'mixed' && !this.isArray();
123 | }
124 |
125 | isRequired(): boolean {
126 | return !(typeof this.sequelizePath.allowNull === 'undefined' || this.sequelizePath.allowNull);
127 | }
128 | }
129 |
130 | export default Property;
131 |
--------------------------------------------------------------------------------
/src/resource.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import { BaseProperty, BaseRecord, BaseResource, Filter, flat } from 'adminjs';
3 | import { Op, Model, ModelAttributeColumnOptions } from 'sequelize';
4 |
5 | import Property from './property.js';
6 | import convertFilter from './utils/convert-filter.js';
7 | import createValidationError from './utils/create-validation-error.js';
8 |
9 | const SEQUELIZE_VALIDATION_ERROR = 'SequelizeValidationError';
10 | const SEQUELIZE_UNIQUE_ERROR = 'SequelizeUniqueConstraintError';
11 |
12 | // this fixes problem with unbound this when you setup type of Mode as a member of another
13 | // class: https://stackoverflow.com/questions/55166230/sequelize-typescript-typeof-model
14 | type Constructor = new (...args: any[]) => T;
15 | export type ModelType> = Constructor & typeof Model;
16 |
17 | type FindOptions = {
18 | limit?: number;
19 | offset?: number;
20 | sort?: {
21 | sortBy?: string;
22 | direction?: 'asc' | 'desc';
23 | };
24 | }
25 |
26 | class Resource extends BaseResource {
27 | private SequelizeModel: ModelType;
28 |
29 | static isAdapterFor(rawResource): boolean {
30 | return rawResource.sequelize && rawResource.sequelize.constructor.name === 'Sequelize';
31 | }
32 |
33 | constructor(SequelizeModel: typeof Model) {
34 | super(SequelizeModel);
35 | this.SequelizeModel = SequelizeModel as ModelType;
36 | }
37 |
38 | rawAttributes(): Record {
39 | // different sequelize versions stores attributes in different places
40 | // .modelDefinition.attributes => sequelize ^7.0.0
41 | // .rawAttributes => sequelize ^5.0.0
42 | // .attributes => sequelize ^4.0.0
43 | return ((this.SequelizeModel as any).attributes
44 | || ((this.SequelizeModel as any).modelDefinition?.attributes && Object.fromEntries((this.SequelizeModel as any).modelDefinition?.attributes))
45 | || (this.SequelizeModel as any).rawAttributes) as Record;
46 | }
47 |
48 | databaseName(): string {
49 | return (this.SequelizeModel.sequelize as any).options.database
50 | || (this.SequelizeModel.sequelize as any).options.host
51 | || 'Sequelize';
52 | }
53 |
54 | databaseType(): string {
55 | return (this.SequelizeModel.sequelize as any).options.dialect || 'other';
56 | }
57 |
58 | name(): string {
59 | // different sequelize versions stores attributes in different places
60 | // .modelDefinition.table => sequelize ^7.0.0
61 | // .tableName => sequelize ^4.0.0
62 | return (
63 | (this.SequelizeModel as any).modelDefinition?.table?.tableName
64 | || this.SequelizeModel.tableName
65 | );
66 | }
67 |
68 | id(): string {
69 | return this.name();
70 | }
71 |
72 | properties(): Array {
73 | return Object.keys(this.rawAttributes()).map((key) => (
74 | new Property(this.rawAttributes()[key])
75 | ));
76 | }
77 |
78 | property(path: string): BaseProperty | null {
79 | const nested = path.split('.');
80 |
81 | // if property is an array return the array property
82 | if (nested.length > 1 && this.rawAttributes()[nested[0]]) {
83 | return new Property(this.rawAttributes()[nested[0]]);
84 | }
85 |
86 | if (!this.rawAttributes()[path]) {
87 | return null;
88 | }
89 | return new Property(this.rawAttributes()[path]);
90 | }
91 |
92 | async count(filter: Filter) {
93 | return this.SequelizeModel.count(({
94 | where: convertFilter(filter),
95 | }));
96 | }
97 |
98 | primaryKey(): string {
99 | return (this.SequelizeModel as any).primaryKeyField || this.SequelizeModel.primaryKeyAttribute;
100 | }
101 |
102 | async populate(baseRecords, property): Promise> {
103 | const ids = baseRecords.map((baseRecord) => (
104 | baseRecord.param(property.name())
105 | ));
106 | const records = await this.SequelizeModel.findAll({
107 | where: { [this.primaryKey()]: ids },
108 | });
109 | const recordsHash = records.reduce((memo, record) => {
110 | memo[record[this.primaryKey()]] = record;
111 | return memo;
112 | }, {});
113 | baseRecords.forEach((baseRecord) => {
114 | const id = baseRecord.param(property.name());
115 | if (recordsHash[id]) {
116 | const referenceRecord = new BaseRecord(
117 | recordsHash[id].toJSON(),
118 | this,
119 | );
120 | baseRecord.populated[property.name()] = referenceRecord;
121 | }
122 | });
123 | return baseRecords;
124 | }
125 |
126 | async find(filter, { limit = 20, offset = 0, sort = {} }: FindOptions) {
127 | const { direction, sortBy } = sort;
128 | const sequelizeObjects = await this.SequelizeModel
129 | .findAll({
130 | where: convertFilter(filter),
131 | limit,
132 | offset,
133 | order: [[sortBy as string, (direction || 'asc').toUpperCase()]],
134 | });
135 | return sequelizeObjects.map(
136 | (sequelizeObject) => new BaseRecord(sequelizeObject.toJSON(), this),
137 | );
138 | }
139 |
140 | async findOne(id): Promise {
141 | const sequelizeObject = await this.findById(id);
142 | if (!sequelizeObject) {
143 | return null;
144 | }
145 | return new BaseRecord(sequelizeObject.toJSON(), this);
146 | }
147 |
148 | async findMany(ids) {
149 | const sequelizeObjects = await this.SequelizeModel.findAll({
150 | where: {
151 | [this.primaryKey()]: { [Op.in]: ids },
152 | },
153 | });
154 | return sequelizeObjects.map(
155 | (sequelizeObject) => new BaseRecord(sequelizeObject.toJSON(), this),
156 | );
157 | }
158 |
159 | async findById(id) {
160 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
161 | // @ts-ignore versions of Sequelize before 5 had findById method - after that there was findByPk
162 | const method = this.SequelizeModel.findByPk ? 'findByPk' : 'findById';
163 | return this.SequelizeModel[method](id);
164 | }
165 |
166 | async create(params): Promise> {
167 | const parsedParams = this.parseParams(params);
168 | const unflattedParams = flat.unflatten(parsedParams);
169 | try {
170 | const record = await this.SequelizeModel.create(unflattedParams);
171 | return record.toJSON();
172 | } catch (error) {
173 | if (error.name === SEQUELIZE_VALIDATION_ERROR) {
174 | throw createValidationError(error);
175 | }
176 | if (error.name === SEQUELIZE_UNIQUE_ERROR) {
177 | throw createValidationError(error);
178 | }
179 | throw error;
180 | }
181 | }
182 |
183 | async update(id, params) {
184 | const parsedParams = this.parseParams(params);
185 | const unflattedParams = flat.unflatten(parsedParams);
186 | try {
187 | await this.SequelizeModel.update(unflattedParams, {
188 | where: {
189 | [this.primaryKey()]: id,
190 | },
191 | individualHooks: true,
192 | hooks: true,
193 | });
194 | const record = await this.findById(id);
195 | return record.toJSON();
196 | } catch (error) {
197 | if (error.name === SEQUELIZE_VALIDATION_ERROR) {
198 | throw createValidationError(error);
199 | }
200 | if (error.name === SEQUELIZE_UNIQUE_ERROR) {
201 | throw createValidationError(error);
202 | }
203 | throw error;
204 | }
205 | }
206 |
207 | async delete(id): Promise {
208 | // we find first because we need to invoke destroy on model, so all hooks
209 | // instance hooks (not bulk) are called.
210 | // We cannot set {individualHooks: true, hooks: false} in this.SequelizeModel.destroy,
211 | // as it is in #update method because for some reason it wont delete the record
212 | const model = await this.SequelizeModel.findByPk(id);
213 | await model.destroy();
214 | }
215 |
216 | /**
217 | * Check all params against values they hold. In case of wrong value it corrects it.
218 | *
219 | * What it does exactly:
220 | * - removes keys with empty strings for the `number`, `float` and 'reference' properties.
221 | *
222 | * @param {Object} params received from AdminJS form
223 | *
224 | * @return {Object} converted params
225 | */
226 | parseParams(params) {
227 | const parsedParams = { ...params };
228 | this.properties().forEach((property) => {
229 | const value = parsedParams[property.name()];
230 | if (value === '') {
231 | if (property.isArray() || property.type() !== 'string') {
232 | delete parsedParams[property.name()];
233 | }
234 | }
235 | });
236 | return parsedParams;
237 | }
238 | }
239 |
240 | export default Resource;
241 |
--------------------------------------------------------------------------------
/src/utils/convert-filter.ts:
--------------------------------------------------------------------------------
1 | import escape from 'escape-regexp';
2 | import { Op } from 'sequelize';
3 |
4 | export const uuidRegex = /^[0-9A-F]{8}-[0-9A-F]{4}-[5|4|3|2|1][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
5 |
6 | export const convertFilter = (filter) => {
7 | if (!filter) {
8 | return {};
9 | }
10 | return filter.reduce((memo, filterProperty) => {
11 | const { property, value, path: filterPath } = filterProperty;
12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
13 | const [_, index] = filterPath.split('.');
14 | const isArray = typeof index !== 'undefined' && !Number.isNaN(Number(index));
15 | const previousValue = memo[property.name()] || {};
16 | switch (property.type()) {
17 | case 'string': {
18 | if (property.sequelizePath.values || uuidRegex.test(value.toString())) {
19 | return {
20 | [property.name()]: { [Op.eq]: `${escape(value)}` },
21 | ...memo,
22 | };
23 | }
24 | if (isArray) {
25 | return {
26 | ...memo,
27 | [property.name()]: {
28 | [Op.in]: [...(previousValue[Op.in] || []), escape(value)],
29 | },
30 | };
31 | }
32 | return {
33 | ...memo,
34 | [Op.and]: [
35 | ...(memo[Op.and] || []),
36 | {
37 | [property.name()]: {
38 | [(Op.like as unknown) as string]: `%${escape(value)}%`,
39 | },
40 | },
41 | ],
42 | };
43 | }
44 | case 'boolean': {
45 | let bool;
46 | if (value === 'true') bool = true;
47 | if (value === 'false') bool = false;
48 | if (bool === undefined) return memo;
49 |
50 | if (isArray) {
51 | return {
52 | ...memo,
53 | [property.name()]: {
54 | [Op.in]: [
55 | ...(previousValue[Op.in] || []),
56 | bool,
57 | ],
58 | },
59 | };
60 | }
61 | return {
62 | ...memo,
63 | [property.name()]: bool,
64 | };
65 | }
66 | case 'number': {
67 | if (!Number.isNaN(Number(value))) {
68 | if (isArray) {
69 | return {
70 | ...memo,
71 | [property.name()]: {
72 | [Op.in]: [...(previousValue[Op.in] || []), Number(value)],
73 | },
74 | };
75 | }
76 | return {
77 | [property.name()]: Number(value),
78 | ...memo,
79 | };
80 | }
81 | return memo;
82 | }
83 | case 'date':
84 | case 'datetime': {
85 | if (value.from || value.to) {
86 | return {
87 | [property.name()]: {
88 | ...(value.from && { [Op.gte]: value.from }),
89 | ...(value.to && { [Op.lte]: value.to }),
90 | },
91 | ...memo,
92 | };
93 | }
94 | break;
95 | }
96 | default:
97 | break;
98 | }
99 | if (isArray) {
100 | return {
101 | ...memo,
102 | [property.name()]: {
103 | [Op.in]: [...(previousValue[Op.in] || []), value],
104 | },
105 | };
106 | }
107 | return {
108 | [property.name()]: value,
109 | ...memo,
110 | };
111 | }, {});
112 | };
113 |
114 | export default convertFilter;
115 |
--------------------------------------------------------------------------------
/src/utils/create-validation-error.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from 'adminjs';
2 |
3 | export const createValidationError = (originalError: any): ValidationError => {
4 | const errors = Object.keys(originalError.errors).reduce((memo, key) => {
5 | const { path, message, validatorKey } = originalError.errors[key];
6 | memo[path] = { message, kind: validatorKey }; // eslint-disable-line no-param-reassign
7 | return memo;
8 | }, {});
9 | return new ValidationError(errors);
10 | };
11 |
12 | export default createValidationError;
13 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './convert-filter.js';
2 | export * from './create-validation-error.js';
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "preserve",
4 | "strictNullChecks": true,
5 | "strictPropertyInitialization": true,
6 | "strictFunctionTypes": true,
7 | "strictBindCallApply": true,
8 | "noImplicitThis": true,
9 | "moduleResolution": "nodenext",
10 | "module": "nodenext",
11 | "baseUrl": "./src",
12 | "skipLibCheck": true,
13 | "target": "esnext",
14 | "sourceMap": false,
15 | "outDir": "lib",
16 | "esModuleInterop": true,
17 | "allowSyntheticDefaultImports": true,
18 | "declaration": true,
19 | "allowJs": true
20 | },
21 | "exclude": [
22 | "node_modules",
23 | "lib",
24 | "spec",
25 | "commitlint.config.cjs",
26 | "**/*.spec.ts",
27 | "models",
28 | "migrations",
29 | "config",
30 | "example-app"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/types/database.d.ts:
--------------------------------------------------------------------------------
1 | import { BaseDatabase, BaseResource } from 'adminjs';
2 | declare class Database extends BaseDatabase {
3 | private sequelize;
4 | static isAdapterFor(database: any): boolean;
5 | constructor(database: any);
6 | resources(): Array;
7 | }
8 | export default Database;
9 |
--------------------------------------------------------------------------------
/types/database.spec.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/types/hooks.spec.d.ts:
--------------------------------------------------------------------------------
1 | declare const db: any;
2 |
--------------------------------------------------------------------------------
/types/property.d.ts:
--------------------------------------------------------------------------------
1 | import { BaseProperty, PropertyType } from 'adminjs';
2 | import { ModelAttributeColumnOptions } from 'sequelize/types';
3 | declare class Property extends BaseProperty {
4 | private sequelizePath;
5 | private fieldName;
6 | constructor(sequelizePath: ModelAttributeColumnOptions);
7 | name(): string;
8 | isEditable(): boolean;
9 | isVisible(): boolean;
10 | isId(): boolean;
11 | reference(): string | null;
12 | availableValues(): Array | null;
13 | isArray(): boolean;
14 | /**
15 | * @returns {PropertyType}
16 | */
17 | type(): PropertyType;
18 | isSortable(): boolean;
19 | isRequired(): boolean;
20 | }
21 | export default Property;
22 |
--------------------------------------------------------------------------------
/types/property.spec.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/types/resource.d.ts:
--------------------------------------------------------------------------------
1 | import { BaseResource, BaseRecord, BaseProperty, Filter } from 'adminjs';
2 | import { Model, ModelAttributeColumnOptions } from 'sequelize/types';
3 | declare type Constructor = new (...args: any[]) => T;
4 | export declare type ModelType> = Constructor & typeof Model;
5 | declare type FindOptions = {
6 | limit?: number;
7 | offset?: number;
8 | sort?: {
9 | sortBy?: string;
10 | direction?: 'asc' | 'desc';
11 | };
12 | };
13 | declare class Resource extends BaseResource {
14 | private SequelizeModel;
15 | static isAdapterFor(rawResource: any): boolean;
16 | constructor(SequelizeModel: typeof Model);
17 | rawAttributes(): Record;
18 | databaseName(): string;
19 | databaseType(): string;
20 | name(): string;
21 | id(): string;
22 | properties(): Array;
23 | property(path: string): BaseProperty | null;
24 | count(filter: Filter): Promise;
25 | primaryKey(): string;
26 | populate(baseRecords: any, property: any): Promise>;
27 | find(filter: any, { limit, offset, sort }: FindOptions): any;
28 | findOne(id: any): Promise;
29 | findMany(ids: any): Promise;
30 | findById(id: any): Promise;
31 | create(params: any): Promise>;
32 | update(id: any, params: any): Promise;
33 | delete(id: any): Promise;
34 | /**
35 | * Check all params against values they hold. In case of wrong value it corrects it.
36 | *
37 | * What it does exactly:
38 | * - removes keys with empty strings for the `number`, `float` and 'reference' properties.
39 | *
40 | * @param {Object} params received from AdminJS form
41 | *
42 | * @return {Object} converted params
43 | */
44 | parseParams(params: any): any;
45 | }
46 | export default Resource;
47 |
--------------------------------------------------------------------------------
/types/resource.spec.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/types/utils/available-test-models.d.ts:
--------------------------------------------------------------------------------
1 | export declare type AvailableTestModels = 'User' | 'Post' | 'Comment';
2 |
--------------------------------------------------------------------------------
/types/utils/convert-filter.d.ts:
--------------------------------------------------------------------------------
1 | declare const convertFilter: (filter: any) => any;
2 | export default convertFilter;
3 |
--------------------------------------------------------------------------------
/types/utils/create-validation-error.d.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from 'adminjs';
2 | declare const createValidationError: (originalError: any) => ValidationError;
3 | export default createValidationError;
4 |
--------------------------------------------------------------------------------