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/cli.js CHANGED
@@ -569,6 +569,21 @@ export default {
569
569
  */
570
570
  // dateType: "date",
571
571
 
572
+ /**
573
+ * Server framework for generated API routes
574
+ * - "hono": Lightweight, edge-compatible web framework (default)
575
+ * - "express": Traditional Node.js framework (planned)
576
+ * - "fastify": High-performance Node.js framework (planned)
577
+ * @default "hono"
578
+ */
579
+ // serverFramework: "hono",
580
+
581
+ /**
582
+ * Use .js extensions in imports (for Vercel Edge, Deno, etc.)
583
+ * @default false
584
+ */
585
+ // useJsExtensions: false,
586
+
572
587
  // ========== AUTHENTICATION ==========
573
588
 
574
589
  /**
@@ -1009,8 +1024,8 @@ export type Update${Type} = z.infer<typeof Update${Type}Schema>;
1009
1024
  `;
1010
1025
  }
1011
1026
 
1012
- // src/emit-routes.ts
1013
- function emitRoutes(table, _graph, opts) {
1027
+ // src/emit-routes-hono.ts
1028
+ function emitHonoRoutes(table, _graph, opts) {
1014
1029
  const fileTableName = table.name;
1015
1030
  const Type = pascal(table.name);
1016
1031
  const rawPk = table.pk;
@@ -1019,34 +1034,36 @@ function emitRoutes(table, _graph, opts) {
1019
1034
  const hasCompositePk = safePkCols.length > 1;
1020
1035
  const pkPath = hasCompositePk ? safePkCols.map((c) => `:${c}`).join("/") : `:${safePkCols[0]}`;
1021
1036
  const softDel = opts.softDeleteColumn && table.columns.some((c) => c.name === opts.softDeleteColumn) ? opts.softDeleteColumn : null;
1022
- const wherePkSql = hasCompositePk ? safePkCols.map((c, i) => `"${c}" = $${i + 1}`).join(" AND ") : `"${safePkCols[0]}" = $1`;
1023
1037
  const getPkParams = hasCompositePk ? `const pkValues = [${safePkCols.map((c) => `c.req.param("${c}")`).join(", ")}];` : `const pkValues = [c.req.param("${safePkCols[0]}")];`;
1024
- 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(", ")`;
1025
- 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;`;
1026
1038
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
1027
- const authImport = hasAuth ? `import { authMiddleware } from "../auth";` : "";
1039
+ const ext = opts.useJsExtensions ? ".js" : "";
1040
+ const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
1028
1041
  return `/* Generated. Do not edit. */
1029
1042
  import { Hono } from "hono";
1030
1043
  import { z } from "zod";
1031
- import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}";
1032
- import { loadIncludes } from "../include-loader";
1044
+ import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
1045
+ import { loadIncludes } from "../include-loader${ext}";
1046
+ import * as coreOps from "../core/operations${ext}";
1033
1047
  ${authImport}
1034
1048
 
1035
- const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
1036
- const log = {
1037
- debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
1038
- error: (...args: any[]) => console.error("[sdk]", ...args),
1039
- };
1040
-
1041
1049
  const listSchema = z.object({
1042
- include: z.any().optional(), // TODO: typed include spec in later pass
1050
+ include: z.any().optional(),
1043
1051
  limit: z.number().int().positive().max(100).optional(),
1044
1052
  offset: z.number().int().min(0).optional(),
1045
- orderBy: z.any().optional() // TODO: typed orderBy in a later pass
1053
+ orderBy: z.any().optional()
1046
1054
  });
1047
1055
 
1048
1056
  export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }) {
1049
1057
  const base = "/v1/${fileTableName}";
1058
+
1059
+ // Create operation context
1060
+ const ctx: coreOps.OperationContext = {
1061
+ pg: deps.pg,
1062
+ table: "${fileTableName}",
1063
+ pkColumns: ${JSON.stringify(safePkCols)},
1064
+ softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
1065
+ includeDepthLimit: ${opts.includeDepthLimit}
1066
+ };
1050
1067
  ${hasAuth ? `
1051
1068
  // \uD83D\uDD10 Auth: protect all routes for this table
1052
1069
  app.use(base, authMiddleware);
@@ -1054,142 +1071,114 @@ ${hasAuth ? `
1054
1071
 
1055
1072
  // CREATE
1056
1073
  app.post(base, async (c) => {
1057
- try {
1058
- const body = await c.req.json().catch(() => ({}));
1059
- log.debug("POST ${fileTableName} body:", body);
1060
- const parsed = Insert${Type}Schema.safeParse(body);
1061
- if (!parsed.success) {
1062
- const issues = parsed.error.flatten();
1063
- log.debug("POST ${fileTableName} invalid:", issues);
1064
- return c.json({ error: "Invalid body", issues }, 400);
1065
- }
1066
-
1067
- const data = parsed.data;
1068
- const cols = Object.keys(data);
1069
- const vals = Object.values(data);
1070
- if (!cols.length) return c.json({ error: "No fields provided" }, 400);
1071
-
1072
- const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
1073
- const text = \`INSERT INTO "${fileTableName}" (\${cols.map(c => '"' + c + '"').join(", ")})
1074
- VALUES (\${placeholders})
1075
- RETURNING *\`;
1076
- log.debug("SQL:", text, "vals:", vals);
1077
- const { rows } = await deps.pg.query(text, vals);
1078
- return c.json(rows[0] ?? null, rows[0] ? 201 : 500);
1079
- } catch (e: any) {
1080
- log.error("POST ${fileTableName} error:", e?.stack ?? e);
1081
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1074
+ const body = await c.req.json().catch(() => ({}));
1075
+ const parsed = Insert${Type}Schema.safeParse(body);
1076
+
1077
+ if (!parsed.success) {
1078
+ const issues = parsed.error.flatten();
1079
+ return c.json({ error: "Invalid body", issues }, 400);
1082
1080
  }
1081
+
1082
+ const result = await coreOps.createRecord(ctx, parsed.data);
1083
+
1084
+ if (result.error) {
1085
+ return c.json({ error: result.error }, result.status as any);
1086
+ }
1087
+
1088
+ return c.json(result.data, result.status as any);
1083
1089
  });
1084
1090
 
1085
1091
  // GET BY PK
1086
1092
  app.get(\`\${base}/${pkPath}\`, async (c) => {
1087
- try {
1088
- ${getPkParams}
1089
- const text = \`SELECT * FROM "${fileTableName}" WHERE ${wherePkSql} LIMIT 1\`;
1090
- log.debug("GET ${fileTableName} by PK:", pkValues, "SQL:", text);
1091
- const { rows } = await deps.pg.query(text, pkValues);
1092
- if (!rows[0]) return c.json(null, 404);
1093
- return c.json(rows[0]);
1094
- } catch (e: any) {
1095
- log.error("GET ${fileTableName} error:", e?.stack ?? e);
1096
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1093
+ ${getPkParams}
1094
+ const result = await coreOps.getByPk(ctx, pkValues);
1095
+
1096
+ if (result.error) {
1097
+ return c.json({ error: result.error }, result.status as any);
1097
1098
  }
1099
+
1100
+ return c.json(result.data, result.status as any);
1098
1101
  });
1099
1102
 
1100
1103
  // LIST
1101
1104
  app.post(\`\${base}/list\`, async (c) => {
1102
- try {
1103
- const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
1104
- if (!body.success) {
1105
- const issues = body.error.flatten();
1106
- log.debug("LIST ${fileTableName} invalid:", issues);
1107
- return c.json({ error: "Invalid body", issues }, 400);
1108
- }
1109
- const { include, limit = 50, offset = 0 } = body.data;
1110
-
1111
- const where = ${softDel ? `\`WHERE "${softDel}" IS NULL\`` : `""`};
1112
- const text = \`SELECT * FROM "${fileTableName}" \${where} LIMIT $1 OFFSET $2\`;
1113
- log.debug("LIST ${fileTableName} SQL:", text, "params:", [limit, offset]);
1114
- const { rows } = await deps.pg.query(text, [limit, offset]);
1115
-
1116
- if (!include) {
1117
- log.debug("LIST ${fileTableName} rows:", rows.length);
1118
- return c.json(rows);
1119
- }
1120
-
1121
- // Attempt include stitching with explicit error handling
1122
- log.debug("LIST ${fileTableName} include spec:", include);
1105
+ const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
1106
+
1107
+ if (!body.success) {
1108
+ const issues = body.error.flatten();
1109
+ return c.json({ error: "Invalid body", issues }, 400);
1110
+ }
1111
+
1112
+ const result = await coreOps.listRecords(ctx, body.data);
1113
+
1114
+ if (result.error) {
1115
+ return c.json({ error: result.error }, result.status as any);
1116
+ }
1117
+
1118
+ // Handle includes if needed
1119
+ if (result.needsIncludes && result.includeSpec) {
1123
1120
  try {
1124
- const stitched = await loadIncludes("${fileTableName}", rows, include, deps.pg, ${opts.includeDepthLimit});
1125
- log.debug("LIST ${fileTableName} stitched count:", Array.isArray(stitched) ? stitched.length : "n/a");
1121
+ const stitched = await loadIncludes(
1122
+ "${fileTableName}",
1123
+ result.data,
1124
+ result.includeSpec,
1125
+ deps.pg,
1126
+ ${opts.includeDepthLimit}
1127
+ );
1126
1128
  return c.json(stitched);
1127
1129
  } catch (e: any) {
1128
- const strict = process.env.SDK_STRICT_INCLUDE === "1" || process.env.SDK_STRICT_INCLUDE === "true";
1129
- const msg = e?.message ?? String(e);
1130
- const stack = e?.stack;
1131
- log.error("LIST ${fileTableName} include stitch FAILED:", msg, stack);
1132
-
1130
+ const strict = process.env.SDK_STRICT_INCLUDE === "1";
1133
1131
  if (strict) {
1134
- return c.json({ error: "include-stitch-failed", message: msg, ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1132
+ return c.json({
1133
+ error: "include-stitch-failed",
1134
+ message: e?.message,
1135
+ ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
1136
+ }, 500);
1135
1137
  }
1136
- // Non-strict fallback: return base rows plus error metadata
1137
- return c.json({ data: rows, includeError: { message: msg, ...(DEBUG ? { stack: e?.stack } : {}) } }, 200);
1138
+ // Non-strict: return base rows with error metadata
1139
+ return c.json({
1140
+ data: result.data,
1141
+ includeError: {
1142
+ message: e?.message,
1143
+ ...(process.env.SDK_DEBUG === "1" ? { stack: e?.stack } : {})
1144
+ }
1145
+ }, 200);
1138
1146
  }
1139
- } catch (e: any) {
1140
- log.error("LIST ${fileTableName} error:", e?.stack ?? e);
1141
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1142
1147
  }
1148
+
1149
+ return c.json(result.data, result.status as any);
1143
1150
  });
1144
1151
 
1145
1152
  // UPDATE
1146
1153
  app.patch(\`\${base}/${pkPath}\`, async (c) => {
1147
- try {
1148
- ${getPkParams}
1149
- const body = await c.req.json().catch(() => ({}));
1150
- log.debug("PATCH ${fileTableName} pk:", pkValues, "patch:", body);
1151
- const parsed = Update${Type}Schema.safeParse(body);
1152
- if (!parsed.success) {
1153
- const issues = parsed.error.flatten();
1154
- log.debug("PATCH ${fileTableName} invalid:", issues);
1155
- return c.json({ error: "Invalid body", issues: issues }, 400);
1156
- }
1157
-
1158
- ${pkFilter}
1159
- if (!Object.keys(updateData).length) return c.json({ error: "No updatable fields provided" }, 400);
1160
-
1161
- const setSql = ${updateSetSql};
1162
- const text = \`UPDATE "${fileTableName}" SET \${setSql} WHERE ${wherePkSql} RETURNING *\`;
1163
- const params = ${hasCompositePk ? `[...pkValues, ...Object.values(updateData)]` : `[pkValues[0], ...Object.values(updateData)]`};
1164
- log.debug("PATCH ${fileTableName} SQL:", text, "params:", params);
1165
- const { rows } = await deps.pg.query(text, params);
1166
- if (!rows[0]) return c.json(null, 404);
1167
- return c.json(rows[0]);
1168
- } catch (e: any) {
1169
- log.error("PATCH ${fileTableName} error:", e?.stack ?? e);
1170
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1154
+ ${getPkParams}
1155
+ const body = await c.req.json().catch(() => ({}));
1156
+ const parsed = Update${Type}Schema.safeParse(body);
1157
+
1158
+ if (!parsed.success) {
1159
+ const issues = parsed.error.flatten();
1160
+ return c.json({ error: "Invalid body", issues }, 400);
1171
1161
  }
1162
+
1163
+ const result = await coreOps.updateRecord(ctx, pkValues, parsed.data);
1164
+
1165
+ if (result.error) {
1166
+ return c.json({ error: result.error }, result.status as any);
1167
+ }
1168
+
1169
+ return c.json(result.data, result.status as any);
1172
1170
  });
1173
1171
 
1174
- // DELETE (soft or hard)
1172
+ // DELETE
1175
1173
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
1176
- try {
1177
- ${getPkParams}
1178
- ${softDel ? `
1179
- const text = \`UPDATE "${fileTableName}" SET "${softDel}" = NOW() WHERE ${wherePkSql} RETURNING *\`;
1180
- log.debug("DELETE (soft) ${fileTableName} SQL:", text, "pk:", pkValues);
1181
- const { rows } = await deps.pg.query(text, pkValues);
1182
- if (!rows[0]) return c.json(null, 404);
1183
- return c.json(rows[0]);` : `
1184
- const text = \`DELETE FROM "${fileTableName}" WHERE ${wherePkSql} RETURNING *\`;
1185
- log.debug("DELETE ${fileTableName} SQL:", text, "pk:", pkValues);
1186
- const { rows } = await deps.pg.query(text, pkValues);
1187
- if (!rows[0]) return c.json(null, 404);
1188
- return c.json(rows[0]);`}
1189
- } catch (e: any) {
1190
- log.error("DELETE ${fileTableName} error:", e?.stack ?? e);
1191
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1174
+ ${getPkParams}
1175
+ const result = await coreOps.deleteRecord(ctx, pkValues);
1176
+
1177
+ if (result.error) {
1178
+ return c.json({ error: result.error }, result.status as any);
1192
1179
  }
1180
+
1181
+ return c.json(result.data, result.status as any);
1193
1182
  });
1194
1183
  }
