@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.
Files changed (58) hide show
  1. package/README.md +33 -38
  2. package/dist/src/cli.d.ts +1 -1
  3. package/dist/src/cli.js +15 -14
  4. package/dist/src/commands/init.d.ts +1 -1
  5. package/dist/src/commands/init.js +36 -47
  6. package/dist/src/commands/prisma.d.ts +4 -4
  7. package/dist/src/commands/prisma.js +12 -12
  8. package/dist/src/generators/sql-schema-generator.js +15 -16
  9. package/dist/src/runtime/index.d.ts +303 -0
  10. package/dist/src/runtime/index.js +216 -0
  11. package/dist/src/schema-generator/effect/enum.d.ts +12 -0
  12. package/dist/src/schema-generator/effect/enum.js +18 -0
  13. package/dist/src/schema-generator/effect/generator.d.ts +16 -0
  14. package/dist/src/schema-generator/effect/generator.js +42 -0
  15. package/dist/src/schema-generator/effect/join-table.d.ts +12 -0
  16. package/dist/src/schema-generator/effect/join-table.js +28 -0
  17. package/dist/src/schema-generator/effect/type.d.ts +18 -0
  18. package/dist/src/schema-generator/effect/type.js +82 -0
  19. package/dist/src/schema-generator/index.d.ts +11 -0
  20. package/dist/src/schema-generator/index.js +83 -0
  21. package/dist/src/schema-generator/kysely/generator.d.ts +11 -0
  22. package/dist/src/schema-generator/kysely/generator.js +7 -0
  23. package/dist/src/schema-generator/kysely/type.d.ts +14 -0
  24. package/dist/src/schema-generator/kysely/type.js +44 -0
  25. package/dist/src/schema-generator/prisma/enum.d.ts +19 -0
  26. package/dist/src/schema-generator/prisma/enum.js +19 -0
  27. package/dist/src/schema-generator/prisma/generator.d.ts +53 -0
  28. package/dist/src/schema-generator/prisma/generator.js +29 -0
  29. package/dist/src/schema-generator/prisma/relation.d.ts +83 -0
  30. package/dist/src/schema-generator/prisma/relation.js +165 -0
  31. package/dist/src/schema-generator/prisma/type.d.ts +108 -0
  32. package/dist/src/schema-generator/prisma/type.js +85 -0
  33. package/dist/src/schema-generator/utils/annotations.d.ts +32 -0
  34. package/dist/src/schema-generator/utils/annotations.js +79 -0
  35. package/dist/src/schema-generator/utils/codegen.d.ts +9 -0
  36. package/dist/src/schema-generator/utils/codegen.js +14 -0
  37. package/dist/src/schema-generator/utils/naming.d.ts +29 -0
  38. package/dist/src/schema-generator/utils/naming.js +68 -0
  39. package/dist/src/schema-generator/utils/type-mappings.d.ts +62 -0
  40. package/dist/src/schema-generator/utils/type-mappings.js +70 -0
  41. package/dist/src/services/formatter-service.d.ts +10 -0
  42. package/dist/src/services/formatter-service.js +18 -0
  43. package/dist/src/services/generator-context.d.ts +2 -2
  44. package/dist/src/services/generator-context.js +2 -2
  45. package/dist/src/services/generator-service.d.ts +9 -8
  46. package/dist/src/services/generator-service.js +39 -43
  47. package/dist/src/services/render-service.d.ts +3 -3
  48. package/dist/src/services/render-service.js +8 -8
  49. package/dist/src/templates/effect-branded-id.eta +2 -0
  50. package/dist/src/templates/effect-enums.eta +9 -0
  51. package/dist/src/templates/effect-index.eta +4 -0
  52. package/dist/src/templates/effect-join-table.eta +8 -0
  53. package/dist/src/templates/effect-model.eta +6 -0
  54. package/dist/src/templates/effect-types-header.eta +7 -0
  55. package/dist/src/templates/index-default.eta +2 -1
  56. package/dist/src/templates/kysely-db-interface.eta +6 -0
  57. package/dist/src/templates/prisma-repository.eta +57 -32
  58. 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
+ }