From aaf516c5992c6aff9647ffa78ebaec1a8a978122 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:18:28 -0300 Subject: [PATCH 01/12] code --- src/generator.ts | 42 +++++-- src/helpers/generateDatabaseType.test.ts | 3 + src/helpers/generateDatabaseType.ts | 3 +- src/helpers/generateEnumType.test.ts | 4 +- src/helpers/generateEnumType.ts | 22 +++- src/helpers/generateFieldType.test.ts | 6 + src/helpers/generateFile.test.ts | 2 +- src/helpers/generateFiles.ts | 111 +++++++++++++---- src/helpers/generateModel.test.ts | 8 +- src/helpers/generateModel.ts | 20 ++- .../generateTypedAliasDeclaration.test.ts | 15 --- src/helpers/generateTypedAliasDeclaration.ts | 13 -- src/helpers/generatedEnumType.test.ts | 8 +- src/helpers/multiSchemaHelpers.test.ts | 11 +- src/helpers/multiSchemaHelpers.ts | 114 ++++++++++++------ src/utils/camelCase.ts | 4 + src/utils/normalizeCase.test.ts | 2 + src/utils/validateConfig.ts | 11 ++ 18 files changed, 287 insertions(+), 112 deletions(-) delete mode 100644 src/helpers/generateTypedAliasDeclaration.test.ts delete mode 100644 src/helpers/generateTypedAliasDeclaration.ts diff --git a/src/generator.ts b/src/generator.ts index 186eca0..1c90d96 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -11,8 +11,11 @@ import { sorted } from "~/utils/sorted"; import { validateConfig } from "~/utils/validateConfig"; import { writeFileSafely } from "~/utils/writeFileSafely"; -import { generateEnumType } from "./helpers/generateEnumType"; -import { convertToMultiSchemaModels } from "./helpers/multiSchemaHelpers"; +import { type EnumType, generateEnumType } from "./helpers/generateEnumType"; +import { + convertToMultiSchemaModels, + parseMultiSchemaMap, +} from "./helpers/multiSchemaHelpers"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version } = require("../package.json"); @@ -33,9 +36,9 @@ generatorHandler({ }); // Generate enum types - const enums = options.dmmf.datamodel.enums.flatMap(({ name, values }) => { - return generateEnumType(name, values); - }); + let enums = options.dmmf.datamodel.enums + .map(({ name, values }) => generateEnumType(name, values)) + .filter((e): e is EnumType => !!e); // Generate DMMF models for implicit many to many tables // @@ -45,31 +48,46 @@ generatorHandler({ options.dmmf.datamodel.models ); + const multiSchemaMap = + config.groupBySchema || + options.generator.previewFeatures?.includes("multiSchema") + ? parseMultiSchemaMap(options.datamodel) + : undefined; + // Generate model types let models = sorted( [...options.dmmf.datamodel.models, ...implicitManyToManyModels], (a, b) => a.name.localeCompare(b.name) - ).map((m) => generateModel(m, config)); + ).map((m) => + generateModel(m, config, config.groupBySchema, multiSchemaMap) + ); // Extend model table names with schema names if using multi-schemas if (options.generator.previewFeatures?.includes("multiSchema")) { - models = convertToMultiSchemaModels(models, options.datamodel); + models = convertToMultiSchemaModels( + models, + config.groupBySchema, + multiSchemaMap + ); + enums = convertToMultiSchemaModels( + enums, + config.groupBySchema, + multiSchemaMap + ); } // Generate the database type that ties it all together - const databaseType = generateDatabaseType( - models.map((m) => ({ tableName: m.tableName, typeName: m.typeName })), - config - ); + const databaseType = generateDatabaseType(models, config); // Parse it all into a string. Either 1 or 2 files depending on user config const files = generateFiles({ databaseType, - modelDefinitions: models.map((m) => m.definition), enumNames: options.dmmf.datamodel.enums.map((e) => e.name), + models, enums, enumsOutfile: config.enumFileName, typesOutfile: config.fileName, + groupBySchema: config.groupBySchema, }); // And write it to a file! diff --git a/src/helpers/generateDatabaseType.test.ts b/src/helpers/generateDatabaseType.test.ts index 81e1ae1..33bfb99 100644 --- a/src/helpers/generateDatabaseType.test.ts +++ b/src/helpers/generateDatabaseType.test.ts @@ -16,6 +16,7 @@ test("it works for plain vanilla type names", () => { enumFileName: "", camelCase: false, readOnlyIds: false, + groupBySchema: false, } ); const result = stringifyTsNode(node); @@ -40,6 +41,7 @@ test("it respects camelCase option names", () => { enumFileName: "", camelCase: true, readOnlyIds: false, + groupBySchema: false, } ); const result = stringifyTsNode(node); @@ -64,6 +66,7 @@ test("it works for table names with spaces and weird symbols", () => { enumFileName: "", camelCase: false, readOnlyIds: false, + groupBySchema: false, } ); const result = stringifyTsNode(node); diff --git a/src/helpers/generateDatabaseType.ts b/src/helpers/generateDatabaseType.ts index a7e6800..f3addba 100644 --- a/src/helpers/generateDatabaseType.ts +++ b/src/helpers/generateDatabaseType.ts @@ -4,9 +4,10 @@ import isValidTSIdentifier from "~/utils/isValidTSIdentifier"; import { normalizeCase } from "~/utils/normalizeCase"; import { sorted } from "~/utils/sorted"; import type { Config } from "~/utils/validateConfig"; +import type { ModelType } from "~/helpers/generateModel"; export const generateDatabaseType = ( - models: { tableName: string; typeName: string }[], + models: Omit[], config: Config ) => { const sortedModels = sorted(models, (a, b) => diff --git a/src/helpers/generateEnumType.test.ts b/src/helpers/generateEnumType.test.ts index a275d9b..4bcb743 100644 --- a/src/helpers/generateEnumType.test.ts +++ b/src/helpers/generateEnumType.test.ts @@ -4,10 +4,10 @@ import { expect, test } from "vitest"; import { generateEnumType } from "./generateEnumType"; test("it generates the enum type", () => { - const [objectDeclaration, typeDeclaration] = generateEnumType("Name", [ + const { objectDeclaration, typeDeclaration } = generateEnumType("Name", [ { name: "FOO", dbName: "FOO" }, { name: "BAR", dbName: "BAR" }, - ]); + ])!; const printer = createPrinter(); diff --git a/src/helpers/generateEnumType.ts b/src/helpers/generateEnumType.ts index 8367749..99e6cee 100644 --- a/src/helpers/generateEnumType.ts +++ b/src/helpers/generateEnumType.ts @@ -6,10 +6,22 @@ import isValidTSIdentifier from "~/utils/isValidTSIdentifier"; import { generateStringLiteralUnion } from "./generateStringLiteralUnion"; import { generateTypedReferenceNode } from "./generateTypedReferenceNode"; -export const generateEnumType = (name: string, values: DMMF.EnumValue[]) => { +export type EnumType = { + objectDeclaration: ts.VariableStatement; + typeDeclaration: ts.TypeAliasDeclaration; + schema?: string; + typeName: string; +}; + +export const generateEnumType = ( + name: string, + values: DMMF.EnumValue[] +): EnumType | undefined => { const type = generateStringLiteralUnion(values.map((v) => v.name)); - if (!type) return []; + if (!type) { + return undefined; + } const objectDeclaration = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -47,5 +59,9 @@ export const generateEnumType = (name: string, values: DMMF.EnumValue[]) => { const typeDeclaration = generateTypedReferenceNode(name); - return [objectDeclaration, typeDeclaration]; + return { + typeName: name, + objectDeclaration, + typeDeclaration, + }; }; diff --git a/src/helpers/generateFieldType.test.ts b/src/helpers/generateFieldType.test.ts index d1f6d90..bb19ccc 100644 --- a/src/helpers/generateFieldType.test.ts +++ b/src/helpers/generateFieldType.test.ts @@ -23,6 +23,7 @@ test("it respects overrides when generating field types", () => { enumFileName: "types.ts", camelCase: false, readOnlyIds: false, + groupBySchema: false, }; const sourceTypes = [ @@ -51,6 +52,7 @@ test("it respects overrides when generating field types", () => { stringTypeOverride: "cheese", camelCase: false, readOnlyIds: false, + groupBySchema: false, }); expect(node).toEqual("cheese"); @@ -63,6 +65,7 @@ test("it respects differences between database engines", () => { enumFileName: "types.ts", camelCase: false, readOnlyIds: false, + groupBySchema: false, }); const mysqlBooleanType = generateFieldType("Boolean", { @@ -71,6 +74,7 @@ test("it respects differences between database engines", () => { enumFileName: "types.ts", camelCase: false, readOnlyIds: false, + groupBySchema: false, }); const sqliteBooleanType = generateFieldType("Boolean", { @@ -79,6 +83,7 @@ test("it respects differences between database engines", () => { enumFileName: "types.ts", camelCase: false, readOnlyIds: false, + groupBySchema: false, }); expect(postgresBooleanType).toEqual("boolean"); @@ -94,6 +99,7 @@ test("it throws an error when unsupported type is encountered", () => { enumFileName: "types.ts", camelCase: false, readOnlyIds: false, + groupBySchema: false, }) ).toThrowError(new Error("Unsupported type Json for database sqlite")); }); diff --git a/src/helpers/generateFile.test.ts b/src/helpers/generateFile.test.ts index 24189c2..a19846b 100644 --- a/src/helpers/generateFile.test.ts +++ b/src/helpers/generateFile.test.ts @@ -13,7 +13,7 @@ test("generates a file!", () => { withEnumImport: { importPath: "./enums", names: ["Foo", "Bar"] }, withLeader: false, }); - console.log(resultwithEnumImport); + expect(resultwithEnumImport).toContain( 'import type { Foo, Bar } from "./enums";' ); diff --git a/src/helpers/generateFiles.ts b/src/helpers/generateFiles.ts index 22885fb..309ab14 100644 --- a/src/helpers/generateFiles.ts +++ b/src/helpers/generateFiles.ts @@ -1,29 +1,42 @@ import path from "path"; -import type { TypeAliasDeclaration, VariableStatement } from "typescript"; +import type { TypeAliasDeclaration } from "typescript"; +import ts from "typescript"; import { generateFile } from "~/helpers/generateFile"; +import type { EnumType } from "./generateEnumType"; +import type { ModelType } from "./generateModel"; + type File = { filepath: string; content: ReturnType }; export function generateFiles(opts: { typesOutfile: string; - enums: (VariableStatement | TypeAliasDeclaration)[]; + enums: EnumType[]; + models: ModelType[]; enumNames: string[]; enumsOutfile: string; databaseType: TypeAliasDeclaration; - modelDefinitions: TypeAliasDeclaration[]; + groupBySchema: boolean; }) { // Don't generate a separate file for enums if there are no enums if (opts.enumsOutfile === opts.typesOutfile || opts.enums.length === 0) { + let statements: Iterable; + + if (!opts.groupBySchema) { + statements = [ + ...opts.enums.flatMap((e) => [e.objectDeclaration, e.typeDeclaration]), + ...opts.models.map((m) => m.definition), + ]; + } else { + statements = groupModelsAndEnum(opts.enums, opts.models); + } + const typesFileWithEnums: File = { filepath: opts.typesOutfile, - content: generateFile( - [...opts.enums, ...opts.modelDefinitions, opts.databaseType], - { - withEnumImport: false, - withLeader: true, - } - ), + content: generateFile([...statements, opts.databaseType], { + withEnumImport: false, + withLeader: true, + }), }; return [typesFileWithEnums]; @@ -31,24 +44,80 @@ export function generateFiles(opts: { const typesFileWithoutEnums: File = { filepath: opts.typesOutfile, - content: generateFile([...opts.modelDefinitions, opts.databaseType], { - withEnumImport: { - importPath: `./${path.parse(opts.enumsOutfile).name}`, - names: opts.enumNames, - }, - withLeader: true, - }), + content: generateFile( + [...opts.models.map((m) => m.definition), opts.databaseType], + { + withEnumImport: { + importPath: `./${path.parse(opts.enumsOutfile).name}`, + names: opts.enumNames, + }, + withLeader: true, + } + ), }; if (opts.enums.length === 0) return [typesFileWithoutEnums]; const enumFile: File = { filepath: opts.enumsOutfile, - content: generateFile(opts.enums, { - withEnumImport: false, - withLeader: false, - }), + content: generateFile( + opts.enums.flatMap((e) => [e.objectDeclaration, e.typeDeclaration]), + { + withEnumImport: false, + withLeader: false, + } + ), }; return [typesFileWithoutEnums, enumFile]; } + +export function* groupModelsAndEnum( + enums: EnumType[], + models: ModelType[] +): Generator { + const groupsMap = new Map(); + + for (const enumType of enums) { + if (!enumType.schema) { + yield enumType.objectDeclaration; + yield enumType.typeDeclaration; + continue; + } + + const group = groupsMap.get(enumType.schema); + + if (!group) { + groupsMap.set(enumType.schema, [ + enumType.objectDeclaration, + enumType.typeDeclaration, + ]); + } else { + group.push(enumType.objectDeclaration, enumType.typeDeclaration); + } + } + + for (const model of models) { + if (!model.schema) { + yield model.definition; + continue; + } + + const group = groupsMap.get(model.schema); + + if (!group) { + groupsMap.set(model.schema, [model.definition]); + } else { + group.push(model.definition); + } + } + + for (const [schema, group] of groupsMap) { + yield ts.factory.createModuleDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(schema), + ts.factory.createModuleBlock(group), + ts.NodeFlags.Namespace + ); + } +} diff --git a/src/helpers/generateModel.test.ts b/src/helpers/generateModel.test.ts index 5850cca..d2631fc 100644 --- a/src/helpers/generateModel.test.ts +++ b/src/helpers/generateModel.test.ts @@ -94,7 +94,9 @@ test("it generates a model!", () => { enumFileName: "", camelCase: false, readOnlyIds: false, - } + groupBySchema: false, + }, + false ); expect(model.tableName).toEqual("User"); @@ -152,7 +154,9 @@ test("it respects camelCase option", () => { enumFileName: "", camelCase: true, readOnlyIds: false, - } + groupBySchema: false, + }, + false ); expect(model.tableName).toEqual("User"); diff --git a/src/helpers/generateModel.ts b/src/helpers/generateModel.ts index 7ba2aa4..5371180 100644 --- a/src/helpers/generateModel.ts +++ b/src/helpers/generateModel.ts @@ -14,7 +14,19 @@ import type { Config } from "~/utils/validateConfig"; */ const defaultTypesImplementedInJS = ["cuid", "uuid"]; -export const generateModel = (model: DMMF.Model, config: Config) => { +export type ModelType = { + typeName: string; + tableName: string; + definition: ts.TypeAliasDeclaration; + schema?: string; +}; + +export const generateModel = ( + model: DMMF.Model, + config: Config, + groupBySchema: boolean, + multiSchemaMap?: Map +): ModelType => { const properties = model.fields.flatMap((field) => { const isGenerated = field.hasDefaultValue && @@ -32,12 +44,16 @@ export const generateModel = (model: DMMF.Model, config: Config) => { const dbName = typeof field.dbName === "string" ? field.dbName : null; + const schemaPrefix = groupBySchema && multiSchemaMap?.get(field.type); + if (field.kind === "enum") { return generateField({ isId: field.isId, name: normalizeCase(dbName || field.name, config), type: ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier(field.type), + ts.factory.createIdentifier( + schemaPrefix ? `${schemaPrefix}.${field.type}` : field.type + ), undefined ), nullable: !field.isRequired, diff --git a/src/helpers/generateTypedAliasDeclaration.test.ts b/src/helpers/generateTypedAliasDeclaration.test.ts deleted file mode 100644 index 53224de..0000000 --- a/src/helpers/generateTypedAliasDeclaration.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import ts from "typescript"; -import { expect, test } from "vitest"; - -import { generateTypedAliasDeclaration } from "~/helpers/generateTypedAliasDeclaration"; -import { stringifyTsNode } from "~/utils/testUtils"; - -test("it creates and exports a type alias :D", () => { - const node = generateTypedAliasDeclaration( - "typeOne", - ts.factory.createLiteralTypeNode(ts.factory.createNull()) - ); - const result = stringifyTsNode(node); - - expect(result).toEqual(`export type typeOne = null;`); -}); diff --git a/src/helpers/generateTypedAliasDeclaration.ts b/src/helpers/generateTypedAliasDeclaration.ts deleted file mode 100644 index 2c584ac..0000000 --- a/src/helpers/generateTypedAliasDeclaration.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ts from "typescript"; - -export const generateTypedAliasDeclaration = ( - name: string, - type: ts.TypeNode -) => { - return ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier(name), - undefined, - type - ); -}; diff --git a/src/helpers/generatedEnumType.test.ts b/src/helpers/generatedEnumType.test.ts index 76335bd..5b972d3 100644 --- a/src/helpers/generatedEnumType.test.ts +++ b/src/helpers/generatedEnumType.test.ts @@ -4,10 +4,10 @@ import { expect, test } from "vitest"; import { generateEnumType } from "./generateEnumType"; test("it generates the enum type", () => { - const [objectDeclaration, typeDeclaration] = generateEnumType("Name", [ + const { objectDeclaration, typeDeclaration } = generateEnumType("Name", [ { name: "FOO", dbName: "FOO" }, { name: "BAR", dbName: "BAR" }, - ]); + ])!; const printer = createPrinter(); @@ -25,10 +25,10 @@ export type Name = (typeof Name)[keyof typeof Name];\n`); }); test("it generates the enum type when using Prisma's @map()", () => { - const [objectDeclaration, typeDeclaration] = generateEnumType("Name", [ + const { objectDeclaration, typeDeclaration } = generateEnumType("Name", [ { name: "FOO", dbName: "foo" }, { name: "BAR", dbName: "bar" }, - ]); + ])!; const printer = createPrinter(); diff --git a/src/helpers/multiSchemaHelpers.test.ts b/src/helpers/multiSchemaHelpers.test.ts index 484279e..12b8d37 100644 --- a/src/helpers/multiSchemaHelpers.test.ts +++ b/src/helpers/multiSchemaHelpers.test.ts @@ -1,6 +1,9 @@ import { expect, test } from "vitest"; -import { convertToMultiSchemaModels } from "./multiSchemaHelpers"; +import { + convertToMultiSchemaModels, + parseMultiSchemaMap, +} from "./multiSchemaHelpers"; const testDataModel = `generator kysely { provider = "node ./dist/bin.js" @@ -35,7 +38,11 @@ test("returns a list of models with schemas appended to the table name", () => { { typeName: "Eagle", tableName: "eagles" }, ]; - const result = convertToMultiSchemaModels(initialModels, testDataModel); + const result = convertToMultiSchemaModels( + initialModels, + false, + parseMultiSchemaMap(testDataModel) + ); expect(result).toEqual([ { typeName: "Elephant", tableName: "mammals.elephants" }, diff --git a/src/helpers/multiSchemaHelpers.ts b/src/helpers/multiSchemaHelpers.ts index 864842b..cea7449 100644 --- a/src/helpers/multiSchemaHelpers.ts +++ b/src/helpers/multiSchemaHelpers.ts @@ -1,59 +1,105 @@ -import { - type BlockAttribute, - type Model, - getSchema, -} from "@mrleebo/prisma-ast"; +import { type BlockAttribute, getSchema } from "@mrleebo/prisma-ast"; +import ts from "typescript"; + +import { capitalize } from "~/utils/camelCase"; type ModelLike = { typeName: string; - tableName: string; + tableName?: string; + schema?: string; }; /** * Appends schema names to the table names of models. * + * @param models list of model names + * @param multiSchemaMap map of model names to schema names + * @returns list of models with schema names appended to the table names ("schema.table") + */ +export const convertToMultiSchemaModels = ( + models: T[], + groupBySchema: boolean, + multiSchemaMap?: Map +): T[] => { + return models.map((model) => { + const schemaName = multiSchemaMap?.get(model.typeName); + + if (!schemaName) { + return model; + } + + return { + ...model, + typeName: + groupBySchema && schemaName + ? `${schemaName}.${model.typeName}` + : model.typeName, + tableName: model.tableName + ? `${schemaName}.${model.tableName}` + : undefined, + schema: schemaName, + }; + }); +}; + +// https://github.com/microsoft/TypeScript/blob/a53c37d59aa0c20f566dec7e5498f05afe45dc6b/src/compiler/scanner.ts#L985 +const isIdentifierText: ( + name: string, + languageVersion: ts.ScriptTarget | undefined, + identifierVariant?: ts.LanguageVariant +) => boolean = + // @ts-expect-error - Internal TS API + ts.isIdentifierText; + +/** + * Parses a data model string and returns a map of model names to schema names. + * * Prisma supports multi-schema databases, but currently doens't include the schema name in the DMMT output. * As a workaround, this function parses the schema separately and matches the schema to the model name. * * TODO: Remove this when @prisma/generator-helper exposes schema names in the models by default. * See thread: https://github.com/prisma/prisma/issues/19987 * - * @param models list of model names * @param dataModelStr the full datamodel string (schema.prisma contents) - * @returns list of models with schema names appended to the table names ("schema.table") + * @returns a map of model names to schema names */ -export const convertToMultiSchemaModels = ( - models: T[], - dataModelStr: string -): T[] => { +export function parseMultiSchemaMap(dataModelStr: string) { const parsedSchema = getSchema(dataModelStr); + const multiSchemaMap = new Map(); - const multiSchemaMap = new Map( - parsedSchema.list - .filter((block): block is Model => block.type === "model") - .map((model) => { - const schemaProperty = model.properties.find( - (prop): prop is BlockAttribute => - prop.type === "attribute" && prop.name === "schema" - ); + for (const block of parsedSchema.list) { + if (block.type !== "model" && block.type !== "enum") { + continue; + } - const schemaName = schemaProperty?.args?.[0].value; + const properties = + block.type === "model" ? block.properties : block.enumerators; - if (typeof schemaName !== "string") { - return [model.name, ""]; - } + const schemaProperty = properties.find( + (prop): prop is BlockAttribute => + prop.type === "attribute" && prop.name === "schema" + ); - return [model.name, schemaName.replace(/"/g, "")]; - }) - ); + const schemaName = schemaProperty?.args?.[0].value; - return models.map((model) => { - const schemaName = multiSchemaMap.get(model.typeName); + if (typeof schemaName !== "string") { + multiSchemaMap.set(block.name, ""); + } else { + const schema: string = JSON.parse(schemaName).toString(); - if (!schemaName) { - return model; + if ( + isIdentifierText && + !isIdentifierText(schema, ts.ScriptTarget.Latest) + ) { + throw new Error( + `Cannot generate identifier for schema "${schema}" in model "${block.name}" because it is not a valid Identifier, please disable \`groupBySchema\` or rename it.` + ); + } + + // capitalize schemas because usually they are in lowercase + multiSchemaMap.set(block.name, capitalize(schema)); } + } - return { ...model, tableName: `${schemaName}.${model.tableName}` }; - }); -}; + return multiSchemaMap; +} diff --git a/src/utils/camelCase.ts b/src/utils/camelCase.ts index 1847703..7704da4 100644 --- a/src/utils/camelCase.ts +++ b/src/utils/camelCase.ts @@ -133,3 +133,7 @@ function memoize(func: StringMapper): StringMapper { return mapped; }; } + +export function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/src/utils/normalizeCase.test.ts b/src/utils/normalizeCase.test.ts index 73b48f3..f81fcae 100644 --- a/src/utils/normalizeCase.test.ts +++ b/src/utils/normalizeCase.test.ts @@ -10,6 +10,7 @@ test("converts names to camel case when config value is set", () => { fileName: "", enumFileName: "", readOnlyIds: false, + groupBySchema: false, }); expect(newName).toEqual("userId"); @@ -23,6 +24,7 @@ test("doesn't convert names to camel case when config value isn't set", () => { fileName: "", enumFileName: "", readOnlyIds: false, + groupBySchema: false, }); expect(newName).toEqual("user_id"); diff --git a/src/utils/validateConfig.ts b/src/utils/validateConfig.ts index 47bed45..e5a2234 100644 --- a/src/utils/validateConfig.ts +++ b/src/utils/validateConfig.ts @@ -40,12 +40,22 @@ export const configValidator = z // Use GeneratedAlways for IDs instead of Generated readOnlyIds: booleanStringLiteral.default(false), + + // Group models in a namespace by their schema. Cannot be defined if enumFileName is defined. + groupBySchema: booleanStringLiteral.default(false), }) .strict() .transform((config) => { if (!config.enumFileName) { config.enumFileName = config.fileName; } + + if (config.groupBySchema && config.enumFileName !== config.fileName) { + throw new Error( + "groupBySchema cannot be defined if enumFileName is defined" + ); + } + return config as Omit & Required>; }); @@ -64,6 +74,7 @@ export const validateConfig = (config: unknown) => { Object.values(parsed.error.flatten().formErrors).forEach((value) => { logger.error(`${value}`); }); + process.exit(1); } return parsed.data; From 969db0f6c197d77e256b879ab1afe8edc1580529 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:32:21 -0300 Subject: [PATCH 02/12] tests --- src/__test__/e2e.test.ts | 78 +++++++++++++++++++++++++++++++ src/helpers/generateFiles.ts | 3 +- src/helpers/generateModel.ts | 3 +- src/helpers/multiSchemaHelpers.ts | 10 ++-- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/__test__/e2e.test.ts b/src/__test__/e2e.test.ts index a7c4e12..0fd12fc 100644 --- a/src/__test__/e2e.test.ts +++ b/src/__test__/e2e.test.ts @@ -280,3 +280,81 @@ test( }, { timeout: 20000 } ); + +test( + "End to end test - multi-schema and groupBySchema support", + async () => { + // Initialize prisma: + await exec("yarn prisma init --datasource-provider postgresql"); + + // Set up a schema + await fs.writeFile( + "./prisma/schema.prisma", + ` +generator kysely { + provider = "node ./dist/bin.js" + previewFeatures = ["multiSchema"] + groupBySchema = true +} + +datasource db { + provider = "postgresql" + schemas = ["mammals", "birds", "world"] + url = env("TEST_DATABASE_URL") +} + +model Elephant { + id Int @id + name String + ability Ability @default(WALK) + color Color + + @@map("elephants") + @@schema("mammals") +} + +model Eagle { + id Int @id + name String + ability Ability @default(FLY) + + @@map("eagles") + @@schema("birds") +} + +enum Ability { + FLY + WALK + + @@schema("world") +} + +enum Color { + GRAY + PINK + + @@schema("mammals") +} + ` + ); + + await exec("yarn prisma generate"); + + // Shouldn't have an empty import statement + const typeFile = await fs.readFile("./prisma/generated/types.ts", { + encoding: "utf-8", + }); + + expect(typeFile).toContain(`export namespace Birds { + export type Eagle = {`); + + expect(typeFile).toContain(`export namespace Mammals { + export const Color = {`); + + expect(typeFile).toContain(`export type DB = { + "birds.eagles": Birds.Eagle; + "mammals.elephants": Mammals.Elephant; +};`); + }, + { timeout: 20000 } +); diff --git a/src/helpers/generateFiles.ts b/src/helpers/generateFiles.ts index 309ab14..560faf7 100644 --- a/src/helpers/generateFiles.ts +++ b/src/helpers/generateFiles.ts @@ -6,6 +6,7 @@ import { generateFile } from "~/helpers/generateFile"; import type { EnumType } from "./generateEnumType"; import type { ModelType } from "./generateModel"; +import { capitalize } from "~/utils/camelCase"; type File = { filepath: string; content: ReturnType }; @@ -115,7 +116,7 @@ export function* groupModelsAndEnum( for (const [schema, group] of groupsMap) { yield ts.factory.createModuleDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier(schema), + ts.factory.createIdentifier(capitalize(schema)), ts.factory.createModuleBlock(group), ts.NodeFlags.Namespace ); diff --git a/src/helpers/generateModel.ts b/src/helpers/generateModel.ts index 5371180..e739deb 100644 --- a/src/helpers/generateModel.ts +++ b/src/helpers/generateModel.ts @@ -4,6 +4,7 @@ import ts from "typescript"; import { generateField } from "~/helpers/generateField"; import { generateFieldType } from "~/helpers/generateFieldType"; import { generateTypeOverrideFromDocumentation } from "~/helpers/generateTypeOverrideFromDocumentation"; +import { capitalize } from "~/utils/camelCase"; import { normalizeCase } from "~/utils/normalizeCase"; import type { Config } from "~/utils/validateConfig"; @@ -52,7 +53,7 @@ export const generateModel = ( name: normalizeCase(dbName || field.name, config), type: ts.factory.createTypeReferenceNode( ts.factory.createIdentifier( - schemaPrefix ? `${schemaPrefix}.${field.type}` : field.type + schemaPrefix ? `${capitalize(schemaPrefix)}.${field.type}` : field.type ), undefined ), diff --git a/src/helpers/multiSchemaHelpers.ts b/src/helpers/multiSchemaHelpers.ts index cea7449..d2c0e9e 100644 --- a/src/helpers/multiSchemaHelpers.ts +++ b/src/helpers/multiSchemaHelpers.ts @@ -16,7 +16,7 @@ type ModelLike = { * @param multiSchemaMap map of model names to schema names * @returns list of models with schema names appended to the table names ("schema.table") */ -export const convertToMultiSchemaModels = ( +export const convertToMultiSchemaModels = ( models: T[], groupBySchema: boolean, multiSchemaMap?: Map @@ -32,12 +32,12 @@ export const convertToMultiSchemaModels = ( ...model, typeName: groupBySchema && schemaName - ? `${schemaName}.${model.typeName}` + ? `${capitalize(schemaName)}.${model.typeName}` : model.typeName, tableName: model.tableName ? `${schemaName}.${model.tableName}` : undefined, - schema: schemaName, + schema: groupBySchema ? schemaName : undefined, }; }); }; @@ -96,8 +96,8 @@ export function parseMultiSchemaMap(dataModelStr: string) { ); } - // capitalize schemas because usually they are in lowercase - multiSchemaMap.set(block.name, capitalize(schema)); + // don't capitalize it here because the DB key is case-sensitive + multiSchemaMap.set(block.name, schema); } } From ceac60af184c921ac823d10427a084fca748c294 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:35:33 -0300 Subject: [PATCH 03/12] docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 574545b..6795c37 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ without losing the safety of the TypeScript type system? output = "../src/db" fileName = "types.ts" // Optionally generate runtime enums to a separate file - enumFileName = "enums.ts" + enumFileName = "enums.ts" } ``` @@ -83,6 +83,7 @@ hope it's just as useful for you! 😎 | `camelCase` | Enable support for Kysely's camelCase plugin | | `readOnlyIds` | Use Kysely's `GeneratedAlways` for `@id` fields with default values, preventing insert and update. | | `[typename]TypeOverride` | Allows you to override the resulting TypeScript type for any Prisma type. Useful when targeting a different environment than Node (e.g. WinterCG compatible runtimes that use UInt8Arrays instead of Buffers for binary types etc.) Check out the [config validator](https://github.com/valtyr/prisma-kysely/blob/main/src/utils/validateConfig.ts) for a complete list of options. | +| `groupBySchema` | When using `multiSchema` preview features, group all models and enums for a schema into their own namespace. (Ex: `model Dog { @@schema("animals") }` will be available under `Animals.Dog`) | ### Per-field type overrides From b5670e226815e9cd5729db64d1d379a2b493a983 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:40:40 -0300 Subject: [PATCH 04/12] formatting --- src/helpers/generateDatabaseType.ts | 2 +- src/helpers/generateFiles.ts | 2 +- src/helpers/generateModel.ts | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/helpers/generateDatabaseType.ts b/src/helpers/generateDatabaseType.ts index f3addba..d02ab52 100644 --- a/src/helpers/generateDatabaseType.ts +++ b/src/helpers/generateDatabaseType.ts @@ -1,10 +1,10 @@ import ts from "typescript"; +import type { ModelType } from "~/helpers/generateModel"; import isValidTSIdentifier from "~/utils/isValidTSIdentifier"; import { normalizeCase } from "~/utils/normalizeCase"; import { sorted } from "~/utils/sorted"; import type { Config } from "~/utils/validateConfig"; -import type { ModelType } from "~/helpers/generateModel"; export const generateDatabaseType = ( models: Omit[], diff --git a/src/helpers/generateFiles.ts b/src/helpers/generateFiles.ts index 560faf7..78ac553 100644 --- a/src/helpers/generateFiles.ts +++ b/src/helpers/generateFiles.ts @@ -3,10 +3,10 @@ import type { TypeAliasDeclaration } from "typescript"; import ts from "typescript"; import { generateFile } from "~/helpers/generateFile"; +import { capitalize } from "~/utils/camelCase"; import type { EnumType } from "./generateEnumType"; import type { ModelType } from "./generateModel"; -import { capitalize } from "~/utils/camelCase"; type File = { filepath: string; content: ReturnType }; diff --git a/src/helpers/generateModel.ts b/src/helpers/generateModel.ts index e739deb..0c4046b 100644 --- a/src/helpers/generateModel.ts +++ b/src/helpers/generateModel.ts @@ -53,7 +53,9 @@ export const generateModel = ( name: normalizeCase(dbName || field.name, config), type: ts.factory.createTypeReferenceNode( ts.factory.createIdentifier( - schemaPrefix ? `${capitalize(schemaPrefix)}.${field.type}` : field.type + schemaPrefix + ? `${capitalize(schemaPrefix)}.${field.type}` + : field.type ), undefined ), From b2c61867beab6c29d2c44a9c8b10eb47eb579efe Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:44:07 -0300 Subject: [PATCH 05/12] another expect statement --- prisma-old/generated/a.ts | 5 ++++ prisma-old/generated/types copy.ts | 19 +++++++++++++ prisma-old/generated/types.ts | 32 ++++++++++++++++++++++ prisma-old/schema.prisma | 43 ++++++++++++++++++++++++++++++ src/__test__/e2e.test.ts | 3 +++ 5 files changed, 102 insertions(+) create mode 100644 prisma-old/generated/a.ts create mode 100644 prisma-old/generated/types copy.ts create mode 100644 prisma-old/generated/types.ts create mode 100644 prisma-old/schema.prisma diff --git a/prisma-old/generated/a.ts b/prisma-old/generated/a.ts new file mode 100644 index 0000000..d1d9fec --- /dev/null +++ b/prisma-old/generated/a.ts @@ -0,0 +1,5 @@ +import {type a,b} from './types' + +type asd= a.TestUser + +const c = b.C \ No newline at end of file diff --git a/prisma-old/generated/types copy.ts b/prisma-old/generated/types copy.ts new file mode 100644 index 0000000..60ac2e5 --- /dev/null +++ b/prisma-old/generated/types copy.ts @@ -0,0 +1,19 @@ +import type { ColumnType } from "kysely"; + +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; +export type Timestamp = ColumnType; + +export type Eagle = { + id: number; + name: string; +}; +export type Elephant = { + id: number; + name: string; +}; +export type DB = { + "birds.eagles": Eagle; + "mammals.elephants": Elephant; +}; diff --git a/prisma-old/generated/types.ts b/prisma-old/generated/types.ts new file mode 100644 index 0000000..a5f9e43 --- /dev/null +++ b/prisma-old/generated/types.ts @@ -0,0 +1,32 @@ +import type { ColumnType } from "kysely"; + +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; +export type Timestamp = ColumnType; + +export const Ability = { + FLY: "FLY", + WALK: "WALK", +} as const; +export type Ability = (typeof Ability)[keyof typeof Ability]; +export const Color = { + GRAY: "GRAY", + PINK: "PINK", +} as const; +export type Color = (typeof Color)[keyof typeof Color]; +export type Eagle = { + id: number; + name: string; + ability: Generated; +}; +export type Elephant = { + id: number; + name: string; + ability: Generated; + color: Color; +}; +export type DB = { + "birds.eagles": Eagle; + "mammals.elephants": Elephant; +}; diff --git a/prisma-old/schema.prisma b/prisma-old/schema.prisma new file mode 100644 index 0000000..f13464b --- /dev/null +++ b/prisma-old/schema.prisma @@ -0,0 +1,43 @@ +generator kysely { + provider = "node ./dist/bin.js" + previewFeatures = ["multiSchema"] +} + +datasource db { + provider = "postgresql" + schemas = ["mammals", "birds", "world"] + url = env("TEST_DATABASE_URL") +} + +model Elephant { + id Int @id + name String + ability Ability @default(WALK) + color Color + + @@map("elephants") + @@schema("mammals") +} + +model Eagle { + id Int @id + name String + ability Ability @default(FLY) + + @@map("eagles") + @@schema("birds") +} + +enum Ability { + FLY + WALK + + @@schema("world") +} + +enum Color { + GRAY + PINK + + @@schema("mammals") +} diff --git a/src/__test__/e2e.test.ts b/src/__test__/e2e.test.ts index 0fd12fc..6d3477a 100644 --- a/src/__test__/e2e.test.ts +++ b/src/__test__/e2e.test.ts @@ -351,6 +351,9 @@ enum Color { expect(typeFile).toContain(`export namespace Mammals { export const Color = {`); + // correctly references the color enum + expect(typeFile).toContain("color: Mammals.Color;"); + expect(typeFile).toContain(`export type DB = { "birds.eagles": Birds.Eagle; "mammals.elephants": Mammals.Elephant; From 926b87948bdace161b8e978a605e0a827b4bc629 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:45:45 -0300 Subject: [PATCH 06/12] improve types --- prisma-old/generated/a.ts | 5 ---- prisma-old/generated/types copy.ts | 19 ------------- prisma-old/generated/types.ts | 32 --------------------- prisma-old/schema.prisma | 43 ----------------------------- src/helpers/generateDatabaseType.ts | 2 +- 5 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 prisma-old/generated/a.ts delete mode 100644 prisma-old/generated/types copy.ts delete mode 100644 prisma-old/generated/types.ts delete mode 100644 prisma-old/schema.prisma diff --git a/prisma-old/generated/a.ts b/prisma-old/generated/a.ts deleted file mode 100644 index d1d9fec..0000000 --- a/prisma-old/generated/a.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {type a,b} from './types' - -type asd= a.TestUser - -const c = b.C \ No newline at end of file diff --git a/prisma-old/generated/types copy.ts b/prisma-old/generated/types copy.ts deleted file mode 100644 index 60ac2e5..0000000 --- a/prisma-old/generated/types copy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { ColumnType } from "kysely"; - -export type Generated = T extends ColumnType - ? ColumnType - : ColumnType; -export type Timestamp = ColumnType; - -export type Eagle = { - id: number; - name: string; -}; -export type Elephant = { - id: number; - name: string; -}; -export type DB = { - "birds.eagles": Eagle; - "mammals.elephants": Elephant; -}; diff --git a/prisma-old/generated/types.ts b/prisma-old/generated/types.ts deleted file mode 100644 index a5f9e43..0000000 --- a/prisma-old/generated/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ColumnType } from "kysely"; - -export type Generated = T extends ColumnType - ? ColumnType - : ColumnType; -export type Timestamp = ColumnType; - -export const Ability = { - FLY: "FLY", - WALK: "WALK", -} as const; -export type Ability = (typeof Ability)[keyof typeof Ability]; -export const Color = { - GRAY: "GRAY", - PINK: "PINK", -} as const; -export type Color = (typeof Color)[keyof typeof Color]; -export type Eagle = { - id: number; - name: string; - ability: Generated; -}; -export type Elephant = { - id: number; - name: string; - ability: Generated; - color: Color; -}; -export type DB = { - "birds.eagles": Eagle; - "mammals.elephants": Elephant; -}; diff --git a/prisma-old/schema.prisma b/prisma-old/schema.prisma deleted file mode 100644 index f13464b..0000000 --- a/prisma-old/schema.prisma +++ /dev/null @@ -1,43 +0,0 @@ -generator kysely { - provider = "node ./dist/bin.js" - previewFeatures = ["multiSchema"] -} - -datasource db { - provider = "postgresql" - schemas = ["mammals", "birds", "world"] - url = env("TEST_DATABASE_URL") -} - -model Elephant { - id Int @id - name String - ability Ability @default(WALK) - color Color - - @@map("elephants") - @@schema("mammals") -} - -model Eagle { - id Int @id - name String - ability Ability @default(FLY) - - @@map("eagles") - @@schema("birds") -} - -enum Ability { - FLY - WALK - - @@schema("world") -} - -enum Color { - GRAY - PINK - - @@schema("mammals") -} diff --git a/src/helpers/generateDatabaseType.ts b/src/helpers/generateDatabaseType.ts index d02ab52..96cf313 100644 --- a/src/helpers/generateDatabaseType.ts +++ b/src/helpers/generateDatabaseType.ts @@ -7,7 +7,7 @@ import { sorted } from "~/utils/sorted"; import type { Config } from "~/utils/validateConfig"; export const generateDatabaseType = ( - models: Omit[], + models: Pick[], config: Config ) => { const sortedModels = sorted(models, (a, b) => From 987ceefd2ecaf082d3adad330119ead4d10cf08a Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:47:23 -0300 Subject: [PATCH 07/12] revert type --- src/helpers/generateDatabaseType.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helpers/generateDatabaseType.ts b/src/helpers/generateDatabaseType.ts index 96cf313..a7e6800 100644 --- a/src/helpers/generateDatabaseType.ts +++ b/src/helpers/generateDatabaseType.ts @@ -1,13 +1,12 @@ import ts from "typescript"; -import type { ModelType } from "~/helpers/generateModel"; import isValidTSIdentifier from "~/utils/isValidTSIdentifier"; import { normalizeCase } from "~/utils/normalizeCase"; import { sorted } from "~/utils/sorted"; import type { Config } from "~/utils/validateConfig"; export const generateDatabaseType = ( - models: Pick[], + models: { tableName: string; typeName: string }[], config: Config ) => { const sortedModels = sorted(models, (a, b) => From acbc62e30a871f9a8802b451882155c827a87961 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:49:12 -0300 Subject: [PATCH 08/12] better error message --- src/utils/validateConfig.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/validateConfig.ts b/src/utils/validateConfig.ts index e5a2234..26938a7 100644 --- a/src/utils/validateConfig.ts +++ b/src/utils/validateConfig.ts @@ -51,9 +51,9 @@ export const configValidator = z } if (config.groupBySchema && config.enumFileName !== config.fileName) { - throw new Error( - "groupBySchema cannot be defined if enumFileName is defined" - ); + // would require https://www.typescriptlang.org/docs/handbook/namespaces.html#splitting-across-files + // which is considered a bad practice + throw new Error("groupBySchema is not compatible with enumFileName"); } return config as Omit & From b9ed9e3b93c66a9962cc7a547524828b08df29bb Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:50:47 -0300 Subject: [PATCH 09/12] improve ts type compatibility --- prisma-old/dev.db | Bin 0 -> 12288 bytes prisma-old/generated/types.ts | 17 +++++++++++++++++ prisma-old/schema.prisma | 17 +++++++++++++++++ src/helpers/multiSchemaHelpers.ts | 7 ++----- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 prisma-old/dev.db create mode 100644 prisma-old/generated/types.ts create mode 100644 prisma-old/schema.prisma diff --git a/prisma-old/dev.db b/prisma-old/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..d504ea3a56217a22a71b393db479a404d068d74b GIT binary patch literal 12288 zcmeI#O-sWt7zgmAippRwx19GHegW$YGVJE88Msr4G#iDzXq&;V`sMsCo_FwQ zGS?k;*~QcNKaiwPpY$caobvn>Wk%C*mQR$SYt~?#vl}AD7`N@fwlZ9PSSEIK`L5vX zV)$LH|1ht%WVL1eYu5v)LjVF0fB*y_009U<00Izz00jO^;G@Tno4(IK3!`4gTI#~| z3!Rsv*NR0b15)_oT13ImcjUY<{lHx^D_tCa~6j3j3cfxo;4`OisH)pCQdTZ9Y zqDHpR?n-el;>{b@wOpBOIts{sKiaO%-%@3C8k&Ia?BB_DM{K54&Mv*9X3OU+o2GhE zypQdfK2dWsTjfc)gygEca_9OJ=UH`K!*vJ = T extends ColumnType + ? ColumnType + : ColumnType; +export type Timestamp = ColumnType; + +export type TestUser = { + id: string; + name: string; + age: number; + rating: number; + updatedAt: string; +}; +export type DB = { + TestUser: TestUser; +}; diff --git a/prisma-old/schema.prisma b/prisma-old/schema.prisma new file mode 100644 index 0000000..50818da --- /dev/null +++ b/prisma-old/schema.prisma @@ -0,0 +1,17 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" + } + + generator kysely { + provider = "node ./dist/bin.js" + enumFileName = "enums.ts" + } + + model TestUser { + id String @id + name String + age Int + rating Float + updatedAt DateTime + } \ No newline at end of file diff --git a/src/helpers/multiSchemaHelpers.ts b/src/helpers/multiSchemaHelpers.ts index d2c0e9e..1855704 100644 --- a/src/helpers/multiSchemaHelpers.ts +++ b/src/helpers/multiSchemaHelpers.ts @@ -45,7 +45,7 @@ export const convertToMultiSchemaModels = ( // https://github.com/microsoft/TypeScript/blob/a53c37d59aa0c20f566dec7e5498f05afe45dc6b/src/compiler/scanner.ts#L985 const isIdentifierText: ( name: string, - languageVersion: ts.ScriptTarget | undefined, + languageVersion?: ts.ScriptTarget | undefined, identifierVariant?: ts.LanguageVariant ) => boolean = // @ts-expect-error - Internal TS API @@ -87,10 +87,7 @@ export function parseMultiSchemaMap(dataModelStr: string) { } else { const schema: string = JSON.parse(schemaName).toString(); - if ( - isIdentifierText && - !isIdentifierText(schema, ts.ScriptTarget.Latest) - ) { + if (isIdentifierText && !isIdentifierText(schema)) { throw new Error( `Cannot generate identifier for schema "${schema}" in model "${block.name}" because it is not a valid Identifier, please disable \`groupBySchema\` or rename it.` ); From cd3426031214f0106e412fec6500b90947620252 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:51:44 -0300 Subject: [PATCH 10/12] remove prisma-old --- prisma-old/dev.db | Bin 12288 -> 0 bytes prisma-old/generated/types.ts | 17 ----------------- prisma-old/schema.prisma | 17 ----------------- 3 files changed, 34 deletions(-) delete mode 100644 prisma-old/dev.db delete mode 100644 prisma-old/generated/types.ts delete mode 100644 prisma-old/schema.prisma diff --git a/prisma-old/dev.db b/prisma-old/dev.db deleted file mode 100644 index d504ea3a56217a22a71b393db479a404d068d74b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI#O-sWt7zgmAippRwx19GHegW$YGVJE88Msr4G#iDzXq&;V`sMsCo_FwQ zGS?k;*~QcNKaiwPpY$caobvn>Wk%C*mQR$SYt~?#vl}AD7`N@fwlZ9PSSEIK`L5vX zV)$LH|1ht%WVL1eYu5v)LjVF0fB*y_009U<00Izz00jO^;G@Tno4(IK3!`4gTI#~| z3!Rsv*NR0b15)_oT13ImcjUY<{lHx^D_tCa~6j3j3cfxo;4`OisH)pCQdTZ9Y zqDHpR?n-el;>{b@wOpBOIts{sKiaO%-%@3C8k&Ia?BB_DM{K54&Mv*9X3OU+o2GhE zypQdfK2dWsTjfc)gygEca_9OJ=UH`K!*vJ = T extends ColumnType - ? ColumnType - : ColumnType; -export type Timestamp = ColumnType; - -export type TestUser = { - id: string; - name: string; - age: number; - rating: number; - updatedAt: string; -}; -export type DB = { - TestUser: TestUser; -}; diff --git a/prisma-old/schema.prisma b/prisma-old/schema.prisma deleted file mode 100644 index 50818da..0000000 --- a/prisma-old/schema.prisma +++ /dev/null @@ -1,17 +0,0 @@ -datasource db { - provider = "sqlite" - url = "file:./dev.db" - } - - generator kysely { - provider = "node ./dist/bin.js" - enumFileName = "enums.ts" - } - - model TestUser { - id String @id - name String - age Int - rating Float - updatedAt DateTime - } \ No newline at end of file From 83ec33b74f0783cc72eabc3b2f9edfd7ed9afc2f Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 11 Oct 2024 16:53:29 -0300 Subject: [PATCH 11/12] changeset --- .changeset/fast-geese-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-geese-smell.md diff --git a/.changeset/fast-geese-smell.md b/.changeset/fast-geese-smell.md new file mode 100644 index 0000000..b6aeb28 --- /dev/null +++ b/.changeset/fast-geese-smell.md @@ -0,0 +1,5 @@ +--- +"prisma-kysely": minor +--- + +Added groupBySchema option From 16d7719054fca17cc3c6a3999bcc56e190e72552 Mon Sep 17 00:00:00 2001 From: Arthur Fiorette Date: Fri, 10 Jan 2025 16:29:08 -0300 Subject: [PATCH 12/12] feat: filterBySchema --- README.md | 1 + src/__test__/e2e.test.ts | 139 ++++++++++++++++++++++++- src/generator.ts | 9 +- src/helpers/multiSchemaHelpers.test.ts | 19 ++++ src/helpers/multiSchemaHelpers.ts | 34 +++--- src/utils/validateConfig.ts | 3 + 6 files changed, 189 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6795c37..dbffffc 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ hope it's just as useful for you! 😎 | `readOnlyIds` | Use Kysely's `GeneratedAlways` for `@id` fields with default values, preventing insert and update. | | `[typename]TypeOverride` | Allows you to override the resulting TypeScript type for any Prisma type. Useful when targeting a different environment than Node (e.g. WinterCG compatible runtimes that use UInt8Arrays instead of Buffers for binary types etc.) Check out the [config validator](https://github.com/valtyr/prisma-kysely/blob/main/src/utils/validateConfig.ts) for a complete list of options. | | `groupBySchema` | When using `multiSchema` preview features, group all models and enums for a schema into their own namespace. (Ex: `model Dog { @@schema("animals") }` will be available under `Animals.Dog`) | +| `filterBySchema` | When using `multiSchema` preview features, only include models and enums for the specified schema. | ### Per-field type overrides diff --git a/src/__test__/e2e.test.ts b/src/__test__/e2e.test.ts index 6d3477a..67a2a77 100644 --- a/src/__test__/e2e.test.ts +++ b/src/__test__/e2e.test.ts @@ -1,6 +1,6 @@ -import { exec as execCb } from "child_process"; -import fs from "fs/promises"; -import { promisify } from "util"; +import { exec as execCb } from "node:child_process"; +import fs from "node:fs/promises"; +import { promisify } from "node:util"; import { afterEach, beforeEach, expect, test } from "vitest"; const exec = promisify(execCb); @@ -281,6 +281,58 @@ test( { timeout: 20000 } ); +test( + "End to end test - multi-schema support", + async () => { + // Initialize prisma: + await exec("yarn prisma init --datasource-provider postgresql"); + + // Set up a schema + await fs.writeFile( + "./prisma/schema.prisma", + `generator kysely { + provider = "node ./dist/bin.js" + previewFeatures = ["multiSchema"] + filterBySchema = ["mammals"] + } + + datasource db { + provider = "postgresql" + schemas = ["mammals", "birds"] + url = env("TEST_DATABASE_URL") + } + + model Elephant { + id Int @id + name String + + @@map("elephants") + @@schema("mammals") + } + + model Eagle { + id Int @id + name String + + @@map("eagles") + @@schema("birds") + }` + ); + + await exec("yarn prisma generate"); + + // Shouldn't have an empty import statement + const typeFile = await fs.readFile("./prisma/generated/types.ts", { + encoding: "utf-8", + }); + + expect(typeFile).toContain(`export type DB = { + "mammals.elephants": Elephant; +};`); + }, + { timeout: 20000 } +); + test( "End to end test - multi-schema and groupBySchema support", async () => { @@ -361,3 +413,84 @@ enum Color { }, { timeout: 20000 } ); + +test( + "End to end test - multi-schema, groupBySchema and filterBySchema support", + async () => { + // Initialize prisma: + await exec("yarn prisma init --datasource-provider postgresql"); + + // Set up a schema + await fs.writeFile( + "./prisma/schema.prisma", + ` +generator kysely { + provider = "node ./dist/bin.js" + previewFeatures = ["multiSchema"] + groupBySchema = true + filterBySchema = ["mammals", "world"] +} + +datasource db { + provider = "postgresql" + schemas = ["mammals", "birds", "world"] + url = env("TEST_DATABASE_URL") +} + +model Elephant { + id Int @id + name String + ability Ability @default(WALK) + color Color + + @@map("elephants") + @@schema("mammals") +} + +model Eagle { + id Int @id + name String + ability Ability @default(FLY) + + @@map("eagles") + @@schema("birds") +} + +enum Ability { + FLY + WALK + + @@schema("world") +} + +enum Color { + GRAY + PINK + + @@schema("mammals") +} + ` + ); + + await exec("yarn prisma generate"); + + // Shouldn't have an empty import statement + const typeFile = await fs.readFile("./prisma/generated/types.ts", { + encoding: "utf-8", + }); + + expect(typeFile).not.toContain(`export namespace Birds { + export type Eagle = {`); + + expect(typeFile).toContain(`export namespace Mammals { + export const Color = {`); + + // correctly references the color enum + expect(typeFile).toContain("color: Mammals.Color;"); + + expect(typeFile).toContain(`export type DB = { + "mammals.elephants": Mammals.Elephant; +};`); + }, + { timeout: 20000 } +); diff --git a/src/generator.ts b/src/generator.ts index 1c90d96..19367b2 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,6 +1,6 @@ import type { GeneratorOptions } from "@prisma/generator-helper"; import { generatorHandler } from "@prisma/generator-helper"; -import path from "path"; +import path from "node:path"; import { GENERATOR_NAME } from "~/constants"; import { generateDatabaseType } from "~/helpers/generateDatabaseType"; @@ -64,14 +64,21 @@ generatorHandler({ // Extend model table names with schema names if using multi-schemas if (options.generator.previewFeatures?.includes("multiSchema")) { + const filterBySchema = config.filterBySchema + ? new Set(config.filterBySchema) + : null; + models = convertToMultiSchemaModels( models, config.groupBySchema, + filterBySchema, multiSchemaMap ); + enums = convertToMultiSchemaModels( enums, config.groupBySchema, + filterBySchema, multiSchemaMap ); } diff --git a/src/helpers/multiSchemaHelpers.test.ts b/src/helpers/multiSchemaHelpers.test.ts index 12b8d37..a8c5564 100644 --- a/src/helpers/multiSchemaHelpers.test.ts +++ b/src/helpers/multiSchemaHelpers.test.ts @@ -41,6 +41,7 @@ test("returns a list of models with schemas appended to the table name", () => { const result = convertToMultiSchemaModels( initialModels, false, + null, parseMultiSchemaMap(testDataModel) ); @@ -49,3 +50,21 @@ test("returns a list of models with schemas appended to the table name", () => { { typeName: "Eagle", tableName: "birds.eagles" }, ]); }); + +test("returns a list of models with schemas appended to the table name filtered by schema", () => { + const initialModels = [ + { typeName: "Elephant", tableName: "elephants" }, + { typeName: "Eagle", tableName: "eagles" }, + ]; + + const result = convertToMultiSchemaModels( + initialModels, + false, + new Set(["mammals"]), + parseMultiSchemaMap(testDataModel) + ); + + expect(result).toEqual([ + { typeName: "Elephant", tableName: "mammals.elephants" }, + ]); +}); diff --git a/src/helpers/multiSchemaHelpers.ts b/src/helpers/multiSchemaHelpers.ts index 1855704..bbfbcb3 100644 --- a/src/helpers/multiSchemaHelpers.ts +++ b/src/helpers/multiSchemaHelpers.ts @@ -14,31 +14,41 @@ type ModelLike = { * * @param models list of model names * @param multiSchemaMap map of model names to schema names + * @param groupBySchema whether to group models by schema + * @param filterBySchema set of schema names to filter by. Use `null` to disable filtering. * @returns list of models with schema names appended to the table names ("schema.table") */ export const convertToMultiSchemaModels = ( models: T[], groupBySchema: boolean, + filterBySchema: Set | null, multiSchemaMap?: Map ): T[] => { - return models.map((model) => { + return models.flatMap((model) => { const schemaName = multiSchemaMap?.get(model.typeName); if (!schemaName) { return model; } - return { - ...model, - typeName: - groupBySchema && schemaName - ? `${capitalize(schemaName)}.${model.typeName}` - : model.typeName, - tableName: model.tableName - ? `${schemaName}.${model.tableName}` - : undefined, - schema: groupBySchema ? schemaName : undefined, - }; + // Filter out models that don't match the schema filter + if (filterBySchema && !filterBySchema.has(schemaName)) { + return []; + } + + return [ + { + ...model, + typeName: + groupBySchema && schemaName + ? `${capitalize(schemaName)}.${model.typeName}` + : model.typeName, + tableName: model.tableName + ? `${schemaName}.${model.tableName}` + : undefined, + schema: groupBySchema ? schemaName : undefined, + }, + ]; }); }; diff --git a/src/utils/validateConfig.ts b/src/utils/validateConfig.ts index 26938a7..e83983f 100644 --- a/src/utils/validateConfig.ts +++ b/src/utils/validateConfig.ts @@ -43,6 +43,9 @@ export const configValidator = z // Group models in a namespace by their schema. Cannot be defined if enumFileName is defined. groupBySchema: booleanStringLiteral.default(false), + + // Group models in a namespace by their schema. Cannot be defined if enumFileName is defined. + filterBySchema: z.array(z.string()).optional(), }) .strict() .transform((config) => {