Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add generate grid form views guide #3338

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: API Entity with CRUD Generator
---

## Generating an entity with the api-generator

Since we are building the API first, we will start by generating a new entity. Go into the `api` folder and create a new directory, e.g., `api/src/customer`. Inside this directory, create a new file `customer.entity.ts` with the following content:

```typescript
import { CrudField, CrudGenerator } from "@comet/cms-api";
import { BaseEntity, Entity, PrimaryKey, Property } from "@mikro-orm/core";
import { Field, ID, ObjectType } from "@nestjs/graphql";
import { v4 } from "uuid";

@Entity()
@ObjectType()
@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "customer" })
export class Customer extends BaseEntity<Customer, "id"> {
@CrudField({ search: true, filter: true, sort: true, input: false })
@Field(() => ID)
@PrimaryKey({ columnType: "uuid" })
id: string = v4();

@CrudField({ search: true, filter: false, sort: false, input: true })
@Field()
@Property({ columnType: "text" })
firstName: string;

@CrudField({ search: true, filter: false, sort: false, input: true })
@Field()
@Property({ columnType: "text" })
lastName: string;

@CrudField({ search: false, filter: false, sort: false, input: false })
@Property({ onUpdate: () => new Date() })
@Field()
updatedAt?: Date = new Date();
}
```

Currently, there is no `api-generator` watch mode - so every time you change the entity following command must be run. Information how to setup API Generator can be found [Setup API Generator](../../../1-getting-started/4-crud-generator/1-api-generator.md) Section.

```bash
cd api
npm run api-generator
```

The **API Generator** will generate multiple files inside the specified `../generated` output directory. It will contain a `customer.resolver.ts` and some dto related files (`customer.filter.ts`, `customer.input.ts`, `customer.sort.ts`, `customer.args.ts`, `paginated-customer.ts`).

![CustomerGeneratedDirectoryStructure](./images/customerDirectoryStructure.png)

To integrate the new entity into the application, you have to add a new NestJS module. Create a new file `customer.module.ts` in the `api/src/customer` directory with the following content:

```typescript
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";

import { Customer } from "./entities/customer.entity";
import { CustomerResolver } from "./generated/customer.resolver";

@Module({
imports: [MikroOrmModule.forFeature([Customer])],
providers: [CustomerResolver],
})
export class CustomerModule {}
```

This generated module must be added to the `api/src/app.module.ts` and registered.

```typescript
import { DynamicModule, Module } from "@nestjs/common";
import { CustomerModule } from "@src/customer/customer.module";
import { DbModule } from "@src/db/db.module";
import { ContentScope as BaseContentScope } from "@src/site-configs";

import { Config } from "./config/config";
import { ConfigModule } from "./config/config.module";

@Module({})
export class AppModule {
static forRoot(config: Config): DynamicModule {
return {
module: AppModule,
imports: [ConfigModule.forRoot(config), DbModule, CustomerModule],
};
}
}

//...
```

As soon the new `CustomerModule` is registered in the app module, the schema will update, and provide the new generated Queries/Mutations, including the Object types and necessary inputs

```graphql
type Customer {
id: ID!
firstName: String!
lastName: String!
updatedAt: DateTime!
}

type PaginatedCustomers {
nodes: [Customer!]!
totalCount: Int!
}

input CustomerFilter {
id: StringFilter
and: [CustomerFilter!]
or: [CustomerFilter!]
}

input CustomerSort {
field: CustomerSortField!
direction: SortDirection! = ASC
}

enum CustomerSortField {
id
}

type Query {
# ...
customer(id: ID!): Customer!
customers(
offset: Int! = 0
limit: Int! = 25
search: String
filter: CustomerFilter
sort: [CustomerSort!]
): PaginatedCustomers!
}

input CustomerInput {
firstName: String!
lastName: String!
}

input CustomerUpdateInput {
firstName: String
lastName: String
}

type Mutation {
# ...
createCustomer(input: CustomerInput!): Customer!
updateCustomer(id: ID!, input: CustomerUpdateInput!): Customer!
deleteCustomer(id: ID!): Boolean!
}
```

Schema should already be available when you start the GraphQL Playground locally `http:4000/api/graphql`, and you should be able to play around with the queries.

![CustomersQueryInPlaygroundWithError](./images/customersQueryInPlaygroundWithError.png)

As soon as you start to execute the first Query, one will see that @mikro-orm will through an error, that the Customer Table does not exists in the Database.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Create a migration
---

Now it's time to create a simple migration that will create an empty table in the database for the newly created Customer entity. First of all, create a migration with the following command:

```
npx mikro-orm migration:create
```

![CreateMigration](./images/createMigration.png)

Mikro orm will crate a Migrations file in the `api/migrations` directory. Revise the migrations, and cleanup not necessary sql statements. The migrations to create the Customer table, should then look something like:

```typescript
import { Migration } from "@mikro-orm/migrations";

export class Migration20250203143416 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table "Customer" ("id" uuid not null, "firstName" text not null, "lastName" text not null, "updatedAt" timestamptz(0) not null, constraint "Customer_pkey" primary key ("id"));',
);
}
}
```