1195
1184
  `;
@@ -1449,13 +1438,14 @@ export abstract class BaseClient {
1449
1438
  }
1450
1439
 
1451
1440
  // src/emit-include-loader.ts
1452
- function emitIncludeLoader(graph, model, maxDepth) {
1441
+ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
1453
1442
  const fkIndex = {};
1454
1443
  for (const t of Object.values(model.tables)) {
1455
1444
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
1456
1445
  }
1446
+ const ext = useJsExtensions ? ".js" : "";
1457
1447
  return `/* Generated. Do not edit. */
1458
- import { RELATION_GRAPH } from "./include-builder";
1448
+ import { RELATION_GRAPH } from "./include-builder${ext}";
1459
1449
 
1460
1450
  // Minimal types to keep the file self-contained
1461
1451
  type Graph = typeof RELATION_GRAPH;
@@ -1974,12 +1964,13 @@ export async function authMiddleware(c: Context, next: Next) {
1974
1964
  `;
1975
1965
  }
1976
1966
 
1977
- // src/emit-router.ts
1978
- function emitRouter(tables, hasAuth) {
1967
+ // src/emit-router-hono.ts
1968
+ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
1979
1969
  const tableNames = tables.map((t) => t.name).sort();
1970
+ const ext = useJsExtensions ? ".js" : "";
1980
1971
  const imports = tableNames.map((name) => {
1981
1972
  const Type = pascal(name);
1982
- return `import { register${Type}Routes } from "./routes/${name}";`;
1973
+ return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
1983
1974
  }).join(`
1984
1975
  `);
1985
1976
  const registrations = tableNames.map((name) => {
@@ -1989,14 +1980,14 @@ function emitRouter(tables, hasAuth) {
1989
1980
  `);
