diff --git a/integration/generated/migration.sql b/integration/generated/migration.sql index 50a3170..e4db9e9 100644 --- a/integration/generated/migration.sql +++ b/integration/generated/migration.sql @@ -40,7 +40,11 @@ CREATE TABLE "ArrayTypes" ( "id" TEXT NOT NULL, "strings" TEXT[], "ints" INTEGER[], + "floats" DOUBLE PRECISION[], "bools" BOOLEAN[], + "dateTimes" TIMESTAMP(3)[], + "bigInts" BIGINT[], + "decimals" DECIMAL(65,30)[], "enums" "Role"[], "jsonArray" JSONB[], @@ -232,6 +236,58 @@ CREATE TABLE "NativeTypes" ( CONSTRAINT "NativeTypes_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "PostgresNativeTypes" ( + "id" UUID NOT NULL, + "text" TEXT NOT NULL, + "varchar255" VARCHAR(255) NOT NULL, + "char10" CHAR(10) NOT NULL, + "xml" XML NOT NULL, + "inet" INET NOT NULL, + "bit8" BIT(8) NOT NULL, + "varbit" VARBIT NOT NULL, + "integer" INTEGER NOT NULL, + "smallint" SMALLINT NOT NULL, + "oid" OID NOT NULL, + "bigint" BIGINT NOT NULL, + "doublePrecision" DOUBLE PRECISION NOT NULL, + "real" REAL NOT NULL, + "decimal102" DECIMAL(10,2) NOT NULL, + "money" MONEY NOT NULL, + "timestamp6" TIMESTAMP(6) NOT NULL, + "timestamptz6" TIMESTAMPTZ(6) NOT NULL, + "date" DATE NOT NULL, + "time6" TIME(6) NOT NULL, + "timetz6" TIMETZ(6) NOT NULL, + "json" JSON NOT NULL, + "jsonb" JSONB NOT NULL, + "boolean" BOOLEAN NOT NULL, + + CONSTRAINT "PostgresNativeTypes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TimestampModel" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TimestampModel_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DefaultFunctions" ( + "id" TEXT NOT NULL, + "cuidField" TEXT NOT NULL, + "nowField" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "intDefault" INTEGER NOT NULL DEFAULT 0, + "boolDefault" BOOLEAN NOT NULL DEFAULT false, + "strDefault" TEXT NOT NULL DEFAULT 'default', + + CONSTRAINT "DefaultFunctions_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "ExcludedModel" ( "id" TEXT NOT NULL, diff --git a/integration/generated/zero/schema.ts b/integration/generated/zero/schema.ts index 2871bbd..2c10064 100644 --- a/integration/generated/zero/schema.ts +++ b/integration/generated/zero/schema.ts @@ -28,7 +28,6 @@ export const scalarTypesTable = table('ScalarTypes') json: json(), bigInt: number(), decimal: number(), - bytes: string(), }) .primaryKey('id'); @@ -48,7 +47,11 @@ export const arrayTypesTable = table('ArrayTypes') id: string(), strings: json(), ints: json(), + floats: json(), bools: json(), + dateTimes: json(), + bigInts: json(), + decimals: json(), enums: json(), jsonArray: json(), }) @@ -220,6 +223,55 @@ export const nativeTypesTable = table('NativeTypes') }) .primaryKey('id'); +export const postgresNativeTypesTable = table('PostgresNativeTypes') + .columns({ + id: string(), + text: string(), + varchar255: string(), + char10: string(), + xml: string(), + inet: string(), + bit8: string(), + varbit: string(), + integer: number(), + smallint: number(), + oid: number(), + bigint: number(), + doublePrecision: number(), + real: number(), + decimal102: number(), + money: number(), + timestamp6: number(), + timestamptz6: number(), + date: number(), + time6: number(), + timetz6: number(), + json: json(), + jsonb: json(), + boolean: boolean(), + }) + .primaryKey('id'); + +export const timestampModelTable = table('TimestampModel') + .columns({ + id: string(), + name: string(), + createdAt: number(), + updatedAt: number(), + }) + .primaryKey('id'); + +export const defaultFunctionsTable = table('DefaultFunctions') + .columns({ + id: string(), + cuidField: string(), + nowField: number(), + intDefault: number(), + boolDefault: boolean(), + strDefault: string(), + }) + .primaryKey('id'); + export const minimalModelTable = table('MinimalModel') .columns({ id: string(), @@ -512,6 +564,9 @@ export const schema = createSchema({ memberTable, enumFieldsTable, nativeTypesTable, + postgresNativeTypesTable, + timestampModelTable, + defaultFunctionsTable, minimalModelTable, reservedWordsTable, _articleToTagTable, diff --git a/integration/schema.prisma b/integration/schema.prisma index f2cbe92..62accf3 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -40,7 +40,7 @@ enum Status { /// - Boolean → boolean() /// - DateTime → number() (timestamp) /// - Json → json() -/// - Bytes → string() (fallback) +/// - Bytes → excluded (unsupported currently) model ScalarTypes { id String @id @default(uuid()) str String @@ -69,7 +69,11 @@ model ArrayTypes { id String @id @default(uuid()) strings String[] ints Int[] + floats Float[] bools Boolean[] + dateTimes DateTime[] + bigInts BigInt[] + decimals Decimal[] enums Role[] jsonArray Json[] } @@ -292,6 +296,60 @@ model NativeTypes { jsonb Json @db.JsonB } +/// TEST: Comprehensive PostgreSQL native types → all map to base Zero types +model PostgresNativeTypes { + id String @id @default(uuid()) @db.Uuid + // String native types + text String @db.Text + varchar255 String @db.VarChar(255) + char10 String @db.Char(10) + xml String @db.Xml + inet String @db.Inet + bit8 String @db.Bit(8) + varbit String @db.VarBit + // Integer native types + integer Int @db.Integer + smallint Int @db.SmallInt + oid Int @db.Oid + // BigInt native types + bigint BigInt @db.BigInt + // Float native types + doublePrecision Float @db.DoublePrecision + real Float @db.Real + // Decimal native types + decimal102 Decimal @db.Decimal(10, 2) + money Decimal @db.Money + // DateTime native types + timestamp6 DateTime @db.Timestamp(6) + timestamptz6 DateTime @db.Timestamptz(6) + date DateTime @db.Date + time6 DateTime @db.Time(6) + timetz6 DateTime @db.Timetz(6) + // Json native types + json Json @db.Json + jsonb Json @db.JsonB + // Boolean native type + boolean Boolean @db.Boolean +} + +/// TEST: @updatedAt attribute → DateTime field auto-updated +model TimestampModel { + id String @id @default(uuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +/// TEST: Various @default functions +model DefaultFunctions { + id String @id @default(uuid()) // uuid() default + cuidField String @default(cuid()) // cuid() default + nowField DateTime @default(now()) // now() default + intDefault Int @default(0) // static default + boolDefault Boolean @default(false) // static default + strDefault String @default("default") // static default +} + // ============================================================================ // EXCLUDED MODEL - Tests excludeTables config // ============================================================================ diff --git a/package.json b/package.json index 16b2a6d..58630e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prisma-zero", - "version": "0.1.1", + "version": "0.1.2", "description": "Generate Zero schemas from Prisma ORM schemas", "type": "module", "scripts": { diff --git a/src/mappers/schema-mapper.ts b/src/mappers/schema-mapper.ts index b429b3f..7d24735 100644 --- a/src/mappers/schema-mapper.ts +++ b/src/mappers/schema-mapper.ts @@ -4,6 +4,7 @@ import type { ZeroRelationship, TransformedSchema, Config, + ZeroTypeMapping, } from '../types'; import {mapPrismaTypeToZero} from './type-mapper'; import camelCase from 'camelcase'; @@ -107,6 +108,13 @@ function createImplicitManyToManyModel( const columnAType = mapPrismaTypeToZero(idFieldA); const columnBType = mapPrismaTypeToZero(idFieldB); + if (!columnAType || !columnBType) { + const unsupportedModel = !columnAType ? modelA : modelB; + throw new Error( + `Implicit relation ${relationName ?? 'unknown'}: Model ${unsupportedModel.name} has an unsupported @id field.`, + ); + } + return { tableName, originalTableName, @@ -141,6 +149,20 @@ function mapRelationships( ): Record { const relationships: Record = {}; + const isSupportedField = (target: DMMF.Model, fieldName: string): boolean => { + const field = target.fields.find(f => f.name === fieldName); + if (!field) { + return true; + } + return mapPrismaTypeToZero(field) !== null; + }; + + const areFieldsSupported = ( + target: DMMF.Model, + fieldNames: string[], + ): boolean => + fieldNames.every(fieldName => isSupportedField(target, fieldName)); + model.fields .filter(field => field.relationName) .forEach(field => { @@ -189,18 +211,31 @@ function mapRelationships( : true : model.name === modelA.name; + const sourceField = [model.fields.find(f => f.isId)?.name || 'id']; + const destField = [isModelA ? 'A' : 'B']; + const targetDestField = [ + targetModel.fields.find(f => f.isId)?.name || 'id', + ]; + + if ( + !areFieldsSupported(model, sourceField) || + !areFieldsSupported(targetModel, targetDestField) + ) { + return; + } + // Create a chained relationship through the join table relationships[field.name] = { type: 'many', chain: [ { - sourceField: [model.fields.find(f => f.isId)?.name || 'id'], - destField: [isModelA ? 'A' : 'B'], + sourceField, + destField, destSchema: getZeroTableName(joinTableName), }, { sourceField: [isModelA ? 'B' : 'A'], - destField: [targetModel.fields.find(f => f.isId)?.name || 'id'], + destField: targetDestField, destSchema: getZeroTableName(targetModel.name), }, ], @@ -216,6 +251,13 @@ function mapRelationships( ? ensureStringArray(backReference.relationFromFields) : []; + if ( + !areFieldsSupported(model, sourceFields) || + !areFieldsSupported(targetModel, destFields) + ) { + return; + } + relationships[field.name] = { sourceField: sourceFields, destField: destFields, @@ -240,6 +282,13 @@ function mapRelationships( destFields = ensureStringArray(backReference.relationFromFields); } + if ( + !areFieldsSupported(model, sourceFields) || + !areFieldsSupported(targetModel, destFields) + ) { + return; + } + relationships[field.name] = { sourceField: sourceFields, destField: destFields, @@ -257,12 +306,16 @@ function mapModel( dmmf: DMMF.Document, config: Config, ): ZeroModel { - const columns: Record> = {}; + const columns: Record = {}; model.fields .filter(field => !field.relationName) .forEach(field => { - columns[field.name] = mapPrismaTypeToZero(field); + const mapping = mapPrismaTypeToZero(field); + if (!mapping) { + return; + } + columns[field.name] = mapping; }); const idField = model.fields.find(f => f.isId)?.name; @@ -271,6 +324,20 @@ function mapModel( throw new Error(`No primary key found for ${model.name}`); } + const unsupportedPrimaryKeys = primaryKey.filter(fieldName => { + const field = model.fields.find(f => f.name === fieldName); + if (!field) { + return false; + } + return mapPrismaTypeToZero(field) === null; + }); + + if (unsupportedPrimaryKeys.length > 0) { + throw new Error( + `Primary key field(s) ${unsupportedPrimaryKeys.join(', ')} in ${model.name} are not supported by Zero.`, + ); + } + // Use the Prisma model name (optionally camelCased) for the Zero table name. // If the Prisma model is mapped to a different DB table (@@map) or camelCase // changes the casing, capture the DB table name in originalTableName so we diff --git a/src/mappers/type-mapper.ts b/src/mappers/type-mapper.ts index 581ae60..5ca7a55 100644 --- a/src/mappers/type-mapper.ts +++ b/src/mappers/type-mapper.ts @@ -12,29 +12,34 @@ const TYPE_MAP: Record = { Decimal: 'number()', }; -export function mapPrismaTypeToZero(field: DMMF.Field): ZeroTypeMapping { +const ARRAY_TS_TYPE_MAP: Record = { + String: 'string[]', + Boolean: 'boolean[]', + Int: 'number[]', + Float: 'number[]', + DateTime: 'number[]', + Json: 'any[]', + BigInt: 'number[]', + Decimal: 'number[]', +}; + +export function mapPrismaTypeToZero(field: DMMF.Field): ZeroTypeMapping | null { + if (field.kind === 'unsupported') { + return null; + } + const isOptional = !field.isRequired; const mappedName = field.dbName && field.dbName !== field.name ? field.dbName : null; // Handle array types - map them to json() since Zero doesn't support arrays natively if (field.isList) { - // Map Prisma types to TypeScript types for arrays - const tsTypeMap: Record = { - String: 'string[]', - Boolean: 'boolean[]', - Int: 'number[]', - Float: 'number[]', - DateTime: 'number[]', - Json: 'any[]', - BigInt: 'number[]', - Decimal: 'number[]', - }; - const tsType = - field.kind === 'enum' - ? `${field.type}[]` - : tsTypeMap[field.type] || 'any[]'; + field.kind === 'enum' ? `${field.type}[]` : ARRAY_TS_TYPE_MAP[field.type]; + + if (!tsType) { + return null; + } return { type: `json<${tsType}>()`, @@ -51,7 +56,15 @@ export function mapPrismaTypeToZero(field: DMMF.Field): ZeroTypeMapping { }; } - const baseType = TYPE_MAP[field.type] || 'string()'; + if (field.kind !== 'scalar') { + return null; + } + + const baseType = TYPE_MAP[field.type]; + if (!baseType) { + return null; + } + return { type: baseType, isOptional, diff --git a/tests/schema-mapper.test.ts b/tests/schema-mapper.test.ts index d4908a3..8bd0b36 100644 --- a/tests/schema-mapper.test.ts +++ b/tests/schema-mapper.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from 'vitest'; import {transformSchema} from '../src/mappers/schema-mapper'; import type {Config} from '../src/types'; -import {createField, createMockDMMF, createModel} from './utils'; +import {createEnum, createField, createMockDMMF, createModel} from './utils'; describe('Schema Mapper', () => { const baseConfig: Config = { @@ -11,6 +11,367 @@ describe('Schema Mapper', () => { camelCase: false, }; + describe('scalar types', () => { + it('should map all basic Prisma scalar types correctly', () => { + const model = createModel('ScalarTypes', [ + createField('id', 'String', {isId: true}), + createField('str', 'String'), + createField('int', 'Int'), + createField('float', 'Float'), + createField('bool', 'Boolean'), + createField('dateTime', 'DateTime'), + createField('json', 'Json'), + createField('bigInt', 'BigInt'), + createField('decimal', 'Decimal'), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const scalarModel = result.models[0]; + expect(scalarModel?.columns?.id?.type).toBe('string()'); + expect(scalarModel?.columns?.str?.type).toBe('string()'); + expect(scalarModel?.columns?.int?.type).toBe('number()'); + expect(scalarModel?.columns?.float?.type).toBe('number()'); + expect(scalarModel?.columns?.bool?.type).toBe('boolean()'); + expect(scalarModel?.columns?.dateTime?.type).toBe('number()'); + expect(scalarModel?.columns?.json?.type).toBe('json()'); + expect(scalarModel?.columns?.bigInt?.type).toBe('number()'); + expect(scalarModel?.columns?.decimal?.type).toBe('number()'); + }); + + it('should exclude Bytes fields from the schema (unsupported)', () => { + const model = createModel('BytesModel', [ + createField('id', 'String', {isId: true}), + createField('data', 'Bytes'), + createField('name', 'String'), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const bytesModel = result.models[0]; + expect(bytesModel?.columns).toHaveProperty('id'); + expect(bytesModel?.columns).toHaveProperty('name'); + expect(bytesModel?.columns).not.toHaveProperty('data'); + }); + + it('should handle optional scalar types correctly', () => { + const model = createModel('OptionalTypes', [ + createField('id', 'String', {isId: true}), + createField('str', 'String', {isRequired: false}), + createField('int', 'Int', {isRequired: false}), + createField('float', 'Float', {isRequired: false}), + createField('bool', 'Boolean', {isRequired: false}), + createField('dateTime', 'DateTime', {isRequired: false}), + createField('json', 'Json', {isRequired: false}), + createField('bigInt', 'BigInt', {isRequired: false}), + createField('decimal', 'Decimal', {isRequired: false}), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const optionalModel = result.models[0]; + expect(optionalModel?.columns?.str?.isOptional).toBe(true); + expect(optionalModel?.columns?.int?.isOptional).toBe(true); + expect(optionalModel?.columns?.float?.isOptional).toBe(true); + expect(optionalModel?.columns?.bool?.isOptional).toBe(true); + expect(optionalModel?.columns?.dateTime?.isOptional).toBe(true); + expect(optionalModel?.columns?.json?.isOptional).toBe(true); + expect(optionalModel?.columns?.bigInt?.isOptional).toBe(true); + expect(optionalModel?.columns?.decimal?.isOptional).toBe(true); + }); + }); + + describe('array types', () => { + it('should map all scalar array types to json with type annotations', () => { + const model = createModel('ArrayTypes', [ + createField('id', 'String', {isId: true}), + createField('strings', 'String', {isList: true}), + createField('ints', 'Int', {isList: true}), + createField('floats', 'Float', {isList: true}), + createField('bools', 'Boolean', {isList: true}), + createField('dateTimes', 'DateTime', {isList: true}), + createField('jsons', 'Json', {isList: true}), + createField('bigInts', 'BigInt', {isList: true}), + createField('decimals', 'Decimal', {isList: true}), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const arrayModel = result.models[0]; + expect(arrayModel?.columns?.strings?.type).toBe('json()'); + expect(arrayModel?.columns?.ints?.type).toBe('json()'); + expect(arrayModel?.columns?.floats?.type).toBe('json()'); + expect(arrayModel?.columns?.bools?.type).toBe('json()'); + expect(arrayModel?.columns?.dateTimes?.type).toBe('json()'); + expect(arrayModel?.columns?.jsons?.type).toBe('json()'); + expect(arrayModel?.columns?.bigInts?.type).toBe('json()'); + expect(arrayModel?.columns?.decimals?.type).toBe('json()'); + }); + + it('should handle optional array types', () => { + const model = createModel('OptionalArrays', [ + createField('id', 'String', {isId: true}), + createField('tags', 'String', {isList: true, isRequired: false}), + createField('scores', 'Int', {isList: true, isRequired: false}), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const optArrayModel = result.models[0]; + expect(optArrayModel?.columns?.tags?.type).toBe('json()'); + expect(optArrayModel?.columns?.tags?.isOptional).toBe(true); + expect(optArrayModel?.columns?.scores?.type).toBe('json()'); + expect(optArrayModel?.columns?.scores?.isOptional).toBe(true); + }); + + it('should exclude Bytes[] from the schema (unsupported)', () => { + const model = createModel('BytesArrayModel', [ + createField('id', 'String', {isId: true}), + createField('data', 'Bytes', {isList: true}), + createField('name', 'String'), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const bytesArrayModel = result.models[0]; + expect(bytesArrayModel?.columns).toHaveProperty('id'); + expect(bytesArrayModel?.columns).toHaveProperty('name'); + expect(bytesArrayModel?.columns).not.toHaveProperty('data'); + }); + }); + + describe('enum types', () => { + it('should map enum fields correctly', () => { + const model = createModel('EnumModel', [ + createField('id', 'String', {isId: true}), + createField('role', 'Role', {kind: 'enum'}), + ]); + const roleEnum = createEnum('Role', ['USER', 'ADMIN']); + + const dmmf = createMockDMMF([model], [roleEnum]); + const result = transformSchema(dmmf, baseConfig); + + const enumModel = result.models[0]; + expect(enumModel?.columns?.role?.type).toBe('enumeration()'); + }); + + it('should map optional enum fields correctly', () => { + const model = createModel('OptionalEnumModel', [ + createField('id', 'String', {isId: true}), + createField('status', 'Status', {kind: 'enum', isRequired: false}), + ]); + const statusEnum = createEnum('Status', ['ACTIVE', 'INACTIVE']); + + const dmmf = createMockDMMF([model], [statusEnum]); + const result = transformSchema(dmmf, baseConfig); + + const enumModel = result.models[0]; + expect(enumModel?.columns?.status?.type).toBe('enumeration()'); + expect(enumModel?.columns?.status?.isOptional).toBe(true); + }); + + it('should map enum array fields to json', () => { + const model = createModel('EnumArrayModel', [ + createField('id', 'String', {isId: true}), + createField('roles', 'Role', {kind: 'enum', isList: true}), + ]); + const roleEnum = createEnum('Role', ['USER', 'ADMIN', 'MODERATOR']); + + const dmmf = createMockDMMF([model], [roleEnum]); + const result = transformSchema(dmmf, baseConfig); + + const enumArrayModel = result.models[0]; + expect(enumArrayModel?.columns?.roles?.type).toBe('json()'); + }); + + it('should include enums in the result with correct values', () => { + const model = createModel('EnumModel', [ + createField('id', 'String', {isId: true}), + createField('role', 'Role', {kind: 'enum'}), + ]); + const roleEnum = createEnum('Role', ['USER', 'ADMIN']); + + const dmmf = createMockDMMF([model], [roleEnum]); + const result = transformSchema(dmmf, baseConfig); + + expect(result.enums).toHaveLength(1); + expect(result.enums[0]?.name).toBe('Role'); + expect(result.enums[0]?.values).toEqual([ + {name: 'USER', dbName: null}, + {name: 'ADMIN', dbName: null}, + ]); + }); + + it('should handle enums with @map attributes', () => { + const model = createModel('MappedEnumModel', [ + createField('id', 'String', {isId: true}), + createField('status', 'Status', {kind: 'enum'}), + ]); + // Simulate enum with mapped values + const statusEnum = { + name: 'Status', + dbName: null, + values: [ + {name: 'ACTIVE', dbName: 'active'}, + {name: 'INACTIVE', dbName: 'inactive'}, + ], + }; + + const dmmf = createMockDMMF([model], [statusEnum]); + const result = transformSchema(dmmf, baseConfig); + + expect(result.enums).toHaveLength(1); + expect(result.enums[0]?.values).toEqual([ + {name: 'ACTIVE', dbName: 'active'}, + {name: 'INACTIVE', dbName: 'inactive'}, + ]); + }); + + it('should handle multiple enums', () => { + const model = createModel('MultiEnumModel', [ + createField('id', 'String', {isId: true}), + createField('role', 'Role', {kind: 'enum'}), + createField('status', 'Status', {kind: 'enum'}), + createField('priority', 'Priority', {kind: 'enum'}), + ]); + const roleEnum = createEnum('Role', ['USER', 'ADMIN']); + const statusEnum = createEnum('Status', [ + 'ACTIVE', + 'INACTIVE', + 'PENDING', + ]); + const priorityEnum = createEnum('Priority', ['LOW', 'MEDIUM', 'HIGH']); + + const dmmf = createMockDMMF( + [model], + [roleEnum, statusEnum, priorityEnum], + ); + const result = transformSchema(dmmf, baseConfig); + + expect(result.enums).toHaveLength(3); + expect(result.enums.map(e => e.name).sort()).toEqual([ + 'Priority', + 'Role', + 'Status', + ]); + }); + }); + + describe('native database types', () => { + it('should map PostgreSQL native types to their base Zero types', () => { + const model = createModel('PostgresNativeTypes', [ + createField('id', 'String', {isId: true}), + // String native types + createField('text', 'String'), + createField('varchar', 'String'), + createField('char', 'String'), + createField('uuid', 'String'), + createField('xml', 'String'), + createField('inet', 'String'), + // Int native types + createField('integer', 'Int'), + createField('smallint', 'Int'), + // BigInt native types + createField('bigint', 'BigInt'), + // Float native types + createField('doublePrecision', 'Float'), + createField('real', 'Float'), + // Decimal native types + createField('decimal', 'Decimal'), + createField('money', 'Decimal'), + // DateTime native types + createField('timestamp', 'DateTime'), + createField('timestamptz', 'DateTime'), + createField('date', 'DateTime'), + createField('time', 'DateTime'), + // Json native types + createField('json', 'Json'), + createField('jsonb', 'Json'), + // Boolean native types + createField('boolean', 'Boolean'), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const nativeModel = result.models[0]; + // All String native types map to string() + expect(nativeModel?.columns?.text?.type).toBe('string()'); + expect(nativeModel?.columns?.varchar?.type).toBe('string()'); + expect(nativeModel?.columns?.char?.type).toBe('string()'); + expect(nativeModel?.columns?.uuid?.type).toBe('string()'); + expect(nativeModel?.columns?.xml?.type).toBe('string()'); + expect(nativeModel?.columns?.inet?.type).toBe('string()'); + // All Int native types map to number() + expect(nativeModel?.columns?.integer?.type).toBe('number()'); + expect(nativeModel?.columns?.smallint?.type).toBe('number()'); + // All BigInt native types map to number() + expect(nativeModel?.columns?.bigint?.type).toBe('number()'); + // All Float native types map to number() + expect(nativeModel?.columns?.doublePrecision?.type).toBe('number()'); + expect(nativeModel?.columns?.real?.type).toBe('number()'); + // All Decimal native types map to number() + expect(nativeModel?.columns?.decimal?.type).toBe('number()'); + expect(nativeModel?.columns?.money?.type).toBe('number()'); + // All DateTime native types map to number() + expect(nativeModel?.columns?.timestamp?.type).toBe('number()'); + expect(nativeModel?.columns?.timestamptz?.type).toBe('number()'); + expect(nativeModel?.columns?.date?.type).toBe('number()'); + expect(nativeModel?.columns?.time?.type).toBe('number()'); + // All Json native types map to json() + expect(nativeModel?.columns?.json?.type).toBe('json()'); + expect(nativeModel?.columns?.jsonb?.type).toBe('json()'); + // Boolean native types map to boolean() + expect(nativeModel?.columns?.boolean?.type).toBe('boolean()'); + }); + }); + + describe('@updatedAt attribute', () => { + it('should include fields with @updatedAt attribute', () => { + const model = createModel('TimestampModel', [ + createField('id', 'String', {isId: true}), + createField('createdAt', 'DateTime'), + createField('updatedAt', 'DateTime', {isUpdatedAt: true}), + createField('name', 'String'), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const timestampModel = result.models[0]; + expect(timestampModel?.columns?.createdAt?.type).toBe('number()'); + expect(timestampModel?.columns?.updatedAt?.type).toBe('number()'); + }); + }); + + describe('@default attribute handling', () => { + it('should include fields with various @default values', () => { + const model = createModel('DefaultsModel', [ + createField('id', 'String', {isId: true, hasDefaultValue: true}), + createField('createdAt', 'DateTime', {hasDefaultValue: true}), + createField('isActive', 'Boolean', {hasDefaultValue: true}), + createField('count', 'Int', {hasDefaultValue: true}), + createField('name', 'String', {hasDefaultValue: true}), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const defaultsModel = result.models[0]; + expect(defaultsModel?.columns?.id?.type).toBe('string()'); + expect(defaultsModel?.columns?.createdAt?.type).toBe('number()'); + expect(defaultsModel?.columns?.isActive?.type).toBe('boolean()'); + expect(defaultsModel?.columns?.count?.type).toBe('number()'); + expect(defaultsModel?.columns?.name?.type).toBe('string()'); + }); + }); + describe('excludeTables', () => { it('should exclude specified tables from the schema', () => { const models = [ @@ -513,6 +874,24 @@ describe('Schema Mapper', () => { }); }); + describe('unsupported columns', () => { + it('should skip bytea and unsupported columns', () => { + const model = createModel('Asset', [ + createField('id', 'String', {isId: true}), + createField('payload', 'Bytes'), + createField('legacy', 'LegacyType', {kind: 'unsupported'}), + ]); + + const dmmf = createMockDMMF([model]); + const result = transformSchema(dmmf, baseConfig); + + const assetModel = result.models[0]; + expect(assetModel?.columns).toHaveProperty('id'); + expect(assetModel?.columns).not.toHaveProperty('payload'); + expect(assetModel?.columns).not.toHaveProperty('legacy'); + }); + }); + describe('Error handling', () => { it('throws when a model has no primary key', () => { const model = createModel('NoPK', [createField('name', 'String')]); diff --git a/tests/type-mapper.test.ts b/tests/type-mapper.test.ts index f8f7344..9d4a99d 100644 --- a/tests/type-mapper.test.ts +++ b/tests/type-mapper.test.ts @@ -1,231 +1,494 @@ -import type {DMMF} from '@prisma/generator-helper'; import {describe, expect, it} from 'vitest'; import {mapPrismaTypeToZero} from '../src/mappers/type-mapper'; +import {createField} from './utils'; describe('mapPrismaTypeToZero', () => { - it('should map basic scalar types correctly', () => { - const testCases: Array<[DMMF.Field, string]> = [ - [ - {type: 'String', kind: 'scalar', isRequired: true} as DMMF.Field, - 'string()', - ], - [ - {type: 'Boolean', kind: 'scalar', isRequired: true} as DMMF.Field, - 'boolean()', - ], - [ - {type: 'Int', kind: 'scalar', isRequired: true} as DMMF.Field, - 'number()', - ], - [ - {type: 'Float', kind: 'scalar', isRequired: true} as DMMF.Field, - 'number()', - ], - [ - {type: 'DateTime', kind: 'scalar', isRequired: true} as DMMF.Field, - 'number()', - ], - [ - {type: 'Json', kind: 'scalar', isRequired: true} as DMMF.Field, - 'json()', - ], - [ - {type: 'BigInt', kind: 'scalar', isRequired: true} as DMMF.Field, - 'number()', - ], - [ - {type: 'Decimal', kind: 'scalar', isRequired: true} as DMMF.Field, - 'number()', - ], - ]; - - testCases.forEach(([field, expectedType]) => { - const result = mapPrismaTypeToZero(field); - expect(result.type).toBe(expectedType); - expect(result.isOptional).toBe(false); + describe('scalar types', () => { + it('should map String to string()', () => { + const field = createField('str', 'String'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + expect(result?.isOptional).toBe(false); }); - }); - it('should map enum types correctly', () => { - const enumField: DMMF.Field = { - type: 'UserRole', - kind: 'enum', - isRequired: true, - } as DMMF.Field; + it('should map Boolean to boolean()', () => { + const field = createField('bool', 'Boolean'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('boolean()'); + expect(result?.isOptional).toBe(false); + }); - const result = mapPrismaTypeToZero(enumField); - expect(result.type).toBe('enumeration()'); - expect(result.isOptional).toBe(false); - }); + it('should map Int to number()', () => { + const field = createField('int', 'Int'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Float to number()', () => { + const field = createField('float', 'Float'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map BigInt to number()', () => { + const field = createField('bigInt', 'BigInt'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Decimal to number()', () => { + const field = createField('decimal', 'Decimal'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map DateTime to number() (timestamp)', () => { + const field = createField('dateTime', 'DateTime'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Json to json()', () => { + const field = createField('json', 'Json'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should return null for Bytes (unsupported in Zero)', () => { + const field = createField('bytes', 'Bytes'); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); - it('should handle optional fields correctly', () => { - const optionalString: DMMF.Field = { - type: 'String', - kind: 'scalar', - isRequired: false, - } as DMMF.Field; - - const optionalEnum: DMMF.Field = { - type: 'UserRole', - kind: 'enum', - isRequired: false, - } as DMMF.Field; - - const stringResult = mapPrismaTypeToZero(optionalString); - expect(stringResult.type).toBe('string()'); - expect(stringResult.isOptional).toBe(true); - - const enumResult = mapPrismaTypeToZero(optionalEnum); - expect(enumResult.type).toBe('enumeration()'); - expect(enumResult.isOptional).toBe(true); + it('should return null for unknown scalar types', () => { + const field = createField('unknown', 'UnknownType'); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); }); - it('should default to string() for unknown types', () => { - const unknownType: DMMF.Field = { - type: 'UnknownType', - kind: 'scalar', - isRequired: true, - } as DMMF.Field; + describe('PostgreSQL native database types', () => { + // String native types + it('should map String with @db.Text to string()', () => { + const field = createField('text', 'String', {nativeType: ['Text', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.VarChar(n) to string()', () => { + const field = createField('varchar', 'String', { + nativeType: ['VarChar', ['255']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Char(n) to string()', () => { + const field = createField('char', 'String', { + nativeType: ['Char', ['10']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Uuid to string()', () => { + const field = createField('uuid', 'String', {nativeType: ['Uuid', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Xml to string()', () => { + const field = createField('xml', 'String', {nativeType: ['Xml', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Inet to string()', () => { + const field = createField('inet', 'String', {nativeType: ['Inet', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Citext to string()', () => { + const field = createField('citext', 'String', { + nativeType: ['Citext', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.Bit(n) to string()', () => { + const field = createField('bit', 'String', {nativeType: ['Bit', ['8']]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + it('should map String with @db.VarBit to string()', () => { + const field = createField('varbit', 'String', { + nativeType: ['VarBit', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + }); + + // Integer native types + it('should map Int with @db.Integer to number()', () => { + const field = createField('integer', 'Int', { + nativeType: ['Integer', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map Int with @db.SmallInt to number()', () => { + const field = createField('smallint', 'Int', { + nativeType: ['SmallInt', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map Int with @db.Oid to number()', () => { + const field = createField('oid', 'Int', {nativeType: ['Oid', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + // BigInt native types + it('should map BigInt with @db.BigInt to number()', () => { + const field = createField('bigint', 'BigInt', { + nativeType: ['BigInt', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + // Float native types + it('should map Float with @db.DoublePrecision to number()', () => { + const field = createField('doublePrecision', 'Float', { + nativeType: ['DoublePrecision', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map Float with @db.Real to number()', () => { + const field = createField('real', 'Float', {nativeType: ['Real', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + // Decimal native types + it('should map Decimal with @db.Decimal(p,s) to number()', () => { + const field = createField('decimal', 'Decimal', { + nativeType: ['Decimal', ['10', '2']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map Decimal with @db.Money to number()', () => { + const field = createField('money', 'Decimal', { + nativeType: ['Money', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + // DateTime native types + it('should map DateTime with @db.Timestamp(n) to number()', () => { + const field = createField('timestamp', 'DateTime', { + nativeType: ['Timestamp', ['6']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map DateTime with @db.Timestamptz(n) to number()', () => { + const field = createField('timestamptz', 'DateTime', { + nativeType: ['Timestamptz', ['6']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map DateTime with @db.Date to number()', () => { + const field = createField('date', 'DateTime', {nativeType: ['Date', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map DateTime with @db.Time(n) to number()', () => { + const field = createField('time', 'DateTime', { + nativeType: ['Time', ['6']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + it('should map DateTime with @db.Timetz(n) to number()', () => { + const field = createField('timetz', 'DateTime', { + nativeType: ['Timetz', ['6']], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + }); + + // Json native types + it('should map Json with @db.Json to json()', () => { + const field = createField('json', 'Json', {nativeType: ['Json', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + }); + + it('should map Json with @db.JsonB to json()', () => { + const field = createField('jsonb', 'Json', {nativeType: ['JsonB', []]}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + }); + + // Boolean native types + it('should map Boolean with @db.Boolean to boolean()', () => { + const field = createField('boolean', 'Boolean', { + nativeType: ['Boolean', []], + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('boolean()'); + }); - const result = mapPrismaTypeToZero(unknownType); - expect(result.type).toBe('string()'); - expect(result.isOptional).toBe(false); + // Bytes native types + it('should return null for Bytes with @db.ByteA', () => { + const field = createField('bytea', 'Bytes', {nativeType: ['ByteA', []]}); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); }); - it('should map array types to json with proper type annotations', () => { - const testCases: Array<[DMMF.Field, string]> = [ - [ - { - type: 'String', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'Boolean', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'Int', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'Float', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'DateTime', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'Json', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'BigInt', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - [ - { - type: 'Decimal', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field, - 'json()', - ], - ]; - - testCases.forEach(([field, expectedType]) => { - const result = mapPrismaTypeToZero(field); - expect(result.type).toBe(expectedType); - expect(result.isOptional).toBe(false); + describe('enum types', () => { + it('should map enum types correctly', () => { + const field = createField('role', 'UserRole', {kind: 'enum'}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('enumeration()'); + expect(result?.isOptional).toBe(false); + }); + + it('should handle optional enum fields', () => { + const field = createField('role', 'UserRole', { + kind: 'enum', + isRequired: false, + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('enumeration()'); + expect(result?.isOptional).toBe(true); + }); + + it('should map enum array types correctly', () => { + const field = createField('roles', 'UserRole', { + kind: 'enum', + isList: true, + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); }); }); - it('should handle optional array fields correctly', () => { - const optionalArray: DMMF.Field = { - type: 'String', - kind: 'scalar', - isRequired: false, - isList: true, - } as DMMF.Field; - - const result = mapPrismaTypeToZero(optionalArray); - expect(result.type).toBe('json()'); - expect(result.isOptional).toBe(true); + describe('optional fields', () => { + it('should handle optional String fields correctly', () => { + const field = createField('str', 'String', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional Int fields correctly', () => { + const field = createField('int', 'Int', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional Boolean fields correctly', () => { + const field = createField('bool', 'Boolean', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('boolean()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional DateTime fields correctly', () => { + const field = createField('dateTime', 'DateTime', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional Json fields correctly', () => { + const field = createField('json', 'Json', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional BigInt fields correctly', () => { + const field = createField('bigInt', 'BigInt', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional Decimal fields correctly', () => { + const field = createField('decimal', 'Decimal', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(true); + }); + + it('should handle optional Float fields correctly', () => { + const field = createField('float', 'Float', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('number()'); + expect(result?.isOptional).toBe(true); + }); + + it('should return null for optional Bytes fields', () => { + const field = createField('bytes', 'Bytes', {isRequired: false}); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); }); - it('should map enum array types correctly', () => { - const enumArrayField: DMMF.Field = { - type: 'UserRole', - kind: 'enum', - isRequired: true, - isList: true, - } as DMMF.Field; - - const result = mapPrismaTypeToZero(enumArrayField); - expect(result.type).toBe('json()'); - expect(result.isOptional).toBe(false); + describe('array types', () => { + it('should map String[] to json()', () => { + const field = createField('strings', 'String', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Boolean[] to json()', () => { + const field = createField('bools', 'Boolean', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Int[] to json()', () => { + const field = createField('ints', 'Int', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Float[] to json()', () => { + const field = createField('floats', 'Float', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map DateTime[] to json()', () => { + const field = createField('dateTimes', 'DateTime', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Json[] to json()', () => { + const field = createField('jsons', 'Json', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map BigInt[] to json()', () => { + const field = createField('bigInts', 'BigInt', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should map Decimal[] to json()', () => { + const field = createField('decimals', 'Decimal', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(false); + }); + + it('should return null for Bytes[] (unsupported)', () => { + const field = createField('bytesArray', 'Bytes', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); + + it('should handle optional array fields correctly', () => { + const field = createField('tags', 'String', { + isList: true, + isRequired: false, + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.isOptional).toBe(true); + }); + + it('should return null for unsupported array types', () => { + const field = createField('unknownArray', 'UnknownType', {isList: true}); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); }); - it('should handle unknown array types', () => { - const unknownArrayType: DMMF.Field = { - type: 'UnknownType', - kind: 'scalar', - isRequired: true, - isList: true, - } as DMMF.Field; - - const result = mapPrismaTypeToZero(unknownArrayType); - expect(result.type).toBe('json()'); - expect(result.isOptional).toBe(false); + describe('field mapping', () => { + it('should handle mapped field names with arrays', () => { + const field = createField('userTags', 'String', { + isList: true, + dbName: 'user_tags', + }); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('json()'); + expect(result?.mappedName).toBe('user_tags'); + }); + + it('should handle mapped field names with scalars', () => { + const field = createField('firstName', 'String', {dbName: 'first_name'}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + expect(result?.mappedName).toBe('first_name'); + }); + + it('should return null mappedName when dbName matches name', () => { + const field = createField('email', 'String', {dbName: 'email'}); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + expect(result?.mappedName).toBeNull(); + }); + + it('should return null mappedName when dbName is undefined', () => { + const field = createField('email', 'String'); + const result = mapPrismaTypeToZero(field); + expect(result?.type).toBe('string()'); + expect(result?.mappedName).toBeNull(); + }); }); - it('should handle mapped field names with arrays', () => { - const arrayFieldWithMapping: DMMF.Field = { - type: 'String', - kind: 'scalar', - isRequired: true, - isList: true, - name: 'userTags', - dbName: 'user_tags', - } as DMMF.Field; - - const result = mapPrismaTypeToZero(arrayFieldWithMapping); - expect(result.type).toBe('json()'); - expect(result.mappedName).toBe('user_tags'); + describe('unsupported types', () => { + it('should return null for unsupported kind', () => { + const field = createField('unsupported', 'SomeUnsupportedType', { + kind: 'unsupported', + }); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); + + it('should return null for object kind (relations)', () => { + const field = createField('user', 'User', {kind: 'object'}); + const result = mapPrismaTypeToZero(field); + expect(result).toBeNull(); + }); }); });