├── .dockerignore
├── .env
├── .eslintrc.json
├── .github
└── workflows
│ └── push.yml
├── .gitignore
├── .prettierrc
├── .sequelizerc
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── docker-compose.yaml
├── nodemon.json
├── package.json
├── prisma
├── migrations
│ ├── 20220713101214_init
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── custom.css
├── favicon.ico
├── gtm.js
└── locales
│ ├── en
│ └── translation.json
│ └── mk
│ └── translation.json
├── src
├── admin
│ ├── admin.utils.ts
│ ├── components.bundler.ts
│ ├── components
│ │ ├── debug.tsx
│ │ ├── design-system-examples
│ │ │ ├── index.ts
│ │ │ └── modal-example.tsx
│ │ ├── detailed-stats.tsx
│ │ ├── dont-touch-this-action.tsx
│ │ ├── login.tsx
│ │ ├── products-list.tsx
│ │ ├── sidebar-resource-section.tsx
│ │ ├── thumb.tsx
│ │ └── top-bar.tsx
│ ├── constants
│ │ └── authUsers.ts
│ ├── features
│ │ └── useEnvironmentVariableToDisableActions.ts
│ ├── index.ts
│ ├── locale
│ │ ├── de
│ │ │ ├── common.json
│ │ │ ├── components.json
│ │ │ ├── index.ts
│ │ │ └── pages.json
│ │ ├── en
│ │ │ ├── common.json
│ │ │ ├── complicated.json
│ │ │ ├── components.json
│ │ │ ├── index.ts
│ │ │ ├── pages.json
│ │ │ └── person.json
│ │ └── index.ts
│ ├── pages
│ │ ├── custom-page.tsx
│ │ ├── design-system-examples
│ │ │ ├── blog-page.tsx
│ │ │ ├── buttons-page.tsx
│ │ │ ├── form-page.tsx
│ │ │ ├── icons-page.tsx
│ │ │ ├── illustrations-page.tsx
│ │ │ ├── index.tsx
│ │ │ ├── messages-page.tsx
│ │ │ ├── modal-page.tsx
│ │ │ ├── tabs-page.tsx
│ │ │ └── typography-page.tsx
│ │ └── index.ts
│ ├── router.ts
│ └── types
│ │ └── index.ts
├── index.ts
├── scripts
│ ├── truncate-mongodb.ts
│ └── truncate-postgres.ts
├── servers
│ ├── express
│ │ └── index.ts
│ ├── fastify.ts
│ ├── hapijs.ts
│ └── nestjs
│ │ ├── admin
│ │ └── admin.setup.ts
│ │ ├── app.controller.ts
│ │ ├── app.module.ts
│ │ ├── app.service.ts
│ │ ├── database.providers.ts
│ │ ├── index.ts
│ │ ├── mongoose
│ │ └── mongoose.module.ts
│ │ └── prisma
│ │ └── prisma.service.ts
├── sources
│ ├── mikroorm
│ │ ├── config.ts
│ │ ├── migrations
│ │ │ └── Migration20220714094312.ts
│ │ ├── models
│ │ │ ├── car.model.ts
│ │ │ ├── index.ts
│ │ │ ├── owner.model.ts
│ │ │ └── seller.model.ts
│ │ ├── resources
│ │ │ ├── car.resource.ts
│ │ │ ├── index.ts
│ │ │ ├── owner.resource.ts
│ │ │ └── seller.resource.ts
│ │ └── seeds
│ │ │ ├── data
│ │ │ ├── cars.ts
│ │ │ ├── index.ts
│ │ │ ├── owners.ts
│ │ │ └── sellers.ts
│ │ │ └── run.ts
│ ├── mongoose
│ │ ├── models
│ │ │ ├── admin.model.ts
│ │ │ ├── article.model.ts
│ │ │ ├── category.model.ts
│ │ │ ├── comment.model.ts
│ │ │ ├── complicated.model.ts
│ │ │ ├── index.ts
│ │ │ └── user.model.ts
│ │ ├── resources
│ │ │ ├── admin.resource.ts
│ │ │ ├── article.resource.ts
│ │ │ ├── category.resource.ts
│ │ │ ├── comment.resource.ts
│ │ │ ├── complicated.resource.ts
│ │ │ ├── index.ts
│ │ │ └── user.resource.ts
│ │ └── seeds
│ │ │ ├── data
│ │ │ ├── articles.ts
│ │ │ ├── categories.ts
│ │ │ ├── comments.ts
│ │ │ ├── index.ts
│ │ │ └── users.ts
│ │ │ └── run.ts
│ ├── objectionjs
│ │ ├── knexfile.cjs
│ │ ├── migrations
│ │ │ └── 20220826123456_initial_schema.cjs
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ ├── manager.entity.ts
│ │ │ └── office.entity.ts
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── manager.resource.ts
│ │ │ └── office.resource.ts
│ │ └── utils
│ │ │ └── base-model.ts
│ ├── prisma
│ │ ├── config.ts
│ │ ├── migrations
│ │ │ ├── 20220218111814_init
│ │ │ │ └── migration.sql
│ │ │ └── migration_lock.toml
│ │ ├── resources
│ │ │ ├── index.ts
│ │ │ ├── post.resource.ts
│ │ │ ├── profile.resource.ts
│ │ │ └── publisher.resource.ts
│ │ └── seeds
│ │ │ ├── data
│ │ │ ├── index.ts
│ │ │ ├── posts.ts
│ │ │ ├── profiles.ts
│ │ │ └── publishers.ts
│ │ │ └── run.ts
│ ├── rest
│ │ └── crypto-database.ts
│ ├── sequelize
│ │ ├── config.js
│ │ ├── functions
│ │ │ └── get-sum.function.ts
│ │ ├── hooks
│ │ │ ├── get-products.hook.ts
│ │ │ └── get-sum.hook.ts
│ │ ├── index.ts
│ │ ├── interfaces.ts
│ │ ├── migrations
│ │ │ └── 20220214101918-init.cjs
│ │ ├── models
│ │ │ ├── cart.model.ts
│ │ │ ├── category.model.ts
│ │ │ ├── index.ts
│ │ │ ├── order.model.ts
│ │ │ └── product.model.ts
│ │ ├── resources
│ │ │ ├── cart.resource.ts
│ │ │ ├── category.resource.ts
│ │ │ ├── index.ts
│ │ │ ├── order.resource.ts
│ │ │ └── product.resource.ts
│ │ └── seeds
│ │ │ ├── data
│ │ │ ├── carts.ts
│ │ │ ├── categories.ts
│ │ │ ├── index.ts
│ │ │ ├── orders.ts
│ │ │ └── products.ts
│ │ │ └── run.ts
│ └── typeorm
│ │ ├── config.ts
│ │ ├── enums
│ │ └── country.enum.ts
│ │ ├── handlers
│ │ └── validate-email.handler.ts
│ │ ├── interfaces.ts
│ │ ├── migrations
│ │ └── 1644569575919-init.ts
│ │ ├── models
│ │ ├── index.ts
│ │ ├── organization.entity.ts
│ │ └── person.entity.ts
│ │ ├── resources
│ │ ├── index.ts
│ │ ├── organization.resource.ts
│ │ └── person.resource.ts
│ │ └── seeds
│ │ ├── data
│ │ ├── index.ts
│ │ ├── organizations.ts
│ │ └── persons.ts
│ │ └── run.ts
└── themes
│ ├── custom-theme
│ ├── components
│ │ ├── LoggedIn.tsx
│ │ └── SidebarFooter.tsx
│ ├── index.ts
│ ├── overrides.ts
│ ├── style.css
│ └── theme.bundle.js
│ └── index.ts
├── tsconfig.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | SERVER="EXPRESS" # EXPRESS HAPIJS FASTIFY NESTJS
3 | SESSION_SECRET=s3sS10n_secr3t
4 | LOCALE=en
5 |
6 | MONGO_DATABASE_URL=mongodb://localhost:27017/adminjs-example-app
7 | POSTGRES_DATABASE_URL=postgres://adminjs:adminjs@localhost:5435/adminjs
8 | MYSQL_DATABASE_URL=mysql://root:adminjs@localhost:3308/adminjs?schema=public
9 | DATABASE_LOGGING=true
10 | DATABASE_SYNC=false
11 | DISABLE_ADMINJS_ACTIONS=false
12 |
13 | GITHUB_URL=https://github.com/SoftwareBrothers/adminjs/issues
14 | SLACK_URL=https://adminjs.page.link/slack
15 | DOCUMENTATION_URL=https://adminjs.co
16 | STORYBOOK_URL=https://adminjs-storybook-beta.web.app/
17 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "jest": true,
5 | "node": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "project": "tsconfig.json",
10 | "sourceType": "module",
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "ecmaVersion": 2022
15 | },
16 | "plugins": [
17 | "@typescript-eslint",
18 | "react",
19 | "prettier"
20 | ],
21 | "extends": [
22 | "eslint:recommended",
23 | "plugin:@typescript-eslint/eslint-recommended",
24 | "plugin:@typescript-eslint/recommended",
25 | "plugin:react/recommended",
26 | "plugin:react/jsx-runtime",
27 | "plugin:react-hooks/recommended",
28 | "prettier"
29 | ],
30 | "rules": {
31 | "prettier/prettier": "error",
32 | "@typescript-eslint/no-var-requires": "off",
33 | "@typescript-eslint/ban-ts-comment": "off",
34 | "@typescript-eslint/no-explicit-any": "off",
35 | "react/jsx-uses-react": "error"
36 | },
37 | "settings": {
38 | "react": {
39 | "version": "detect"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 | on: push
3 |
4 | jobs:
5 | test:
6 | name: test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 | - name: Setup
12 | uses: actions/setup-node@v2
13 | with:
14 | node-version: "18.x"
15 | - uses: actions/cache@v2
16 | id: yarn-cache
17 | with:
18 | path: node_modules
19 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
20 | restore-keys: |
21 | ${{ runner.os }}-node_modules-
22 | - name: Install
23 | if: steps.yarn-cache.outputs.cache-hit != 'true'
24 | run: yarn install
25 | - name: Lint
26 | run: yarn lint
27 | - name: Build
28 | run: yarn build
29 |
--------------------------------------------------------------------------------
/.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 | # parcel-bundler cache (https://parceljs.org/)
72 | .cache
73 | .parcel-cache
74 |
75 | # Next.js build output
76 | .next
77 | out
78 |
79 | # Nuxt.js build / generate output
80 | .nuxt
81 | dist
82 |
83 | # Gatsby files
84 | .cache/
85 | # Comment in the public line in if your project uses Gatsby and not Next.js
86 | # https://nextjs.org/blog/next-9-1#public-directory-support
87 | # public
88 |
89 | # vuepress build output
90 | .vuepress/dist
91 |
92 | # Serverless directories
93 | .serverless/
94 |
95 | # FuseBox cache
96 | .fusebox/
97 |
98 | # DynamoDB Local files
99 | .dynamodb/
100 |
101 | # TernJS port file
102 | .tern-port
103 |
104 | # Stores VSCode versions used for testing VSCode extensions
105 | .vscode-test
106 |
107 | # yarn v2
108 | .yarn/cache
109 | .yarn/unplugged
110 | .yarn/build-state.yml
111 | .yarn/install-state.gz
112 | .pnp.*
113 |
114 | node_modules
115 | .nyc_output
116 | coverage
117 | .DS_store
118 | .adminjs
119 | .adminbro
120 | .idea
121 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "printWidth": 120,
5 | "trailingComma": "all"
6 | }
7 |
--------------------------------------------------------------------------------
/.sequelizerc:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | require('dotenv').config();
3 |
4 | module.exports = {
5 | 'config': path.resolve('src', 'sources', 'sequelize', 'config.js'),
6 | 'models-path': path.resolve('src', 'sources', 'sequelize', 'models'),
7 | 'seeders-path': path.resolve('src', 'sources', 'sequelize', 'seeders'),
8 | 'migrations-path': path.resolve('src', 'sources', 'sequelize', 'migrations')
9 | }
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 |
3 | WORKDIR /admin
4 |
5 | ENV NODE_ENV="production"
6 | ENV TZ="UTC"
7 |
8 | COPY package.json ./
9 | COPY yarn.lock ./
10 |
11 | RUN yarn install --frozen-lockfile --production
12 | COPY . .
13 |
14 | RUN npm i -g typescript
15 | RUN yarn build
16 | RUN npx prisma generate
17 | RUN rm -rf src
18 |
19 | ENV ADMIN_JS_SKIP_BUNDLE="true"
20 |
21 | EXPOSE 3000
22 | CMD yarn start
23 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | release: yarn migration:up:production
2 | web: yarn start
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## AdminJS-example-app
2 |
3 | Example application using [adminjs](https://github.com/SoftwareBrothers/adminjs)
4 |
5 | ## Demo
6 |
7 | You can check out the current demo version at: https://demo.adminjs.co
8 |
9 | Login: admin@example.com
10 | Password: password
11 |
12 | ## Prerequisites
13 |
14 | Install Docker if you don't have it: https://docs.docker.com/desktop/#download-and-install
15 |
16 | Run:
17 | ```bash
18 | $ docker-compose up -d
19 | ```
20 | to setup databases.
21 |
22 | Make sure your `.env` file is configured. If you didn't do any changes to `docker-compose.yml` file,
23 | the default contents of the `.env` file should work for you.
24 |
25 | ## Starting the app
26 |
27 | First, install all dependencies
28 |
29 | ```bash
30 | yarn install --frozen-lockfile
31 | ```
32 |
33 | Make sure you have all environmental variables set up (read the previous paragraph).
34 |
35 | Then create postgres database and run migrations:
36 |
37 | ```bash
38 | $ npx prisma generate # # this sets up Prisma Client in your node_modules
39 | $ yarn migration:up
40 | ```
41 |
42 | Note: If you see the error below when Prisma MySQL migration is run:
43 | ```
44 | Error: P1017: Server has closed the connection.
45 | ```
46 | Please wait a minute or two for the MySQL server to start and retry.
47 |
48 | In the end, you can launch the app
49 |
50 | ```bash
51 | $ yarn build:watch # keep it running if developing
52 | $ yarn start:dev # in a separate terminal tab, concurrently
53 | ```
54 |
55 | By default the app will be available under: `http://localhost:3000/admin`
56 |
57 | ## Developing the app
58 |
59 | The best way of developing the app is to do this via https://github.com/SoftwareBrothers/adminjs-dev.
60 |
61 | Alternatively, you can fork and clone each repository separately and link them using:
62 |
63 | * `yarn link`
64 | * `npm link`
65 |
66 | to see your local changes.
67 |
68 | #### Sequelize
69 | ##### migrations
70 | - `yarn sequelize migration:generate --name init`
71 | - `yarn sequelize db:migrate`
72 | - `yarn sequelize db:migrate:undo`
73 |
74 | #### Typeorm
75 | ##### migrations
76 | - `yarn typeorm migration:generate -n init`
77 | - `yarn typeorm migration:run`
78 | - `yarn typeorm migration:revert`
79 |
80 |
81 | #### mikro-orm
82 | ##### migrations
83 | - `yarn mikro-orm migration:create`
84 | - `yarn mikro-orm migration:up`
85 | - `yarn mikro-orm migration:down`
86 |
87 | #### prisma
88 | - `npx prisma migrate dev --schema prisma/schema.prisma`
89 |
90 | ## License
91 |
92 | 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.
93 |
94 | ## About rst.software
95 |
96 |
97 |
98 | We’re an open, friendly team that helps clients from all over the world to transform their businesses and create astonishing products.
99 |
100 | * We are available for [hire](https://www.rst.software/estimate-your-project).
101 | * If you want to work for us - check out the [career page](https://www.rst.software/join-us).
102 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | mongo_db:
5 | container_name: adminjs-example-mongo
6 | image: mongo
7 | ports:
8 | - "27017:27017"
9 | volumes:
10 | - mongo_db_example_app:/data/db
11 |
12 | postgres_db:
13 | container_name: adminjs-example-postgres
14 | image: postgres
15 | environment:
16 | - POSTGRES_DB=adminjs
17 | - POSTGRES_USER=adminjs
18 | - POSTGRES_PASSWORD=adminjs
19 | ports:
20 | - "5435:5432"
21 | volumes:
22 | - postgres_db_example_app:/var/lib/postgresql/data
23 |
24 | mysql_db:
25 | container_name: adminjs-example-mysql
26 | image: mysql
27 | environment:
28 | MYSQL_USER: adminjs
29 | MYSQL_PASSWORD: adminjs
30 | MYSQL_ROOT_PASSWORD: adminjs
31 | MYSQL_DATABASE: adminjs
32 | ports:
33 | - "3308:3306"
34 | volumes:
35 | - mysql_db_example_app:/var/lib/mysql
36 |
37 | volumes:
38 | mongo_db_example_app:
39 | postgres_db_example_app:
40 | mysql_db_example_app:
41 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [
3 | "dist"
4 | ],
5 | "ext": "js,json",
6 | "ignore": [
7 | "*.spec.*",
8 | "test"
9 | ],
10 | "exec": "node ./dist/index.js"
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "",
6 | "type": "module",
7 | "license": "ISC",
8 | "engines": {
9 | "node": ">=18"
10 | },
11 | "scripts": {
12 | "lint": "eslint src --ext .ts,.tsx",
13 | "typeorm": "./node_modules/typeorm/cli.js --dataSource dist/sources/typeorm/config.js",
14 | "sequelize": "yarn sequelize-cli --config src/sources/sequelize/config.js",
15 | "knex": "dotenv -c '.env' -- npx knex --knexpath dist/sources/objectionjs/index.js --knexfile dist/sources/objectionjs/knexfile.cjs",
16 | "mikroorm": "node --experimental-specifier-resolution=node --loader ts-node/esm ./node_modules/.bin/mikro-orm",
17 | "migration:up": "yarn sequelize db:migrate && yarn typeorm migration:run && yarn mikroorm migration:up && yarn prisma migrate dev --schema prisma/schema.prisma && yarn knex migrate:latest",
18 | "migration:up:production": "yarn sequelize db:migrate && yarn typeorm migration:run && yarn mikro-orm migration:up && yarn prisma migrate deploy --schema prisma/schema.prisma",
19 | "docker:up": "docker-compose -f docker-compose.yaml up -d",
20 | "start:dev": "nodemon",
21 | "start": "node dist/index.js",
22 | "build": "yarn clean && tsc && yarn copy-theme",
23 | "build:watch": "tsc -w",
24 | "postgres:clean": "node dist/scripts/truncate-postgres",
25 | "mysql:clean": "prisma migrate reset --force && prisma migrate deploy",
26 | "mongodb:clean": "node dist/scripts/truncate-mongodb",
27 | "db:clean": "yarn postgres:clean && yarn mysql:clean && yarn mongodb:clean",
28 | "seed:typeorm": "node dist/sources/typeorm/seeds/run",
29 | "seed:sequelize": "node dist/sources/sequelize/seeds/run",
30 | "seed:mongoose": "node dist/sources/mongoose/seeds/run",
31 | "seed:mikro-orm": "node dist/sources/mikroorm/seeds/run",
32 | "seed:prisma": "node dist/sources/prisma/seeds/run",
33 | "seed:all": "yarn seed:typeorm && yarn seed:sequelize && yarn seed:mongoose && yarn seed:mikro-orm && yarn seed:prisma",
34 | "clean": "rimraf dist",
35 | "copy-theme": "copyfiles -u 1 src/themes/**/theme.bundle.js src/themes/**/*.css dist/"
36 | },
37 | "mikro-orm": {
38 | "useTsNode": true,
39 | "configPaths": [
40 | "./dist/sources/mikroorm/config.js",
41 | "./src/sources/mikroorm/config.ts"
42 | ]
43 | },
44 | "prisma": {
45 | "schema": "prisma/schema.prisma"
46 | },
47 | "dependencies": {
48 | "@adminjs/express": "^6.0.0",
49 | "@adminjs/fastify": "^4.0.0",
50 | "@adminjs/hapi": "^7.0.0",
51 | "@adminjs/koa": "^4.0.0",
52 | "@adminjs/mikroorm": "^3.0.0",
53 | "@adminjs/mongoose": "^4.0.0",
54 | "@adminjs/objection": "^2.0.0",
55 | "@adminjs/passwords": "^4.0.0",
56 | "@adminjs/prisma": "^4.0.0",
57 | "@adminjs/sequelize": "^4.0.0",
58 | "@adminjs/themes": "^1.0.0",
59 | "@adminjs/typeorm": "^5.0.0",
60 | "@faker-js/faker": "^7.6.0",
61 | "@hapi/boom": "^10.0.1",
62 | "@hapi/cookie": "^12.0.1",
63 | "@hapi/hapi": "^21.3.0",
64 | "@koa/router": "^12.0.0",
65 | "@mikro-orm/cli": "^5.6.15",
66 | "@mikro-orm/core": "^5.6.15",
67 | "@mikro-orm/migrations": "^5.6.15",
68 | "@mikro-orm/nestjs": "^5.1.7",
69 | "@mikro-orm/postgresql": "^5.6.15",
70 | "@nestjs/common": "^9.3.12",
71 | "@nestjs/core": "^9.3.12",
72 | "@nestjs/mongoose": "^9.2.2",
73 | "@nestjs/platform-express": "^9.3.12",
74 | "@nestjs/typeorm": "^9.0.1",
75 | "@prisma/client": "^4.11.0",
76 | "adminjs": "^7.0.9",
77 | "ajv-formats": "^2.1.1",
78 | "argon2": "^0.30.3",
79 | "class-transformer": "^0.5.1",
80 | "class-validator": "^0.14.0",
81 | "connect-pg-simple": "^8.0.0",
82 | "cors": "^2.8.5",
83 | "dotenv": "^16.0.3",
84 | "express": "^4.18.2",
85 | "express-formidable": "^1.2.0",
86 | "express-session": "^1.17.3",
87 | "fastify-static": "^4.7.0",
88 | "i18next": "^22.4.13",
89 | "knex": "^2.4.2",
90 | "koa": "^2.14.2",
91 | "koa2-formidable": "^1.0.3",
92 | "mongoose": "^7.0.2",
93 | "nodemon": "^2.0.21",
94 | "objection": "^3.0.1",
95 | "pg": "^8.10.0",
96 | "prisma": "^4.11.0",
97 | "reflect-metadata": "^0.1.13",
98 | "rxjs": "^7.8.0",
99 | "sequelize": "^6.29.3",
100 | "sequelize-cli": "^6.6.0",
101 | "tslib": "^2.5.0",
102 | "typeorm": "^0.3.12",
103 | "uuid": "^9.0.0"
104 | },
105 | "devDependencies": {
106 | "@types/argon2": "^0.15.0",
107 | "@types/connect-pg-simple": "^7.0.0",
108 | "@types/cors": "^2.8.13",
109 | "@types/express-session": "^1.17.7",
110 | "@types/lodash": "^4.14.191",
111 | "@types/node": "^18.15.5",
112 | "@types/uuid": "^9.0.1",
113 | "@typescript-eslint/eslint-plugin": "^5.56.0",
114 | "@typescript-eslint/parser": "^5.56.0",
115 | "copyfiles": "^2.4.1",
116 | "dotenv-cli": "^7.1.0",
117 | "eslint": "^8.36.0",
118 | "eslint-config-prettier": "^8.8.0",
119 | "eslint-plugin-prettier": "^4.2.1",
120 | "eslint-plugin-react": "^7.32.2",
121 | "eslint-plugin-react-hooks": "^4.6.0",
122 | "prettier": "^2.8.6",
123 | "rimraf": "^4.4.1",
124 | "ts-node": "^10.9.1",
125 | "tsconfig-paths": "^4.1.2",
126 | "typescript": "^5.0.2"
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/prisma/migrations/20220713101214_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Post` (
3 | `id` INTEGER NOT NULL AUTO_INCREMENT,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `title` VARCHAR(255) NOT NULL,
7 | `content` VARCHAR(191) NULL,
8 | `someJson` JSON NULL,
9 | `status` ENUM('ACTIVE', 'REMOVED') NOT NULL DEFAULT 'ACTIVE',
10 | `published` BOOLEAN NOT NULL DEFAULT false,
11 | `publisherId` INTEGER NOT NULL,
12 |
13 | PRIMARY KEY (`id`)
14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
15 |
16 | -- CreateTable
17 | CREATE TABLE `Profile` (
18 | `id` INTEGER NOT NULL AUTO_INCREMENT,
19 | `bio` VARCHAR(191) NULL,
20 | `publisherId` INTEGER NOT NULL,
21 |
22 | UNIQUE INDEX `Profile_publisherId_key`(`publisherId`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
26 | -- CreateTable
27 | CREATE TABLE `Publisher` (
28 | `id` INTEGER NOT NULL AUTO_INCREMENT,
29 | `email` VARCHAR(191) NOT NULL,
30 | `name` VARCHAR(191) NULL,
31 |
32 | UNIQUE INDEX `Publisher_email_key`(`email`),
33 | PRIMARY KEY (`id`)
34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
35 |
36 | -- AddForeignKey
37 | ALTER TABLE `Post` ADD CONSTRAINT `Post_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
39 | -- AddForeignKey
40 | ALTER TABLE `Profile` ADD CONSTRAINT `Profile_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
41 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "mysql"
3 | url = env("MYSQL_DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | enum Status {
11 | ACTIVE
12 | REMOVED
13 | }
14 |
15 | model Post {
16 | id Int @id @default(autoincrement())
17 | createdAt DateTime @default(now())
18 | updatedAt DateTime @updatedAt
19 | title String @db.VarChar(255)
20 | content String?
21 | someJson Json? @db.Json
22 | status Status @default(ACTIVE)
23 | published Boolean @default(false)
24 | publisher Publisher @relation(fields: [publisherId], references: [id])
25 | publisherId Int
26 | }
27 |
28 | model Profile {
29 | id Int @id @default(autoincrement())
30 | bio String?
31 | publisher Publisher @relation(fields: [publisherId], references: [id])
32 | publisherId Int @unique
33 | }
34 |
35 | model Publisher {
36 | id Int @id @default(autoincrement())
37 | email String @unique
38 | name String?
39 | posts Post[]
40 | profile Profile?
41 | }
42 |
--------------------------------------------------------------------------------
/public/custom.css:
--------------------------------------------------------------------------------
1 | html {
2 | scroll-behavior: smooth;
3 | }
4 |
5 | a {
6 | text-decoration: none;
7 | color: inherit;
8 | }
9 |
10 | a.adminjs_Header {
11 | display: block;
12 | }
13 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoftwareBrothers/adminjs-example-app/09a6efdb27d584a67fc82e08945ea570a014b7c1/public/favicon.ico
--------------------------------------------------------------------------------
/public/gtm.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const headGtm = `
3 |
4 | (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
5 | new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
6 | j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
7 | 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
8 | })(window,document,'script','dataLayer','GTM-K6R3HZ4');
9 |
10 | `;
11 | const bodyGtm = `
12 |
13 |
15 |
16 | `;
17 | const headScript = window.document.createElement('script');
18 | headScript.type = 'text/javascript';
19 | headScript.text = headGtm;
20 | const bodyScript = window.document.createElement('noscript');
21 | bodyScript.text = bodyGtm;
22 |
23 | window.document.head.appendChild(headScript);
24 | window.document.body.appendChild(bodyScript);
25 | })();
26 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/public/locales/mk/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": {
3 | "new": "Создадете ново",
4 | "edit": "Уредување",
5 | "show": "Прикажи",
6 | "delete": "Избриши",
7 | "bulkDelete": "Избриши ги сите",
8 | "list": "Список"
9 | },
10 | "buttons": {
11 | "save": "Зачувај",
12 | "addNewItem": "Додадете нова ставка",
13 | "filter": "Филтер",
14 | "applyChanges": "Примени промени",
15 | "resetFilter": "Ресетирај",
16 | "confirmRemovalMany": "Потврдете го отстранувањето на {{count}} рекорд",
17 | "confirmRemovalMany_plural": "Потврдете го отстранувањето на {{count}} рекорди",
18 | "logout": "Одјави се",
19 | "login": "Логирај Се",
20 | "seeTheDocumentation": "Видете: <1>документацијата1>",
21 | "createFirstRecord": "Создадете прв запис",
22 | "cancel": "Откажи",
23 | "confirm": "Потврди"
24 | },
25 | "components": {
26 | "DropZone": {
27 | "placeholder": "Спуштете ја вашата датотека овде или кликнете за да пребарувате",
28 | "acceptedSize": "Максимална големина: {{maxSize}}",
29 | "acceptedType": "Поддржува: {{mimeTypes}}",
30 | "unsupportedSize": "Датотека {{fileName}} е преголем",
31 | "unsupportedType": "Датотека {{fileName}} има неподдржан тип: {{fileType}}"
32 | }
33 | },
34 | "labels": {
35 | "navigation": "Навигација",
36 | "pages": "Страници",
37 | "selectedRecords": "Избрано ({{selected}})",
38 | "filters": "Филтри",
39 | "adminVersion": "Администратор: {{version}}",
40 | "appVersion": "Апликација: {{version}}",
41 | "loginWelcome": "Добредојдовте",
42 | "dashboard": "Контролна табла"
43 | },
44 | "properties": {
45 | "length": "Должина",
46 | "from": "Од",
47 | "to": "До"
48 | },
49 | "resources": {},
50 | "messages": {
51 | "successfullyBulkDeleted": "успешно отстранети {{count}} рекорд",
52 | "successfullyBulkDeleted_plural": "успешно отстранети {{count}} рекорди",
53 | "successfullyDeleted": "Дадениот запис е успешно избришан",
54 | "successfullyUpdated": "Дадениот запис е успешно ажуриран",
55 | "thereWereValidationErrors": "Има грешки при валидација - проверете ги подолу",
56 | "forbiddenError": "Не можете да извршите дејство {{actionName}} на {{resourceId}}",
57 | "anyForbiddenError": "Не можете да извршите дадено дејство",
58 | "successfullyCreated": "Успешно создаде нов рекорд",
59 | "bulkDeleteError": "Настана грешка при бришење записи. Проверете ја конзолата за да видите повеќе информации",
60 | "errorFetchingRecords": "Настана грешка при преземањето на записите. Проверете ја конзолата за да видите повеќе информации",
61 | "errorFetchingRecord": "Настана грешка при преземањето на записот. Проверете ја конзолата за да видите повеќе информации",
62 | "noRecordsSelected": "Не избравте ниедна запис",
63 | "theseRecordsWillBeRemoved": "Следниот запис ќе биде отстранет",
64 | "theseRecordsWillBeRemoved_plural": "Следните записи ќе бидат отстранети",
65 | "pickSomeFirstToRemove": "За да ги отстраните записите, прво треба да ги изберете",
66 | "error404Resource": "Ресурс на дадениот ид: {{resourceId}} не може да се најде",
67 | "error404Action": "Ресурс на дадениот ид: {{resourceId}} нема дејство со име: {{actionName}} или не сте овластени да го користите!",
68 | "error404Record": "Ресурс на дадениот ид: {{resourceId}} нема запис со ид: {{recordId}} или не сте овластени да го користите!",
69 | "seeConsoleForMore": "Погледнете ја развојната конзола за повеќе детали...",
70 | "noActionComponent": "Мора да имплементирате акциона компонента за вашата акција",
71 | "noRecordsInResource": "Нема записи во овој ресурс",
72 | "noRecords": "Нема записи",
73 | "confirmDelete": "Дали навистина сакате да ја отстраните оваа ставка?",
74 | "welcomeOnBoard_title": "Добредојдовте на одборот!",
75 | "welcomeOnBoard_subtitle": "Сега сте еден од нас! ",
76 | "loginWelcome": "до AdminJS – водечки светски административен панел генериран автоматски со отворен код за вашата апликација Node.js кој ви овозможува да управувате со сите ваши податоци на едно место",
77 | "addingResources_title": "Додавање ресурси",
78 | "addingResources_subtitle": "Како да додадете нови ресурси на страничната лента",
79 | "customizeResources_title": "Прилагодете ги ресурсите",
80 | "customizeResources_subtitle": "Дефинирање на однесување, додавање својства и повеќе...",
81 | "customizeActions_title": "Приспособете ги акциите",
82 | "customizeActions_subtitle": "Изменување на постоечки дејства и додавање нови",
83 | "writeOwnComponents_title": "Напиши компоненти",
84 | "writeOwnComponents_subtitle": "Како да го измените изгледот и чувството на AdminJS",
85 | "customDashboard_title": "Прилагодена контролна табла",
86 | "customDashboard_subtitle": "Како да го измените овој приказ и да додадете нови страници на страничната лента",
87 | "roleBasedAccess_title": "Контрола на пристап заснована на улоги",
88 | "roleBasedAccess_subtitle": "Креирајте кориснички улоги и дозволи во AdminJS",
89 | "community_title": "Придружете се на опуштената заедница",
90 | "community_subtitle": "Разговарајте со креаторите на AdminJS и другите корисници на AdminJS",
91 | "foundBug_title": "Најдовте бубачка? ",
92 | "foundBug_subtitle": "Поставете проблем на нашето складиште на GitHub",
93 | "needMoreSolutions_title": "Ви требаат понапредни решенија?",
94 | "needMoreSolutions_subtitle": "Ние сме тука да ви обезбедиме прекрасен дизајн на UX/UI и прилагоден софтвер базиран (не само) на AdminJS",
95 | "invalidCredentials": "Погрешна е-пошта и/или лозинка",
96 | "keyPlaceholder": "КЛУЧ",
97 | "valuePlaceholder": "ВРЕДНОСТ",
98 | "initialKey": "клуч-{{number}}",
99 | "keyInUse": "Копчињата за објекти мора да бидат единствени.",
100 | "keyValuePropertyDefaultDescription": "Сите вредности се зачувани како текст. "
101 | }
102 | }
--------------------------------------------------------------------------------
/src/admin/admin.utils.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest } from 'adminjs';
2 |
3 | export const isPOSTMethod = ({ method }: ActionRequest): boolean => method.toLowerCase() === 'post';
4 |
5 | export const isGETMethod = ({ method }: ActionRequest): boolean => method.toLowerCase() === 'get';
6 |
7 | export const isNewAction = ({ params: { action } }: ActionRequest): boolean => action === 'new';
8 |
9 | export const isEditAction = ({ params: { action } }: ActionRequest): boolean => action === 'edit';
10 |
--------------------------------------------------------------------------------
/src/admin/components.bundler.ts:
--------------------------------------------------------------------------------
1 | import { ComponentLoader, OverridableComponent } from 'adminjs';
2 | import path from 'path';
3 | import * as url from 'url';
4 |
5 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
6 | export const componentLoader = new ComponentLoader();
7 |
8 | export const add = (url: string, componentName: string): string =>
9 | componentLoader.add(componentName, path.join(__dirname, url));
10 |
11 | export const override = (url: string, componentName: OverridableComponent): string =>
12 | componentLoader.override(componentName, path.join(__dirname, url));
13 |
14 | /**
15 | * Overridable components
16 | */
17 | override('components/top-bar', 'Version');
18 | override('components/login', 'Login');
19 | override('components/sidebar-resource-section', 'SidebarResourceSection');
20 |
21 | /**
22 | * Common components
23 | */
24 | export const PRODUCTS_LIST = add('components/products-list', 'ProductList');
25 | export const DONT_TOUCH_THIS_ACTION = add('components/dont-touch-this-action', 'CustomAction');
26 | export const DETAILED_STATS = add('components/detailed-stats', 'DetailedStats');
27 | export const THUMB = add('components/thumb', 'Thumb');
28 |
29 | /**
30 | * Pages
31 | */
32 | export const CUSTOM_PAGE = add('pages/custom-page', 'CustomPage');
33 | export const DESIGN_SYSTEM_PAGE = add('pages/design-system-examples/index', 'DesignSystemPage');
34 |
--------------------------------------------------------------------------------
/src/admin/components/debug.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Debug = (props) =>
{JSON.stringify(props, null, 4)}
;
4 |
5 | export default Debug;
6 |
--------------------------------------------------------------------------------
/src/admin/components/design-system-examples/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ModalExample } from './modal-example.js';
2 |
--------------------------------------------------------------------------------
/src/admin/components/design-system-examples/modal-example.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Modal, ModalProps } from '@adminjs/design-system';
2 | import React, { FC, useCallback, useState } from 'react';
3 |
4 | const ModalExample: FC = () => {
5 | const [showModal, setShowModal] = useState(false);
6 | const handleButtonClick = useCallback(() => setShowModal(true), []);
7 |
8 | const modalProps: ModalProps = {
9 | variant: 'primary',
10 | label: 'Modal header',
11 | icon: 'Bookmark',
12 | title: 'Modal title',
13 | subTitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
14 | buttons: [{ label: 'Cancel' }, { label: 'Delete', color: 'danger' }],
15 | onClose: () => setShowModal(false),
16 | onOverlayClick: () => setShowModal(false),
17 | };
18 |
19 | return (
20 |
21 |
22 | {showModal && }
23 |
24 | );
25 | };
26 |
27 | export default ModalExample;
28 |
--------------------------------------------------------------------------------
/src/admin/components/detailed-stats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { H5, Text, DrawerContent } from '@adminjs/design-system';
3 | import { ActionHeader, ActionHeaderProps } from 'adminjs';
4 |
5 | const DetailedStats = (props: ActionHeaderProps) => {
6 | return (
7 |
8 |
9 | Custom action example
10 | Where you can do whatever you like...
11 |
12 | );
13 | };
14 |
15 | export default DetailedStats;
16 |
--------------------------------------------------------------------------------
/src/admin/components/dont-touch-this-action.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H3, Text } from '@adminjs/design-system';
2 | import { BasePropertyProps } from 'adminjs';
3 | import React from 'react';
4 |
5 | const DontTouchThis = (props: BasePropertyProps) => {
6 | const { record } = props;
7 |
8 | return (
9 |
10 |
11 | Example of a simple page
12 | Where you can put almost everything like this:
13 |
14 |
15 |
16 |
17 |
18 | Or (more likely), operate on a returned record:
19 |
20 | {JSON.stringify(record, null, 2)}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DontTouchThis;
28 |
--------------------------------------------------------------------------------
/src/admin/components/login.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useSelector } from 'react-redux';
4 | import {
5 | Box,
6 | BoxProps,
7 | H5,
8 | H2,
9 | Label,
10 | Illustration,
11 | Input,
12 | FormGroup,
13 | Button,
14 | Text,
15 | MessageBox,
16 | MadeWithLove,
17 | themeGet,
18 | } from '@adminjs/design-system';
19 | import { styled } from '@adminjs/design-system/styled-components';
20 | import { ReduxState, useTranslation } from 'adminjs';
21 | import { AuthUser, AuthUsers } from '../constants/authUsers.js';
22 |
23 | const Wrapper = styled(Box)`
24 | align-items: center;
25 | justify-content: center;
26 | flex-direction: column;
27 | height: 100%;
28 | `;
29 |
30 | const StyledLogo = styled.img`
31 | max-width: 200px;
32 | margin: ${themeGet('space', 'md')} 0;
33 | `;
34 |
35 | const IllustrationsWrapper = styled(Box)`
36 | display: flex;
37 | flex-wrap: wrap;
38 | align-items: center;
39 | justify-content: center;
40 | & svg [stroke='#3B3552'] {
41 | stroke: rgba(255, 255, 255, 0.5);
42 | }
43 | & svg [fill='#3040D6'] {
44 | fill: rgba(255, 255, 255, 1);
45 | }
46 | `;
47 |
48 | export type LoginProps = {
49 | credentials: Pick;
50 | action: string;
51 | errorMessage?: string;
52 | children?: any;
53 | };
54 |
55 | export const Login: React.FC = (props) => {
56 | const { action, errorMessage } = props;
57 | const { translateComponent, translateMessage } = useTranslation();
58 | const [defaultUser] = AuthUsers;
59 | const branding = useSelector((state: ReduxState) => state.branding);
60 | const message = `Email: ${defaultUser.email}\nPassword: ${defaultUser.password}`;
61 |
62 | return (
63 |
64 |
65 |
66 |
75 | {translateComponent('Login.welcomeHeader')}
76 |
77 | {translateComponent('Login.welcomeMessage')}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | {branding.logo ? : branding.companyName}
94 |
95 |
96 | {errorMessage && (
97 | 1 ? errorMessage : translateMessage(errorMessage)}
100 | variant="danger"
101 | />
102 | )}
103 |
104 |
105 |
110 |
111 |
112 |
113 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | {branding.withMadeWithLove ? (
127 |
128 |
129 |
130 | ) : null}
131 |
132 |
133 | );
134 | };
135 |
136 | export default Login;
137 |
--------------------------------------------------------------------------------
/src/admin/components/products-list.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, FormGroup, Label, Table, TableBody, TableCell, TableHead, TableRow } from '@adminjs/design-system';
3 | import { flat, useTranslation, BasePropertyProps } from 'adminjs';
4 |
5 | import { ProductListInterface } from '../../sources/sequelize/interfaces.js';
6 |
7 | const ProductsList = (props: BasePropertyProps) => {
8 | const { translateLabel } = useTranslation();
9 | const params = flat.unflatten(props.record.params);
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 | {translateLabel('ID')}
19 | {translateLabel('Name')}
20 | {translateLabel('Quantity')}
21 | {translateLabel('Unit price')}
22 | {translateLabel('Sum')}
23 |
24 |
25 |
26 | {!params.products.length && (
27 |
28 |
29 | No records
30 |
31 |
32 | )}
33 | {params.products.length > 0 &&
34 | params.products.map(({ quantity, product }) => (
35 |
36 | {product.id}
37 | {product.name}
38 | {quantity}
39 | {product.price / 100}
40 | {(product.price * quantity) / 100}
41 |
42 | ))}
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ProductsList;
51 |
--------------------------------------------------------------------------------
/src/admin/components/sidebar-resource-section.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/jsx-one-expression-per-line */
2 | import React, { FC } from 'react';
3 | import { Navigation } from '@adminjs/design-system';
4 | import { useTranslation, type SidebarResourceSectionProps, useNavigationResources } from 'adminjs';
5 |
6 | const SidebarResourceSection: FC = ({ resources }) => {
7 | const elements = useNavigationResources(resources);
8 | const { translateLabel } = useTranslation();
9 |
10 | const openUrl = (url: string) => () => {
11 | window.open(url, '_blank');
12 | };
13 |
14 | elements.unshift({
15 | icon: 'Truck',
16 | label: translateLabel('kanbanBoard'),
17 | onClick: openUrl('https://github.com/orgs/SoftwareBrothers/projects/5'),
18 | });
19 |
20 | elements.unshift({
21 | icon: 'PieChart',
22 | label: translateLabel('stats'),
23 | onClick: openUrl('https://stats.adminjs.co'),
24 | });
25 |
26 | return ;
27 | };
28 |
29 | export default SidebarResourceSection;
30 |
--------------------------------------------------------------------------------
/src/admin/components/thumb.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from '@adminjs/design-system';
2 | import type { BasePropertyProps } from 'adminjs';
3 | import React, { FC } from 'react';
4 |
5 | const Thumb: FC = (props: BasePropertyProps) => {
6 | const { record, property } = props;
7 | const value = record.params[property.name];
8 |
9 | return ;
10 | };
11 |
12 | export default Thumb;
13 |
--------------------------------------------------------------------------------
/src/admin/components/top-bar.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Icon, Text } from '@adminjs/design-system';
2 | import { ReduxState } from 'adminjs';
3 | import React, { FC } from 'react';
4 | import { useSelector } from 'react-redux';
5 |
6 | const TopBar: FC = () => {
7 | const versions = useSelector((state: ReduxState) => state.versions);
8 | const GITHUB_URL = (window as any).AdminJS.env.GITHUB_URL;
9 | const SLACK_URL = (window as any).AdminJS.env.SLACK_URL;
10 | const DOCUMENTATION_URL = (window as any).AdminJS.env.DOCUMENTATION_URL;
11 |
12 | return (
13 |
14 |
15 | {versions.admin}
16 |
17 |
21 |
25 |
29 |
30 | );
31 | };
32 |
33 | export { TopBar };
34 | export default TopBar;
35 |
--------------------------------------------------------------------------------
/src/admin/constants/authUsers.ts:
--------------------------------------------------------------------------------
1 | export type AuthUser = {
2 | email: string;
3 | password: string;
4 | title: string;
5 | theme: string;
6 | };
7 |
8 | export const AuthUsers: AuthUser[] = [
9 | {
10 | email: 'admin@example.com',
11 | password: 'password',
12 | title: 'Admin',
13 | theme: 'light',
14 | },
15 | {
16 | email: 'dark@example.com',
17 | password: 'password',
18 | title: 'AdminJS dark theme',
19 | theme: 'dark',
20 | },
21 | {
22 | email: 'no-sidebar@example.com',
23 | password: 'password',
24 | title: 'AdminJS no-sidebar theme',
25 | theme: 'no-sidebar',
26 | },
27 | {
28 | email: 'custom@example.com',
29 | password: 'password',
30 | title: 'AdminJS custom theme',
31 | theme: 'custom-theme',
32 | },
33 | ];
34 |
--------------------------------------------------------------------------------
/src/admin/features/useEnvironmentVariableToDisableActions.ts:
--------------------------------------------------------------------------------
1 | import { buildFeature, FeatureType } from 'adminjs';
2 |
3 | export const useEnvironmentVariableToDisableActions = (): FeatureType => {
4 | if (process.env.DISABLE_ADMINJS_ACTIONS === 'true') {
5 | return buildFeature({
6 | actions: {
7 | edit: { isAccessible: false },
8 | delete: { isAccessible: false },
9 | bulkDelete: { isAccessible: false },
10 | new: { isAccessible: false },
11 | },
12 | });
13 | }
14 | return buildFeature({});
15 | };
16 |
--------------------------------------------------------------------------------
/src/admin/index.ts:
--------------------------------------------------------------------------------
1 | // Adapters
2 | import { Database as MikroormDatabase, Resource as MikroormResource } from '@adminjs/mikroorm';
3 | import { Database as MongooseDatabase, Resource as MongooseResource } from '@adminjs/mongoose';
4 | import { Database as ObjectionDatabase, Resource as ObjectionResource } from '@adminjs/objection';
5 | import { Database as PrismaDatabase, Resource as PrismaResource } from '@adminjs/prisma';
6 | import { Database as SequelizeDatabase, Resource as SequelizeResource } from '@adminjs/sequelize';
7 | import { dark, light, noSidebar } from '@adminjs/themes';
8 | import { Database as TypeormDatabase, Resource as TypeormResource } from '@adminjs/typeorm';
9 |
10 | import AdminJS, { AdminJSOptions, ResourceOptions } from 'adminjs';
11 | import argon2 from 'argon2';
12 | import { CreateCarResource, CreateOwnerResource, CreateSellerResource } from '../sources/mikroorm/resources/index.js';
13 | import { AdminModel } from '../sources/mongoose/models/index.js';
14 | import {
15 | CreateAdminResource,
16 | CreateArticleResource,
17 | CreateCategoryResource,
18 | CreateCommentResource,
19 | CreateComplicatedResource,
20 | CreateUserResource,
21 | } from '../sources/mongoose/resources/index.js';
22 | import { CreateManagerResource, CreateOfficeResource } from '../sources/objectionjs/resources/index.js';
23 | import {
24 | CreatePostResource,
25 | CreateProfileResource,
26 | CreatePublisherResource,
27 | } from '../sources/prisma/resources/index.js';
28 | import { CryptoDatabase } from '../sources/rest/crypto-database.js';
29 | import {
30 | CreateCartResource,
31 | CreateOrderResource,
32 | CreateProductResource,
33 | CreateCategoryResource as CreateSequelizeCategoryResource,
34 | } from '../sources/sequelize/resources/index.js';
35 | import { CreateOrganizationResource, CreatePersonResource } from '../sources/typeorm/resources/index.js';
36 | import './components.bundler.js';
37 | import { componentLoader } from './components.bundler.js';
38 | import { AuthUsers } from './constants/authUsers.js';
39 | import { locale } from './locale/index.js';
40 | import pages from './pages/index.js';
41 | import { customTheme } from '../themes/index.js';
42 |
43 | AdminJS.registerAdapter({ Database: MikroormDatabase, Resource: MikroormResource });
44 | AdminJS.registerAdapter({ Database: MongooseDatabase, Resource: MongooseResource });
45 | AdminJS.registerAdapter({ Database: ObjectionDatabase, Resource: ObjectionResource });
46 | AdminJS.registerAdapter({ Database: PrismaDatabase, Resource: PrismaResource });
47 | AdminJS.registerAdapter({ Database: SequelizeDatabase, Resource: SequelizeResource });
48 | AdminJS.registerAdapter({ Database: TypeormDatabase, Resource: TypeormResource });
49 |
50 | export const menu: Record = {
51 | mongoose: { name: 'Mongoose', icon: 'Folder' },
52 | sequelize: { name: 'Sequelize', icon: 'Folder' },
53 | typeorm: { name: 'Typeorm', icon: 'Folder' },
54 | mikroorm: { name: 'Mikroorm', icon: 'Folder' },
55 | prisma: { name: 'Prisma', icon: 'Folder' },
56 | objection: { name: 'Objection', icon: 'Folder' },
57 | rest: { name: 'REST', icon: 'Link' },
58 | };
59 |
60 | export const generateAdminJSConfig: () => AdminJSOptions = () => ({
61 | version: { admin: true, app: '2.0.0' },
62 | rootPath: '/admin',
63 | locale,
64 | assets: {
65 | styles: ['/custom.css'],
66 | scripts: process.env.NODE_ENV === 'production' ? ['/gtm.js'] : [],
67 | },
68 | branding: {
69 | companyName: 'AdminJS demo page',
70 | favicon: '/favicon.ico',
71 | theme: {
72 | colors: { primary100: '#4D70EB' },
73 | },
74 | },
75 | defaultTheme: 'light',
76 | availableThemes: [light, dark, noSidebar, customTheme],
77 | componentLoader,
78 | pages,
79 | env: {
80 | STORYBOOK_URL: process.env.STORYBOOK_URL,
81 | GITHUB_URL: process.env.GITHUB_URL,
82 | SLACK_URL: process.env.SLACK_URL,
83 | DOCUMENTATION_URL: process.env.DOCUMENTATION_URL,
84 | },
85 | resources: [
86 | // mongo
87 | CreateAdminResource(),
88 | CreateUserResource(),
89 | CreateCategoryResource(),
90 | CreateArticleResource(),
91 | CreateCommentResource(),
92 | CreateComplicatedResource(),
93 | // sequelize
94 | CreateSequelizeCategoryResource(),
95 | CreateProductResource(),
96 | CreateOrderResource(),
97 | CreateCartResource(),
98 | // typeorm
99 | CreateOrganizationResource(),
100 | CreatePersonResource(),
101 | // mikroorm
102 | CreateCarResource(),
103 | CreateSellerResource(),
104 | CreateOwnerResource(),
105 | // prisma
106 | CreatePublisherResource(),
107 | CreateProfileResource(),
108 | CreatePostResource(),
109 | // objectionjs
110 | CreateOfficeResource(),
111 | CreateManagerResource(),
112 | // custom
113 | new CryptoDatabase(),
114 | ],
115 | });
116 |
117 | export const createAuthUsers = async () =>
118 | Promise.all(
119 | AuthUsers.map(async ({ email, password }) => {
120 | const admin = await AdminModel.findOne({ email });
121 | if (!admin) {
122 | await AdminModel.create({ email, password: await argon2.hash(password) });
123 | }
124 | }),
125 | );
126 |
--------------------------------------------------------------------------------
/src/admin/locale/de/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "CustomPage": {
4 | "message": "Ich wurde angeklickt !!!",
5 | "messageWithInterpolation": "Übersetzte Nachrichten mit Interpolation: {{someParams}}"
6 | },
7 | "I am fetched from the backend": "Ich werde aus dem Backend geholt"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/admin/locale/de/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "CustomPage": {
4 | "header": "Hier können Sie eine vollständig benutzerdefinierte Seite angeben",
5 | "introduction": "Mit einigen Daten, die vom Backend abgerufen wurden:",
6 | "ending": "und andere Interaktionen wie Toast :)",
7 | "button": "Klick mich"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/admin/locale/de/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleTranslations } from 'adminjs';
2 |
3 | import common from './common.json' assert { type: 'json' };
4 | import components from './components.json' assert { type: 'json' };
5 | import pages from './pages.json' assert { type: 'json' };
6 |
7 | const deLocale: LocaleTranslations = {
8 | ...common,
9 | ...components,
10 | ...pages,
11 | };
12 |
13 | export default deLocale;
14 |
--------------------------------------------------------------------------------
/src/admin/locale/de/pages.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {
3 | "customPage": "Benutzerdefinierte Seite"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/admin/locale/en/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "loginWelcome": "to the demo application made with AdminJS - the best admin framework for Node.js apps, based on React.",
4 | "Has to be filled": "Has to be filled",
5 | "CustomPage": {
6 | "message": "I was clicked !!!",
7 | "messageWithInterpolation": "Translated messages with interpolation {{someParams}}"
8 | }
9 | },
10 | "properties": {
11 | "id": "#",
12 | "Products": {
13 | "name": "Product's name",
14 | "price": "Price"
15 | }
16 | },
17 | "labels.navigation": "",
18 | "labels.dashboard": "Home"
19 | }
20 |
--------------------------------------------------------------------------------
/src/admin/locale/en/complicated.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": {
3 | "nestedDetails.age": "Person age",
4 | "nestedDetails.height": "Person height",
5 | "nestedDetails.nested.extremelyNested": "This nesting is crazy"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/admin/locale/en/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "CustomPage": {
4 | "header": "Here you can specify a totally custom page",
5 | "introduction": "With some data fetched from the backend:",
6 | "ending": "and other interactions like toast :)",
7 | "button": "Click me"
8 | },
9 | "LanguageSelector": {
10 | "availableLanguages": {
11 | "mk": "Macedonian"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/admin/locale/en/index.ts:
--------------------------------------------------------------------------------
1 | import type { LocaleTranslations } from 'adminjs';
2 |
3 | import common from './common.json' assert { type: 'json' };
4 | import Complicated from './complicated.json' assert { type: 'json' };
5 | import components from './components.json' assert { type: 'json' };
6 | import pages from './pages.json' assert { type: 'json' };
7 | import Person from './person.json' assert { type: 'json' };
8 |
9 | const enLocale: LocaleTranslations = {
10 | ...common,
11 | ...components,
12 | ...pages,
13 | resources: {
14 | Complicated,
15 | Person,
16 | products: {
17 | properties: {
18 | categoryId: 'Category',
19 | },
20 | },
21 | },
22 | };
23 |
24 | export default enLocale;
25 |
--------------------------------------------------------------------------------
/src/admin/locale/en/pages.json:
--------------------------------------------------------------------------------
1 | {
2 | "pages": {
3 | "customPage": "Custom Page example"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/admin/locale/en/person.json:
--------------------------------------------------------------------------------
1 | {
2 | "labels": {
3 | "Person": "Staff"
4 | },
5 | "actions": {
6 | "dontTouchThis": "Don't touch this!!!"
7 | },
8 | "properties": {
9 | "organizationId": "Organization"
10 | },
11 | "messages": {
12 | "youCanSetupGuards": "You can setup guards before an action - just in case."
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/admin/locale/index.ts:
--------------------------------------------------------------------------------
1 | import { Locale, locales as AdminJSLocales } from 'adminjs';
2 |
3 | import de from './de/index.js';
4 | import en from './en/index.js';
5 |
6 | const localeKey = process.env.LOCALE || 'en';
7 | const customLanguage = 'mk';
8 |
9 | export const locale: Locale = {
10 | language: localeKey,
11 | availableLanguages: [...Object.keys(AdminJSLocales), customLanguage].sort(),
12 | localeDetection: true,
13 | withBackend: true,
14 | translations: {
15 | de,
16 | en,
17 | [customLanguage]: {
18 | components: {
19 | LanguageSelector: {
20 | availableLanguages: {
21 | de: 'германски',
22 | en: 'Англиски',
23 | es: 'шпански',
24 | it: 'италијански',
25 | pl: 'полски',
26 | mk: 'македонски',
27 | 'pt-BR': 'португалски (Бразил)',
28 | ua: 'украински',
29 | 'zh-CN': 'кинески',
30 | },
31 | },
32 | },
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/src/admin/pages/custom-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, H3, Link, Placeholder, Text } from '@adminjs/design-system';
2 | import { ApiClient, useNotice, useTranslation } from 'adminjs';
3 | import React, { FC, useEffect, useState } from 'react';
4 |
5 | const api = new ApiClient();
6 |
7 | type ApiGetPageResponse = { text: string };
8 |
9 | const CustomPage: FC = () => {
10 | const [text, setText] = useState();
11 | const addNotice = useNotice();
12 | const {
13 | tc,
14 | tm,
15 | i18n: { language },
16 | } = useTranslation();
17 |
18 | useEffect(() => {
19 | api.getPage({ pageName: 'customPage' }).then((res) => {
20 | setText(tm(res.data.text, { defaultValue: res.data.text }));
21 | });
22 | // eslint-disable-next-line react-hooks/exhaustive-deps
23 | }, [language]);
24 |
25 | const sendSimpleNotice = () =>
26 | addNotice({
27 | message: 'CustomPage.message',
28 | type: 'success',
29 | });
30 |
31 | const sendTranslatedNotice = () =>
32 | addNotice({
33 | message: 'CustomPage.messageWithInterpolation',
34 | options: {
35 | someParams: ['param 1', 'param2'].join(', '),
36 | },
37 | body: (
38 | <>
39 | {tm('CustomPage.message')} {tc('CustomPage.button')}
40 | >
41 | ),
42 | } as any);
43 |
44 | return (
45 |
46 |
47 | {tc('CustomPage.header')}
48 |
49 | {tc('CustomPage.introduction')}
50 | {text ? JSON.stringify(text, null, 2) : }
51 | {tc('CustomPage.ending')}
52 |
53 |
54 |
57 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default CustomPage;
67 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/blog-page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | Drawer,
5 | DrawerContent,
6 | DropZone,
7 | Header,
8 | Icon,
9 | Input,
10 | Label,
11 | RichTextEditor,
12 | } from '@adminjs/design-system';
13 | import React, { useState } from 'react';
14 |
15 | const BlogPage: React.FC = () => {
16 | const [isDrawerVisible, setIsDrawerVisible] = useState(false);
17 |
18 | const handler = (html) => {
19 | console.log(html);
20 | };
21 |
22 | return (
23 |
24 |
27 |
28 | {isDrawerVisible && (
29 |
30 |
31 |
32 |
35 | Article settings
36 |
37 |
38 |
39 |
40 |
41 | )}
42 |
43 |
44 |
47 |
51 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default BlogPage;
68 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/buttons-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Button, Header, Icon, Text } from '@adminjs/design-system';
2 | import React, { FC, PropsWithChildren } from 'react';
3 |
4 | const Wrapper: FC = ({ children, title }) => (
5 |
6 | {title}
7 |
8 | {children}
9 |
10 |
11 | );
12 |
13 | const colors = ['primary', 'secondary', 'success', 'info', 'danger', 'text'] as const;
14 | const [primary] = colors;
15 |
16 | const ButtonsPage = () => (
17 |
18 |
21 |
22 |
23 | {colors.map((color) => (
24 |
27 | ))}
28 |
29 |
30 | {colors.map((color) => (
31 |
34 | ))}
35 |
36 |
37 | {colors.map((color) => (
38 |
41 | ))}
42 |
43 |
44 | {colors.map((color) => (
45 |
48 | ))}
49 |
50 |
51 | {colors.map((color) => (
52 |
55 | ))}
56 |
57 |
58 | {colors.map((color) => (
59 |
62 | ))}
63 |
64 |
65 | {colors.map((color) => (
66 |
69 | ))}
70 |
71 |
72 | {colors.map((color) => (
73 |
76 | ))}
77 |
78 |
79 |
82 |
85 |
88 |
89 |
90 | {colors.map((color) => (
91 |
95 | ))}
96 |
97 |
98 | {colors.map((color) => (
99 |
102 | ))}
103 |
104 |
105 | {colors.map((color) => (
106 |
109 | ))}
110 |
111 |
112 |
113 | );
114 |
115 | export default ButtonsPage;
116 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/form-page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | CheckBox,
4 | CurrencyInput,
5 | DatePicker,
6 | DropZone,
7 | DropZoneProps,
8 | Header,
9 | Input,
10 | Label,
11 | PhoneInput,
12 | Select,
13 | TextArea,
14 | } from '@adminjs/design-system';
15 | import { useTranslation } from 'adminjs';
16 | import React, { useState } from 'react';
17 |
18 | const FormPage = () => {
19 | const [value, setValue] = useState();
20 | const [date, setDate] = useState('2021-06-17');
21 | const options = [
22 | { value: '1', label: 'Office 1' },
23 | { value: '2', label: 'Office 2' },
24 | ];
25 | const { translateComponent } = useTranslation();
26 |
27 | return (
28 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default FormPage;
88 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/icons-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H6, Header, Icon } from '@adminjs/design-system';
2 | import React from 'react';
3 | import * as FeatherIcons from 'react-feather';
4 |
5 | const IconsPage = () => {
6 | const IconsSet = Object.keys(FeatherIcons)
7 | .filter((name) => name !== 'default')
8 | .map((iconName) => (
9 |
10 | {iconName}
11 |
12 |
13 | ));
14 |
15 | return (
16 |
17 |
20 |
21 | {IconsSet}
22 |
23 |
24 | );
25 | };
26 |
27 | export default IconsPage;
28 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/illustrations-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, Illustration, Text } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | const variants = [
5 | 'Accept',
6 | 'Cup',
7 | 'Bag',
8 | 'Beware',
9 | 'Notebook',
10 | 'NotFound',
11 | 'Padlock',
12 | 'Photos',
13 | 'Plug',
14 | 'RocketNew',
15 | 'Tags',
16 | 'Folder',
17 | 'Box',
18 | 'Calendar',
19 | 'Cancel',
20 | 'Cards',
21 | 'Clip',
22 | 'Cloud',
23 | 'Details',
24 | 'Docs',
25 | 'Drawer',
26 | 'IdentityCard',
27 | ] as const;
28 |
29 | const IllustrationPage: FC = () => (
30 |
31 |
34 |
35 | {variants.map((variant) => (
36 |
37 |
38 | {variant}
39 |
40 | ))}
41 |
42 |
43 | );
44 |
45 | export default IllustrationPage;
46 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, Link, Placeholder, Text } from '@adminjs/design-system';
2 | import React, { FC, lazy, Suspense } from 'react';
3 |
4 | const BlogPage = lazy(() => import('./blog-page.js'));
5 | const ButtonsPage = lazy(() => import('./buttons-page.js'));
6 | const FormPage = lazy(() => import('./form-page.js'));
7 | const IconsPage = lazy(() => import('./icons-page.js'));
8 | const IllustrationPage = lazy(() => import('./illustrations-page.js'));
9 | const MessagesPage = lazy(() => import('./messages-page.js'));
10 | const ModalPage = lazy(() => import('./modal-page.js'));
11 | const TabsPage = lazy(() => import('./tabs-page.js'));
12 | const TypographyPage = lazy(() => import('./typography-page.js'));
13 |
14 | const DesignSystemPage: FC = () => {
15 | const STORYBOOK_URL = (window as any).AdminJS.env.STORYBOOK_URL;
16 | return (
17 | <>
18 |
19 |
22 |
23 |
24 | For more examples visit our Storybook
25 |
26 | {STORYBOOK_URL}
27 |
28 |
29 |
30 |
31 | }>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | const DesignSytemPagePlaceholder = () => (
47 | <>
48 | {Array.from({ length: 3 }).map((_, index) => (
49 |
50 |
51 |
52 |
53 |
54 |
55 | ))}
56 | >
57 | );
58 |
59 | export default DesignSystemPage;
60 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/messages-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header, MessageBox, MessageBoxProps, Text } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | const MessagesPage: FC = () => {
5 | const variants: MessageBoxProps['variant'][] = ['info', 'danger', 'success', 'warning'];
6 |
7 | return (
8 |
9 |
12 |
13 | {variants.map((variant) => (
14 | // eslint-disable-next-line @typescript-eslint/no-empty-function
15 | {}} />
16 | ))}
17 | With extra body
18 | {variants.map((variant) => (
19 | // eslint-disable-next-line @typescript-eslint/no-empty-function
20 | {}}>
21 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. Fuga itaque quaerat quia eum ratione ipsum
22 | deleniti. Officiis nisi non necessitatibus laudantium blanditiis inventore.
23 |
24 | ))}
25 |
26 |
27 | );
28 | };
29 |
30 | export default MessagesPage;
31 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/modal-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Header } from '@adminjs/design-system';
2 | import React, { FC } from 'react';
3 |
4 | import { ModalExample } from '../../components/design-system-examples/index.js';
5 |
6 | const ModalPage: FC = () => (
7 |
8 |
11 |
12 |
13 |
14 |
15 | );
16 |
17 | export default ModalPage;
18 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/tabs-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Tab, Tabs, Header } from '@adminjs/design-system';
2 | import React, { FC, useState } from 'react';
3 |
4 | const TabsPage: FC = () => {
5 | const [selectedTab, setSelectedTab] = useState('first');
6 |
7 | return (
8 |
9 |
12 |
13 |
14 |
15 | First
16 |
17 |
18 | Second
19 |
20 |
21 | Third
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default TabsPage;
30 |
--------------------------------------------------------------------------------
/src/admin/pages/design-system-examples/typography-page.tsx:
--------------------------------------------------------------------------------
1 | import { Box, H1, H2, H3, H4, H5, H6, Header, Text } from '@adminjs/design-system';
2 | import React from 'react';
3 |
4 | const TypographyPage = () => (
5 |
6 |
9 |
10 | This is H1 header
11 | This is H2 header
12 | This is H3 header
13 | This is H4 header
14 | This is H5 header
15 | This is H6 header
16 |
17 |
18 |
19 | Sed tempus tempor dictum. Integer in lacus lacus. Curabitur sit amet ante eget ipsum finibus gravida. Donec
20 | viverra aliquet libero. Integer a nisl ac neque tempor pharetra. Donec sapien tortor, fermentum eu justo sed,
21 | egestas ultricies lacus. Pellentesque eget tincidunt nibh. Ut non lectus varius, semper lorem vel, porta orci.
22 | Duis id risus eu arcu efficitur bibendum. In eget eros ex. Integer et malesuada tellus.
23 |
24 |
25 |
26 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis semper massa a rhoncus. Praesent eu
27 | rutrum leo. Donec malesuada quis metus vel pulvinar. Quisque in vehicula nulla. Nam vestibulum facilisis lorem,
28 | ac mattis odio aliquet et. Suspendisse enim ligula, ultricies pellentesque ligula id, molestie sodales quam.
29 | Etiam in lectus ut nibh laoreet consequat.
30 |
31 |
32 |
33 | Suspendisse efficitur, urna sit amet tempor dignissim, ex est feugiat ex, sed molestie ante erat eget dolor.
34 | Duis purus orci, commodo non semper sed, laoreet quis nisl. Donec a bibendum arcu. Donec eget justo nunc. Nunc
35 | elementum augue et bibendum molestie. Duis tincidunt pellentesque enim ac mattis. Nunc congue, ante id efficitur
36 | gravida, turpis velit porta nunc, at egestas urna odio eget arcu. Ut enim metus, fringilla eu risus eu, sodales
37 | venenatis lacus.
38 |
39 |
40 |
41 | Donec vel malesuada turpis. Curabitur ultricies neque a sapien ullamcorper, quis faucibus felis porta. Etiam
42 | fermentum odio at rutrum pretium. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
43 | cubilia curae; Suspendisse aliquet suscipit turpis at placerat. Cras quis sem vitae velit vehicula congue et nec
44 | libero. Curabitur aliquam, est id dapibus egestas, felis augue suscipit est, eu venenatis leo justo vel sapien.
45 | Donec id dignissim diam. Nulla feugiat ex sit amet augue sollicitudin pulvinar. Pellentesque id felis rhoncus,
46 | varius sem et, condimentum ligula. Proin accumsan sit amet nisl ac vehicula. Nulla facilisi. Ut pharetra vel
47 | tortor vel lacinia. Nulla sit amet neque lectus. Mauris tristique justo in sem tempus, at sagittis lectus
48 | molestie. Phasellus sit amet nulla id mi rutrum varius in eu nisi.
49 |
50 |
51 |
52 | );
53 |
54 | export default TypographyPage;
55 |
--------------------------------------------------------------------------------
/src/admin/pages/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { AdminJSOptions } from 'adminjs';
3 |
4 | import { CUSTOM_PAGE, DESIGN_SYSTEM_PAGE } from '../components.bundler.js';
5 |
6 | const pages: AdminJSOptions['pages'] = {
7 | customPage: {
8 | component: CUSTOM_PAGE,
9 | icon: 'File',
10 | handler: async (request, response, context) => {
11 | return {
12 | text: 'I am fetched from the backend',
13 | };
14 | },
15 | },
16 | designSystemExamples: {
17 | component: DESIGN_SYSTEM_PAGE,
18 | icon: 'Layout',
19 | },
20 | };
21 |
22 | export default pages;
23 |
--------------------------------------------------------------------------------
/src/admin/router.ts:
--------------------------------------------------------------------------------
1 | import AdminJSExpress from '@adminjs/express';
2 | import AdminJSFastify from '@adminjs/fastify';
3 | import AdminJS from 'adminjs';
4 | import argon2 from 'argon2';
5 | import { FastifyInstance } from 'fastify';
6 | import ConnectPgSimple from 'connect-pg-simple';
7 | import session from 'express-session';
8 | import { Router } from 'express';
9 |
10 | import { AdminModel } from '../sources/mongoose/models/index.js';
11 | import { AuthUsers } from './constants/authUsers.js';
12 |
13 | export const authenticateUser = async (email, password) => {
14 | const user = await AdminModel.findOne({ email });
15 | if (user && (await argon2.verify(user.password, password))) {
16 | const userData = AuthUsers.find((au) => email === au.email);
17 | return { ...userData, ...user.toObject() };
18 | }
19 | return null;
20 | };
21 |
22 | export const expressAuthenticatedRouter = (adminJs: AdminJS, router: Router | null = null) => {
23 | const ConnectSession = ConnectPgSimple(session);
24 |
25 | const sessionStore = new ConnectSession({
26 | conObject: {
27 | connectionString: process.env.POSTGRES_DATABASE_URL,
28 | ssl: process.env.NODE_ENV === 'production',
29 | },
30 | tableName: 'session',
31 | createTableIfMissing: true,
32 | });
33 |
34 | return AdminJSExpress.buildAuthenticatedRouter(
35 | adminJs,
36 | {
37 | authenticate: authenticateUser,
38 | cookieName: 'adminjs',
39 | cookiePassword: process.env.SESSION_SECRET ?? 'sessionsecret',
40 | },
41 | router,
42 | {
43 | store: sessionStore,
44 | resave: true,
45 | saveUninitialized: true,
46 | secret: process.env.SESSION_SECRET ?? 'sessionsecret',
47 | cookie: {
48 | httpOnly: true,
49 | secure: process.env.NODE_ENV === 'production',
50 | },
51 | name: 'adminjs',
52 | },
53 | );
54 | };
55 |
56 | export const fastifyAuthenticatedRouter = (adminJs: AdminJS, app: FastifyInstance) =>
57 | AdminJSFastify.buildAuthenticatedRouter(
58 | adminJs,
59 | {
60 | cookiePassword: 'secretsecretsecretsecretsecretsecretsecretsecret',
61 | authenticate: authenticateUser,
62 | },
63 | app,
64 | );
65 |
--------------------------------------------------------------------------------
/src/admin/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { ResourceOptions, FeatureType } from 'adminjs';
2 |
3 | export type CreateResourceResult = {
4 | resource: T;
5 | options: ResourceOptions;
6 | features?: Array;
7 | };
8 |
9 | export type ResourceFunction = () => CreateResourceResult;
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | switch (process.env.SERVER) {
7 | default:
8 | case 'EXPRESS':
9 | await import('./servers/express/index.js');
10 | break;
11 | case 'HAPIJS':
12 | await import('./servers/hapijs.js');
13 | break;
14 | case 'FASTIFY':
15 | await import('./servers/fastify.js');
16 | break;
17 | case 'NESTJS':
18 | await import('./servers/nestjs/index.js');
19 | break;
20 | }
21 |
--------------------------------------------------------------------------------
/src/scripts/truncate-mongodb.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import mongoose from 'mongoose';
7 | import { AuthUsers } from '../admin/constants/authUsers.js';
8 | import {
9 | AdminModel,
10 | UserModel,
11 | ArticleModel,
12 | CommentModel,
13 | CategoryModel,
14 | ComplicatedModel,
15 | } from '../sources/mongoose/models/index.js';
16 |
17 | async function truncateMongodb() {
18 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
19 | await UserModel.deleteMany({});
20 | await ArticleModel.deleteMany({});
21 | await CommentModel.deleteMany({});
22 | await CategoryModel.deleteMany({});
23 | await ComplicatedModel.deleteMany({});
24 | await AdminModel.deleteMany({
25 | email: {
26 | $nin: AuthUsers.map((user) => user.email),
27 | },
28 | });
29 | }
30 |
31 | truncateMongodb()
32 | .then(() => process.exit(0))
33 | .catch((e) => {
34 | console.log(e);
35 | process.exit(1);
36 | });
37 |
--------------------------------------------------------------------------------
/src/scripts/truncate-postgres.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { sequelize } from '../sources/sequelize/index.js';
7 |
8 | const truncatePostgres = async () => {
9 | await sequelize.query(`
10 | CREATE OR REPLACE FUNCTION truncate_tables() RETURNS void AS $$
11 | DECLARE
12 | row record;
13 | BEGIN
14 | FOR row IN
15 | SELECT
16 | tablename
17 | FROM
18 | pg_catalog.pg_tables
19 | WHERE
20 | schemaname = 'public'
21 | AND
22 | tablename NOT IN ('SequelizeMeta', 'migrations', 'mikro_orm_migrations')
23 | LOOP
24 | EXECUTE format('TRUNCATE %I RESTART IDENTITY CASCADE', row.tablename);
25 | END LOOP;
26 | END;
27 | $$ LANGUAGE plpgsql;
28 |
29 | SELECT truncate_tables();
30 | `);
31 | };
32 |
33 | truncatePostgres()
34 | .then(() => process.exit(0))
35 | .catch((e) => {
36 | console.log(e);
37 | process.exit(1);
38 | });
39 |
--------------------------------------------------------------------------------
/src/servers/express/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import mongoose from 'mongoose';
3 | import express, { Express } from 'express';
4 | import cors from 'cors';
5 | import AdminJS from 'adminjs';
6 | import * as url from 'url';
7 |
8 | import { createAuthUsers, generateAdminJSConfig } from '../../admin/index.js';
9 | import { expressAuthenticatedRouter } from '../../admin/router.js';
10 | import { init } from '../../sources/mikroorm/config.js';
11 | import dataSource from '../../sources/typeorm/config.js';
12 |
13 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
14 |
15 | const attachAdminJS = async (app: Express) => {
16 | const config = generateAdminJSConfig();
17 | const adminJS = new AdminJS(config);
18 |
19 | if (process.env.NODE_ENV === 'production') await adminJS.initialize();
20 | else adminJS.watch();
21 |
22 | const adminRouter = expressAuthenticatedRouter(adminJS);
23 |
24 | app.use(adminJS.options.rootPath, adminRouter);
25 | app.get('/', (req, res) => res.redirect(adminJS.options.rootPath));
26 | app.use(express.static(path.join(__dirname, '../../../public')));
27 |
28 | await createAuthUsers();
29 | };
30 |
31 | const start = async () => {
32 | const app = express();
33 | app.enable('trust proxy');
34 | app.use(cors({ credentials: true, origin: true }));
35 |
36 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
37 | await init();
38 | await dataSource.initialize();
39 |
40 | await attachAdminJS(app);
41 |
42 | app.listen(process.env.PORT, async () => {
43 | console.log(`AdminJS is under http://localhost:${process.env.PORT}`);
44 | });
45 | };
46 |
47 | start();
48 |
--------------------------------------------------------------------------------
/src/servers/fastify.ts:
--------------------------------------------------------------------------------
1 | import fastify from 'fastify';
2 | import mongoose from 'mongoose';
3 | import AdminJS from 'adminjs';
4 | import fastifyStatic from 'fastify-static';
5 | import path from 'path';
6 | import * as url from 'url';
7 |
8 | import { createAuthUsers, generateAdminJSConfig } from '../admin/index.js';
9 | import { init } from '../sources/mikroorm/config.js';
10 | import dataSource from '../sources/typeorm/config.js';
11 | import { fastifyAuthenticatedRouter } from '../admin/router.js';
12 |
13 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
14 |
15 | const app = fastify();
16 |
17 | const attachAdminJS = async () => {
18 | const config = generateAdminJSConfig();
19 | const adminJS = new AdminJS(config);
20 | await fastifyAuthenticatedRouter(adminJS, app);
21 | await createAuthUsers();
22 | };
23 |
24 | const run = async (): Promise => {
25 | try {
26 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
27 | await init();
28 | await dataSource.initialize();
29 |
30 | app.register(fastifyStatic, {
31 | root: path.join(__dirname, '../assets'),
32 | prefix: '/assets/',
33 | });
34 |
35 | await attachAdminJS();
36 |
37 | await app.listen(process.env.PORT);
38 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
39 | } catch (err) {
40 | app.log.error(err);
41 | process.exit(1);
42 | }
43 | };
44 |
45 | run();
46 |
--------------------------------------------------------------------------------
/src/servers/hapijs.ts:
--------------------------------------------------------------------------------
1 | import { init } from '../sources/mikroorm/config.js';
2 | import dataSource from '../sources/typeorm/config.js';
3 | import { generateAdminJSConfig } from '../admin/index.js';
4 | import { expressAuthenticatedRouter } from '../admin/router.js';
5 | import AdminJS from 'adminjs';
6 |
7 | import Hapi, { ServerRegisterPluginObjectDirect } from '@hapi/hapi';
8 | import mongoose from 'mongoose';
9 | import AdminJSPlugin from '@adminjs/hapi';
10 |
11 | const start = async () => {
12 | try {
13 | const server = Hapi.server({
14 | port: process.env.PORT || 3000,
15 | });
16 |
17 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
18 | await init();
19 | await dataSource.initialize();
20 |
21 | const adminJS = generateAdminJSConfig();
22 |
23 | await server.register({
24 | plugin: AdminJSPlugin,
25 | options: {
26 | ...adminJS,
27 | auth: expressAuthenticatedRouter(new AdminJS(adminJS)),
28 | },
29 | } as ServerRegisterPluginObjectDirect);
30 |
31 | await server.start();
32 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
33 | } catch (error) {
34 | console.log(error);
35 | process.exit(1);
36 | }
37 | };
38 |
39 | start();
40 |
--------------------------------------------------------------------------------
/src/servers/nestjs/admin/admin.setup.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication } from '@nestjs/common';
2 | import AdminJS from 'adminjs';
3 | import { AbstractHttpAdapter, HttpAdapterHost } from '@nestjs/core';
4 |
5 | import { generateAdminJSConfig } from '../../../admin/index.js';
6 | import { expressAuthenticatedRouter } from '../../../admin/router.js';
7 |
8 | export const setupAdminJS = async (app: INestApplication): Promise => {
9 | const expressApp: AbstractHttpAdapter = app.get(HttpAdapterHost).httpAdapter;
10 | const config = generateAdminJSConfig();
11 | const adminJS = new AdminJS(config);
12 | expressApp.use(adminJS.options.rootPath, expressAuthenticatedRouter(adminJS));
13 | };
14 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Post } from '@nestjs/common';
2 | import { IsString } from 'class-validator';
3 | import { Expose } from 'class-transformer';
4 |
5 | import { AppService } from './app.service.js';
6 |
7 | export class Hello {
8 | @Expose()
9 | @IsString()
10 | public hello!: string;
11 | }
12 |
13 | @Controller()
14 | export class AppController {
15 | constructor(private readonly appService: AppService) {}
16 |
17 | @Get()
18 | public getHello(): string {
19 | return this.appService.getHello();
20 | }
21 |
22 | @Post()
23 | public postHello(@Body() testBody: Hello): string {
24 | return testBody.hello;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MikroOrmModule } from '@mikro-orm/nestjs';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 |
5 | import { AppController } from './app.controller.js';
6 | import { AppService } from './app.service.js';
7 | import { PrismaService } from './prisma/prisma.service.js';
8 | import { params } from '../../sources/typeorm/config.js';
9 | import { MongooseSchemasModule } from './mongoose/mongoose.module.js';
10 | import config from '../../sources/mikroorm/config.js';
11 | import { databaseProviders } from './database.providers.js';
12 |
13 | @Module({
14 | imports: [MikroOrmModule.forRoot(config), TypeOrmModule.forRoot(params), MongooseSchemasModule],
15 | controllers: [AppController],
16 | providers: [AppService, PrismaService, ...databaseProviders],
17 | })
18 | export class AppModule {}
19 |
--------------------------------------------------------------------------------
/src/servers/nestjs/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | public getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/servers/nestjs/database.providers.ts:
--------------------------------------------------------------------------------
1 | import { sequelize } from '../../sources/sequelize/index.js';
2 |
3 | export const databaseProviders = [
4 | {
5 | provide: 'SEQUELIZE',
6 | useFactory: () => sequelize,
7 | },
8 | ];
9 |
--------------------------------------------------------------------------------
/src/servers/nestjs/index.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ValidationPipe } from '@nestjs/common';
3 | import mongoose from 'mongoose';
4 |
5 | import { AppModule } from './app.module.js';
6 | import { setupAdminJS } from './admin/admin.setup.js';
7 | import { init } from '../../sources/mikroorm/config.js';
8 |
9 | const bootstrap = async () => {
10 | const app = await NestFactory.create(AppModule);
11 |
12 | app.useGlobalPipes(
13 | new ValidationPipe({
14 | transform: true,
15 | whitelist: true,
16 | }),
17 | );
18 |
19 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
20 | await init();
21 | await setupAdminJS(app);
22 |
23 | await app.listen(process.env.PORT);
24 | console.log(`AdminJS is under http://localhost:${process.env.PORT}/admin`);
25 | };
26 |
27 | bootstrap();
28 |
--------------------------------------------------------------------------------
/src/servers/nestjs/mongoose/mongoose.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MongooseModule } from '@nestjs/mongoose';
3 |
4 | import { AdminSchema } from '../../../sources/mongoose/models/admin.model.js';
5 | import { UserSchema } from '../../../sources/mongoose/models/user.model.js';
6 | import { ArticleSchema } from '../../../sources/mongoose/models/article.model.js';
7 | import { CategorySchema } from '../../../sources/mongoose/models/category.model.js';
8 | import { CommentSchema } from '../../../sources/mongoose/models/comment.model.js';
9 | import { ComplicatedSchema } from '../../../sources/mongoose/models/complicated.model.js';
10 |
11 | @Module({
12 | imports: [
13 | MongooseModule.forRoot(process.env.MONGO_DATABASE_URL),
14 | MongooseModule.forFeature([
15 | { name: 'Admin', schema: AdminSchema },
16 | { name: 'User', schema: UserSchema },
17 | { name: 'Article', schema: ArticleSchema },
18 | { name: 'Category', schema: CategorySchema },
19 | { name: 'Comment', schema: CommentSchema },
20 | { name: 'Complicated', schema: ComplicatedSchema },
21 | ]),
22 | ],
23 | exports: [MongooseModule],
24 | })
25 | export class MongooseSchemasModule {}
26 |
--------------------------------------------------------------------------------
/src/servers/nestjs/prisma/prisma.service.ts:
--------------------------------------------------------------------------------
1 | import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | @Injectable()
5 | export class PrismaService extends PrismaClient implements OnModuleInit {
6 | async onModuleInit() {
7 | await this.$connect();
8 | }
9 |
10 | async enableShutdownHooks(app: INestApplication) {
11 | this.$on('beforeExit', async () => {
12 | await app.close();
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/config.ts:
--------------------------------------------------------------------------------
1 | import { MikroORM, Options } from '@mikro-orm/core';
2 |
3 | import { Owner, Car, Seller } from './models/index.js';
4 |
5 | const driverOptions: Options['driverOptions'] = {};
6 |
7 | if (process.env.NODE_ENV === 'production') {
8 | driverOptions.connection = { ssl: true };
9 | }
10 |
11 | const config: Options = {
12 | entities: [Owner, Car, Seller],
13 | type: 'postgresql' as const,
14 | clientUrl: process.env.POSTGRES_DATABASE_URL,
15 | migrations: {
16 | path: 'src/sources/mikroorm/migrations',
17 | emit: 'ts',
18 | disableForeignKeys: false,
19 | },
20 | driverOptions,
21 | debug: process.env.DATABASE_LOGGING === 'true',
22 | };
23 |
24 | export const init = async () => {
25 | orm = await MikroORM.init(config);
26 | };
27 |
28 | export default config;
29 | export let orm: MikroORM;
30 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/migrations/Migration20220714094312.ts:
--------------------------------------------------------------------------------
1 | import { Migration } from '@mikro-orm/migrations';
2 |
3 | export class Migration20220714094312 extends Migration {
4 | async up(): Promise {
5 | this.addSql('create table "sellers" ("id" uuid not null, "name" text not null);');
6 | this.addSql('alter table "sellers" add constraint "sellers_pkey" primary key ("id");');
7 |
8 | this.addSql(
9 | 'create table "owners" ("id" uuid not null, "first_name" text not null, "last_name" text not null, "age" integer not null, "role" text check ("role" in (\'admin\', \'client\')) not null, "created_at" timestamptz not null, "updated_at" timestamptz not null);',
10 | );
11 | this.addSql('alter table "owners" add constraint "owners_pkey" primary key ("id");');
12 |
13 | this.addSql(
14 | 'create table "cars" ("id" serial primary key, "name" text not null, "meta" jsonb not null default \'{}\', "created_at" timestamptz not null, "updated_at" timestamptz not null, "owner_id" uuid not null, "seller_id" uuid not null);',
15 | );
16 |
17 | this.addSql(
18 | 'alter table "cars" add constraint "cars_owner_id_foreign" foreign key ("owner_id") references "owners" ("id") on update cascade;',
19 | );
20 | this.addSql(
21 | 'alter table "cars" add constraint "cars_seller_id_foreign" foreign key ("seller_id") references "sellers" ("id") on update cascade;',
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/car.model.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
2 |
3 | import type { Seller } from './seller.model.js';
4 | import type { Owner } from './owner.model.js';
5 |
6 | export interface ICar {
7 | name: string;
8 | meta: Record;
9 | owner: Owner;
10 | seller: Seller;
11 | }
12 |
13 | @Entity({ tableName: 'cars' })
14 | export class Car extends BaseEntity implements ICar {
15 | @PrimaryKey()
16 | id: number;
17 |
18 | @Property({ fieldName: 'name', columnType: 'text' })
19 | name: string;
20 |
21 | @Property({ fieldName: 'meta', columnType: 'jsonb', default: '{}' })
22 | meta: Record;
23 |
24 | @Property({ fieldName: 'created_at', columnType: 'timestamptz' })
25 | createdAt: Date = new Date();
26 |
27 | @Property({
28 | fieldName: 'updated_at',
29 | columnType: 'timestamptz',
30 | onUpdate: () => new Date(),
31 | })
32 | updatedAt: Date = new Date();
33 |
34 | @ManyToOne('Owner', { mapToPk: true })
35 | owner: Owner;
36 |
37 | @ManyToOne('Seller', { mapToPk: true })
38 | seller: Seller;
39 | }
40 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Owner } from './owner.model.js';
2 | export { Car } from './car.model.js';
3 | export { Seller } from './seller.model.js';
4 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/owner.model.ts:
--------------------------------------------------------------------------------
1 | import { v4 } from 'uuid';
2 | import { BaseEntity, Entity, Enum, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
3 |
4 | import type { Car } from './car.model.js';
5 |
6 | export enum UserRole {
7 | ADMIN = 'admin',
8 | CLIENT = 'client',
9 | }
10 |
11 | export interface IOwner {
12 | firstName: string;
13 | lastName: string;
14 | age: number;
15 | role: UserRole;
16 | }
17 |
18 | @Entity({ tableName: 'owners' })
19 | export class Owner extends BaseEntity implements IOwner {
20 | @PrimaryKey({ columnType: 'uuid' })
21 | id = v4();
22 |
23 | @Property({ fieldName: 'first_name', columnType: 'text' })
24 | firstName: string;
25 |
26 | @Property({ fieldName: 'last_name', columnType: 'text' })
27 | lastName: string;
28 |
29 | @Property({ fieldName: 'age', columnType: 'integer' })
30 | age: number;
31 |
32 | @Enum(() => UserRole)
33 | role: UserRole;
34 |
35 | @Property({ fieldName: 'created_at', columnType: 'timestamptz' })
36 | createdAt: Date = new Date();
37 |
38 | @Property({
39 | fieldName: 'updated_at',
40 | columnType: 'timestamptz',
41 | onUpdate: () => new Date(),
42 | })
43 | updatedAt: Date = new Date();
44 |
45 | @OneToMany('Car', (car: Car) => car.owner)
46 | cars: Car[];
47 | }
48 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/models/seller.model.ts:
--------------------------------------------------------------------------------
1 | import { Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
2 | import { v4 } from 'uuid';
3 |
4 | import type { Car } from './car.model.js';
5 |
6 | export interface ISeller {
7 | name: string;
8 | }
9 |
10 | @Entity({ tableName: 'sellers' })
11 | export class Seller implements ISeller {
12 | @PrimaryKey({ columnType: 'uuid' })
13 | public id = v4();
14 |
15 | @Property({ fieldName: 'name', columnType: 'text' })
16 | name: string;
17 |
18 | @OneToMany('Car', (car: Car) => car.seller)
19 | cars: Car[];
20 | }
21 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/car.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Car } from '../models/index.js';
6 |
7 | export const CreateCarResource: ResourceFunction<{ model: typeof Car; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Car,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | properties: {
16 | meta: { type: 'mixed' },
17 | 'meta.title': {
18 | type: 'string',
19 | },
20 | 'meta.description': {
21 | type: 'string',
22 | },
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateOwnerResource } from './owner.resource.js';
2 | export { CreateCarResource } from './car.resource.js';
3 | export { CreateSellerResource } from './seller.resource.js';
4 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/owner.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Owner } from '../models/index.js';
6 |
7 | export const CreateOwnerResource: ResourceFunction<{ model: typeof Owner; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Owner,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | properties: {
16 | lastName: { isTitle: true },
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/resources/seller.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { orm } from '../config.js';
5 | import { Seller } from '../models/index.js';
6 |
7 | export const CreateSellerResource: ResourceFunction<{ model: typeof Seller; orm: typeof orm }> = () => ({
8 | resource: {
9 | model: Seller,
10 | orm,
11 | },
12 | features: [useEnvironmentVariableToDisableActions()],
13 | options: {
14 | navigation: menu.mikroorm,
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/cars.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { Owner, Seller } from '../../models/index.js';
3 |
4 | const cars = (count: number, { owners, sellers }) =>
5 | Array.from({ length: count }, () => ({
6 | name: `${faker.vehicle.manufacturer()} ${faker.vehicle.model()}`,
7 | owner: faker.helpers.arrayElement(owners).id,
8 | seller: faker.helpers.arrayElement(sellers).id,
9 | meta: {
10 | title: faker.lorem.slug(),
11 | description: faker.lorem.sentences(3),
12 | },
13 | }));
14 |
15 | export default cars;
16 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as cars } from './cars.js';
2 | export { default as owners } from './owners.js';
3 | export { default as sellers } from './sellers.js';
4 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/owners.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { IOwner, UserRole } from '../../models/owner.model.js';
4 |
5 | const owners = (count: number): IOwner[] =>
6 | Array.from({ length: count }, () => ({
7 | firstName: faker.name.firstName(),
8 | lastName: faker.name.lastName(),
9 | age: Number(faker.random.numeric(2)),
10 | role: faker.helpers.arrayElement(Object.values(UserRole)),
11 | }));
12 |
13 | export default owners;
14 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/data/sellers.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { ISeller } from '../../models/seller.model.js';
4 |
5 | const sellers = (count: number): ISeller[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.company.name(),
8 | }));
9 |
10 | export default sellers;
11 |
--------------------------------------------------------------------------------
/src/sources/mikroorm/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { init, orm } from '../config.js';
7 | import { Car, Owner, Seller } from '../models/index.js';
8 | import { cars, owners, sellers } from './data/index.js';
9 |
10 | const ownersCount = 4;
11 | const sellersCount = 4;
12 | const carsCount = 15;
13 |
14 | const run = async () => {
15 | await init();
16 | const ownerRepository = orm.em.getRepository(Owner);
17 | const createdOwners = owners(ownersCount).map((o) => {
18 | const owner = ownerRepository.create(o);
19 | orm.em.persist(owner);
20 |
21 | return owner;
22 | });
23 | await orm.em.flush();
24 |
25 | const sellerRepository = orm.em.getRepository(Seller);
26 | const createdSellers = sellers(sellersCount).map((s) => {
27 | const seller = sellerRepository.create(s);
28 | orm.em.persist(seller);
29 |
30 | return seller;
31 | });
32 | await orm.em.flush();
33 |
34 | const carRepository = orm.em.getRepository(Car);
35 | cars(carsCount, {
36 | owners: createdOwners,
37 | sellers: createdSellers,
38 | }).map((c) => {
39 | const car = carRepository.create(c);
40 | orm.em.persist(car);
41 |
42 | return car;
43 | });
44 | await orm.em.flush();
45 | };
46 |
47 | run()
48 | .then(() => process.exit(0))
49 | .catch((e) => {
50 | console.log(e);
51 | process.exit(1);
52 | });
53 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/admin.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema } from 'mongoose';
2 |
3 | export interface Admin {
4 | email: string;
5 | password: string;
6 | }
7 |
8 | export const AdminSchema = new Schema({
9 | email: { type: 'String', required: true },
10 | password: { type: 'String', required: true },
11 | });
12 |
13 | export const AdminModel = model('Admin', AdminSchema);
14 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/article.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Article {
4 | title: string;
5 | content: string;
6 | photo: string;
7 | author: Types.ObjectId;
8 | category: Types.ObjectId;
9 | published: boolean;
10 | }
11 |
12 | export const ArticleSchema = new Schema({
13 | title: { type: 'String', required: true },
14 | content: { type: 'String', required: true },
15 | photo: { type: 'String' },
16 | author: { type: Schema.Types.ObjectId, ref: 'User' },
17 | category: { type: Schema.Types.ObjectId, ref: 'Category', required: true },
18 | published: { type: 'Boolean' },
19 | });
20 |
21 | export const ArticleModel = model('Article', ArticleSchema);
22 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/category.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Category {
4 | title: string;
5 | owner: Types.ObjectId;
6 | nested: {
7 | field: string;
8 | value: string;
9 | };
10 | createdAt: Date;
11 | updatedAt: Date;
12 | }
13 |
14 | export const CategorySchema = new Schema(
15 | {
16 | title: { type: 'String', required: true },
17 | owner: { type: Schema.Types.ObjectId, ref: 'User' },
18 | nested: new Schema({ field: 'String', value: 'String' }),
19 | },
20 | { timestamps: true },
21 | );
22 |
23 | export const CategoryModel = model('Category', CategorySchema);
24 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/comment.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Comment {
4 | content: string;
5 | flagged: boolean;
6 | article: Types.ObjectId;
7 | createdAt: Date;
8 | updatedAt: Date;
9 | }
10 |
11 | export const CommentSchema = new Schema({
12 | content: { type: 'String', required: true },
13 | article: { type: Schema.Types.ObjectId, ref: 'Article', required: true },
14 | flagged: { type: 'Boolean' },
15 | });
16 |
17 | export const CommentModel = model('Comment', CommentSchema);
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/complicated.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema, Types } from 'mongoose';
2 |
3 | export interface Complicated {
4 | name: string;
5 | stringArray: string[];
6 | authors: Types.ObjectId[];
7 | nestedDetails: {
8 | age: number;
9 | height: number;
10 | placeOfBirth: string;
11 | nested: {
12 | extremelyNested: string;
13 | };
14 | };
15 | parents: Types.ObjectId[];
16 | items: {
17 | imageVariants: {
18 | imageUrl: string;
19 | isApproved: boolean;
20 | dateCreated: Date;
21 | isDeleted: boolean;
22 | }[];
23 | }[];
24 | }
25 |
26 | export const ComplicatedSchema = new Schema({
27 | name: { type: 'String', required: true },
28 | stringArray: { type: ['String'] },
29 | authors: [{ type: Types.ObjectId, ref: 'User' }],
30 | nestedDetails: new Schema(
31 | {
32 | age: { type: 'Number', required: true },
33 | height: { type: 'Number', required: true },
34 | placeOfBirth: { type: 'String', required: true },
35 | nested: new Schema(
36 | {
37 | extremelyNested: { type: 'String', required: true },
38 | },
39 | { _id: false },
40 | ),
41 | },
42 | { _id: false },
43 | ),
44 | parents: [
45 | new Schema(
46 | {
47 | firstName: { type: 'String', required: true },
48 | lastName: { type: 'String', required: true },
49 | },
50 | { _id: false },
51 | ),
52 | ],
53 | items: [
54 | new Schema(
55 | {
56 | imageVariants: [
57 | new Schema(
58 | {
59 | imageUrl: { type: 'String', required: true },
60 | isApproved: { type: 'Boolean' },
61 | isDeleted: { type: 'Boolean' },
62 | dateCreated: { type: 'Date', default: Date.now },
63 | },
64 | { _id: false },
65 | ),
66 | ],
67 | },
68 | { _id: false },
69 | ),
70 | ],
71 | });
72 |
73 | export const ComplicatedModel = model('Complicated', ComplicatedSchema);
74 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/index.ts:
--------------------------------------------------------------------------------
1 | export { UserModel } from './user.model.js';
2 | export { AdminModel } from './admin.model.js';
3 | export { ArticleModel } from './article.model.js';
4 | export { CategoryModel } from './category.model.js';
5 | export { CommentModel } from './comment.model.js';
6 | export { ComplicatedModel } from './complicated.model.js';
7 |
--------------------------------------------------------------------------------
/src/sources/mongoose/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import { model, Schema } from 'mongoose';
2 |
3 | export enum Gender {
4 | Male = 'male',
5 | Female = 'female',
6 | }
7 |
8 | export interface User {
9 | firstName: string;
10 | lastName: string;
11 | gender: Gender;
12 | email: string;
13 | isMyFavourite: boolean;
14 | }
15 |
16 | export const UserSchema = new Schema({
17 | firstName: { type: 'String', required: true },
18 | lastName: { type: 'String', required: true },
19 | gender: { type: 'String', required: true, enum: Gender },
20 | email: { type: 'String', required: true },
21 | isMyFavourite: { type: 'Boolean', required: true },
22 | });
23 |
24 | export const UserModel = model('User', UserSchema);
25 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/admin.resource.ts:
--------------------------------------------------------------------------------
1 | import passwordsFeature from '@adminjs/passwords';
2 | import argon2 from 'argon2';
3 |
4 | import { componentLoader } from '../../../admin/components.bundler.js';
5 | import { menu } from '../../../admin/index.js';
6 | import { ResourceFunction } from '../../../admin/types/index.js';
7 | import { AdminModel } from '../models/index.js';
8 |
9 | export const CreateAdminResource: ResourceFunction = () => ({
10 | resource: AdminModel,
11 | features: [
12 | (options): object => ({
13 | ...options,
14 | listProperties: ['_id', 'email'],
15 | showProperties: ['_id', 'email'],
16 | editProperties: ['email', 'newPassword'],
17 | }),
18 | passwordsFeature({
19 | properties: {
20 | password: 'newPassword',
21 | encryptedPassword: 'password',
22 | },
23 | hash: argon2.hash,
24 | componentLoader,
25 | }),
26 | ],
27 | options: {
28 | navigation: menu.mongoose,
29 | sort: {
30 | direction: 'asc',
31 | sortBy: 'email',
32 | },
33 | actions: {
34 | delete: { isAccessible: false },
35 | bulkDelete: { isAccessible: false },
36 | edit: { isAccessible: false },
37 | new: { isAccessible: false },
38 | },
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/article.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { THUMB } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { ArticleModel } from '../models/index.js';
6 |
7 | export const CreateArticleResource: ResourceFunction = () => ({
8 | resource: ArticleModel,
9 | features: [useEnvironmentVariableToDisableActions()],
10 | options: {
11 | navigation: menu.mongoose,
12 | properties: {
13 | _id: {
14 | isTitle: true,
15 | },
16 | content: {
17 | type: 'richtext',
18 | },
19 | published: {
20 | components: {
21 | list: THUMB,
22 | },
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/category.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CategoryModel } from '../models/index.js';
5 |
6 | export const CreateCategoryResource: ResourceFunction = () => ({
7 | resource: CategoryModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | actions: {
12 | show: {
13 | showInDrawer: true,
14 | },
15 | edit: {
16 | showInDrawer: true,
17 | },
18 | new: {
19 | showInDrawer: true,
20 | },
21 | },
22 | properties: {
23 | _id: {
24 | isTitle: true,
25 | },
26 | },
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/comment.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CommentModel } from '../models/index.js';
5 |
6 | export const CreateCommentResource: ResourceFunction = () => ({
7 | resource: CommentModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | actions: {
12 | show: {
13 | isAccessible: false,
14 | },
15 | edit: {
16 | showInDrawer: true,
17 | },
18 | },
19 | properties: {
20 | _id: {
21 | isTitle: true,
22 | },
23 | content: {
24 | type: 'textarea',
25 | },
26 | },
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/complicated.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { ComplicatedModel } from '../models/index.js';
5 |
6 | export const CreateComplicatedResource: ResourceFunction = () => ({
7 | resource: ComplicatedModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | properties: {
12 | _id: {
13 | isTitle: true,
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateAdminResource } from './admin.resource.js';
2 | export { CreateUserResource } from './user.resource.js';
3 | export { CreateArticleResource } from './article.resource.js';
4 | export { CreateCategoryResource } from './category.resource.js';
5 | export { CreateCommentResource } from './comment.resource.js';
6 | export { CreateComplicatedResource } from './complicated.resource.js';
7 |
--------------------------------------------------------------------------------
/src/sources/mongoose/resources/user.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { UserModel } from '../models/index.js';
5 |
6 | export const CreateUserResource: ResourceFunction = () => ({
7 | resource: UserModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.mongoose,
11 | properties: {
12 | _id: {
13 | isTitle: true,
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/articles.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Article } from '../../models/article.model.js';
4 |
5 | const articles = (count: number, { authorId, categoryId }): Article[] =>
6 | Array.from({ length: count }, () => ({
7 | title: faker.lorem.sentence(4),
8 | content: faker.lorem.paragraphs(3),
9 | photo: faker.image.imageUrl(80, 80),
10 | published: faker.datatype.boolean(),
11 | author: authorId,
12 | category: categoryId,
13 | createdAt: new Date(),
14 | updatedAt: new Date(),
15 | }));
16 |
17 | export default articles;
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/categories.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Category } from '../../models/category.model.js';
4 |
5 | const categories = (count: number, { userId }): Category[] =>
6 | Array.from({ length: count }, () => ({
7 | title: faker.commerce.department(),
8 | owner: userId,
9 | nested: {
10 | field: faker.database.column(),
11 | value: faker.lorem.word(),
12 | },
13 | createdAt: new Date(),
14 | updatedAt: new Date(),
15 | }));
16 |
17 | export default categories;
18 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/comments.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { Comment } from '../../models/comment.model.js';
4 |
5 | const comments = (count: number, { articleId }): Comment[] =>
6 | Array.from({ length: count }, () => ({
7 | content: faker.lorem.paragraph(3),
8 | flagged: faker.datatype.boolean(),
9 | article: articleId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default comments;
15 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as articles } from './articles.js';
2 | export { default as categories } from './categories.js';
3 | export { default as comments } from './comments.js';
4 | export { default as users } from './users.js';
5 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/data/users.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { User, Gender } from '../../models/user.model.js';
4 |
5 | const users = (count: number): User[] =>
6 | Array.from({ length: count }, () => ({
7 | firstName: faker.name.firstName(),
8 | lastName: faker.name.lastName(),
9 | gender: faker.name.sex() as Gender,
10 | email: faker.internet.email(),
11 | isMyFavourite: faker.datatype.boolean(),
12 | }));
13 |
14 | export default users;
15 |
--------------------------------------------------------------------------------
/src/sources/mongoose/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import flatten from 'lodash/flatten.js';
7 | import mongoose from 'mongoose';
8 | import { ArticleModel, CategoryModel, CommentModel, UserModel } from '../models/index.js';
9 | import { articles, categories, comments, users } from './data/index.js';
10 |
11 | const usersCount = 3;
12 | const categoriesPerUserCount = 1;
13 | const articlesPerCategoryCount = 1;
14 | const commentsPerArticleCount = 5;
15 |
16 | const run = async () => {
17 | await mongoose.connect(process.env.MONGO_DATABASE_URL);
18 |
19 | const createdUsers = await Promise.all(users(usersCount).map((u) => new UserModel(u).save()));
20 | const createdCategories: Record[] = flatten(
21 | await Promise.all(
22 | createdUsers.map((u) =>
23 | Promise.all(categories(categoriesPerUserCount, { userId: u._id }).map((c) => new CategoryModel(c).save())),
24 | ),
25 | ),
26 | );
27 | const createdArticles: Record[] = flatten(
28 | await Promise.all(
29 | createdUsers.map((u, idx) =>
30 | Promise.all(
31 | articles(articlesPerCategoryCount, { authorId: u._id, categoryId: createdCategories[idx]._id }).map((a) =>
32 | new ArticleModel(a).save(),
33 | ),
34 | ),
35 | ),
36 | ),
37 | );
38 | await Promise.all(
39 | createdArticles.map((a) =>
40 | Promise.all(comments(commentsPerArticleCount, { articleId: a._id }).map((c) => new CommentModel(c).save())),
41 | ),
42 | );
43 | // TODO: Add ComplicatedModel seeds
44 | };
45 |
46 | run()
47 | .then(() => process.exit(0))
48 | .catch((e) => {
49 | console.log(e);
50 | process.exit(1);
51 | });
52 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/knexfile.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | development: {
3 | client: 'pg',
4 | useNullAsDefault: true,
5 | connection: process.env.POSTGRES_DATABASE_URL,
6 | migrations: {
7 | tableName: 'knex_migrations',
8 | },
9 | },
10 | production: {
11 | client: 'pg',
12 | connection: {
13 | connectionString: process.env.POSTGRES_DATABASE_URL,
14 | ssl: {
15 | rejectUnauthorized: false,
16 | require: true,
17 | },
18 | },
19 | pool: {
20 | min: 2,
21 | max: 10,
22 | },
23 | migrations: {
24 | tableName: 'knex_migrations',
25 | },
26 | },
27 | };
28 |
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/migrations/20220826123456_initial_schema.cjs:
--------------------------------------------------------------------------------
1 | exports.up = (knex) =>
2 | knex.schema
3 | .createTable('offices', (table) => {
4 | table.increments('id').primary();
5 | table.string('name');
6 | table.timestamps(true, true, true);
7 | table.jsonb('address');
8 | })
9 | .createTable('managers', (table) => {
10 | table.increments('id').primary();
11 |
12 | table.string('firstName');
13 | table.string('lastName');
14 | table.integer('age');
15 | table.timestamps(true, true, true);
16 | table.integer('officeId').unsigned().references('id').inTable('offices').onDelete('CASCADE').index();
17 | });
18 |
19 | exports.down = (knex) => knex.schema.dropTableIfExists('managers').dropTableIfExists('offices');
20 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/index.ts:
--------------------------------------------------------------------------------
1 | import Knex from 'knex';
2 |
3 | import knexConfig from '../knexfile.cjs';
4 | import Office from './office.entity.js';
5 | import Manager from './manager.entity.js';
6 | import { BaseModel } from '../utils/base-model.js';
7 |
8 | const knex = BaseModel.knex(Knex.default(knexConfig[process.env.NODE_ENV ?? 'development']));
9 |
10 | export { Office, Manager };
11 | export default knex;
12 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/manager.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel } from '../utils/base-model.js';
2 |
3 | import Office from './office.entity.js';
4 |
5 | class Manager extends BaseModel {
6 | id: number;
7 |
8 | firstName: string;
9 |
10 | lastName: string;
11 |
12 | age: number;
13 |
14 | officeId?: number;
15 |
16 | office?: Office;
17 |
18 | static tableName = 'managers';
19 |
20 | static jsonSchema = {
21 | type: 'object',
22 | required: ['firstName', 'lastName'],
23 |
24 | properties: {
25 | id: { type: 'integer' },
26 | officeId: { type: 'integer' },
27 | firstName: { type: 'string', minLength: 1, maxLength: 255 },
28 | lastName: { type: 'string', minLength: 1, maxLength: 255 },
29 | age: { type: 'number' },
30 | createdAt: { type: 'string', format: 'date-time', readOnly: true },
31 | updatedAt: { type: 'string', format: 'date-time', readOnly: true },
32 | },
33 | };
34 |
35 | static relationMappings = () => ({
36 | office: {
37 | relation: BaseModel.BelongsToOneRelation,
38 | modelClass: Office,
39 | join: {
40 | from: 'managers.officeId',
41 | to: 'offices.id',
42 | },
43 | },
44 | });
45 | }
46 |
47 | export default Manager;
48 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/models/office.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseModel } from '../utils/base-model.js';
2 |
3 | import Manager from './manager.entity.js';
4 |
5 | export interface OfficeAddress {
6 | street: string;
7 | city: string;
8 | zipCode: string;
9 | }
10 |
11 | class Office extends BaseModel {
12 | id: number;
13 |
14 | name: string;
15 |
16 | address?: OfficeAddress;
17 |
18 | managers?: Manager[];
19 |
20 | static tableName = 'offices';
21 |
22 | static jsonSchema = {
23 | type: 'object',
24 | required: ['name'],
25 | properties: {
26 | id: { type: 'integer' },
27 | name: { type: 'string', minLength: 1, maxLength: 255 },
28 | address: {
29 | type: 'object',
30 | properties: {
31 | street: { type: 'string' },
32 | city: { type: 'string' },
33 | zipCode: { type: 'string' },
34 | },
35 | },
36 | createdAt: { type: 'string', format: 'date-time' },
37 | updatedAt: { type: 'string', format: 'date-time' },
38 | },
39 | };
40 |
41 | static relationMappings = () => ({
42 | managers: {
43 | relation: BaseModel.HasManyRelation,
44 | modelClass: Manager,
45 | join: {
46 | from: 'offices.id',
47 | to: 'managers.officeId',
48 | },
49 | },
50 | });
51 | }
52 |
53 | export default Office;
54 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateManagerResource } from './manager.resource.js';
2 | export { CreateOfficeResource } from './office.resource.js';
3 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/manager.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Manager } from '../models/index.js';
5 |
6 | export const CreateManagerResource: ResourceFunction = () => ({
7 | resource: Manager,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.objection,
11 | properties: {
12 | createdAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
13 | updatedAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/resources/office.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Office } from '../models/index.js';
5 |
6 | export const CreateOfficeResource: ResourceFunction = () => ({
7 | resource: Office,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.objection,
11 | properties: {
12 | createdAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
13 | updatedAt: { isVisible: { edit: false, list: true, show: true, filter: true } },
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/src/sources/objectionjs/utils/base-model.ts:
--------------------------------------------------------------------------------
1 | import addFormatsPlugin from 'ajv-formats';
2 | import { AjvValidator, Model } from 'objection';
3 |
4 | const addFormats = addFormatsPlugin.default;
5 |
6 | // https://github.com/Vincit/objection.js/issues/2249#issuecomment-1075898552
7 | // There is no support for Date type in JSONSchema, so this class does two things for us:
8 | // 1. It adds support to formats such as `format: "date-format"` in your "jsonSchema"
9 | // 2. It adds $beforeInsert and $beforeUpdate hooks to update your timestamps
10 | // AdminJS can not load your models properly without it.
11 | export abstract class BaseModel extends Model {
12 | createdAt: string;
13 |
14 | updatedAt: string;
15 |
16 | static createValidator(): AjvValidator {
17 | return new AjvValidator({
18 | onCreateAjv: (ajv) => {
19 | addFormats(ajv);
20 | },
21 | options: {
22 | allErrors: true,
23 | validateSchema: false,
24 | ownProperties: true,
25 | },
26 | });
27 | }
28 |
29 | $beforeInsert(): void {
30 | this.createdAt = new Date().toISOString();
31 | }
32 |
33 | $beforeUpdate(): void {
34 | this.updatedAt = new Date().toISOString();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/sources/prisma/config.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 | import { DMMFClass } from '@prisma/client/runtime';
3 |
4 | export const client = new PrismaClient();
5 |
6 | export const dmmf = (client as any)._baseDmmf as DMMFClass;
7 |
--------------------------------------------------------------------------------
/src/sources/prisma/migrations/20220218111814_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Post` (
3 | `id` INTEGER NOT NULL AUTO_INCREMENT,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `title` VARCHAR(255) NOT NULL,
7 | `content` VARCHAR(191) NULL,
8 | `someJson` JSON NULL,
9 | `status` ENUM('ACTIVE', 'REMOVED') NOT NULL DEFAULT 'ACTIVE',
10 | `published` BOOLEAN NOT NULL DEFAULT false,
11 | `publisherId` INTEGER NOT NULL,
12 |
13 | PRIMARY KEY (`id`)
14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
15 |
16 | -- CreateTable
17 | CREATE TABLE `Profile` (
18 | `id` INTEGER NOT NULL AUTO_INCREMENT,
19 | `bio` VARCHAR(191) NULL,
20 | `publisherId` INTEGER NOT NULL,
21 |
22 | UNIQUE INDEX `Profile_publisherId_key`(`publisherId`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
26 | -- CreateTable
27 | CREATE TABLE `Publisher` (
28 | `id` INTEGER NOT NULL AUTO_INCREMENT,
29 | `email` VARCHAR(191) NOT NULL,
30 | `name` VARCHAR(191) NULL,
31 |
32 | UNIQUE INDEX `Publisher_email_key`(`email`),
33 | PRIMARY KEY (`id`)
34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
35 |
36 | -- AddForeignKey
37 | ALTER TABLE `Post` ADD CONSTRAINT `Post_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
39 | -- AddForeignKey
40 | ALTER TABLE `Profile` ADD CONSTRAINT `Profile_publisherId_fkey` FOREIGN KEY (`publisherId`) REFERENCES `Publisher`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
41 |
--------------------------------------------------------------------------------
/src/sources/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/src/sources/prisma/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreatePostResource } from './post.resource.js';
2 | export { CreateProfileResource } from './profile.resource.js';
3 | export { CreatePublisherResource } from './publisher.resource.js';
4 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/post.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreatePostResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Post;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Post,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | properties: {
18 | content: { type: 'richtext' },
19 | someJson: { type: 'mixed', isArray: true },
20 | 'someJson.number': { type: 'number' },
21 | 'someJson.string': { type: 'string' },
22 | 'someJson.boolean': { type: 'boolean' },
23 | 'someJson.date': { type: 'datetime' },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/profile.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreateProfileResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Profile;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Profile,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/sources/prisma/resources/publisher.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { client, dmmf } from '../config.js';
5 |
6 | export const CreatePublisherResource: ResourceFunction<{
7 | model: typeof dmmf.modelMap.Publisher;
8 | client: typeof client;
9 | }> = () => ({
10 | resource: {
11 | model: dmmf.modelMap.Publisher,
12 | client,
13 | },
14 | features: [useEnvironmentVariableToDisableActions()],
15 | options: {
16 | navigation: menu.prisma,
17 | },
18 | });
19 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as posts } from './posts.js';
2 | export { default as profiles } from './profiles.js';
3 | export { default as publishers } from './publishers.js';
4 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/posts.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const publishers = (count: number, { publishers }) =>
4 | Array.from({ length: count }, () => ({
5 | title: faker.lorem.sentence(3),
6 | content: faker.lorem.sentence(8),
7 | status: faker.helpers.arrayElement(['ACTIVE', 'REMOVED']) as any,
8 | published: faker.datatype.boolean(),
9 | someJson: [
10 | {
11 | number: faker.random.numeric(8),
12 | string: faker.lorem.words(3),
13 | boolean: faker.datatype.boolean(),
14 | date: faker.date.recent().toISOString(),
15 | },
16 | ],
17 | publisherId: faker.helpers.arrayElement(publishers).id,
18 | }));
19 |
20 | export default publishers;
21 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/profiles.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const profiles = (count: number, { publisherId }) =>
4 | Array.from({ length: count }, () => ({
5 | bio: faker.lorem.sentence(4),
6 | publisherId,
7 | }));
8 |
9 | export default profiles;
10 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/data/publishers.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | const publishers = (count: number) =>
4 | Array.from({ length: count }, () => ({
5 | name: faker.company.name(),
6 | email: faker.internet.email(),
7 | }));
8 |
9 | export default publishers;
10 |
--------------------------------------------------------------------------------
/src/sources/prisma/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import { PrismaClient } from '@prisma/client';
7 | import { publishers, posts, profiles } from './data/index.js';
8 |
9 | const publishersCount = 5;
10 | const postsCount = 20;
11 |
12 | const run = async () => {
13 | const prisma = new PrismaClient();
14 | await prisma.$connect();
15 |
16 | const createdPublishers = await prisma.$transaction(
17 | publishers(publishersCount).map((publisher) => prisma.publisher.create({ data: publisher })),
18 | );
19 | await prisma.$transaction(
20 | createdPublishers.map((publisher) =>
21 | prisma.profile.create({ data: profiles(1, { publisherId: publisher.id })[0] }),
22 | ),
23 | );
24 | await prisma.post.createMany({ data: posts(postsCount, { publishers: createdPublishers }) });
25 | };
26 |
27 | run()
28 | .then(() => process.exit(0))
29 | .catch((e) => {
30 | console.log(e);
31 | process.exit(1);
32 | });
33 |
--------------------------------------------------------------------------------
/src/sources/rest/crypto-database.ts:
--------------------------------------------------------------------------------
1 | import AdminJS, { BaseResource, BaseProperty, BaseRecord, ResourceOptions } from 'adminjs';
2 | import axios from 'axios';
3 |
4 | import { menu } from '../../admin/index.js';
5 |
6 | export class CryptoDatabase extends BaseResource {
7 | public totalCount = 0;
8 |
9 | private readonly _properties: BaseProperty[] = [
10 | new BaseProperty({
11 | path: 'website_slug',
12 | type: 'string',
13 | isId: true,
14 | isSortable: false,
15 | position: 1,
16 | }),
17 | new BaseProperty({
18 | path: 'name',
19 | type: 'string',
20 | isSortable: false,
21 | position: 2,
22 | }),
23 | new BaseProperty({
24 | path: 'symbol',
25 | type: 'string',
26 | isSortable: false,
27 | position: 3,
28 | }),
29 | new BaseProperty({
30 | path: 'quotes.USD.price',
31 | type: 'string',
32 | isSortable: false,
33 | position: 4,
34 | }),
35 | new BaseProperty({
36 | path: 'quotes.USD.percent_change_1h',
37 | type: 'string',
38 | isSortable: false,
39 | position: 5,
40 | }),
41 | new BaseProperty({
42 | path: 'quotes.USD.percent_change_24h',
43 | type: 'string',
44 | isSortable: false,
45 | position: 6,
46 | }),
47 | new BaseProperty({
48 | path: 'quotes.USD.percent_change_7d',
49 | type: 'string',
50 | isSortable: false,
51 | position: 7,
52 | }),
53 | new BaseProperty({
54 | path: 'last_updated',
55 | type: 'date',
56 | isSortable: false,
57 | position: 8,
58 | }),
59 | ];
60 |
61 | public assignDecorator(admin: AdminJS, options?: ResourceOptions) {
62 | super.assignDecorator(admin, {
63 | ...options,
64 | navigation: menu.rest,
65 | listProperties: ['website_slug', 'name', 'symbol', 'quotes.USD.price'],
66 | actions: {
67 | list: {
68 | showFilter: false,
69 | },
70 | show: {
71 | showInDrawer: true,
72 | },
73 | delete: {
74 | isAccessible: false,
75 | },
76 | bulkDelete: {
77 | isAccessible: false,
78 | },
79 | new: {
80 | isAccessible: false,
81 | },
82 | edit: {
83 | isAccessible: false,
84 | },
85 | },
86 | });
87 | }
88 |
89 | public id(): string {
90 | return 'crypto-database';
91 | }
92 |
93 | public name(): string {
94 | return 'Crypto Database';
95 | }
96 |
97 | public properties(): BaseProperty[] {
98 | return this._properties;
99 | }
100 |
101 | public property(path): BaseProperty {
102 | return this._properties.find((p) => p.path === path);
103 | }
104 |
105 | async count(): Promise {
106 | return this.totalCount;
107 | }
108 |
109 | async find(query, { limit = 20, offset = 0 }): Promise {
110 | const { data } = await axios.get('https://api.alternative.me/v2/ticker/', {
111 | params: { start: offset / limit, limit, structure: 'array' },
112 | });
113 | this.totalCount = data.metadata.num_cryptocurrencies;
114 |
115 | return data.data.map((obj) => new BaseRecord(obj, this));
116 | }
117 |
118 | async findOne(id): Promise {
119 | const { data } = await axios.get(`https://api.alternative.me/v2/ticker/${id}/`, {
120 | params: { structure: 'array' },
121 | });
122 | const record = data.data[0] as { last_updated: number };
123 |
124 | return new BaseRecord(
125 | {
126 | ...record,
127 | last_updated: record.last_updated ? new Date(record.last_updated * 1000) : null,
128 | },
129 | this,
130 | );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/sources/sequelize/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | development: {
3 | use_env_variable: 'POSTGRES_DATABASE_URL',
4 | dialect: 'postgres',
5 | },
6 | production: {
7 | use_env_variable: 'POSTGRES_DATABASE_URL',
8 | dialect: 'postgres',
9 | dialectOptions: {
10 | ssl: {
11 | require: true,
12 | rejectUnauthorized: false,
13 | },
14 | },
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/sources/sequelize/functions/get-sum.function.ts:
--------------------------------------------------------------------------------
1 | import { CartModel, OrderModel, ProductModel } from '../models/index.js';
2 |
3 | export const getSum = async (id: number): Promise => {
4 | const order = await OrderModel.findByPk(id, {
5 | include: [
6 | {
7 | model: CartModel,
8 | as: 'carts',
9 | include: [
10 | {
11 | model: ProductModel,
12 | as: 'product',
13 | },
14 | ],
15 | },
16 | ],
17 | });
18 |
19 | return (
20 | order.carts.reduce((curr, next) => {
21 | const price = parseInt(next.product.price, 10) * next.quantity;
22 | return curr + price;
23 | }, 0) / 100
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/sources/sequelize/hooks/get-products.hook.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest, ActionResponse, After, flat } from 'adminjs';
2 |
3 | import { isGETMethod } from '../../../admin/admin.utils.js';
4 | import { CartModel, OrderModel, ProductModel } from '../models/index.js';
5 | import { ProductListInterface } from '../interfaces.js';
6 |
7 | export const getProducts =
8 | (): After =>
9 | async (response: ActionResponse, request: ActionRequest): Promise => {
10 | if (!isGETMethod(request)) {
11 | return response;
12 | }
13 |
14 | const id = parseInt(request.params.recordId, 10);
15 |
16 | const order = await OrderModel.findByPk(id, {
17 | include: [
18 | {
19 | model: CartModel,
20 | as: 'carts',
21 | include: [
22 | {
23 | model: ProductModel,
24 | as: 'product',
25 | },
26 | ],
27 | },
28 | ],
29 | });
30 |
31 | const params = flat.unflatten(response.record.params);
32 | params.products = order.carts?.map((cart) => ({
33 | quantity: cart.quantity,
34 | product: {
35 | id: cart.product?.id,
36 | name: cart.product?.name,
37 | price: parseFloat(cart.product?.price),
38 | },
39 | }));
40 |
41 | response.record.params = flat.flatten(params);
42 |
43 | return response;
44 | };
45 |
--------------------------------------------------------------------------------
/src/sources/sequelize/hooks/get-sum.hook.ts:
--------------------------------------------------------------------------------
1 | import { ActionRequest, ActionResponse, After, flat } from 'adminjs';
2 |
3 | import { isGETMethod } from '../../../admin/admin.utils.js';
4 | import { getSum } from '../functions/get-sum.function.js';
5 | import { ProductSumInterface } from '../interfaces.js';
6 |
7 | export const getSumForOrder =
8 | (): After =>
9 | async (response: ActionResponse, request: ActionRequest): Promise => {
10 | if (!isGETMethod(request)) {
11 | return response;
12 | }
13 |
14 | if (response.records) {
15 | await Promise.all(
16 | response.records.map(async (record) => {
17 | const params = flat.unflatten(record.params);
18 | params.sum = await getSum(params.id);
19 | record.params = flat.flatten(params);
20 | }),
21 | );
22 | } else {
23 | const params = flat.unflatten(response.record.params);
24 | params.sum = await getSum(params.id);
25 | response.record.params = flat.flatten(params);
26 | }
27 |
28 | return response;
29 | };
30 |
--------------------------------------------------------------------------------
/src/sources/sequelize/index.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize } from 'sequelize';
2 |
3 | export const sequelize = new Sequelize(process.env.POSTGRES_DATABASE_URL, {
4 | dialect: 'postgres',
5 | logging: process.env.DATABASE_LOGGING === 'true',
6 | dialectOptions:
7 | process.env.NODE_ENV === 'production'
8 | ? {
9 | ssl: {
10 | require: true,
11 | rejectUnauthorized: false,
12 | },
13 | }
14 | : undefined,
15 | });
16 |
--------------------------------------------------------------------------------
/src/sources/sequelize/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface ProductListInterface {
2 | products: {
3 | quantity: number;
4 | product: {
5 | id: number;
6 | name: string;
7 | price: number;
8 | };
9 | }[];
10 | }
11 |
12 | export interface ProductSumInterface {
13 | id: number;
14 | sum: number;
15 | }
16 |
--------------------------------------------------------------------------------
/src/sources/sequelize/migrations/20220214101918-init.cjs:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 |
3 | module.exports = {
4 | async up(queryInterface, Sequelize) {
5 | await queryInterface.createTable('categories', {
6 | id: {
7 | type: DataTypes.INTEGER,
8 | autoIncrement: true,
9 | primaryKey: true,
10 | },
11 | name: {
12 | type: new DataTypes.STRING(),
13 | allowNull: false,
14 | },
15 | createdAt: {
16 | type: DataTypes.DATE,
17 | default: Date.now,
18 | },
19 | updatedAt: {
20 | type: DataTypes.DATE,
21 | default: Date.now,
22 | },
23 | });
24 |
25 | await queryInterface.createTable('products', {
26 | id: {
27 | type: DataTypes.INTEGER,
28 | autoIncrement: true,
29 | primaryKey: true,
30 | },
31 | name: {
32 | type: new DataTypes.STRING(),
33 | allowNull: false,
34 | },
35 | price: {
36 | type: new DataTypes.DECIMAL(15, 6),
37 | allowNull: false,
38 | },
39 | createdAt: {
40 | type: DataTypes.DATE,
41 | default: Date.now,
42 | },
43 | updatedAt: {
44 | type: DataTypes.DATE,
45 | default: Date.now,
46 | },
47 | categoryId: {
48 | type: DataTypes.INTEGER,
49 | references: {
50 | model: 'categories',
51 | key: 'id',
52 | },
53 | allowNull: false,
54 | },
55 | });
56 |
57 | await queryInterface.createTable('orders', {
58 | id: {
59 | type: DataTypes.INTEGER,
60 | autoIncrement: true,
61 | primaryKey: true,
62 | },
63 | isPaid: {
64 | type: DataTypes.BOOLEAN,
65 | defaultValue: false,
66 | },
67 | delivery: {
68 | type: DataTypes.STRING,
69 | },
70 | createdAt: {
71 | type: DataTypes.DATE,
72 | },
73 | updatedAt: {
74 | type: DataTypes.DATE,
75 | },
76 | });
77 |
78 | await queryInterface.createTable('cart_products', {
79 | id: {
80 | type: DataTypes.INTEGER,
81 | autoIncrement: true,
82 | primaryKey: true,
83 | },
84 | quantity: {
85 | type: DataTypes.INTEGER,
86 | allowNull: false,
87 | },
88 | createdAt: {
89 | type: DataTypes.DATE,
90 | default: Date.now,
91 | },
92 | updatedAt: {
93 | type: DataTypes.DATE,
94 | default: Date.now,
95 | },
96 | orderId: {
97 | type: DataTypes.INTEGER,
98 | references: {
99 | model: 'orders',
100 | key: 'id',
101 | },
102 | allowNull: false,
103 | },
104 | productId: {
105 | type: DataTypes.INTEGER,
106 | references: {
107 | model: 'products',
108 | key: 'id',
109 | },
110 | allowNull: false,
111 | },
112 | });
113 | },
114 |
115 | async down(queryInterface, Sequelize) {
116 | await queryInterface.dropTable('cart_products');
117 | await queryInterface.dropTable('orders');
118 | await queryInterface.dropTable('products');
119 | await queryInterface.dropTable('categories');
120 | },
121 | };
122 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/cart.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, HasOneGetAssociationMixin, NonAttribute } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { ProductModel } from './product.model.js';
5 | import { OrderModel } from './order.model.js';
6 |
7 | type Cart = {
8 | id: number;
9 | quantity: number;
10 | orderId: number;
11 | productId: number;
12 | createdAt: Date;
13 | updatedAt: Date;
14 | };
15 |
16 | export type CartCreationAttributes = Optional;
17 |
18 | export class CartModel extends Model {
19 | declare id: number;
20 | declare quantity: number;
21 | declare createdAt: Date;
22 | declare updatedAt: Date;
23 |
24 | declare product?: NonAttribute;
25 | declare order?: NonAttribute;
26 |
27 | declare getProduct: HasOneGetAssociationMixin;
28 | declare getOrder: HasOneGetAssociationMixin;
29 |
30 | declare static associations: {
31 | product: Association;
32 | order: Association;
33 | };
34 | }
35 |
36 | CartModel.init(
37 | {
38 | id: {
39 | type: DataTypes.INTEGER,
40 | autoIncrement: true,
41 | primaryKey: true,
42 | },
43 | quantity: {
44 | type: DataTypes.INTEGER,
45 | allowNull: false,
46 | },
47 | createdAt: {
48 | type: DataTypes.DATE,
49 | },
50 | updatedAt: {
51 | type: DataTypes.DATE,
52 | },
53 | productId: {
54 | type: DataTypes.INTEGER,
55 | references: {
56 | model: 'products',
57 | key: 'id',
58 | },
59 | allowNull: false,
60 | },
61 | orderId: {
62 | type: DataTypes.INTEGER,
63 | references: {
64 | model: 'orders',
65 | key: 'id',
66 | },
67 | allowNull: false,
68 | },
69 | },
70 | {
71 | sequelize,
72 | tableName: 'cart_products',
73 | modelName: 'cart',
74 | },
75 | );
76 |
77 | CartModel.belongsTo(ProductModel, {
78 | targetKey: 'id',
79 | as: 'product',
80 | foreignKey: 'productId',
81 | });
82 |
83 | CartModel.belongsTo(OrderModel, {
84 | targetKey: 'id',
85 | as: 'order',
86 | foreignKey: 'orderId',
87 | });
88 | OrderModel.hasMany(CartModel, {
89 | sourceKey: 'id',
90 | as: 'carts',
91 | foreignKey: 'orderId',
92 | });
93 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/category.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 |
5 | type Category = {
6 | id: number;
7 | name: string;
8 | createdAt: Date;
9 | updatedAt: Date;
10 | };
11 |
12 | export type CategoryCreationAttributes = Optional;
13 |
14 | export class CategoryModel extends Model {
15 | declare id: number;
16 | declare name: string;
17 | declare createdAt: Date;
18 | declare updatedAt: Date;
19 | }
20 |
21 | CategoryModel.init(
22 | {
23 | id: {
24 | type: DataTypes.INTEGER,
25 | autoIncrement: true,
26 | primaryKey: true,
27 | },
28 | name: {
29 | type: new DataTypes.STRING(128),
30 | allowNull: false,
31 | },
32 | createdAt: {
33 | type: DataTypes.DATE,
34 | },
35 | updatedAt: {
36 | type: DataTypes.DATE,
37 | },
38 | },
39 | {
40 | sequelize,
41 | tableName: 'categories',
42 | modelName: 'category',
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/index.ts:
--------------------------------------------------------------------------------
1 | export { ProductModel } from './product.model.js';
2 | export { CategoryModel } from './category.model.js';
3 | export { OrderModel } from './order.model.js';
4 | export { CartModel } from './cart.model.js';
5 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/order.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, NonAttribute, HasManyGetAssociationsMixin } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { CartModel } from './cart.model.js';
5 |
6 | type Order = {
7 | id: number;
8 | isPaid: boolean;
9 | delivery: string;
10 | createdAt: Date;
11 | updatedAt: Date;
12 | };
13 |
14 | export type OrderCreationAttributes = Optional;
15 |
16 | export class OrderModel extends Model {
17 | declare id: number;
18 | declare isPaid: boolean;
19 | declare delivery: string;
20 | declare createdAt: Date;
21 | declare updatedAt: Date;
22 |
23 | declare carts?: NonAttribute;
24 |
25 | declare getCarts: HasManyGetAssociationsMixin;
26 |
27 | declare static associations: {
28 | carts: Association;
29 | };
30 | }
31 |
32 | OrderModel.init(
33 | {
34 | id: {
35 | type: DataTypes.INTEGER,
36 | autoIncrement: true,
37 | primaryKey: true,
38 | },
39 | isPaid: {
40 | type: DataTypes.BOOLEAN,
41 | defaultValue: false,
42 | },
43 | delivery: {
44 | type: DataTypes.STRING,
45 | },
46 | createdAt: {
47 | type: DataTypes.DATE,
48 | },
49 | updatedAt: {
50 | type: DataTypes.DATE,
51 | },
52 | },
53 | {
54 | sequelize,
55 | tableName: 'orders',
56 | modelName: 'order',
57 | },
58 | );
59 |
--------------------------------------------------------------------------------
/src/sources/sequelize/models/product.model.ts:
--------------------------------------------------------------------------------
1 | import { DataTypes, Model, Optional, Association, HasOneGetAssociationMixin, NonAttribute } from 'sequelize';
2 |
3 | import { sequelize } from '../index.js';
4 | import { CategoryModel } from './category.model.js';
5 |
6 | type Product = {
7 | id: number;
8 | name: string;
9 | price: number;
10 | categoryId: number;
11 | createdAt: Date;
12 | updatedAt: Date;
13 | };
14 |
15 | export type ProductCreationAttributes = Optional;
16 |
17 | export class ProductModel extends Model {
18 | declare id: number;
19 | declare name: string;
20 | declare price: string;
21 | declare categoryId: number;
22 | declare createdAt: Date;
23 | declare updatedAt: Date;
24 |
25 | declare category?: NonAttribute;
26 |
27 | declare getCategory: HasOneGetAssociationMixin;
28 |
29 | declare static associations: {
30 | category: Association;
31 | };
32 | }
33 |
34 | ProductModel.init(
35 | {
36 | id: {
37 | type: DataTypes.INTEGER,
38 | autoIncrement: true,
39 | primaryKey: true,
40 | },
41 | name: {
42 | type: new DataTypes.STRING(128),
43 | allowNull: false,
44 | },
45 | price: {
46 | type: new DataTypes.DECIMAL(15, 6),
47 | allowNull: false,
48 | },
49 | createdAt: {
50 | type: DataTypes.DATE,
51 | },
52 | updatedAt: {
53 | type: DataTypes.DATE,
54 | },
55 | categoryId: {
56 | type: DataTypes.INTEGER,
57 | references: {
58 | model: 'categories',
59 | key: 'id',
60 | },
61 | allowNull: false,
62 | },
63 | },
64 | {
65 | sequelize,
66 | tableName: 'products',
67 | modelName: 'product',
68 | },
69 | );
70 |
71 | ProductModel.belongsTo(CategoryModel, {
72 | foreignKey: 'categoryId',
73 | targetKey: 'id',
74 | as: 'category',
75 | });
76 | CategoryModel.hasMany(ProductModel, { sourceKey: 'id' });
77 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/cart.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CartModel } from '../models/index.js';
5 |
6 | export const CreateCartResource: ResourceFunction = () => ({
7 | resource: CartModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | createdAt: {
13 | isVisible: {
14 | show: true,
15 | edit: false,
16 | },
17 | },
18 | updatedAt: {
19 | isVisible: {
20 | show: true,
21 | edit: false,
22 | },
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/category.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { CategoryModel } from '../models/index.js';
5 |
6 | export const CreateCategoryResource: ResourceFunction = () => ({
7 | resource: CategoryModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | name: {
13 | isTitle: true,
14 | },
15 | createdAt: {
16 | isVisible: {
17 | show: true,
18 | edit: false,
19 | },
20 | },
21 | updatedAt: {
22 | isVisible: {
23 | show: true,
24 | edit: false,
25 | },
26 | },
27 | },
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateProductResource } from './product.resource.js';
2 | export { CreateCategoryResource } from './category.resource.js';
3 | export { CreateCartResource } from './cart.resource.js';
4 | export { CreateOrderResource } from './order.resource.js';
5 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/order.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { PRODUCTS_LIST } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { getProducts } from '../hooks/get-products.hook.js';
6 | import { getSumForOrder } from '../hooks/get-sum.hook.js';
7 | import { OrderModel } from '../models/index.js';
8 |
9 | export const CreateOrderResource: ResourceFunction = () => ({
10 | resource: OrderModel,
11 | features: [useEnvironmentVariableToDisableActions()],
12 | options: {
13 | navigation: menu.sequelize,
14 | actions: {
15 | list: {
16 | after: [getSumForOrder()],
17 | },
18 | show: {
19 | after: [getProducts(), getSumForOrder()],
20 | },
21 | },
22 | properties: {
23 | sum: {
24 | isVisible: {
25 | edit: false,
26 | list: true,
27 | filter: false,
28 | show: true,
29 | },
30 | position: 998,
31 | },
32 | productsList: {
33 | isVisible: {
34 | edit: false,
35 | list: false,
36 | filter: false,
37 | show: true,
38 | },
39 | components: {
40 | show: PRODUCTS_LIST,
41 | },
42 | position: 999,
43 | },
44 | createdAt: {
45 | isVisible: {
46 | show: true,
47 | edit: false,
48 | },
49 | },
50 | updatedAt: {
51 | isVisible: {
52 | show: true,
53 | edit: false,
54 | },
55 | },
56 | },
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/src/sources/sequelize/resources/product.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { ProductModel } from '../models/index.js';
5 |
6 | export const CreateProductResource: ResourceFunction = () => ({
7 | resource: ProductModel,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.sequelize,
11 | properties: {
12 | price: {
13 | type: 'currency',
14 | props: {
15 | decimalSeparator: '.',
16 | disableGroupSeparators: true,
17 | prefix: '$',
18 | fixedDecimalLength: 2,
19 | },
20 | },
21 | name: {
22 | isTitle: true,
23 | },
24 | createdAt: {
25 | isVisible: {
26 | show: true,
27 | edit: false,
28 | },
29 | },
30 | updatedAt: {
31 | isVisible: {
32 | show: true,
33 | edit: false,
34 | },
35 | },
36 | },
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/carts.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { CartCreationAttributes } from '../../models/cart.model.js';
4 |
5 | const carts = (count: number, { productId, orderId }): CartCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | quantity: Number(faker.random.numeric(1, { bannedDigits: '0' })),
8 | productId,
9 | orderId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default carts;
15 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/categories.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { CategoryCreationAttributes } from '../../models/category.model.js';
4 |
5 | const categories = (count: number): CategoryCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.commerce.department(),
8 | createdAt: new Date(),
9 | updatedAt: new Date(),
10 | }));
11 |
12 | export default categories;
13 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as carts } from './carts.js';
2 | export { default as categories } from './categories.js';
3 | export { default as orders } from './orders.js';
4 | export { default as products } from './products.js';
5 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/orders.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { OrderCreationAttributes } from '../../models/order.model.js';
4 |
5 | const orders = (count: number): OrderCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | isPaid: faker.datatype.boolean(),
8 | delivery: `${faker.address.zipCode()} ${faker.address.city()}, ${faker.address.street()} ${faker.address.buildingNumber()}`,
9 | createdAt: new Date(),
10 | updatedAt: new Date(),
11 | }));
12 |
13 | export default orders;
14 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/data/products.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 |
3 | import { ProductCreationAttributes } from '../../models/product.model.js';
4 |
5 | const products = (count: number, { categoryId }): ProductCreationAttributes[] =>
6 | Array.from({ length: count }, () => ({
7 | name: faker.commerce.productName(),
8 | price: Number(faker.commerce.price(1, 100, 2)),
9 | categoryId,
10 | createdAt: new Date(),
11 | updatedAt: new Date(),
12 | }));
13 |
14 | export default products;
15 |
--------------------------------------------------------------------------------
/src/sources/sequelize/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import flatten from 'lodash/flatten.js';
7 | import { CategoryModel, CartModel, ProductModel, OrderModel } from '../models/index.js';
8 | import { categories, carts, products, orders } from './data/index.js';
9 |
10 | const categoriesCount = 2;
11 | const productsPerCategory = 2;
12 | const cartsPerProduct = 1;
13 |
14 | const run = async () => {
15 | const createdCategories = await CategoryModel.bulkCreate(categories(categoriesCount));
16 | const createdProducts = flatten(
17 | await Promise.all(
18 | createdCategories.map((cat) =>
19 | ProductModel.bulkCreate(products(productsPerCategory, { categoryId: cat.get('id') })),
20 | ),
21 | ),
22 | );
23 | // TODO: Look into Order/Cart relationship, it's a bit odd...
24 | const createdOrder = await OrderModel.create(orders(1)[0]);
25 | await Promise.all(
26 | createdProducts.map((p) =>
27 | CartModel.bulkCreate(carts(cartsPerProduct, { orderId: createdOrder.get('id'), productId: p.get('id') })),
28 | ),
29 | );
30 | };
31 |
32 | run()
33 | .then(() => process.exit(0))
34 | .catch((e) => {
35 | console.log(e);
36 | process.exit(1);
37 | });
38 |
--------------------------------------------------------------------------------
/src/sources/typeorm/config.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config();
3 |
4 | import { DataSource, DataSourceOptions } from 'typeorm';
5 | import { init1644569575919 } from './migrations/1644569575919-init.js';
6 | import { Organization, Person } from './models/index.js';
7 |
8 | export const params: DataSourceOptions = {
9 | type: 'postgres' as const,
10 | url: process.env.POSTGRES_DATABASE_URL,
11 | synchronize: process.env.DATABASE_SYNC === 'true',
12 | logging: process.env.DATABASE_LOGGING === 'true',
13 | entities: [Organization, Person],
14 | migrations: [init1644569575919],
15 | migrationsRun: true,
16 | subscribers: [],
17 | extra:
18 | process.env.NODE_ENV === 'production'
19 | ? {
20 | ssl: true,
21 | }
22 | : undefined,
23 | };
24 |
25 | const dataSource = new DataSource(params);
26 |
27 | export default dataSource;
28 |
--------------------------------------------------------------------------------
/src/sources/typeorm/enums/country.enum.ts:
--------------------------------------------------------------------------------
1 | export enum CountryEnum {
2 | Poland = 'PL',
3 | GreatBritain = 'GB',
4 | Germany = 'DE',
5 | }
6 |
--------------------------------------------------------------------------------
/src/sources/typeorm/handlers/validate-email.handler.ts:
--------------------------------------------------------------------------------
1 | import { flat, PropertyErrors, ValidationError, ActionRequest, ActionContext, Before } from 'adminjs';
2 |
3 | import { isPOSTMethod } from '../../../admin/admin.utils.js';
4 | import { PostPayload } from '../interfaces.js';
5 |
6 | export const validateEmail: Before = async (request: ActionRequest, context: ActionContext): Promise => {
7 | if (!isPOSTMethod(request)) {
8 | return request;
9 | }
10 |
11 | const payload = flat.unflatten(request.payload);
12 | const errors: PropertyErrors = {};
13 |
14 | if (!payload.email?.trim()?.length) {
15 | errors.email = { message: context.translateMessage('Has to be filled') };
16 | }
17 |
18 | if (Object.keys(errors).length) {
19 | throw new ValidationError(errors);
20 | }
21 |
22 | return request;
23 | };
24 |
--------------------------------------------------------------------------------
/src/sources/typeorm/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface PostPayload {
2 | email: string;
3 | }
4 |
--------------------------------------------------------------------------------
/src/sources/typeorm/migrations/1644569575919-init.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner } from 'typeorm';
2 |
3 | export class init1644569575919 implements MigrationInterface {
4 | name = 'init1644569575919';
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.query(`CREATE TYPE "public"."organizations_country_enum" AS ENUM('PL', 'GB', 'DE')`);
8 | await queryRunner.query(
9 | `CREATE TABLE "organizations" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "address" character varying NOT NULL, "city" character varying NOT NULL, "postal_code" character varying NOT NULL, "country" "public"."organizations_country_enum" NOT NULL, CONSTRAINT "PK_6b031fcd0863e3f6b44230163f9" PRIMARY KEY ("id"))`,
10 | );
11 | await queryRunner.query(
12 | `CREATE TABLE "persons" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "phone" character varying NOT NULL, "organization_id" integer NOT NULL, CONSTRAINT "PK_74278d8812a049233ce41440ac7" PRIMARY KEY ("id"))`,
13 | );
14 | await queryRunner.query(
15 | `ALTER TABLE "persons" ADD CONSTRAINT "FK_e1929e2e66a63922e776334f77b" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
16 | );
17 | }
18 |
19 | public async down(queryRunner: QueryRunner): Promise {
20 | await queryRunner.query(`ALTER TABLE "persons" DROP CONSTRAINT "FK_e1929e2e66a63922e776334f77b"`);
21 | await queryRunner.query(`DROP TABLE "persons"`);
22 | await queryRunner.query(`DROP TABLE "organizations"`);
23 | await queryRunner.query(`DROP TYPE "public"."organizations_country_enum"`);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/index.ts:
--------------------------------------------------------------------------------
1 | export { Organization } from './organization.entity.js';
2 | export { Person } from './person.entity.js';
3 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/organization.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
2 | import { CountryEnum } from '../enums/country.enum.js';
3 |
4 | @Entity({ name: 'organizations' })
5 | export class Organization extends BaseEntity {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column()
10 | public name: string;
11 |
12 | @Column()
13 | public address: string;
14 |
15 | @Column()
16 | public city: string;
17 |
18 | @Column({ name: 'postal_code' })
19 | public postalCode: string;
20 |
21 | @Column({
22 | type: 'enum',
23 | enum: CountryEnum,
24 | nullable: false,
25 | })
26 | public country: CountryEnum;
27 | }
28 |
--------------------------------------------------------------------------------
/src/sources/typeorm/models/person.entity.ts:
--------------------------------------------------------------------------------
1 | import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from 'typeorm';
2 | import { Organization } from './organization.entity.js';
3 |
4 | @Entity({ name: 'persons' })
5 | export class Person extends BaseEntity {
6 | @PrimaryGeneratedColumn()
7 | public id: number;
8 |
9 | @Column({ name: 'email', nullable: false })
10 | public email: string;
11 |
12 | @Column({ name: 'first_name' })
13 | public firstName: string;
14 |
15 | @Column({ name: 'last_name' })
16 | public lastName: string;
17 |
18 | @Column({ name: 'phone' })
19 | public phone: string;
20 |
21 | @ManyToOne(() => Organization)
22 | @JoinColumn({ name: 'organization_id' })
23 | public organization: Organization;
24 |
25 | @RelationId((person: Person) => person.organization)
26 | @Column({ name: 'organization_id' })
27 | public organizationId: number;
28 |
29 | public getFullName(): string {
30 | return `${this.firstName} ${this.lastName}`;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/index.ts:
--------------------------------------------------------------------------------
1 | export { CreateOrganizationResource } from './organization.resource.js';
2 | export { CreatePersonResource } from './person.resource.js';
3 |
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/organization.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
3 | import { ResourceFunction } from '../../../admin/types/index.js';
4 | import { Organization } from '../models/index.js';
5 |
6 | export const CreateOrganizationResource: ResourceFunction = () => ({
7 | resource: Organization,
8 | features: [useEnvironmentVariableToDisableActions()],
9 | options: {
10 | navigation: menu.typeorm,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/src/sources/typeorm/resources/person.resource.ts:
--------------------------------------------------------------------------------
1 | import { menu } from '../../../admin/index.js';
2 | import { DETAILED_STATS, DONT_TOUCH_THIS_ACTION } from '../../../admin/components.bundler.js';
3 | import { useEnvironmentVariableToDisableActions } from '../../../admin/features/useEnvironmentVariableToDisableActions.js';
4 | import { ResourceFunction } from '../../../admin/types/index.js';
5 | import { validateEmail } from '../handlers/validate-email.handler.js';
6 | import { Person } from '../models/index.js';
7 |
8 | export const CreatePersonResource: ResourceFunction = () => ({
9 | resource: Person,
10 | features: [useEnvironmentVariableToDisableActions()],
11 | options: {
12 | navigation: menu.typeorm,
13 | properties: {
14 | phone: {
15 | type: 'phone',
16 | },
17 | },
18 | actions: {
19 | new: {
20 | before: [validateEmail],
21 | },
22 | edit: {
23 | before: [validateEmail],
24 | },
25 | detailedStats: {
26 | actionType: 'resource',
27 | icon: 'BarChart2',
28 | name: 'Resource statistics',
29 | component: DETAILED_STATS,
30 | handler: async () => {
31 | return { true: 'ueas' };
32 | },
33 | showInDrawer: true,
34 | },
35 | dontTouchThis: {
36 | actionType: 'record',
37 | icon: 'Exit',
38 | guard: 'youCanSetupGuards',
39 | component: DONT_TOUCH_THIS_ACTION,
40 | handler: async (request, response, context) => {
41 | return {
42 | record: context.record.toJSON(),
43 | };
44 | },
45 | },
46 | },
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/index.ts:
--------------------------------------------------------------------------------
1 | export { default as organizations } from './organizations.js';
2 | export { default as persons } from './persons.js';
3 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/organizations.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 |
4 | import { Organization } from '../../models/index.js';
5 | import { CountryEnum } from './../../enums/country.enum.js';
6 |
7 | const organizations = (count: number): DeepPartial[] =>
8 | Array.from({ length: count }, () => ({
9 | name: faker.company.name(),
10 | city: faker.address.city(),
11 | address: `${faker.address.street()} ${faker.address.buildingNumber()}`,
12 | postalCode: faker.address.zipCode(),
13 | country: CountryEnum.GreatBritain,
14 | }));
15 |
16 | export default organizations;
17 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/data/persons.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 |
4 | import { Person } from '../../models/index.js';
5 |
6 | const persons = (count: number, { organizationId }): DeepPartial[] =>
7 | Array.from({ length: count }, () => ({
8 | email: faker.internet.email(),
9 | firstName: faker.name.firstName(),
10 | lastName: faker.name.lastName(),
11 | phone: faker.phone.number(),
12 | organizationId,
13 | }));
14 |
15 | export default persons;
16 |
--------------------------------------------------------------------------------
/src/sources/typeorm/seeds/run.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 | dotenv.config({
3 | path: `${process.cwd()}/.env`,
4 | });
5 |
6 | import dataSource from '../config.js';
7 | import { Organization, Person } from '../models/index.js';
8 | import { organizations, persons } from './data/index.js';
9 |
10 | const organizationsCount = 3;
11 | const personsPerOrganizationCount = 2;
12 |
13 | const run = async () => {
14 | await dataSource.initialize();
15 | const em = dataSource.manager;
16 | const organizationRepository = em.getRepository(Organization);
17 | const personRepository = em.getRepository(Person);
18 |
19 | const createdOrganizations = await organizationRepository.insert(organizations(organizationsCount));
20 |
21 | await Promise.all(
22 | createdOrganizations.identifiers.map(({ id: organizationId }) =>
23 | personRepository.insert(persons(personsPerOrganizationCount, { organizationId })),
24 | ),
25 | );
26 | };
27 |
28 | run()
29 | .then(() => process.exit(0))
30 | .catch((e) => {
31 | console.log(e);
32 | process.exit(1);
33 | });
34 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/components/LoggedIn.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@adminjs/design-system';
2 | import React from 'react';
3 |
4 | const LoggedIn = () => ;
5 |
6 | export default LoggedIn;
7 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/components/SidebarFooter.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Box, SmallText, Title, Tooltip } from '@adminjs/design-system';
2 | import { ReduxState, ViewHelpers, useTranslation } from 'adminjs';
3 | import React from 'react';
4 | import { useSelector } from 'react-redux';
5 |
6 | const h = new ViewHelpers();
7 |
8 | const SidebarFooter = () => {
9 | const session = useSelector((state: ReduxState) => state.session);
10 | const { title, email, avatarUrl } = session;
11 | const { tb } = useTranslation();
12 |
13 | if (!session) return null;
14 |
15 | return (
16 |
17 |
18 |
19 | {email.slice(0, 1).toUpperCase()}
20 |
21 |
22 |
23 | {email}
24 | {title && {title}}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default SidebarFooter;
33 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/index.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeConfig } from 'adminjs';
2 | import { overrides } from './overrides.js';
3 |
4 | export const themeConfig: ThemeConfig = {
5 | id: 'custom-theme',
6 | name: 'custom theme',
7 | overrides,
8 | };
9 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/overrides.ts:
--------------------------------------------------------------------------------
1 | import type { ThemeConfig } from 'adminjs';
2 |
3 | export const overrides: ThemeConfig['overrides'] = {
4 | colors: {
5 | primary100: '#E67E22',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/themes/custom-theme/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SoftwareBrothers/adminjs-example-app/09a6efdb27d584a67fc82e08945ea570a014b7c1/src/themes/custom-theme/style.css
--------------------------------------------------------------------------------
/src/themes/custom-theme/theme.bundle.js:
--------------------------------------------------------------------------------
1 | var THEME_COMPONENTS=function(e,t,l,r,n){"use strict";const a=new r.ViewHelpers;return e.LoggedIn=()=>l.createElement(t.Box,{mr:"lg"}),e.SidebarFooter=()=>{const e=n.useSelector((e=>e.session)),{title:o,email:i,avatarUrl:c}=e,{tb:m}=r.useTranslation();return e?l.createElement(t.Box,{mt:"lg",mb:"md"},l.createElement(t.Box,{flex:!0,flexDirection:"row",alignItems:"center",px:"xl",mb:"lg"},l.createElement(t.Avatar,{src:c,alt:i,mr:"lg"},i.slice(0,1).toUpperCase()),l.createElement(t.Tooltip,{direction:"right",title:m("logout")},l.createElement(t.Box,{as:"a",href:a.logoutUrl()},l.createElement(t.Title,null,i),o&&l.createElement(t.SmallText,null,o))))):null},e}({},AdminJSDesignSystem,React,AdminJS,ReactRedux);
2 |
--------------------------------------------------------------------------------
/src/themes/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import url from 'url';
3 |
4 | import type { ThemeConfig } from 'adminjs';
5 | import { themeConfig } from './custom-theme/index.js';
6 |
7 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
8 | const getThemeDir = (theme: string) => path.join(__dirname, `./${theme}`);
9 |
10 | export const customTheme: ThemeConfig = {
11 | ...themeConfig,
12 | bundlePath: `${getThemeDir(themeConfig.id)}/theme.bundle.js`,
13 | stylePath: `${getThemeDir(themeConfig.id)}/style.css`,
14 | };
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "declaration": false,
7 | "removeComments": true,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "allowSyntheticDefaultImports": true,
11 | "allowJs": true,
12 | "target": "ESNext",
13 | "sourceMap": false,
14 | "outDir": "./dist",
15 | "baseUrl": ".",
16 | "incremental": true,
17 | "skipLibCheck": true,
18 | "resolveJsonModule": true,
19 | "esModuleInterop": true
20 | },
21 | "exclude": ["node_modules", "src/**/theme.bundle.js"],
22 | "include": ["src"]
23 | }
24 |
--------------------------------------------------------------------------------