Skip to content

Instantly share code, notes, and snippets.

@saiashirwad
Last active March 19, 2025 03:02
Show Gist options
  • Save saiashirwad/d305425c038bd8c5e81d53b2fd4b4dd2 to your computer and use it in GitHub Desktop.
Save saiashirwad/d305425c038bd8c5e81d53b2fd4b4dd2 to your computer and use it in GitHub Desktop.
A prisma -> effect schema codegen script for a very opinionated setup using the parserator library
// this is incredibly hacky, but it works great for me
// for this to work, i have a file called json-types.ts that contains a bunch of exported interfaces, one for each of
// the tables i have that have any JSON fields. json-types.ts should only have Effect Schema imports, like so:
// import { Schema1 } from '../../somewhere'
// import ...
// %
// ^ that is essential lol. that tells the script that the imports section is over
// export interface SomeTable {
// something: Schema1,
// ... (you only need the fields that are JSON typed, everything else can be whatever)
// }
import {
Parser,
alphabet,
char,
digit,
many0,
many1,
optional,
or,
sepBy,
skipSpaces,
skipUntil,
string,
takeUntil,
} from "parserator";
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.then(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,
};
});
}
namespace Prisma {
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 = (name: string, interfaces: JsonTypes.InterfacesMap) =>
Parser.gen(function* () {
const fields = [];
while (true) {
yield* whitespace;
const next = yield* optional(peekAhead(1));
if (!next) {
break;
}
if (next === "@") {
yield* skipUntil(char("\n"));
} else if (next === "}") {
yield* char("}");
break;
} else {
const field = yield* fieldParser.thenDiscard(whitespace);
if (basicPrismaTypes.includes(field.type)) {
fields.push(field);
}
}
}
return {
type: "model" as const,
name,
fields,
};
}).map(({ fields, name }) => {
return `export class ${name} extends Schema.Class<${name}>("${name}")({
${fields
.map((field) => {
const fieldTypeMap: Record<string, string> = {
String: "Schema.String",
Int: "Schema.Number",
Float: "Schema.Number",
Boolean: "Schema.Boolean",
DateTime: "Schema.Date",
};
let fieldType = fieldTypeMap[field.type];
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.optional)`;
}
return `${field.name}: ${fieldType}`;
})
.join(",\n")}
}) {}`;
});
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 {
type: "enum" as const,
name,
variants,
};
}).map(({ name, variants }) => {
return `
export const ${name} = Schema.Union(${variants.map((variant) => `Schema.Literal("${variant}")`).join(", ")});
export type ${name} = Schema.Schema.Type<typeof ${name}>;
`;
});
export const parser = Parser.gen(function* () {
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") {
return yield* parseModel(name, jsonTypesResult.interfaces);
}
if (blockName === "enum") {
return yield* parseEnum(name);
}
yield* skipUntil(char("}"));
}),
);
return result.filter((x) => x !== undefined);
});
}
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 result = `
import { Schema } from "effect";
${jsonTypesResult.imports}
${Prisma.parser.parseOrThrow(schemaPrisma).join("\n\n")}`;
await Bun.write("src/db/schema.ts", result);
await Bun.$`bun biome format --write src/db/schema.ts`;
import {
Parser,
alphabet,
char,
digit,
many0,
many1,
optional,
or,
skipSpaces,
skipUntil,
string,
} from "parserator";
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 basicPrismaTypes = [
"String",
"Int",
"Float",
"Boolean",
"DateTime",
"Json",
];
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* skipUntil(char("\n"));
return {
name,
...type,
};
});
const parseModel = (name: string) =>
Parser.gen(function* () {
const fields = [];
while (true) {
yield* whitespace;
const next = yield* optional(peekAhead(1));
if (!next) {
break;
}
if (next === "@") {
yield* skipUntil(char("\n"));
} else if (next === "}") {
yield* char("}");
break;
} else {
const field = yield* fieldParser.thenDiscard(whitespace);
if (basicPrismaTypes.includes(field.type)) {
fields.push(field);
}
}
}
return {
type: "model" as const,
name,
fields,
};
}).map(({ fields, name }) => {
return `export class ${name} extends Schema.Class<${name}>("${name}")({
${fields
.map((field) => {
const fieldTypeMap: Record<string, string> = {
String: "Schema.String",
Int: "Schema.Number",
Float: "Schema.Number",
Boolean: "Schema.Boolean",
DateTime: "Schema.Date",
};
let fieldType = fieldTypeMap[field.type];
if (field.type === "Json") {
// TODO: inject json types here
fieldType = "Schema.Any";
}
if (field.following === "[]") {
fieldType = `Schema.Array(${fieldType})`;
} else if (field.following === "?") {
fieldType = `${fieldType}.pipe(Schema.optional)`;
}
return `${field.name}: ${fieldType}`;
})
.join(",\n")}
}) {}`;
});
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* skipUntil(char("\n"));
} else if (next === "}") {
yield* char("}");
break;
} else {
const variant = yield* word;
variants.push(variant);
yield* skipUntil(char("\n"));
}
}
return {
type: "enum" as const,
name,
variants,
};
}).map(({ name, variants }) => {
return `
export const ${name} = Schema.Union(${variants.map((variant) => `Schema.Literal("${variant}")`).join(", ")});
export type ${name} = Schema.Schema.Type<typeof ${name}>;
`;
});
const schemaParser = Parser.gen(function* () {
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") {
return yield* parseModel(name);
}
if (blockName === "enum") {
return yield* parseEnum(name);
}
yield* skipUntil(char("}"));
}),
);
return result.filter((x) => x !== undefined);
});
const schemaPrisma = await Bun.file("prisma/schema.prisma").text();
let result = schemaParser.parseOrThrow(schemaPrisma).join("\n\n");
result = `import { Schema } from "effect";
${result}`;
await Bun.write("src/db/schema.ts", result);
await Bun.$`bun biome format --write src/db/schema.ts`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment