Skip to content

Latest commit

 

History

History
597 lines (511 loc) · 17.3 KB

File metadata and controls

597 lines (511 loc) · 17.3 KB

Getting Started

Step-by-step guide to creating an AdminForth app with the CLI (bootstrapping), understanding the generated project structure, running migrations, and starting the server.

Getting Started

This page provides a step-by-step guide to quickly get started with AdminForth using the adminforth CLI. You will learn how to set up a new project using the adminforth create-app command and explore AdminForth’s fundamentals.

👆 For setup example without CLI check out Hello World without CLI

Prerequisites

AdminForth requires Node v20 or higher. If you’re on a different version, you can switch or install using:

nvm install 20
nvm alias default 20
nvm use 20

Creating an AdminForth Project

The recommended way to get started with AdminForth is via the create-app CLI, which scaffolds a basic fully functional back-office application. Apart from boilerplate, it creates one resource for user management.

There are two common setup paths:

Path 1: Existing Database

Use this path when you already have a database and your own schema or migrations. Pass your database URL with --db, or enter it when the CLI asks Please specify the database URL to use:

npx adminforth create-app --app-name myadmin --db "postgresql://user:password@localhost:5432/dbname"

When you provide your own database URL, the CLI treats this as your own database. It does not create Prisma schema or Prisma migration scripts for that database. Instead, the generated project README contains the SQL or schema notes for adding the required adminuser table with your own migration tool.

After the project is created, navigate into it and generate resources from your existing tables:

cd myadmin
npx adminforth resource

Resource files are needed for AdminForth to know about your tables and define how to work with them. Use npx adminforth resource again when you add new tables or change their schema.

Path 2: New Database

Use this path when you want AdminForth to scaffold a standalone app with a new local SQLite database. Omit --db, or accept the default sqlite://.db.sqlite value in the interactive prompt:

npx adminforth create-app --app-name myadmin

Or omit all options to be prompted interactively:

npx adminforth create-app

Once the project is created, navigate into its directory:

cd myadmin # or any other name you provided

For the new database path, the CLI can scaffold Prisma files and migration scripts for the default SQLite database.

CLI options:

  • --app-name - name for your project. Used in package.json, index.ts branding, etc. Default value: adminforth-app.
  • --db - database connection string. Currently PostgreSQL, MongoDB, SQLite, MySQL, Clickhouse and Qdrant (read only) are supported. Default value: sqlite://.db.sqlite

☝️ Database Connection String format:

Format is <scheme>://<username>:<password>@<host>:<port>/<database>. Examples:

  • SQLite — sqlite://.db.sqlite. If database not yet exists it will be created
  • PostgreSQL — postgresql://user:password@localhost:5432/dbname
  • MongoDB — mongodb://localhost:27017/dbname
  • Clickhouse — clickhouse://localhost:8123/dbname
  • MySQL — mysql://user:password@localhost:3306/dbname
  • Qdrant - qdrant://localhost:6333

Understand the generated Project Structure

The CLI will create boilerplate files and folders in your current directory and install dependencies. A typical layout looks like this:

myadmin/
├── custom
│   ├── assets/           # Static assets like images, fonts, etc.
│   ├── package.json      # For any custom npm packages you will use in Vue files
│   └── tsconfig.json     # Tsconfig for Vue project (adds completion for AdminForth core components)
├── resources
│   └── adminuser.ts      # Example resource file for users management
├── schema.prisma         # Prisma schema file, generated only for the new database path
├── index.ts              # Main entry point: configures AdminForth & starts the server
├── package.json          # Project dependencies
├── pnpm-workspace.yaml
├── tsconfig.json         # TypeScript configuration
├── .env                  # Env vars like tokens, secrets that should not be in version control
├── .env.local            # General local environment variables
└── .gitignore

Initial Migration & Future Migrations

For the new database path, the CLI creates Prisma files for managing migrations. Prisma is not required by AdminForth itself, but it is a convenient migration tool for standalone projects that do not have database management yet.

