postgresdk 0.4.0 → 0.5.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -772,8 +772,8 @@ export type Update${Type} = z.infer<typeof Update${Type}Schema>;
772
772
  `;
773
773
  }
774
774
 
775
- // src/emit-routes.ts
776
- function emitRoutes(table, _graph, opts) {
775
+ // src/emit-routes-hono.ts
776
+ function emitHonoRoutes(table, _graph, opts) {
777
777
  const fileTableName = table.name;
778
778
  const Type = pascal(table.name);
779
779
  const rawPk = table.pk;
@@ -782,34 +782,36 @@ function emitRoutes(table, _graph, opts) {
782
782
  const hasCompositePk = safePkCols.length > 1;
783
783
  const pkPath = hasCompositePk ? safePkCols.map((c) => `:${c}`).join("/") : `:${safePkCols[0]}`;
784
784
  const softDel = opts.softDeleteColumn && table.columns.some((c) => c.name === opts.softDeleteColumn) ? opts.softDeleteColumn : null;
785
- const wherePkSql = hasCompositePk ? safePkCols.map((c, i) => `"${c}" = $${i + 1}`).join(" AND ") : `"${safePkCols[0]}" = $1`;
786
785
  const getPkParams = hasCompositePk ? `const pkValues = [${safePkCols.map((c) => `c.req.param("${c}")`).join(", ")}];` : `const pkValues = [c.req.param("${safePkCols[0]}")];`;
787
- const updateSetSql = hasCompositePk ? `Object.keys(updateData).map((k, i) => \`"\${k}" = $\${i + ${safePkCols.length} + 1}\`).join(", ")` : `Object.keys(updateData).map((k, i) => \`"\${k}" = $\${i + 2}\`).join(", ")`;
788
- const pkFilter = safePkCols.length ? `const updateData = Object.fromEntries(Object.entries(parsed.data).filter(([k]) => !new Set(${JSON.stringify(safePkCols)}).has(k)));` : `const updateData = parsed.data;`;
789
786
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
790
- const authImport = hasAuth ? `import { authMiddleware } from "../auth";` : "";
787
+ const ext = opts.useJsExtensions ? ".js" : "";
788
+ const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
791
789
  return `/* Generated. Do not edit. */
792
790
  import { Hono } from "hono";
793
791
  import { z } from "zod";
794
- import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}";
795
- import { loadIncludes } from "../include-loader";
792
+ import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
793
+ import { loadIncludes } from "../include-loader${ext}";
794
+ import * as coreOps from "../core/operations${ext}";
796
795
  ${authImport}
797
796
 
798
- const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
799
- const log = {
800
- debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
801
- error: (...args: any[]) => console.error("[sdk]", ...args),
802
- };
803
-
804
797
  const listSchema = z.object({
805
- include: z.any().optional(), // TODO: typed include spec in later pass
798
+ include: z.any().optional(),
806
799
  limit: z.number().int().positive().max(100).optional(),
807
800
  offset: z.number().int().min(0).optional(),
808
- orderBy: z.any().optional() // TODO: typed orderBy in a later pass
801
+ orderBy: z.any().optional()
809
802
  });
810
803
 
811
804
  export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }) {
812
805
  const base = "/v1/${fileTableName}";
806
+
807
+ // Create operation context
808
+ const ctx: coreOps.OperationContext = {
809
+ pg: deps.pg,
810
+ table: "${fileTableName}",
811
+ pkColumns: ${JSON.stringify(safePkCols)},
812
+ softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
813
+ includeDepthLimit: ${opts.includeDepthLimit}
814
+ };
813
815
  ${hasAuth ? `
814
816
  // \uD83D\uDD10 Auth: protect all routes for this table
815
817
  app.use(base, authMiddleware);
@@ -817,142 +819,114 @@ ${hasAuth ? `
817
819
 
818
820
  // CREATE
819
821
  app.post(base, async (c) => {
820
- try {
821
- const body = await c.req.json().catch(() => ({}));
822
- log.debug("POST ${fileTableName} body:", body);
823
- const parsed = Insert${Type}Schema.safeParse(body);
824
- if (!parsed.success) {
825
- const issues = parsed.error.flatten();
826
- log.debug("POST ${fileTableName} invalid:", issues);
827
- return c.json({ error: "Invalid body", issues }, 400);
828
- }
829
-
830
- const data = parsed.data;
831
- const cols = Object.keys(data);
832
- const vals = Object.values(data);
833
- if (!cols.length) return c.json({ error: "No fields provided" }, 400);
834
-
835
- const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
836
- const text = \`INSERT INTO "${fileTableName}" (\${cols.map(c => '"' + c + '"').join(", ")})
837
- VALUES (\${placeholders})
838
- RETURNING *\`;
839
- log.debug("SQL:", text, "vals:", vals);
840
- const { rows } = await deps.pg.query(text, vals);
841
- return c.json(rows[0] ?? null, rows[0] ? 201 : 500);
842
- } catch (e: any) {
843
- log.error("POST ${fileTableName} error:", e?.stack ?? e);
844
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
822
+ const body = await c.req.json().catch(() => ({}));
823
+ const parsed = Insert${Type}Schema.safeParse(body);
824
+
825
+ if (!parsed.success) {
826
+ const issues = parsed.error.flatten();
827
+ return c.json({ error: "Invalid body", issues }, 400);
845
828
  }
829
+
830
+ const result = await coreOps.createRecord(ctx, parsed.data);
831
+
832
+ if (result.error) {
833
+ return c.json({ error: result.error }, result.status as any);
834
+ }
835
+
836
+ return c.json(result.data, result.status as any);
846
837
  });
847
838
 
848
839
  // GET BY PK
849
840
  app.get(\`\${base}/${pkPath}\`, async (c) => {
850
- try {
851
- ${getPkParams}
852
- const text = \`SELECT * FROM "${fileTableName}" WHERE ${wherePkSql} LIMIT 1\`;
853
- log.debug("GET ${fileTableName} by PK:", pkValues, "SQL:", text);
854
- const { rows } = await deps.pg.query(text, pkValues);
855
- if (!rows[0]) return c.json(null, 404);
856
- return c.json(rows[0]);
857
- } catch (e: any) {
858
- log.error("GET ${fileTableName} error:", e?.stack ?? e);
859
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
841
+ ${getPkParams}
842
+ const result = await coreOps.getByPk(ctx, pkValues);
843
+
844
+ if (result.error) {
845
+ return c.json({ error: result.error }, result.status as any);
860
846
  }
847
+
848
+ return c.json(result.data, result.status as any);
861
849
  });
862
850
 
863
851
  // LIST
864
852
  app.post(\`\${base}/list\`, async (c) => {
865
- try {
866
- const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
867
- if (!body.success) {
868
- const issues = body.error.flatten();
869
- log.debug("LIST ${fileTableName} invalid:", issues);
870
- return c.json({ error: "Invalid body", issues }, 400);
871
- }
872
- const { include, limit = 50, offset = 0 } = body.data;
873
-
874
- const where = ${softDel ? `\`WHERE "${softDel}" IS NULL\`` : `""`};
875
- const text = \`SELECT * FROM "${fileTableName}" \${where} LIMIT $1 OFFSET $2\`;
876
- log.debug("LIST ${fileTableName} SQL:", text, "params:", [limit, offset]);
877
- const { rows } = await deps.pg.query(text, [limit, offset]);
878
-
879
- if (!include) {
880
- log.debug("LIST ${fileTableName} rows:", rows.length);
881
- return c.json(rows);
882
- }
883
-
884
- // Attempt include stitching with explicit error handling
885
- log.debug("LIST ${fileTableName} include spec:", include);
853
+ const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
854
+
855
+ if (!body.success) {
856
+ const issues = body.error.flatten();
857
+ return c.json({ error: "Invalid body", issues }, 400);
858
+ }
859
+
860
+ const result = await coreOps.listRecords(ctx, body.data);
861
+
862
+ if (result.error) {
863
+ return c.json({ error: result.error }, result.status as any);
864
+ }
865
+
866
+ // Handle includes if needed
867
+ if (result.needsIncludes && result.includeSpec) {
886
868
  try {
887
- const stitched = await loadIncludes("${fileTableName}", rows, include, deps.pg, ${opts.includeDepthLimit});
888
- log.debug("LIST ${fileTableName} stitched count:", Array.isArray(stitched) ? stitched.length : "n/a");
869
+ const stitched = await loadIncludes(
870
+ "${fileTableName}",
871
+ result.data,
872
+ result.includeSpec,
873
+ deps.pg,
874
+ ${opts.includeDepthLimit}
875
+ );
889
876
  return c.json(stitched);
890
877
  } catch (e: any) {
891
- const strict = process.env.SDK_STRICT_INCLUDE === "1" || process.env.SDK_STRICT_INCLUDE === "true";
892
- const msg = e?.message ?? String(e);
893
- const stack = e?.stack;
894
- log.error("LIST ${fileTableName} include stitch FAILED:", msg, stack);
895
-
878
+ const strict = process.env.SDK_STRICT_INCLUDE === "1";
896
879
  if (strict) {
897
- return c.json({ error: "include-stitch-failed", message: msg, ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
880
+ return c.json({
881
+ error: "include-stitch-failed",
882
+ message: e?.message,
883
+ ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
884
+ }, 500);
898
885
  }
899
- // Non-strict fallback: return base rows plus error metadata
900
- return c.json({ data: rows, includeError: { message: msg, ...(DEBUG ? { stack: e?.stack } : {}) } }, 200);
886
+ // Non-strict: return base rows with error metadata
887
+ return c.json({
888
+ data: result.data,
889
+ includeError: {
890
+ message: e?.message,
891
+ ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
892
+ }
893
+ }, 200);
901
894
  }
902
- } catch (e: any) {
903
- log.error("LIST ${fileTableName} error:", e?.stack ?? e);
904
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
905
895
  }
896
+
897
+ return c.json(result.data, result.status as any);
906
898
  });
907
899
 
908
900
  // UPDATE
909
901
  app.patch(\`\${base}/${pkPath}\`, async (c) => {
910
- try {
911
- ${getPkParams}
912
- const body = await c.req.json().catch(() => ({}));
913
- log.debug("PATCH ${fileTableName} pk:", pkValues, "patch:", body);
914
- const parsed = Update${Type}Schema.safeParse(body);
915
- if (!parsed.success) {
916
- const issues = parsed.error.flatten();
917
- log.debug("PATCH ${fileTableName} invalid:", issues);
918
- return c.json({ error: "Invalid body", issues: issues }, 400);
919
- }
920
-
921
- ${pkFilter}
922
- if (!Object.keys(updateData).length) return c.json({ error: "No updatable fields provided" }, 400);
923
-
924
- const setSql = ${updateSetSql};
925
- const text = \`UPDATE "${fileTableName}" SET \${setSql} WHERE ${wherePkSql} RETURNING *\`;
926
- const params = ${hasCompositePk ? `[...pkValues, ...Object.values(updateData)]` : `[pkValues[0], ...Object.values(updateData)]`};
927
- log.debug("PATCH ${fileTableName} SQL:", text, "params:", params);
928
- const { rows } = await deps.pg.query(text, params);
929
- if (!rows[0]) return c.json(null, 404);
930
- return c.json(rows[0]);
931
- } catch (e: any) {
932
- log.error("PATCH ${fileTableName} error:", e?.stack ?? e);
933
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
902
+ ${getPkParams}
903
+ const body = await c.req.json().catch(() => ({}));
904
+ const parsed = Update${Type}Schema.safeParse(body);
905
+
906
+ if (!parsed.success) {
907
+ const issues = parsed.error.flatten();
908
+ return c.json({ error: "Invalid body", issues }, 400);
934
909
  }
910
+
911
+ const result = await coreOps.updateRecord(ctx, pkValues, parsed.data);
912
+
913
+ if (result.error) {
914
+ return c.json({ error: result.error }, result.status as any);
915
+ }
916
+
917
+ return c.json(result.data, result.status as any);
935
918
  });
936
919
 
937
- // DELETE (soft or hard)
920
+ // DELETE
938
921
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
939
- try {
940
- ${getPkParams}
941
- ${softDel ? `
942
- const text = \`UPDATE "${fileTableName}" SET "${softDel}" = NOW() WHERE ${wherePkSql} RETURNING *\`;
943
- log.debug("DELETE (soft) ${fileTableName} SQL:", text, "pk:", pkValues);
944
- const { rows } = await deps.pg.query(text, pkValues);
945
- if (!rows[0]) return c.json(null, 404);
946
- return c.json(rows[0]);` : `
947
- const text = \`DELETE FROM "${fileTableName}" WHERE ${wherePkSql} RETURNING *\`;
948
- log.debug("DELETE ${fileTableName} SQL:", text, "pk:", pkValues);
949
- const { rows } = await deps.pg.query(text, pkValues);
950
- if (!rows[0]) return c.json(null, 404);
951
- return c.json(rows[0]);`}
952
- } catch (e: any) {
953
- log.error("DELETE ${fileTableName} error:", e?.stack ?? e);
954
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
922
+ ${getPkParams}
923
+ const result = await coreOps.deleteRecord(ctx, pkValues);
924
+
925
+ if (result.error) {
926
+ return c.json({ error: result.error }, result.status as any);
955
927
  }
928
+
929
+ return c.json(result.data, result.status as any);
956
930
  });
