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/README.md +15 -1
- package/dist/cli.js +365 -144
- 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-routes-hono.d.ts +11 -0
- package/dist/index.js +350 -144
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
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
|
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
|
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(),
|
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()
|
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
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
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
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
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
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
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(
|
1125
|
-
|
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"
|
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({
|
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
|
1137
|
-
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);
|
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
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
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
|
1172
|
+
// DELETE
|
1175
1173
|
app.delete(\`\${base}/${pkPath}\`, async (c) => {
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
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
|
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
|
-
|
2206
|
-
|
2207
|
-
|
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
|
-
|
2223
|
-
|
2224
|
-
|
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
|
});
|