Skip to content

Instantly share code, notes, and snippets.

@saiashirwad
Created January 16, 2025 09:47
Show Gist options
  • Save saiashirwad/ef16c4bf605d65de8f53d7a23a611e71 to your computer and use it in GitHub Desktop.
Save saiashirwad/ef16c4bf605d65de8f53d7a23a611e71 to your computer and use it in GitHub Desktop.
cursed prisma codegen
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