DEV Community

Israel Ayanwola
Israel Ayanwola

Posted on • Originally published at thecodeway.hashnode.dev on

Creating Admin Panels for NestJs and Prisma Made Easy with AdminJS

Are you tired of spending too much time creating admin panels for your NestJs and Prisma applications? Look no further than AdminJS! This powerful framework allows you to create a fully functional admin dashboard with just a few lines of code. With its compatibility with various ORM and ODM models, it simplifies the process of performing CRUD operations on your models.

Plus, with customization options available, you can tailor the dashboard to meet your specific project needs. Keep reading to learn how to get started with AdminJS and take control of your application's data management.

What is ORM

Object-relational mapping (ORM, O/RM, and O/R mapping tool) in computer science is a programming technique for converting data between incompatible type systems using object-oriented programming languages.

What is ODM?

Object Document Mapper (ODM) is a library that helps you to work with MongoDB databases. It provides an abstraction layer between the database and the application code. It allows you to use the object-oriented paradigm to work with the database.

Getting Started

AdminJs/NestJs => AdminJs/Prisma => Resources and Customizations

Above is a very simple flow that describes the process of setting up AdminJs on your NestJs project.

Install Requirements

yarn add adminjs @adminjs/nestjs @adminjs/express express-session express-formidable

Enter fullscreen mode Exit fullscreen mode

app.module.ts

import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

@Module({
  imports: [
    AdminModule.createAdminAsync({
      useFactory: () => ({
        adminJsOptions: {
          rootPath: '/admin',
          resources: [],
        },
        auth: {
          authenticate,
          cookieName: 'adminjs',
          cookiePassword: 'secret'
        },
        sessionOptions: {
          resave: true,
          saveUninitialized: true,
          secret: 'secret'
        },
      }),
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Don't want authentication?

You might want your admin panel to not require authentication depending on the project you are working on. By removing the auth and sessionOptions from the object returned by the factory provider.

...
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [],
    },
  }),
}),
...

Enter fullscreen mode Exit fullscreen mode

Start your server

nest start --watch
# OR
nest start

Enter fullscreen mode Exit fullscreen mode

Visit 127.0.0.1:<your port>/admin . If you have authentication enabled it will automatically redirect you to the login page.

Enter the details from the DEFAULT_ADMIN object in the app.module.ts program.

Adding Database models

Upon logging in, if you're unable to locate your models on the dashboard, resources can be of great help. AdminJs leverages the resources added to its options to effectively showcase, evaluate, and utilize them.

Assuming you have this Prisma schema,

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url = env("DATABASE_URL")
}

model User {
  pk Int @id @default(autoincrement())
  id String @unique @default(uuid()) @db.Uuid
  email String @unique
  password String
  active Boolean @default(true)
  staff Boolean @default(false)
  admin Boolean @default(false)
  profile Profile?

  @@unique([pk, id])
  @@map("users")
}

model Profile {
  pk Int @id @default(autoincrement())
  id String @unique @default(uuid()) @db.Uuid
  firstName String? @map("first_name")
  lastName String? @map("last_name")
  userPk Int @unique @map("user_pk")
  userId String @unique @map("user_id") @db.Uuid
  user User @relation(fields: [userPk, userId], references: [pk, id], onDelete: Cascade, onUpdate: Cascade)

  @@unique([pk, id])
  @@unique([userPk, userId])
  @@map("profiles")
}

Enter fullscreen mode Exit fullscreen mode

app.module.ts

import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'
import { Database, Resource } from '@adminjs/prisma';
import AdminJS from 'adminjs'
import { DMMFClass } from '@prisma/client/runtime';
import { PrismaService } from '@database/prisma.service';
import { DatabaseModule } from '@database/database.module';

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

AdminJS.registerAdapter({ Database, Resource });

