Created
January 16, 2025 09:47
-
-
Save saiashirwad/ef16c4bf605d65de8f53d7a23a611e71 to your computer and use it in GitHub Desktop.
cursed prisma codegen
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ts from "typescript"; | |
import { | |
Parser, | |
alphabet, | |
char, | |
digit, | |
many0, | |
many1, | |
optional, | |
or, | |
sepBy, | |
skipSpaces, | |
skipUntil, | |
string, | |
takeUntil, | |
} from "parserator"; | |
import { Effect } from "effect"; | |
import { camelCase } from "lodash-es"; | |
export const peekAhead = (n: number) => | |
new Parser((s) => { | |
return Parser.succeed(s.remaining.slice(0, n), s); | |
}); | |
const whitespace = many0(or(char(" "), char("\n"), char("\t"))).withName( | |
"whitespace", | |
); | |
const word = many1(or(alphabet, digit, char("_"))).map((chars) => | |
chars.join(""), | |
); | |
const skipLine = skipUntil(char("\n")); | |
const takeLine = takeUntil(char("\n")); | |
const basicPrismaTypes = [ | |
"String", | |
"Int", | |
"Float", | |
"Boolean", | |
"DateTime", | |
"Json", | |
]; | |
namespace JsonTypes { | |
type FieldsMap = Map< | |
string, | |
{ | |
type: string; | |
following?: string; | |
}[] | |
>; | |
export type InterfacesMap = Map<string, FieldsMap>; | |
export const parser = Parser.gen(function* () { | |
yield* whitespace; | |
const imports = yield* takeUntil(char("%")); | |
yield* skipLine.trimRight(whitespace); | |
const interfaces = yield* many0(parseInterface); | |
const interfacesMap: InterfacesMap = new Map(); | |
for (const iface of interfaces) { | |
const fields = iface.fields; | |
const fieldsMap: FieldsMap = new Map(); | |
for (const field of fields) { | |
fieldsMap.set(field.name, field.types); | |
} | |
interfacesMap.set(iface.name, fieldsMap); | |
} | |
return { imports, interfaces: interfacesMap }; | |
}); | |
const parseFieldTypes = sepBy( | |
char("|"), | |
whitespace | |
.then( | |
word.zip(optional(string("[]"))).map(([typeName, following]) => ({ | |
type: typeName, | |
following, | |
})), | |
) | |
.thenDiscard(whitespace), | |
); | |
const parseInterface = Parser.gen(function* () { | |
const name = yield* whitespace | |
.then(string("export interface")) | |
.then(whitespace) | |
.then(word) | |
.thenDiscard(whitespace); | |
yield* char("{"); | |
const fields = []; | |
while (true) { | |
yield* whitespace; | |
const next = yield* peekAhead(1); | |
if (next === "}") { | |
yield* char("}"); | |
break; | |
} | |
if (next === ";") { | |
yield* skipUntil(char("\n")); | |
continue; | |
} | |
const fieldName = yield* word; | |
yield* skipSpaces.then(char(":")).thenDiscard(skipSpaces); | |
const types = yield* parseFieldTypes; | |
fields.push({ name: fieldName, types }); | |
} | |
return { | |
type: "interface" as const, | |
name, | |
fields, | |
}; | |
}); | |
} | |
const fieldParser = Parser.gen(function* () { | |
yield* whitespace; | |
const name = yield* word; | |
const type = yield* whitespace.then( | |
Parser.gen(function* () { | |
return { | |
type: yield* word, | |
following: yield* optional(or(string("[]"), char("?"))), | |
}; | |
}), | |
); | |
yield* skipLine; | |
return { | |
name, | |
...type, | |
}; | |
}); | |
const parseModel = ( | |
enums: Map<string, string[]>, | |
name: string, | |
interfaces: JsonTypes.InterfacesMap, | |
) => | |
Parser.gen(function* () { | |
const types = [...enums.keys(), ...basicPrismaTypes]; | |
const fields = []; | |
let mappedName = ""; | |
while (true) { | |
yield* whitespace; | |
const next = yield* optional(peekAhead(1)); | |
if (!next) { | |
break; | |
} | |
if (next === "@") { | |
const isMapStatement = yield* peekAhead(5).map((s) => | |
s.startsWith("@@map"), | |
); | |
if (isMapStatement) { | |
yield* skipUntil(char('"')); | |
mappedName = yield* word; | |
} | |
yield* skipUntil(char("\n")); | |
} else if (next === "}") { | |
yield* char("}"); | |
break; | |
} else { | |
const field = yield* fieldParser.thenDiscard(whitespace); | |
if (types.includes(field.type)) { | |
fields.push(field); | |
} | |
} | |
} | |
return { | |
type: "model" as const, | |
name, | |
fields, | |
mappedName: mappedName === "" ? null : mappedName, | |
}; | |
}).map(({ fields, name, mappedName }) => { | |
return { | |
mappedName, | |
content: `export const ${name} = Schema.Struct({ | |
${fields | |
.map((field) => { | |
let fieldType = { | |
String: "Schema.String", | |
Int: "Schema.Number", | |
Float: "Schema.Number", | |
Boolean: "Schema.Boolean", | |
DateTime: "Schema.Date", | |
}[field.type]; | |
if (enums.get(field.type)) { | |
fieldType = field.type; | |
} else if (field.type === "Json") { | |
const fields = interfaces.get(name); | |
if (fields) { | |
const fieldTypes = fields.get(field.name)?.map((type) => { | |
let t = ""; | |
if (type.type === "any") { | |
t = "Schema.Any"; | |
} else if (type.type === "null") { | |
t = "Schema.Null"; | |
} else if (type.type === "string") { | |
t = "Schema.String"; | |
} else { | |
t = type.type; | |
} | |
if (type.following === "[]") { | |
t = `Schema.Array(${t})`; | |
} | |
return t; | |
}); | |
if (fieldTypes?.length) { | |
fieldType = `Schema.Union(${fieldTypes.join(", ")})`; | |
} else { | |
fieldType = "Schema.Any"; | |
} | |
} else { | |
fieldType = "Schema.Any"; | |
} | |
} | |
if (field.following === "[]") { | |
fieldType = `Schema.Array(${fieldType})`; | |
} else if (field.following === "?") { | |
fieldType = `${fieldType}.pipe(Schema.NullishOr)`; | |
} | |
return `${field.name}: ${fieldType}`; | |
}) | |
.join(",\n")} | |
}); | |
export type ${name} = Schema.Schema.Type<typeof ${name}>; | |
`, | |
}; | |
}); | |
const parseEnum = (name: string) => | |
Parser.gen(function* () { | |
const variants = []; | |
while (true) { | |
yield* whitespace; | |
const next = yield* optional(peekAhead(1)); | |
if (!next) { | |
break; | |
} | |
if (next === "@") { | |
yield* skipLine; | |
} else if (next === "}") { | |
yield* char("}"); | |
break; | |
} else { | |
const variant = yield* word; | |
variants.push(variant); | |
yield* skipLine; | |
} | |
} | |
return { | |
name, | |
variants, | |
}; | |
}); | |
const parseEnums = many0( | |
Parser.gen(function* () { | |
yield* whitespace; | |
const blockName = yield* word; | |
yield* skipSpaces; | |
const name = yield* word; | |
yield* whitespace.then(char("{").thenDiscard(whitespace)); | |
if (blockName === "enum") { | |
return yield* parseEnum(name); | |
} | |
yield* skipUntil(char("}")); | |
}), | |
).map((enums) => enums.filter((x) => x != null)); | |
const getEnums = parseEnums.map((enums) => { | |
const enumsMap = new Map<string, string[]>(); | |
for (const enumObj of enums) { | |
enumsMap.set(enumObj.name, enumObj.variants); | |
} | |
return enumsMap; | |
}); | |
const processEnums = parseEnums.map((enums) => { | |
return enums | |
.map( | |
({ name, variants }) => ` | |
export const ${name} = Schema.Union(${variants | |
.map((variant) => `Schema.Literal("${variant}")`) | |
.join(", ")}); | |
export type ${name} = Schema.Schema.Type<typeof ${name}>; | |
`, | |
) | |
.join("\n"); | |
}); | |
const parseModels = ( | |
enums: Map<string, string[]>, | |
interfaces: JsonTypes.InterfacesMap, | |
) => | |
Parser.gen(function* () { | |
const mappedNames: [string, string][] = []; | |
const result = yield* many0( | |
Parser.gen(function* () { | |
yield* whitespace; | |
const blockName = yield* word; | |
yield* skipSpaces; | |
const name = yield* word; | |
yield* whitespace.then(char("{").thenDiscard(whitespace)); | |
if (blockName === "model") { | |
const { content, mappedName } = yield* parseModel( | |
enums, | |
name, | |
interfaces, | |
); | |
mappedNames.push([name, mappedName ?? name]); | |
return content; | |
} | |
yield* skipUntil(char("}")); | |
}), | |
); | |
return { content: result.filter((x) => x !== undefined), mappedNames }; | |
}); | |
async function generateSchema() { | |
const jsonTypesFile = await Bun.file("src/db/json-types.ts").text(); | |
const jsonTypesResult = JsonTypes.parser.parseOrThrow(jsonTypesFile); | |
const schemaPrisma = await Bun.file("prisma/schema.prisma").text(); | |
const enums = getEnums.parseOrThrow(schemaPrisma); | |
const schemaResult = parseModels( | |
enums, | |
jsonTypesResult.interfaces, | |
).parseOrThrow(schemaPrisma); | |
await Bun.write( | |
"src/db/schema.ts", | |
// biome-ignore lint/style/useTemplate: <explanation> | |
'import { Schema } from "effect"\n' + | |
`${jsonTypesResult.imports}\n` + | |
`${processEnums.parseOrThrow(schemaPrisma)}\n` + | |
`${schemaResult.content.join("\n\n")}`, | |
); | |
await Bun.$`bun biome format --write src/db/schema.ts`; | |
} | |
const createProgram = Effect.gen(function* () { | |
const configPath = yield* Effect.fromNullable( | |
ts.findConfigFile(".", ts.sys.fileExists, "tsconfig.json"), | |
); | |
const configFile = ts.readConfigFile(configPath, ts.sys.readFile); | |
const parsedCommandLine = ts.parseJsonConfigFileContent( | |
configFile.config, | |
ts.sys, | |
".", | |
); | |
return ts.createProgram( | |
parsedCommandLine.fileNames, | |
parsedCommandLine.options, | |
); | |
}); | |
const main = Effect.gen(function* () { | |
// Effect schema | |
const jsonTypesFile = yield* Effect.promise(() => | |
Bun.file("src/db/json-types.ts").text(), | |
); | |
const jsonTypesResult = JsonTypes.parser.parseOrThrow(jsonTypesFile); | |
const schemaPrisma = yield* Effect.promise(() => | |
Bun.file("prisma/schema.prisma").text(), | |
); | |
const enums = getEnums.parseOrThrow(schemaPrisma); | |
const schemaResult = parseModels( | |
enums, | |
jsonTypesResult.interfaces, | |
).parseOrThrow(schemaPrisma); | |
yield* Effect.promise(() => | |
Bun.write( | |
"src/db/schema.ts", | |
// biome-ignore lint/style/useTemplate: <explanation> | |
'import { Schema } from "effect"\n' + | |
`${jsonTypesResult.imports}\n` + | |
`${processEnums.parseOrThrow(schemaPrisma)}\n` + | |
`${schemaResult.content.join("\n\n")}`, | |
), | |
); | |
yield* Effect.promise(() => Bun.$`bun biome format --write src/db/schema.ts`); | |
console.log("Schema generated"); | |
// type generation | |
const program = yield* createProgram; | |
const checker = program.getTypeChecker(); | |
const source: ts.SourceFile = yield* Effect.fromNullable( | |
program.getSourceFile("src/db/schema.ts"), | |
); | |
const types = new Map<string, string>(); | |
ts.forEachChild(source, (node) => { | |
if (ts.isTypeAliasDeclaration(node)) { | |
const type = checker.getTypeAtLocation(node); | |
if (node.name) { | |
const typeName = node.name.getText(); | |
types.set( | |
typeName, | |
`type ${typeName} = ${checker.typeToString( | |
type, | |
undefined, | |
ts.TypeFormatFlags.NoTruncation | | |
ts.TypeFormatFlags.MultilineObjectLiterals | | |
ts.TypeFormatFlags.WriteClassExpressionAsTypeLiteral | | |
ts.TypeFormatFlags.WriteTypeArgumentsOfSignature, | |
)}`, | |
); | |
} | |
} | |
}); | |
const tableTypes = Array.from(types.keys()).filter((type) => { | |
return !enums.get(type); | |
}); | |
const result = ` | |
${jsonTypesResult.imports} | |
${Array.from(types.values()).join(";\n\n")} | |
export type DB = { | |
${tableTypes.map((t) => `${camelCase(t)}:${t}`).join(";\n")} | |
} | |
`; | |
yield* Effect.promise(() => Bun.write("src/db/types.ts", result)); | |
yield* Effect.promise(() => Bun.$`bun biome format --write src/db/types.ts`); | |
console.log("Types generated"); | |
}); | |
await Effect.runPromise(main); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment