postgresdk 0.3.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
@@ -570,12 +570,19 @@ export default {
570
570
  // dateType: "date",
571
571
 
572
572
  /**
573
- * Database driver to use for connection
574
- * - "pg": Node.js pg driver (default, works anywhere Node.js runs)
575
- * - "neon": Neon serverless driver (edge-compatible, works on Vercel Edge/Cloudflare Workers)
576
- * @default "pg"
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"
577
578
  */
578
- // driver: "pg",
579
+ // serverFramework: "hono",
580
+
581
+ /**
582
+ * Use .js extensions in imports (for Vercel Edge, Deno, etc.)
583
+ * @default false
584
+ */
585
+ // useJsExtensions: false,
579
586
 
580
587
  // ========== AUTHENTICATION ==========
581
588
 
@@ -1017,8 +1024,8 @@ export type Update${Type} = z.infer<typeof Update${Type}Schema>;
1017
1024
  `;
1018
1025
  }
1019
1026
 
1020
- // src/emit-routes.ts
1021
- function emitRoutes(table, _graph, opts) {
1027
+ // src/emit-routes-hono.ts
1028
+ function emitHonoRoutes(table, _graph, opts) {
1022
1029
  const fileTableName = table.name;
1023
1030
  const Type = pascal(table.name);
1024
1031
  const rawPk = table.pk;
@@ -1027,34 +1034,36 @@ function emitRoutes(table, _graph, opts) {
1027
1034
  const hasCompositePk = safePkCols.length > 1;
1028
1035
  const pkPath = hasCompositePk ? safePkCols.map((c) => `:${c}`).join("/") : `:${safePkCols[0]}`;
1029
1036
  const softDel = opts.softDeleteColumn && table.columns.some((c) => c.name === opts.softDeleteColumn) ? opts.softDeleteColumn : null;
1030
- const wherePkSql = hasCompositePk ? safePkCols.map((c, i) => `"${c}" = $${i + 1}`).join(" AND ") : `"${safePkCols[0]}" = $1`;
1031
1037
  const getPkParams = hasCompositePk ? `const pkValues = [${safePkCols.map((c) => `c.req.param("${c}")`).join(", ")}];` : `const pkValues = [c.req.param("${safePkCols[0]}")];`;
1032
- 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(", ")`;
1033
- 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;`;
1034
1038
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
1035
- const authImport = hasAuth ? `import { authMiddleware } from "../auth";` : "";
1039
+ const ext = opts.useJsExtensions ? ".js" : "";
1040
+ const authImport = hasAuth ? `import { authMiddleware } from "../auth${ext}";` : "";
1036
1041
  return `/* Generated. Do not edit. */
1037
1042
  import { Hono } from "hono";
1038
1043
  import { z } from "zod";
1039
- import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}";
1040
- 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}";
1041
1047
  ${authImport}
1042
1048
 
1043
- const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
1044
- const log = {
1045
- debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
1046
- error: (...args: any[]) => console.error("[sdk]", ...args),
1047
- };
1048
-
1049
1049
  const listSchema = z.object({
1050
- include: z.any().optional(), // TODO: typed include spec in later pass
1050
+ include: z.any().optional(),
1051
1051
  limit: z.number().int().positive().max(100).optional(),
1052
1052
  offset: z.number().int().min(0).optional(),
1053
- orderBy: z.any().optional() // TODO: typed orderBy in a later pass
1053
+ orderBy: z.any().optional()
1054
1054
  });
1055
1055
 
