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/README.md +123 -1
- package/dist/cli.js +383 -193
- package/dist/core/operations.d.ts +65 -0
- package/dist/emit-core-operations.d.ts +4 -0
- package/dist/emit-include-loader.d.ts +1 -1
- package/dist/emit-router-hono.d.ts +5 -0
- package/dist/emit-router.d.ts +1 -1
- package/dist/emit-routes-hono.d.ts +11 -0
- package/dist/index.js +371 -188
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
@@ -570,12 +570,19 @@ export default {
|
|
570
570
|
// dateType: "date",
|
571
571
|
|
572
572
|
/**
|
573
|
-
*
|
574
|
-
* - "
|
575
|
-
* - "
|
576
|
-
*
|
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
|
-
//
|
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
|
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
|
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(),
|
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()
|
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
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
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
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
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
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
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(
|
1133
|
-
|
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"
|
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({
|
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
|
1145
|
-
return c.json({
|
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
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
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
|
1172
|
+
// DELETE
|
1183
1173
|
app.delete(\`\${base}/${pkPath}\`, async (c) => {
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
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
|
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"
|
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"
|
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
|
-
*
|
2097
|
-
* const
|
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
|
-
|
2237
|
-
|
2238
|
-
|
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
|
-
|
2254
|
-
|
2255
|
-
|
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
|
});
|