@Module({
  imports: [
    AdminModule.createAdminAsync({
      imports: [DatabaseModule],
      useFactory: (prisma: PrismaService) => {
        const dmmf = (prisma as any)._baseDmmf as DMMFClass;

        return {
          adminJsOptions: {
            rootPath: '/admin',
            resources: [
              {
                resource: { model: dmmf.modelMap['User'], client: prisma },
                options: {},
              },
              {
                resource: { model: dmmf.modelMap['Profile'], client: prisma },
                options: {},
              },
            ],
          },
          auth: {
            authenticate,
            cookieName: 'adminjs',
            cookiePassword: 'secret'
          },
          sessionOptions: {
            resave: true,
            saveUninitialized: true,
            secret: 'secret'
          },
        }
      },
      inject: [PrismaService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

Explore the basic Resource object format and effortlessly add multiple resources to your project.

{
  resource: { model: dmmf.modelMap[{Model name here}], client: prisma },
  options: {},
}

Enter fullscreen mode Exit fullscreen mode

Make sure you replace {Model name here} with the name of the model you want to use. For example, if you have a model Post, you would replace {Model name here} with Post.

How does this work?

The Database Model Mapper (DMMF) object comprises a comprehensive depiction of your database schema. Its functionality extends to facilitating the extraction of your model from Prisma. The DMMF achieves this by mapping model names to their respective model objects. For example, to retrieve the User model from the map, one can utilize the syntax dmmf.modelMap['User']. Once the model object is obtained, it can be assigned to the resource's model property.

The Prisma client serves as the client property, facilitating connectivity to the database for conducting CRUD operations.

You can add as many resources as you need. For example, if you have a Post model in your Prisma schema that you want to be displayed in the Admin Dashboard. You just need to add it to the resources array;

app.module.ts

...
{
  resource: { model: dmmf.modelMap['Post'], client: prisma },
  options: {},
},
...

Enter fullscreen mode Exit fullscreen mode

Now restart your server if it's not on fast reload.

Simplify adding resources

When managing a substantial number of database models, it is common to encounter the need for duplicating codes while incorporating new resources for each model.

Did you know that the Builder pattern can help you add multiple resources to your code without repeating the same code? This makes your code more efficient and easier to maintain. So why not give it a try?

import { Module } from '@nestjs/common'
import { AdminModule } from '@adminjs/nestjs'
import { Database, Resource } from '@adminjs/prisma';
import AdminJS from 'adminjs'
import { DMMFClass } from '@prisma/client/runtime';
import { PrismaService } from '@database/prisma.service';
import { DatabaseModule } from '@database/database.module';
import { ResourceWithOptions, ResourceOptions } from 'adminjs/types/src';

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

AdminJS.registerAdapter({ Database, Resource });

class CResource {
  model: any;
  options: ResourceOptions;

  constructor(model: any, options?: ResourceOptions) {
    this.model = model;
    this.options = options || {};
  }
}

class CResourceBuilder {
  private readonly resources: Array<CResource> = [];
  dmmf: DMMFClass;

  constructor(private readonly prisma: PrismaService) {
    this.dmmf = ((prisma as any)._baseDmmf as DMMFClass)
  }

  /**
   * Adds a resource to the builder
   * 
   * @param resource string
   * @param options ResourceOptions
   * @returns this
   */
  public addResource(resource: string, options?: ResourceOptions): this {
    const obj = new CResource(this.dmmf.modelMap[resource], options);
    this.resources.push(obj);
    return this;
  }

  /**
   * Compiles the resources into an array of objects
   * that can be passed to the AdminJS module
   * 
   * @returns Array<ResourceWithOptions | any>
   */
  public build(): Array<ResourceWithOptions | any> {
    return this.resources.map((resource) => {
      return {
        resource: {
          model: resource.model,
          client: this.prisma,
        },
        options: resource.options,
      }
    })
  }
}

@Module({
  imports: [
    AdminModule.createAdminAsync({
      imports: [DatabaseModule],
      useFactory: (prisma: PrismaService) => {
        const dmmf = (prisma as any)._baseDmmf as DMMFClass;

        return {
          adminJsOptions: {
            rootPath: '/admin',

            // updated here
            resources: new CResourceBuilder(prisma)
              .addResource('User')
              .addResource('Profile')
              .addResource('Post')
              .build(),
          },
          auth: {
            authenticate,
            cookieName: 'adminjs',
            cookiePassword: 'secret'
          },
          sessionOptions: {
            resave: true,
            saveUninitialized: true,
            secret: 'secret'
          },
        }
      },
      inject: [PrismaService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

Enter fullscreen mode Exit fullscreen mode

You can always export the classes, functions and objects from an external file.

Mastering AdminJs

Conclusion

In conclusion, AdminJS is a powerful tool for creating admin dashboards for NestJS and Prisma applications. With its adapters for various ORM and ODM models, it simplifies the process of performing CRUD operations on models. It also provides customization options for the dashboard, making it easy to tailor it to specific project needs. By following the steps outlined in the article, developers can quickly set up AdminJS and start using it to manage their application's data.

Follow me on Twitter @netrobeweb and Hashnode where I post amazing projects and articles.

Thanks for reading, 😉.

Top comments (1)

Collapse
 
alip990 profile image
Alireza Alami

hi,
nestjs need ,
{
"compilerOptions": {
"moduleResolution": "node16",
"module": "commonjs",
"target": "esnext",
// ...
}
}

in tsconfig for importing @adminjs/nestjs