1056
1056
  export function register${Type}Routes(app: Hono, deps: { pg: { query: (text: string, params?: any[]) => Promise<{ rows: any[] }> } }) {
1057
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
+ };
1058
1067
  ${hasAuth ? `
1059
1068
  // \uD83D\uDD10 Auth: protect all routes for this table
1060
1069
  app.use(base, authMiddleware);
@@ -1062,142 +1071,114 @@ ${hasAuth ? `
1062
1071
 
1063
1072
  // CREATE
1064
1073
  app.post(base, async (c) => {
1065
- try {
1066
- const body = await c.req.json().catch(() => ({}));
1067
- log.debug("POST ${fileTableName} body:", body);
1068
- const parsed = Insert${Type}Schema.safeParse(body);
1069
- if (!parsed.success) {
1070
- const issues = parsed.error.flatten();
1071
- log.debug("POST ${fileTableName} invalid:", issues);
1072
- return c.json({ error: "Invalid body", issues }, 400);
1073
- }
1074
-
1075
- const data = parsed.data;
1076
- const cols = Object.keys(data);
1077
- const vals = Object.values(data);
1078
- if (!cols.length) return c.json({ error: "No fields provided" }, 400);
1079
-
1080
- const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
1081
- const text = \`INSERT INTO "${fileTableName}" (\${cols.map(c => '"' + c + '"').join(", ")})
1082
- VALUES (\${placeholders})
1083
- RETURNING *\`;
1084
- log.debug("SQL:", text, "vals:", vals);
1085
- const { rows } = await deps.pg.query(text, vals);
1086
- return c.json(rows[0] ?? null, rows[0] ? 201 : 500);
1087
- } catch (e: any) {
1088
- log.error("POST ${fileTableName} error:", e?.stack ?? e);
1089
- 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);
1090
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);
1091
1089
  });
1092
1090
 
1093
1091
  // GET BY PK
1094
1092
  app.get(\`\${base}/${pkPath}\`, async (c) => {
1095
- try {
1096
- ${getPkParams}
1097
- const text = \`SELECT * FROM "${fileTableName}" WHERE ${wherePkSql} LIMIT 1\`;
1098
- log.debug("GET ${fileTableName} by PK:", pkValues, "SQL:", text);
1099
- const { rows } = await deps.pg.query(text, pkValues);
1100
- if (!rows[0]) return c.json(null, 404);
1101
- return c.json(rows[0]);
1102
- } catch (e: any) {
1103
- log.error("GET ${fileTableName} error:", e?.stack ?? e);
1104
- 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);
1105
1098
  }
1099
+
1100
+ return c.json(result.data, result.status as any);
1106
1101
  });
1107
1102
 
1108
1103
  // LIST
1109
1104
  app.post(\`\${base}/list\`, async (c) => {
1110
- try {
1111
- const body = listSchema.safeParse(await c.req.json().catch(() => ({})));
1112
- if (!body.success) {
1113
- const issues = body.error.flatten();
1114
- log.debug("LIST ${fileTableName} invalid:", issues);
1115
- return c.json({ error: "Invalid body", issues }, 400);
1116
- }
1117
- const { include, limit = 50, offset = 0 } = body.data;
1118
-
1119
- const where = ${softDel ? `\`WHERE "${softDel}" IS NULL\`` : `""`};
1120
- const text = \`SELECT * FROM "${fileTableName}" \${where} LIMIT $1 OFFSET $2\`;
1121
- log.debug("LIST ${fileTableName} SQL:", text, "params:", [limit, offset]);
1122
- const { rows } = await deps.pg.query(text, [limit, offset]);
1123
-
1124
- if (!include) {
1125
- log.debug("LIST ${fileTableName} rows:", rows.length);
1126
- return c.json(rows);
1127
- }
1128
-
1129
- // Attempt include stitching with explicit error handling
1130
- 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) {
1131
1120
  try {
1132
- const stitched = await loadIncludes("${fileTableName}", rows, include, deps.pg, ${opts.includeDepthLimit});
1133
- 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
+ );
1134
1128
  return c.json(stitched);
1135
1129
  } catch (e: any) {
1136
- const strict = process.env.SDK_STRICT_INCLUDE === "1" || process.env.SDK_STRICT_INCLUDE === "true";
1137
- const msg = e?.message ?? String(e);
1138
- const stack = e?.stack;
1139
- log.error("LIST ${fileTableName} include stitch FAILED:", msg, stack);
1140
-
1130
+ const strict = process.env.SDK_STRICT_INCLUDE === "1";
1141
1131
  if (strict) {
1142
- 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);
1143
1137
  }
1144
- // Non-strict fallback: return base rows plus error metadata
1145
- 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);
1146
1146
  }
1147
- } catch (e: any) {
1148
- log.error("LIST ${fileTableName} error:", e?.stack ?? e);
1149
- return c.json({ error: e?.message ?? "Internal error", ...(DEBUG ? { stack: e?.stack } : {}) }, 500);
1150
1147
  }
1148
+
1149
+ return c.json(result.data, result.status as any);
1151
1150
  });
1152
1151
 
1153
1152
  // UPDATE
1154
1153
  app.patch(\`\${base}/${pkPath}\`, async (c) => {
1155
- try {
1156
- ${getPkParams}
1157
- const body = await c.req.json().catch(() => ({}));
1158
- log.debug("PATCH ${fileTableName} pk:", pkValues, "patch:", body);
1159
- const parsed = Update${Type}Schema.safeParse(body);
1160
- if (!parsed.success) {
1161
- const issues = parsed.error.flatten();
1162
- log.debug("PATCH ${fileTableName} invalid:", issues);
1163
- return c.json({ error: "Invalid body", issues: issues }, 400);
1164
- }
1165
-
1166
- ${pkFilter}
1167
- if (!Object.keys(updateData).length) return c.json({ error: "No updatable fields provided" }, 400);
1168
-
1169
- const setSql = ${updateSetSql};
1170
- const text = \`UPDATE "${fileTableName}" SET \${setSql} WHERE ${wherePkSql} RETURNING *\`;
1171
- const params = ${hasCompositePk ? `[...pkValues, ...Object.values(updateData)]` : `[pkValues[0], ...Object.values(updateData)]`};
1172
- log.debug("PATCH ${fileTableName} SQL:", text, "params:", params);
1173
- const { rows } = await deps.pg.query(text, params);
1174
- if (!rows[0]) return c.json(null, 404);
1175
- return c.json(rows[0]);
1176
- } catch (e: any) {
1177
- log.error("PATCH ${fileTableName} error:", e?.stack ?? e);
1178
- 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);
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);
1179
1167
  }
1168
+
1169
+ return c.json(result.data, result.status as any);
1180
1170
  });
1181
1171
 
1182
- // DELETE (soft or hard)
1172
+ // DELETE
1183
1173
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
1184
- try {
1185
- ${getPkParams}
1186
- ${softDel ? `
1187
- const text = \`UPDATE "${fileTableName}" SET "${softDel}" = NOW() WHERE ${wherePkSql} RETURNING *\`;
1188
- log.debug("DELETE (soft) ${fileTableName} SQL:", text, "pk:", pkValues);
1189
- const { rows } = await deps.pg.query(text, pkValues);
1190
- if (!rows[0]) return c.json(null, 404);
1191
- return c.json(rows[0]);` : `
1192
- const text = \`DELETE FROM "${fileTableName}" WHERE ${wherePkSql} RETURNING *\`;
1193
- log.debug("DELETE ${fileTableName} SQL:", text, "pk:", pkValues);
1194
- const { rows } = await deps.pg.query(text, pkValues);
1195
- if (!rows[0]) return c.json(null, 404);
1196
- return c.json(rows[0]);`}
1197
- } catch (e: any) {
1198
- log.error("DELETE ${fileTableName} error:", e?.stack ?? e);
1199
- 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);
1200
1179
  }