1990
1981
  const reExports = tableNames.map((name) => {
1991
1982
  const Type = pascal(name);
1992
- return `export { register${Type}Routes } from "./routes/${name}";`;
1983
+ return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
1993
1984
  }).join(`
1994
1985
  `);
1995
1986
  return `/* Generated. Do not edit. */
1996
1987
  import { Hono } from "hono";
1997
- import { SDK_MANIFEST } from "./sdk-bundle";
1988
+ import { SDK_MANIFEST } from "./sdk-bundle${ext}";
1998
1989
  ${imports}
1999
- ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1990
+ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
2000
1991
 
2001
1992
  /**
2002
1993
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
@@ -2112,6 +2103,222 @@ export const SDK_MANIFEST = {
2112
2103
  `;
2113
2104
  }
2114
2105
 
2106
+ // src/emit-core-operations.ts
2107
+ function emitCoreOperations() {
2108
+ return `/**
2109
+ * Core database operations that are framework-agnostic.
2110
+ * These functions handle the actual database logic and can be used by any framework adapter.
2111
+ */
2112
+
2113
+ import type { z } from "zod";
2114
+
2115
+ export interface DatabaseClient {
2116
+ query: (text: string, params?: any[]) => Promise<{ rows: any[] }>;
2117
+ }
2118
+
2119
+ export interface OperationContext {
2120
+ pg: DatabaseClient;
2121
+ table: string;
2122
+ pkColumns: string[];
2123
+ softDeleteColumn?: string | null;
2124
+ includeDepthLimit: number;
2125
+ }
2126
+
2127
+ const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
2128
+ const log = {
2129
+ debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
2130
+ error: (...args: any[]) => console.error("[sdk]", ...args),
2131
+ };
2132
+
2133
+ /**
2134
+ * CREATE operation - Insert a new record
2135
+ */
2136
+ export async function createRecord(
2137
+ ctx: OperationContext,
2138
+ data: Record<string, any>
2139
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
2140
+ try {
2141
+ const cols = Object.keys(data);
2142
+ const vals = Object.values(data);
2143
+
2144
+ if (!cols.length) {
2145
+ return { error: "No fields provided", status: 400 };
2146
+ }
2147
+
2148
+ const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
2149
+ const text = \`INSERT INTO "\${ctx.table}" (\${cols.map(c => '"' + c + '"').join(", ")})
2150
+ VALUES (\${placeholders})
2151
+ RETURNING *\`;
2152
+
2153
+ log.debug("SQL:", text, "vals:", vals);
2154
+ const { rows } = await ctx.pg.query(text, vals);
2155
+
2156
+ return { data: rows[0] ?? null, status: rows[0] ? 201 : 500 };
2157
+ } catch (e: any) {
2158
+ log.error(\`POST \${ctx.table} error:\`, e?.stack ?? e);
2159
+ return {
2160
+ error: e?.message ?? "Internal error",
2161
+ ...(DEBUG ? { stack: e?.stack } : {}),
2162
+ status: 500
2163
+ };
2164
+ }
2165
+ }
2166
+
2167
+ /**
2168
+ * READ operation - Get a record by primary key
2169
+ */
2170
+ export async function getByPk(
2171
+ ctx: OperationContext,
2172
+ pkValues: any[]
2173
+ ): Promise<{ data?: any; error?: string; status: number }> {
2174
+ try {
2175
+ const hasCompositePk = ctx.pkColumns.length > 1;
2176
+ const wherePkSql = hasCompositePk
2177
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
2178
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
2179
+
2180
+ const text = \`SELECT * FROM "\${ctx.table}" WHERE \${wherePkSql} LIMIT 1\`;
2181
+ log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
2182
+
2183
+ const { rows } = await ctx.pg.query(text, pkValues);
2184
+
2185
+ if (!rows[0]) {
2186
+ return { data: null, status: 404 };
2187
+ }
2188
+
2189
+ return { data: rows[0], status: 200 };
2190
+ } catch (e: any) {
2191
+ log.error(\`GET \${ctx.table} error:\`, e?.stack ?? e);
2192
+ return {
2193
+ error: e?.message ?? "Internal error",
2194
+ ...(DEBUG ? { stack: e?.stack } : {}),
2195
+ status: 500
2196
+ };
2197
+ }
2198
+ }
2199
+
2200
+ /**
2201
+ * LIST operation - Get multiple records with optional filters
2202
+ */
2203
+ export async function listRecords(
2204
+ ctx: OperationContext,
2205
+ params: { limit?: number; offset?: number; include?: any }
2206
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number; needsIncludes?: boolean; includeSpec?: any }> {
2207
+ try {
2208
+ const { limit = 50, offset = 0, include } = params;
2209
+
2210
+ const where = ctx.softDeleteColumn
2211
+ ? \`WHERE "\${ctx.softDeleteColumn}" IS NULL\`
2212
+ : "";
2213
+
2214
+ const text = \`SELECT * FROM "\${ctx.table}" \${where} LIMIT $1 OFFSET $2\`;
2215
+ log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", [limit, offset]);
2216
+
2217
+ const { rows } = await ctx.pg.query(text, [limit, offset]);
2218
+
2219
+ if (!include) {
2220
+ log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
2221
+ return { data: rows, status: 200 };
2222
+ }
2223
+
2224
+ // Include logic will be handled by the include-loader
2225
+ // For now, just return the rows with a note that includes need to be applied
2226
+ log.debug(\`LIST \${ctx.table} include spec:\`, include);
2227
+ return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
2228
+ } catch (e: any) {
2229
+ log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
2230
+ return {
2231
+ error: e?.message ?? "Internal error",
2232
+ ...(DEBUG ? { stack: e?.stack } : {}),
2233
+ status: 500
2234
+ };
2235
+ }
2236
+ }
2237
+
2238
+ /**
2239
+ * UPDATE operation - Update a record by primary key
2240
+ */
2241
+ export async function updateRecord(
2242
+ ctx: OperationContext,
2243
+ pkValues: any[],
2244
+ updateData: Record<string, any>
2245
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
2246
+ try {
2247
+ // Filter out PK columns from update data
2248
+ const filteredData = Object.fromEntries(
2249
+ Object.entries(updateData).filter(([k]) => !ctx.pkColumns.includes(k))
2250
+ );
2251
+
2252
+ if (!Object.keys(filteredData).length) {
2253
+ return { error: "No updatable fields provided", status: 400 };
2254
+ }
2255
+
2256
+ const hasCompositePk = ctx.pkColumns.length > 1;
2257
+ const wherePkSql = hasCompositePk
2258
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
2259
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
2260
+
2261
+ const setSql = Object.keys(filteredData)
2262
+ .map((k, i) => \`"\${k}" = $\${i + pkValues.length + 1}\`)
2263
+ .join(", ");
2264
+
2265
+ const text = \`UPDATE "\${ctx.table}" SET \${setSql} WHERE \${wherePkSql} RETURNING *\`;
2266
+ const params = [...pkValues, ...Object.values(filteredData)];
2267
+
2268
+ log.debug(\`PATCH \${ctx.table} SQL:\`, text, "params:", params);
2269
+ const { rows } = await ctx.pg.query(text, params);
2270
+
2271
+ if (!rows[0]) {
2272
+ return { data: null, status: 404 };
2273
+ }
2274
+
2275
+ return { data: rows[0], status: 200 };
2276
+ } catch (e: any) {
2277
+ log.error(\`PATCH \${ctx.table} error:\`, e?.stack ?? e);
2278
+ return {
2279
+ error: e?.message ?? "Internal error",
2280
+ ...(DEBUG ? { stack: e?.stack } : {}),
2281
+ status: 500
2282
+ };
2283
+ }
2284
+ }
2285
+
2286
+ /**
2287
+ * DELETE operation - Delete or soft-delete a record by primary key
2288
+ */
2289
+ export async function deleteRecord(
2290
+ ctx: OperationContext,
2291
+ pkValues: any[]
2292
+ ): Promise<{ data?: any; error?: string; status: number }> {
2293
+ try {
2294
+ const hasCompositePk = ctx.pkColumns.length > 1;
2295
+ const wherePkSql = hasCompositePk
2296
+ ? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
2297
+ : \`"\${ctx.pkColumns[0]}" = $1\`;
2298
+
2299
+ const text = ctx.softDeleteColumn
2300
+ ? \`UPDATE "\${ctx.table}" SET "\${ctx.softDeleteColumn}" = NOW() WHERE \${wherePkSql} RETURNING *\`
2301
+ : \`DELETE FROM "\${ctx.table}" WHERE \${wherePkSql} RETURNING *\`;
2302
+
2303
+ log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
2304
+ const { rows } = await ctx.pg.query(text, pkValues);
2305
+
2306
+ if (!rows[0]) {
2307
+ return { data: null, status: 404 };
2308
+ }
2309
+
2310
+ return { data: rows[0], status: 200 };
2311
+ } catch (e: any) {
2312
+ log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
2313
+ return {
2314
+ error: e?.message ?? "Internal error",
2315
+ ...(DEBUG ? { stack: e?.stack } : {}),
2316
+ status: 500
2317
+ };
2318
+ }
2319
+ }`;
2320
+ }
2321
+
2115
2322
  // src/types.ts
