import type { PrismaAdapter, PrismaSchema, PrismaModel, PrismaField, PrismaEnum, EnumValue, Datasource, Generator, UniqueConstraint, Index, CompositePrimaryKey, DefaultValue, } from '../types'; export class Prisma5Adapter implements PrismaAdapter { async parse(schemaContent: string): Promise { const models = this.parseModels(schemaContent); const enums = this.parseEnums(schemaContent); const datasource = this.parseDatasource(schemaContent); const generators = this.parseGenerators(schemaContent); return { version: 5, models, enums, datasource, generators, }; } private parseModels(schema: string): PrismaModel[] { const models: PrismaModel[] = []; const modelRegex = /(?:\/\/\/\s*(.+?)\n)?model\s+(\w+)\s*\{([^}]+)\}/gs; let match; while ((match = modelRegex.exec(schema)) !== null) { const documentation = match[1]?.trim(); const name = match[2]; const body = match[3]; const model: PrismaModel = { name, fields: this.parseFields(body), uniqueConstraints: this.parseUniqueConstraints(body), indexes: this.parseIndexes(body), documentation, }; // Parse @@map for dbName const mapMatch = body.match(/@@map\("([^"]+)"\)/); if (mapMatch) { model.dbName = mapMatch[1]; } // Parse @@id for composite primary key const idMatch = body.match(/@@id\(\[([^\]]+)\]\)/); if (idMatch) { model.primaryKey = { fields: idMatch[1].split(',').map((f) => f.trim()), }; } models.push(model); } return models; } private parseFields(body: string): PrismaField[] { const fields: PrismaField[] = []; const lines = body.split('\n'); let currentDoc = ''; for (const line of lines) { const trimmedLine = line.trim(); // Capture documentation if (trimmedLine.startsWith('///')) { currentDoc = trimmedLine.replace('///', '').trim(); continue; } // Skip empty lines and block attributes if (!trimmedLine || trimmedLine.startsWith('@@')) { currentDoc = ''; continue; } // Parse field const fieldMatch = trimmedLine.match(/^(\w+)\s+(\w+)(\[\])?\??(.*)$/); if (fieldMatch) { const [, fieldName, fieldType, isList, attributes] = fieldMatch; const field: PrismaField = { name: fieldName, type: fieldType, kind: this.getFieldKind(fieldType, attributes), isList: !!isList, isRequired: !trimmedLine.includes('?') || trimmedLine.includes('@id'), isUnique: attributes.includes('@unique'), isId: attributes.includes('@id'), isUpdatedAt: attributes.includes('@updatedAt'), hasDefaultValue: attributes.includes('@default'), documentation: currentDoc || undefined, }; // Parse @default value if (field.hasDefaultValue) { field.default = this.parseDefaultValue(attributes); } // Parse @relation if (attributes.includes('@relation')) { this.parseRelation(field, attributes); } fields.push(field); currentDoc = ''; } } return fields; } private getFieldKind( type: string, attributes: string ): 'scalar' | 'object' | 'enum' | 'unsupported' { const scalarTypes = [ 'String', 'Int', 'Float', 'Boolean', 'DateTime', 'Json', 'Bytes', 'BigInt', 'Decimal', ]; if (attributes.includes('@relation')) { return 'object'; } if (scalarTypes.includes(type)) { return 'scalar'; } if (attributes.includes('Unsupported')) { return 'unsupported'; } // If it's not a scalar and not a relation, it's likely an enum return 'enum'; } private parseDefaultValue(attributes: string): DefaultValue | undefined { const defaultMatch = attributes.match(/@default\(([^)]+)\)/); if (!defaultMatch) return undefined; const value = defaultMatch[1].trim(); // Function call (e.g., autoincrement(), now(), uuid()) if (value.includes('(')) { const funcMatch = value.match(/(\w+)\(([^)]*)\)/); if (funcMatch) { return { name: funcMatch[1], args: funcMatch[2] ? [funcMatch[2]] : [], }; } } // Boolean if (value === 'true') return true; if (value === 'false') return false; // Number if (/^-?\d+(\.\d+)?$/.test(value)) { return parseFloat(value); } // String if (value.startsWith('"') && value.endsWith('"')) { return value.slice(1, -1); } return value; } private parseRelation(field: PrismaField, attributes: string): void { // Parse relation name const nameMatch = attributes.match(/@relation\("([^"]+)"/); if (nameMatch) { field.relationName = nameMatch[1]; } // Parse fields const fieldsMatch = attributes.match(/fields:\s*\[([^\]]+)\]/); if (fieldsMatch) { field.relationFromFields = fieldsMatch[1].split(',').map((f) => f.trim()); } // Parse references const refsMatch = attributes.match(/references:\s*\[([^\]]+)\]/); if (refsMatch) { field.relationToFields = refsMatch[1].split(',').map((f) => f.trim()); } } private parseUniqueConstraints(body: string): UniqueConstraint[] { const constraints: UniqueConstraint[] = []; const regex = /@@unique\(\[([^\]]+)\](?:,\s*name:\s*"([^"]+)")?\)/g; let match; while ((match = regex.exec(body)) !== null) { constraints.push({ fields: match[1].split(',').map((f) => f.trim()), name: match[2], }); } return constraints; } private parseIndexes(body: string): Index[] { const indexes: Index[] = []; const regex = /@@index\(\[([^\]]+)\](?:,\s*name:\s*"([^"]+)")?\)/g; let match; while ((match = regex.exec(body)) !== null) { indexes.push({ fields: match[1].split(',').map((f) => f.trim()), name: match[2], }); } return indexes; } private parseEnums(schema: string): PrismaEnum[] { const enums: PrismaEnum[] = []; const enumRegex = /(?:\/\/\/\s*(.+?)\n)?enum\s+(\w+)\s*\{([^}]+)\}/gs; let match; while ((match = enumRegex.exec(schema)) !== null) { const documentation = match[1]?.trim(); const name = match[2]; const body = match[3]; const values = this.parseEnumValues(body); enums.push({ name, values, documentation, }); } return enums; } private parseEnumValues(body: string): EnumValue[] { const values: EnumValue[] = []; const lines = body.split('\n'); let currentDoc = ''; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('///')) { currentDoc = trimmedLine.replace('///', '').trim(); continue; } if (!trimmedLine) { currentDoc = ''; continue; } const valueMatch = trimmedLine.match(/^(\w+)(?:\s+@map\("([^"]+)"\))?/); if (valueMatch) { values.push({ name: valueMatch[1], dbName: valueMatch[2], documentation: currentDoc || undefined, }); currentDoc = ''; } } return values; } private parseDatasource(schema: string): Datasource { const dsMatch = schema.match(/datasource\s+(\w+)\s*\{([^}]+)\}/s); if (!dsMatch) { return { name: 'db', provider: 'postgresql', url: 'env("DATABASE_URL")', }; } const name = dsMatch[1]; const body = dsMatch[2]; const providerMatch = body.match(/provider\s*=\s*"([^"]+)"/); const urlMatch = body.match(/url\s*=\s*(?:env\("([^"]+)"\)|"([^"]+)")/); const directUrlMatch = body.match(/directUrl\s*=\s*(?:env\("([^"]+)"\)|"([^"]+)")/); return { name, provider: providerMatch?.[1] || 'postgresql', url: urlMatch?.[1] ? `env("${urlMatch[1]}")` : urlMatch?.[2] || '', directUrl: directUrlMatch?.[1] ? `env("${directUrlMatch[1]}")` : directUrlMatch?.[2], }; } private parseGenerators(schema: string): Generator[] { const generators: Generator[] = []; const genRegex = /generator\s+(\w+)\s*\{([^}]+)\}/gs; let match; while ((match = genRegex.exec(schema)) !== null) { const name = match[1]; const body = match[2]; const providerMatch = body.match(/provider\s*=\s*"([^"]+)"/); const outputMatch = body.match(/output\s*=\s*"([^"]+)"/); const previewMatch = body.match(/previewFeatures\s*=\s*\[([^\]]+)\]/); const binaryMatch = body.match(/binaryTargets\s*=\s*\[([^\]]+)\]/); generators.push({ name, provider: providerMatch?.[1] || 'prisma-client-js', output: outputMatch?.[1], previewFeatures: previewMatch?.[1]?.split(',').map((f) => f.trim().replace(/"/g, '')), binaryTargets: binaryMatch?.[1]?.split(',').map((t) => t.trim().replace(/"/g, '')), }); } return generators; } }