957
931
  }
958
932
  `;
@@ -1212,13 +1186,14 @@ export abstract class BaseClient {
1212
1186
  }
1213
1187
 
1214
1188
  // src/emit-include-loader.ts
1215
- function emitIncludeLoader(graph, model, maxDepth) {
1189
+ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
1216
1190
  const fkIndex = {};
1217
1191
  for (const t of Object.values(model.tables)) {
1218
1192
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
1219
1193
  }
1194
+ const ext = useJsExtensions ? ".js" : "";
1220
1195
  return `/* Generated. Do not edit. */
1221
- import { RELATION_GRAPH } from "./include-builder";
1196
+ import { RELATION_GRAPH } from "./include-builder${ext}";
1222
1197
 
1223
1198
  // Minimal types to keep the file self-contained
1224
1199
  type Graph = typeof RELATION_GRAPH;
@@ -1737,12 +1712,13 @@ export async function authMiddleware(c: Context, next: Next) {
1737
1712
  `;
1738
1713
  }
1739
1714
 
1740
- // src/emit-router.ts
1741
- function emitRouter(tables, hasAuth) {
1715
+ // src/emit-router-hono.ts
1716
+ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
1742
1717
  const tableNames = tables.map((t) => t.name).sort();
1718
+ const ext = useJsExtensions ? ".js" : "";
1743
1719
  const imports = tableNames.map((name) => {
1744
1720
  const Type = pascal(name);
1745
- return `import { register${Type}Routes } from "./routes/${name}";`;
1721
+ return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
1746
1722
  }).join(`
1747
1723
  `);
1748
1724
  const registrations = tableNames.map((name) => {
@@ -1752,14 +1728,14 @@ function emitRouter(tables, hasAuth) {
1752
1728
  `);
1753
1729
  const reExports = tableNames.map((name) => {
1754
1730
  const Type = pascal(name);
1755
- return `export { register${Type}Routes } from "./routes/${name}";`;
1731
+ return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
1756
1732
  }).join(`
1757
1733
  `);
1758
1734
  return `/* Generated. Do not edit. */
1759
1735
  import { Hono } from "hono";
1760
- import { SDK_MANIFEST } from "./sdk-bundle";
1736
+ import { SDK_MANIFEST } from "./sdk-bundle${ext}";
1761
1737
  ${imports}
1762
- ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1738
+ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
1763
1739
 
1764
1740
  /**
1765
1741
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
@@ -1875,6 +1851,222 @@ export const SDK_MANIFEST = {
1875
1851
  `;
1876
1852
  }
1877
1853
 
1854
+ // src/emit-core-operations.ts
1855
+ function emitCoreOperations() {
1856
+ return `/**
1857
+ * Core database operations that are framework-agnostic.
1858
+ * These functions handle the actual database logic and can be used by any framework adapter.
1859
+ */
1860
+
1861
+ import type { z } from "zod";
1862
+
1863
+ export interface DatabaseClient {
1864
+ query: (text: string, params?: any[]) => Promise<{ rows: any[] }>;
1865
+ }
1866
+
1867
+ export interface OperationContext {
1868
+ pg: DatabaseClient;
1869
+ table: string;
1870
+ pkColumns: string[];
1871
+ softDeleteColumn?: string | null;
1872
+ includeDepthLimit: number;
1873
+ }
1874
+
1875
+ const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
1876
+ const log = {
1877
+ debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
1878
+ error: (...args: any[]) => console.error("[sdk]", ...args),
1879
+ };
1880
+
1881
+ /**
1882
+ * CREATE operation - Insert a new record
1883
+ */
1884
+ export async function createRecord(
1885
+ ctx: OperationContext,
1886
+ data: Record<string, any>
1887
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
1888
+ try {
1889
+ const cols = Object.keys(data);
1890
+ const vals = Object.values(data);
1891
+
1892
+ if (!cols.length) {
1893
+ return { error: "No fields provided", status: 400 };
1894
+ }
1895
+
1896
+ const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
1897
+ const text = \`INSERT INTO "\${ctx.table}" (\${cols.map(c => '"' + c + '"').join(", ")})
1898
+ VALUES (\${placeholders})
1899
+ RETURNING *\`;
1900
+
1901
+ log.debug("SQL:", text, "vals:", vals);
1902
+ const { rows } = await ctx.pg.query(text, vals);
1903
+
1904
+ return { data: rows[0] ?? null, status: rows[0] ? 201 : 500 };
1905
+ } catch (e: any) {
1906
+ log.error(\`POST \${ctx.table} error:\`, e?.stack ?? e);
1907
+ return {
1908
+ error: e?.message ?? "Internal error",
1909
+ ...(DEBUG ? { stack: e?.stack } : {}),
1910
+ status: 500
1911
+ };
1912
+ }
1913
+ }
1914
+
1915
+ /**
1916
+ * READ operation - Get a record by primary key
1917
+ */
1918
+ export async function getByPk(
1919
+ ctx: OperationContext,
1920
+ pkValues: any[]
1921
+ ): Promise<{ data?: any; error?: string; status: number }> {
1922
+ try {
1923
+ const hasCompositePk = ctx.pkColumns.length > 1;
1924
+ const wherePkSql = hasCompositePk
1925
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
1926
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
1927
+
1928
+ const text = \`SELECT * FROM "\${ctx.table}" WHERE \${wherePkSql} LIMIT 1\`;
1929
+ log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
1930
+
1931
+ const { rows } = await ctx.pg.query(text, pkValues);
1932
+
1933
+ if (!rows[0]) {
1934
+ return { data: null, status: 404 };
1935
+ }
1936
+
1937
+ return { data: rows[0], status: 200 };
1938
+ } catch (e: any) {
1939
+ log.error(\`GET \${ctx.table} error:\`, e?.stack ?? e);
1940
+ return {
1941
+ error: e?.message ?? "Internal error",
1942
+ ...(DEBUG ? { stack: e?.stack } : {}),
1943
+ status: 500
1944
+ };
1945
+ }
1946
+ }
1947
+
1948
+ /**
1949
+ * LIST operation - Get multiple records with optional filters
1950
+ */
1951
+ export async function listRecords(
1952
+ ctx: OperationContext,
1953
+ params: { limit?: number; offset?: number; include?: any }
1954
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number; needsIncludes?: boolean; includeSpec?: any }> {
1955
+ try {
1956
+ const { limit = 50, offset = 0, include } = params;
1957
+
1958
+ const where = ctx.softDeleteColumn
1959
+ ? \`WHERE "\${ctx.softDeleteColumn}" IS NULL\`
1960
+ : "";
1961
+
1962
+ const text = \`SELECT * FROM "\${ctx.table}" \${where} LIMIT $1 OFFSET $2\`;
1963
+ log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", [limit, offset]);
1964
+
1965
+ const { rows } = await ctx.pg.query(text, [limit, offset]);
1966
+
1967
+ if (!include) {
1968
+ log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
1969
+ return { data: rows, status: 200 };
1970
+ }
1971
+
1972
+ // Include logic will be handled by the include-loader
1973
+ // For now, just return the rows with a note that includes need to be applied
1974
+ log.debug(\`LIST \${ctx.table} include spec:\`, include);
1975
+ return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
1976
+ } catch (e: any) {
1977
+ log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
1978
+ return {
1979
+ error: e?.message ?? "Internal error",
1980
+ ...(DEBUG ? { stack: e?.stack } : {}),
1981
+ status: 500
1982
+ };
1983
+ }
1984
+ }
1985
+
1986
+ /**
1987
+ * UPDATE operation - Update a record by primary key
1988
+ */
1989
+ export async function updateRecord(
1990
+ ctx: OperationContext,
1991
+ pkValues: any[],
1992
+ updateData: Record<string, any>
1993
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
1994
+ try {
1995
+ // Filter out PK columns from update data
1996
+ const filteredData = Object.fromEntries(
1997
+ Object.entries(updateData).filter(([k]) => !ctx.pkColumns.includes(k))
1998
+ );
1999
+
2000
+ if (!Object.keys(filteredData).length) {
2001
+ return { error: "No updatable fields provided", status: 400 };
2002
+ }
2003
+
2004
+ const hasCompositePk = ctx.pkColumns.length > 1;
2005
+ const wherePkSql = hasCompositePk
2006
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
2007
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
2008
+
2009
+ const setSql = Object.keys(filteredData)
2010
+ .map((k, i) => \`"\${k}" = $\${i + pkValues.length + 1}\`)
2011
+ .join(", ");
2012
+
2013
+ const text = \`UPDATE "\${ctx.table}" SET \${setSql} WHERE \${wherePkSql} RETURNING *\`;
2014
+ const params = [...pkValues, ...Object.values(filteredData)];
2015
+
2016
+ log.debug(\`PATCH \${ctx.table} SQL:\`, text, "params:", params);
2017
+ const { rows } = await ctx.pg.query(text, params);
2018
+
2019
+ if (!rows[0]) {
2020
+ return { data: null, status: 404 };
2021
+ }
2022
+
2023
+ return { data: rows[0], status: 200 };
2024
+ } catch (e: any) {
2025
+ log.error(\`PATCH \${ctx.table} error:\`, e?.stack ?? e);
2026
+ return {
2027
+ error: e?.message ?? "Internal error",
2028
+ ...(DEBUG ? { stack: e?.stack } : {}),
2029
+ status: 500
2030
+ };
2031
+ }
2032
+ }
2033
+
2034
+ /**
2035
+ * DELETE operation - Delete or soft-delete a record by primary key
2036
+ */
2037
+ export async function deleteRecord(
2038
+ ctx: OperationContext,
2039
+ pkValues: any[]
2040
+ ): Promise<{ data?: any; error?: string; status: number }> {
2041
+ try {
2042
+ const hasCompositePk = ctx.pkColumns.length > 1;
2043
+ const wherePkSql = hasCompositePk
2044
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
2045
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
2046
+
2047
+ const text = ctx.softDeleteColumn
2048
+ ? \`UPDATE "\${ctx.table}" SET "\${ctx.softDeleteColumn}" = NOW() WHERE \${wherePkSql} RETURNING *\`
2049
+ : \`DELETE FROM "\${ctx.table}" WHERE \${wherePkSql} RETURNING *\`;
2050
+
2051
+ log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
2052
+ const { rows } = await ctx.pg.query(text, pkValues);
2053
+
2054
+ if (!rows[0]) {
2055
+ return { data: null, status: 404 };
2056
+ }
2057
+
2058
+ return { data: rows[0], status: 200 };
2059
+ } catch (e: any) {
2060
+ log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
2061
+ return {
2062
+ error: e?.message ?? "Internal error",
2063
+ ...(DEBUG ? { stack: e?.stack } : {}),
2064
+ status: 500
2065
+ };
2066
+ }
2067
+ }`;
2068
+ }
2069
+
1878
2070
  // src/types.ts
1879
2071
  function normalizeAuthConfig(input) {
1880
2072
  if (!input)
@@ -1931,6 +2123,7 @@ async function generate(configPath) {
1931
2123
  clientDir = join(originalClientDir, "sdk");
1932
2124
  }
1933
2125
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2126
+ const serverFramework = cfg.serverFramework || "hono";
1934
2127
  console.log("\uD83D\uDCC1 Creating directories...");
1935
2128
  await ensureDirs([
1936
2129
  serverDir,
@@ -1951,12 +2144,16 @@ async function generate(configPath) {
1951
2144
  });
1952
2145
  files.push({
1953
2146
  path: join(serverDir, "include-loader.ts"),
1954
- content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
2147
+ content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3, cfg.useJsExtensions)
1955
2148
  });
1956
2149
  files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
1957
2150
  if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
1958
2151
  files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
1959
2152
  }
2153
+ files.push({
2154
+ path: join(serverDir, "core", "operations.ts"),
2155
+ content: emitCoreOperations()
2156
+ });
1960
2157
  for (const table of Object.values(model.tables)) {
1961
2158
  const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
1962
2159
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
@@ -1965,13 +2162,20 @@ async function generate(configPath) {
1965
2162
  path: join(serverDir, "zod", `${table.name}.ts`),
1966
2163
  content: emitZod(table, { dateType: normDateType, numericMode: "string" })
1967
2164
  });
1968
- files.push({
1969
- path: join(serverDir, "routes", `${table.name}.ts`),
1970
- content: emitRoutes(table, graph, {
2165
+ let routeContent;
2166
+ if (serverFramework === "hono") {
2167
+ routeContent = emitHonoRoutes(table, graph, {
1971
2168
  softDeleteColumn: cfg.softDeleteColumn || null,
1972
2169
  includeDepthLimit: cfg.includeDepthLimit || 3,
1973
- authStrategy: normalizedAuth?.strategy
1974
- })
2170
+ authStrategy: normalizedAuth?.strategy,
2171
+ useJsExtensions: cfg.useJsExtensions
2172
+ });
2173
+ } else {
2174
+ throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
2175
+ }
2176
+ files.push({
2177
+ path: join(serverDir, "routes", `${table.name}.ts`),
2178
+ content: routeContent
1975
2179
  });
1976
2180
  files.push({
1977
2181
  path: join(clientDir, `${table.name}.ts`),
@@ -1982,10 +2186,12 @@ async function generate(configPath) {
1982
2186
  path: join(clientDir, "index.ts"),
1983
2187
  content: emitClientIndex(Object.values(model.tables))
1984
2188
  });
1985
- files.push({
1986
- path: join(serverDir, "router.ts"),
1987
- content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
1988
- });
2189
+ if (serverFramework === "hono") {
2190
+ files.push({
2191
+ path: join(serverDir, "router.ts"),
2192
+ content: emitHonoRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.useJsExtensions)
2193
+ });
2194
+ }
1989
2195
  const clientFiles = files.filter((f) => {
1990
2196
  return f.path.includes(clientDir);
1991
2197
  });
package/dist/types.d.ts CHANGED
@@ -26,8 +26,10 @@ export interface Config {
26
26
  softDeleteColumn?: string | null;
27
27
  includeDepthLimit?: number;
28
28
  dateType?: "date" | "string";
29
+ serverFramework?: "hono" | "express" | "fastify";
29
30
  auth?: AuthConfigInput;
30
31
  pull?: PullConfig;
32
+ useJsExtensions?: boolean;
31
33
  }
32
34
  export interface PullConfig {
33
35
  from: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.1-alpha.0",
4
4
  "description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
5
5
  "type": "module",
6
6
  "bin": {