postgresdk 0.4.0 → 0.5.1-alpha.1
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 +387 -158
- package/dist/core/operations.d.ts +65 -0
- package/dist/emit-client.d.ts +2 -2
- 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 +366 -158
- package/dist/types.d.ts +3 -0
- 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,159 +819,132 @@ ${hasAuth ? `
|
|
817
819
|
|
818
820
|
// CREATE
|
819
821
|
app.post(base, async (c) => {
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
log.debug("POST ${fileTableName} invalid:", issues);
|
827
|
-
return c.json({ error: "Invalid body", issues }, 400);
|
828
|
-
}
|
829
|
-
|
830
|
-
const data = parsed.data;
|
831
|
-
const cols = Object.keys(data);
|
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);
|
845
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);
|
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
|
-
log.debug("PATCH ${fileTableName} invalid:", issues);
|
918
|
-
return c.json({ error: "Invalid body", issues: issues }, 400);
|
919
|
-
}
|
920
|
-
|
921
|
-
${pkFilter}
|
922
|
-
if (!Object.keys(updateData).length) return c.json({ error: "No updatable fields provided" }, 400);
|
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);
|
934
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);
|
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
|
`;
|
959
933
|
}
|
960
934
|
|
961
935
|
// src/emit-client.ts
|
962
|
-
function emitClient(table) {
|
936
|
+
function emitClient(table, useJsExtensions) {
|
963
937
|
const Type = pascal(table.name);
|
938
|
+
const ext = useJsExtensions ? ".js" : "";
|
964
939
|
const pkCols = Array.isArray(table.pk) ? table.pk : table.pk ? [table.pk] : [];
|
965
940
|
const safePk = pkCols.length ? pkCols : ["id"];
|
966
941
|
const hasCompositePk = safePk.length > 1;
|
967
942
|
const pkType = hasCompositePk ? `{ ${safePk.map((c) => `${c}: string`).join("; ")} }` : `string`;
|
968
943
|
const pkPathExpr = hasCompositePk ? safePk.map((c) => `pk.${c}`).join(` + "/" + `) : `pk`;
|
969
944
|
return `/* Generated. Do not edit. */
|
970
|
-
import { BaseClient } from "./base-client";
|
971
|
-
import type { ${Type}IncludeSpec } from "./include-spec";
|
972
|
-
import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}";
|
945
|
+
import { BaseClient } from "./base-client${ext}";
|
946
|
+
import type { ${Type}IncludeSpec } from "./include-spec${ext}";
|
947
|
+
import type { Insert${Type}, Update${Type}, Select${Type} } from "./types/${table.name}${ext}";
|
973
948
|
|
974
949
|
/**
|
975
950
|
* Client for ${table.name} table operations
|
@@ -1009,19 +984,20 @@ export class ${Type}Client extends BaseClient {
|
|
1009
984
|
}
|
1010
985
|
`;
|
1011
986
|
}
|
1012
|
-
function emitClientIndex(tables) {
|
987
|
+
function emitClientIndex(tables, useJsExtensions) {
|
988
|
+
const ext = useJsExtensions ? ".js" : "";
|
1013
989
|
let out = `/* Generated. Do not edit. */
|
1014
990
|
`;
|
1015
|
-
out += `import { BaseClient, type AuthConfig } from "./base-client";
|
991
|
+
out += `import { BaseClient, type AuthConfig } from "./base-client${ext}";
|
1016
992
|
`;
|
1017
993
|
for (const t of tables) {
|
1018
|
-
out += `import { ${pascal(t.name)}Client } from "./${t.name}";
|
994
|
+
out += `import { ${pascal(t.name)}Client } from "./${t.name}${ext}";
|
1019
995
|
`;
|
1020
996
|
}
|
1021
997
|
out += `
|
1022
998
|
// Re-export auth types for convenience
|
1023
999
|
`;
|
1024
|
-
out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client";
|
1000
|
+
out += `export type { AuthConfig as SDKAuth, AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${ext}";
|
1025
1001
|
|
1026
1002
|
`;
|
1027
1003
|
out += `/**
|
@@ -1053,18 +1029,18 @@ function emitClientIndex(tables) {
|
|
1053
1029
|
out += `// Export individual table clients
|
1054
1030
|
`;
|
1055
1031
|
for (const t of tables) {
|
1056
|
-
out += `export { ${pascal(t.name)}Client } from "./${t.name}";
|
1032
|
+
out += `export { ${pascal(t.name)}Client } from "./${t.name}${ext}";
|
1057
1033
|
`;
|
1058
1034
|
}
|
1059
1035
|
out += `
|
1060
1036
|
// Export base client for custom extensions
|
1061
1037
|
`;
|
1062
|
-
out += `export { BaseClient } from "./base-client";
|
1038
|
+
out += `export { BaseClient } from "./base-client${ext}";
|
1063
1039
|
`;
|
1064
1040
|
out += `
|
1065
1041
|
// Export include specifications
|
1066
1042
|
`;
|
1067
|
-
out += `export * from "./include-spec";
|
1043
|
+
out += `export * from "./include-spec${ext}";
|
1068
1044
|
`;
|
1069
1045
|
return out;
|
1070
1046
|
}
|
@@ -1212,13 +1188,14 @@ export abstract class BaseClient {
|
|
1212
1188
|
}
|
1213
1189
|
|
1214
1190
|
// src/emit-include-loader.ts
|
1215
|
-
function emitIncludeLoader(graph, model, maxDepth) {
|
1191
|
+
function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
|
1216
1192
|
const fkIndex = {};
|
1217
1193
|
for (const t of Object.values(model.tables)) {
|
1218
1194
|
fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
|
1219
1195
|
}
|
1196
|
+
const ext = useJsExtensions ? ".js" : "";
|
1220
1197
|
return `/* Generated. Do not edit. */
|
1221
|
-
import { RELATION_GRAPH } from "./include-builder";
|
1198
|
+
import { RELATION_GRAPH } from "./include-builder${ext}";
|
1222
1199
|
|
1223
1200
|
// Minimal types to keep the file self-contained
|
1224
1201
|
type Graph = typeof RELATION_GRAPH;
|
@@ -1737,12 +1714,13 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
1737
1714
|
`;
|
1738
1715
|
}
|
1739
1716
|
|
1740
|
-
// src/emit-router.ts
|
1741
|
-
function
|
1717
|
+
// src/emit-router-hono.ts
|
1718
|
+
function emitHonoRouter(tables, hasAuth, useJsExtensions) {
|
1742
1719
|
const tableNames = tables.map((t) => t.name).sort();
|
1720
|
+
const ext = useJsExtensions ? ".js" : "";
|
1743
1721
|
const imports = tableNames.map((name) => {
|
1744
1722
|
const Type = pascal(name);
|
1745
|
-
return `import { register${Type}Routes } from "./routes/${name}";`;
|
1723
|
+
return `import { register${Type}Routes } from "./routes/${name}${ext}";`;
|
1746
1724
|
}).join(`
|
1747
1725
|
`);
|
1748
1726
|
const registrations = tableNames.map((name) => {
|
@@ -1752,14 +1730,14 @@ function emitRouter(tables, hasAuth) {
|
|
1752
1730
|
`);
|
1753
1731
|
const reExports = tableNames.map((name) => {
|
1754
1732
|
const Type = pascal(name);
|
1755
|
-
return `export { register${Type}Routes } from "./routes/${name}";`;
|
1733
|
+
return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
|
1756
1734
|
}).join(`
|
1757
1735
|
`);
|
1758
1736
|
return `/* Generated. Do not edit. */
|
1759
1737
|
import { Hono } from "hono";
|
1760
|
-
import { SDK_MANIFEST } from "./sdk-bundle";
|
1738
|
+
import { SDK_MANIFEST } from "./sdk-bundle${ext}";
|
1761
1739
|
${imports}
|
1762
|
-
${hasAuth ? `export { authMiddleware } from "./auth";` : ""}
|
1740
|
+
${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
1763
1741
|
|
1764
1742
|
/**
|
1765
1743
|
* Creates a Hono router with all generated routes that can be mounted into your existing app.
|
@@ -1848,7 +1826,7 @@ ${registrations.replace(/router/g, "app")}
|
|
1848
1826
|
${reExports}
|
1849
1827
|
|
1850
1828
|
// Re-export types and schemas for convenience
|
1851
|
-
export * from "./include-spec";
|
1829
|
+
export * from "./include-spec${ext}";
|
1852
1830
|
`;
|
1853
1831
|
}
|
1854
1832
|
|
@@ -1875,6 +1853,222 @@ export const SDK_MANIFEST = {
|
|
1875
1853
|
`;
|
1876
1854
|
}
|
1877
1855
|
|
1856
|
+
// src/emit-core-operations.ts
|
1857
|
+
function emitCoreOperations() {
|
1858
|
+
return `/**
|
1859
|
+
* Core database operations that are framework-agnostic.
|
1860
|
+
* These functions handle the actual database logic and can be used by any framework adapter.
|
1861
|
+
*/
|
1862
|
+
|
1863
|
+
import type { z } from "zod";
|
1864
|
+
|
1865
|
+
export interface DatabaseClient {
|
1866
|
+
query: (text: string, params?: any[]) => Promise<{ rows: any[] }>;
|
1867
|
+
}
|
1868
|
+
|
1869
|
+
export interface OperationContext {
|
1870
|
+
pg: DatabaseClient;
|
1871
|
+
table: string;
|
1872
|
+
pkColumns: string[];
|
1873
|
+
softDeleteColumn?: string | null;
|
1874
|
+
includeDepthLimit: number;
|
1875
|
+
}
|
1876
|
+
|
1877
|
+
const DEBUG = process.env.SDK_DEBUG === "1" || process.env.SDK_DEBUG === "true";
|
1878
|
+
const log = {
|
1879
|
+
debug: (...args: any[]) => { if (DEBUG) console.debug("[sdk]", ...args); },
|
1880
|
+
error: (...args: any[]) => console.error("[sdk]", ...args),
|
1881
|
+
};
|
1882
|
+
|
1883
|
+
/**
|
1884
|
+
* CREATE operation - Insert a new record
|
1885
|
+
*/
|
1886
|
+
export async function createRecord(
|
1887
|
+
ctx: OperationContext,
|
1888
|
+
data: Record<string, any>
|
1889
|
+
): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
|
1890
|
+
try {
|
1891
|
+
const cols = Object.keys(data);
|
1892
|
+
const vals = Object.values(data);
|
1893
|
+
|
1894
|
+
if (!cols.length) {
|
1895
|
+
return { error: "No fields provided", status: 400 };
|
1896
|
+
}
|
1897
|
+
|
1898
|
+
const placeholders = cols.map((_, i) => '$' + (i + 1)).join(", ");
|
1899
|
+
const text = \`INSERT INTO "\${ctx.table}" (\${cols.map(c => '"' + c + '"').join(", ")})
|
1900
|
+
VALUES (\${placeholders})
|
1901
|
+
RETURNING *\`;
|
1902
|
+
|
1903
|
+
log.debug("SQL:", text, "vals:", vals);
|
1904
|
+
const { rows } = await ctx.pg.query(text, vals);
|
1905
|
+
|
1906
|
+
return { data: rows[0] ?? null, status: rows[0] ? 201 : 500 };
|
1907
|
+
} catch (e: any) {
|
1908
|
+
log.error(\`POST \${ctx.table} error:\`, e?.stack ?? e);
|
1909
|
+
return {
|
1910
|
+
error: e?.message ?? "Internal error",
|
1911
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
1912
|
+
status: 500
|
1913
|
+
};
|
1914
|
+
}
|
1915
|
+
}
|
1916
|
+
|
1917
|
+
/**
|
1918
|
+
* READ operation - Get a record by primary key
|
1919
|
+
*/
|
1920
|
+
export async function getByPk(
|
1921
|
+
ctx: OperationContext,
|
1922
|
+
pkValues: any[]
|
1923
|
+
): Promise<{ data?: any; error?: string; status: number }> {
|
1924
|
+
try {
|
1925
|
+
const hasCompositePk = ctx.pkColumns.length > 1;
|
1926
|
+
const wherePkSql = hasCompositePk
|
1927
|
+
? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
|
1928
|
+
: \`"\${ctx.pkColumns[0]}" = $1\`;
|
1929
|
+
|
1930
|
+
const text = \`SELECT * FROM "\${ctx.table}" WHERE \${wherePkSql} LIMIT 1\`;
|
1931
|
+
log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
|
1932
|
+
|
1933
|
+
const { rows } = await ctx.pg.query(text, pkValues);
|
1934
|
+
|
1935
|
+
if (!rows[0]) {
|
1936
|
+
return { data: null, status: 404 };
|
1937
|
+
}
|
1938
|
+
|
1939
|
+
return { data: rows[0], status: 200 };
|
1940
|
+
} catch (e: any) {
|
1941
|
+
log.error(\`GET \${ctx.table} error:\`, e?.stack ?? e);
|
1942
|
+
return {
|
1943
|
+
error: e?.message ?? "Internal error",
|
1944
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
1945
|
+
status: 500
|
1946
|
+
};
|
1947
|
+
}
|
1948
|
+
}
|
1949
|
+
|
1950
|
+
/**
|
1951
|
+
* LIST operation - Get multiple records with optional filters
|
1952
|
+
*/
|
1953
|
+
export async function listRecords(
|
1954
|
+
ctx: OperationContext,
|
1955
|
+
params: { limit?: number; offset?: number; include?: any }
|
1956
|
+
): Promise<{ data?: any; error?: string; issues?: any; status: number; needsIncludes?: boolean; includeSpec?: any }> {
|
1957
|
+
try {
|
1958
|
+
const { limit = 50, offset = 0, include } = params;
|
1959
|
+
|
1960
|
+
const where = ctx.softDeleteColumn
|
1961
|
+
? \`WHERE "\${ctx.softDeleteColumn}" IS NULL\`
|
1962
|
+
: "";
|
1963
|
+
|
1964
|
+
const text = \`SELECT * FROM "\${ctx.table}" \${where} LIMIT $1 OFFSET $2\`;
|
1965
|
+
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", [limit, offset]);
|
1966
|
+
|
1967
|
+
const { rows } = await ctx.pg.query(text, [limit, offset]);
|
1968
|
+
|
1969
|
+
if (!include) {
|
1970
|
+
log.debug(\`LIST \${ctx.table} rows:\`, rows.length);
|
1971
|
+
return { data: rows, status: 200 };
|
1972
|
+
}
|
1973
|
+
|
1974
|
+
// Include logic will be handled by the include-loader
|
1975
|
+
// For now, just return the rows with a note that includes need to be applied
|
1976
|
+
log.debug(\`LIST \${ctx.table} include spec:\`, include);
|
1977
|
+
return { data: rows, needsIncludes: true, includeSpec: include, status: 200 };
|
1978
|
+
} catch (e: any) {
|
1979
|
+
log.error(\`LIST \${ctx.table} error:\`, e?.stack ?? e);
|
1980
|
+
return {
|
1981
|
+
error: e?.message ?? "Internal error",
|
1982
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
1983
|
+
status: 500
|
1984
|
+
};
|
1985
|
+
}
|
1986
|
+
}
|
1987
|
+
|
1988
|
+
/**
|
1989
|
+
* UPDATE operation - Update a record by primary key
|
1990
|
+
*/
|
1991
|
+
export async function updateRecord(
|
1992
|
+
ctx: OperationContext,
|
1993
|
+
pkValues: any[],
|
1994
|
+
updateData: Record<string, any>
|
1995
|
+
): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
|
1996
|
+
try {
|
1997
|
+
// Filter out PK columns from update data
|
1998
|
+
const filteredData = Object.fromEntries(
|
1999
|
+
Object.entries(updateData).filter(([k]) => !ctx.pkColumns.includes(k))
|
2000
|
+
);
|
2001
|
+
|
2002
|
+
if (!Object.keys(filteredData).length) {
|
2003
|
+
return { error: "No updatable fields provided", status: 400 };
|
2004
|
+
}
|
2005
|
+
|
2006
|
+
const hasCompositePk = ctx.pkColumns.length > 1;
|
2007
|
+
const wherePkSql = hasCompositePk
|
2008
|
+
? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
|
2009
|
+
: \`"\${ctx.pkColumns[0]}" = $1\`;
|
2010
|
+
|
2011
|
+
const setSql = Object.keys(filteredData)
|
2012
|
+
.map((k, i) => \`"\${k}" = $\${i + pkValues.length + 1}\`)
|
2013
|
+
.join(", ");
|
2014
|
+
|
2015
|
+
const text = \`UPDATE "\${ctx.table}" SET \${setSql} WHERE \${wherePkSql} RETURNING *\`;
|
2016
|
+
const params = [...pkValues, ...Object.values(filteredData)];
|
2017
|
+
|
2018
|
+
log.debug(\`PATCH \${ctx.table} SQL:\`, text, "params:", params);
|
2019
|
+
const { rows } = await ctx.pg.query(text, params);
|
2020
|
+
|
2021
|
+
if (!rows[0]) {
|
2022
|
+
return { data: null, status: 404 };
|
2023
|
+
}
|
2024
|
+
|
2025
|
+
return { data: rows[0], status: 200 };
|
2026
|
+
} catch (e: any) {
|
2027
|
+
log.error(\`PATCH \${ctx.table} error:\`, e?.stack ?? e);
|
2028
|
+
return {
|
2029
|
+
error: e?.message ?? "Internal error",
|
2030
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
2031
|
+
status: 500
|
2032
|
+
};
|
2033
|
+
}
|
2034
|
+
}
|
2035
|
+
|
2036
|
+
/**
|
2037
|
+
* DELETE operation - Delete or soft-delete a record by primary key
|
2038
|
+
*/
|
2039
|
+
export async function deleteRecord(
|
2040
|
+
ctx: OperationContext,
|
2041
|
+
pkValues: any[]
|
2042
|
+
): Promise<{ data?: any; error?: string; status: number }> {
|
2043
|
+
try {
|
2044
|
+
const hasCompositePk = ctx.pkColumns.length > 1;
|
2045
|
+
const wherePkSql = hasCompositePk
|
2046
|
+
? ctx.pkColumns.map((c, i) => \`"\${c}" = $\${i + 1}\`).join(" AND ")
|
2047
|
+
: \`"\${ctx.pkColumns[0]}" = $1\`;
|
2048
|
+
|
2049
|
+
const text = ctx.softDeleteColumn
|
2050
|
+
? \`UPDATE "\${ctx.table}" SET "\${ctx.softDeleteColumn}" = NOW() WHERE \${wherePkSql} RETURNING *\`
|
2051
|
+
: \`DELETE FROM "\${ctx.table}" WHERE \${wherePkSql} RETURNING *\`;
|
2052
|
+
|
2053
|
+
log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
|
2054
|
+
const { rows } = await ctx.pg.query(text, pkValues);
|
2055
|
+
|
2056
|
+
if (!rows[0]) {
|
2057
|
+
return { data: null, status: 404 };
|
2058
|
+
}
|
2059
|
+
|
2060
|
+
return { data: rows[0], status: 200 };
|
2061
|
+
} catch (e: any) {
|
2062
|
+
log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
|
2063
|
+
return {
|
2064
|
+
error: e?.message ?? "Internal error",
|
2065
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
2066
|
+
status: 500
|
2067
|
+
};
|
2068
|
+
}
|
2069
|
+
}`;
|
2070
|
+
}
|
2071
|
+
|
1878
2072
|
// src/types.ts
|
1879
2073
|
function normalizeAuthConfig(input) {
|
1880
2074
|
if (!input)
|
@@ -1931,6 +2125,7 @@ async function generate(configPath) {
|
|
1931
2125
|
clientDir = join(originalClientDir, "sdk");
|
1932
2126
|
}
|
1933
2127
|
const normDateType = cfg.dateType === "string" ? "string" : "date";
|
2128
|
+
const serverFramework = cfg.serverFramework || "hono";
|
1934
2129
|
console.log("\uD83D\uDCC1 Creating directories...");
|
1935
2130
|
await ensureDirs([
|
1936
2131
|
serverDir,
|
@@ -1951,12 +2146,16 @@ async function generate(configPath) {
|
|
1951
2146
|
});
|
1952
2147
|
files.push({
|
1953
2148
|
path: join(serverDir, "include-loader.ts"),
|
1954
|
-
content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3)
|
2149
|
+
content: emitIncludeLoader(graph, model, cfg.includeDepthLimit || 3, cfg.useJsExtensions)
|
1955
2150
|
});
|
1956
2151
|
files.push({ path: join(serverDir, "logger.ts"), content: emitLogger() });
|
1957
2152
|
if (normalizedAuth?.strategy && normalizedAuth.strategy !== "none") {
|
1958
2153
|
files.push({ path: join(serverDir, "auth.ts"), content: emitAuth(normalizedAuth) });
|
1959
2154
|
}
|
2155
|
+
files.push({
|
2156
|
+
path: join(serverDir, "core", "operations.ts"),
|
2157
|
+
content: emitCoreOperations()
|
2158
|
+
});
|
1960
2159
|
for (const table of Object.values(model.tables)) {
|
1961
2160
|
const typesSrc = emitTypes(table, { dateType: normDateType, numericMode: "string" });
|
1962
2161
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
@@ -1965,27 +2164,36 @@ async function generate(configPath) {
|
|
1965
2164
|
path: join(serverDir, "zod", `${table.name}.ts`),
|
1966
2165
|
content: emitZod(table, { dateType: normDateType, numericMode: "string" })
|
1967
2166
|
});
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
2167
|
+
let routeContent;
|
2168
|
+
if (serverFramework === "hono") {
|
2169
|
+
routeContent = emitHonoRoutes(table, graph, {
|
1971
2170
|
softDeleteColumn: cfg.softDeleteColumn || null,
|
1972
2171
|
includeDepthLimit: cfg.includeDepthLimit || 3,
|
1973
|
-
authStrategy: normalizedAuth?.strategy
|
1974
|
-
|
2172
|
+
authStrategy: normalizedAuth?.strategy,
|
2173
|
+
useJsExtensions: cfg.useJsExtensions
|
2174
|
+
});
|
2175
|
+
} else {
|
2176
|
+
throw new Error(`Framework "${serverFramework}" is not yet supported. Currently only "hono" is available.`);
|
2177
|
+
}
|
2178
|
+
files.push({
|
2179
|
+
path: join(serverDir, "routes", `${table.name}.ts`),
|
2180
|
+
content: routeContent
|
1975
2181
|
});
|
1976
2182
|
files.push({
|
1977
2183
|
path: join(clientDir, `${table.name}.ts`),
|
1978
|
-
content: emitClient(table)
|
2184
|
+
content: emitClient(table, cfg.useJsExtensionsClient)
|
1979
2185
|
});
|
1980
2186
|
}
|
1981
2187
|
files.push({
|
1982
2188
|
path: join(clientDir, "index.ts"),
|
1983
|
-
content: emitClientIndex(Object.values(model.tables))
|
1984
|
-
});
|
1985
|
-
files.push({
|
1986
|
-
path: join(serverDir, "router.ts"),
|
1987
|
-
content: emitRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none")
|
2189
|
+
content: emitClientIndex(Object.values(model.tables), cfg.useJsExtensionsClient)
|
1988
2190
|
});
|
2191
|
+
if (serverFramework === "hono") {
|
2192
|
+
files.push({
|
2193
|
+
path: join(serverDir, "router.ts"),
|
2194
|
+
content: emitHonoRouter(Object.values(model.tables), !!normalizedAuth?.strategy && normalizedAuth.strategy !== "none", cfg.useJsExtensions)
|
2195
|
+
});
|
2196
|
+
}
|
1989
2197
|
const clientFiles = files.filter((f) => {
|
1990
2198
|
return f.path.includes(clientDir);
|
1991
2199
|
});
|