1180
+
1181
+ return c.json(result.data, result.status as any);
1201
1182
  });
1202
1183
  }
1203
1184
  `;
@@ -1257,7 +1238,7 @@ export class ${Type}Client extends BaseClient {
1257
1238
  function emitClientIndex(tables) {
1258
1239
  let out = `/* Generated. Do not edit. */
1259
1240
  `;
1260
- out += `import { BaseClient, AuthConfig } from "./base-client";
1241
+ out += `import { BaseClient, type AuthConfig } from "./base-client";
1261
1242
  `;
1262
1243
  for (const t of tables) {
1263
1244
  out += `import { ${pascal(t.name)}Client } from "./${t.name}";
@@ -1457,13 +1438,14 @@ export abstract class BaseClient {
1457
1438
  }
1458
1439
 
1459
1440
  // src/emit-include-loader.ts
1460
- function emitIncludeLoader(graph, model, maxDepth) {
1441
+ function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
1461
1442
  const fkIndex = {};
1462
1443
  for (const t of Object.values(model.tables)) {
1463
1444
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
1464
1445
  }
1446
+ const ext = useJsExtensions ? ".js" : "";
1465
1447
  return `/* Generated. Do not edit. */
1466
- import { RELATION_GRAPH } from "./include-builder";
1448
+ import { RELATION_GRAPH } from "./include-builder${ext}";
1467
1449
 
1468
1450
  // Minimal types to keep the file self-contained
1469
1451
  type Graph = typeof RELATION_GRAPH;
@@ -1982,12 +1964,13 @@ export async function authMiddleware(c: Context, next: Next) {
1982
1964
  `;
1983
1965
  }
1984
1966
 
1985
- // src/emit-router.ts
1986
- function emitRouter(tables, hasAuth, driver = "pg") {
1967
+ // src/emit-router-hono.ts
1968
+ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
1987
1969
  const tableNames = tables.map((t) => t.name).sort();
1970
+ const ext = useJsExtensions ? ".js" : "";
1988
1971
  const imports = tableNames.map((name) => {
1989
1972
  const Type = pascal(name);
1990
- return `import { register${Type}Routes } from "./routes/${name}";`;
1973
+ return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
1991
1974
  }).join(`
1992
1975
  `);
1993
1976
  const registrations = tableNames.map((name) => {
@@ -1997,48 +1980,36 @@ function emitRouter(tables, hasAuth, driver = "pg") {
1997
1980
  `);
1998
1981
  const reExports = tableNames.map((name) => {
1999
1982
  const Type = pascal(name);
2000
- return `export { register${Type}Routes } from "./routes/${name}";`;
1983
+ return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
2001
1984
  }).join(`
2002
1985
  `);
2003
- const pgExample = driver === "pg" ? `
2004
- * import { Client } from "pg";
2005
- * import { createRouter } from "./generated/server/router";
2006
- *
2007
- * const app = new Hono();
2008
- * const pg = new Client({ connectionString: process.env.DATABASE_URL });
2009
- * await pg.connect();
2010
- *
2011
- * // Mount all generated routes under /api
2012
- * const apiRouter = createRouter({ pg });
2013
- * app.route("/api", apiRouter);` : `
2014
- * import { neon } from "@neondatabase/serverless";
2015
- * import { createRouter } from "./generated/server/router";
2016
- *
2017
- * const app = new Hono();
2018
- * const sql = neon(process.env.DATABASE_URL!);
2019
- *
2020
- * // Create pg-compatible adapter for Neon
2021
- * const pg = {
2022
- * async query(text: string, params?: any[]) {
2023
- * const rows = await sql(text, params);
2024
- * return { rows };
2025
- * }
2026
- * };
2027
- *
2028
- * // Mount all generated routes under /api
2029
- * const apiRouter = createRouter({ pg });
2030
- * app.route("/api", apiRouter);`;
2031
1986
  return `/* Generated. Do not edit. */
2032
1987
  import { Hono } from "hono";
2033
- import { SDK_MANIFEST } from "./sdk-bundle";
1988
+ import { SDK_MANIFEST } from "./sdk-bundle${ext}";
2034
1989
  ${imports}
2035
- ${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
1990
+ ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
2036
1991
 
2037
1992
  /**
2038
1993
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
2039
1994
  *
2040
1995
  * @example
2041
- * import { Hono } from "hono";${pgExample}
1996
+ * import { Hono } from "hono";
1997
+ * import { createRouter } from "./generated/server/router";
1998
+ *
1999
+ * // Using pg driver (Node.js)
2000
+ * import { Client } from "pg";
2001
+ * const pg = new Client({ connectionString: process.env.DATABASE_URL });
2002
+ * await pg.connect();
2003
+ *
2004
+ * // OR using Neon driver (Edge-compatible)
2005
+ * import { Pool } from "@neondatabase/serverless";
2006
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
2007
+ * const pg = pool; // Pool already has the compatible query method
2008
+ *
2009
+ * // Mount all generated routes
2010
+ * const app = new Hono();
2011
+ * const apiRouter = createRouter({ pg });
2012
+ * app.route("/api", apiRouter);
2042
2013
  *
2043
2014
  * // Or mount directly at root
2044
2015
  * const router = createRouter({ pg });
@@ -2083,24 +2054,13 @@ ${registrations}
2083
2054
  * Register all generated routes directly on an existing Hono app.
2084
2055
  *
2085
2056
  * @example
2086
- * import { Hono } from "hono";${driver === "pg" ? `
2087
- * import { Client } from "pg";
2057
+ * import { Hono } from "hono";
2088
2058
  * import { registerAllRoutes } from "./generated/server/router";
2089
2059
  *
2090
2060
  * const app = new Hono();
2091
- * const pg = new Client({ connectionString: process.env.DATABASE_URL });
2092
- * await pg.connect();` : `
2093
- * import { neon } from "@neondatabase/serverless";
2094
- * import { registerAllRoutes } from "./generated/server/router";
2095
2061
  *
2096
- * const app = new Hono();
2097
- * const sql = neon(process.env.DATABASE_URL!);
2098
- * const pg = {
2099
- * async query(text: string, params?: any[]) {
2100
- * const rows = await sql(text, params);
2101
- * return { rows };
2102
- * }
2103
- * };`}
2062
+ * // Setup database connection (see createRouter example for both pg and Neon options)
2063
+ * const pg = yourDatabaseClient;
2104
2064
  *
2105
2065
  * // Register all routes at once
2106
2066
  * registerAllRoutes(app, { pg });
@@ -2143,6 +2103,222 @@ export const SDK_MANIFEST = {
2143
2103
  `;
2144
2104
  }
