diff --git a/packages/accel-record-factory/README-ja.md b/packages/accel-record-factory/README-ja.md new file mode 100644 index 00000000..8f7de991 --- /dev/null +++ b/packages/accel-record-factory/README-ja.md @@ -0,0 +1,226 @@ +Language: [English](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README.md) | [日本語](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README-ja.md) + +# Accel Record Factory + +Accel Record用のファクトリーライブラリです。 + +## Getting Started + +```bash +npm install -D accel-record-factory +``` + +`prisma/schema.prisma` へ `prisma-generator-accel-record` の設定を追加します。 + +```prisma +generator client { + provider = "prisma-client-js" + output = "../src/prisma/client" +} + +generator accelRecord { + provider = "prisma-generator-accel-record" + output = "../src/models" + factoryPath = "../tests/factories" // ファクトリーファイルの出力先を追加 +} +``` + +`prisma/schema.prisma` を変更したら、以下のコマンドを実行してください。 + +```bash +npx prisma generate +``` + +例えば以下のようにUserモデルを定義した場合、 + +```ts +// prisma/schema.prisma +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + age Int? +} +``` + +以下のようなファクトリーファイルが自動生成されます。 + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + // firstName: "MyString", + // lastName: "MyString", + // age: 1, +}); + +export { UserFactory as $User }; +``` + +ファクトリーをインポートして使用することができます。 + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); + +const user = $User.create({ + firstName: "John", + lastName: "Doe", + age: 20, +}); +``` + +## 使い方 + +### デフォルト値の設定 + +defineFactoryの第二引数にデフォルト値を設定することができます。 + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: "John", // デフォルト値を設定 + lastName: "Doe", + age: 20, +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); +newUser.firstName; // => "John" +newUser.lastName; // => "Doe" +newUser.age; // => 20 +``` + +### 連番の利用 + +defineFactoryでデフォルト値を設定する際に、関数を指定することで連番を利用することができます。 + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: (seq) => `User${seq}`, // 連番を利用する関数を指定 +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const user1 = $User.build(); +user1.firstName; // => "User1" + +const user2 = $User.build(); +user2.firstName; // => "User2" +``` + +### 関連の生成 + +デフォルト値に関数を指定することで、関連を持つモデルを生成することができます。 + +```ts +// prisma/schema.prisma +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + age Int? + setting Setting? // 関連を持つモデル +} + +model Setting { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id]) + notify Boolean +} +``` + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User, Setting } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + setting: () => Setting.build({ notify: true }), // 関連を生成する関数を指定 +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const user = $User.build(); +user.setting.notify; // => true +``` + +### Traits + +defineFactoryの第三引数にtraitsを指定することで、複数種類のデフォルト値を設定することができます。 + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory( + User, + { + firstName: "John", + lastName: "Doe", + }, + { + traits: { + // trait名を指定 + foo: { + firstName: "Foo", + lastName: "Bar", + }, + }, + } +); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const john = $User.build({}); +john.firstName; // => "John" +john.lastName; // => "Doe" + +const foo = $User.build({}, "foo"); // traitを使用する +foo.firstName; // => "Foo" +foo.lastName; // => "Bar" +``` diff --git a/packages/accel-record-factory/README.md b/packages/accel-record-factory/README.md new file mode 100644 index 00000000..ddcb7fb3 --- /dev/null +++ b/packages/accel-record-factory/README.md @@ -0,0 +1,226 @@ +Language: [English](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README.md) | [日本語](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README-ja.md) + +# Accel Record Factory + +This is a factory library for Accel Record. + +## Getting Started + +```bash +npm install -D accel-record-factory +``` + +Add the configuration for `prisma-generator-accel-record` to `prisma/schema.prisma`. + +```prisma +generator client { + provider = "prisma-client-js" + output = "../src/prisma/client" +} + +generator accelRecord { + provider = "prisma-generator-accel-record" + output = "../src/models" + factoryPath = "../tests/factories" // Add the output destination for factory files +} +``` + +After modifying `prisma/schema.prisma`, run the following command: + +```bash +npx prisma generate +``` + +For example, if you define a User model like this: + +```ts +// prisma/schema.prisma +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + age Int? +} +``` + +The following factory file will be automatically generated: + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + // firstName: "MyString", + // lastName: "MyString", + // age: 1, +}); + +export { UserFactory as $User }; +``` + +You can import and use the factory: + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); + +const user = $User.create({ + firstName: "John", + lastName: "Doe", + age: 20, +}); +``` + +## Usage + +### Setting Default Values + +You can set default values by passing them as the second argument to defineFactory. + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: "John", // Set default value + lastName: "Doe", + age: 20, +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); +newUser.firstName; // => "John" +newUser.lastName; // => "Doe" +newUser.age; // => 20 +``` + +### Using Sequential Numbers + +When setting default values with defineFactory, you can use a function to utilize sequential numbers. + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: (seq) => `User${seq}`, // Specify a function to use sequential numbers +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const user1 = $User.build(); +user1.firstName; // => "User1" + +const user2 = $User.build(); +user2.firstName; // => "User2" +``` + +### Generating Associations + +By specifying a function for the default value, you can generate models with associations. + +```ts +// prisma/schema.prisma +model User { + id Int @id @default(autoincrement()) + firstName String + lastName String + age Int? + setting Setting? // Associated model +} + +model Setting { + id Int @id @default(autoincrement()) + userId Int + user User @relation(fields: [userId], references: [id]) + notify Boolean +} +``` + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User, Setting } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + setting: () => Setting.build({ notify: true }), // Specify a function to generate the association +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const user = $User.build(); +user.setting.notify; // => true +``` + +### Traits + +By specifying traits as the third argument to defineFactory, you can set multiple default values. + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory( + User, + { + firstName: "John", + lastName: "Doe", + }, + { + traits: { + // Specify the trait name + foo: { + firstName: "Foo", + lastName: "Bar", + }, + }, + } +); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const john = $User.build({}); +john.firstName; // => "John" +john.lastName; // => "Doe" + +const foo = $User.build({}, "foo"); // Use the trait +foo.firstName; // => "Foo" +foo.lastName; // => "Bar" +``` diff --git a/packages/accel-record-factory/index.ts b/packages/accel-record-factory/index.ts index 69217163..4d0794ff 100644 --- a/packages/accel-record-factory/index.ts +++ b/packages/accel-record-factory/index.ts @@ -1,35 +1,66 @@ -import { Model } from "accel-record-core"; +import { Meta, Model } from "accel-record-core"; -type BuildParams = Parameters>[0]; +type Functionable = { + [K in keyof T]: T[K] | ((seq?: number) => T[K]); +}; +type FunctionableUnion = D extends infer T + ? Functionable> + : never; + +export type BuildParams = Partial< + FunctionableUnion["CreateInput"]> +>; + +type BuildParamsCallable = + | BuildParams + | ((opt: { seq: number }) => BuildParams); -export const defineFactory = ( +export const defineFactory = < + T extends typeof Model, + S extends { [key: string]: BuildParamsCallable }, +>( model: T, - defaults: BuildParams | ((opt: { seq: number }) => BuildParams) + defaults: BuildParamsCallable, + options?: { + traits?: S; + } ) => { let seq = 0; - const devalueValues = () => { - if (typeof defaults == "function") { - return defaults({ seq: ++seq }); - } else { - return defaults; + type Trait = keyof S; + const callIfFunc = (arg: any) => + typeof arg === "function" ? arg(++seq) : arg; + const getValues = (params: BuildParams, traits: Trait[]) => { + const data = { ...callIfFunc(defaults) }; + for (const trait of traits) { + Object.assign(data, callIfFunc(options?.traits?.[trait])); } + Object.assign(data, params); + const ret = {} as any; + for (const [key, value] of Object.entries(data)) { + ret[key] = callIfFunc(value); + } + return ret; }; return { - create(params: BuildParams = {}): ReturnType> { - return model.create({ ...devalueValues(), ...params } as any); + create( + params: BuildParams = {}, + ...traits: Trait[] + ): ReturnType> { + return model.create(getValues(params, traits)); }, - build(params: BuildParams = {}): ReturnType> { - return model.build({ ...devalueValues(), ...params }); + build( + params: BuildParams = {}, + ...traits: Trait[] + ): ReturnType> { + return model.build(getValues(params, traits)); }, - createList(count: number, params: BuildParams = {}) { + createList(count: number, params: BuildParams = {}, ...traits: Trait[]) { return Array.from({ length: count }, () => - this.create({ ...defaults, ...params }) + this.create(params, ...traits) ); }, - buildList(count: number, params: BuildParams = {}) { - return Array.from({ length: count }, () => - this.build({ ...defaults, ...params }) - ); + buildList(count: number, params: BuildParams = {}, ...traits: Trait[]) { + return Array.from({ length: count }, () => this.build(params, ...traits)); }, }; }; diff --git a/packages/accel-record/README-ja.md b/packages/accel-record/README-ja.md index ad109380..0bca38c8 100644 --- a/packages/accel-record/README-ja.md +++ b/packages/accel-record/README-ja.md @@ -783,6 +783,37 @@ export default { }; ``` +### モデルのFactory + +テスト用のレコードを生成するために、Factoryを利用することができます。 +詳細は[accel-record-factory](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README-ja.md)を参照してください。 + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: "John", + lastName: "Doe", + age: 20, +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); +newUser.firstName; // => "John" +newUser.lastName; // => "Doe" +newUser.age; // => 20 +``` + ## バリデーション ### バリデーションのサンプル @@ -917,7 +948,5 @@ user.update({ age: undefined }); - [accel-record-core] 複合IDの対応 - [accel-record-core] クエリインターフェースの拡充 - [accel-record-core] 国際化(I18n) -- [accel-record-factory] trait -- [prisma-generator-accel-record] 各モデル用Factoryの生成 関連: [Accel Record Roadmap](https://github.com/koyopro/accella/issues/1) diff --git a/packages/accel-record/README.md b/packages/accel-record/README.md index 289fbd08..b33b7424 100644 --- a/packages/accel-record/README.md +++ b/packages/accel-record/README.md @@ -782,6 +782,37 @@ export default { }; ``` +### Model Factory + +To generate test records, you can use a Factory. +Please refer to [accel-record-factory](https://github.com/koyopro/accella/blob/main/packages/accel-record-factory/README.md) for more details. + +```ts +// tests/factories/user.ts + +import { defineFactory } from "accel-record-factory"; +import { User } from "../../src/models/index.js"; + +export const UserFactory = defineFactory(User, { + firstName: "John", + lastName: "Doe", + age: 20, +}); + +export { UserFactory as $User }; +``` + +```ts +// tests/user.test.ts + +import { $User } from "./factories/user"; + +const newUser = $User.build(); +newUser.firstName; // => "John" +newUser.lastName; // => "Doe" +newUser.age; // => 20 +``` + ## Validation ### Sample Validation @@ -916,7 +947,5 @@ user.update({ age: undefined }); - [accel-record-core] Support for Composite IDs - [accel-record-core] Expansion of Query Interface - [accel-record-core] Internationalization (I18n) -- [accel-record-factory] Trait -- [prisma-generator-accel-record] Generation of Factories for each Model Related: [Accel Record Roadmap](https://github.com/koyopro/accella/issues/1) diff --git a/packages/prisma-generator-accel-record/src/generator.ts b/packages/prisma-generator-accel-record/src/generator.ts index 61743682..0dcfa11a 100644 --- a/packages/prisma-generator-accel-record/src/generator.ts +++ b/packages/prisma-generator-accel-record/src/generator.ts @@ -1,15 +1,17 @@ import { generatorHandler, GeneratorOptions } from "@prisma/generator-helper"; -// import { logger } from "@prisma/sdk"; import * as fs from "fs"; import path from "path"; import { GENERATOR_NAME } from "./constants"; import { ensureApplicationRecord } from "./generators/applicationRecord"; +import { generateFactory } from "./generators/factory"; import { generateIndex, toCamelCase } from "./generators/index"; import { getModel as generateModel } from "./generators/model"; import { writeFileSafely } from "./utils/writeFileSafely"; const { version } = require("../package.json"); +const currentDir = process.cwd(); + generatorHandler({ onManifest() { return { @@ -31,12 +33,47 @@ generatorHandler({ await ensureApplicationRecord(options); + await generateModels(options, outputDir); + + await generateFactories(options, indexFile); + }, +}); + +const green = (text: string) => `\x1b[32m${text}\x1b[39m`; + +const generateModels = async (options: GeneratorOptions, outputDir: string) => { + for (const model of options.dmmf.datamodel.models) { + const fileName = `${toCamelCase(model.name)}.ts`; + const filePath = path.join(outputDir, fileName); + if (fs.existsSync(filePath)) continue; + await writeFileSafely(filePath, generateModel(model)); + console.info(`${green("create")}: ${path.relative(currentDir, filePath)}`); + } +}; + +const generateFactories = async ( + options: GeneratorOptions, + indexFile: string +) => { + const factoryDir = options.generator.config.factoryPath; + if (typeof factoryDir === "string") { + const prefix = factoryDir.startsWith("/") + ? "" + : path.dirname(options.schemaPath); for (const model of options.dmmf.datamodel.models) { const fileName = `${toCamelCase(model.name)}.ts`; - const filePath = path.join(outputDir, fileName); + const filePath = path.join(prefix, factoryDir, fileName); if (fs.existsSync(filePath)) continue; - await writeFileSafely(filePath, generateModel(model)); - // logger.info(`added: ${fileName}`); + const relative = path + .relative(path.dirname(filePath), indexFile) + .replace(/.ts$/, ".js"); + await writeFileSafely( + filePath, + generateFactory(model, { pathToIndex: relative }) + ); + console.info( + `${green("create")}: ${path.relative(currentDir, filePath)}` + ); } - }, -}); + } +}; diff --git a/packages/prisma-generator-accel-record/src/generators/factory.ts b/packages/prisma-generator-accel-record/src/generators/factory.ts new file mode 100644 index 00000000..b7b37d6b --- /dev/null +++ b/packages/prisma-generator-accel-record/src/generators/factory.ts @@ -0,0 +1,40 @@ +import { DMMF } from "@prisma/generator-helper"; + +export const generateFactory = ( + model: DMMF.Model, + params: { pathToIndex: string } +) => { + return `import { defineFactory } from "accel-record-factory"; +import { ${model.name} } from "${params.pathToIndex}"; + +export const ${model.name}Factory = defineFactory(${model.name}, { + ${model.fields + .filter((f) => !f.isId && !f.relationName && !f.isUpdatedAt && !f.default) + .map((field) => `// ${field.name}: ${value(field)}`) + .join(",\n ")} +}); + +export { ${model.name}Factory as $${model.name} }; +`; +}; + +const value = (field: DMMF.Field) => { + switch (field.type) { + case "BigInt": + case "Decimal": + case "Float": + case "Int": + return 1; + case "Bytes": + case "String": + return '"MyString"'; + case "Boolean": + return true; + case "DateTime": + return "new Date()"; + case "Json": + return "{}"; + default: + return undefined; + } +}; diff --git a/scripts/dev.patch b/scripts/dev.patch index 04bf47bb..d6cb07c5 100644 --- a/scripts/dev.patch +++ b/scripts/dev.patch @@ -1,9 +1,9 @@ diff --git packages/accel-record-core/package.json packages/accel-record-core/package.json -index 2f13478..0cf903f 100644 +index e4c8aaa..0f317ab 100644 --- packages/accel-record-core/package.json +++ packages/accel-record-core/package.json @@ -3,8 +3,8 @@ - "version": "0.2.0", + "version": "1.1.0", "description": "", "type": "module", - "main": "dist/index.js", @@ -11,5 +11,18 @@ index 2f13478..0cf903f 100644 + "main": "src/index.ts", + "types": "src/index.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc && cd src/ && find . -name '*js' -print0 | cpio -pd0 ../dist/" }, +diff --git packages/accel-record-factory/package.json packages/accel-record-factory/package.json +index ec3dfa9..03dfd7f 100644 +--- packages/accel-record-factory/package.json ++++ packages/accel-record-factory/package.json +@@ -3,7 +3,7 @@ + "version": "1.1.0", + "description": "", + "type": "module", +- "main": "dist/index.js", ++ "main": "index.ts", + "types": "index.ts", + "scripts": { + "build": "tsc" diff --git a/tests/factories/company.ts b/tests/factories/company.ts new file mode 100644 index 00000000..02c32a16 --- /dev/null +++ b/tests/factories/company.ts @@ -0,0 +1,8 @@ +import { defineFactory } from "accel-record-factory"; +import { Company } from "../models/index.js"; + +export const CompanyFactory = defineFactory(Company, { + // name: "MyString" +}); + +export { CompanyFactory as $Company }; diff --git a/tests/factories/employee.ts b/tests/factories/employee.ts new file mode 100644 index 00000000..9e6773c6 --- /dev/null +++ b/tests/factories/employee.ts @@ -0,0 +1,9 @@ +import { defineFactory } from "accel-record-factory"; +import { Employee } from "../models/index.js"; + +export const EmployeeFactory = defineFactory(Employee, { + // name: "MyString", + // companyId: 1 +}); + +export { EmployeeFactory as $Employee }; diff --git a/tests/factories/profile.ts b/tests/factories/profile.ts new file mode 100644 index 00000000..6a83ab5a --- /dev/null +++ b/tests/factories/profile.ts @@ -0,0 +1,8 @@ +import { defineFactory } from "accel-record-factory"; +import { Profile } from "../models/index.js"; + +export const ProfileFactory = defineFactory(Profile, { + // userId: 1 +}); + +export { ProfileFactory as $Profile }; diff --git a/tests/factories/user.ts b/tests/factories/user.ts index ef95e79c..0f7d6cf8 100644 --- a/tests/factories/user.ts +++ b/tests/factories/user.ts @@ -1,9 +1,27 @@ -import { User } from "../models/index.js"; import { defineFactory } from "accel-record-factory"; +import { User } from "../models/index.js"; +import { $setting } from "./setting.js"; -export const UserFactory = defineFactory(User, ({ seq }) => ({ - name: "hoge", - email: `user${seq}@email.com`, -})); +export const UserFactory = defineFactory( + User, + { + name: "hoge", + email: (seq) => `user${seq}@email.com`, + }, + { + traits: { + admin: { + name: "admin", + }, + john: () => ({ + name: "John", + age: 20, + }), + withSetting: { + setting: () => $setting.build(), + }, + }, + } +); export { UserFactory as $user }; diff --git a/tests/factories/userTeam.ts b/tests/factories/userTeam.ts new file mode 100644 index 00000000..509c0e54 --- /dev/null +++ b/tests/factories/userTeam.ts @@ -0,0 +1,10 @@ +import { defineFactory } from "accel-record-factory"; +import { UserTeam } from "../models/index.js"; + +export const UserTeamFactory = defineFactory(UserTeam, { + // userId: 1, + // teamId: 1, + // assignedBy: "MyString" +}); + +export { UserTeamFactory as $UserTeam }; diff --git a/tests/models/factory.test-d.ts b/tests/models/factory.test-d.ts new file mode 100644 index 00000000..9f337800 --- /dev/null +++ b/tests/models/factory.test-d.ts @@ -0,0 +1,15 @@ +import { $user } from "../factories/user"; + +test("traits", () => { + // @ts-expect-error + $user.build({}, "foo"); + + // @ts-expect-error + $user.create({}, "john", "bar"); + + // @ts-expect-error + $user.buildList(2, {}, "", "withSetting"); + + // @ts-expect-error + $user.createList(2, {}, undefined, "withSetting"); +}); diff --git a/tests/models/factory.test.ts b/tests/models/factory.test.ts new file mode 100644 index 00000000..54cf7edd --- /dev/null +++ b/tests/models/factory.test.ts @@ -0,0 +1,18 @@ +import { $user } from "../factories/user"; + +test("traits", () => { + const admin = $user.build({}, "admin"); + expect(admin.name).toBe("admin"); + + const john = $user.create({ age: 10 }, "john"); + expect(john.name).toBe("John"); + expect(john.age).toBe(10); + + const admins = $user.buildList(2, {}, "admin", "withSetting"); + expect(admins[0].name).toBe("admin"); + expect(admins[1].setting).not.toBeUndefined(); + + const johns = $user.createList(2, {}, "john", "withSetting"); + expect(johns[0].name).toBe("John"); + expect(johns[1].setting).not.toBeUndefined(); +}); diff --git a/tests/prisma_mysql/schema.prisma b/tests/prisma_mysql/schema.prisma index 9aae60fa..3edd2fe3 100644 --- a/tests/prisma_mysql/schema.prisma +++ b/tests/prisma_mysql/schema.prisma @@ -3,8 +3,9 @@ generator client { } generator custom_generator { - provider = "node node_modules/prisma-generator-accel-record" - output = "../models" + provider = "node node_modules/prisma-generator-accel-record" + output = "../models" + factoryPath = "../factories" } datasource db {