2116
2323
  function normalizeAuthConfig(input) {
2117
2324
  if (!input)
@@ -2168,6 +2375,7 @@ async function generate(configPath) {
2168
2375
  clientDir = join(originalClientDir, "sdk");
2169
2376
  }
2170
2377
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2378
+ const serverFramework = cfg.serverFramework || "hono";
2171
2379
  console.log("\uD83D\uDCC1 Creating directories...");
2172
2380
  await ensureDirs([
2173
2381
  serverDir,
@@ -2188,12 +2396,16 @@ async function generate(configPath) {
2188
2396
  });
2189
2397
  files.push({
2190
2398
  path: join(serverDir, "include-loader.ts"),
2191
- content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
2399
+ content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3, cfg.useJsExtensions)
2192
2400
  });
2193
2401
  files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
2194
2402
  if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
2195
2403
  files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
2196
2404
  }
2405
+ files.push({
2406
+ path: join(serverDir, "core", "operations.ts"),
2407
+ content: emitCoreOperations()
2408
+ });
2197
2409
  for (const table of Object.values(model.tables)) {
2198
2410
  const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
2199
2411
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
@@ -2202,13 +2414,20 @@ async function generate(configPath) {
2202
2414
  path: join(serverDir, "zod", `${table.name}.ts`),
2203
2415
  content: emitZod(table, { dateType: normDateType, numericMode: "string" })
2204
2416
  });
