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