More infos concerning mikro-orm and the related cli can be found here [mikro-orm - using via cli](https://mikro-orm.io/docs/migrations#using-via-cli).

To execute the migration and insert the data in the database run the following command:

```bash
npm run db:migrate
```

![ExecuteMigration](./images/executeMigration.png)

Now we are ready to execute the Query again in the GraphQL Playground, and one should see that the Query is executed successfully.

![CustomersQueryInPlaygroundWithError](./images/customerQueryInPlaygroundSuccessfull.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Create fixtures
---

To insert some data into the database, we will use fixtures. Fixtures are a way to insert data into the database. The fixtures are defined in the `db/fixtures/generators` directory in the `api` directory. To generate fixtures, it's often useful to generate a lot of data.[@faker-js/faker](https://github.com/faker-js/faker) is often a good choice for generating a lot of random data.

## Create Fixtures

Create a new file in the `fixtures` directory with the following content:

```
customer-fixtures.service.ts
```

```typescript
import { faker } from "@faker-js/faker";
import { EntityRepository } from "@mikro-orm/postgresql";
import { Customer } from "@src/customer/entities/customer.entity";
import { mapLimit } from "async";
import { SingleBar } from "cli-progress";

interface GenerateCustomerOptions {
repository: EntityRepository<Customer>;
bar: SingleBar;
total: number;
}

export const generateCustomers = async ({
repository,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good if we'd promote a real service with dependency injection here: https://github.com/vivid-planet/comet/blob/main/demo/api/src/db/fixtures/generators/products-fixture.service.ts#L11

bar,
total,
}: GenerateCustomerOptions): Promise<Customer[]> => {
const generateRandomCustomer = async (): Promise<Customer> => {
const customer = repository.create({
id: faker.string.uuid(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
});

bar.increment(1, {
title: "Customer",
});

return customer;
};

return mapLimit<number, Customer>(Array(total), 100, async () => generateRandomCustomer());
};
```

The `generateCustomers` functions is a convenience function, that receives some options, like the repository, a progress bar and the total number of customers to generate. The function then generates the customers and increments the progress bar.

## Add Fixtures to the FixturesConsole

Additionally, the created fixtures (`generateCustomer`) must be called and executed. Open `db/fixtures/generators/fixtures.console.ts` an add the fixtures functions calls so the function gets execute when fixtures get created:

```typescript
export class FixturesConsole {
//....
async execute(total?: string | number): Promise<void> {
//...

const multiBar = new MultiBar(this.barOptions, Presets.shades_classic);

// Add your fixtures here
await Promise.all([
generateCustomers({
repository: this.orm.em.getRepository(Customer),
bar: multiBar.create(total, 0),
total,
}),
]);

multiBar.stop();
// ...

await this.orm.em.flush();
}
}
```

## Execute Fixtures

Everything should be set up now. To execute the fixtures, run the following command:

```bash
cd api
npm run fixtures
```

## Verify Fixtures

Now we are ready to execute the Query again in the GraphQL Playground, and one should see that the Query is executed successfully and will return the generated Data

![Fixtures](./images/customerQueryInPlaygroundWithFixtureData.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Admin Generator create DataGrid
---

The Admin Generator is a powerful tool to generate a complete CRUD interface for your entities. We will show you how to create a DataGrid with the Admin Generator.

## Generate DataGrid

To generate a DataGrid with the Admin Generator, you have to create a `.cometGen.ts` file. Those files are used by the Admin Generator to generate different kinds of interfaces (DataGrids or Forms). Create a new file `CustomerGrid.cometGen.ts` in the `api/src/customer` directory with the following content:

```typescript
import { future_GridConfig as GridConfig } from "@comet/cms-admin";
import { GQLCustomer } from "@src/graphql.generated";

export const CustomerGrid: GridConfig<GQLCustomer> = {
type: "grid",
gqlType: "Customer",
columns: [
{ type: "text", name: "id" },
{ type: "text", name: "firstName" },
{ type: "text", name: "lastName" },
],
};
```

The `@comet/cms-admin` has types available for all the different components that can be generated. The `GridConfig` type is used to define the configuration of the DataGrid. The `GQLCustomer` got created in the section before, with the crud generator. The `GQLCustomer`
type is the GraphQL type that will be used to guarantee type safety. The `columns` array defines the columns of the DataGrid. Each column has a `type`, `name` and other properties.


## Run Admin Generator

Unfortunately there is no watch mode available for the Admin Generator. To generate the DataGrid, you have to run the Admin Generator manually. This will delete all already generated files and generate them again.

Run the following command:

```bash
cd admin
npm run admin-generator
```

The `admin-generator` script is configured in `admin/package.json` and will execute the admin generator binary.

![AdminGeneratorCLI](./images/adminGeneratorCli.png)

Two files will be generated in the `admin/src/customers/generated` directory. The `CustomerGrid.tsx` and the `CustomerGrid.generated.ts`. The `CustomerGrid.tsx` is the React component that will render the DataGrid. The `CustomerGrid.generated.ts` has related types and interfaces from the GraphQL Api.

![CustomerGridDirectoryStructure](./images/customerGridIDE.png)

The component is ready to be used in the application. Simple use the generated component somewhere in your React Application:

`src/customers/CustomerPage.tsx`

```typescript
import { StackMainContent } from "@comet/admin";
import { CustomersGrid } from "@src/customers/generated/CustomerGrid";
import { FunctionComponent } from "react";

export const CustomerPage: FunctionComponent = () => {
return (
<StackMainContent fullHeight>
<CustomersGrid />
</StackMainContent>
);
};
```

The generated CustomerGrid will look like this:

![CustomerGrid](./images/customerDataGrid.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading