postgresdk 0.18.30 → 0.19.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/LICENSE +1 -1
- package/README.md +59 -8
- package/dist/cli-config-utils.d.ts +2 -1
- package/dist/cli.js +768 -175
- package/dist/emit-client.d.ts +2 -0
- package/dist/emit-router-hono.d.ts +5 -1
- package/dist/emit-routes-hono.d.ts +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +733 -120
- package/dist/types.d.ts +10 -3
- package/dist/utils.d.ts +4 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -491,6 +491,14 @@ var require_config = __commonJS(() => {
|
|
|
491
491
|
import { mkdir, writeFile, readFile, readdir, unlink } from "fs/promises";
|
|
492
492
|
import { dirname, join } from "path";
|
|
493
493
|
import { existsSync } from "fs";
|
|
494
|
+
function isVectorType(pgType) {
|
|
495
|
+
const t = pgType.toLowerCase();
|
|
496
|
+
return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
|
|
497
|
+
}
|
|
498
|
+
function isJsonbType(pgType) {
|
|
499
|
+
const t = pgType.toLowerCase();
|
|
500
|
+
return t === "json" || t === "jsonb";
|
|
501
|
+
}
|
|
494
502
|
async function writeFilesIfChanged(files) {
|
|
495
503
|
let written = 0;
|
|
496
504
|
let unchanged = 0;
|
|
@@ -889,9 +897,10 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
889
897
|
const hasSinglePK = table.pk.length === 1;
|
|
890
898
|
const pkField = hasSinglePK ? table.pk[0] : "id";
|
|
891
899
|
const enums = model.enums || {};
|
|
892
|
-
const overrides = config?.softDeleteColumnOverrides;
|
|
893
|
-
const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.softDeleteColumn ?? null;
|
|
900
|
+
const overrides = config?.delete?.softDeleteColumnOverrides;
|
|
901
|
+
const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.delete?.softDeleteColumn ?? null;
|
|
894
902
|
const softDeleteCol = resolvedSoftDeleteCol && table.columns.some((c) => c.name === resolvedSoftDeleteCol) ? resolvedSoftDeleteCol : null;
|
|
903
|
+
const exposeHardDelete = config?.delete?.exposeHardDelete ?? true;
|
|
895
904
|
const sdkMethods = [];
|
|
896
905
|
const endpoints = [];
|
|
897
906
|
sdkMethods.push({
|
|
@@ -967,16 +976,47 @@ const withDeleted = await sdk.${tableName}.getByPk('id', { includeSoftDeleted: t
|
|
|
967
976
|
}
|
|
968
977
|
if (hasSinglePK) {
|
|
969
978
|
sdkMethods.push({
|
|
970
|
-
name: "
|
|
971
|
-
signature: `
|
|
972
|
-
description: `
|
|
973
|
-
example: `const
|
|
974
|
-
|
|
979
|
+
name: "upsert",
|
|
980
|
+
signature: `upsert(args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} }): Promise<${Type}>`,
|
|
981
|
+
description: `Insert or update a ${tableName} based on a conflict target. The 'where' keys define the unique conflict columns (must be a unique constraint). 'create' is used if no conflict; 'update' is applied if a conflict occurs.`,
|
|
982
|
+
example: `const result = await sdk.${tableName}.upsert({
|
|
983
|
+
where: { ${pkField}: 'some-id' },
|
|
984
|
+
create: { ${generateExampleFields(table, "create")} },
|
|
985
|
+
update: { ${generateExampleFields(table, "update")} },
|
|
986
|
+
});`,
|
|
987
|
+
correspondsTo: `POST ${basePath}/upsert`
|
|
975
988
|
});
|
|
989
|
+
endpoints.push({
|
|
990
|
+
method: "POST",
|
|
991
|
+
path: `${basePath}/upsert`,
|
|
992
|
+
description: `Upsert ${tableName} — insert if no conflict on 'where' columns, update otherwise`,
|
|
993
|
+
requestBody: `{ where: Update${Type}; create: Insert${Type}; update: Update${Type} }`,
|
|
994
|
+
responseBody: `${Type}`
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (hasSinglePK) {
|
|
998
|
+
if (softDeleteCol) {
|
|
999
|
+
sdkMethods.push({
|
|
1000
|
+
name: "softDelete",
|
|
1001
|
+
signature: `softDelete(${pkField}: string): Promise<${Type}>`,
|
|
1002
|
+
description: `Soft-delete a ${tableName} (sets ${softDeleteCol})`,
|
|
1003
|
+
example: `const deleted = await sdk.${tableName}.softDelete('id');`,
|
|
1004
|
+
correspondsTo: `DELETE ${basePath}/:${pkField}`
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
if (!softDeleteCol || exposeHardDelete) {
|
|
1008
|
+
sdkMethods.push({
|
|
1009
|
+
name: "hardDelete",
|
|
1010
|
+
signature: `hardDelete(${pkField}: string): Promise<${Type}>`,
|
|
1011
|
+
description: `Permanently delete a ${tableName}`,
|
|
1012
|
+
example: `const deleted = await sdk.${tableName}.hardDelete('id');`,
|
|
1013
|
+
correspondsTo: softDeleteCol ? `DELETE ${basePath}/:${pkField}?hard=true` : `DELETE ${basePath}/:${pkField}`
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
976
1016
|
endpoints.push({
|
|
977
1017
|
method: "DELETE",
|
|
978
1018
|
path: `${basePath}/:${pkField}`,
|
|
979
|
-
description: `Delete ${tableName}`,
|
|
1019
|
+
description: softDeleteCol ? `Delete ${tableName} (soft-delete by default${exposeHardDelete ? "; add ?hard=true for permanent deletion" : ""})` : `Delete ${tableName}`,
|
|
980
1020
|
responseBody: `${Type}`
|
|
981
1021
|
});
|
|
982
1022
|
}
|
|
@@ -1879,6 +1919,18 @@ ${insertFields}
|
|
|
1879
1919
|
});
|
|
1880
1920
|
|
|
1881
1921
|
export const Update${Type}Schema = Insert${Type}Schema.partial();
|
|
1922
|
+
|
|
1923
|
+
export const Upsert${Type}Schema = z.object({
|
|
1924
|
+
where: Update${Type}Schema.refine(
|
|
1925
|
+
(d) => Object.keys(d).length > 0,
|
|
1926
|
+
{ message: "where must specify at least one column" }
|
|
1927
|
+
),
|
|
1928
|
+
create: Insert${Type}Schema,
|
|
1929
|
+
update: Update${Type}Schema.refine(
|
|
1930
|
+
(d) => Object.keys(d).length > 0,
|
|
1931
|
+
{ message: "update must specify at least one column" }
|
|
1932
|
+
),
|
|
1933
|
+
});
|
|
1882
1934
|
`;
|
|
1883
1935
|
}
|
|
1884
1936
|
|
|
@@ -1972,13 +2024,6 @@ export interface PaginatedResponse<T> {
|
|
|
1972
2024
|
|
|
1973
2025
|
// src/emit-routes-hono.ts
|
|
1974
2026
|
init_utils();
|
|
1975
|
-
function isVectorType(pgType) {
|
|
1976
|
-
const t = pgType.toLowerCase();
|
|
1977
|
-
return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
|
|
1978
|
-
}
|
|
1979
|
-
function isJsonbType(pgType) {
|
|
1980
|
-
return pgType.toLowerCase() === "json" || pgType.toLowerCase() === "jsonb";
|
|
1981
|
-
}
|
|
1982
2027
|
function emitHonoRoutes(table, _graph, opts) {
|
|
1983
2028
|
const fileTableName = table.name;
|
|
1984
2029
|
const Type = pascal(table.name);
|
|
@@ -1992,6 +2037,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
1992
2037
|
const hasCompositePk = safePkCols.length > 1;
|
|
1993
2038
|
const pkPath = hasCompositePk ? safePkCols.map((c) => `:${c}`).join("/") : `:${safePkCols[0]}`;
|
|
1994
2039
|
const softDel = opts.softDeleteColumn && table.columns.some((c) => c.name === opts.softDeleteColumn) ? opts.softDeleteColumn : null;
|
|
2040
|
+
const exposeHard = !!(softDel && opts.exposeHardDelete);
|
|
1995
2041
|
const getPkParams = hasCompositePk ? `const pkValues = [${safePkCols.map((c) => `c.req.param("${c}")`).join(", ")}];` : `const pkValues = [c.req.param("${safePkCols[0]}")];`;
|
|
1996
2042
|
const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
|
|
1997
2043
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
@@ -2008,7 +2054,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
2008
2054
|
import { Hono } from "hono";
|
|
2009
2055
|
import type { Context } from "hono";
|
|
2010
2056
|
import { z } from "zod";
|
|
2011
|
-
import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
|
|
2057
|
+
import { Insert${Type}Schema, Update${Type}Schema, Upsert${Type}Schema } from "../zod/${fileTableName}${ext}";
|
|
2012
2058
|
import { loadIncludes } from "../include-loader${ext}";
|
|
2013
2059
|
import * as coreOps from "../core/operations${ext}";
|
|
2014
2060
|
${authImport}
|
|
@@ -2017,10 +2063,12 @@ const columnEnum = z.enum([${columnNames}]);
|
|
|
2017
2063
|
|
|
2018
2064
|
const createSchema = Insert${Type}Schema;
|
|
2019
2065
|
const updateSchema = Update${Type}Schema;
|
|
2066
|
+
const upsertSchema = Upsert${Type}Schema;
|
|
2020
2067
|
|
|
2021
2068
|
const deleteSchema = z.object({
|
|
2022
2069
|
select: z.array(z.string()).min(1).optional(),
|
|
2023
|
-
exclude: z.array(z.string()).min(1).optional()
|
|
2070
|
+
exclude: z.array(z.string()).min(1).optional(),${exposeHard ? `
|
|
2071
|
+
hard: z.boolean().optional(),` : ""}
|
|
2024
2072
|
}).strict().refine(
|
|
2025
2073
|
(data) => !(data.select && data.exclude),
|
|
2026
2074
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -2243,6 +2291,39 @@ ${hasAuth ? `
|
|
|
2243
2291
|
}, result.status as any);
|
|
2244
2292
|
});
|
|
2245
2293
|
|
|
2294
|
+
// UPSERT
|
|
2295
|
+
app.post(\`\${base}/upsert\`, async (c) => {
|
|
2296
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2297
|
+
const parsed = upsertSchema.safeParse(body);
|
|
2298
|
+
|
|
2299
|
+
if (!parsed.success) {
|
|
2300
|
+
const issues = parsed.error.flatten();
|
|
2301
|
+
return c.json({ error: "Invalid body", issues }, 400);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const selectParam = c.req.query("select");
|
|
2305
|
+
const excludeParam = c.req.query("exclude");
|
|
2306
|
+
const select = selectParam ? selectParam.split(",") : undefined;
|
|
2307
|
+
const exclude = excludeParam ? excludeParam.split(",") : undefined;
|
|
2308
|
+
|
|
2309
|
+
if (select && exclude) {
|
|
2310
|
+
return c.json({ error: "Cannot specify both 'select' and 'exclude' parameters" }, 400);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
if (deps.onRequest) {
|
|
2314
|
+
await deps.onRequest(c, deps.pg);
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
const ctx = { ...baseCtx, select, exclude };
|
|
2318
|
+
const result = await coreOps.upsertRecord(ctx, parsed.data);
|
|
2319
|
+
|
|
2320
|
+
if (result.error) {
|
|
2321
|
+
return c.json({ error: result.error }, result.status as any);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
return c.json(result.data, result.status as any);
|
|
2325
|
+
});
|
|
2326
|
+
|
|
2246
2327
|
// UPDATE
|
|
2247
2328
|
app.patch(\`\${base}/${pkPath}\`, async (c) => {
|
|
2248
2329
|
${getPkParams}
|
|
@@ -2282,13 +2363,15 @@ ${hasAuth ? `
|
|
|
2282
2363
|
app.delete(\`\${base}/${pkPath}\`, async (c) => {
|
|
2283
2364
|
${getPkParams}
|
|
2284
2365
|
|
|
2285
|
-
// Parse query params for select/exclude
|
|
2366
|
+
// Parse query params for select/exclude${exposeHard ? " and hard" : ""}
|
|
2286
2367
|
const selectParam = c.req.query("select");
|
|
2287
2368
|
const excludeParam = c.req.query("exclude");
|
|
2288
2369
|
const queryData: any = {};
|
|
2289
2370
|
if (selectParam) queryData.select = selectParam.split(",");
|
|
2290
2371
|
if (excludeParam) queryData.exclude = excludeParam.split(",");
|
|
2291
|
-
|
|
2372
|
+
${exposeHard ? ` const hardParam = c.req.query("hard");
|
|
2373
|
+
if (hardParam !== undefined) queryData.hard = hardParam === "true";
|
|
2374
|
+
` : ""}
|
|
2292
2375
|
const queryParsed = deleteSchema.safeParse(queryData);
|
|
2293
2376
|
if (!queryParsed.success) {
|
|
2294
2377
|
const issues = queryParsed.error.flatten();
|
|
@@ -2300,7 +2383,7 @@ ${hasAuth ? `
|
|
|
2300
2383
|
}
|
|
2301
2384
|
|
|
2302
2385
|
const ctx = { ...baseCtx, select: queryParsed.data.select, exclude: queryParsed.data.exclude };
|
|
2303
|
-
const result = await coreOps.deleteRecord(ctx, pkValues);
|
|
2386
|
+
const result = await coreOps.deleteRecord(ctx, pkValues${exposeHard ? ", { hard: queryParsed.data.hard }" : ""});
|
|
2304
2387
|
|
|
2305
2388
|
if (result.error) {
|
|
2306
2389
|
return c.json({ error: result.error }, result.status as any);
|
|
@@ -2315,14 +2398,6 @@ ${hasAuth ? `
|
|
|
2315
2398
|
// src/emit-client.ts
|
|
2316
2399
|
init_utils();
|
|
2317
2400
|
init_emit_include_methods();
|
|
2318
|
-
function isVectorType2(pgType) {
|
|
2319
|
-
const t = pgType.toLowerCase();
|
|
2320
|
-
return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
|
|
2321
|
-
}
|
|
2322
|
-
function isJsonbType2(pgType) {
|
|
2323
|
-
const t = pgType.toLowerCase();
|
|
2324
|
-
return t === "json" || t === "jsonb";
|
|
2325
|
-
}
|
|
2326
2401
|
function toIncludeParamName(relationKey) {
|
|
2327
2402
|
return `${relationKey}Include`;
|
|
2328
2403
|
}
|
|
@@ -2345,10 +2420,12 @@ function emitClient(table, graph, opts, model) {
|
|
|
2345
2420
|
const Type = pascal(table.name);
|
|
2346
2421
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
2347
2422
|
const trigramParamType = `{ field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: string[]; strategy?: "greatest" | "concat"; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: Array<{ field: string; weight: number }>; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number }`;
|
|
2348
|
-
const hasVectorColumns = table.columns.some((c) =>
|
|
2349
|
-
const hasJsonbColumns = table.columns.some((c) =>
|
|
2423
|
+
const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
|
|
2424
|
+
const hasJsonbColumns = table.columns.some((c) => isJsonbType(c.pgType));
|
|
2425
|
+
const hasSoftDelete = !!opts.softDeleteColumn;
|
|
2426
|
+
const exposeHard = !hasSoftDelete || (opts.exposeHardDelete ?? true);
|
|
2350
2427
|
if (process.env.SDK_DEBUG) {
|
|
2351
|
-
const vectorCols = table.columns.filter((c) =>
|
|
2428
|
+
const vectorCols = table.columns.filter((c) => isVectorType(c.pgType));
|
|
2352
2429
|
if (vectorCols.length > 0) {
|
|
2353
2430
|
console.log(`[DEBUG] Table ${table.name}: Found ${vectorCols.length} vector columns:`, vectorCols.map((c) => `${c.name} (${c.pgType})`));
|
|
2354
2431
|
}
|
|
@@ -2578,6 +2655,68 @@ function emitClient(table, graph, opts, model) {
|
|
|
2578
2655
|
`;
|
|
2579
2656
|
}
|
|
2580
2657
|
}
|
|
2658
|
+
const buildDeleteMethod = (methodName, setHardTrue) => {
|
|
2659
|
+
const actionName = methodName === "softDelete" ? "Soft-delete" : "Hard-delete (permanently remove)";
|
|
2660
|
+
const hardLine = setHardTrue ? `
|
|
2661
|
+
queryParams.set('hard', 'true');` : "";
|
|
2662
|
+
const pksLabel = hasCompositePk ? "s" : "";
|
|
2663
|
+
const G = hasJsonbColumns ? `<TJsonb extends Partial<Select${Type}> = {}>` : "";
|
|
2664
|
+
const RBase = hasJsonbColumns ? `Select${Type}<TJsonb>` : `Select${Type}`;
|
|
2665
|
+
const RPart = hasJsonbColumns ? `Partial<Select${Type}<TJsonb>>` : `Partial<Select${Type}>`;
|
|
2666
|
+
return ` /**
|
|
2667
|
+
` + ` * ${actionName} a ${table.name} record by primary key with field selection
|
|
2668
|
+
` + ` * @param pk - The primary key value${pksLabel}
|
|
2669
|
+
` + ` * @param options - Select specific fields to return
|
|
2670
|
+
` + ` * @returns The deleted record with only selected fields if found, null otherwise
|
|
2671
|
+
` + ` */
|
|
2672
|
+
` + ` async ${methodName}${G}(pk: ${pkType}, options: { select: string[] }): Promise<${RPart} | null>;
|
|
2673
|
+
` + ` /**
|
|
2674
|
+
` + ` * ${actionName} a ${table.name} record by primary key with field exclusion
|
|
2675
|
+
` + ` * @param pk - The primary key value${pksLabel}
|
|
2676
|
+
` + ` * @param options - Exclude specific fields from return
|
|
2677
|
+
` + ` * @returns The deleted record without excluded fields if found, null otherwise
|
|
2678
|
+
` + ` */
|
|
2679
|
+
` + ` async ${methodName}${G}(pk: ${pkType}, options: { exclude: string[] }): Promise<${RPart} | null>;
|
|
2680
|
+
` + ` /**
|
|
2681
|
+
` + ` * ${actionName} a ${table.name} record by primary key
|
|
2682
|
+
` + ` * @param pk - The primary key value${pksLabel}
|
|
2683
|
+
` + ` * @returns The deleted record with all fields if found, null otherwise
|
|
2684
|
+
` + ` */
|
|
2685
|
+
` + ` async ${methodName}${G}(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<${RBase} | null>;
|
|
2686
|
+
` + ` async ${methodName}${G}(
|
|
2687
|
+
` + ` pk: ${pkType},
|
|
2688
|
+
` + ` options?: { select?: string[]; exclude?: string[] }
|
|
2689
|
+
` + ` ): Promise<${RBase} | ${RPart} | null> {
|
|
2690
|
+
` + ` const path = ${pkPathExpr};
|
|
2691
|
+
` + ` const queryParams = new URLSearchParams();${hardLine}
|
|
2692
|
+
` + ` if (options?.select) queryParams.set('select', options.select.join(','));
|
|
2693
|
+
` + ` if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
2694
|
+
` + ` const query = queryParams.toString();
|
|
2695
|
+
` + " const url = query ? `${this.resource}/${path}?${query}` : `${this.resource}/${path}`;\n" + ` return this.del<${RBase} | null>(url);
|
|
2696
|
+
` + ` }`;
|
|
2697
|
+
};
|
|
2698
|
+
const deleteMethodParts = [];
|
|
2699
|
+
if (hasSoftDelete)
|
|
2700
|
+
deleteMethodParts.push(buildDeleteMethod("softDelete", false));
|
|
2701
|
+
if (exposeHard)
|
|
2702
|
+
deleteMethodParts.push(buildDeleteMethod("hardDelete", hasSoftDelete));
|
|
2703
|
+
const deleteMethodsCode = deleteMethodParts.join(`
|
|
2704
|
+
|
|
2705
|
+
`);
|
|
2706
|
+
const txDeleteParts = [];
|
|
2707
|
+
if (hasSoftDelete)
|
|
2708
|
+
txDeleteParts.push(` /** Build a lazy soft-DELETE descriptor for use with sdk.$transaction([...]) */
|
|
2709
|
+
` + ` $softDelete(pk: ${pkType}): TxOp<Select${Type} | null> {
|
|
2710
|
+
` + ` return { _table: "${table.name}", _op: "softDelete", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"} };
|
|
2711
|
+
` + ` }`);
|
|
2712
|
+
if (exposeHard)
|
|
2713
|
+
txDeleteParts.push(` /** Build a lazy hard-DELETE descriptor for use with sdk.$transaction([...]) */
|
|
2714
|
+
` + ` $hardDelete(pk: ${pkType}): TxOp<Select${Type} | null> {
|
|
2715
|
+
` + ` return { _table: "${table.name}", _op: "hardDelete", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"} };
|
|
2716
|
+
` + ` }`);
|
|
2717
|
+
const txDeleteMethodsCode = txDeleteParts.join(`
|
|
2718
|
+
|
|
2719
|
+
`);
|
|
2581
2720
|
return `/**
|
|
2582
2721
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
2583
2722
|
*
|
|
@@ -2587,6 +2726,7 @@ function emitClient(table, graph, opts, model) {
|
|
|
2587
2726
|
* To make changes, modify your schema or configuration and regenerate.
|
|
2588
2727
|
*/
|
|
2589
2728
|
import { BaseClient } from "./base-client${ext}";
|
|
2729
|
+
import type { TxOp } from "./base-client${ext}";
|
|
2590
2730
|
import type { Where } from "./where-types${ext}";
|
|
2591
2731
|
import type { PaginatedResponse } from "./types/shared${ext}";
|
|
2592
2732
|
${typeImports}
|
|
@@ -2668,6 +2808,78 @@ ${hasJsonbColumns ? ` /**
|
|
|
2668
2808
|
return this.post<Select${Type}>(url, data);
|
|
2669
2809
|
}`}
|
|
2670
2810
|
|
|
2811
|
+
${hasJsonbColumns ? ` /**
|
|
2812
|
+
* Upsert a ${table.name} record with field selection
|
|
2813
|
+
*/
|
|
2814
|
+
async upsert<TJsonb extends Partial<Select${Type}> = {}>(
|
|
2815
|
+
args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
|
|
2816
|
+
options: { select: string[] }
|
|
2817
|
+
): Promise<Partial<Select${Type}<TJsonb>>>;
|
|
2818
|
+
/**
|
|
2819
|
+
* Upsert a ${table.name} record with field exclusion
|
|
2820
|
+
*/
|
|
2821
|
+
async upsert<TJsonb extends Partial<Select${Type}> = {}>(
|
|
2822
|
+
args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
|
|
2823
|
+
options: { exclude: string[] }
|
|
2824
|
+
): Promise<Partial<Select${Type}<TJsonb>>>;
|
|
2825
|
+
/**
|
|
2826
|
+
* Upsert a ${table.name} record — insert if no conflict on 'where' columns, update otherwise.
|
|
2827
|
+
* @param args.where - Conflict target column(s) (must be a unique constraint)
|
|
2828
|
+
* @param args.create - Full insert data used when no conflict occurs
|
|
2829
|
+
* @param args.update - Partial data applied when a conflict occurs
|
|
2830
|
+
* @returns The resulting record
|
|
2831
|
+
*/
|
|
2832
|
+
async upsert<TJsonb extends Partial<Select${Type}> = {}>(
|
|
2833
|
+
args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
|
|
2834
|
+
options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>
|
|
2835
|
+
): Promise<Select${Type}<TJsonb>>;
|
|
2836
|
+
async upsert<TJsonb extends Partial<Select${Type}> = {}>(
|
|
2837
|
+
args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
|
|
2838
|
+
options?: { select?: string[]; exclude?: string[] }
|
|
2839
|
+
): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>> {
|
|
2840
|
+
const queryParams = new URLSearchParams();
|
|
2841
|
+
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
2842
|
+
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
2843
|
+
const query = queryParams.toString();
|
|
2844
|
+
const url = query ? \`\${this.resource}/upsert?\${query}\` : \`\${this.resource}/upsert\`;
|
|
2845
|
+
return this.post<Select${Type}<TJsonb>>(url, args);
|
|
2846
|
+
}` : ` /**
|
|
2847
|
+
* Upsert a ${table.name} record with field selection
|
|
2848
|
+
*/
|
|
2849
|
+
async upsert(
|
|
2850
|
+
args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
|
|
2851
|
+
options: { select: string[] }
|
|
2852
|
+
): Promise<Partial<Select${Type}>>;
|
|
2853
|
+
/**
|
|
2854
|
+
* Upsert a ${table.name} record with field exclusion
|
|
2855
|
+
*/
|
|
2856
|
+
async upsert(
|
|
2857
|
+
args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
|
|
2858
|
+
options: { exclude: string[] }
|
|
2859
|
+
): Promise<Partial<Select${Type}>>;
|
|
2860
|
+
/**
|
|
2861
|
+
* Upsert a ${table.name} record — insert if no conflict on 'where' columns, update otherwise.
|
|
2862
|
+
* @param args.where - Conflict target column(s) (must be a unique constraint)
|
|
2863
|
+
* @param args.create - Full insert data used when no conflict occurs
|
|
2864
|
+
* @param args.update - Partial data applied when a conflict occurs
|
|
2865
|
+
* @returns The resulting record
|
|
2866
|
+
*/
|
|
2867
|
+
async upsert(
|
|
2868
|
+
args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
|
|
2869
|
+
options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>
|
|
2870
|
+
): Promise<Select${Type}>;
|
|
2871
|
+
async upsert(
|
|
2872
|
+
args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
|
|
2873
|
+
options?: { select?: string[]; exclude?: string[] }
|
|
2874
|
+
): Promise<Select${Type} | Partial<Select${Type}>> {
|
|
2875
|
+
const queryParams = new URLSearchParams();
|
|
2876
|
+
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
2877
|
+
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
2878
|
+
const query = queryParams.toString();
|
|
2879
|
+
const url = query ? \`\${this.resource}/upsert?\${query}\` : \`\${this.resource}/upsert\`;
|
|
2880
|
+
return this.post<Select${Type}>(url, args);
|
|
2881
|
+
}`}
|
|
2882
|
+
|
|
2671
2883
|
${hasJsonbColumns ? ` /**
|
|
2672
2884
|
* Get a ${table.name} record by primary key with field selection
|
|
2673
2885
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
@@ -3008,72 +3220,24 @@ ${hasJsonbColumns ? ` /**
|
|
|
3008
3220
|
return this.patch<Select${Type} | null>(url, patch);
|
|
3009
3221
|
}`}
|
|
3010
3222
|
|
|
3011
|
-
${
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
/**
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
/**
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
* @example
|
|
3030
|
-
* // With JSONB type override:
|
|
3031
|
-
* const user = await client.delete<{ metadata: Metadata }>('user-id');
|
|
3032
|
-
*/
|
|
3033
|
-
async delete<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type}<TJsonb> | null>;
|
|
3034
|
-
async delete<TJsonb extends Partial<Select${Type}> = {}>(
|
|
3035
|
-
pk: ${pkType},
|
|
3036
|
-
options?: { select?: string[]; exclude?: string[] }
|
|
3037
|
-
): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>> | null> {
|
|
3038
|
-
const path = ${pkPathExpr};
|
|
3039
|
-
const queryParams = new URLSearchParams();
|
|
3040
|
-
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
3041
|
-
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
3042
|
-
const query = queryParams.toString();
|
|
3043
|
-
const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
|
|
3044
|
-
return this.del<Select${Type}<TJsonb> | null>(url);
|
|
3045
|
-
}` : ` /**
|
|
3046
|
-
* Delete a ${table.name} record by primary key with field selection
|
|
3047
|
-
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3048
|
-
* @param options - Select specific fields to return
|
|
3049
|
-
* @returns The deleted record with only selected fields if found, null otherwise
|
|
3050
|
-
*/
|
|
3051
|
-
async delete(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}> | null>;
|
|
3052
|
-
/**
|
|
3053
|
-
* Delete a ${table.name} record by primary key with field exclusion
|
|
3054
|
-
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3055
|
-
* @param options - Exclude specific fields from return
|
|
3056
|
-
* @returns The deleted record without excluded fields if found, null otherwise
|
|
3057
|
-
*/
|
|
3058
|
-
async delete(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}> | null>;
|
|
3059
|
-
/**
|
|
3060
|
-
* Delete a ${table.name} record by primary key
|
|
3061
|
-
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3062
|
-
* @returns The deleted record with all fields if found, null otherwise
|
|
3063
|
-
*/
|
|
3064
|
-
async delete(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type} | null>;
|
|
3065
|
-
async delete(
|
|
3066
|
-
pk: ${pkType},
|
|
3067
|
-
options?: { select?: string[]; exclude?: string[] }
|
|
3068
|
-
): Promise<Select${Type} | Partial<Select${Type}> | null> {
|
|
3069
|
-
const path = ${pkPathExpr};
|
|
3070
|
-
const queryParams = new URLSearchParams();
|
|
3071
|
-
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
3072
|
-
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
3073
|
-
const query = queryParams.toString();
|
|
3074
|
-
const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
|
|
3075
|
-
return this.del<Select${Type} | null>(url);
|
|
3076
|
-
}`}
|
|
3223
|
+
${deleteMethodsCode}
|
|
3224
|
+
|
|
3225
|
+
/** Build a lazy CREATE descriptor for use with sdk.$transaction([...]) */
|
|
3226
|
+
$create(data: Insert${Type}): TxOp<Select${Type}> {
|
|
3227
|
+
return { _table: "${table.name}", _op: "create", _data: data as Record<string, unknown> };
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
/** Build a lazy UPDATE descriptor for use with sdk.$transaction([...]) */
|
|
3231
|
+
$update(pk: ${pkType}, data: Update${Type}): TxOp<Select${Type} | null> {
|
|
3232
|
+
return { _table: "${table.name}", _op: "update", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"}, _data: data as Record<string, unknown> };
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
${txDeleteMethodsCode}
|
|
3236
|
+
|
|
3237
|
+
/** Build a lazy UPSERT descriptor for use with sdk.$transaction([...]) */
|
|
3238
|
+
$upsert(args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} }): TxOp<Select${Type}> {
|
|
3239
|
+
return { _table: "${table.name}", _op: "upsert", _data: args as Record<string, unknown> };
|
|
3240
|
+
}
|
|
3077
3241
|
${includeMethodsCode}}
|
|
3078
3242
|
`;
|
|
3079
3243
|
}
|
|
@@ -3088,7 +3252,9 @@ function emitClientIndex(tables, useJsExtensions, graph, includeOpts) {
|
|
|
3088
3252
|
* To make changes, modify your schema or configuration and regenerate.
|
|
3089
3253
|
*/
|
|
3090
3254
|
`;
|
|
3091
|
-
out += `import
|
|
3255
|
+
out += `import { BaseClient } from "./base-client${ext}";
|
|
3256
|
+
`;
|
|
3257
|
+
out += `import type { AuthConfig, TxOp } from "./base-client${ext}";
|
|
3092
3258
|
`;
|
|
3093
3259
|
for (const t of tables) {
|
|
3094
3260
|
out += `import { ${pascal(t.name)}Client } from "./${t.name}${ext}";
|
|
@@ -3104,7 +3270,7 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
3104
3270
|
`;
|
|
3105
3271
|
out += ` */
|
|
3106
3272
|
`;
|
|
3107
|
-
out += `export class SDK {
|
|
3273
|
+
out += `export class SDK extends BaseClient {
|
|
3108
3274
|
`;
|
|
3109
3275
|
for (const t of tables) {
|
|
3110
3276
|
out += ` public ${t.name}: ${pascal(t.name)}Client;
|
|
@@ -3114,17 +3280,101 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
|
|
|
3114
3280
|
constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: AuthConfig }) {
|
|
3115
3281
|
`;
|
|
3116
3282
|
out += ` const f = cfg.fetch ?? fetch;
|
|
3283
|
+
`;
|
|
3284
|
+
out += ` super(cfg.baseUrl, f, cfg.auth);
|
|
3117
3285
|
`;
|
|
3118
3286
|
for (const t of tables) {
|
|
3119
3287
|
out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
|
|
3120
3288
|
`;
|
|
3121
3289
|
}
|
|
3122
3290
|
out += ` }
|
|
3291
|
+
`;
|
|
3292
|
+
out += `
|
|
3293
|
+
`;
|
|
3294
|
+
out += ` /**
|
|
3295
|
+
`;
|
|
3296
|
+
out += ` * Execute multiple operations atomically in one PostgreSQL transaction.
|
|
3297
|
+
`;
|
|
3298
|
+
out += ` * All ops are validated before BEGIN is issued — fail-fast on bad input.
|
|
3299
|
+
`;
|
|
3300
|
+
out += ` *
|
|
3301
|
+
`;
|
|
3302
|
+
out += ` * @example
|
|
3303
|
+
`;
|
|
3304
|
+
out += ` * const [order, user] = await sdk.$transaction([
|
|
3305
|
+
`;
|
|
3306
|
+
out += ` * sdk.orders.$create({ user_id: 1, total: 99 }),
|
|
3307
|
+
`;
|
|
3308
|
+
out += ` * sdk.users.$update('1', { last_order_at: new Date().toISOString() }),
|
|
3309
|
+
`;
|
|
3310
|
+
out += ` * ]);
|
|
3311
|
+
`;
|
|
3312
|
+
out += ` */
|
|
3313
|
+
`;
|
|
3314
|
+
out += ` async $transaction<const T extends readonly TxOp<unknown>[]>(
|
|
3315
|
+
`;
|
|
3316
|
+
out += ` ops: [...T]
|
|
3317
|
+
`;
|
|
3318
|
+
out += ` ): Promise<{ [K in keyof T]: T[K] extends TxOp<infer R> ? R : never }> {
|
|
3319
|
+
`;
|
|
3320
|
+
out += ` const payload = ops.map(op => ({
|
|
3321
|
+
`;
|
|
3322
|
+
out += ` op: op._op,
|
|
3323
|
+
`;
|
|
3324
|
+
out += ` table: op._table,
|
|
3325
|
+
`;
|
|
3326
|
+
out += ` ...(op._data !== undefined ? { data: op._data } : {}),
|
|
3327
|
+
`;
|
|
3328
|
+
out += ` ...(op._pk !== undefined ? { pk: op._pk } : {}),
|
|
3329
|
+
`;
|
|
3330
|
+
out += ` }));
|
|
3331
|
+
`;
|
|
3332
|
+
out += `
|
|
3333
|
+
`;
|
|
3334
|
+
out += ` const res = await this.fetchFn(\`\${this.baseUrl}/v1/transaction\`, {
|
|
3335
|
+
`;
|
|
3336
|
+
out += ` method: "POST",
|
|
3337
|
+
`;
|
|
3338
|
+
out += ` headers: await this.headers(true),
|
|
3339
|
+
`;
|
|
3340
|
+
out += ` body: JSON.stringify({ ops: payload }),
|
|
3341
|
+
`;
|
|
3342
|
+
out += ` });
|
|
3343
|
+
`;
|
|
3344
|
+
out += `
|
|
3345
|
+
`;
|
|
3346
|
+
out += ` if (!res.ok) {
|
|
3347
|
+
`;
|
|
3348
|
+
out += ` let errBody: Record<string, unknown> = {};
|
|
3349
|
+
`;
|
|
3350
|
+
out += ` try { errBody = await res.json() as Record<string, unknown>; } catch {}
|
|
3351
|
+
`;
|
|
3352
|
+
out += ` const err = Object.assign(
|
|
3353
|
+
`;
|
|
3354
|
+
out += ` new Error((errBody.error as string | undefined) ?? \`$transaction failed: \${res.status}\`),
|
|
3355
|
+
`;
|
|
3356
|
+
out += ` { failedAt: errBody.failedAt as number | undefined, issues: errBody.issues }
|
|
3357
|
+
`;
|
|
3358
|
+
out += ` );
|
|
3359
|
+
`;
|
|
3360
|
+
out += ` throw err;
|
|
3361
|
+
`;
|
|
3362
|
+
out += ` }
|
|
3363
|
+
`;
|
|
3364
|
+
out += `
|
|
3365
|
+
`;
|
|
3366
|
+
out += ` const json = await res.json() as { results: unknown[] };
|
|
3367
|
+
`;
|
|
3368
|
+
out += ` return json.results as unknown as { [K in keyof T]: T[K] extends TxOp<infer R> ? R : never };
|
|
3369
|
+
`;
|
|
3370
|
+
out += ` }
|
|
3123
3371
|
`;
|
|
3124
3372
|
out += `}
|
|
3125
3373
|
|
|
3126
3374
|
`;
|
|
3127
3375
|
out += `export { BaseClient } from "./base-client${ext}";
|
|
3376
|
+
`;
|
|
3377
|
+
out += `export type { TxOp } from "./base-client${ext}";
|
|
3128
3378
|
`;
|
|
3129
3379
|
out += `
|
|
3130
3380
|
// Include specification types for custom queries
|
|
@@ -3325,15 +3575,29 @@ export abstract class BaseClient {
|
|
|
3325
3575
|
method: "DELETE",
|
|
3326
3576
|
headers: await this.headers(),
|
|
3327
3577
|
});
|
|
3328
|
-
|
|
3578
|
+
|
|
3329
3579
|
if (res.status === 404) {
|
|
3330
3580
|
return null as T;
|
|
3331
3581
|
}
|
|
3332
|
-
|
|
3582
|
+
|
|
3333
3583
|
await this.okOrThrow(res, "DELETE", path);
|
|
3334
3584
|
return (await res.json()) as T;
|
|
3335
3585
|
}
|
|
3336
3586
|
}
|
|
3587
|
+
|
|
3588
|
+
/**
|
|
3589
|
+
* Lazy operation descriptor returned by $create/$update/$softDelete/$hardDelete/$upsert.
|
|
3590
|
+
* \`__resultType\` is a phantom field — never assigned at runtime, exists only
|
|
3591
|
+
* so TypeScript can infer the correct tuple element type inside \`$transaction\`.
|
|
3592
|
+
*/
|
|
3593
|
+
export type TxOp<T = unknown> = {
|
|
3594
|
+
readonly _table: string;
|
|
3595
|
+
readonly _op: "create" | "update" | "softDelete" | "hardDelete" | "upsert";
|
|
3596
|
+
readonly _data?: Record<string, unknown>;
|
|
3597
|
+
readonly _pk?: string | Record<string, unknown>;
|
|
3598
|
+
/** @internal */
|
|
3599
|
+
readonly __resultType?: T;
|
|
3600
|
+
};
|
|
3337
3601
|
`;
|
|
3338
3602
|
}
|
|
3339
3603
|
|
|
@@ -3877,14 +4141,14 @@ function tsTypeFor(pgType, opts, enums) {
|
|
|
3877
4141
|
return "number[]";
|
|
3878
4142
|
return "string";
|
|
3879
4143
|
}
|
|
3880
|
-
function
|
|
4144
|
+
function isJsonbType2(pgType) {
|
|
3881
4145
|
const t = pgType.toLowerCase();
|
|
3882
4146
|
return t === "json" || t === "jsonb";
|
|
3883
4147
|
}
|
|
3884
4148
|
var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
|
3885
4149
|
function emitTypes(table, opts, enums) {
|
|
3886
4150
|
const Type = pascal2(table.name);
|
|
3887
|
-
const hasJsonbColumns = table.columns.some((col) =>
|
|
4151
|
+
const hasJsonbColumns = table.columns.some((col) => isJsonbType2(col.pgType));
|
|
3888
4152
|
const insertFields = table.columns.map((col) => {
|
|
3889
4153
|
const base = tsTypeFor(col.pgType, opts, enums);
|
|
3890
4154
|
const optional = col.hasDefault || col.nullable ? "?" : "";
|
|
@@ -4335,9 +4599,12 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
|
4335
4599
|
|
|
4336
4600
|
// src/emit-router-hono.ts
|
|
4337
4601
|
init_utils();
|
|
4338
|
-
function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
|
|
4602
|
+
function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken, opts) {
|
|
4339
4603
|
const tableNames = tables.map((t) => t.name).sort();
|
|
4340
4604
|
const ext = useJsExtensions ? ".js" : "";
|
|
4605
|
+
const apiPathPrefix = opts?.apiPathPrefix ?? "/v1";
|
|
4606
|
+
const softDeleteCols = opts?.softDeleteCols ?? {};
|
|
4607
|
+
const includeMethodsDepth = opts?.includeMethodsDepth ?? 2;
|
|
4341
4608
|
let resolvedPullToken;
|
|
4342
4609
|
let pullTokenEnvVar;
|
|
4343
4610
|
if (pullToken) {
|
|
@@ -4363,6 +4630,93 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
|
|
|
4363
4630
|
const Type = pascal(name);
|
|
4364
4631
|
return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
|
|
4365
4632
|
}).join(`
|
|
4633
|
+
`);
|
|
4634
|
+
const txSchemaImports = tableNames.map((name) => {
|
|
4635
|
+
const Type = pascal(name);
|
|
4636
|
+
return `import { Insert${Type}Schema, Update${Type}Schema } from "./zod/${name}${ext}";`;
|
|
4637
|
+
}).join(`
|
|
4638
|
+
`);
|
|
4639
|
+
function txRouteBlock(appVar) {
|
|
4640
|
+
const authLine = hasAuth ? ` ${appVar}.use(\`${apiPathPrefix}/transaction\`, authMiddleware);
|
|
4641
|
+
` : "";
|
|
4642
|
+
return `${authLine} ${appVar}.post(\`${apiPathPrefix}/transaction\`, async (c) => {
|
|
4643
|
+
const body = await c.req.json().catch(() => ({}));
|
|
4644
|
+
const rawOps: unknown[] = Array.isArray(body.ops) ? body.ops : [];
|
|
4645
|
+
|
|
4646
|
+
if (rawOps.length === 0) {
|
|
4647
|
+
return c.json({ error: "ops must be a non-empty array" }, 400);
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
// Validate all ops against their table schemas BEFORE opening a transaction
|
|
4651
|
+
const validatedOps: coreOps.TransactionOperation[] = [];
|
|
4652
|
+
for (let i = 0; i < rawOps.length; i++) {
|
|
4653
|
+
const item = rawOps[i] as any;
|
|
4654
|
+
const entry = TABLE_TX_METADATA[item?.table as string];
|
|
4655
|
+
if (!entry) {
|
|
4656
|
+
return c.json({ error: \`Unknown table "\${item?.table}" at index \${i}\`, failedAt: i }, 400);
|
|
4657
|
+
}
|
|
4658
|
+
if (item.op === "create") {
|
|
4659
|
+
const parsed = entry.insertSchema.safeParse(item.data ?? {});
|
|
4660
|
+
if (!parsed.success) {
|
|
4661
|
+
return c.json({ error: "Validation failed", issues: parsed.error.flatten(), failedAt: i }, 400);
|
|
4662
|
+
}
|
|
4663
|
+
validatedOps.push({ op: "create", table: item.table, data: parsed.data });
|
|
4664
|
+
} else if (item.op === "update") {
|
|
4665
|
+
const parsed = entry.updateSchema.safeParse(item.data ?? {});
|
|
4666
|
+
if (!parsed.success) {
|
|
4667
|
+
return c.json({ error: "Validation failed", issues: parsed.error.flatten(), failedAt: i }, 400);
|
|
4668
|
+
}
|
|
4669
|
+
if (item.pk == null) {
|
|
4670
|
+
return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
|
|
4671
|
+
}
|
|
4672
|
+
validatedOps.push({ op: "update", table: item.table, pk: item.pk, data: parsed.data });
|
|
4673
|
+
} else if (item.op === "softDelete") {
|
|
4674
|
+
if (item.pk == null) {
|
|
4675
|
+
return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
|
|
4676
|
+
}
|
|
4677
|
+
validatedOps.push({ op: "softDelete", table: item.table, pk: item.pk });
|
|
4678
|
+
} else if (item.op === "hardDelete") {
|
|
4679
|
+
if (item.pk == null) {
|
|
4680
|
+
return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
|
|
4681
|
+
}
|
|
4682
|
+
validatedOps.push({ op: "hardDelete", table: item.table, pk: item.pk });
|
|
4683
|
+
} else {
|
|
4684
|
+
return c.json({ error: \`Unknown op "\${item?.op}" at index \${i}\`, failedAt: i }, 400);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
const onBegin = deps.onRequest
|
|
4689
|
+
? (txClient: typeof deps.pg) => deps.onRequest!(c, txClient)
|
|
4690
|
+
: undefined;
|
|
4691
|
+
|
|
4692
|
+
const result = await coreOps.executeTransaction(deps.pg, validatedOps, TABLE_TX_METADATA, onBegin);
|
|
4693
|
+
|
|
4694
|
+
if (!result.ok) {
|
|
4695
|
+
return c.json({ error: result.error, failedAt: result.failedAt }, 400);
|
|
4696
|
+
}
|
|
4697
|
+
return c.json({ results: result.results.map(r => r.data) }, 200);
|
|
4698
|
+
});`;
|
|
4699
|
+
}
|
|
4700
|
+
const txMetadataEntries = tables.slice().sort((a, b) => a.name.localeCompare(b.name)).map((t) => {
|
|
4701
|
+
const rawPk = t.pk;
|
|
4702
|
+
const pkCols = Array.isArray(rawPk) ? rawPk : rawPk ? [rawPk] : ["id"];
|
|
4703
|
+
const softDel = softDeleteCols[t.name] ?? null;
|
|
4704
|
+
const allCols = t.columns.map((c) => `"${c.name}"`).join(", ");
|
|
4705
|
+
const vecCols = t.columns.filter((c) => isVectorType(c.pgType)).map((c) => `"${c.name}"`).join(", ");
|
|
4706
|
+
const jsonCols = t.columns.filter((c) => isJsonbType(c.pgType)).map((c) => `"${c.name}"`).join(", ");
|
|
4707
|
+
const Type = pascal(t.name);
|
|
4708
|
+
return ` "${t.name}": {
|
|
4709
|
+
table: "${t.name}",
|
|
4710
|
+
pkColumns: ${JSON.stringify(pkCols)},
|
|
4711
|
+
softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
|
|
4712
|
+
allColumnNames: [${allCols}],
|
|
4713
|
+
vectorColumns: [${vecCols}],
|
|
4714
|
+
jsonbColumns: [${jsonCols}],
|
|
4715
|
+
includeMethodsDepth: ${includeMethodsDepth},
|
|
4716
|
+
insertSchema: Insert${Type}Schema,
|
|
4717
|
+
updateSchema: Update${Type}Schema,
|
|
4718
|
+
}`;
|
|
4719
|
+
}).join(`,
|
|
4366
4720
|
`);
|
|
4367
4721
|
return `/**
|
|
4368
4722
|
* AUTO-GENERATED FILE - DO NOT EDIT
|
|
@@ -4376,8 +4730,27 @@ import { Hono } from "hono";
|
|
|
4376
4730
|
import type { Context } from "hono";
|
|
4377
4731
|
import { SDK_MANIFEST } from "./sdk-bundle${ext}";
|
|
4378
4732
|
import { getContract } from "./contract${ext}";
|
|
4733
|
+
import * as coreOps from "./core/operations${ext}";
|
|
4734
|
+
${txSchemaImports}
|
|
4379
4735
|
${imports}
|
|
4380
|
-
${hasAuth ? `
|
|
4736
|
+
${hasAuth ? `import { authMiddleware } from "./auth${ext}";
|
|
4737
|
+
export { authMiddleware };` : ""}
|
|
4738
|
+
|
|
4739
|
+
/** Discriminated result from safeParse — mirrors Zod's actual return shape */
|
|
4740
|
+
type SchemaParseResult =
|
|
4741
|
+
| { success: true; data: Record<string, unknown> }
|
|
4742
|
+
| { success: false; error: { flatten: () => unknown } };
|
|
4743
|
+
|
|
4744
|
+
/** Registry entry — core metadata + Zod schemas for request validation */
|
|
4745
|
+
interface TxTableRegistry extends coreOps.TransactionTableMetadata {
|
|
4746
|
+
insertSchema: { safeParse: (v: unknown) => SchemaParseResult };
|
|
4747
|
+
updateSchema: { safeParse: (v: unknown) => SchemaParseResult };
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
// Registry used by POST /v1/transaction — maps table name to metadata + Zod schemas
|
|
4751
|
+
const TABLE_TX_METADATA: Record<string, TxTableRegistry> = {
|
|
4752
|
+
${txMetadataEntries}
|
|
4753
|
+
};
|
|
4381
4754
|
|
|
4382
4755
|
/**
|
|
4383
4756
|
* Creates a Hono router with all generated routes that can be mounted into your existing app.
|
|
@@ -4447,7 +4820,7 @@ ${pullToken ? `
|
|
|
4447
4820
|
if (!expectedToken) {
|
|
4448
4821
|
// Token not configured in environment - reject request
|
|
4449
4822
|
return c.json({
|
|
4450
|
-
error: "SDK endpoints are protected but pullToken environment variable not set. ${pullTokenEnvVar ? `Set ${pullTokenEnvVar} in your environment or remove pullToken from config.` : "
|
|
4823
|
+
error: "SDK endpoints are protected but pullToken environment variable not set. ${pullTokenEnvVar ? `Set ${pullTokenEnvVar} in your environment, or remove pullToken from config.` : "Remove pullToken from config or set the expected environment variable."}"
|
|
4451
4824
|
}, 500);
|
|
4452
4825
|
}
|
|
4453
4826
|
|
|
@@ -4464,6 +4837,9 @@ ${pullToken ? `
|
|
|
4464
4837
|
await next();
|
|
4465
4838
|
});
|
|
4466
4839
|
` : ""}
|
|
4840
|
+
// Transaction endpoint — executes multiple operations atomically
|
|
4841
|
+
${txRouteBlock("router")}
|
|
4842
|
+
|
|
4467
4843
|
// SDK distribution endpoints
|
|
4468
4844
|
router.get("/_psdk/sdk/manifest", (c) => {
|
|
4469
4845
|
return c.json({
|
|
@@ -4547,6 +4923,7 @@ export function registerAllRoutes(
|
|
|
4547
4923
|
}
|
|
4548
4924
|
) {
|
|
4549
4925
|
${registrations.replace(/router/g, "app")}
|
|
4926
|
+
${txRouteBlock("app")}
|
|
4550
4927
|
}
|
|
4551
4928
|
|
|
4552
4929
|
// Individual route registrations (for selective use)
|
|
@@ -4734,6 +5111,96 @@ export async function createRecord(
|
|
|
4734
5111
|
}
|
|
4735
5112
|
}
|
|
4736
5113
|
|
|
5114
|
+
/**
|
|
5115
|
+
* UPSERT operation - Insert or update a record based on a conflict target
|
|
5116
|
+
*/
|
|
5117
|
+
export async function upsertRecord(
|
|
5118
|
+
ctx: OperationContext,
|
|
5119
|
+
args: {
|
|
5120
|
+
where: Record<string, any>; // conflict target columns (keys only matter)
|
|
5121
|
+
create: Record<string, any>; // full insert data
|
|
5122
|
+
update: Record<string, any>; // update data on conflict
|
|
5123
|
+
}
|
|
5124
|
+
): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
|
|
5125
|
+
try {
|
|
5126
|
+
const { where, create: createData, update: updateData } = args;
|
|
5127
|
+
|
|
5128
|
+
const conflictCols = Object.keys(where);
|
|
5129
|
+
if (!conflictCols.length) {
|
|
5130
|
+
return { error: "where must specify at least one column", status: 400 };
|
|
5131
|
+
}
|
|
5132
|
+
|
|
5133
|
+
const insertCols = Object.keys(createData);
|
|
5134
|
+
const insertVals = Object.values(createData);
|
|
5135
|
+
if (!insertCols.length) {
|
|
5136
|
+
return { error: "No fields provided in create", status: 400 };
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
// Filter PK columns from update (mirrors updateRecord behaviour)
|
|
5140
|
+
const filteredUpdate = Object.fromEntries(
|
|
5141
|
+
Object.entries(updateData).filter(([k]) => !ctx.pkColumns.includes(k))
|
|
5142
|
+
);
|
|
5143
|
+
if (!Object.keys(filteredUpdate).length) {
|
|
5144
|
+
return { error: "update must include at least one non-PK field", status: 400 };
|
|
5145
|
+
}
|
|
5146
|
+
|
|
5147
|
+
// Serialise JSONB/vector values for create (same pattern as createRecord)
|
|
5148
|
+
const preparedInsertVals = insertVals.map((v, i) =>
|
|
5149
|
+
v !== null && v !== undefined && typeof v === 'object' &&
|
|
5150
|
+
(ctx.jsonbColumns?.includes(insertCols[i]!) || ctx.vectorColumns?.includes(insertCols[i]!))
|
|
5151
|
+
? JSON.stringify(v) : v
|
|
5152
|
+
);
|
|
5153
|
+
|
|
5154
|
+
// Serialise JSONB/vector values for update
|
|
5155
|
+
const updateCols = Object.keys(filteredUpdate);
|
|
5156
|
+
const updateVals = Object.values(filteredUpdate);
|
|
5157
|
+
const preparedUpdateVals = updateVals.map((v, i) =>
|
|
5158
|
+
v !== null && v !== undefined && typeof v === 'object' &&
|
|
5159
|
+
(ctx.jsonbColumns?.includes(updateCols[i]!) || ctx.vectorColumns?.includes(updateCols[i]!))
|
|
5160
|
+
? JSON.stringify(v) : v
|
|
5161
|
+
);
|
|
5162
|
+
|
|
5163
|
+
const placeholders = insertCols.map((_, i) => \`$\${i + 1}\`).join(', ');
|
|
5164
|
+
const conflictSQL = conflictCols.map(c => \`"\${c}"\`).join(', ');
|
|
5165
|
+
const setSql = updateCols.map((k, i) => \`"\${k}" = $\${insertCols.length + i + 1}\`).join(', ');
|
|
5166
|
+
const returningClause = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
5167
|
+
|
|
5168
|
+
const text = \`INSERT INTO "\${ctx.table}" (\${insertCols.map(c => \`"\${c}"\`).join(', ')})
|
|
5169
|
+
VALUES (\${placeholders})
|
|
5170
|
+
ON CONFLICT (\${conflictSQL}) DO UPDATE SET \${setSql}
|
|
5171
|
+
RETURNING \${returningClause}\`;
|
|
5172
|
+
const params = [...preparedInsertVals, ...preparedUpdateVals];
|
|
5173
|
+
|
|
5174
|
+
log.debug("UPSERT SQL:", text, "params:", params);
|
|
5175
|
+
const { rows } = await ctx.pg.query(text, params);
|
|
5176
|
+
const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
|
|
5177
|
+
|
|
5178
|
+
if (!parsedRows[0]) {
|
|
5179
|
+
return { data: null, status: 500 };
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
return { data: parsedRows[0], status: 200 };
|
|
5183
|
+
} catch (e: any) {
|
|
5184
|
+
const errorMsg = e?.message ?? "";
|
|
5185
|
+
const isJsonError = errorMsg.includes("invalid input syntax for type json");
|
|
5186
|
+
if (isJsonError) {
|
|
5187
|
+
log.error(\`UPSERT \${ctx.table} - Invalid JSON input detected!\`);
|
|
5188
|
+
log.error("Input args that caused error:", JSON.stringify(args, null, 2));
|
|
5189
|
+
log.error("Filtered update data (sent to DB):", JSON.stringify(Object.fromEntries(
|
|
5190
|
+
Object.entries(args.update).filter(([k]) => !ctx.pkColumns.includes(k))
|
|
5191
|
+
), null, 2));
|
|
5192
|
+
log.error("PostgreSQL error:", errorMsg);
|
|
5193
|
+
} else {
|
|
5194
|
+
log.error(\`UPSERT \${ctx.table} error:\`, e?.stack ?? e);
|
|
5195
|
+
}
|
|
5196
|
+
return {
|
|
5197
|
+
error: e?.message ?? "Internal error",
|
|
5198
|
+
...(DEBUG ? { stack: e?.stack } : {}),
|
|
5199
|
+
status: 500
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
|
|
4737
5204
|
/**
|
|
4738
5205
|
* READ operation - Get a record by primary key
|
|
4739
5206
|
*/
|
|
@@ -5410,7 +5877,8 @@ export async function updateRecord(
|
|
|
5410
5877
|
*/
|
|
5411
5878
|
export async function deleteRecord(
|
|
5412
5879
|
ctx: OperationContext,
|
|
5413
|
-
pkValues: any[]
|
|
5880
|
+
pkValues: any[],
|
|
5881
|
+
opts?: { hard?: boolean }
|
|
5414
5882
|
): Promise<{ data?: any; error?: string; status: number }> {
|
|
5415
5883
|
try {
|
|
5416
5884
|
const hasCompositePk = ctx.pkColumns.length > 1;
|
|
@@ -5419,11 +5887,12 @@ export async function deleteRecord(
|
|
|
5419
5887
|
: \`"\${ctx.pkColumns[0]}" = $1\`;
|
|
5420
5888
|
|
|
5421
5889
|
const returningClause = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
5422
|
-
const
|
|
5890
|
+
const doSoftDelete = ctx.softDeleteColumn && !opts?.hard;
|
|
5891
|
+
const text = doSoftDelete
|
|
5423
5892
|
? \`UPDATE "\${ctx.table}" SET "\${ctx.softDeleteColumn}" = NOW() WHERE \${wherePkSql} RETURNING \${returningClause}\`
|
|
5424
5893
|
: \`DELETE FROM "\${ctx.table}" WHERE \${wherePkSql} RETURNING \${returningClause}\`;
|
|
5425
5894
|
|
|
5426
|
-
log.debug(\`DELETE \${
|
|
5895
|
+
log.debug(\`DELETE \${doSoftDelete ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
|
|
5427
5896
|
const { rows } = await ctx.pg.query(text, pkValues);
|
|
5428
5897
|
const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
|
|
5429
5898
|
|
|
@@ -5434,12 +5903,126 @@ export async function deleteRecord(
|
|
|
5434
5903
|
return { data: parsedRows[0], status: 200 };
|
|
5435
5904
|
} catch (e: any) {
|
|
5436
5905
|
log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
|
|
5437
|
-
return {
|
|
5438
|
-
error: e?.message ?? "Internal error",
|
|
5906
|
+
return {
|
|
5907
|
+
error: e?.message ?? "Internal error",
|
|
5439
5908
|
...(DEBUG ? { stack: e?.stack } : {}),
|
|
5440
|
-
status: 500
|
|
5909
|
+
status: 500
|
|
5441
5910
|
};
|
|
5442
5911
|
}
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
/**
|
|
5915
|
+
* Static metadata for a table used during transaction execution.
|
|
5916
|
+
* Mirrors the fields needed to construct an OperationContext (minus the pg client,
|
|
5917
|
+
* which is injected at transaction time).
|
|
5918
|
+
*/
|
|
5919
|
+
export interface TransactionTableMetadata {
|
|
5920
|
+
table: string;
|
|
5921
|
+
pkColumns: string[];
|
|
5922
|
+
softDeleteColumn: string | null;
|
|
5923
|
+
allColumnNames: string[];
|
|
5924
|
+
vectorColumns: string[];
|
|
5925
|
+
jsonbColumns: string[];
|
|
5926
|
+
includeMethodsDepth: number;
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
export type TransactionOperation =
|
|
5930
|
+
| { op: "create"; table: string; data: Record<string, unknown> }
|
|
5931
|
+
| { op: "update"; table: string; pk: string | Record<string, unknown>; data: Record<string, unknown> }
|
|
5932
|
+
| { op: "softDelete"; table: string; pk: string | Record<string, unknown> }
|
|
5933
|
+
| { op: "hardDelete"; table: string; pk: string | Record<string, unknown> }
|
|
5934
|
+
| { op: "upsert"; table: string; data: { where: Record<string, unknown>; create: Record<string, unknown>; update: Record<string, unknown> } };
|
|
5935
|
+
|
|
5936
|
+
/**
|
|
5937
|
+
* Executes a list of operations atomically inside a single PostgreSQL transaction.
|
|
5938
|
+
*
|
|
5939
|
+
* - When \`pg\` has a \`.connect()\` method (Pool), acquires a dedicated connection.
|
|
5940
|
+
* - Otherwise uses \`pg\` directly (already a single connected Client).
|
|
5941
|
+
* - Calls \`onBegin(txClient)\` after BEGIN and before any operations (for SET LOCAL etc.).
|
|
5942
|
+
* - Any status >= 400 or thrown error triggers ROLLBACK.
|
|
5943
|
+
* - \`failedAt: -1\` indicates an unexpected exception (e.g. connectivity failure).
|
|
5944
|
+
*/
|
|
5945
|
+
export async function executeTransaction(
|
|
5946
|
+
pg: DatabaseClient & { connect?: () => Promise<DatabaseClient & { release?: () => void }> },
|
|
5947
|
+
ops: TransactionOperation[],
|
|
5948
|
+
metadata: Record<string, TransactionTableMetadata>,
|
|
5949
|
+
onBegin?: (txClient: DatabaseClient) => Promise<void>
|
|
5950
|
+
): Promise<
|
|
5951
|
+
| { ok: true; results: Array<{ data: unknown }> }
|
|
5952
|
+
| { ok: false; error: string; failedAt: number }
|
|
5953
|
+
> {
|
|
5954
|
+
// Fail-fast: validate all table names before touching the DB
|
|
5955
|
+
for (let i = 0; i < ops.length; i++) {
|
|
5956
|
+
if (!metadata[ops[i]!.table]) {
|
|
5957
|
+
return { ok: false, error: \`Unknown table "\${ops[i]!.table}"\`, failedAt: i };
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
// Pool gives a dedicated connection; plain Client is used directly
|
|
5962
|
+
const isPool = typeof (pg as any).connect === "function";
|
|
5963
|
+
const txClient: DatabaseClient & { release?: () => void } = isPool
|
|
5964
|
+
? await (pg as any).connect()
|
|
5965
|
+
: pg;
|
|
5966
|
+
|
|
5967
|
+
try {
|
|
5968
|
+
await txClient.query("BEGIN");
|
|
5969
|
+
if (onBegin) await onBegin(txClient);
|
|
5970
|
+
|
|
5971
|
+
const results: Array<{ data: unknown }> = [];
|
|
5972
|
+
|
|
5973
|
+
for (let i = 0; i < ops.length; i++) {
|
|
5974
|
+
const op = ops[i]!;
|
|
5975
|
+
const meta = metadata[op.table]!;
|
|
5976
|
+
const ctx: OperationContext = {
|
|
5977
|
+
pg: txClient,
|
|
5978
|
+
table: meta.table,
|
|
5979
|
+
pkColumns: meta.pkColumns,
|
|
5980
|
+
softDeleteColumn: meta.softDeleteColumn,
|
|
5981
|
+
allColumnNames: meta.allColumnNames,
|
|
5982
|
+
vectorColumns: meta.vectorColumns,
|
|
5983
|
+
jsonbColumns: meta.jsonbColumns,
|
|
5984
|
+
includeMethodsDepth: meta.includeMethodsDepth,
|
|
5985
|
+
};
|
|
5986
|
+
|
|
5987
|
+
let result: { data?: unknown; error?: string; status: number };
|
|
5988
|
+
|
|
5989
|
+
if (op.op === "create") {
|
|
5990
|
+
result = await createRecord(ctx, op.data);
|
|
5991
|
+
} else if (op.op === "upsert") {
|
|
5992
|
+
result = await upsertRecord(ctx, op.data);
|
|
5993
|
+
} else {
|
|
5994
|
+
const pkValues = Array.isArray(op.pk)
|
|
5995
|
+
? op.pk
|
|
5996
|
+
: typeof op.pk === "object" && op.pk !== null
|
|
5997
|
+
? meta.pkColumns.map(c => (op.pk as Record<string, unknown>)[c])
|
|
5998
|
+
: [op.pk];
|
|
5999
|
+
result = op.op === "update"
|
|
6000
|
+
? await updateRecord(ctx, pkValues as string[], op.data)
|
|
6001
|
+
: op.op === "hardDelete"
|
|
6002
|
+
? await deleteRecord(ctx, pkValues as string[], { hard: true })
|
|
6003
|
+
: await deleteRecord(ctx, pkValues as string[]);
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
if (result.status >= 400) {
|
|
6007
|
+
try { await txClient.query("ROLLBACK"); } catch {}
|
|
6008
|
+
txClient.release?.();
|
|
6009
|
+
return {
|
|
6010
|
+
ok: false,
|
|
6011
|
+
error: result.error ?? \`Op \${i} returned status \${result.status}\`,
|
|
6012
|
+
failedAt: i,
|
|
6013
|
+
};
|
|
6014
|
+
}
|
|
6015
|
+
results.push({ data: result.data ?? null });
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
await txClient.query("COMMIT");
|
|
6019
|
+
txClient.release?.();
|
|
6020
|
+
return { ok: true, results };
|
|
6021
|
+
} catch (e: unknown) {
|
|
6022
|
+
try { await txClient.query("ROLLBACK"); } catch {}
|
|
6023
|
+
txClient.release?.();
|
|
6024
|
+
return { ok: false, error: (e instanceof Error ? e.message : String(e)) ?? "Transaction error", failedAt: -1 };
|
|
6025
|
+
}
|
|
5443
6026
|
}`;
|
|
5444
6027
|
}
|
|
5445
6028
|
|
|
@@ -5871,7 +6454,7 @@ function generateForeignKeySetup(table, model, clientPath) {
|
|
|
5871
6454
|
// Clean up parent ${foreignTableName} record
|
|
5872
6455
|
if (${foreignTableName}Id) {
|
|
5873
6456
|
try {
|
|
5874
|
-
await sdk.${foreignTableName}.
|
|
6457
|
+
await sdk.${foreignTableName}.hardDelete(${foreignTableName}Id);
|
|
5875
6458
|
} catch (e) {
|
|
5876
6459
|
// Parent might already be deleted due to cascading
|
|
5877
6460
|
}
|
|
@@ -5888,7 +6471,7 @@ function generateForeignKeySetup(table, model, clientPath) {
|
|
|
5888
6471
|
// Clean up parent ${foreignTableName} record
|
|
5889
6472
|
if (${foreignTableName}Key) {
|
|
5890
6473
|
try {
|
|
5891
|
-
await sdk.${foreignTableName}.
|
|
6474
|
+
await sdk.${foreignTableName}.hardDelete(${foreignTableName}Key);
|
|
5892
6475
|
} catch (e) {
|
|
5893
6476
|
// Parent might already be deleted due to cascading
|
|
5894
6477
|
}
|
|
@@ -6153,19 +6736,34 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
|
6153
6736
|
console.warn('No ID from create test, skipping update test');
|
|
6154
6737
|
return;
|
|
6155
6738
|
}
|
|
6156
|
-
|
|
6739
|
+
|
|
6157
6740
|
const updateData: Update${Type} = ${updateData};
|
|
6158
6741
|
const updated = await sdk.${table.name}.update(createdId, updateData);
|
|
6159
6742
|
expect(updated).toBeDefined();
|
|
6743
|
+
});
|
|
6744
|
+
|
|
6745
|
+
it('should upsert (update path) ${table.name}', async () => {
|
|
6746
|
+
if (!createdId) {
|
|
6747
|
+
console.warn('No ID from create test, skipping upsert test');
|
|
6748
|
+
return;
|
|
6749
|
+
}
|
|
6750
|
+
// where: use PK as conflict target; create includes PK so conflict is guaranteed
|
|
6751
|
+
const result = await sdk.${table.name}.upsert({
|
|
6752
|
+
where: { ${table.pk[0]}: createdId },
|
|
6753
|
+
create: { ${table.pk[0]}: createdId, ...${sampleData} },
|
|
6754
|
+
update: ${updateData},
|
|
6755
|
+
});
|
|
6756
|
+
expect(result).toBeDefined();
|
|
6757
|
+
expect(result.${table.pk[0]}).toBe(createdId);
|
|
6160
6758
|
});` : ""}
|
|
6161
|
-
|
|
6759
|
+
|
|
6162
6760
|
it('should delete ${table.name}', async () => {
|
|
6163
6761
|
if (!createdId) {
|
|
6164
6762
|
console.warn('No ID from create test, skipping delete test');
|
|
6165
6763
|
return;
|
|
6166
6764
|
}
|
|
6167
6765
|
|
|
6168
|
-
const deleted = await sdk.${table.name}.
|
|
6766
|
+
const deleted = await sdk.${table.name}.hardDelete(createdId);
|
|
6169
6767
|
expect(deleted).toBeDefined();
|
|
6170
6768
|
|
|
6171
6769
|
// Verify deletion
|
|
@@ -6182,10 +6780,16 @@ var __filename2 = fileURLToPath(import.meta.url);
|
|
|
6182
6780
|
var __dirname2 = dirname2(__filename2);
|
|
6183
6781
|
var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
|
|
6184
6782
|
function resolveSoftDeleteColumn(cfg, tableName) {
|
|
6185
|
-
const
|
|
6783
|
+
const del = cfg.delete;
|
|
6784
|
+
if (!del)
|
|
6785
|
+
return null;
|
|
6786
|
+
const overrides = del.softDeleteColumnOverrides;
|
|
6186
6787
|
if (overrides && tableName in overrides)
|
|
6187
6788
|
return overrides[tableName] ?? null;
|
|
6188
|
-
return
|
|
6789
|
+
return del.softDeleteColumn ?? null;
|
|
6790
|
+
}
|
|
6791
|
+
function resolveExposeHardDelete(cfg) {
|
|
6792
|
+
return cfg.delete?.exposeHardDelete ?? true;
|
|
6189
6793
|
}
|
|
6190
6794
|
async function generate(configPath, options) {
|
|
6191
6795
|
if (!existsSync2(configPath)) {
|
|
@@ -6275,6 +6879,7 @@ async function generate(configPath, options) {
|
|
|
6275
6879
|
path: join2(serverDir, "core", "operations.ts"),
|
|
6276
6880
|
content: emitCoreOperations()
|
|
6277
6881
|
});
|
|
6882
|
+
const exposeHardDelete = resolveExposeHardDelete(cfg);
|
|
6278
6883
|
if (process.env.SDK_DEBUG) {
|
|
6279
6884
|
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
|
6280
6885
|
}
|
|
@@ -6292,6 +6897,7 @@ async function generate(configPath, options) {
|
|
|
6292
6897
|
if (serverFramework === "hono") {
|
|
6293
6898
|
routeContent = emitHonoRoutes(table, graph, {
|
|
6294
6899
|
softDeleteColumn: softDeleteCols[table.name] ?? null,
|
|
6900
|
+
exposeHardDelete,
|
|
6295
6901
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
6296
6902
|
authStrategy: getAuthStrategy(normalizedAuth),
|
|
6297
6903
|
useJsExtensions: cfg.useJsExtensions,
|
|
@@ -6307,6 +6913,8 @@ async function generate(configPath, options) {
|
|
|
6307
6913
|
files.push({
|
|
6308
6914
|
path: join2(clientDir, `${table.name}.ts`),
|
|
6309
6915
|
content: emitClient(table, graph, {
|
|
6916
|
+
softDeleteColumn: softDeleteCols[table.name] ?? null,
|
|
6917
|
+
exposeHardDelete,
|
|
6310
6918
|
useJsExtensions: cfg.useJsExtensionsClient,
|
|
6311
6919
|
includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
|
|
6312
6920
|
skipJunctionTables: cfg.skipJunctionTables ?? true
|
|
@@ -6320,7 +6928,11 @@ async function generate(configPath, options) {
|
|
|
6320
6928
|
if (serverFramework === "hono") {
|
|
6321
6929
|
files.push({
|
|
6322
6930
|
path: join2(serverDir, "router.ts"),
|
|
6323
|
-
content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken
|
|
6931
|
+
content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken, {
|
|
6932
|
+
apiPathPrefix: cfg.apiPathPrefix || "/v1",
|
|
6933
|
+
softDeleteCols: Object.fromEntries(Object.values(model.tables).map((t) => [t.name, resolveSoftDeleteColumn(cfg, t.name)])),
|
|
6934
|
+
includeMethodsDepth: cfg.includeMethodsDepth ?? 2
|
|
6935
|
+
})
|
|
6324
6936
|
});
|
|
6325
6937
|
}
|
|
6326
6938
|
const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
|
|
@@ -6448,5 +7060,6 @@ async function generate(configPath, options) {
|
|
|
6448
7060
|
}
|
|
6449
7061
|
export {
|
|
6450
7062
|
resolveSoftDeleteColumn,
|
|
7063
|
+
resolveExposeHardDelete,
|
|
6451
7064
|
generate
|
|
6452
7065
|
};
|