diff --git a/index.ts b/index.ts index 0977c59..025d5fe 100644 --- a/index.ts +++ b/index.ts @@ -13,7 +13,11 @@ const writeFiles = async ( supabaseFile: File, hookFiles: HookFile[], metadataFile: File, - componentFiles: File[] + componentFiles: { + tableComponents: File[]; + joinTableComponents: File[]; + viewComponents: File[]; + } ) => { await writeFile( `${DIRECTORY}/${supabaseFile.fileName}`, @@ -29,14 +33,36 @@ const writeFiles = async ( `${DIRECTORY}/${metadataFile.fileName}`, metadataFile.content ); - await Promise.all( - componentFiles.map((hookFile) => { + + const tableComponentPromises = componentFiles.tableComponents.map( + (componentFile) => { return writeFile( - `${DIRECTORY}/components/${hookFile.fileName}`, - hookFile.content + `${DIRECTORY}/components/tables/${componentFile.fileName}`, + componentFile.content ); - }) + } + ); + const joinTableComponentPromises = componentFiles.joinTableComponents.map( + (componentFile) => { + return writeFile( + `${DIRECTORY}/components/joinTables/${componentFile.fileName}`, + componentFile.content + ); + } + ); + const viewComponentPromises = componentFiles.viewComponents.map( + (componentFile) => { + return writeFile( + `${DIRECTORY}/components/views/${componentFile.fileName}`, + componentFile.content + ); + } ); + await Promise.all([ + ...tableComponentPromises, + ...joinTableComponentPromises, + ...viewComponentPromises, + ]); }; const run = async () => { @@ -45,7 +71,9 @@ const run = async () => { await remove(DIRECTORY); await ensureDir(`${DIRECTORY}/hooks`); - await ensureDir(`${DIRECTORY}/components`); + await ensureDir(`${DIRECTORY}/components/tables`); + await ensureDir(`${DIRECTORY}/components/joinTables`); + await ensureDir(`${DIRECTORY}/components/views`); await writeFile(`${DIRECTORY}/${types.fileName}`, types.content); const supabaseFile = await parseSupabaseFile(); diff --git a/package.json b/package.json index 21cbc6a..e0dd213 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@backengine/codegen", - "version": "1.0.16", + "version": "2.0.0", "description": "Generate code for Backengine projects.", "bin": "build/index.js", "files": [ diff --git a/src/components/index.ts b/src/components/index.ts index b4b29ce..7f9f803 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,63 +1,28 @@ -import comment from "../comment"; +import { fetchTables } from "../pgMeta/fetchTables"; import type { File, HookFile } from "../types"; -import prettier from "prettier"; -import { parseNameFormats } from "../utils"; +import { parseComponentFilesForTables } from "./tables"; +import { parseComponentFilesForJoinTables } from "./joinTables"; +import { parseComponentFilesForViews } from "./views"; -const mapHookFileToComponent = async (hookFile: HookFile): Promise => { - const { - file: { fileName }, - } = hookFile; - - const componentName = fileName.replace("use", ""); - const { camelCasePlural } = parseNameFormats(componentName); - - const content = ` - ${comment} - - "use client"; - - import ${fileName} from "../hooks/${fileName}"; - - export default function ${componentName}() { - const { ${camelCasePlural} } = ${fileName}(); - - return ( -
- ${camelCasePlural} -
-            {JSON.stringify(${camelCasePlural}, null, 2)}
-          
-
- ) - }; - `; - - const formattedContent = await prettier.format(content, { - parser: "typescript", - }); +export const parseComponentFiles = async ( + hookFiles: HookFile[] +): Promise<{ + tableComponents: File[]; + joinTableComponents: File[]; + viewComponents: File[]; +}> => { + const { tables, joinTables } = await fetchTables(); + + const tableComponents = await parseComponentFilesForTables(hookFiles, tables); + const joinTableComponents = await parseComponentFilesForJoinTables( + hookFiles, + joinTables + ); + const viewComponents = await parseComponentFilesForViews(hookFiles); return { - fileName: `${componentName}.tsx`, - content: formattedContent, + tableComponents, + joinTableComponents, + viewComponents, }; }; - -export const parseComponentFiles = async ( - hookFiles: HookFile[] -): Promise => { - const componentPromises = hookFiles.map(mapHookFileToComponent); - const files = await Promise.all(componentPromises); - return files; -}; diff --git a/src/components/joinTables/get.ts b/src/components/joinTables/get.ts new file mode 100644 index 0000000..ce62884 --- /dev/null +++ b/src/components/joinTables/get.ts @@ -0,0 +1,164 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import { TablesResponse } from "../../pgMeta/fetchTables"; +import type { File, HookFile } from "../../types"; +import { parseFetchFunctionNamesForJoinTable } from "./utils"; + +export const mapHookFileToGetComponent = async ( + hookFile: HookFile, + joinTables: TablesResponse +): Promise => { + const { + entityName, + file: { fileName }, + } = hookFile; + + const componentName = `${fileName.replace("use", "")}`; + + const { tableOneFetchFunctionName, tableTwoFetchFunctionName } = + parseFetchFunctionNamesForJoinTable(joinTables, entityName); + + const content = ` + ${comment} + + import { useState } from "react"; + import type { TableOneRow, TableTwoRow } from "../../hooks/${fileName}"; + + export default function Get${componentName}({ + ${tableOneFetchFunctionName}, + ${tableTwoFetchFunctionName} + }: + { + ${tableOneFetchFunctionName}: (id: TableOneRow["id"]) => Promise, + ${tableTwoFetchFunctionName}: (id: TableTwoRow["id"]) => Promise + }) { + const [id, setId] = useState(""); + const [data, setData] = useState(); + + return ( +
+
+              {JSON.stringify(data, null, 2)}
+            
+
+ + setId(event.target.value)} + /> +
+
+ + +
+
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Get${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/joinTables/index.ts b/src/components/joinTables/index.ts new file mode 100644 index 0000000..7a7ecfc --- /dev/null +++ b/src/components/joinTables/index.ts @@ -0,0 +1,72 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { TablesResponse } from "../../pgMeta/fetchTables"; +import type { File, HookFile } from "../../types"; +import { mapHookFileToGetComponent } from "./get"; +import { parseFetchFunctionNamesForJoinTable } from "./utils"; + +export const mapHookFileToComponent = async ( + hookFile: HookFile, + joinTables: TablesResponse +): Promise => { + const { + entityName, + file: { fileName }, + } = hookFile; + + const componentName = fileName.replace("use", ""); + const getComponentName = `Get${componentName}`; + + const { tableOneFetchFunctionName, tableTwoFetchFunctionName } = + parseFetchFunctionNamesForJoinTable(joinTables, entityName); + + const content = ` + ${comment} + + "use client"; + + import ${fileName} from "../../hooks/${fileName}"; + import ${getComponentName} from "./${getComponentName}"; + + export default function ${componentName}() { + const { ${tableOneFetchFunctionName}, ${tableTwoFetchFunctionName} } = ${fileName}(); + + return ( +
+ <${getComponentName} ${tableOneFetchFunctionName}={${tableOneFetchFunctionName}} ${tableTwoFetchFunctionName}={${tableTwoFetchFunctionName}} /> +
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `${componentName}.tsx`, + content: formattedContent, + }; +}; + +export const parseComponentFilesForJoinTables = async ( + hookFiles: HookFile[], + joinTables: TablesResponse +): Promise => { + const joinTableHookFiles = hookFiles.filter( + (hookFile) => hookFile.entityType === "JOIN_TABLE" + ); + + const componentPromises = joinTableHookFiles.map((joinTableHookFile) => + mapHookFileToComponent(joinTableHookFile, joinTables) + ); + const getComponentPromises = joinTableHookFiles.map((joinTableHookFile) => + mapHookFileToGetComponent(joinTableHookFile, joinTables) + ); + + const files = await Promise.all([ + ...componentPromises, + ...getComponentPromises, + ]); + return files; +}; diff --git a/src/components/joinTables/utils.ts b/src/components/joinTables/utils.ts new file mode 100644 index 0000000..4cd6eb7 --- /dev/null +++ b/src/components/joinTables/utils.ts @@ -0,0 +1,18 @@ +import type { TablesResponse } from "../../pgMeta/fetchTables"; +import { parseNameFormats } from "../../utils"; + +export const parseFetchFunctionNamesForJoinTable = ( + joinTables: TablesResponse, + entityName: string +) => { + const joinTable = joinTables.find((table) => table.name === entityName)!; + 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}`; + return { tableOneFetchFunctionName, tableTwoFetchFunctionName }; +}; diff --git a/src/components/tables/create.ts b/src/components/tables/create.ts new file mode 100644 index 0000000..c7094f2 --- /dev/null +++ b/src/components/tables/create.ts @@ -0,0 +1,199 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { ColumnResponse, TablesResponse } from "../../pgMeta/fetchTables"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; + +const mapColumns = ( + columns?: ColumnResponse[] +): { fields: string; inputs: string } => { + if (!columns) { + return { fields: "", inputs: "" }; + } + + const filteredColumns = columns.filter((column) => !column.isIdentity); + const fields = filteredColumns.map((column) => `"${column.name}"`).join(","); + const inputs = filteredColumns + .map((column, index) => { + const label = + !column.isNullable && !column.defaultValue + ? `${column.name}*` + : column.name; + return ` +
+ + + 0 ? 'marginTop: "10px",' : ""} + background: "#000", + color: "#fff", + border: "1px solid #34383A", + marginLeft: "10px", + flex: "1", + borderRadius: "0.375rem", + padding: "4px 16px", + }} + /> +
+ `; + }) + .join(" "); + + return { fields, inputs }; +}; + +export const mapHookFileToCreateComponent = async ( + hookFile: HookFile, + tables: TablesResponse +): Promise => { + const { + entityName, + file: { fileName }, + } = hookFile; + + const table = tables.find((table) => table.name === entityName); + const componentName = `${fileName.replace("use", "")}`; + const { pascalCase } = parseNameFormats(componentName); + + const { fields, inputs } = mapColumns(table?.columns); + + const content = ` + ${comment} + + "use client"; + + import { FormEventHandler, MouseEventHandler, useState } from "react"; + import type { Row, Insert${pascalCase} } from "../../hooks/${fileName}"; + + const fields: Array = [${fields}] + + export default function Create${componentName}({ onCreate, onFetch }: { onCreate: (newRow: Insert${pascalCase}) => Promise, onFetch: () => Promise }) { + const [message, setMessage] = useState(); + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + const target = event.target as typeof event.target & Insert${pascalCase}; + const newRow = fields + .map((field) => ({ field, value: (target[field] as any)?.value })) + .reduce((newRow, { field,value }) => { + if (value.trim() !== "") { + newRow[field] = value; + } + return newRow; + }, {} as Record); + onCreate(newRow) + .then((task) => { + if (task) { + setMessage("row with id " + task.id + " created!"); + onFetch(); + } else { + setMessage("failed to create row!"); + } + }) + .catch((error) => { + if (error.message) { + setMessage(error.message); + } else { + setMessage("failed to create row!"); + } + }); + }; + + const handleClick: MouseEventHandler = () => { + setMessage(undefined); + } + + if (message) { + return
+ {message} + +
+ } + + return ( +
+
+ ${inputs} + +
+
+ ); + } + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Create${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/tables/delete.ts b/src/components/tables/delete.ts new file mode 100644 index 0000000..4b5e2c4 --- /dev/null +++ b/src/components/tables/delete.ts @@ -0,0 +1,164 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { File, HookFile } from "../../types"; +import { TablesResponse } from "../../pgMeta/fetchTables"; + +export const mapHookFileToDeleteComponent = async ( + hookFile: HookFile, + tables: TablesResponse +): Promise => { + const { + file: { fileName }, + entityName, + } = hookFile; + + const table = tables.find((table) => table.name === entityName); + const componentName = `${fileName.replace("use", "")}`; + + const primaryKeyColumn = table?.columns?.find( + (column) => column.name === table?.primaryKeys.at(0)?.name + ); + + const content = ` + ${comment} + + "use client"; + + import { FormEventHandler, MouseEventHandler, useState } from "react"; + import type { Row } from "../../hooks/${fileName}"; + + export default function Delete${componentName}({ + onDelete, + onFetch + }: { + onDelete: (id: Row["id"]) => Promise, + onFetch: () => Promise + }) { + const [message, setMessage] = useState(); + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + const target = event.target as typeof event.target & { + id: { value: string }; + }; + onDelete(target.id.value as any).then((count) => { + if (count === 1) { + setMessage("row deleted!"); + onFetch(); + } else { + setMessage("failed to delete row!"); + } + }); + }; + + const handleClick: MouseEventHandler = () => { + setMessage(undefined); + } + + if (message) { + return
+ {message} + +
+ } + + return ( +
+
+
+ + + +
+ +
+
+ ); + } + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Delete${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/tables/get.ts b/src/components/tables/get.ts new file mode 100644 index 0000000..19a40d9 --- /dev/null +++ b/src/components/tables/get.ts @@ -0,0 +1,83 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; + +export const mapHookFileToGetComponent = async ( + hookFile: HookFile +): Promise => { + const { + file: { fileName }, + } = hookFile; + + const componentName = `${fileName.replace("use", "")}`; + const { camelCasePlural } = parseNameFormats(componentName); + + const content = ` + ${comment} + + import type { Row } from "../../hooks/${fileName}"; + + export default function Get${componentName}({ ${camelCasePlural}, onFetch }: { ${camelCasePlural}: Row[], onFetch: () => Promise }) { + return ( +
+
+              {JSON.stringify(${camelCasePlural}, null, 2)}
+            
+ +
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Get${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/tables/index.ts b/src/components/tables/index.ts new file mode 100644 index 0000000..0df9ff5 --- /dev/null +++ b/src/components/tables/index.ts @@ -0,0 +1,89 @@ +import prettier from "prettier"; +import { TablesResponse } from "../../pgMeta/fetchTables"; +import comment from "../../comment"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; +import { mapHookFileToCreateComponent } from "./create"; +import { mapHookFileToUpdateComponent } from "./update"; +import { mapHookFileToDeleteComponent } from "./delete"; +import { mapHookFileToGetComponent } from "./get"; + +export const mapHookFileToComponent = async ( + hookFile: HookFile +): Promise => { + const { + file: { fileName }, + } = hookFile; + + const componentName = fileName.replace("use", ""); + const getComponentName = `Get${componentName}`; + const createComponentName = `Create${componentName}`; + const updateComponentName = `Update${componentName}`; + const deleteComponentName = `Delete${componentName}`; + const { camelCasePlural, pascalCase, pascalCasePlural } = + parseNameFormats(componentName); + + const content = ` + ${comment} + + "use client"; + + import ${fileName} from "../../hooks/${fileName}"; + import ${getComponentName} from "./${getComponentName}"; + import ${createComponentName} from "./${createComponentName}"; + import ${updateComponentName} from "./${updateComponentName}"; + import ${deleteComponentName} from "./${deleteComponentName}"; + + export default function ${componentName}() { + const { ${camelCasePlural}, fetch${pascalCasePlural}, create${pascalCase}, update${pascalCase}, delete${pascalCase} } = ${fileName}(); + + return ( +
+ <${getComponentName} ${camelCasePlural}={${camelCasePlural}} onFetch={fetch${pascalCasePlural}} /> + <${createComponentName} onCreate={create${pascalCase}} onFetch={fetch${pascalCasePlural}} /> + <${updateComponentName} onUpdate={update${pascalCase}} onFetch={fetch${pascalCasePlural}} /> + <${deleteComponentName} onDelete={delete${pascalCase}} onFetch={fetch${pascalCasePlural}} /> +
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `${componentName}.tsx`, + content: formattedContent, + }; +}; + +export const parseComponentFilesForTables = async ( + hookFiles: HookFile[], + tables: TablesResponse +): Promise => { + const tableHookFiles = hookFiles.filter( + (hookFile) => hookFile.entityType === "TABLE" + ); + + const componentPromises = tableHookFiles.map(mapHookFileToComponent); + const getComponentPromises = tableHookFiles.map(mapHookFileToGetComponent); + const createComponentPromises = tableHookFiles.map((tableHookFile) => + mapHookFileToCreateComponent(tableHookFile, tables) + ); + const updateComponentPromises = tableHookFiles.map((tableHookFile) => + mapHookFileToUpdateComponent(tableHookFile, tables) + ); + const deleteComponentPromises = tableHookFiles.map((tableHookFile) => + mapHookFileToDeleteComponent(tableHookFile, tables) + ); + + const files = await Promise.all([ + ...componentPromises, + ...createComponentPromises, + ...updateComponentPromises, + ...getComponentPromises, + ...deleteComponentPromises, + ]); + return files; +}; diff --git a/src/components/tables/update.ts b/src/components/tables/update.ts new file mode 100644 index 0000000..9b0e50f --- /dev/null +++ b/src/components/tables/update.ts @@ -0,0 +1,208 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { ColumnResponse, TablesResponse } from "../../pgMeta/fetchTables"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; + +const mapColumns = ( + columns?: ColumnResponse[] +): { fields: string; inputs: string } => { + if (!columns) { + return { fields: "", inputs: "" }; + } + + const filteredColumns = columns.filter( + (column) => column.isIdentity || column.isUpdatable + ); + const fields = filteredColumns + .filter((column) => !column.isIdentity) + .map((column) => `"${column.name}"`) + .join(","); + const inputs = filteredColumns + .map((column, index) => { + const label = column.isIdentity ? `${column.name}*` : column.name; + return ` +
+ + + 0 ? 'marginTop: "10px",' : ""} + background: "#000", + color: "#fff", + border: "1px solid #34383A", + marginLeft: "10px", + flex: "1", + borderRadius: "0.375rem", + padding: "4px 16px", + }} + /> +
+ `; + }) + .join(" "); + + return { fields, inputs }; +}; + +export const mapHookFileToUpdateComponent = async ( + hookFile: HookFile, + tables: TablesResponse +): Promise => { + const { + entityName, + file: { fileName }, + } = hookFile; + + const table = tables.find((table) => table.name === entityName); + const componentName = `${fileName.replace("use", "")}`; + const { pascalCase } = parseNameFormats(componentName); + + const { fields, inputs } = mapColumns(table?.columns); + + const content = ` + ${comment} + + "use client"; + + import { FormEventHandler, MouseEventHandler, useState } from "react"; + import type { Row, Update${pascalCase} } from "../../hooks/${fileName}"; + + const fields: Array = [${fields}] + + export default function Update${componentName}({ + onUpdate, + onFetch + }: { + onUpdate: (id: Row["id"], updatedRow: Update${pascalCase}) => Promise, + onFetch: () => Promise + }) { + const [message, setMessage] = useState(); + + const handleSubmit: FormEventHandler = (event) => { + event.preventDefault(); + const target = event.target as typeof event.target & Update${pascalCase}; + const id = (target["id"] as any)?.value; + const updatedRow = fields + .map((field) => ({ field, value: (target[field] as any)?.value })) + .reduce((newRow, { field,value }) => { + if (value.trim() !== "") { + newRow[field] = value; + } + return newRow; + }, {} as Record); + onUpdate(id, updatedRow) + .then((task) => { + if (task) { + setMessage("row with id " + task.id + " updated!"); + onFetch(); + } else { + setMessage("failed to update row!"); + } + }) + .catch((error) => { + if (error.message) { + setMessage(error.message); + } else { + setMessage("failed to update row!"); + } + }); + }; + + const handleClick: MouseEventHandler = () => { + setMessage(undefined); + } + + if (message) { + return
+ {message} + +
+ } + + return ( +
+
+ ${inputs} + +
+
+ ); + } + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Update${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/views/get.ts b/src/components/views/get.ts new file mode 100644 index 0000000..19a40d9 --- /dev/null +++ b/src/components/views/get.ts @@ -0,0 +1,83 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; + +export const mapHookFileToGetComponent = async ( + hookFile: HookFile +): Promise => { + const { + file: { fileName }, + } = hookFile; + + const componentName = `${fileName.replace("use", "")}`; + const { camelCasePlural } = parseNameFormats(componentName); + + const content = ` + ${comment} + + import type { Row } from "../../hooks/${fileName}"; + + export default function Get${componentName}({ ${camelCasePlural}, onFetch }: { ${camelCasePlural}: Row[], onFetch: () => Promise }) { + return ( +
+
+              {JSON.stringify(${camelCasePlural}, null, 2)}
+            
+ +
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `Get${componentName}.tsx`, + content: formattedContent, + }; +}; diff --git a/src/components/views/index.ts b/src/components/views/index.ts new file mode 100644 index 0000000..e842818 --- /dev/null +++ b/src/components/views/index.ts @@ -0,0 +1,62 @@ +import prettier from "prettier"; +import comment from "../../comment"; +import type { File, HookFile } from "../../types"; +import { parseNameFormats } from "../../utils"; +import { mapHookFileToGetComponent } from "./get"; + +export const mapHookFileToComponent = async ( + hookFile: HookFile +): Promise => { + const { + file: { fileName }, + } = hookFile; + + const componentName = fileName.replace("use", ""); + const getComponentName = `Get${componentName}`; + const { camelCasePlural, pascalCasePlural } = parseNameFormats(componentName); + + const content = ` + ${comment} + + "use client"; + + import ${fileName} from "../../hooks/${fileName}"; + import ${getComponentName} from "./${getComponentName}"; + + export default function ${componentName}() { + const { ${camelCasePlural}, fetch${pascalCasePlural} } = ${fileName}(); + + return ( +
+ <${getComponentName} ${camelCasePlural}={${camelCasePlural}} onFetch={fetch${pascalCasePlural}} /> +
+ ) + }; + `; + + const formattedContent = await prettier.format(content, { + parser: "typescript", + }); + + return { + fileName: `${componentName}.tsx`, + content: formattedContent, + }; +}; + +export const parseComponentFilesForViews = async ( + hookFiles: HookFile[] +): Promise => { + const viewHookFiles = hookFiles.filter( + (hookFile) => hookFile.entityType === "VIEW" + ); + + const componentPromises = viewHookFiles.map(mapHookFileToComponent); + const getComponentPromises = viewHookFiles.map(mapHookFileToGetComponent); + + const files = await Promise.all([ + ...componentPromises, + ...getComponentPromises, + ]); + return files; +}; diff --git a/src/hooks/joinTable.ts b/src/hooks/joinTable.ts index aa893e0..3f506d0 100644 --- a/src/hooks/joinTable.ts +++ b/src/hooks/joinTable.ts @@ -1,6 +1,6 @@ import prettier from "prettier"; import comment from "../comment"; -import { TableResponse, TablesResponse } from "../pgMeta/fetchTables"; +import type { TableResponse, TablesResponse } from "../pgMeta/fetchTables"; import type { File, HookFile } from "../types"; import { DIRECTORY, parseNameFormats } from "../utils"; @@ -25,38 +25,30 @@ const mapJoinTableToFile = async ( 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"] + export type TableOneRow = Database["public"]["Tables"]["${tableOneFormats.name}"]["Row"] + export type TableTwoRow = 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 ${tableOneFetchFunctionName} = async(id: TableOneRow["id"]) => { + const { data, error } = await supabase + .from("${tableOneFormats.name}") + .select("*, ${tableTwoFormats.name}(*)") + .eq("id", id); + if (error) { + throw error; } + return data.map((d) => d.${tableTwoFormats.name}).flat(); }; - 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); + const ${tableTwoFetchFunctionName} = async(id: TableTwoRow["id"]) => { + const { data, error } = await supabase + .from("${tableTwoFormats.name}") + .select("*, ${tableOneFormats.name}(*)") + .eq("id", id); + if (error) { + throw error; } + return data.map((d) => d.${tableOneFormats.name}).flat(); }; return { ${tableOneFetchFunctionName}, ${tableTwoFetchFunctionName} }; @@ -73,13 +65,14 @@ const mapJoinTableToFile = async ( fileName: `use${joinTableFormats.pascalCasePlural}`, content: formattedContent, }; - const usage = `const { ${tableOneFetchFunctionName} } = use${joinTableFormats.pascalCasePlural}();`; + const usage = `const { ${tableOneFetchFunctionName}, ${tableTwoFetchFunctionName} } = use${joinTableFormats.pascalCasePlural}();`; return { file, location: `${DIRECTORY}/hooks/${file.fileName}.ts`, type: "HOOK", - entity: "TABLE", + entityType: "JOIN_TABLE", + entityName: joinTableFormats.name, usage, }; }; diff --git a/src/hooks/table.ts b/src/hooks/table.ts index 4d6ec92..0daae99 100644 --- a/src/hooks/table.ts +++ b/src/hooks/table.ts @@ -17,83 +17,74 @@ const mapTableToFile = async (table: TableResponse): Promise => { import { Database } from "../types"; type Table = Database["public"]["Tables"]["${tableName}"] - type ${pascalCase} = Table["Row"]; - type Insert${pascalCase} = Table["Insert"]; - type Update${pascalCase} = Table["Update"]; + export type Row = Table["Row"]; + export type Insert${pascalCase} = Table["Insert"]; + export type Update${pascalCase} = Table["Update"]; const use${pascalCasePlural} = () => { - const [${camelCasePlural}, set${pascalCasePlural}] = useState<${pascalCase}[]>([]); + const [${camelCasePlural}, set${pascalCasePlural}] = useState([]); useEffect(() => { fetch${pascalCasePlural}(); }, []); const fetch${pascalCasePlural} = async() => { - try { - const { data, error } = await supabase - .from("${tableName}") - .select(); - if (error) { - throw error; - } - set${pascalCasePlural}(data || []); - } catch (error) { - console.error("Error fetching", error); - } - }; - - const create${pascalCase} = async (newData: Insert${pascalCase}) => { try { const { data, error } = await supabase .from("${tableName}") - .insert([newData]) .select(); if (error) { throw error; } - set${pascalCasePlural}([...${camelCasePlural}, data[0]]); + set${pascalCasePlural}(data || []); } catch (error) { - console.error("Error creating", error); + console.error("Error fetching", error); } }; - const update${pascalCase} = async (id: ${pascalCase}["id"], updatedData: Update${pascalCase}) => { - try { - const { data, error } = await supabase - .from("${tableName}") - .update(updatedData) - .eq("id", id) - .select(); - if (error) { - throw error; - } - set${pascalCasePlural}( - ${camelCasePlural}.map((${camelCase}) => - ${camelCase}.id === id ? { ...${camelCase}, ...data[0] } : ${camelCase} - ) - ); - } catch (error) { - console.error("Error updating alert:", error); + const create${pascalCase} = async (newData: Insert${pascalCase}) => { + const { data, error } = await supabase + .from("${tableName}") + .insert([newData]) + .select(); + if (error) { + throw error; } + set${pascalCasePlural}([...${camelCasePlural}, data[0]]); + return data[0] }; - const delete${pascalCase} = async (id: ${pascalCase}["id"]) => { - try { - const { error } = await supabase - .from("${tableName}") - .delete() - .eq("id", id); - if (error) { - throw error; - } - const filtered = ${camelCasePlural}.filter((${camelCase}) => ${camelCase}.id !== id); - set${pascalCasePlural}(filtered); - } catch (error) { - console.error("Error deleting", error); + const update${pascalCase} = async (id: Row["id"], updatedData: Update${pascalCase}) => { + const { data, error } = await supabase + .from("${tableName}") + .update(updatedData) + .eq("id", id) + .select(); + if (error) { + throw error; + } + set${pascalCasePlural}( + ${camelCasePlural}.map((${camelCase}) => + ${camelCase}.id === id ? { ...${camelCase}, ...data[0] } : ${camelCase} + ) + ); + return data[0]; + }; + + const delete${pascalCase} = async (id: Row["id"]): Promise => { + const { error, count } = await supabase + .from("${tableName}") + .delete({ count: "exact" }) + .eq("id", id); + if (error) { + throw error; } + const filtered = ${camelCasePlural}.filter((${camelCase}) => ${camelCase}.id !== id); + set${pascalCasePlural}(filtered); + return count }; - return { ${camelCasePlural}, create${pascalCase}, update${pascalCase}, delete${pascalCase} }; + return { ${camelCasePlural}, fetch${pascalCasePlural}, create${pascalCase}, update${pascalCase}, delete${pascalCase} }; }; export default use${pascalCasePlural}; @@ -107,13 +98,14 @@ const mapTableToFile = async (table: TableResponse): Promise => { fileName: `use${pascalCasePlural}`, content: formattedContent, }; - const usage = `const { ${camelCasePlural}, create${pascalCase}, update${pascalCase}, delete${pascalCase} } = use${pascalCasePlural}();`; + const usage = `const { ${camelCasePlural}, fetch${pascalCasePlural}, create${pascalCase}, update${pascalCase}, delete${pascalCase} } = use${pascalCasePlural}();`; return { file, location: `${DIRECTORY}/hooks/${file.fileName}.ts`, type: "HOOK", - entity: "TABLE", + entityType: "TABLE", + entityName: tableName, usage, }; }; diff --git a/src/hooks/view.ts b/src/hooks/view.ts index 5270077..848a24f 100644 --- a/src/hooks/view.ts +++ b/src/hooks/view.ts @@ -54,8 +54,7 @@ const parseViewNames = async (): Promise => { }; const mapViewToFile = async (viewName: string): Promise => { - const { pascalCase, pascalCasePlural, camelCasePlural } = - parseNameFormats(viewName); + const { pascalCasePlural, camelCasePlural } = parseNameFormats(viewName); const content = ` ${comment} @@ -65,30 +64,30 @@ const mapViewToFile = async (viewName: string): Promise => { import { Database } from "../types"; type View = Database["public"]["Views"]["${viewName}"] - type ${pascalCase} = View["Row"]; + export type Row = View["Row"]; const use${pascalCasePlural} = () => { - const [${camelCasePlural}, set${pascalCasePlural}] = useState<${pascalCase}[]>([]); + const [${camelCasePlural}, set${pascalCasePlural}] = useState([]); useEffect(() => { fetch${pascalCasePlural}(); }, []); const fetch${pascalCasePlural} = async() => { - try { - const { data, error } = await supabase - .from("${viewName}") - .select(); - if (error) { - throw error; - } - set${pascalCasePlural}(data || []); - } catch (error) { - console.error("Error fetching", error); + try { + const { data, error } = await supabase + .from("${viewName}") + .select(); + if (error) { + throw error; } + set${pascalCasePlural}(data || []); + } catch (error) { + console.error("Error fetching", error); + } }; - return { ${camelCasePlural} }; + return { ${camelCasePlural}, fetch${pascalCasePlural} }; }; export default use${pascalCasePlural}; @@ -108,7 +107,8 @@ const mapViewToFile = async (viewName: string): Promise => { file, location: `${DIRECTORY}/hooks/${file.fileName}.ts`, type: "HOOK", - entity: "VIEW", + entityType: "VIEW", + entityName: viewName, usage, }; }; diff --git a/src/metadata.ts b/src/metadata.ts index 960718c..f8005e9 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -10,7 +10,8 @@ export const parseMetadataFile = async ( name: hookFile.file.fileName, location: hookFile.location, type: hookFile.type, - entity: hookFile.entity, + entityType: hookFile.entityType, + entityName: hookFile.entityName, usage: hookFile.usage, }; }) diff --git a/src/pgMeta/fetchTables.ts b/src/pgMeta/fetchTables.ts index 7e8de22..3be7b2d 100644 --- a/src/pgMeta/fetchTables.ts +++ b/src/pgMeta/fetchTables.ts @@ -74,6 +74,7 @@ export async function fetchTables(): Promise<{ (table) => table.schema === "public" ); + // TODO: handle tables with a primary key column not named "id" const tables = publicTables.filter(({ primaryKeys }) => primaryKeys.some(({ name }) => name === "id") ); diff --git a/src/types.ts b/src/types.ts index 3dd5e7d..3cfeb8d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,8 @@ export type HookFile = { file: File; location: string; type: "HOOK"; - entity: "TABLE" | "VIEW"; + entityType: "TABLE" | "JOIN_TABLE" | "VIEW"; + entityName: string; usage: string; };