Initial commit
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# @prisma-uml/parser
|
||||
|
||||
Internal package for parsing Prisma schemas.
|
||||
|
||||
## Features
|
||||
|
||||
- Parse Prisma schemas (v5, v6, v7)
|
||||
- Auto-detect Prisma version
|
||||
- Export to Mermaid, PlantUML, DBML
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { parsePrismaSchema, toMermaid } from '@prisma-uml/parser';
|
||||
|
||||
const parsed = await parsePrismaSchema(schemaString);
|
||||
const mermaid = toMermaid(parsed);
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@prisma-uml/parser",
|
||||
"version": "0.1.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./renderers/mermaid": {
|
||||
"types": "./dist/renderers/mermaid.d.ts",
|
||||
"import": "./dist/renderers/mermaid.mjs",
|
||||
"require": "./dist/renderers/mermaid.js"
|
||||
},
|
||||
"./renderers/plantuml": {
|
||||
"types": "./dist/renderers/plantuml.d.ts",
|
||||
"import": "./dist/renderers/plantuml.mjs",
|
||||
"require": "./dist/renderers/plantuml.js"
|
||||
},
|
||||
"./renderers/dbml": {
|
||||
"types": "./dist/renderers/dbml.d.ts",
|
||||
"import": "./dist/renderers/dbml.mjs",
|
||||
"require": "./dist/renderers/dbml.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@prisma/internals": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@prisma/internals": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/internals": "^6.3.0",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parsePrismaSchema, detectPrismaVersion, validatePrismaSchema } from '../index';
|
||||
|
||||
describe('Prisma Schema Parser', () => {
|
||||
it('should parse a basic Prisma 5 schema', async () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await parsePrismaSchema(schema);
|
||||
|
||||
expect(result.version).toBe(5);
|
||||
expect(result.models).toHaveLength(1);
|
||||
expect(result.models[0].name).toBe('User');
|
||||
expect(result.models[0].fields).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should detect Prisma 6 features', () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
password String @omit
|
||||
}
|
||||
`;
|
||||
|
||||
expect(detectPrismaVersion(schema)).toBe(6);
|
||||
});
|
||||
|
||||
it('should detect Prisma 7 features', () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
}
|
||||
`;
|
||||
|
||||
expect(detectPrismaVersion(schema)).toBe(7);
|
||||
});
|
||||
|
||||
it('should handle relations correctly', async () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
posts Post[]
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId Int
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await parsePrismaSchema(schema);
|
||||
|
||||
const userModel = result.models.find((m) => m.name === 'User');
|
||||
const postModel = result.models.find((m) => m.name === 'Post');
|
||||
|
||||
expect(userModel?.fields.find((f) => f.name === 'posts')?.kind).toBe('object');
|
||||
expect(postModel?.fields.find((f) => f.name === 'author')?.relationFromFields).toEqual([
|
||||
'authorId',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse enums correctly', async () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
USER
|
||||
GUEST
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
role Role @default(USER)
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await parsePrismaSchema(schema);
|
||||
|
||||
expect(result.enums).toHaveLength(1);
|
||||
expect(result.enums[0].name).toBe('Role');
|
||||
expect(result.enums[0].values).toHaveLength(3);
|
||||
expect(result.enums[0].values.map((v) => v.name)).toEqual(['ADMIN', 'USER', 'GUEST']);
|
||||
});
|
||||
|
||||
it('should validate schema correctly', () => {
|
||||
const validSchema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
}
|
||||
`;
|
||||
|
||||
const invalidSchema = `
|
||||
model User {
|
||||
id Int @id
|
||||
`;
|
||||
|
||||
expect(validatePrismaSchema(validSchema).valid).toBe(true);
|
||||
expect(validatePrismaSchema(invalidSchema).valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse composite primary keys', async () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model PostTag {
|
||||
postId Int
|
||||
tagId Int
|
||||
|
||||
@@id([postId, tagId])
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await parsePrismaSchema(schema);
|
||||
const model = result.models[0];
|
||||
|
||||
expect(model.primaryKey).toBeDefined();
|
||||
expect(model.primaryKey?.fields).toEqual(['postId', 'tagId']);
|
||||
});
|
||||
|
||||
it('should parse indexes and unique constraints', async () => {
|
||||
const schema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
email String
|
||||
firstName String
|
||||
lastName String
|
||||
|
||||
@@unique([email])
|
||||
@@index([firstName, lastName])
|
||||
}
|
||||
`;
|
||||
|
||||
const result = await parsePrismaSchema(schema);
|
||||
const model = result.models[0];
|
||||
|
||||
expect(model.uniqueConstraints).toHaveLength(1);
|
||||
expect(model.uniqueConstraints[0].fields).toEqual(['email']);
|
||||
expect(model.indexes).toHaveLength(1);
|
||||
expect(model.indexes[0].fields).toEqual(['firstName', 'lastName']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parsePrismaSchema, toMermaid, toPlantUML, toDBML } from '../index';
|
||||
|
||||
const testSchema = `
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
USER
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
role Role @default(USER)
|
||||
posts Post[]
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String?
|
||||
published Boolean @default(false)
|
||||
author User @relation(fields: [authorId], references: [id])
|
||||
authorId Int
|
||||
}
|
||||
`;
|
||||
|
||||
describe('Renderers', () => {
|
||||
describe('Mermaid Renderer', () => {
|
||||
it('should generate valid Mermaid ER diagram', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const mermaid = toMermaid(schema);
|
||||
|
||||
expect(mermaid).toContain('erDiagram');
|
||||
expect(mermaid).toContain('User {');
|
||||
expect(mermaid).toContain('Post {');
|
||||
expect(mermaid).toContain('int id "PK"');
|
||||
expect(mermaid).toContain('string email "UK"');
|
||||
});
|
||||
|
||||
it('should include relations', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const mermaid = toMermaid(schema);
|
||||
|
||||
expect(mermaid).toMatch(/User.*Post/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlantUML Renderer', () => {
|
||||
it('should generate valid PlantUML diagram', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const plantuml = toPlantUML(schema);
|
||||
|
||||
expect(plantuml).toContain('@startuml');
|
||||
expect(plantuml).toContain('@enduml');
|
||||
expect(plantuml).toContain('entity');
|
||||
expect(plantuml).toContain('User');
|
||||
expect(plantuml).toContain('Post');
|
||||
});
|
||||
|
||||
it('should include enums', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const plantuml = toPlantUML(schema);
|
||||
|
||||
expect(plantuml).toContain('enum Role');
|
||||
expect(plantuml).toContain('ADMIN');
|
||||
expect(plantuml).toContain('USER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DBML Renderer', () => {
|
||||
it('should generate valid DBML', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const dbml = toDBML(schema);
|
||||
|
||||
expect(dbml).toContain('Project prisma_schema');
|
||||
expect(dbml).toContain('Table User');
|
||||
expect(dbml).toContain('Table Post');
|
||||
expect(dbml).toContain('Ref:');
|
||||
});
|
||||
|
||||
it('should include field constraints', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const dbml = toDBML(schema);
|
||||
|
||||
expect(dbml).toContain('pk');
|
||||
expect(dbml).toContain('unique');
|
||||
});
|
||||
|
||||
it('should include enums', async () => {
|
||||
const schema = await parsePrismaSchema(testSchema);
|
||||
const dbml = toDBML(schema);
|
||||
|
||||
expect(dbml).toContain('Enum Role');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Prisma5Adapter } from './prisma5';
|
||||
export { Prisma6Adapter } from './prisma6';
|
||||
export { Prisma7Adapter } from './prisma7';
|
||||
@@ -0,0 +1,349 @@
|
||||
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<PrismaSchema> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { PrismaAdapter, PrismaSchema } from '../types';
|
||||
import { Prisma5Adapter } from './prisma5';
|
||||
|
||||
export class Prisma6Adapter extends Prisma5Adapter implements PrismaAdapter {
|
||||
async parse(schemaContent: string): Promise<PrismaSchema> {
|
||||
const baseSchema = await super.parse(schemaContent);
|
||||
|
||||
baseSchema.version = 6;
|
||||
|
||||
this.parsePrisma6Features(baseSchema, schemaContent);
|
||||
|
||||
return baseSchema;
|
||||
}
|
||||
|
||||
private parsePrisma6Features(schema: PrismaSchema, schemaContent: string): void {
|
||||
for (const model of schema.models) {
|
||||
for (const field of model.fields) {
|
||||
const fieldPattern = new RegExp(`${field.name}\\s+${field.type}[^\\n]*@omit`, 'g');
|
||||
if (fieldPattern.test(schemaContent)) {
|
||||
field.isOmitted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { PrismaAdapter, PrismaSchema } from '../types';
|
||||
import { Prisma6Adapter } from './prisma6';
|
||||
|
||||
export class Prisma7Adapter extends Prisma6Adapter implements PrismaAdapter {
|
||||
async parse(schemaContent: string): Promise<PrismaSchema> {
|
||||
const baseSchema = await super.parse(schemaContent);
|
||||
|
||||
baseSchema.version = 7;
|
||||
|
||||
this.parsePrisma7Features(baseSchema, schemaContent);
|
||||
|
||||
return baseSchema;
|
||||
}
|
||||
|
||||
private parsePrisma7Features(schema: PrismaSchema, schemaContent: string): void {
|
||||
for (const model of schema.models) {
|
||||
for (const field of model.fields) {
|
||||
const fieldLine = this.findFieldLine(schemaContent, model.name, field.name);
|
||||
if (fieldLine) {
|
||||
const nativeTypeMatch = fieldLine.match(/@db\.(\w+)(?:\(([^)]*)\))?/);
|
||||
if (nativeTypeMatch) {
|
||||
field.nativeType = nativeTypeMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findFieldLine(schema: string, modelName: string, fieldName: string): string | null {
|
||||
const modelRegex = new RegExp(`model\\s+${modelName}\\s*\\{([^}]+)\\}`, 's');
|
||||
const modelMatch = schema.match(modelRegex);
|
||||
|
||||
if (!modelMatch) return null;
|
||||
|
||||
const lines = modelMatch[1].split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith(fieldName)) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Main exports
|
||||
export { parsePrismaSchema, validatePrismaSchema } from './parser';
|
||||
export { detectPrismaVersion, getVersionFeatures } from './version-detector';
|
||||
|
||||
// Adapters
|
||||
export { Prisma5Adapter, Prisma6Adapter, Prisma7Adapter } from './adapters';
|
||||
|
||||
// Renderers
|
||||
export { toMermaid } from './renderers/mermaid';
|
||||
export { toPlantUML } from './renderers/plantuml';
|
||||
export { toDBML } from './renderers/dbml';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
PrismaSchema,
|
||||
PrismaModel,
|
||||
PrismaField,
|
||||
PrismaEnum,
|
||||
EnumValue,
|
||||
Datasource,
|
||||
Generator,
|
||||
PrismaVersion,
|
||||
ParserOptions,
|
||||
PrismaAdapter,
|
||||
DefaultValue,
|
||||
CompositePrimaryKey,
|
||||
UniqueConstraint,
|
||||
Index,
|
||||
} from './types';
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Main parser module
|
||||
* Detects Prisma version and delegates to the appropriate adapter
|
||||
*/
|
||||
|
||||
import type { PrismaSchema, ParserOptions, PrismaVersion } from './types';
|
||||
import { detectPrismaVersion } from './version-detector';
|
||||
import { Prisma5Adapter, Prisma6Adapter, Prisma7Adapter } from './adapters';
|
||||
|
||||
// each Prisma version has slightly different features
|
||||
const adapters = {
|
||||
5: Prisma5Adapter,
|
||||
6: Prisma6Adapter,
|
||||
7: Prisma7Adapter,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Parse a Prisma schema string into a structured format
|
||||
*/
|
||||
export async function parsePrismaSchema(
|
||||
schemaContent: string,
|
||||
options?: ParserOptions
|
||||
): Promise<PrismaSchema> {
|
||||
// auto-detect version if not specified
|
||||
const version: PrismaVersion = options?.version ?? detectPrismaVersion(schemaContent);
|
||||
|
||||
const AdapterClass = adapters[version];
|
||||
const adapter = new AdapterClass();
|
||||
|
||||
return adapter.parse(schemaContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation - checks for common issues before parsing
|
||||
*/
|
||||
export function validatePrismaSchema(schemaContent: string): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
// must have a datasource
|
||||
if (!schemaContent.includes('datasource')) {
|
||||
errors.push('Missing datasource block');
|
||||
}
|
||||
|
||||
// should have at least one model
|
||||
if (!schemaContent.includes('model ')) {
|
||||
errors.push('No models found in schema');
|
||||
}
|
||||
|
||||
// check for balanced braces (common mistake)
|
||||
const openBraces = (schemaContent.match(/{/g) || []).length;
|
||||
const closeBraces = (schemaContent.match(/}/g) || []).length;
|
||||
if (openBraces !== closeBraces) {
|
||||
errors.push('Mismatched braces in schema');
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { PrismaSchema, PrismaField } from '../types';
|
||||
|
||||
export function toDBML(schema: PrismaSchema): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push('// Generated from Prisma Schema');
|
||||
lines.push(`// Prisma Version: ${schema.version}`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('Project prisma_schema {');
|
||||
lines.push(` database_type: '${mapProviderToDBType(schema.datasource.provider)}'`);
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
|
||||
for (const enumDef of schema.enums) {
|
||||
if (enumDef.documentation) {
|
||||
lines.push(`// ${enumDef.documentation}`);
|
||||
}
|
||||
lines.push(`Enum ${enumDef.name} {`);
|
||||
for (const value of enumDef.values) {
|
||||
const note = value.documentation ? ` [note: '${value.documentation}']` : '';
|
||||
lines.push(` ${value.name}${note}`);
|
||||
}
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
for (const model of schema.models) {
|
||||
if (model.documentation) {
|
||||
lines.push(`// ${model.documentation}`);
|
||||
}
|
||||
|
||||
const tableName = model.dbName || model.name;
|
||||
lines.push(`Table ${tableName} {`);
|
||||
|
||||
for (const field of model.fields) {
|
||||
// Skip relation fields (object kind)
|
||||
if (field.kind === 'object') continue;
|
||||
|
||||
const dbmlField = fieldToDBML(field);
|
||||
lines.push(` ${dbmlField}`);
|
||||
}
|
||||
|
||||
// Add indexes
|
||||
if (model.indexes.length > 0 || model.uniqueConstraints.length > 0) {
|
||||
lines.push('');
|
||||
lines.push(' indexes {');
|
||||
|
||||
for (const idx of model.indexes) {
|
||||
const idxName = idx.name ? ` [name: '${idx.name}']` : '';
|
||||
lines.push(` (${idx.fields.join(', ')})${idxName}`);
|
||||
}
|
||||
|
||||
for (const unique of model.uniqueConstraints) {
|
||||
const uniqueName = unique.name ? ` [name: '${unique.name}']` : '';
|
||||
lines.push(` (${unique.fields.join(', ')}) [unique]${uniqueName}`);
|
||||
}
|
||||
|
||||
lines.push(' }');
|
||||
}
|
||||
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
for (const model of schema.models) {
|
||||
for (const field of model.fields) {
|
||||
if (field.kind === 'object' && field.relationFromFields?.length) {
|
||||
const targetModel = field.type.replace('[]', '');
|
||||
const sourceFields = field.relationFromFields.join(', ');
|
||||
const targetFields = field.relationToFields?.join(', ') || 'id';
|
||||
|
||||
const relationType = field.isList ? '<>' : field.isRequired ? '-' : '-';
|
||||
const sourceName = model.dbName || model.name;
|
||||
const targetName = schema.models.find((m) => m.name === targetModel)?.dbName || targetModel;
|
||||
|
||||
lines.push(
|
||||
`Ref: ${sourceName}.${sourceFields} ${relationType} ${targetName}.${targetFields}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function fieldToDBML(field: PrismaField): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(field.name);
|
||||
|
||||
const dbmlType = mapPrismaTypeToDBML(field.type, field.isList);
|
||||
parts.push(dbmlType);
|
||||
|
||||
const settings: string[] = [];
|
||||
|
||||
if (field.isId) {
|
||||
settings.push('pk');
|
||||
}
|
||||
|
||||
if (field.isUnique && !field.isId) {
|
||||
settings.push('unique');
|
||||
}
|
||||
|
||||
if (!field.isRequired && !field.isId) {
|
||||
settings.push('null');
|
||||
} else if (!field.isId) {
|
||||
settings.push('not null');
|
||||
}
|
||||
|
||||
if (field.hasDefaultValue && field.default !== undefined) {
|
||||
const defaultStr = formatDefault(field.default);
|
||||
if (defaultStr) {
|
||||
settings.push(`default: ${defaultStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.isUpdatedAt) {
|
||||
settings.push("note: 'Updated automatically'");
|
||||
}
|
||||
|
||||
if (field.documentation) {
|
||||
settings.push(`note: '${field.documentation}'`);
|
||||
}
|
||||
|
||||
if (settings.length > 0) {
|
||||
parts.push(`[${settings.join(', ')}]`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function mapPrismaTypeToDBML(type: string, isList: boolean): string {
|
||||
const mapping: Record<string, string> = {
|
||||
String: 'varchar',
|
||||
Int: 'integer',
|
||||
BigInt: 'bigint',
|
||||
Float: 'float',
|
||||
Decimal: 'decimal',
|
||||
Boolean: 'boolean',
|
||||
DateTime: 'timestamp',
|
||||
Json: 'json',
|
||||
Bytes: 'bytea',
|
||||
};
|
||||
|
||||
const mappedType = mapping[type] || type.toLowerCase();
|
||||
return isList ? `${mappedType}[]` : mappedType;
|
||||
}
|
||||
|
||||
function mapProviderToDBType(provider: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
postgresql: 'PostgreSQL',
|
||||
mysql: 'MySQL',
|
||||
sqlite: 'SQLite',
|
||||
sqlserver: 'SQL Server',
|
||||
mongodb: 'MongoDB',
|
||||
cockroachdb: 'CockroachDB',
|
||||
};
|
||||
return mapping[provider] || provider;
|
||||
}
|
||||
|
||||
function formatDefault(value: unknown): string | null {
|
||||
if (typeof value === 'object' && value !== null && 'name' in value) {
|
||||
const func = value as { name: string; args: unknown[] };
|
||||
switch (func.name) {
|
||||
case 'autoincrement':
|
||||
return null; // Handled by pk
|
||||
case 'now':
|
||||
return '`now()`';
|
||||
case 'uuid':
|
||||
return '`uuid()`';
|
||||
case 'cuid':
|
||||
return '`cuid()`';
|
||||
default:
|
||||
return `\`${func.name}()\``;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { toMermaid } from './mermaid';
|
||||
export { toPlantUML } from './plantuml';
|
||||
export { toDBML } from './dbml';
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { PrismaSchema, PrismaField } from '../types';
|
||||
|
||||
export function toMermaid(schema: PrismaSchema): string {
|
||||
const lines: string[] = ['erDiagram'];
|
||||
|
||||
for (const model of schema.models) {
|
||||
lines.push(` ${model.name} {`);
|
||||
|
||||
for (const field of model.fields) {
|
||||
if (field.kind === 'scalar' || field.kind === 'enum') {
|
||||
const type = mapPrismaTypeToMermaid(field.type);
|
||||
const constraints: string[] = [];
|
||||
|
||||
if (field.isId) constraints.push('PK');
|
||||
if (field.isUnique) constraints.push('UK');
|
||||
if (field.relationFromFields?.length) constraints.push('FK');
|
||||
|
||||
const constraintStr = constraints.length ? ` "${constraints.join(', ')}"` : '';
|
||||
lines.push(` ${type} ${field.name}${constraintStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(' }');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
for (const enumDef of schema.enums) {
|
||||
lines.push(` ${enumDef.name} {`);
|
||||
for (const value of enumDef.values) {
|
||||
lines.push(` string ${value.name}`);
|
||||
}
|
||||
lines.push(' }');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
||||
const processedRelations = new Set<string>();
|
||||
|
||||
for (const model of schema.models) {
|
||||
for (const field of model.fields) {
|
||||
if (field.kind === 'object') {
|
||||
const targetModel = field.type.replace('[]', '');
|
||||
const relationKey = [model.name, targetModel].sort().join('-');
|
||||
|
||||
if (processedRelations.has(relationKey)) continue;
|
||||
processedRelations.add(relationKey);
|
||||
|
||||
const cardinality = getCardinality(field, schema, model.name, targetModel);
|
||||
const relationLabel = field.relationName || field.name;
|
||||
|
||||
lines.push(` ${model.name} ${cardinality} ${targetModel} : "${relationLabel}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getCardinality(
|
||||
field: PrismaField,
|
||||
schema: PrismaSchema,
|
||||
sourceModel: string,
|
||||
targetModel: string
|
||||
): string {
|
||||
const target = schema.models.find((m) => m.name === targetModel);
|
||||
const inverseField = target?.fields.find(
|
||||
(f) => f.kind === 'object' && f.type.replace('[]', '') === sourceModel
|
||||
);
|
||||
|
||||
const sourceIsList = field.isList;
|
||||
const targetIsList = inverseField?.isList ?? false;
|
||||
|
||||
// One-to-Many
|
||||
if (!sourceIsList && targetIsList) {
|
||||
return '||--o{';
|
||||
}
|
||||
|
||||
// Many-to-One
|
||||
if (sourceIsList && !targetIsList) {
|
||||
return '}o--||';
|
||||
}
|
||||
|
||||
// Many-to-Many
|
||||
if (sourceIsList && targetIsList) {
|
||||
return '}o--o{';
|
||||
}
|
||||
|
||||
// One-to-One
|
||||
return '||--||';
|
||||
}
|
||||
|
||||
function mapPrismaTypeToMermaid(type: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
String: 'string',
|
||||
Int: 'int',
|
||||
BigInt: 'bigint',
|
||||
Float: 'float',
|
||||
Decimal: 'decimal',
|
||||
Boolean: 'boolean',
|
||||
DateTime: 'datetime',
|
||||
Json: 'json',
|
||||
Bytes: 'bytes',
|
||||
};
|
||||
return mapping[type] || type.toLowerCase();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { PrismaSchema } from '../types';
|
||||
|
||||
export function toPlantUML(schema: PrismaSchema): string {
|
||||
const lines: string[] = [
|
||||
'@startuml',
|
||||
'!theme blueprint',
|
||||
'skinparam linetype ortho',
|
||||
'skinparam class {',
|
||||
' BackgroundColor #1e1e2e',
|
||||
' BorderColor #89b4fa',
|
||||
' ArrowColor #89b4fa',
|
||||
' FontColor #cdd6f4',
|
||||
'}',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const model of schema.models) {
|
||||
const displayName = model.dbName ? `"${model.name}\\n(${model.dbName})"` : `"${model.name}"`;
|
||||
lines.push(`entity ${displayName} as ${model.name} {`);
|
||||
|
||||
const pkFields = model.fields.filter((f) => f.isId);
|
||||
const fkFields = model.fields.filter(
|
||||
(f) => f.relationFromFields?.length && !f.isId && f.kind !== 'object'
|
||||
);
|
||||
const regularFields = model.fields.filter(
|
||||
(f) => !f.isId && f.kind !== 'object' && !f.relationFromFields?.length
|
||||
);
|
||||
|
||||
// Primary keys
|
||||
for (const field of pkFields) {
|
||||
const nullable = field.isRequired ? '' : ' (nullable)';
|
||||
lines.push(` * ${field.name} : ${field.type}${nullable} <<PK>>`);
|
||||
}
|
||||
|
||||
if (pkFields.length && (fkFields.length || regularFields.length)) {
|
||||
lines.push(' --');
|
||||
}
|
||||
|
||||
// Foreign keys
|
||||
for (const field of fkFields) {
|
||||
const optional = field.isRequired ? '*' : 'o';
|
||||
const unique = field.isUnique ? ' <<UK>>' : '';
|
||||
lines.push(` ${optional} ${field.name} : ${field.type}${unique} <<FK>>`);
|
||||
}
|
||||
|
||||
if (fkFields.length && regularFields.length) {
|
||||
lines.push(' ..');
|
||||
}
|
||||
|
||||
// Regular fields
|
||||
for (const field of regularFields) {
|
||||
const optional = field.isRequired ? '*' : 'o';
|
||||
const unique = field.isUnique ? ' <<UK>>' : '';
|
||||
const updatedAt = field.isUpdatedAt ? ' <<updatedAt>>' : '';
|
||||
lines.push(` ${optional} ${field.name} : ${field.type}${unique}${updatedAt}`);
|
||||
}
|
||||
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
for (const enumDef of schema.enums) {
|
||||
lines.push(`enum ${enumDef.name} {`);
|
||||
for (const value of enumDef.values) {
|
||||
const dbName = value.dbName ? ` [${value.dbName}]` : '';
|
||||
lines.push(` ${value.name}${dbName}`);
|
||||
}
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const processedRelations = new Set<string>();
|
||||
|
||||
for (const model of schema.models) {
|
||||
for (const field of model.fields) {
|
||||
if (field.kind === 'object' && field.relationFromFields?.length) {
|
||||
const target = field.type.replace('[]', '');
|
||||
const relationKey = `${model.name}-${target}-${field.relationName || field.name}`;
|
||||
|
||||
if (processedRelations.has(relationKey)) continue;
|
||||
processedRelations.add(relationKey);
|
||||
|
||||
const sourceCard = field.isRequired ? '1' : '0..1';
|
||||
const targetCard = field.isList ? '*' : '1';
|
||||
|
||||
lines.push(`${model.name} "${sourceCard}" --> "${targetCard}" ${target} : ${field.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('@enduml');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
export type PrismaVersion = 5 | 6 | 7;
|
||||
|
||||
export type ScalarDefaultValue = string | number | boolean;
|
||||
|
||||
export interface FunctionDefaultValue {
|
||||
name: string;
|
||||
args: unknown[];
|
||||
}
|
||||
|
||||
export type DefaultValue = ScalarDefaultValue | FunctionDefaultValue;
|
||||
|
||||
export interface PrismaField {
|
||||
name: string;
|
||||
type: string;
|
||||
kind: 'scalar' | 'object' | 'enum' | 'unsupported';
|
||||
isList: boolean;
|
||||
isRequired: boolean;
|
||||
isUnique: boolean;
|
||||
isId: boolean;
|
||||
isUpdatedAt: boolean;
|
||||
hasDefaultValue: boolean;
|
||||
default?: DefaultValue;
|
||||
relationName?: string;
|
||||
relationFromFields?: string[];
|
||||
relationToFields?: string[];
|
||||
documentation?: string;
|
||||
isOmitted?: boolean;
|
||||
nativeType?: string;
|
||||
}
|
||||
|
||||
export interface CompositePrimaryKey {
|
||||
name?: string;
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
export interface UniqueConstraint {
|
||||
name?: string;
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
export interface Index {
|
||||
name?: string;
|
||||
fields: string[];
|
||||
type?: 'btree' | 'hash' | 'gist' | 'gin' | 'spgist' | 'brin';
|
||||
}
|
||||
|
||||
export interface PrismaModel {
|
||||
name: string;
|
||||
dbName?: string;
|
||||
fields: PrismaField[];
|
||||
primaryKey?: CompositePrimaryKey;
|
||||
uniqueConstraints: UniqueConstraint[];
|
||||
indexes: Index[];
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
export interface EnumValue {
|
||||
name: string;
|
||||
dbName?: string;
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
export interface PrismaEnum {
|
||||
name: string;
|
||||
values: EnumValue[];
|
||||
documentation?: string;
|
||||
}
|
||||
|
||||
export interface Datasource {
|
||||
name: string;
|
||||
provider: string;
|
||||
url: string;
|
||||
directUrl?: string;
|
||||
shadowDatabaseUrl?: string;
|
||||
}
|
||||
|
||||
export interface Generator {
|
||||
name: string;
|
||||
provider: string;
|
||||
output?: string;
|
||||
previewFeatures?: string[];
|
||||
binaryTargets?: string[];
|
||||
}
|
||||
|
||||
export interface PrismaSchema {
|
||||
version: PrismaVersion;
|
||||
models: PrismaModel[];
|
||||
enums: PrismaEnum[];
|
||||
datasource: Datasource;
|
||||
generators: Generator[];
|
||||
}
|
||||
|
||||
export interface ParserOptions {
|
||||
version?: PrismaVersion;
|
||||
strict?: boolean;
|
||||
}
|
||||
|
||||
export interface PrismaAdapter {
|
||||
parse(schemaContent: string): Promise<PrismaSchema>;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { PrismaVersion } from './types';
|
||||
|
||||
export function detectPrismaVersion(schema: string): PrismaVersion {
|
||||
if (
|
||||
schema.includes('typedSql') ||
|
||||
schema.includes('@prisma/client/sql') ||
|
||||
/generator\s+\w+\s*\{[^}]*provider\s*=\s*"prisma-client"/s.test(schema)
|
||||
) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
if (
|
||||
schema.includes('@omit') ||
|
||||
schema.includes('strictUndefinedChecks') ||
|
||||
schema.includes('prismaSchemaFolder') ||
|
||||
schema.includes('omitApi')
|
||||
) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
export function getVersionFeatures(version: PrismaVersion): string[] {
|
||||
const features: Record<PrismaVersion, string[]> = {
|
||||
5: ['relations', 'enums', 'compositeTypes', 'views'],
|
||||
6: ['relations', 'enums', 'compositeTypes', 'views', 'omit', 'strictUndefinedChecks'],
|
||||
7: [
|
||||
'relations',
|
||||
'enums',
|
||||
'compositeTypes',
|
||||
'views',
|
||||
'omit',
|
||||
'strictUndefinedChecks',
|
||||
'typedSql',
|
||||
],
|
||||
};
|
||||
|
||||
return features[version];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
'renderers/mermaid': 'src/renderers/mermaid.ts',
|
||||
'renderers/plantuml': 'src/renderers/plantuml.ts',
|
||||
'renderers/dbml': 'src/renderers/dbml.ts',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
splitting: false,
|
||||
treeshake: true,
|
||||
});
|
||||
Reference in New Issue
Block a user