├── .dockerignore ├── src ├── themes │ ├── custom-theme │ │ ├── style.css │ │ ├── components │ │ │ ├── LoggedIn.tsx │ │ │ └── SidebarFooter.tsx │ │ ├── overrides.ts │ │ ├── index.ts │ │ └── theme.bundle.js │ └── index.ts ├── sources │ ├── typeorm │ │ ├── interfaces.ts │ │ ├── enums │ │ │ └── country.enum.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── organization.entity.ts │ │ │ └── person.entity.ts │ │ ├── seeds │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── persons.ts │ │ │ │ └── organizations.ts │ │ │ └── run.ts │ │ ├── resources │ │ │ ├── index.ts │ │ │ ├── organization.resource.ts │ │ │ └── person.resource.ts │ │ ├── handlers │ │ │ └── validate-email.handler.ts │ │ ├── config.ts │ │ └── migrations │ │ │ └── 1644569575919-init.ts │ ├── mikroorm │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── seller.model.ts │ │ │ ├── car.model.ts │ │ │ └── owner.model.ts │ │ ├── seeds │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── sellers.ts │ │ │ │ ├── owners.ts │ │ │ │ └── cars.ts │ │ │ └── run.ts │ │ ├── resources │ │ │ ├── index.ts │ │ │ ├── seller.resource.ts │ │ │ ├── owner.resource.ts │ │ │ └── car.resource.ts │ │ ├── config.ts │ │ └── migrations │ │ │ └── Migration20220714094312.ts │ ├── objectionjs │ │ ├── resources │ │ │ ├── index.ts │ │ │ ├── office.resource.ts │ │ │ └── manager.resource.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── manager.entity.ts │ │ │ └── office.entity.ts │ │ ├── knexfile.cjs │ │ ├── migrations │ │ │ └── 20220826123456_initial_schema.cjs │ │ └── utils │ │ │ └── base-model.ts │ ├── prisma │ │ ├── migrations │ │ │ ├── migration_lock.toml │ │ │ └── 20220218111814_init │ │ │ │ └── migration.sql │ │ ├── seeds │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── profiles.ts │ │ │ │ ├── publishers.ts │ │ │ │ └── posts.ts │ │ │ └── run.ts │ │ ├── resources │ │ │ ├── index.ts │ │ │ ├── profile.resource.ts │ │ │ ├── publisher.resource.ts │ │ │ └── post.resource.ts │ │ └── config.ts │ ├── sequelize │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── category.model.ts │ │ │ ├── order.model.ts │ │ │ ├── product.model.ts │ │ │ └── cart.model.ts │ │ ├── seeds │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── categories.ts │ │ │ │ ├── carts.ts │ │ │ │ ├── products.ts │ │ │ │ └── orders.ts │ │ │ └── run.ts │ │ ├── resources │ │ │ ├── index.ts │ │ │ ├── cart.resource.ts │ │ │ ├── category.resource.ts │ │ │ ├── product.resource.ts │ │ │ └── order.resource.ts │ │ ├── interfaces.ts │ │ ├── config.js │ │ ├── index.ts │ │ ├── functions │ │ │ └── get-sum.function.ts │ │ ├── hooks │ │ │ ├── get-sum.hook.ts │ │ │ └── get-products.hook.ts │ │ └── migrations │ │ │ └── 20220214101918-init.cjs │ ├── mongoose │ │ ├── seeds │ │ │ ├── data │ │ │ │ ├── index.ts │ │ │ │ ├── comments.ts │ │ │ │ ├── users.ts │ │ │ │ ├── categories.ts │ │ │ │ └── articles.ts │ │ │ └── run.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── admin.model.ts │ │ │ ├── comment.model.ts │ │ │ ├── category.model.ts │ │ │ ├── user.model.ts │ │ │ ├── article.model.ts │ │ │ └── complicated.model.ts │ │ └── resources │ │ │ ├── index.ts │ │ │ ├── user.resource.ts │ │ │ ├── complicated.resource.ts │ │ │ ├── comment.resource.ts │ │ │ ├── category.resource.ts │ │ │ ├── article.resource.ts │ │ │ └── admin.resource.ts │ └── rest │ │ └── crypto-database.ts ├── admin │ ├── locale │ │ ├── en │ │ │ ├── pages.json │ │ │ ├── complicated.json │ │ │ ├── person.json │ │ │ ├── components.json │ │ │ ├── common.json │ │ │ └── index.ts │ │ ├── de │ │ │ ├── pages.json │ │ │ ├── common.json │ │ │ ├── components.json │ │ │ └── index.ts │ │ └── index.ts │ ├── components │ │ ├── design-system-examples │ │ │ ├── index.ts │ │ │ └── modal-example.tsx │ │ ├── debug.tsx │ │ ├── thumb.tsx │ │ ├── detailed-stats.tsx │ │ ├── dont-touch-this-action.tsx │ │ ├── sidebar-resource-section.tsx │ │ ├── top-bar.tsx │ │ ├── products-list.tsx │ │ └── login.tsx │ ├── types │ │ └── index.ts │ ├── admin.utils.ts │ ├── pages │ │ ├── design-system-examples │ │ │ ├── modal-page.tsx │ │ │ ├── tabs-page.tsx │ │ │ ├── icons-page.tsx │ │ │ ├── illustrations-page.tsx │ │ │ ├── messages-page.tsx │ │ │ ├── blog-page.tsx │ │ │ ├── index.tsx │ │ │ ├── form-page.tsx │ │ │ ├── typography-page.tsx │ │ │ └── buttons-page.tsx │ │ ├── index.ts │ │ └── custom-page.tsx │ ├── features │ │ └── useEnvironmentVariableToDisableActions.ts │ ├── constants │ │ └── authUsers.ts │ ├── components.bundler.ts │ ├── router.ts │ └── index.ts ├── servers │ ├── nestjs │ │ ├── app.service.ts │ │ ├── database.providers.ts │ │ ├── prisma │ │ │ └── prisma.service.ts │ │ ├── admin │ │ │ └── admin.setup.ts │ │ ├── app.controller.ts │ │ ├── index.ts │ │ ├── app.module.ts │ │ └── mongoose │ │ │ └── mongoose.module.ts │ ├── hapijs.ts │ ├── fastify.ts │ └── express │ │ └── index.ts ├── index.ts └── scripts │ ├── truncate-postgres.ts │ └── truncate-mongodb.ts ├── public ├── locales │ ├── en │ │ └── translation.json │ └── mk │ │ └── translation.json ├── favicon.ico ├── custom.css └── gtm.js ├── Procfile ├── .prettierrc ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20220713101214_init │ │ └── migration.sql └── schema.prisma ├── nodemon.json ├── Dockerfile ├── .sequelizerc ├── .env ├── tsconfig.json ├── .github └── workflows │ └── push.yml ├── docker-compose.yaml ├── LICENSE ├── .eslintrc.json ├── .gitignore ├── README.md └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/themes/custom-theme/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: yarn migration:up:production 2 | web: yarn start 3 | -------------------------------------------------------------------------------- /src/sources/typeorm/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface PostPayload { 2 | email: string; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoftwareBrothers/adminjs-example-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/admin/locale/en/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "customPage": "Custom Page example" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/admin/components/design-system-examples/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ModalExample } from './modal-example.js'; 2 | -------------------------------------------------------------------------------- /src/admin/locale/de/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": { 3 | "customPage": "Benutzerdefinierte Seite" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /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/models/index.ts: -------------------------------------------------------------------------------- 1 | export { Organization } from './organization.entity.js'; 2 | export { Person } from './person.entity.js'; 3 | -------------------------------------------------------------------------------- /src/sources/typeorm/seeds/data/index.ts: -------------------------------------------------------------------------------- 1 | export { default as organizations } from './organizations.js'; 2 | export { default as persons } from './persons.js'; 3 | -------------------------------------------------------------------------------- /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/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/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/objectionjs/resources/index.ts: -------------------------------------------------------------------------------- 1 | export { CreateManagerResource } from './manager.resource.js'; 2 | export { CreateOfficeResource } from './office.resource.js'; 3 | -------------------------------------------------------------------------------- /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/typeorm/resources/index.ts: -------------------------------------------------------------------------------- 1 | export { CreateOrganizationResource } from './organization.resource.js'; 2 | export { CreatePersonResource } from './person.resource.js'; 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/overrides.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeConfig } from 'adminjs'; 2 | 3 | export const overrides: ThemeConfig['overrides'] = { 4 | colors: { 5 | primary100: '#E67E22', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 |
9 | Modal 10 |
11 | 12 | 13 | 14 |
15 | ); 16 | 17 | export default ModalPage; 18 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 |
10 | Tabs 11 |
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/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 |
18 | Icons 19 |
20 | 21 | {IconsSet} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default IconsPage; 28 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | stupid cat 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 |
32 | Illustrations 33 |
34 | 35 | {variants.map((variant) => ( 36 | 37 | 38 | {variant} 39 | 40 | ))} 41 | 42 |
43 | ); 44 | 45 | export default IllustrationPage; 46 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 |
10 | Messages 11 |
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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 |
25 | Blog 26 |
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/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/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/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 |
20 | Storybook 21 |
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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 |
30 | Form 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |