diff --git a/README.md b/README.md index 03d4040..549807d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ pnpm i -D prismabox bun add -D prismabox ``` +If you use Prisma 7+, configure your datasource URL in `prisma.config.ts` (instead of `schema.prisma`) before running `prisma generate`. + then add ```prisma generator prismabox { @@ -18,12 +20,23 @@ generator prismabox { output = "./myCoolPrismaboxDirectory" // if you want, you can customize the imported variable name that is used for the schemes. Defaults to "Type" which is what the standard typebox package offers typeboxImportVariableName = "t" - // you also can specify the dependency from which the above import should happen. This is useful if a package re-exports the typebox package and you would like to use that - typeboxImportDependencyName = "elysia" + // you also can specify the dependency from which the above import should happen. Defaults to "typebox" + // this is useful if a package re-exports the typebox package and you would like to use that + typeboxImportDependencyName = "@sinclair/typebox" | "elysia" // by default the generated schemes do not allow additional properties. You can allow them by setting this to true additionalProperties = true // optionally enable the data model generation. See the data model section below for more info inputModel = true + // DateTime handling: + // false (default): use native date-compatible type + // true: emit string with format "date-time" + // "transformer": use generated transform helper (__transformDate__) + useJsonTypes = false + // recursion handling for where/whereUnique: + // true (default): generate recursive schema + // false: generate non-recursive schema + // in typebox mode this uses Type.Cyclic, in legacy mode Type.Recursive + allowRecursion = true } ``` to your `prisma.schema`. You can modify the settings to your liking, please see the respective comments for info on what the option does. @@ -67,6 +80,22 @@ enum Account { ``` > Please note that you cannot use multiple annotations in one line! Each needs to be in its own! + +## TypeBox Compatibility +By default prismabox targets `typebox` (1.x). If you need legacy output, set `typeboxImportDependencyName = "@sinclair/typebox"`. + +- `typebox` mode includes compatibility mappings for TypeBox 1.x: + - `Composite -> Evaluate(Intersect(...))` + - `Transform -> Codec` + - `Recursive -> Cyclic` +- `@sinclair/typebox` mode preserves legacy output behavior. + +Dependency note: +- Default mode (`typeboxImportDependencyName = "typebox"`): install `typebox`. +- Legacy mode (`typeboxImportDependencyName = "@sinclair/typebox"`): install `@sinclair/typebox`. +- `useJsonTypes` default is `false`. +- `allowRecursion` default is `true`. + ## Generated Schemes The generator will output schema objects based on the models: ```ts @@ -113,4 +142,3 @@ If enabled, the generator will additonally output more schemes for each model wh Prismabox wraps nullable fields in a custom `__nullable__` method which allows `null` in addition to `undefined`. From the relevant [issue comment](https://github.com/m1212e/prismabox/issues/33#issuecomment-2708755442): > prisma in some scenarios allows null OR undefined as types where optional only allows for undefined/is reflected as undefined in TS types - diff --git a/build.ts b/build.ts index 4cefafd..4b19761 100644 --- a/build.ts +++ b/build.ts @@ -10,7 +10,7 @@ const output = await build({ entryPoints: ["./src/cli.ts"], outdir: "./dist", platform: "node", - format: "cjs", + format: "esm", sourcemap: "external", minify: true, bundle: true, @@ -20,7 +20,7 @@ const output = await build({ ], }); -if (output.errors) { +if (output.errors.length > 0) { console.error(output.errors); } else { console.info("Built successfully!"); @@ -37,6 +37,7 @@ await writeFile( "./dist/package.json", JSON.stringify({ ...packagejson, + type: "module", version, bin: { prismabox: "cli.js" }, }), diff --git a/bun.lockb b/bun.lockb index c6f002f..341033e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a1d496e..0812230 100644 --- a/package.json +++ b/package.json @@ -25,19 +25,20 @@ "build": "bun run typecheck && bun build.ts", "lint": "biome check --write .", "format": "bun run lint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test:types": "bun test tests" }, "dependencies": { - "@prisma/generator-helper": "^6.13.0", - "@sinclair/typebox": "^0.34.3", - "prettier": "^3.6.2" + "@prisma/generator-helper": "^7.4.2", + "prettier": "^3.6.2", + "typebox": "^1.1.6" }, "devDependencies": { "@biomejs/biome": "^2.1.3", - "@prisma/client": "6.13.0", + "@prisma/client": "7.4.2", "@types/bun": "latest", "esbuild": "^0.25.8", - "prisma": "6.13.0", + "prisma": "7.4.2", "typescript": "^5.9.2" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c1299a..09a50c3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,5 @@ datasource db { provider = "postgresql" - url = env("DATABASE_URL") } generator client { diff --git a/src/config.ts b/src/config.ts index 85bf5f9..3b6f76c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ -import { type Static, Type } from "@sinclair/typebox"; -import { Value } from "@sinclair/typebox/value"; +import { type Static, Type } from "typebox"; +import { Value } from "typebox/value"; const configSchema = Type.Object( { @@ -14,7 +14,7 @@ const configSchema = Type.Object( /** * The name of the dependency to import the Type from typebox */ - typeboxImportDependencyName: Type.String({ default: "@sinclair/typebox" }), + typeboxImportDependencyName: Type.String({ default: "typebox" }), /** * Whether to allow additional properties in the generated schemes */ @@ -49,7 +49,7 @@ const configSchema = Type.Object( allowRecursion: Type.Boolean({ default: true }), /** * Additional fields to add to the generated schemes (must be valid strings in the context of usage) - * @example + * @example * ```prisma * generator prismabox { provider = "node ./dist/cli.js" @@ -93,12 +93,13 @@ let config: Static = {} as unknown as any; export function setConfig(input: unknown) { try { - Value.Clean(configSchema, input); - Value.Default(configSchema, input); - config = Value.Decode(configSchema, Value.Convert(configSchema, input)); + const converted = Value.Convert(configSchema, input); + Value.Default(configSchema, converted); + Value.Clean(configSchema, converted); + config = Value.Parse(configSchema, converted); Object.freeze(config); } catch (error) { - console.error(Value.Errors(configSchema, input).First); + console.error(Value.Errors(configSchema, input)[0]); throw error; } } diff --git a/src/generators/orderBy.ts b/src/generators/orderBy.ts index 1551d87..264e48d 100644 --- a/src/generators/orderBy.ts +++ b/src/generators/orderBy.ts @@ -43,6 +43,7 @@ export function stringifyOrderBy(data: DMMF.Model) { // `${getConfig().typeboxImportVariableName}.Literal('desc')`, // ] // )}})`; + return undefined; }) .filter((x) => x) as string[]; diff --git a/src/generators/primitiveField.ts b/src/generators/primitiveField.ts index ac8fbd3..55b98e1 100644 --- a/src/generators/primitiveField.ts +++ b/src/generators/primitiveField.ts @@ -1,4 +1,8 @@ import { getConfig } from "../config"; +import { + TYPEBOX_DATE_NAME, + TYPEBOX_UINT8_ARRAY_NAME, +} from "./wrappers/typeboxCompat"; const PrimitiveFields = [ "Int", @@ -56,6 +60,10 @@ export function stringifyPrimitiveType({ return `${config.typeboxImportVariableName}.String(${opts})`; } + if (getConfig().typeboxImportDependencyName === "typebox") { + return `${TYPEBOX_DATE_NAME}(${options})`; + } + return `${getConfig().typeboxImportVariableName}.Date(${options})`; } @@ -68,6 +76,10 @@ export function stringifyPrimitiveType({ } if (fieldType === "Bytes") { + if (getConfig().typeboxImportDependencyName === "typebox") { + return `${TYPEBOX_UINT8_ARRAY_NAME}(${options})`; + } + return `${getConfig().typeboxImportVariableName}.Uint8Array(${options})`; } diff --git a/src/generators/transformDate.ts b/src/generators/transformDate.ts index 239914d..e01cd52 100644 --- a/src/generators/transformDate.ts +++ b/src/generators/transformDate.ts @@ -1,16 +1,35 @@ import { getConfig } from "../config"; export function transformDateType() { - return `import { ${getConfig().typeboxImportVariableName} } from "${getConfig().typeboxImportDependencyName}"; - export const ${getConfig().transformDateName} = (options?: Parameters[0]) => ${ - getConfig().typeboxImportVariableName - }.Transform(${getConfig().typeboxImportVariableName}.String({ format: 'date-time', ...options })) + const { + typeboxImportDependencyName, + typeboxImportVariableName, + transformDateName, + } = getConfig(); + + if (typeboxImportDependencyName === "typebox") { + return `import ${ + typeboxImportVariableName + } from "${typeboxImportDependencyName}"; + export const ${transformDateName} = (options?: Parameters[0]) => ${ + typeboxImportVariableName + }.Codec(${typeboxImportVariableName}.String({ format: 'date-time', ...options })) + .Decode((value) => new Date(value)) + .Encode((value) => value.toISOString())\n`; + } + + return `import { ${typeboxImportVariableName} } from "${typeboxImportDependencyName}"; + export const ${transformDateName} = (options?: Parameters[0]) => ${ + typeboxImportVariableName + }.Transform(${typeboxImportVariableName}.String({ format: 'date-time', ...options })) .Decode((value) => new Date(value)) .Encode((value) => value.toISOString())\n`; } export function transformDateImportStatement() { - return `import { ${getConfig().transformDateName} } from "./${ - getConfig().transformDateName - }${getConfig().importFileExtension}"\n`; + const { importFileExtension, transformDateName } = getConfig(); + + return `import { ${transformDateName} } from "./${ + transformDateName + }${importFileExtension}"\n`; } diff --git a/src/generators/where.ts b/src/generators/where.ts index 98dc0e3..9e01954 100644 --- a/src/generators/where.ts +++ b/src/generators/where.ts @@ -19,6 +19,14 @@ import { makeUnion } from "./wrappers/union"; const selfReferenceName = "Self"; +function selfReferenceToken() { + if (getConfig().typeboxImportDependencyName === "typebox") { + return `${getConfig().typeboxImportVariableName}.Ref("${selfReferenceName}")`; + } + + return selfReferenceName; +} + export const processedWhere: ProcessedModel[] = []; export function processWhere(models: DMMF.Model[] | Readonly) { @@ -76,6 +84,21 @@ export function stringifyWhere(data: DMMF.Model) { .filter((x) => x) as string[]; if (getConfig().allowRecursion) { + if (getConfig().typeboxImportDependencyName === "typebox") { + return `${ + getConfig().typeboxImportVariableName + }.Cyclic({ ${selfReferenceName}: ${wrapWithPartial( + `${ + getConfig().typeboxImportVariableName + }.Object({${AND_OR_NOT()},${fields.join(",")}},${generateTypeboxOptions( + { + exludeAdditionalProperties: true, + input: annotations, + }, + )})`, + )} }, "${selfReferenceName}", { $id: "${data.name}" })`; + } + return wrapWithPartial( `${ getConfig().typeboxImportVariableName @@ -253,9 +276,7 @@ export function stringifyWhereUnique(data: DMMF.Model) { )}},${generateTypeboxOptions({ exludeAdditionalProperties: true, input: annotations })})`; if (getConfig().allowRecursion) { - return `${ - getConfig().typeboxImportVariableName - }.Recursive(${selfReferenceName} => ${makeIntersection([ + const recursiveType = makeIntersection([ wrapWithPartial(uniqueBaseObject, true), makeUnion( [...uniqueFields, ...uniqueCompositeFields].map( @@ -271,7 +292,17 @@ export function stringifyWhereUnique(data: DMMF.Model) { getConfig().typeboxImportVariableName }.Object({${allFields.join(",")}}, ${generateTypeboxOptions()})`, ), - ])}, { $id: "${data.name}"})`; + ]); + + if (getConfig().typeboxImportDependencyName === "typebox") { + return `${ + getConfig().typeboxImportVariableName + }.Cyclic({ ${selfReferenceName}: ${recursiveType} }, "${selfReferenceName}", { $id: "${data.name}" })`; + } + + return `${ + getConfig().typeboxImportVariableName + }.Recursive(${selfReferenceName} => ${recursiveType}, { $id: "${data.name}"})`; } return makeIntersection([ @@ -290,11 +321,12 @@ export function stringifyWhereUnique(data: DMMF.Model) { } function AND_OR_NOT() { + const token = selfReferenceToken(); return `AND: ${ getConfig().typeboxImportVariableName - }.Union([${selfReferenceName}, ${wrapWithArray(selfReferenceName)}]), + }.Union([${token}, ${wrapWithArray(token)}]), NOT: ${ getConfig().typeboxImportVariableName - }.Union([${selfReferenceName}, ${wrapWithArray(selfReferenceName)}]), - OR: ${wrapWithArray(selfReferenceName)}`; + }.Union([${token}, ${wrapWithArray(token)}]), + OR: ${wrapWithArray(token)}`; } diff --git a/src/generators/wrappers/composite.ts b/src/generators/wrappers/composite.ts index 0254bc6..a98e707 100644 --- a/src/generators/wrappers/composite.ts +++ b/src/generators/wrappers/composite.ts @@ -2,7 +2,19 @@ import { generateTypeboxOptions } from "../../annotations/options"; import { getConfig } from "../../config"; export function makeComposite(inputModels: string[]) { - return `${ - getConfig().typeboxImportVariableName - }.Composite([${inputModels.map((i) => `${getConfig().exportedTypePrefix}${i}`).join(",")}], ${generateTypeboxOptions()})\n`; + const { + typeboxImportDependencyName, + typeboxImportVariableName, + exportedTypePrefix, + } = getConfig(); + + if (typeboxImportDependencyName === "typebox") { + return `${typeboxImportVariableName}.Evaluate(${typeboxImportVariableName}.Intersect([${inputModels + .map((i) => `${exportedTypePrefix}${i}`) + .join(",")}], ${generateTypeboxOptions()}))\n`; + } + + return `${getConfig().typeboxImportVariableName}.Composite([${inputModels + .map((i) => `${getConfig().exportedTypePrefix}${i}`) + .join(",")}], ${generateTypeboxOptions()})\n`; } diff --git a/src/generators/wrappers/nullable.ts b/src/generators/wrappers/nullable.ts index 568b4a7..aef530d 100644 --- a/src/generators/wrappers/nullable.ts +++ b/src/generators/wrappers/nullable.ts @@ -1,20 +1,39 @@ import { getConfig } from "../../config"; export function nullableType() { + const { + typeboxImportDependencyName, + typeboxImportVariableName, + nullableName, + } = getConfig(); + + if (typeboxImportDependencyName === "typebox") { + return `import ${ + typeboxImportVariableName + }, { type TSchema } from "${typeboxImportDependencyName}" +export const ${nullableName} = (schema: T) => ${ + typeboxImportVariableName + }.Union([${typeboxImportVariableName}.Null(), schema])\n`; + } + return `import { ${ - getConfig().typeboxImportVariableName - }, type TSchema } from "${getConfig().typeboxImportDependencyName}" -export const ${getConfig().nullableName} = (schema: T) => ${ - getConfig().typeboxImportVariableName - }.Union([${getConfig().typeboxImportVariableName}.Null(), schema])\n`; + typeboxImportVariableName + }, type TSchema } from "${typeboxImportDependencyName}" +export const ${nullableName} = (schema: T) => ${ + typeboxImportVariableName + }.Union([${typeboxImportVariableName}.Null(), schema])\n`; } export function nullableImport() { - return `import { ${getConfig().nullableName} } from "./${ - getConfig().nullableName - }${getConfig().importFileExtension}"\n`; + const { nullableName, importFileExtension } = getConfig(); + + return `import { ${nullableName} } from "./${ + nullableName + }${importFileExtension}"\n`; } export function wrapWithNullable(input: string) { - return `${getConfig().nullableName}(${input})`; + const { nullableName } = getConfig(); + + return `${nullableName}(${input})`; } diff --git a/src/generators/wrappers/typeboxCompat.ts b/src/generators/wrappers/typeboxCompat.ts new file mode 100644 index 0000000..bdd42f9 --- /dev/null +++ b/src/generators/wrappers/typeboxCompat.ts @@ -0,0 +1,54 @@ +import { getConfig } from "../../config"; + +export const TYPEBOX_DATE_NAME = "__typeboxDate__"; +export const TYPEBOX_UINT8_ARRAY_NAME = "__typeboxUint8Array__"; + +export function typeboxCompatImportStatement() { + const { typeboxImportDependencyName, importFileExtension } = getConfig(); + + if (typeboxImportDependencyName !== "typebox") { + return ""; + } + + return `import { ${TYPEBOX_DATE_NAME} } from "./${TYPEBOX_DATE_NAME}${importFileExtension}"\nimport { ${TYPEBOX_UINT8_ARRAY_NAME} } from "./${TYPEBOX_UINT8_ARRAY_NAME}${importFileExtension}"\n`; +} + +export function typeboxDateType() { + const { typeboxImportDependencyName, typeboxImportVariableName } = + getConfig(); + if (typeboxImportDependencyName !== "typebox") { + return ""; + } + + return `import ${ + typeboxImportVariableName + } from "${typeboxImportDependencyName}" +export function ${TYPEBOX_DATE_NAME}(options?: Record) { + return ${typeboxImportVariableName}.Refine( + ${typeboxImportVariableName}.Unsafe({ ...(options ?? {}) }), + (value) => value instanceof globalThis.Date, + "must be Date", + ) +}\n`; +} + +export function typeboxUint8ArrayType() { + const { typeboxImportDependencyName, typeboxImportVariableName } = + getConfig(); + if (typeboxImportDependencyName !== "typebox") { + return ""; + } + + return `import ${ + typeboxImportVariableName + } from "${typeboxImportDependencyName}" +export function ${TYPEBOX_UINT8_ARRAY_NAME}(options?: Record) { + return ${typeboxImportVariableName}.Refine( + ${typeboxImportVariableName}.Unsafe({ + ...(options ?? {}), + }), + (value) => value instanceof globalThis.Uint8Array, + "must be Uint8Array", + ) +}\n`; +} diff --git a/src/index.ts b/src/index.ts index bb3dfb3..a6a40d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { access, mkdir, rm } from "node:fs/promises"; -import { generatorHandler } from "@prisma/generator-helper"; +import generatorHelper from "@prisma/generator-helper"; import { getConfig, setConfig } from "./config"; import { processEnums } from "./generators/enum"; import { processInclude } from "./generators/include"; @@ -16,6 +16,8 @@ import { processSelect } from "./generators/select"; import { processWhere, processWhereUnique } from "./generators/where"; import { write } from "./writer"; +const { generatorHandler } = generatorHelper; + generatorHandler({ onManifest() { return { diff --git a/src/model.ts b/src/model.ts index 8258755..b774058 100644 --- a/src/model.ts +++ b/src/model.ts @@ -18,6 +18,13 @@ import { import { processedWhere, processedWhereUnique } from "./generators/where"; import { makeComposite } from "./generators/wrappers/composite"; import { nullableImport, nullableType } from "./generators/wrappers/nullable"; +import { + TYPEBOX_DATE_NAME, + TYPEBOX_UINT8_ARRAY_NAME, + typeboxCompatImportStatement, + typeboxDateType, + typeboxUint8ArrayType, +} from "./generators/wrappers/typeboxCompat"; export type ProcessedModel = { name: string; @@ -27,16 +34,28 @@ export type ProcessedModel = { function convertModelToStandalone( input: Pick, ) { - return `export const ${getConfig().exportedTypePrefix}${input.name} = ${input.stringRepresentation}\n`; + const { exportedTypePrefix } = getConfig(); + return `export const ${exportedTypePrefix}${input.name} = ${input.stringRepresentation}\n`; } function typepoxImportStatement() { - return `import { ${getConfig().typeboxImportVariableName} } from "${ - getConfig().typeboxImportDependencyName + const { typeboxImportDependencyName, typeboxImportVariableName } = + getConfig(); + if (typeboxImportDependencyName === "typebox") { + return `import ${typeboxImportVariableName} from "${ + typeboxImportDependencyName + }"\n`; + } + + return `import { ${typeboxImportVariableName} } from "${ + typeboxImportDependencyName }"\n`; } export function mapAllModelsForWrite() { + const { nullableName, transformDateName, typeboxImportDependencyName } = + getConfig(); + const modelsPerName = new Map< ProcessedModel["name"], ProcessedModel["stringRepresentation"] @@ -132,12 +151,16 @@ export function mapAllModelsForWrite() { for (const [key, value] of modelsPerName) { modelsPerName.set( key, - `${typepoxImportStatement()}\n${transformDateImportStatement()}\n${nullableImport()}\n${value}`, + `${typepoxImportStatement()}\n${transformDateImportStatement()}\n${nullableImport()}\n${typeboxCompatImportStatement()}\n${value}`, ); } - modelsPerName.set(getConfig().nullableName, nullableType()); - modelsPerName.set(getConfig().transformDateName, transformDateType()); + modelsPerName.set(nullableName, nullableType()); + modelsPerName.set(transformDateName, transformDateType()); + if (typeboxImportDependencyName === "typebox") { + modelsPerName.set(TYPEBOX_DATE_NAME, typeboxDateType()); + modelsPerName.set(TYPEBOX_UINT8_ARRAY_NAME, typeboxUint8ArrayType()); + } return modelsPerName; } diff --git a/tests/config.test.ts b/tests/config.test.ts new file mode 100644 index 0000000..fa8eae5 --- /dev/null +++ b/tests/config.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test"; +import { getConfig, setConfig } from "../src/config"; + +describe("config defaults", () => { + test("uses typebox as default import dependency", () => { + setConfig({}); + expect(getConfig().typeboxImportDependencyName).toBe("typebox"); + expect(getConfig().typeboxImportDependencyName === "typebox").toBe(true); + }); + + test("detects legacy mode when dependency is overridden", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + }); + expect(getConfig().typeboxImportDependencyName === "typebox").toBe(false); + }); +}); diff --git a/tests/generation-legacy.test.ts b/tests/generation-legacy.test.ts new file mode 100644 index 0000000..edc0b32 --- /dev/null +++ b/tests/generation-legacy.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import { setConfig } from "../src/config"; +import { stringifyPrimitiveType } from "../src/generators/primitiveField"; +import { transformDateType } from "../src/generators/transformDate"; +import { makeComposite } from "../src/generators/wrappers/composite"; +import { nullableType } from "../src/generators/wrappers/nullable"; +import { + TYPEBOX_DATE_NAME, + TYPEBOX_UINT8_ARRAY_NAME, +} from "../src/generators/wrappers/typeboxCompat"; +import { mapAllModelsForWrite } from "../src/model"; + +describe("legacy generation mode", () => { + test("keeps composite for legacy typebox dependency", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + typeboxImportVariableName: "Type", + additionalProperties: false, + }); + + const output = makeComposite(["PostPlain", "PostRelations"]); + expect(output).toContain(".Composite([PostPlain,PostRelations]"); + expect(output).not.toContain(".Evaluate("); + }); + + test("keeps transform helper for legacy dependency", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + typeboxImportVariableName: "Type", + }); + + const output = transformDateType(); + expect(output).toContain('import { Type } from "@sinclair/typebox"'); + expect(output).toContain(".Transform("); + }); + + test("keeps nullable helper import format for legacy dependency", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + typeboxImportVariableName: "Type", + }); + + const output = nullableType(); + expect(output).toContain( + 'import { Type, type TSchema } from "@sinclair/typebox"', + ); + }); + + test("keeps Date and Uint8Array primitive outputs for legacy dependency", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + typeboxImportVariableName: "Type", + useJsonTypes: false, + }); + + expect( + stringifyPrimitiveType({ fieldType: "DateTime", options: "{}" }), + ).toContain("Type.Date({})"); + expect( + stringifyPrimitiveType({ fieldType: "Bytes", options: "{}" }), + ).toContain("Type.Uint8Array({})"); + }); + + test("does not write compat helper files for legacy dependency", () => { + setConfig({ + typeboxImportDependencyName: "@sinclair/typebox", + typeboxImportVariableName: "Type", + }); + + const mappings = mapAllModelsForWrite(); + expect(mappings.has(TYPEBOX_DATE_NAME)).toBe(false); + expect(mappings.has(TYPEBOX_UINT8_ARRAY_NAME)).toBe(false); + }); +}); diff --git a/tests/generation-typebox.test.ts b/tests/generation-typebox.test.ts new file mode 100644 index 0000000..ba2ee77 --- /dev/null +++ b/tests/generation-typebox.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "bun:test"; +import { setConfig } from "../src/config"; +import { stringifyPrimitiveType } from "../src/generators/primitiveField"; +import { transformDateType } from "../src/generators/transformDate"; +import { stringifyWhere } from "../src/generators/where"; +import { makeComposite } from "../src/generators/wrappers/composite"; +import { nullableType } from "../src/generators/wrappers/nullable"; +import { + TYPEBOX_DATE_NAME, + TYPEBOX_UINT8_ARRAY_NAME, +} from "../src/generators/wrappers/typeboxCompat"; +import { mapAllModelsForWrite } from "../src/model"; + +describe("typebox generation mode", () => { + test("uses evaluate + intersect instead of composite", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + additionalProperties: false, + }); + + const output = makeComposite(["PostPlain", "PostRelations"]); + expect(output).toContain( + "Type.Evaluate(Type.Intersect([PostPlain,PostRelations], {additionalProperties: false}))", + ); + expect(output).not.toContain(".Composite("); + }); + + test("uses codec and default import in transform date helper", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + }); + + const output = transformDateType(); + expect(output).toContain('import Type from "typebox"'); + expect(output).toContain(".Codec("); + expect(output).not.toContain(".Transform("); + }); + + test("uses nullable helper import format for typebox", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + }); + + const output = nullableType(); + expect(output).toContain('import Type, { type TSchema } from "typebox"'); + }); + + test("uses typebox date and uint8array compat helper in primitive field output", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + useJsonTypes: false, + }); + + expect( + stringifyPrimitiveType({ fieldType: "DateTime", options: "{}" }), + ).toContain(`${TYPEBOX_DATE_NAME}({})`); + expect( + stringifyPrimitiveType({ fieldType: "Bytes", options: "{}" }), + ).toContain(`${TYPEBOX_UINT8_ARRAY_NAME}({})`); + }); + + test("writes compat helper files into output mapping", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + }); + + const mappings = mapAllModelsForWrite(); + const dateCompat = mappings.get(TYPEBOX_DATE_NAME) ?? ""; + const uint8Compat = mappings.get(TYPEBOX_UINT8_ARRAY_NAME) ?? ""; + expect(dateCompat).toContain("Type.Refine("); + expect(dateCompat).toContain("Type.Unsafe"); + expect(uint8Compat).toContain("Type.Refine("); + expect(uint8Compat).toContain("Type.Unsafe"); + }); + + test("uses cyclic in where generation when recursion is enabled", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + allowRecursion: true, + }); + + const output = + stringifyWhere({ + name: "User", + documentation: null, + fields: [ + { + name: "id", + type: "String", + documentation: null, + isList: false, + isRequired: true, + }, + ], + } as never) ?? ""; + + expect(output).toContain("Type.Cyclic("); + expect(output).toContain('Type.Ref("Self")'); + expect(output).toContain('{ $id: "User" }'); + expect(output).not.toContain("Type.Recursive("); + }); +}); diff --git a/tests/transform-modes.test.ts b/tests/transform-modes.test.ts new file mode 100644 index 0000000..4f93094 --- /dev/null +++ b/tests/transform-modes.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test"; +import { setConfig } from "../src/config"; +import { stringifyPrimitiveType } from "../src/generators/primitiveField"; + +describe("datetime generation modes", () => { + test("useJsonTypes = true emits string with date-time format", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + useJsonTypes: true, + }); + + const output = stringifyPrimitiveType({ + fieldType: "DateTime", + options: "{description: 'a date'}", + }); + expect(output).toContain("Type.String("); + expect(output).toContain("format: 'date-time'"); + }); + + test("useJsonTypes = transformer emits transform helper call", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + useJsonTypes: "transformer", + }); + + const output = stringifyPrimitiveType({ + fieldType: "DateTime", + options: "{}", + }); + expect(output).toContain("__transformDate__({})"); + }); + + test("useJsonTypes = false emits date compat helper in typebox mode", () => { + setConfig({ + typeboxImportDependencyName: "typebox", + typeboxImportVariableName: "Type", + useJsonTypes: false, + }); + + const output = stringifyPrimitiveType({ + fieldType: "DateTime", + options: "{}", + }); + expect(output).toContain("__typeboxDate__({})"); + }); +});