Skip to content

Commit

Permalink
Fail on error, support array operations, CompositeTypes, quoted ident…
Browse files Browse the repository at this point in the history
…ifiers and single-member enums

This makes the library capable of handling the TypeScript types as generated by recent versions of the Supabase CLI as well as makes it able to cover some more exotic scenarios like the aforementioned single-member enums and quoted identifiers.
  • Loading branch information
TomasHubelbauer committed Jun 26, 2024
1 parent 896b752 commit 95214e2
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 4 deletions.
2 changes: 2 additions & 0 deletions src/lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
temp.ts
temp
5 changes: 5 additions & 0 deletions src/lib/get-node-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export const getNodeName = (n: ts.Node) => {
if (ts.isIdentifier(n)) {
name = n.text;
}

// Handle quoted identifiers in case they contain special characters
if (ts.isStringLiteral(n)) {
name = n.text;
}
});
if (!name) throw new Error('Cannot get name of node');
return name;
Expand Down
74 changes: 71 additions & 3 deletions src/lib/transform-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { z } from 'zod';
import { getNodeName } from './get-node-name';

const enumFormatterSchema = z.function().args(z.string()).returns(z.string());
const compositeTypeFormatterSchema = z
.function()
.args(z.string())
.returns(z.string());

const functionFormatterSchema = z
.function()
Expand All @@ -18,6 +22,9 @@ export const transformTypesOptionsSchema = z.object({
sourceText: z.string(),
schema: z.string().default('public'),
enumFormatter: enumFormatterSchema.default(() => (name: string) => name),
compositeTypeFormatter: compositeTypeFormatterSchema.default(
() => (name: string) => name
),
functionFormatter: functionFormatterSchema.default(
() => (name: string, type: string) => `${name}${type}`
),
Expand All @@ -33,8 +40,13 @@ export const transformTypes = z
.args(transformTypesOptionsSchema)
.returns(z.string())
.implement((opts) => {
const { schema, tableOrViewFormatter, enumFormatter, functionFormatter } =
opts;
const {
schema,
tableOrViewFormatter,
enumFormatter,
compositeTypeFormatter,
functionFormatter,
} = opts;
const sourceFile = ts.createSourceFile(
'index.ts',
opts.sourceText,
Expand All @@ -43,6 +55,7 @@ export const transformTypes = z

const typeStrings: string[] = [];
const enumNames: { name: string; formattedName: string }[] = [];
const compositeTypeNames: { name: string; formattedName: string }[] = [];

sourceFile.forEachChild((n) => {
const processDatabase = (n: ts.Node | ts.TypeNode) => {
Expand All @@ -68,7 +81,11 @@ export const transformTypes = z
const operation = getNodeName(n);
if (operation) {
n.forEachChild((n) => {
if (ts.isTypeLiteralNode(n)) {
if (
ts.isTypeLiteralNode(n) ||
// Handle `Relationships` operation which is an array
ts.isTupleTypeNode(n)
) {
typeStrings.push(
`export type ${tableOrViewFormatter(
tableOrViewName,
Expand Down Expand Up @@ -106,6 +123,46 @@ export const transformTypes = z
name: enumName,
});
}

// Handle single-member enums
if (ts.isIdentifier(n)) {
const formattedName = enumFormatter(enumName);
typeStrings.push(
`export type ${formattedName} = '${n.getText(
sourceFile,
)}'`,
);
enumNames.push({
formattedName,
name: enumName,
});
}
});
}
});
}
});
}
if ('CompositeTypes' === n.name.text) {
n.forEachChild((n) => {
if (ts.isTypeLiteralNode(n)) {
n.forEachChild((n) => {
const enumName = getNodeName(n);
if (ts.isPropertySignature(n)) {
n.forEachChild((n) => {
if (ts.isTypeLiteralNode(n)) {
const formattedName =
compositeTypeFormatter(enumName);
typeStrings.push(
`export type ${formattedName} = ${n.getText(
sourceFile,
)}`,
);
compositeTypeNames.push({
formattedName,
name: enumName,
});
}
});
}
});
Expand Down Expand Up @@ -181,5 +238,16 @@ export const transformTypes = z
);
}

for (const { name, formattedName } of compositeTypeNames) {
parsedTypes = parsedTypes.replaceAll(
`Database["${schema}"]["CompositeTypes"]["${name}"]`,
formattedName,
);
parsedTypes = parsedTypes.replaceAll(
`Database['${schema}']['CompositeTypes']['${name}']`,
formattedName,
);
}

return parsedTypes;
});
6 changes: 5 additions & 1 deletion src/supabase-to-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ export default async function supabaseToZod(opts: SupabaseToZodOptions) {

const parsedTypes = transformTypes({ sourceText, ...opts });

const { getZodSchemasFile } = generate({
const { getZodSchemasFile, errors } = generate({
sourceText: parsedTypes,
...opts,
});

if (errors.length > 0) {
throw new Error(errors.join('\n'));
}

const zodSchemasFile = getZodSchemasFile(
getImportPath(outputPath, inputPath)
);
Expand Down

0 comments on commit 95214e2

Please sign in to comment.