2145
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
+
2146
2322
  // src/types.ts
2147
2323
  function normalizeAuthConfig(input) {
2148
2324
  if (!input)
@@ -2199,6 +2375,7 @@ async function generate(configPath) {
2199
2375
  clientDir = join(originalClientDir, "sdk");
2200
2376
  }
2201
2377
  const normDateType = cfg.dateType === "string" ? "string" : "date";
2378
+ const serverFramework = cfg.serverFramework || "hono";
2202
2379
  console.log("\uD83D\uDCC1 Creating directories...");
2203
2380
  await ensureDirs([
2204
2381
  serverDir,
@@ -2219,12 +2396,16 @@ async function generate(configPath) {
2219
2396
  });
2220
2397
  files.push({
2221
2398
  path: join(serverDir, "include-loader.ts"),
2222
- content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
2399
+ content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3, cfg.useJsExtensions)
2223
2400
  });
2224
2401
  files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
2225
2402
  if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
2226
2403
  files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
2227
2404
  }
2405
+ files.push({
2406
+ path: join(serverDir, "core", "operations.ts"),
2407
+ content: emitCoreOperations()
2408
+ });
2228
2409
  for (const table of Object.values(model.tables)) {
2229
2410
  const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
2230
2411
  files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
@@ -2233,13 +2414,20 @@ async function generate(configPath) {
2233
2414
  path: join(serverDir, "zod", `${table.name}.ts`),
2234
2415
  content: emitZod(table, { dateType: normDateType, numericMode: "string" })
2235
2416
  });
2236
- files.push({
2237
- path: join(serverDir, "routes", `${table.name}.ts`),
2238
- content: emitRoutes(table, graph, {
2417
+ let routeContent;
2418
+ if (serverFramework === "hono") {
2419
+ routeContent = emitHonoRoutes(table, graph, {
2239
2420
  softDeleteColumn: cfg.softDeleteColumn || null,
2240
2421
  includeDepthLimit: cfg.includeDepthLimit || 3,
2241
- authStrategy: normalizedAuth?.strategy
2242
- })
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
2243
2431
  });
2244
2432
  files.push({
2245
2433
  path: join(clientDir, `${table.name}.ts`),
@@ -2250,10 +2438,12 @@ async function generate(configPath) {
2250
2438
  path: join(clientDir, "index.ts"),
2251
2439
  content: emitClientIndex(Object.values(model.tables))
2252
2440
  });
2253
- files.push({
2254
- path: join(serverDir, "router.ts"),
2255
- content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.driver || "pg")
2256
- });
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
+ }
2257
2447
  const clientFiles = files.filter((f) => {
2258
2448
  return f.path.includes(clientDir);
2259
2449
  });