2205
- files.push({
2206
- path: join(serverDir, "routes", `${table.name}.ts`),
2207
- content: emitRoutes(table, graph, {
2417
+ let routeContent;
2418
+ if (serverFramework === "hono") {
2419
+ routeContent = emitHonoRoutes(table, graph, {
2208
2420
  softDeleteColumn: cfg.softDeleteColumn || null,
2209
2421
  includeDepthLimit: cfg.includeDepthLimit || 3,
2210
- authStrategy: normalizedAuth?.strategy
2211
- })
2422
+ authStrategy: normalizedAuth?.strategy,
2423
+ useJsExtensions: cfg.useJsExtensions
2424
+ });
2425
+ } else {
2426
+ throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
2427
+ }
2428
+ files.push({
2429
+ path: join(serverDir, "routes", `${table.name}.ts`),
2430
+ content: routeContent
2212
2431
  });
2213
2432
  files.push({
2214
2433
  path: join(clientDir, `${table.name}.ts`),
@@ -2219,10 +2438,12 @@ async function generate(configPath) {
2219
2438
  path: join(clientDir, "index.ts"),
2220
2439
  content: emitClientIndex(Object.values(model.tables))
2221
2440
  });
2222
- files.push({
2223
- path: join(serverDir, "router.ts"),
2224
- content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
2225
- });
2441
+ if (serverFramework === "hono") {
2442
+ files.push({
2443
+ path: join(serverDir, "router.ts"),
2444
+ content: emitHonoRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.useJsExtensions)
2445
+ });
2446
+ }
2226
2447
  const clientFiles = files.filter((f) => {
2227
2448
  return f.path.includes(clientDir);
2228
2449
  });