CLI will suggest you a command to initialize the database with Prisma:

pnpm makemigration --name init && pnpm migrate:local

This will create a migration file and apply it to the database.

In future, when you need to add new resources, you need to modify schema.prisma (add models, change fields, etc.). After doing any modification you need to create a new migration using next command:

pnpm makemigration --name init ; pnpm migrate:local

Other developers need to pull migration and run pnpm migrate:local to apply any unapplied migrations.

For the existing database path, use your own migration tool instead. The generated project README shows how to add the required adminuser table to your database.

Run the Server

Now you can run your app:

pnpm start

Open http://localhost:3500 in your browser and (default credentials are adminforth/adminforth if you haven’t changed them).

alt text

AdminForth Basic Philosophy

AdminForth connects to existing databases and provides a back-office for managing data including CRUD operations, filtering, sorting, and more.

Database can be already created by using any database management tool, ORM or migrator.

AdminForth itself never modifies database schema, does not add columns or new tables. However for those who have no own migration managment AdminForth CLI suggests using Prisma. This allows to provide simple and reliable schema management for standalone projects which have no DB yet.

If you already have a database, you pass a connection string to AdminForth and define resources(tables) and describe columns you would like to see in back-office. For most DBs AdminForth can "discover" column types and constraints (e.g. max-length) by connecting to DB. However you can redefine them in AdminForth configuration. Type and constraints definition in AdminForth resource are take precedence over DB schema.

Also in AdminForth you can define in "Vue" way:

  • how each field will be rendered
  • create own pages e.g. Dashboard using AdminForth Components Library (AFCL) or any other Vue componetns.
  • insert injections into standard pages (e.g. add diagram to list view)

Adding an apartments Model

So far, our freshly generated AdminForth project includes a default adminuser model and a corresponding adminuser resource.

Let’s expand our app to suport managment of apartments model. Adding new resource will involve next steps:

  1. Add a new Prisma model to your schema.prisma.
  2. Run a Prisma migration to update your database schema.
  3. Create a corresponding resource in the resources/ folder.
  4. Register the new resource in index.ts and see it in your AdminForth back-office.

Please note that steps 1 and 2 are compleatly independent from 3 and 4, so you can make them with any other way then Prisma.

Step 1. Define the apartments Model in schema.prisma

Open schema.prisma in your project root and add a new model for apartments:

...
//diff-add
model apartments {
//diff-add
  id                String     @id
//diff-add
  created_at        DateTime?
//diff-add
  title             String
//diff-add
  square_meter      Float?
//diff-add
  price             Decimal
//diff-add
  number_of_rooms   Int?
//diff-add
  description       String?
//diff-add
  country           String?
//diff-add
  listed            Boolean
//diff-add
  realtor_id        String?
//diff-add
}

Step 2. Create and Apply the Migration

Run the following command to create a new migration:

pnpm makemigration --name add-apartments ; pnpm migrate:local

Step3. Create the apartments resource

Use command to create a new file apartments.ts in the resources/ folder

npx adminforth resource

After the resource file is generated, extend it with display and validation settings.

  • Use recordLabel to control how each record is represented in lists and relations.
  • Apply fillOnCreate to automatically populate fields during creation (e.g., generated IDs or timestamps).
  • Add minLength and maxLength to string fields to enforce input constraints.
  • Use enum to limit fields to predefined values and render them as dropdowns in the UI.
  • Configure allowedActions to control which operations are available for the resource (editing, deleting, viewing, and filtering).

To properly apply these changes, refer to the example below and adjust the configuration according to your settings

import { AdminForthResourceInput, AdminForthDataTypes } from 'adminforth';

