Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add groupBySchema to group types and enums under a namespace #110

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/fast-geese-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"prisma-kysely": minor
---

Added groupBySchema option
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

Expand Down Expand Up @@ -83,6 +83,8 @@ 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`) |
| `filterBySchema` | When using `multiSchema` preview features, only include models and enums for the specified schema. |

### Per-field type overrides

Expand Down
220 changes: 217 additions & 3 deletions src/__test__/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -280,3 +280,217 @@ 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 () => {
// 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 = {`);

// 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;
};`);
},
{ 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 }
);
51 changes: 38 additions & 13 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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");
Expand All @@ -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
//
Expand All @@ -45,31 +48,53 @@ 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);
const filterBySchema = config.filterBySchema
? new Set(config.filterBySchema)
: null;

models = convertToMultiSchemaModels(
models,
config.groupBySchema,
filterBySchema,
multiSchemaMap
);

enums = convertToMultiSchemaModels(
enums,
config.groupBySchema,
filterBySchema,
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!
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/generateDatabaseType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test("it works for plain vanilla type names", () => {
enumFileName: "",
camelCase: false,
readOnlyIds: false,
groupBySchema: false,
}
);
const result = stringifyTsNode(node);
Expand All @@ -40,6 +41,7 @@ test("it respects camelCase option names", () => {
enumFileName: "",
camelCase: true,
readOnlyIds: false,
groupBySchema: false,
}
);
const result = stringifyTsNode(node);
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/generateEnumType.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading
Loading