Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@backengine/codegen",
"version": "1.0.15",
"version": "1.0.16",
"description": "Generate code for Backengine projects.",
"bin": "build/index.js",
"files": [
Expand Down
9 changes: 7 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { fetchTables } from "../pgMeta/fetchTables";
import type { HookFile } from "../types";
import { parseTableFiles } from "./table";
import { parseJoinTableFiles } from "./joinTable";
import { parseViewFiles } from "./view";

export const parseHookFiles = async (): Promise<HookFile[]> => {
const tableFiles = await parseTableFiles();
const { tables, joinTables } = await fetchTables();

const tableFiles = await parseTableFiles(tables);
const joinTableFiles = await parseJoinTableFiles(joinTables);
const viewFiles = await parseViewFiles();
return [...tableFiles, ...viewFiles];
return [...tableFiles, ...joinTableFiles, ...viewFiles];
};
95 changes: 95 additions & 0 deletions src/hooks/joinTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import prettier from "prettier";
import comment from "../comment";
import { TableResponse, TablesResponse } from "../pgMeta/fetchTables";
import type { File, HookFile } from "../types";
import { DIRECTORY, parseNameFormats } from "../utils";

const mapJoinTableToFile = async (
joinTable: TableResponse
): Promise<HookFile> => {
const joinTableFormats = parseNameFormats(joinTable.name);

const tableOneFormats = parseNameFormats(
joinTable.relationships.at(0)!.targetTableName
);
const tableTwoFormats = parseNameFormats(
joinTable.relationships.at(1)!.targetTableName
);

const tableOneFetchFunctionName = `fetch${tableTwoFormats.pascalCasePlural}For${tableOneFormats.pascalCase}`;
const tableTwoFetchFunctionName = `fetch${tableOneFormats.pascalCasePlural}For${tableTwoFormats.pascalCase}`;

const content = `
${comment}

import { supabase } from "../supabase";
import { Database } from "../types";

type ${tableOneFormats.pascalCase}Row = Database["public"]["Tables"]["${tableOneFormats.name}"]["Row"]
type ${tableTwoFormats.pascalCase}Row = Database["public"]["Tables"]["${tableTwoFormats.name}"]["Row"]

const use${joinTableFormats.pascalCasePlural} = () => {
const ${tableOneFetchFunctionName} = async(id: ${tableOneFormats.pascalCase}Row["id"]) => {
try {
const { data, error } = await supabase
.from("${tableOneFormats.name}")
.select("*, ${tableTwoFormats.name}(*)")
.eq("id", id);
if (error) {
throw error;
}
return data
} catch (error) {
console.error("Error fetching", error);
}
};

const ${tableTwoFetchFunctionName} = async(id: ${tableTwoFormats.pascalCase}Row["id"]) => {
try {
const { data, error } = await supabase
.from("${tableTwoFormats.name}")
.select("*, ${tableOneFormats.name}(*)")
.eq("id", id);
if (error) {
throw error;
}
return data
} catch (error) {
console.error("Error fetching", error);
}
};

return { ${tableOneFetchFunctionName}, ${tableTwoFetchFunctionName} };
};

export default use${joinTableFormats.pascalCasePlural};
`;

const formattedContent = await prettier.format(content, {
parser: "typescript",
});

const file: File = {
fileName: `use${joinTableFormats.pascalCasePlural}`,
content: formattedContent,
};
const usage = `const { ${tableOneFetchFunctionName} } = use${joinTableFormats.pascalCasePlural}();`;

return {
file,
location: `${DIRECTORY}/hooks/${file.fileName}.ts`,
type: "HOOK",
entity: "TABLE",
usage,
};
};

export const parseJoinTableFiles = async (
joinTables: TablesResponse
): Promise<HookFile[]> => {
const joinTableHookPromises =
joinTables.map<Promise<HookFile>>(mapJoinTableToFile);
const joinTableFiles = await Promise.all(joinTableHookPromises);

return joinTableFiles;
};
92 changes: 11 additions & 81 deletions src/hooks/table.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,11 @@
import axios from "axios";
import prettier from "prettier";
import comment from "../comment";
import type { File, HookFile } from "../types";
import { DIRECTORY, log, parseNameFormats } from "../utils";

export type ColumnResponse = {
tableId: number;
schema: string;
table: string;
id: string;
ordinalPosition: number;
name: string;
defaultValue: unknown;
dataType: string;
format: string;
isIdentity: boolean;
identityGeneration: "ALWAYS" | "BY DEFAULT" | null;
isGenerated: boolean;
isNullable: boolean;
isUpdatable: boolean;
isUnique: boolean;
enums: string[];
check: string | null;
comment: string | null;
};

export type TableResponse = {
id: number;
schema: string;
name: string;
rlsEnabled: boolean;
rlsForced: boolean;
replicaIdentity: "DEFAULT" | "INDEX" | "FULL" | "NOTHING";
bytes: number;
size: string;
liveRowsEstimate: number;
deadRowsEstimate: number;
comment: string | null;
columns?: ColumnResponse[];
primaryKeys: {
schema: string;
tableName: string;
name: string;
tableId: number;
}[];
relationships: {
id: number;
constraintName: string;
sourceSchema: string;
sourceTableName: string;
sourceColumnName: string;
targetTableSchema: string;
targetTableName: string;
targetColumnName: string;
}[];
};
import { DIRECTORY, parseNameFormats } from "../utils";
import { TableResponse, TablesResponse } from "../pgMeta/fetchTables";

export type TablesResponse = TableResponse[];

const parseTableNames = async (): Promise<string[]> => {
const tablesResponse = await axios.get<TablesResponse>(
`${process.env.BACKENGINE_BASE_URL}/api/v1/projects/${process.env.BACKENGINE_PROJECT_ID}/pg-meta/tables`,
{
headers: {
Authorization: `Bearer ${process.env.BACKENGINE_API_KEY}`,
Accept: "application/json",
},
}
);
log("Fetched table metadata");

const publicTables = tablesResponse.data
.filter((table) => table.schema === "public")
.filter(({ primaryKeys }) => primaryKeys.some(({ name }) => name === "id"));

return publicTables.map((table) => table.name);
};

const mapTableToFile = async (tableName: string): Promise<HookFile> => {
const mapTableToFile = async (table: TableResponse): Promise<HookFile> => {
const { name: tableName } = table;
const { pascalCase, pascalCasePlural, camelCase, camelCasePlural } =
parseNameFormats(tableName);

Expand Down Expand Up @@ -190,9 +118,11 @@ const mapTableToFile = async (tableName: string): Promise<HookFile> => {
};
};

export const parseTableFiles = async (): Promise<HookFile[]> => {
const tableNames = await parseTableNames();
const hookPromises = tableNames.map<Promise<HookFile>>(mapTableToFile);
const files = await Promise.all(hookPromises);
return files;
export const parseTableFiles = async (
tables: TablesResponse
): Promise<HookFile[]> => {
const tableHookPromises = tables.map<Promise<HookFile>>(mapTableToFile);
const tableFiles = await Promise.all(tableHookPromises);

return tableFiles;
};
85 changes: 85 additions & 0 deletions src/pgMeta/fetchTables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import axios from "axios";
import { log } from "../utils";

export type ColumnResponse = {
tableId: number;
schema: string;
table: string;
id: string;
ordinalPosition: number;
name: string;
defaultValue: unknown;
dataType: string;
format: string;
isIdentity: boolean;
identityGeneration: "ALWAYS" | "BY DEFAULT" | null;
isGenerated: boolean;
isNullable: boolean;
isUpdatable: boolean;
isUnique: boolean;
enums: string[];
check: string | null;
comment: string | null;
};

export type TableResponse = {
id: number;
schema: string;
name: string;
rlsEnabled: boolean;
rlsForced: boolean;
replicaIdentity: "DEFAULT" | "INDEX" | "FULL" | "NOTHING";
bytes: number;
size: string;
liveRowsEstimate: number;
deadRowsEstimate: number;
comment: string | null;
columns?: ColumnResponse[];
primaryKeys: {
schema: string;
tableName: string;
name: string;
tableId: number;
}[];
relationships: {
id: number;
constraintName: string;
sourceSchema: string;
sourceTableName: string;
sourceColumnName: string;
targetTableSchema: string;
targetTableName: string;
targetColumnName: string;
}[];
};

export type TablesResponse = TableResponse[];

export async function fetchTables(): Promise<{
tables: TablesResponse;
joinTables: TablesResponse;
}> {
const tablesResponse = await axios.get<TablesResponse>(
`${process.env.BACKENGINE_BASE_URL}/api/v1/projects/${process.env.BACKENGINE_PROJECT_ID}/pg-meta/tables`,
{
headers: {
Authorization: `Bearer ${process.env.BACKENGINE_API_KEY}`,
Accept: "application/json",
},
}
);
log("Fetched table metadata");

const publicTables = tablesResponse.data.filter(
(table) => table.schema === "public"
);

const tables = publicTables.filter(({ primaryKeys }) =>
primaryKeys.some(({ name }) => name === "id")
);
const joinTables = publicTables.filter(
({ primaryKeys, relationships }) =>
primaryKeys.length === 2 && relationships.length === 2
);
return { tables, joinTables };
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ const sanitiseFormat = (format: string): string => {
export const parseNameFormats = (
name: string
): {
name: string;
pascalCase: string;
pascalCasePlural: string;
camelCase: string;
camelCasePlural: string;
} => {
return {
name,
pascalCase: sanitiseFormat(singular(toPascalCase(name))),
pascalCasePlural: sanitiseFormat(plural(toPascalCase(name))),
camelCase: sanitiseFormat(singular(toCamelCase(name))),
Expand Down