@effectify/prisma 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -38
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +15 -14
- package/dist/src/commands/init.d.ts +1 -1
- package/dist/src/commands/init.js +36 -47
- package/dist/src/commands/prisma.d.ts +4 -4
- package/dist/src/commands/prisma.js +12 -12
- package/dist/src/generators/sql-schema-generator.js +15 -16
- package/dist/src/runtime/index.d.ts +303 -0
- package/dist/src/runtime/index.js +216 -0
- package/dist/src/schema-generator/effect/enum.d.ts +12 -0
- package/dist/src/schema-generator/effect/enum.js +18 -0
- package/dist/src/schema-generator/effect/generator.d.ts +16 -0
- package/dist/src/schema-generator/effect/generator.js +42 -0
- package/dist/src/schema-generator/effect/join-table.d.ts +12 -0
- package/dist/src/schema-generator/effect/join-table.js +28 -0
- package/dist/src/schema-generator/effect/type.d.ts +18 -0
- package/dist/src/schema-generator/effect/type.js +82 -0
- package/dist/src/schema-generator/index.d.ts +11 -0
- package/dist/src/schema-generator/index.js +83 -0
- package/dist/src/schema-generator/kysely/generator.d.ts +11 -0
- package/dist/src/schema-generator/kysely/generator.js +7 -0
- package/dist/src/schema-generator/kysely/type.d.ts +14 -0
- package/dist/src/schema-generator/kysely/type.js +44 -0
- package/dist/src/schema-generator/prisma/enum.d.ts +19 -0
- package/dist/src/schema-generator/prisma/enum.js +19 -0
- package/dist/src/schema-generator/prisma/generator.d.ts +53 -0
- package/dist/src/schema-generator/prisma/generator.js +29 -0
- package/dist/src/schema-generator/prisma/relation.d.ts +83 -0
- package/dist/src/schema-generator/prisma/relation.js +165 -0
- package/dist/src/schema-generator/prisma/type.d.ts +108 -0
- package/dist/src/schema-generator/prisma/type.js +85 -0
- package/dist/src/schema-generator/utils/annotations.d.ts +32 -0
- package/dist/src/schema-generator/utils/annotations.js +79 -0
- package/dist/src/schema-generator/utils/codegen.d.ts +9 -0
- package/dist/src/schema-generator/utils/codegen.js +14 -0
- package/dist/src/schema-generator/utils/naming.d.ts +29 -0
- package/dist/src/schema-generator/utils/naming.js +68 -0
- package/dist/src/schema-generator/utils/type-mappings.d.ts +62 -0
- package/dist/src/schema-generator/utils/type-mappings.js +70 -0
- package/dist/src/services/formatter-service.d.ts +10 -0
- package/dist/src/services/formatter-service.js +18 -0
- package/dist/src/services/generator-context.d.ts +2 -2
- package/dist/src/services/generator-context.js +2 -2
- package/dist/src/services/generator-service.d.ts +9 -8
- package/dist/src/services/generator-service.js +39 -43
- package/dist/src/services/render-service.d.ts +3 -3
- package/dist/src/services/render-service.js +8 -8
- package/dist/src/templates/effect-branded-id.eta +2 -0
- package/dist/src/templates/effect-enums.eta +9 -0
- package/dist/src/templates/effect-index.eta +4 -0
- package/dist/src/templates/effect-join-table.eta +8 -0
- package/dist/src/templates/effect-model.eta +6 -0
- package/dist/src/templates/effect-types-header.eta +7 -0
- package/dist/src/templates/index-default.eta +2 -1
- package/dist/src/templates/kysely-db-interface.eta +6 -0
- package/dist/src/templates/prisma-repository.eta +57 -32
- package/package.json +11 -6
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isListField, isRequiredField, isUuidField } from "../prisma/type.js";
|
|
2
|
+
import { extractEffectTypeOverride } from "../utils/annotations.js";
|
|
3
|
+
import { toPascalCase } from "../utils/naming.js";
|
|
4
|
+
/**
|
|
5
|
+
* Prisma scalar type mapping to Effect Schema types
|
|
6
|
+
* Uses const assertion to avoid type guards
|
|
7
|
+
*
|
|
8
|
+
* Note: DateTime uses Schema.DateFromSelf (not Schema.Date) so that:
|
|
9
|
+
* - Type = Date (runtime)
|
|
10
|
+
* - Encoded = Date (database)
|
|
11
|
+
* This allows Kysely to work with native Date objects directly.
|
|
12
|
+
* Schema.Date would encode to string, requiring ISO string conversions.
|
|
13
|
+
*/
|
|
14
|
+
const PRISMA_SCALAR_MAP = {
|
|
15
|
+
String: "Schema.String",
|
|
16
|
+
Int: "Schema.Number",
|
|
17
|
+
Float: "Schema.Number",
|
|
18
|
+
BigInt: "Schema.BigInt",
|
|
19
|
+
Decimal: "Schema.String", // For precision
|
|
20
|
+
Boolean: "Schema.Boolean",
|
|
21
|
+
DateTime: "Schema.DateFromSelf", // Native Date type for Kysely compatibility
|
|
22
|
+
Json: "Schema.Unknown", // Safe unknown type
|
|
23
|
+
Bytes: "Schema.Uint8Array",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Map Prisma field type to Effect Schema type
|
|
27
|
+
* Priority order: annotation → FK branded → UUID → scalar → enum → unknown fallback
|
|
28
|
+
*
|
|
29
|
+
* @param field - The Prisma field to map
|
|
30
|
+
* @param dmmf - The full DMMF document for enum lookups
|
|
31
|
+
* @param fkMap - Optional FK field → target model mapping for branded FK types
|
|
32
|
+
*/
|
|
33
|
+
export function mapFieldToEffectType(field, dmmf, fkMap) {
|
|
34
|
+
// PRIORITY 1: Check for @customType annotation
|
|
35
|
+
const typeOverride = extractEffectTypeOverride(field);
|
|
36
|
+
if (typeOverride) {
|
|
37
|
+
return typeOverride;
|
|
38
|
+
}
|
|
39
|
+
// PRIORITY 2: Check if this is a FK field with branded target
|
|
40
|
+
// FK fields use the referenced model's branded ID (e.g., UserId for user_id)
|
|
41
|
+
if (fkMap && fkMap.has(field.name)) {
|
|
42
|
+
const targetModel = fkMap.get(field.name);
|
|
43
|
+
return `${toPascalCase(targetModel)}Id`;
|
|
44
|
+
}
|
|
45
|
+
// PRIORITY 3: Handle String type with UUID detection (non-FK UUIDs)
|
|
46
|
+
if (field.type === "String" && isUuidField(field)) {
|
|
47
|
+
return "Schema.UUID";
|
|
48
|
+
}
|
|
49
|
+
// PRIORITY 4: Handle scalar types with const assertion lookup
|
|
50
|
+
const scalarType = PRISMA_SCALAR_MAP[field.type];
|
|
51
|
+
if (scalarType) {
|
|
52
|
+
return scalarType;
|
|
53
|
+
}
|
|
54
|
+
// PRIORITY 5: Check if it's an enum
|
|
55
|
+
const enumDef = dmmf.datamodel.enums.find((e) => e.name === field.type);
|
|
56
|
+
if (enumDef) {
|
|
57
|
+
// PascalCase name IS the Schema now (not raw enum)
|
|
58
|
+
return toPascalCase(field.type);
|
|
59
|
+
}
|
|
60
|
+
// PRIORITY 6: Fallback to Unknown
|
|
61
|
+
return "Schema.Unknown";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build complete field type with array and optional wrapping
|
|
65
|
+
*
|
|
66
|
+
* @param field - The Prisma field to build type for
|
|
67
|
+
* @param dmmf - The full DMMF document for enum lookups
|
|
68
|
+
* @param fkMap - Optional FK field → target model mapping for branded FK types
|
|
69
|
+
*/
|
|
70
|
+
export function buildFieldType(field, dmmf, fkMap) {
|
|
71
|
+
let baseType = mapFieldToEffectType(field, dmmf, fkMap);
|
|
72
|
+
// Handle arrays
|
|
73
|
+
if (isListField(field)) {
|
|
74
|
+
baseType = `Schema.Array(${baseType})`;
|
|
75
|
+
}
|
|
76
|
+
// Handle nullable fields - wrap with NullOr regardless of default value
|
|
77
|
+
// This ensures SELECT type correctly allows null values (e.g., Boolean? @default(false))
|
|
78
|
+
if (!isRequiredField(field)) {
|
|
79
|
+
baseType = `Schema.NullOr(${baseType})`;
|
|
80
|
+
}
|
|
81
|
+
return baseType;
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
2
|
+
import * as Path from "@effect/platform/Path";
|
|
3
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import { RenderService } from "../services/render-service.js";
|
|
6
|
+
import { FormatterService } from "../services/formatter-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* Generate Effect schemas (enums, types, index)
|
|
9
|
+
* Replicates legacy generator logic using Effect patterns and Eta templates
|
|
10
|
+
*/
|
|
11
|
+
export declare const generateSchemas: (dmmf: DMMF.Document, outputDir: string) => Effect.Effect<void, import("@effect/platform/Error").PlatformError | Error, FileSystem.FileSystem | Path.Path | RenderService | FormatterService>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem";
|
|
2
|
+
import * as Path from "@effect/platform/Path";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { RenderService } from "../services/render-service.js";
|
|
5
|
+
import { FormatterService } from "../services/formatter-service.js";
|
|
6
|
+
import * as EnumGenerator from "./effect/enum.js";
|
|
7
|
+
import * as EffectGenerator from "./effect/generator.js";
|
|
8
|
+
import * as JoinTableGenerator from "./effect/join-table.js";
|
|
9
|
+
import * as KyselyGenerator from "./kysely/generator.js";
|
|
10
|
+
import * as PrismaGenerator from "./prisma/generator.js";
|
|
11
|
+
/**
|
|
12
|
+
* Generate Effect schemas (enums, types, index)
|
|
13
|
+
* Replicates legacy generator logic using Effect patterns and Eta templates
|
|
14
|
+
*/
|
|
15
|
+
export const generateSchemas = (dmmf, outputDir) => Effect.gen(function* () {
|
|
16
|
+
const fs = yield* FileSystem.FileSystem;
|
|
17
|
+
const path = yield* Path.Path;
|
|
18
|
+
const { render } = yield* RenderService;
|
|
19
|
+
const { format } = yield* FormatterService;
|
|
20
|
+
yield* fs.makeDirectory(outputDir, { recursive: true });
|
|
21
|
+
const writeFile = (filename, content) => Effect.gen(function* () {
|
|
22
|
+
const formatted = yield* format(content);
|
|
23
|
+
const filePath = path.join(outputDir, filename);
|
|
24
|
+
yield* fs.writeFileString(filePath, formatted);
|
|
25
|
+
});
|
|
26
|
+
// Generate Enums
|
|
27
|
+
const enums = PrismaGenerator.getEnums(dmmf);
|
|
28
|
+
const enumsData = EnumGenerator.prepareEnumsData(enums);
|
|
29
|
+
if (enumsData) {
|
|
30
|
+
const content = yield* render("effect-enums", enumsData);
|
|
31
|
+
yield* writeFile("enums.ts", content);
|
|
32
|
+
}
|
|
33
|
+
// Generate Types
|
|
34
|
+
const models = PrismaGenerator.getModels(dmmf);
|
|
35
|
+
const joinTables = PrismaGenerator.getManyToManyJoinTables(dmmf);
|
|
36
|
+
const hasEnums = enums.length > 0;
|
|
37
|
+
// Header
|
|
38
|
+
const headerData = EffectGenerator.prepareTypesHeaderData(dmmf, hasEnums);
|
|
39
|
+
let content = yield* render("effect-types-header", headerData);
|
|
40
|
+
// Branded IDs
|
|
41
|
+
const brandedIdsData = models
|
|
42
|
+
.map((model) => {
|
|
43
|
+
const fields = PrismaGenerator.getModelFields(model);
|
|
44
|
+
return EffectGenerator.prepareBrandedIdSchemaData(model, fields);
|
|
45
|
+
})
|
|
46
|
+
.filter((data) => data !== null);
|
|
47
|
+
if (brandedIdsData.length > 0) {
|
|
48
|
+
content += `\n\n// ===== Branded ID Schemas =====`;
|
|
49
|
+
for (const data of brandedIdsData) {
|
|
50
|
+
const idContent = yield* render("effect-branded-id", data);
|
|
51
|
+
content += `\n\n${idContent}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Models
|
|
55
|
+
const modelsData = models.map((model) => {
|
|
56
|
+
const fields = PrismaGenerator.getModelFields(model);
|
|
57
|
+
return EffectGenerator.prepareModelSchemaData(dmmf, model, fields);
|
|
58
|
+
});
|
|
59
|
+
if (modelsData.length > 0) {
|
|
60
|
+
content += `\n\n// ===== Model Schemas =====`;
|
|
61
|
+
for (const data of modelsData) {
|
|
62
|
+
const modelContent = yield* render("effect-model", data);
|
|
63
|
+
content += `\n\n${modelContent}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Join Tables
|
|
67
|
+
const joinTablesData = joinTables.map((jt) => JoinTableGenerator.prepareJoinTableData(jt, dmmf));
|
|
68
|
+
if (joinTablesData.length > 0) {
|
|
69
|
+
for (const data of joinTablesData) {
|
|
70
|
+
const jtContent = yield* render("effect-join-table", data);
|
|
71
|
+
content += `\n\n${jtContent}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// DB Interface
|
|
75
|
+
const dbInterfaceData = KyselyGenerator.prepareDBInterfaceData(models, joinTables);
|
|
76
|
+
const dbInterfaceContent = yield* render("kysely-db-interface", dbInterfaceData);
|
|
77
|
+
content += `\n\n${dbInterfaceContent}`;
|
|
78
|
+
yield* writeFile("types.ts", content);
|
|
79
|
+
// Index
|
|
80
|
+
const indexData = KyselyGenerator.prepareIndexData(hasEnums);
|
|
81
|
+
const indexContent = yield* render("effect-index", indexData);
|
|
82
|
+
yield* writeFile("index.ts", indexContent);
|
|
83
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
2
|
+
import type { JoinTableInfo } from "../prisma/relation.js";
|
|
3
|
+
export declare const prepareDBInterfaceData: (models: readonly DMMF.Model[], joinTables?: JoinTableInfo[]) => {
|
|
4
|
+
models: {
|
|
5
|
+
tableName: string;
|
|
6
|
+
typeName: string;
|
|
7
|
+
}[];
|
|
8
|
+
};
|
|
9
|
+
export declare const prepareIndexData: (hasEnums?: boolean) => {
|
|
10
|
+
hasEnums: boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { prepareDBInterfaceData as prepareDBInterfaceDataHelper } from "./type.js";
|
|
2
|
+
export const prepareDBInterfaceData = (models, joinTables = []) => {
|
|
3
|
+
return prepareDBInterfaceDataHelper(models, joinTables);
|
|
4
|
+
};
|
|
5
|
+
export const prepareIndexData = (hasEnums = true) => {
|
|
6
|
+
return { hasEnums };
|
|
7
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
2
|
+
import type { JoinTableInfo } from "../prisma/relation.js";
|
|
3
|
+
export declare function needsColumnType(field: DMMF.Field): boolean;
|
|
4
|
+
export declare function isEnumField(field: DMMF.Field, dmmf: DMMF.Document): boolean;
|
|
5
|
+
export declare function needsGenerated(field: DMMF.Field, dmmf: DMMF.Document): boolean;
|
|
6
|
+
export declare function applyKyselyHelpers(fieldType: string, field: DMMF.Field, dmmf: DMMF.Document, modelName?: string): string;
|
|
7
|
+
export declare function applyMapDirective(fieldType: string, field: DMMF.Field): string;
|
|
8
|
+
export declare function buildKyselyFieldType(baseFieldType: string, field: DMMF.Field, dmmf: DMMF.Document, modelName?: string): string;
|
|
9
|
+
export declare function prepareDBInterfaceData(models: readonly DMMF.Model[], joinTables?: JoinTableInfo[]): {
|
|
10
|
+
models: {
|
|
11
|
+
tableName: string;
|
|
12
|
+
typeName: string;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getFieldDbName, hasDefaultValue, isIdField } from "../prisma/type.js";
|
|
2
|
+
import { toPascalCase } from "../utils/naming.js";
|
|
3
|
+
export function needsColumnType(field) {
|
|
4
|
+
return hasDefaultValue(field) && isIdField(field);
|
|
5
|
+
}
|
|
6
|
+
export function isEnumField(field, dmmf) {
|
|
7
|
+
return dmmf.datamodel.enums.some((e) => e.name === field.type);
|
|
8
|
+
}
|
|
9
|
+
export function needsGenerated(field, dmmf) {
|
|
10
|
+
return hasDefaultValue(field) && !isIdField(field) && !isEnumField(field, dmmf);
|
|
11
|
+
}
|
|
12
|
+
export function applyKyselyHelpers(fieldType, field, dmmf, modelName) {
|
|
13
|
+
if (needsColumnType(field)) {
|
|
14
|
+
const idType = modelName ? `${toPascalCase(modelName)}Id` : fieldType;
|
|
15
|
+
return `generated(${idType})`;
|
|
16
|
+
}
|
|
17
|
+
else if (needsGenerated(field, dmmf)) {
|
|
18
|
+
return `generated(${fieldType})`;
|
|
19
|
+
}
|
|
20
|
+
return fieldType;
|
|
21
|
+
}
|
|
22
|
+
export function applyMapDirective(fieldType, field) {
|
|
23
|
+
const dbName = getFieldDbName(field);
|
|
24
|
+
if (field.dbName && field.dbName !== field.name) {
|
|
25
|
+
return `Schema.propertySignature(${fieldType}).pipe(Schema.fromKey("${dbName}"))`;
|
|
26
|
+
}
|
|
27
|
+
return fieldType;
|
|
28
|
+
}
|
|
29
|
+
export function buildKyselyFieldType(baseFieldType, field, dmmf, modelName) {
|
|
30
|
+
let fieldType = applyKyselyHelpers(baseFieldType, field, dmmf, modelName);
|
|
31
|
+
fieldType = applyMapDirective(fieldType, field);
|
|
32
|
+
return fieldType;
|
|
33
|
+
}
|
|
34
|
+
export function prepareDBInterfaceData(models, joinTables = []) {
|
|
35
|
+
const modelEntries = models.map((m) => ({
|
|
36
|
+
tableName: m.dbName || m.name,
|
|
37
|
+
typeName: toPascalCase(m.name),
|
|
38
|
+
}));
|
|
39
|
+
const joinTableEntries = joinTables.map((jt) => ({
|
|
40
|
+
tableName: jt.tableName,
|
|
41
|
+
typeName: toPascalCase(jt.relationName),
|
|
42
|
+
}));
|
|
43
|
+
return { models: [...modelEntries, ...joinTableEntries] };
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
2
|
+
/**
|
|
3
|
+
* Extract enum definitions from Prisma DMMF
|
|
4
|
+
* Handles @map directive for database-level enum values
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractEnums(dmmf: DMMF.Document): readonly DMMF.ReadonlyDeep<DMMF.ReadonlyDeep<DMMF.ReadonlyDeep<{
|
|
7
|
+
name: string;
|
|
8
|
+
values: DMMF.EnumValue[];
|
|
9
|
+
dbName?: string | null;
|
|
10
|
+
documentation?: string;
|
|
11
|
+
}>>>[];
|
|
12
|
+
/**
|
|
13
|
+
* Get the database value for an enum value (respects @map directive)
|
|
14
|
+
*/
|
|
15
|
+
export declare function getEnumValueDbName(enumValue: DMMF.EnumValue): string;
|
|
16
|
+
/**
|
|
17
|
+
* Get all database values for an enum
|
|
18
|
+
*/
|
|
19
|
+
export declare function getEnumDbValues(enumDef: DMMF.DatamodelEnum): string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract enum definitions from Prisma DMMF
|
|
3
|
+
* Handles @map directive for database-level enum values
|
|
4
|
+
*/
|
|
5
|
+
export function extractEnums(dmmf) {
|
|
6
|
+
return dmmf.datamodel.enums;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get the database value for an enum value (respects @map directive)
|
|
10
|
+
*/
|
|
11
|
+
export function getEnumValueDbName(enumValue) {
|
|
12
|
+
return enumValue.dbName ?? enumValue.name;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get all database values for an enum
|
|
16
|
+
*/
|
|
17
|
+
export function getEnumDbValues(enumDef) {
|
|
18
|
+
return enumDef.values.map(getEnumValueDbName);
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
2
|
+
/**
|
|
3
|
+
* Get all enums from DMMF
|
|
4
|
+
*/
|
|
5
|
+
export declare const getEnums: (dmmf: DMMF.Document) => readonly DMMF.ReadonlyDeep<DMMF.ReadonlyDeep<DMMF.ReadonlyDeep<{
|
|
6
|
+
name: string;
|
|
7
|
+
values: DMMF.EnumValue[];
|
|
8
|
+
dbName?: string | null;
|
|
9
|
+
documentation?: string;
|
|
10
|
+
}>>>[];
|
|
11
|
+
/**
|
|
12
|
+
* Get all models from DMMF (filtered and sorted)
|
|
13
|
+
*/
|
|
14
|
+
export declare const getModels: (dmmf: DMMF.Document) => DMMF.ReadonlyDeep<{
|
|
15
|
+
name: string;
|
|
16
|
+
dbName: string | null;
|
|
17
|
+
schema: string | null;
|
|
18
|
+
fields: DMMF.Field[];
|
|
19
|
+
uniqueFields: string[][];
|
|
20
|
+
uniqueIndexes: DMMF.uniqueIndex[];
|
|
21
|
+
documentation?: string;
|
|
22
|
+
primaryKey: DMMF.PrimaryKey | null;
|
|
23
|
+
isGenerated?: boolean;
|
|
24
|
+
}>[];
|
|
25
|
+
/**
|
|
26
|
+
* Get schema fields for a model (filtered and sorted)
|
|
27
|
+
*/
|
|
28
|
+
export declare const getModelFields: (model: DMMF.Model) => DMMF.ReadonlyDeep<{
|
|
29
|
+
kind: DMMF.FieldKind;
|
|
30
|
+
name: string;
|
|
31
|
+
isRequired: boolean;
|
|
32
|
+
isList: boolean;
|
|
33
|
+
isUnique: boolean;
|
|
34
|
+
isId: boolean;
|
|
35
|
+
isReadOnly: boolean;
|
|
36
|
+
isGenerated?: boolean;
|
|
37
|
+
isUpdatedAt?: boolean;
|
|
38
|
+
type: string;
|
|
39
|
+
nativeType?: [string, string[]] | null;
|
|
40
|
+
dbName?: string | null;
|
|
41
|
+
hasDefaultValue: boolean;
|
|
42
|
+
default?: DMMF.FieldDefault | DMMF.FieldDefaultScalar | DMMF.FieldDefaultScalar[];
|
|
43
|
+
relationFromFields?: string[];
|
|
44
|
+
relationToFields?: string[];
|
|
45
|
+
relationOnDelete?: string;
|
|
46
|
+
relationOnUpdate?: string;
|
|
47
|
+
relationName?: string;
|
|
48
|
+
documentation?: string;
|
|
49
|
+
}>[];
|
|
50
|
+
/**
|
|
51
|
+
* Get implicit many-to-many join tables
|
|
52
|
+
*/
|
|
53
|
+
export declare const getManyToManyJoinTables: (dmmf: DMMF.Document) => import("./relation.js").JoinTableInfo[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as PrismaEnum from "./enum.js";
|
|
2
|
+
import { detectImplicitManyToMany } from "./relation.js";
|
|
3
|
+
import * as PrismaType from "./type.js";
|
|
4
|
+
/**
|
|
5
|
+
* Get all enums from DMMF
|
|
6
|
+
*/
|
|
7
|
+
export const getEnums = (dmmf) => {
|
|
8
|
+
return PrismaEnum.extractEnums(dmmf);
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Get all models from DMMF (filtered and sorted)
|
|
12
|
+
*/
|
|
13
|
+
export const getModels = (dmmf) => {
|
|
14
|
+
const filtered = PrismaType.filterInternalModels(dmmf.datamodel.models);
|
|
15
|
+
return PrismaType.sortModels(filtered);
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Get schema fields for a model (filtered and sorted)
|
|
19
|
+
*/
|
|
20
|
+
export const getModelFields = (model) => {
|
|
21
|
+
const filtered = PrismaType.filterSchemaFields(model.fields);
|
|
22
|
+
return PrismaType.sortFields(filtered);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get implicit many-to-many join tables
|
|
26
|
+
*/
|
|
27
|
+
export const getManyToManyJoinTables = (dmmf) => {
|
|
28
|
+
return detectImplicitManyToMany(dmmf.datamodel.models);
|
|
29
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { DMMF } from "@prisma/generator-helper";
|
|
2
|
+
/**
|
|
3
|
+
* Metadata for implicit many-to-many join tables
|
|
4
|
+
*/
|
|
5
|
+
export interface JoinTableInfo {
|
|
6
|
+
/** Table name in database (e.g., "_CategoryToPost") */
|
|
7
|
+
tableName: string;
|
|
8
|
+
/** Relation name without underscore (e.g., "CategoryToPost") */
|
|
9
|
+
relationName: string;
|
|
10
|
+
/** First model name (alphabetically) */
|
|
11
|
+
modelA: string;
|
|
12
|
+
/** Second model name (alphabetically) */
|
|
13
|
+
modelB: string;
|
|
14
|
+
/** Type of column A (from modelA's ID field) */
|
|
15
|
+
columnAType: string;
|
|
16
|
+
/** Type of column B (from modelB's ID field) */
|
|
17
|
+
columnBType: string;
|
|
18
|
+
/** Whether column A is a UUID */
|
|
19
|
+
columnAIsUuid: boolean;
|
|
20
|
+
/** Whether column B is a UUID */
|
|
21
|
+
columnBIsUuid: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get the ID field from a model
|
|
25
|
+
* Handles both single @id and composite @@id
|
|
26
|
+
*/
|
|
27
|
+
export declare function getModelIdField(model: DMMF.Model): DMMF.ReadonlyDeep<DMMF.ReadonlyDeep<{
|
|
28
|
+
kind: DMMF.FieldKind;
|
|
29
|
+
name: string;
|
|
30
|
+
isRequired: boolean;
|
|
31
|
+
isList: boolean;
|
|
32
|
+
isUnique: boolean;
|
|
33
|
+
isId: boolean;
|
|
34
|
+
isReadOnly: boolean;
|
|
35
|
+
isGenerated?: boolean;
|
|
36
|
+
isUpdatedAt?: boolean;
|
|
37
|
+
type: string;
|
|
38
|
+
nativeType?: [string, string[]] | null;
|
|
39
|
+
dbName?: string | null;
|
|
40
|
+
hasDefaultValue: boolean;
|
|
41
|
+
default?: DMMF.FieldDefault | DMMF.FieldDefaultScalar | DMMF.FieldDefaultScalar[];
|
|
42
|
+
relationFromFields?: string[];
|
|
43
|
+
relationToFields?: string[];
|
|
44
|
+
relationOnDelete?: string;
|
|
45
|
+
relationOnUpdate?: string;
|
|
46
|
+
relationName?: string;
|
|
47
|
+
documentation?: string;
|
|
48
|
+
}>>;
|
|
49
|
+
/**
|
|
50
|
+
* Build FK field → target model mapping for a model
|
|
51
|
+
* Returns map from FK field name to target model name
|
|
52
|
+
*
|
|
53
|
+
* Detection logic: A scalar field is a FK if any relation field
|
|
54
|
+
* in the same model has this field in its `relationFromFields` array.
|
|
55
|
+
*
|
|
56
|
+
* IMPORTANT: Only includes FKs that reference the target model's ID field.
|
|
57
|
+
* FKs to non-ID unique fields (like enums) should use the target field's type,
|
|
58
|
+
* not a branded ID schema.
|
|
59
|
+
*
|
|
60
|
+
* @param model - The model to analyze
|
|
61
|
+
* @param models - All models in the datamodel (for looking up target model ID fields)
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // For Seller model with user_id FK:
|
|
65
|
+
* // fields = [
|
|
66
|
+
* // { name: "user_id", kind: "scalar", type: "String" },
|
|
67
|
+
* // { name: "user", kind: "object", type: "User", relationFromFields: ["user_id"], relationToFields: ["id"] }
|
|
68
|
+
* // ]
|
|
69
|
+
* // Returns: Map { "user_id" => "User" }
|
|
70
|
+
*
|
|
71
|
+
* // For Product model with market FK to market.code (enum):
|
|
72
|
+
* // fields = [
|
|
73
|
+
* // { name: "market", kind: "scalar", type: "MARKET_TYPE" },
|
|
74
|
+
* // { name: "market_relation", kind: "object", type: "market", relationFromFields: ["market"], relationToFields: ["code"] }
|
|
75
|
+
* // ]
|
|
76
|
+
* // Returns: Map {} (empty - code is not the ID field)
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildForeignKeyMap(model: DMMF.Model, models?: readonly DMMF.Model[]): Map<string, string>;
|
|
79
|
+
/**
|
|
80
|
+
* Detect all implicit many-to-many relations from DMMF models
|
|
81
|
+
* Returns metadata for generating join table schemas
|
|
82
|
+
*/
|
|
83
|
+
export declare function detectImplicitManyToMany(models: readonly DMMF.Model[]): JoinTableInfo[];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isUuidField } from "./type.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detect if a relation field is part of an implicit many-to-many relation
|
|
4
|
+
* Criteria:
|
|
5
|
+
* - kind === "object" (relation field)
|
|
6
|
+
* - isList === true (many side)
|
|
7
|
+
* - relationFromFields is empty (no foreign key on this side)
|
|
8
|
+
* - relationToFields is empty (no explicit relation table)
|
|
9
|
+
*/
|
|
10
|
+
function isImplicitManyToManyField(field) {
|
|
11
|
+
return (field.kind === "object" &&
|
|
12
|
+
field.isList === true &&
|
|
13
|
+
field.relationFromFields !== undefined &&
|
|
14
|
+
field.relationFromFields.length === 0 &&
|
|
15
|
+
field.relationToFields !== undefined &&
|
|
16
|
+
field.relationToFields.length === 0);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the ID field from a model
|
|
20
|
+
* Handles both single @id and composite @@id
|
|
21
|
+
*/
|
|
22
|
+
export function getModelIdField(model) {
|
|
23
|
+
// Try to find single @id field
|
|
24
|
+
const idField = model.fields.find((f) => f.isId === true);
|
|
25
|
+
if (idField) {
|
|
26
|
+
return idField;
|
|
27
|
+
}
|
|
28
|
+
// For composite @@id, get the first field from primaryKey
|
|
29
|
+
if (model.primaryKey && model.primaryKey.fields.length > 0) {
|
|
30
|
+
const firstIdFieldName = model.primaryKey.fields[0];
|
|
31
|
+
const field = model.fields.find((f) => f.name === firstIdFieldName);
|
|
32
|
+
if (field) {
|
|
33
|
+
return field;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Model ${model.name} has no ID field (@id or @@id required)`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build FK field → target model mapping for a model
|
|
40
|
+
* Returns map from FK field name to target model name
|
|
41
|
+
*
|
|
42
|
+
* Detection logic: A scalar field is a FK if any relation field
|
|
43
|
+
* in the same model has this field in its `relationFromFields` array.
|
|
44
|
+
*
|
|
45
|
+
* IMPORTANT: Only includes FKs that reference the target model's ID field.
|
|
46
|
+
* FKs to non-ID unique fields (like enums) should use the target field's type,
|
|
47
|
+
* not a branded ID schema.
|
|
48
|
+
*
|
|
49
|
+
* @param model - The model to analyze
|
|
50
|
+
* @param models - All models in the datamodel (for looking up target model ID fields)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // For Seller model with user_id FK:
|
|
54
|
+
* // fields = [
|
|
55
|
+
* // { name: "user_id", kind: "scalar", type: "String" },
|
|
56
|
+
* // { name: "user", kind: "object", type: "User", relationFromFields: ["user_id"], relationToFields: ["id"] }
|
|
57
|
+
* // ]
|
|
58
|
+
* // Returns: Map { "user_id" => "User" }
|
|
59
|
+
*
|
|
60
|
+
* // For Product model with market FK to market.code (enum):
|
|
61
|
+
* // fields = [
|
|
62
|
+
* // { name: "market", kind: "scalar", type: "MARKET_TYPE" },
|
|
63
|
+
* // { name: "market_relation", kind: "object", type: "market", relationFromFields: ["market"], relationToFields: ["code"] }
|
|
64
|
+
* // ]
|
|
65
|
+
* // Returns: Map {} (empty - code is not the ID field)
|
|
66
|
+
*/
|
|
67
|
+
export function buildForeignKeyMap(model, models) {
|
|
68
|
+
const fkMap = new Map();
|
|
69
|
+
// Find all relation fields (kind === "object")
|
|
70
|
+
const relationFields = model.fields.filter((f) => f.kind === "object");
|
|
71
|
+
for (const relation of relationFields) {
|
|
72
|
+
if (relation.relationFromFields && relation.relationFromFields.length > 0) {
|
|
73
|
+
// Check if this FK references the target model's ID field
|
|
74
|
+
const targetModel = models?.find((m) => m.name === relation.type);
|
|
75
|
+
if (!targetModel) {
|
|
76
|
+
// Can't verify - skip this FK to be safe
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Get target model's ID field name
|
|
80
|
+
let targetIdFieldName;
|
|
81
|
+
const idField = targetModel.fields.find((f) => f.isId === true);
|
|
82
|
+
if (idField) {
|
|
83
|
+
targetIdFieldName = idField.name;
|
|
84
|
+
}
|
|
85
|
+
else if (targetModel.primaryKey && targetModel.primaryKey.fields.length > 0) {
|
|
86
|
+
targetIdFieldName = targetModel.primaryKey.fields[0];
|
|
87
|
+
}
|
|
88
|
+
if (!targetIdFieldName) {
|
|
89
|
+
// Target model has no ID field - skip
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Check if relationToFields references the ID field
|
|
93
|
+
const refersToIdField = relation.relationToFields?.length === 1 &&
|
|
94
|
+
relation.relationToFields[0] === targetIdFieldName;
|
|
95
|
+
if (!refersToIdField) {
|
|
96
|
+
// FK points to non-ID field (e.g., enum) - don't use branded ID
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Map each FK field to the target model
|
|
100
|
+
for (const fkFieldName of relation.relationFromFields) {
|
|
101
|
+
fkMap.set(fkFieldName, relation.type); // relation.type is the target model name
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return fkMap;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Detect all implicit many-to-many relations from DMMF models
|
|
109
|
+
* Returns metadata for generating join table schemas
|
|
110
|
+
*/
|
|
111
|
+
export function detectImplicitManyToMany(models) {
|
|
112
|
+
const joinTables = new Map();
|
|
113
|
+
for (const model of models) {
|
|
114
|
+
const validFields = model.fields.filter((field) => shouldProcessField(field, model, models));
|
|
115
|
+
for (const field of validFields) {
|
|
116
|
+
const relatedModel = models.find((m) => m.name === field.type);
|
|
117
|
+
if (!(relatedModel && isValidImplicitRelation(field, relatedModel))) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!field.relationName || joinTables.has(field.relationName)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const joinTableInfo = createJoinTableInfo(field, model, relatedModel, models);
|
|
124
|
+
if (joinTableInfo) {
|
|
125
|
+
joinTables.set(field.relationName, joinTableInfo);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return Array.from(joinTables.values());
|
|
130
|
+
}
|
|
131
|
+
function shouldProcessField(field, model, models) {
|
|
132
|
+
if (!isImplicitManyToManyField(field)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const relatedModel = models.find((m) => m.name === field.type);
|
|
136
|
+
return !!(relatedModel && model.name !== relatedModel.name);
|
|
137
|
+
}
|
|
138
|
+
function createJoinTableInfo(field, model, relatedModel, models) {
|
|
139
|
+
const modelNames = [model.name, relatedModel.name].sort();
|
|
140
|
+
const modelA = models.find((m) => m.name === modelNames[0]);
|
|
141
|
+
const modelB = models.find((m) => m.name === modelNames[1]);
|
|
142
|
+
if (!(modelA && modelB)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const modelAIdField = getModelIdField(modelA);
|
|
146
|
+
const modelBIdField = getModelIdField(modelB);
|
|
147
|
+
const relationName = field.relationName;
|
|
148
|
+
if (!relationName) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
tableName: `_${relationName}`,
|
|
153
|
+
relationName,
|
|
154
|
+
modelA: modelNames[0],
|
|
155
|
+
modelB: modelNames[1],
|
|
156
|
+
columnAType: modelAIdField.type,
|
|
157
|
+
columnBType: modelBIdField.type,
|
|
158
|
+
columnAIsUuid: isUuidField(modelAIdField),
|
|
159
|
+
columnBIsUuid: isUuidField(modelBIdField),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isValidImplicitRelation(field, relatedModel) {
|
|
163
|
+
const relatedField = relatedModel.fields.find((f) => f.relationName === field.relationName && f.isList === true);
|
|
164
|
+
return !!(relatedField && isImplicitManyToManyField(relatedField));
|
|
165
|
+
}
|