diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..c05b260 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,21 @@ +name: Publish package + +on: + release: + types: [created] + +jobs: + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm run build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml new file mode 100644 index 0000000..10ea393 --- /dev/null +++ b/.github/workflows/npm-test.yml @@ -0,0 +1,21 @@ +name: Run tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: "npm" + - run: npm ci + - run: npm run lint + - run: npm test diff --git a/README.md b/README.md new file mode 100644 index 0000000..730e6ff --- /dev/null +++ b/README.md @@ -0,0 +1,343 @@ +# prisma-generator-typescript-interfaces + +A [Prisma generator](https://www.prisma.io/docs/concepts/components/prisma-schema/generators) which creates zero-dependency Typescript interfaces from Prisma schema. + +## Motivation + +The Prisma client are generated types generally sufficient for most use cases, however there are some some scenarios where using them is not convenient or possible, due to the fact that they rely on both the `@prisma/client` package and on the client generated from your prisma schema. That is where this generator comes in to play. It generates a zero-dependency Typescript file containing type definitions for all your models. Zero-dependency in this case means that the file does not import any other packages, and can be used standalone in any Typescript app. By default the definitions are [type compatible](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) with the Prisma client types, however this can be customized via the [options](#options), see below for more info. + +As example of why this is useful, say have an API which uses Prisma and responds with models from your DB, and you want to create a DTO package with Typescript definitions for all the data your API returns. You wouldn't really want to include your entire generated Prisma client in that package, and you don't want to require users to install `@prisma/client` just to use your DTO. So instead, you can use this generator to create a zero-dependency typescript file containing definitions for all your models, and then use that in your DTO package. + +## Usage + +To use this generator, first install the package: + +``` +npm install --save-dev prisma-generator-typescript-interfaces +``` + +Next add the generator to your Prisma schema: + +```prisma +generator typescriptInterfaces { + provider = "prisma-generator-typescript-interfaces" +} +``` + +And finally generate your Prisma schema: + +``` +npx prisma generate +``` + +By default that will output the Typescript interface definitions to a file called `interfaces.ts` in your `prisma` folder, this can be changed by specifying the `output` option. As mentioned above, by default the generated types will be [type compatible](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) with the Prisma client types. If you instead wanted to generate types matching `JSON.stringify`-ed versions of your models, you will need to use some of the options to change the output behavior: + +```prisma +generator typescriptInterfaces { + provider = "prisma-generator-typescript-interfaces" + dateType = "string" + bigIntType = "string" + decimalType = "string" + bytesType = "BufferObject" +} +``` + +## Options + +| **Option** | **Type** | **Default** | **Description** | +| ----------------- | :----------------------------------------------------: | :---------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| output | `string` | `"interfaces.ts"` | The output location for the generated Typescript interfaces. | +| enumPrefix | `string` | `""` | Prefix to add to enum types. | +| enumSuffix | `string` | `""` | Suffix to add to enum types. | +| modelPrefix | `string` | `""` | Prefix to add to enum types. | +| modelSuffix | `string` | `""` | Suffix to add to model types. | +| typePrefix | `string` | `""` | Prefix to add to [type](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#defining-composite-types) types (mongodb only). | +| typeSuffix | `string` | `""` | Suffix to add to [type](https://www.prisma.io/docs/concepts/components/prisma-schema/data-model#defining-composite-types) types (mongodb only). | +| enumType | `"stringUnion" \| "enum"` | `"stringUnion"` | Controls how enums are generated. `"enum"` will create Typescript enums, `"stringUnion"` will create union types with all the enum values. | +| dateType | `"Date" \| "string"` | `"Date"` | The type to use for DateTime model fields. | +| bigIntType | `"bigint" \| "string" \| "number"` | `"bigint"` | The type to use for BigInt model fields. | +| decimalType | `"Decimal" \| "string"` | `"Decimal"` | The type to use for Decimal model fields. Note that the `Decimal` type here is just an interface with a `getValue` function. You will need to cast to an actual Decimal type if you want to use other methods | +| bytesType | `"Buffer" \| "BufferObject" \| "string" \| "number[]"` | `"Buffer"` | The type to use for Bytes model fields. `BufferObject` here is an object matching the default `JSON.stringify`-ed version of a Buffer. | +| optionalRelations | `boolean` | `true` | Controls whether model relation fields are optional or not. If `true`, all model relation fields will use `?:` in the field definition. | +| prettier | `boolean` | `false` | Formats the output using Prettier. Setting this to `true` requires that the `prettier` package is available. [Prettier settings files](https://prettier.io/docs/en/configuration.html) will be respected. | + +## Example + +Here is an example of a configuration which generates two separate outputs, `interfaces.ts` with types compatible with the Prisma client types, and a second `json-interfaces.ts` file with types matching the output of `JSON.stringify` when run on the models. Both files are output to the `src/dto` folder (which will be created if it doesn't exist). Both outputs are formatted using Prettier. The models in `json-interfaces.ts` get a `Json` suffix attached to them. + +#### Input + +
+prisma/schema.prisma + +```prisma +datasource db { + provider = "postgresql" + url = "postgresql://postgres:postgres@localhost:5432/example?schema=public" +} + +generator client { + provider = "prisma-client-js" +} + +generator typescriptInterfaces { + provider = "prisma-generator-typescript-interfaces" + output = "../src/dto/interfaces.ts" + prettier = true +} + +generator typescriptInterfacesJson { + provider = "prisma-generator-typescript-interfaces" + output = "../src/dto/json-interfaces.ts" + modelSuffix = "Json" + dateType = "string" + bigIntType = "string" + decimalType = "string" + bytesType = "BufferObject" + prettier = true +} + +enum Fruits { + Apple + Banana + Orange + Pear +} + +model RelationA { + id Int @id + Data Data[] +} + +model RelationB { + id Int @id + dataId Int @unique + data Data @relation(fields: [dataId], references: [id]) +} + +model RelationC { + id Int @id + dataId Int + data Data @relation(fields: [dataId], references: [id]) +} + +model Data { + id Int @id + stringField String + booleanField Boolean + intField Int + bigIntField BigInt + floatField Float + decimalField Decimal + dateField DateTime + jsonField Json + bytesField Bytes + enumField Fruits + relationId Int + relationField RelationA @relation(fields: [relationId], references: [id]) + + optionalStringField String? + optionalBooleanField Boolean? + optionalIntField Int? + optionalBigIntField BigInt? + optionalFloatField Float? + optionalDecimalField Decimal? + optionalDateField DateTime? + optionalJsonField Json? + optionalBytesField Bytes? + optionalEnumField Fruits? + optionalRelationField RelationB? + + stringArrayField String[] + booleanArrayField Boolean[] + intArrayField Int[] + bigIntArrayField BigInt[] + floatArrayField Float[] + decimalArrayField Decimal[] + dateArrayField DateTime[] + jsonArrayField Json[] + bytesArrayField Bytes[] + enumArrayField Fruits[] + relationArray RelationC[] +} +``` + +
+ +#### Output + +
+src/interfaces.ts + +```typescript +export type Fruits = "Apple" | "Banana" | "Orange" | "Pear"; + +export interface RelationA { + id: number; + Data?: Data[]; +} + +export interface RelationB { + id: number; + dataId: number; + data?: Data; +} + +export interface RelationC { + id: number; + dataId: number; + data?: Data; +} + +export interface Data { + id: number; + stringField: string; + booleanField: boolean; + intField: number; + bigIntField: bigint; + floatField: number; + decimalField: Decimal; + dateField: Date; + jsonField: JsonValue; + bytesField: Buffer; + enumField: Fruits; + relationId: number; + relationField?: RelationA; + optionalStringField: string | null; + optionalBooleanField: boolean | null; + optionalIntField: number | null; + optionalBigIntField: bigint | null; + optionalFloatField: number | null; + optionalDecimalField: Decimal | null; + optionalDateField: Date | null; + optionalJsonField: JsonValue | null; + optionalBytesField: Buffer | null; + optionalEnumField: Fruits | null; + optionalRelationField?: RelationB | null; + stringArrayField: string[]; + booleanArrayField: boolean[]; + intArrayField: number[]; + bigIntArrayField: bigint[]; + floatArrayField: number[]; + decimalArrayField: Decimal[]; + dateArrayField: Date[]; + jsonArrayField: JsonValue[]; + bytesArrayField: Buffer[]; + enumArrayField: Fruits[]; + relationArray?: RelationC[]; +} + +type Decimal = { valueOf(): string }; + +type JsonValue = + | string + | number + | boolean + | { [key in string]?: JsonValue } + | Array + | null; +``` + +
+ +
+src/json-interfaces.ts + +```typescript +export type Fruits = "Apple" | "Banana" | "Orange" | "Pear"; + +export interface RelationAJson { + id: number; + Data?: DataJson[]; +} + +export interface RelationBJson { + id: number; + dataId: number; + data?: DataJson; +} + +export interface RelationCJson { + id: number; + dataId: number; + data?: DataJson; +} + +export interface DataJson { + id: number; + stringField: string; + booleanField: boolean; + intField: number; + bigIntField: string; + floatField: number; + decimalField: string; + dateField: string; + jsonField: JsonValue; + bytesField: BufferObject; + enumField: Fruits; + relationId: number; + relationField?: RelationAJson; + optionalStringField: string | null; + optionalBooleanField: boolean | null; + optionalIntField: number | null; + optionalBigIntField: string | null; + optionalFloatField: number | null; + optionalDecimalField: string | null; + optionalDateField: string | null; + optionalJsonField: JsonValue | null; + optionalBytesField: BufferObject | null; + optionalEnumField: Fruits | null; + optionalRelationField?: RelationBJson | null; + stringArrayField: string[]; + booleanArrayField: boolean[]; + intArrayField: number[]; + bigIntArrayField: string[]; + floatArrayField: number[]; + decimalArrayField: string[]; + dateArrayField: string[]; + jsonArrayField: JsonValue[]; + bytesArrayField: BufferObject[]; + enumArrayField: Fruits[]; + relationArray?: RelationCJson[]; +} + +type JsonValue = + | string + | number + | boolean + | { [key in string]?: JsonValue } + | Array + | null; + +type BufferObject = { type: "Buffer"; data: number[] }; +``` + +
+ +## Issues + +Please report any issues to the [issues](https://github.com/mogzol/prisma-generator-typescript-interfaces/issues) page. I am actively using this package, so I'll try my best to address any issues that are reported. Alternatively, feel free to submit a PR. + +## Developing + +As this is a fairly simple generator, all the code is contained within the `generator.ts` file. You can build the generator by running `npm install` then `npm run build`. + +### Tests + +You can run tests with `npm run test`. Tests are run using a custom script, see `test.ts` for details. You can add new tests by placing a prisma schema and the expected output in a folder under the `tests` directory, you may want to look at the `tests/no-options` test as an example. + +You can run specific tests by passing them as arguments to the test command: + +``` +npm run test -- buffer-array-type mongo-types required-relations +``` + +When a test fails, you can see the generated output in a `__TEST_TMP__` folder inside the test's directory. Compare this with the expected output to see why it failed. + +By default the test runner will quit when it encounters it's first failure. If you want it to continue after failures, use the `-c` (or `--continue`) option: + +``` +npm run test -- -c +``` + +Please ensure all tests are passing and that the code is properly linted (`npm run lint`) before submitting a PR, thanks! diff --git a/generator.ts b/generator.ts index 503d362..08c5870 100644 --- a/generator.ts +++ b/generator.ts @@ -3,17 +3,17 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; interface Config { - enumType: "stringUnion" | "enum"; enumPrefix: string; enumSuffix: string; modelPrefix: string; modelSuffix: string; typePrefix: string; typeSuffix: string; - dateType: "string" | "Date"; - bigIntType: "string" | "bigint"; - decimalType: "string" | "Decimal"; - bytesType: "string" | "Buffer" | "BufferObject" | "number[]"; + enumType: "stringUnion" | "enum"; + dateType: "Date" | "string"; + bigIntType: "bigint" | "string" | "number"; + decimalType: "Decimal" | "string"; + bytesType: "Buffer" | "BufferObject" | "string" | "number[]"; optionalRelations: boolean; prettier: boolean; } @@ -45,16 +45,16 @@ function validateConfig(config: Config) { if (!["stringUnion", "enum"].includes(config.enumType)) { errors.push(`Invalid enumType: ${config.enumType}`); } - if (!["string", "Date"].includes(config.dateType)) { + if (!["Date", "string"].includes(config.dateType)) { errors.push(`Invalid dateType: ${config.dateType}`); } - if (!["string", "bigint"].includes(config.bigIntType)) { + if (!["bigint", "string", "number"].includes(config.bigIntType)) { errors.push(`Invalid bigIntType: ${config.bigIntType}`); } - if (!["string", "Decimal"].includes(config.decimalType)) { + if (!["Decimal", "string"].includes(config.decimalType)) { errors.push(`Invalid decimalType: ${config.decimalType}`); } - if (!["string", "Buffer", "BufferObject", "number[]"].includes(config.bytesType)) { + if (!["Buffer", "BufferObject", "string", "number[]"].includes(config.bytesType)) { errors.push(`Invalid bytesType: ${config.bytesType}`); } if (errors.length > 0) { @@ -150,13 +150,13 @@ generatorHandler({ async onGenerate(options) { const baseConfig = options.generator.config; const config: Config = { - enumType: "stringUnion", enumPrefix: "", enumSuffix: "", modelPrefix: "", modelSuffix: "", typePrefix: "", typeSuffix: "", + enumType: "stringUnion", dateType: "Date", bigIntType: "bigint", decimalType: "Decimal", diff --git a/test.ts b/test.ts index a6d1c47..ea7cde4 100644 --- a/test.ts +++ b/test.ts @@ -8,7 +8,7 @@ * npm run test -- custom-output no-options prettier * * If you want to run all tests even if some fail, pass the --continue or -c flag: - * npm run test -- -c + * npm run test -- -c */ import { exec } from "node:child_process"; @@ -19,8 +19,8 @@ import path from "node:path"; const TEMP_TEST_DIRNAME = "__TEST_TMP__"; const BASE_REPLACE_REGEX = /^\/\/ ?#INSERT base\.([a-z]+)\.prisma$/gm; -const RED = "\x1b[1m\x1b[41m\x1b[97m"; -const GREEN = "\x1b[1m\x1b[42m\x1b[97m"; +const RED = "\x1b[1;97;41m"; +const GREEN = "\x1b[1;102;30m"; const RESET = "\x1b[0m"; const execAsync = promisify(exec); diff --git a/tests/bigint-number-type/expected/interfaces.ts b/tests/bigint-number-type/expected/interfaces.ts new file mode 100644 index 0000000..29ef1e2 --- /dev/null +++ b/tests/bigint-number-type/expected/interfaces.ts @@ -0,0 +1,65 @@ +export type Gender = "Male" | "Female" | "Other"; + +export type DataTest = "Apple" | "Banana" | "Orange" | "Pear"; + +export interface Person { + id: number; + name: string; + age: number; + email: string | null; + gender: Gender; + addressId: number; + address?: Address; + friends?: Person[]; + friendsOf?: Person[]; + data?: Data | null; +} + +export interface Address { + id: number; + streetNumber: number; + streetName: string; + city: string; + isBilling: boolean; + people?: Person[]; +} + +export interface Data { + id: string; + stringField: string; + booleanField: boolean; + intField: number; + bigIntField: number; + floatField: number; + decimalField: Decimal; + dateField: Date; + jsonField: JsonValue; + bytesField: Buffer; + enumField: DataTest; + optionalStringField: string | null; + optionalBooleanField: boolean | null; + optionalIntField: number | null; + optionalBigIntField: number | null; + optionalFloatField: number | null; + optionalDecimalField: Decimal | null; + optionalDateField: Date | null; + optionalJsonField: JsonValue | null; + optionalBytesField: Buffer | null; + optionalEnumField: DataTest | null; + stringArrayField: string[]; + booleanArrayField: boolean[]; + intArrayField: number[]; + bigIntArrayField: number[]; + floatArrayField: number[]; + decimalArrayField: Decimal[]; + dateArrayField: Date[]; + jsonArrayField: JsonValue[]; + bytesArrayField: Buffer[]; + enumArrayField: DataTest[]; + personId: number; + person?: Person; +} + +type Decimal = { valueOf(): string }; + +type JsonValue = string | number | boolean | { [key in string]?: JsonValue } | Array | null; diff --git a/tests/bigint-number-type/schema.prisma b/tests/bigint-number-type/schema.prisma new file mode 100644 index 0000000..bddcca3 --- /dev/null +++ b/tests/bigint-number-type/schema.prisma @@ -0,0 +1,6 @@ +generator typescriptInterfaces { + provider = "node generator.js" + bigIntType = "number" +} + +// #INSERT base.postgres.prisma