Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/quramy/prisma-fabbrica

Prisma generator to define model factory
https://github.com/quramy/prisma-fabbrica

factory factory-bot jest prisma prisma-orm

Last synced: about 19 hours ago
JSON representation

Prisma generator to define model factory

Awesome Lists containing this project

README

        

# prisma-fabbrica

[![github actions](https://github.com/Quramy/prisma-fabbrica/workflows/build/badge.svg)](https://github.com/Quramy/talt/actions)
[![npm version](https://badge.fury.io/js/@quramy%2Fprisma-fabbrica.svg)](https://badge.fury.io/js/@quramy%2Fprisma-fabbrica)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/Quramy/prisma-fabbrica/main/LICENSE)

Prisma generator for model factories.

## ToC

- [Getting started](#getting-started)
- [Usage of factories](#usage-of-factories)
- [Field default values](#field-default-values)
- [Use sequence for scalar fields](#use-sequence-for-scalar-fields)
- [Shorthand for create list](#shorthand-for-create-list)
- [Required relation](#required-relation)
- [Connection helper](#connection-helper)
- [Build input data only](#build-input-data-only)
- [has-many / has-one relation](#has-many--has-one-relation)
- [Custom scalar field generation](#custom-scalar-field-generation)
- [Traits](#traits)
- [Callbacks](#callbacks)
- [Transient fields](#transient-fields)
- [Field value precedence](#field-value-precedence)
- [More examples](#more-examples)
- [Generator configuration](#generator-configuration)
- [Tips](#tips)
- [Works with jest-prisma](#works-with-jest-prisma)
- [Suppress TS circular dependencies error](#suppress-ts-circular-dependencies-error)
- [Factory interface with types](#factory-interface-with-types)
- [Version compatibility](#version-compatibility)
- [License](#license)

## Getting started

```sh
npm i @quramy/prisma-fabbrica -D
```

Then, edit your `prisma/schema.prisma` and append the prisma-fabbrica generator configuration:

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

generator fabbrica {
provider = "prisma-fabbrica"
}
```

And run generate command.

```sh
npx prisma generate
```

The above command generates JavaScript and TypeScript type definition files under `src/__generated__/fabbrica` directory. You can define your model factory importing them.

For example, if `schema.prisma` has the following `User` model, you can import `defineUserFactory` and define `UserFactory` using this function.

```graphql
model User {
id String @id
name String
posts Post[]
}
```

```ts
/* src/seed.ts */

import { PrismaClient } from "@prisma/client";

import { initialize, defineUserFactory } from "./__generated__/fabbrica";

const prisma = new PrismaClient();
initialize({ prisma });

async function seed() {
const UserFactory = defineUserFactory();

await UserFactory.create();
await UserFactory.create({ name: "Alice" });
await UserFactory.create({ id: "user002", name: "Bob" });

console.log(await prisma.user.count()); // -> 3
}

seed();
```

> [!NOTE]
> The factories use Prisma client instance passed by `initialize` function.

If you want to use factories in your test code see [Works with jest-prisma](#works-with-jest-prisma) section below.

## Usage of factories

### Field default values

Factory by defined with `defineUserFactory` automatically fills required scalar fields.

For example, the following `User` model has some required field, `id`, `email`, `firstName` and `lastName` .

```graphql
model User {
id Int @id
email String @unique
firstName String
lastName String
middleName String?
createdAt DateTime @default(now())
}
```

```ts
const UserFactory = defineUserFactory();

await UserFactory.create(); // Insert record with auto filled id, email, firstName and lastName values
```

See https://github.com/Quramy/prisma-fabbrica/blob/main/packages/prisma-fabbrica/src/scalar/gen.ts if you want auto filling rule details.

> [!NOTE]
> prisma-fabbrica auto filling does not generate values of fields with `@default()` function because these fields are not required and values of them are generated by Prisma engine.

Default filling rule also can be overwritten.

```ts
const UserFactory = defineUserFactory({
defaultData: async () => ({
email: await generateRandomEmailAddress(),
}),
});

await UserFactory.create();
```

### Use sequence for scalar fields

`seq` parameter provides sequential number which increments when called `.create()` .

```ts
const UserFactory = defineUserFactory({
defaultData: async ({ seq }) => ({
id: `user${seq.toString().padStart(3, "0")}`,
}),
});

await UserFactory.create(); // Insert with id: "user000"
await UserFactory.create(); // Insert with id: "user001"
await UserFactory.create(); // Insert with id: "user002"
```

And the sequential number can be reset via `resetSequence` .

```ts
/* your.testSetup.ts */

import { resetSequence } from "./__generated__/fabbrica";

beforeEach(() => resetSequence());
```

### Shorthand for create list

Each factory provides `.createList` method to insert multiple records.

```ts
await UserFactory.createList(3);

// The above code is equivalent to the following

await Promise.all([0, 1, 2].map(() => UserFactory.create()));
```

The 2nd argument(optional) accepts an object which is assignable to `Partial` :

```ts
await UserFactory.createList(3, { name: "Bob" });
```

You can also pass list data assignable to `Partial[]` :

```ts
await UserFactory.createList([{ id: "user01" }, { id: "user02" }]);
```

### Required relation

Sometimes, creating a model requires other model existence. For example, the following model `Post` belongs to other model `User`.

```graphql
model User {
id String @id
name String
posts Post[]
}

model Post {
id String @id
title String
author User @relation(fields: [authorId], references: [id])
authorId String
}
```

You should tell how to connect `author` field when define Post factory.

#### Using related model factory (recommended)

The easiest way is to give `UserFactory` when `definePostFactory` like this:

```ts
const UserFactory = defineUserFactory();

const PostFactory = definePostFactory({
defaultData: {
author: UserFactory,
},
});
```

The above `PostFactory` creates `User` model for each `PostFactory.create()` calling,

#### Manual create or connect

Similar to using `prisma.post.create`, you can also use `connect` / `create` / `createOrConnect` options.

```ts
const PostFactory = definePostFactory({
defaultData: async () => ({
author: {
connect: {
id: (await prisma.user.findFirst()!).id,
},
// Alternatively, create or createOrConnect options are allowed.
},
}),
});
```

### Connection helper

Required relation rules can be overwritten when `.create` method. `createForConnect` can be used to connect.

```ts
const UserFactory = defineUserFactory();

const PostFactory = definePostFactory({
defaultData: {
author: UserFactory,
},
});

const author = await UserFactory.createForConnect();
await PostFactory.create({ author: { connect: author } });
await PostFactory.create({ author: { connect: author } });

const { posts } = await prisma.user.findUnique({ where: author, include: { posts: true } });
console.log(posts.length); // -> 2
```

### Build input data only

`.build` method in factories provides data set to create the model, but never insert.

```ts
await UserFactory.create();

// The above code is equivalent to the bellow:
const data = await UserFactory.build();
await prisma.user.create({ data });
```

For example, you can use `.build` method in other model's factory definition:

```ts
const UserFactory = defineUserFactory();

const PostFactory = definePostFactory({
defaultData: async () => ({
author: {
connectOrCreate: {
where: {
id: "user001",
},
create: await UserFactory.build({
id: "user001",
}),
},
},
}),
});

await PostFactory.create();
await PostFactory.create();

console.log(await prisma.user.count()); // -> 1
```

Like `createList`, `buildList` is also available.

### has-many / has-one relation

Sometimes, you may want a user data whose has post record. You can use `PostFactory.build` or `PostFactory.buildList` .

```ts
await UserFactory.create({
posts: {
create: await PostFactory.buildList(2),
},
});

console.log(await prisma.post.count()); // -> 2
```

> [!NOTE]
> In the above example, `PostFactory.build()` resolves JSON data such as:

```ts
{
id: "...",
title: "...",
author: { ... } // Derived from PostFactory defaultData
}
```

The `author` field is not allowed in `prisma.user.create` context. So `UserFactory` automatically filters the `author` field out in `.create` method.

### Custom scalar field generation

prisma-fabbrica provides function to complete scalar fields( https://github.com/Quramy/prisma-fabbrica/blob/main/packages/prisma-fabbrica/src/scalar/gen.ts ).

`registerScalarFieldValueGenerator` allows to custom this rule. For example:

```ts
import { registerScalarFieldValueGenerator } from "./__generated__/fabbrica";

registerScalarFieldValueGenerator({
String: ({ modelName, fieldName, seq }) => `${modelName}_${fieldName}_${seq}`,
});
```

`registerScalarFieldValueGenerator` accepts an object `Record`.
Field type is one of `Boolean`, `String`, `Int`, `Float`, `BigInt`, `Decimal`, `DateTime`, `Bytes`, and `Json`.
`FieldGenerateFunction` is a function to return corresponding fieled type.

See also https://github.com/Quramy/prisma-fabbrica/blob/main/packages/prisma-fabbrica/src/scalar/types.ts .

### Traits

Traits allow you to group fields together and apply them to factory.

```ts
const UserFactory = defineUserFactory({
defaultData: {
name: "sample user",
},
traits: {
withdrawal: {
data: {
name: "****",
status: "WITHDRAWAL",
},
},
},
});
```

`traits` option accepts an object and the option object's keys are treated as the trait's name. And you can set `data` option to the each trait key. The `data` option accepts value of the same types as the `defaultData` (i.e. plain object, function, async function)

And you can pass the trait's name to `UserFactory.use` function:

```ts
const deactivatedUser = await UserFactory.use("withdrawal").create();
```

Multiple traits are also available:

```ts
await UserFactory.use("someTrait", "anotherTrait").create();
```

### Callbacks

You can set callback function before or after factory execution.

```ts
const UserFactory = defineUserFactory({
onAfterCreate: async user => {
await PostFactory.create({
author: { connect: uesr },
});
},
});

await UserFactory.create();
```

Callback functions are also available within trait definition.

```ts
const UserFactory = defineUserFactory({
traits: {
withComment: {
onAfterCreate: async user => {
await PostFactory.create({
author: { connect: uesr },
});
},
},
},
});

await UserFactory.create();
await UserFactory.use("withComment").create();
```

> [!NOTE]
> The above code is to explain the callback. If you want to create association, first consider to use `defaultData` and `trait.data` option as in [has-many / has-one relation](#has-many--has-one-relation).

The following three types are available as callback function:

```ts
const UserFactory = defineUserFactory({
onAfterBuild: async createInput => {
// do something
},
onBeforeCreate: async createInput => {
// do something
},
onAfterCreate: async createdData => {
// do something
},
});
```

And here, the parameter types are:

- `createInput` is assignable to model create function parameter (e.g. `Prsima.UserCreateInput`).
- `createdData` is resolved object by model create function (e.g. `User` model type)

### Transient fields

Transient fields allow to define arbitrary parameters to factory and to pass them when calling `create` or `build`.

```ts
const UserFactory = defineUserFactory.withTransientFields({
loginCount: 0, // `0` is default value of this parameter
})({
defaultData: async ({ loginCount }) => {
// using loginCount
},
});

await UserFactory.create({ name: "Bob", loginCount: 10 });
```

Transient fields passed from factories' `create` method don't affect Prisma's `create` result.

> [!NOTE]
> You can't use model field names defined in your schema.prisma as transient parameters because they're not passed to `prisma.user.create` method.

Transient fields also can be accessed from [traits](#traits) or [callbacks](#callbacks).

```ts
const UserFactory = defineUserFactory.withTransientFields({
loginCount: 0,
})({
// Transient fields are passed to callback functions as the 2nd argument.
onAfterCreate: async (createdUser, { loginCount }) => {
for (let i = 0; i < loginCount; i++) {
await writeLoginLog(createdUser.id);
}
},
traits: {
data: async ({ loginCount }) => {
// using loginCount
},
},
});
```

### Field value precedence

Each field is determined in the following priority order(lower numbers have higher priority):

1. Factory's `.create` or `.build` function's argument
1. The applied trait's `data` entry
1. Factories `defaultData` entry
1. Value derived from `registerScalarFieldValueGenerator` if the field is required scalar(or enum)

### More examples

There are more example codes in https://github.com/Quramy/prisma-fabbrica/tree/main/examples/example-prj/src .

## Generator configuration

The following options are available:

```graphql
generator fabbrica {
provider = "prisma-fabbrica"
output = "../src/__generated__/fabbrica"
tsconfig = "../tsconfig.json"
noTranspile = false
}
```

- `output`: Directory path to generate files.
- `tsconfig`: TypeScript configuration file path. prisma-fabbrica uses it's `compilerOptions` when generating `.js` and `.d.ts` files. If missing tsconfig json file, fallback to `--target es2020 --module commonjs`.
- `noTranspile`: If set `true`, this generator only generates raw `.ts` file and stop to transpile to `.js` and `.d.ts` .

## Tips

### Works with jest-prisma

If you use [@quramy/jest-prisma](https://github.com/Quramy/jest-prisma) or [@quramy/jest-prisma-node](https://github.com/Quramy/jest-prisma/tree/main/packages/jest-prisma-node), you can pass `@quramy/prisma-fabbrica/scripts/jest-prisma` to `setupFilesAfterEnv` in your Jest configuration file.

```js
/* jest.config.mjs */

export default {
preset: "ts-jest",
transform: {
"^.+\\.tsx?$": "ts-jest",
},
testEnvironment: "@quramy/jest-prisma/environment",
setupFilesAfterEnv: ["@quramy/prisma-fabbrica/scripts/jest-prisma"],
};
```

This script calls prisma-fabbrica's `initialize` function and configures Prisma client used from each factory to integrate to join to transaction managed by jest-prisma.

### Suppress TS circular dependencies error

Sometimes, factories need each other factory as the following, however TypeScript compiler emits errors via circular dependencies.

```ts
// 'UserFactory' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
export const UserFactory = defineUserFactory({
defaultData: async () => ({
posts: {
connect: await PostFactory.buildList(1),
},
}),
});

// 'PostFactory' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
const PostFactory = definePostFactory({
defaultData: {
author: UserFactory,
},
});
```

`FactoryInterface` types are available to avoid this error. See [Factory interface with types](#factory-interface-with-types) section if you want usage of factory interface.

```ts
import { defineUserFactory, definePostFactory, type UserFactoryInterface } from "./__generated__/fabbrica";

const UserFactory = defineUserFactory({
defaultData: async () => ({
posts: {
connect: await PostFactory.buildList(1),
},
}),
});

function getUserFactory(): UserFactoryInterface {
return UserFactory;
}

const PostFactory = definePostFactory({
defaultData: {
author: getUserFactory(),
},
});
```

### Factory interface with types

> [!WARNING]
> Factory interface type parameters may change in future versions without notice.

Factory interface (e.g. `UserFactory` ) takes 2 optional type parameters:

- `TTransientFields`: Type of [transient fields](#transient-fields) object. By default, `Record` .
- `TTraitName`: Names of available [traits](#traits). By default, `string | symbol` .

For example:

```ts
// Specify transient fields type
declare function getUserFactory(): UserFactoryInterface<{ loginCount: number }>;

await getUserFactory().create({ loginCount: 10 });

// @ts-expect-error
await getUserFactory().create({ hoge: 10 });
```

```ts
// Specify available trait names
declare function getUserFactory(): UserFactoryInterface<{}, "someTrait" | "anotherTrait">;

await getUserFactory().use("someTrait").create();
await getUserFactory().use("anotherTrait").create();

// @ts-expect-error
await getUserFactory().use("hoge").create();
```

## Version compatibility

- **If your @prisma/client's version >= 5.0.0, install `@quramy/[email protected]`.**
- **If your @prisma/client's version < 5.0.0, install `@quramy/[email protected]`.**

## License

MIT