export default {
  dataSource: 'maindb',
  table: 'apartments',
  //diff-remove
  resourceId: 'apartments'
  //diff-add
  resourceId: 'aparts', // resourceId is defaulted to table name but you can redefine it like this e.g. 
  //diff-add
  // in case of same table names from different data sources
  label: 'Apartments',   // label is defaulted to table name but you can change it
  //diff-add
  recordLabel: (r) => `🏡 ${r.title}`,
  columns: [
    {
      name: 'id',
      //diff-add
      type: AdminForthDataTypes.STRING,
      //diff-add
      label: 'Identifier',  // if you wish you can redefine label, defaulted to uppercased name
      showIn: { // show column in filter and in show page
        //diff-remove
        all:true,
        //diff-add
        list: false,
        //diff-add
        edit: false,
        //diff-add
        create: false,
      },
      //diff-add
      primaryKey: true,
      //diff-add
      fillOnCreate: ({ initialRecord, adminUser }) => Math.random().toString(36).substring(7),  // called during creation to generate content of field, initialRecord is values user entered, adminUser object of user who creates record
    },
    {
      name: "title",
      //diff-add
      required: true,
      showIn: {
        all:true, // all available options
      },
      //diff-add
      type: AdminForthDataTypes.STRING,
      //diff-add
      maxLength: 255,  // you can set max length for string fields
      //diff-add
      minLength: 3,  // you can set min length for string fields
    },
    {
      name: 'created_at',
      //diff-add
      type: AdminForthDataTypes.DATETIME,
      //diff-add
      allowMinMaxQuery: true,
      showIn: {
        //diff-remove
        all:true,
        //diff-add
        create: false,
      },
      //diff-add
      fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
    },
    {
      name: 'price',
      showIn: {
        all:true,
      },
      //diff-add
      inputSuffix: 'USD', // you can add a suffix to an input field that will be displayed when creating or editing records
      //diff-add
      allowMinMaxQuery: true,  // use better experience for filtering e.g. date range, set it only if you have index on this column or if you sure there will be low number of rows
      //diff-add
      editingNote: 'Price is in USD',  // you can put a note near field on editing or creating page
    },
    {
      name: 'square_meter',
      //diff-add
      label: 'Square',
      //diff-add
      allowMinMaxQuery: true,
      showIn: {
        all:true,
      },
      //diff-add
      minValue: 1,  // you can set min /max value for number columns so users will not be able to enter more/less
      //diff-add
      maxValue: 1000,
    },
    {
      name: 'number_of_rooms',
      //diff-add
      allowMinMaxQuery: true,
      showIn: {
        all:true,
      },
      //diff-add
      enum: [
        //diff-add
        { value: 1, label: '1 room' },
        //diff-add
        { value: 2, label: '2 rooms' },
        //diff-add
        { value: 3, label: '3 rooms' },
        //diff-add
        { value: 4, label: '4 rooms' },
        //diff-add
        { value: 5, label: '5 rooms' },
        //diff-add
      ],
    },
    {
      name: 'description',
      //diff-add
      sortable: false,
      showIn: {
        //diff-remove
        all:true,
        //diff-add
        list: false,
      }
    },
    {
      name: 'country',
      showIn: {
        all:true,
      },
      //diff-add
      enum: [{
        //diff-add
        value: 'US',
        //diff-add
        label: 'United States'
        //diff-add
      }, {
        //diff-add
        value: 'DE',
        //diff-add
        label: 'Germany'
        //diff-add
      }, {
        //diff-add
        value: 'FR',
        //diff-add
        label: 'France'
        //diff-add
      }, {
        //diff-add
        value: 'GB',
        //diff-add
        label: 'United Kingdom'
        //diff-add
      }, {
        //diff-add
        value: 'NL',
        //diff-add
        label: 'Netherlands'
        //diff-add
      }, {
        //diff-add
        value: 'IT',
        //diff-add
        label: 'Italy'
        //diff-add
      }, {
        //diff-add
        value: 'ES',
        //diff-add
        label: 'Spain'
        //diff-add
      }, {
        //diff-add
        value: 'DK',
        //diff-add
        label: 'Denmark'
        //diff-add
      }, {
        //diff-add
        value: 'PL',
        //diff-add
        label: 'Poland'
        //diff-add
      }, {
        //diff-add
        value: 'UA',
        //diff-add
        label: 'Ukraine'
        //diff-add
      }, {
        //diff-add
        value: null,
        //diff-add
        label: 'Not defined'
        //diff-add
      }],
    },
    {
      name: 'listed',
      //diff-add
      required: true,  // will be required on create/edit
      showIn: {
        all:true,
      }
    },
    {
      name: 'realtor_id',
      //diff-add
      foreignResource: {
        //diff-add
        resourceId: 'adminuser',
        //diff-add
        searchableFields: ["id", "email"], // fields available for search in filter
        //diff-add
      },
      showIn: {
        all:true,
      }
    }
  ],
  options: {
    listPageSize: 10,
    //diff-add
    allowedActions: {
      //diff-add
      edit: true,
      //diff-add
      delete: true,
      //diff-add
      show: true,
      //diff-add
      filter: true,
      //diff-add
    },
  },
} as AdminForthResourceInput;

Step 4. Register the apartments Resource

Open index.ts in your project root and import the new resource:

...
//diff-add
import apartmentsResource from "./resources/apartments.js";

...
export const admin = new AdminForth({
  ...
  menu: [
//diff-add
    {
//diff-add
      label: 'Core',
//diff-add
      icon: 'flowbite:brain-solid',
//diff-add
      open: true,
//diff-add
      children: [
//diff-add
        {
//diff-add
          homepage: true,
//diff-add
          label: 'Apartments',
//diff-add
          icon: 'flowbite:home-solid',
//diff-add
          resourceId: 'aparts',
//diff-add
        },
//diff-add
      ]
//diff-add
    },
//diff-add
    { type: 'gap' },
//diff-add
    { type: 'divider' },
    {
      type: 'heading',
      label: 'SYSTEM'
    },
    {
      label: 'Users',
      icon: 'flowbite:user-solid',
      resourceId: 'adminuser'
    },
    {
      label: "Apartments",
      icon: "flowbite:user-solid",
      //diff-remove
      resourceId: "apartments",
      //diff-add
      resourceId: "aparts",
    },
  ],
  ...
});

Generating fake appartments

//diff-add
async function seedDatabase() {
//diff-add
  if (await admin.resource('aparts').count() > 0) {
//diff-add
    return
//diff-add
  }
//diff-add
  for (let i = 0; i < 100; i++) {
//diff-add
    await admin.resource('aparts').create({
//diff-add
      id: `${i}`,
//diff-add
      title: `Apartment ${i}`,
//diff-add
      square_meter: Number((Math.random() * 100).toFixed(1)),
//diff-add
      price: (Math.random() * 10000).toFixed(2),
//diff-add
      number_of_rooms: Math.floor(Math.random() * 4) + 1,
//diff-add
      description: 'Next gen apartments',
//diff-add
      created_at: (new Date(Date.now() - Math.random() * 60 * 60 * 24 * 14 * 1000)).toISOString(),
//diff-add
      listed: i % 2 == 0,
//diff-add
      country: `${['US', 'DE', 'FR', 'GB', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA'][Math.floor(Math.random() * 10)]}`
//diff-add
    });
//diff-add
  };
//diff-add
};

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {

  ...

  admin.discoverDatabases().then(async () => {
    if (!await admin.resource('adminuser').get([Filters.EQ('email', 'adminforth')])) {
      await admin.resource('adminuser').create({
        email: 'adminforth',
        password_hash: await AdminForth.Utils.generatePasswordHash('adminforth'),
        role: 'superadmin',
      });
    }
//diff-add
    await seedDatabase();
  });

This will create records during first launch. Now you should see: alt text

Feel free to play with the data, add more fields, and customize the UI to your liking.