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/dist/cli.js CHANGED
@@ -492,6 +492,14 @@ var require_config = __commonJS(() => {
492
492
  import { mkdir, writeFile, readFile, readdir, unlink } from "fs/promises";
493
493
  import { dirname, join } from "path";
494
494
  import { existsSync } from "fs";
495
+ function isVectorType(pgType) {
496
+ const t = pgType.toLowerCase();
497
+ return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
498
+ }
499
+ function isJsonbType(pgType) {
500
+ const t = pgType.toLowerCase();
501
+ return t === "json" || t === "jsonb";
502
+ }
495
503
  async function writeFilesIfChanged(files) {
496
504
  let written = 0;
497
505
  let unchanged = 0;
@@ -890,9 +898,10 @@ function generateResourceWithSDK(table, model, graph, config) {
890
898
  const hasSinglePK = table.pk.length === 1;
891
899
  const pkField = hasSinglePK ? table.pk[0] : "id";
892
900
  const enums = model.enums || {};
893
- const overrides = config?.softDeleteColumnOverrides;
894
- const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.softDeleteColumn ?? null;
901
+ const overrides = config?.delete?.softDeleteColumnOverrides;
902
+ const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.delete?.softDeleteColumn ?? null;
895
903
  const softDeleteCol = resolvedSoftDeleteCol && table.columns.some((c) => c.name === resolvedSoftDeleteCol) ? resolvedSoftDeleteCol : null;
904
+ const exposeHardDelete = config?.delete?.exposeHardDelete ?? true;
896
905
  const sdkMethods = [];
897
906
  const endpoints = [];
898
907
  sdkMethods.push({
@@ -968,16 +977,47 @@ const withDeleted = await sdk.${tableName}.getByPk('id', { includeSoftDeleted: t
968
977
  }
969
978
  if (hasSinglePK) {
970
979
  sdkMethods.push({
971
- name: "delete",
972
- signature: `delete(${pkField}: string): Promise<${Type}>`,
973
- description: `Delete a ${tableName}`,
974
- example: `const deleted = await sdk.${tableName}.delete('id');`,
975
- correspondsTo: `DELETE ${basePath}/:${pkField}`
980
+ name: "upsert",
981
+ signature: `upsert(args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} }): Promise<${Type}>`,
982
+ 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.`,
983
+ example: `const result = await sdk.${tableName}.upsert({
984
+ where: { ${pkField}: 'some-id' },
985
+ create: { ${generateExampleFields(table, "create")} },
986
+ update: { ${generateExampleFields(table, "update")} },
987
+ });`,
988
+ correspondsTo: `POST ${basePath}/upsert`
989
+ });
990
+ endpoints.push({
991
+ method: "POST",
992
+ path: `${basePath}/upsert`,
993
+ description: `Upsert ${tableName} — insert if no conflict on 'where' columns, update otherwise`,
994
+ requestBody: `{ where: Update${Type}; create: Insert${Type}; update: Update${Type} }`,
995
+ responseBody: `${Type}`
976
996
  });
997
+ }
998
+ if (hasSinglePK) {
999
+ if (softDeleteCol) {
1000
+ sdkMethods.push({
1001
+ name: "softDelete",
1002
+ signature: `softDelete(${pkField}: string): Promise<${Type}>`,
1003
+ description: `Soft-delete a ${tableName} (sets ${softDeleteCol})`,
1004
+ example: `const deleted = await sdk.${tableName}.softDelete('id');`,
1005
+ correspondsTo: `DELETE ${basePath}/:${pkField}`
1006
+ });
1007
+ }
1008
+ if (!softDeleteCol || exposeHardDelete) {
1009
+ sdkMethods.push({
1010
+ name: "hardDelete",
1011
+ signature: `hardDelete(${pkField}: string): Promise<${Type}>`,
1012
+ description: `Permanently delete a ${tableName}`,
1013
+ example: `const deleted = await sdk.${tableName}.hardDelete('id');`,
1014
+ correspondsTo: softDeleteCol ? `DELETE ${basePath}/:${pkField}?hard=true` : `DELETE ${basePath}/:${pkField}`
1015
+ });
1016
+ }
977
1017
  endpoints.push({
978
1018
  method: "DELETE",
979
1019
  path: `${basePath}/:${pkField}`,
980
- description: `Delete ${tableName}`,
1020
+ description: softDeleteCol ? `Delete ${tableName} (soft-delete by default${exposeHardDelete ? "; add ?hard=true for permanent deletion" : ""})` : `Delete ${tableName}`,
981
1021
  responseBody: `${Type}`
982
1022
  });
983
1023
  }
@@ -1475,7 +1515,7 @@ function extractConfigFields(configContent) {
1475
1515
  if (connectionMatch) {
1476
1516
  fields.push({
1477
1517
  key: "connectionString",
1478
- value: connectionMatch[1]?.trim(),
1518
+ value: connectionMatch[1]?.trim().replace(/\s*\/\/.*$/, ""),
1479
1519
  description: "PostgreSQL connection string",
1480
1520
  isRequired: true,
1481
1521
  isCommented: false
@@ -1495,7 +1535,7 @@ function extractConfigFields(configContent) {
1495
1535
  const outClientMatch = configContent.match(/^\s*(\/\/)?\s*outClient:\s*"(.+)"/m);
1496
1536
  if (outDirMatch) {
1497
1537
  let value = outDirMatch[2]?.trim() || "";
1498
- value = value.replace(/,\s*$/, "");
1538
+ value = value.replace(/\s*\/\/.*$/, "").replace(/,\s*$/, "");
1499
1539
  fields.push({
1500
1540
  key: "outDir",
1501
1541
  value,
@@ -1513,13 +1553,13 @@ function extractConfigFields(configContent) {
1513
1553
  isCommented
1514
1554
  });
1515
1555
  }
1516
- const softDeleteMatch = configContent.match(/^\s*(\/\/)?\s*softDeleteColumn:\s*(.+),?$/m);
1517
- if (softDeleteMatch) {
1556
+ const deleteBlock = extractComplexBlock(configContent, "delete");
1557
+ if (deleteBlock) {
1518
1558
  fields.push({
1519
- key: "softDeleteColumn",
1520
- value: softDeleteMatch[2]?.trim().replace(/,$/, "").replace(/["']/g, ""),
1521
- description: "Column name for soft deletes",
1522
- isCommented: !!softDeleteMatch[1]
1559
+ key: "delete",
1560
+ value: deleteBlock.content,
1561
+ description: "Delete configuration (soft/hard delete behavior)",
1562
+ isCommented: deleteBlock.isCommented
1523
1563
  });
1524
1564
  }
1525
1565
  const depthMatch = configContent.match(/^\s*(\/\/)?\s*(includeMethodsDepth|includeDepthLimit):\s*(\d+)/m);
@@ -1594,11 +1634,11 @@ function extractConfigFields(configContent) {
1594
1634
  isCommented: pullBlock.isCommented
1595
1635
  });
1596
1636
  }
1597
- const pullTokenMatch = configContent.match(/^\s*(\/\/)?\s*pullToken:\s*(.+),?$/m);
1637
+ const pullTokenMatch = configContent.match(/^[ \t]{0,3}(\/\/)?\s*pullToken:\s*(.+),?$/m);
1598
1638
  if (pullTokenMatch) {
1599
1639
  fields.push({
1600
1640
  key: "pullToken",
1601
- value: pullTokenMatch[2]?.trim().replace(/,$/, ""),
1641
+ value: pullTokenMatch[2]?.trim().replace(/\s*\/\/.*$/, "").replace(/,$/, ""),
1602
1642
  description: "Token for protecting /_psdk/* endpoints",
1603
1643
  isCommented: !!pullTokenMatch[1]
1604
1644
  });
@@ -1701,12 +1741,12 @@ export default {
1701
1741
  // ========== ADVANCED OPTIONS ==========
1702
1742
 
1703
1743
  /**
1704
- * Column name for soft deletes. When set, DELETE operations will update
1705
- * this column instead of removing rows.
1706
- * @default null (hard deletes)
1707
- * @example "deleted_at"
1744
+ * Delete configuration (soft/hard delete behavior).
1745
+ * When softDeleteColumn is set, DELETE operations update that column instead of removing rows.
1746
+ * Set exposeHardDelete: false to prevent permanent deletion via the API.
1747
+ * @default undefined (hard deletes only)
1708
1748
  */
1709
- ${getFieldLine("softDeleteColumn", existingFields, mergeStrategy, "null", userChoices)}
1749
+ ${getComplexBlockLine("delete", existingFields, mergeStrategy, userChoices)}
1710
1750
 
1711
1751
  /**
1712
1752
  * How to type numeric columns in TypeScript
@@ -1795,6 +1835,12 @@ export default {
1795
1835
  `;
1796
1836
  return template;
1797
1837
  }
1838
+ function wrapValue(value) {
1839
+ if (typeof value === "string" && !value.startsWith('"') && !value.startsWith("{") && !value.startsWith("[")) {
1840
+ return `"${value}"`;
1841
+ }
1842
+ return String(value);
1843
+ }
1798
1844
  function getFieldValue(key, existingFields, mergeStrategy, userChoices) {
1799
1845
  const existing = existingFields.find((f) => f.key === key);
1800
1846
  if (mergeStrategy === "keep-existing" && existing && !existing.isCommented) {
@@ -1821,31 +1867,29 @@ function getFieldLine(key, existingFields, mergeStrategy, defaultValue, userChoi
1821
1867
  const shouldUseExisting = mergeStrategy === "keep-existing" && existing && !existing.isCommented || mergeStrategy === "interactive" && userChoices?.get(key) === "keep" && existing && !existing.isCommented;
1822
1868
  const shouldUseNew = mergeStrategy === "use-defaults" || mergeStrategy === "interactive" && userChoices?.get(key) === "new";
1823
1869
  if (shouldUseExisting && existing) {
1824
- let value = existing.value;
1825
- if (typeof value === "string" && !value.startsWith('"') && !value.startsWith("{") && !value.startsWith("[")) {
1826
- value = `"${value}"`;
1827
- }
1828
- return `${key}: ${value},`;
1870
+ return `${key}: ${wrapValue(existing.value)},`;
1829
1871
  }
1830
1872
  if (shouldUseNew) {
1831
1873
  return `${key}: ${defaultValue},`;
1832
1874
  }
1833
- return `// ${key}: ${defaultValue},`;
1875
+ return `// ${key}: ${existing ? wrapValue(existing.value) : defaultValue},`;
1834
1876
  }
1835
1877
  function getComplexBlockLine(key, existingFields, mergeStrategy, userChoices) {
1836
1878
  const existing = existingFields.find((f) => f.key === key);
1837
1879
  const shouldUseExisting = mergeStrategy === "keep-existing" && existing && !existing.isCommented || mergeStrategy === "interactive" && userChoices?.get(key) === "keep" && existing && !existing.isCommented;
1838
- const shouldUseNew = mergeStrategy === "use-defaults" || mergeStrategy === "interactive" && userChoices?.get(key) === "new";
1839
1880
  if (shouldUseExisting && existing) {
1840
1881
  return `${key}: ${existing.value},`;
1841
1882
  }
1842
- if (shouldUseNew) {
1843
- return getDefaultComplexBlock(key);
1844
- }
1845
1883
  return getDefaultComplexBlock(key);
1846
1884
  }
1847
1885
  function getDefaultComplexBlock(key) {
1848
1886
  switch (key) {
1887
+ case "delete":
1888
+ return `// delete: {
1889
+ // softDeleteColumn: "deleted_at",
1890
+ // exposeHardDelete: true, // default: true
1891
+ // // softDeleteColumnOverrides: { audit_logs: null }
1892
+ // },`;
1849
1893
  case "tests":
1850
1894
  return `// tests: {
1851
1895
  // generate: true,
@@ -1973,30 +2017,6 @@ async function initCommand(args) {
1973
2017
  }
1974
2018
  userChoices.set(field.key, choice);
1975
2019
  }
1976
- const newOptions = [
1977
- { key: "tests", description: "Enable test generation" },
1978
- { key: "auth", description: "Add authentication" },
1979
- { key: "pullToken", description: "Add SDK endpoint protection" },
1980
- { key: "pull", description: "Configure SDK distribution" }
1981
- ];
1982
- const existingKeys = new Set(existingFields.map((f) => f.key));
1983
- const missingOptions = newOptions.filter((opt) => !existingKeys.has(opt.key));
1984
- if (missingOptions.length > 0) {
1985
- console.log(`
1986
- \uD83D\uDCE6 New configuration options available:
1987
- `);
1988
- for (const option of missingOptions) {
1989
- const { addOption } = await prompts2({
1990
- type: "confirm",
1991
- name: "addOption",
1992
- message: `Add ${option.description} configuration? (commented out by default)`,
1993
- initial: false
1994
- });
1995
- if (addOption) {
1996
- userChoices.set(option.key, "add-commented");
1997
- }
1998
- }
1999
- }
2000
2020
  }
2001
2021
  const backupPath = configPath + ".backup." + Date.now();
2002
2022
  copyFileSync(configPath, backupPath);
@@ -2137,13 +2157,14 @@ export default {
2137
2157
  // ========== ADVANCED OPTIONS ==========
2138
2158
 
2139
2159
  /**
2140
- * Column name for soft deletes. When set, DELETE operations will update
2141
- * this column instead of removing rows.
2142
- *
2143
- * Default: null (hard deletes)
2144
- * Example: "deleted_at"
2160
+ * Delete configuration (soft/hard delete behavior).
2161
+ * When softDeleteColumn is set, DELETE operations update that column instead of removing rows.
2162
+ * Set exposeHardDelete: false to prevent permanent deletion via the API.
2145
2163
  */
2146
- // softDeleteColumn: null,
2164
+ // delete: {
2165
+ // softDeleteColumn: "deleted_at",
2166
+ // exposeHardDelete: true,
2167
+ // },
2147
2168
 
2148
2169
  /**
2149
2170
  * How to type numeric columns in TypeScript
@@ -2858,6 +2879,18 @@ ${insertFields}
2858
2879
  });
2859
2880
 
2860
2881
  export const Update${Type}Schema = Insert${Type}Schema.partial();
2882
+
2883
+ export const Upsert${Type}Schema = z.object({
2884
+ where: Update${Type}Schema.refine(
2885
+ (d) => Object.keys(d).length > 0,
2886
+ { message: "where must specify at least one column" }
2887
+ ),
2888
+ create: Insert${Type}Schema,
2889
+ update: Update${Type}Schema.refine(
2890
+ (d) => Object.keys(d).length > 0,
2891
+ { message: "update must specify at least one column" }
2892
+ ),
2893
+ });
2861
2894
  `;
2862
2895
  }
2863
2896
 
@@ -2951,13 +2984,6 @@ export interface PaginatedResponse<T> {
2951
2984
 
2952
2985
  // src/emit-routes-hono.ts
2953
2986
  init_utils();
2954
- function isVectorType(pgType) {
2955
- const t = pgType.toLowerCase();
2956
- return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
2957
- }
2958
- function isJsonbType(pgType) {
2959
- return pgType.toLowerCase() === "json" || pgType.toLowerCase() === "jsonb";
2960
- }
2961
2987
  function emitHonoRoutes(table, _graph, opts) {
2962
2988
  const fileTableName = table.name;
2963
2989
  const Type = pascal(table.name);
@@ -2971,6 +2997,7 @@ function emitHonoRoutes(table, _graph, opts) {
2971
2997
  const hasCompositePk = safePkCols.length > 1;
2972
2998
  const pkPath = hasCompositePk ? safePkCols.map((c) => `:${c}`).join("/") : `:${safePkCols[0]}`;
2973
2999
  const softDel = opts.softDeleteColumn && table.columns.some((c) => c.name === opts.softDeleteColumn) ? opts.softDeleteColumn : null;
3000
+ const exposeHard = !!(softDel && opts.exposeHardDelete);
2974
3001
  const getPkParams = hasCompositePk ? `const pkValues = [${safePkCols.map((c) => `c.req.param("${c}")`).join(", ")}];` : `const pkValues = [c.req.param("${safePkCols[0]}")];`;
2975
3002
  const hasAuth = opts.authStrategy && opts.authStrategy !== "none";
2976
3003
  const ext = opts.useJsExtensions ? ".js" : "";
@@ -2987,7 +3014,7 @@ function emitHonoRoutes(table, _graph, opts) {
2987
3014
  import { Hono } from "hono";
2988
3015
  import type { Context } from "hono";
2989
3016
  import { z } from "zod";
2990
- import { Insert${Type}Schema, Update${Type}Schema } from "../zod/${fileTableName}${ext}";
3017
+ import { Insert${Type}Schema, Update${Type}Schema, Upsert${Type}Schema } from "../zod/${fileTableName}${ext}";
2991
3018
  import { loadIncludes } from "../include-loader${ext}";
2992
3019
  import * as coreOps from "../core/operations${ext}";
2993
3020
  ${authImport}
@@ -2996,10 +3023,12 @@ const columnEnum = z.enum([${columnNames}]);
2996
3023
 
2997
3024
  const createSchema = Insert${Type}Schema;
2998
3025
  const updateSchema = Update${Type}Schema;
3026
+ const upsertSchema = Upsert${Type}Schema;
2999
3027
 
3000
3028
  const deleteSchema = z.object({
3001
3029
  select: z.array(z.string()).min(1).optional(),
3002
- exclude: z.array(z.string()).min(1).optional()
3030
+ exclude: z.array(z.string()).min(1).optional(),${exposeHard ? `
3031
+ hard: z.boolean().optional(),` : ""}
3003
3032
  }).strict().refine(
3004
3033
  (data) => !(data.select && data.exclude),
3005
3034
  { message: "Cannot specify both 'select' and 'exclude' parameters" }
@@ -3222,6 +3251,39 @@ ${hasAuth ? `
3222
3251
  }, result.status as any);
3223
3252
  });
3224
3253
 
3254
+ // UPSERT
3255
+ app.post(\`\${base}/upsert\`, async (c) => {
3256
+ const body = await c.req.json().catch(() => ({}));
3257
+ const parsed = upsertSchema.safeParse(body);
3258
+
3259
+ if (!parsed.success) {
3260
+ const issues = parsed.error.flatten();
3261
+ return c.json({ error: "Invalid body", issues }, 400);
3262
+ }
3263
+
3264
+ const selectParam = c.req.query("select");
3265
+ const excludeParam = c.req.query("exclude");
3266
+ const select = selectParam ? selectParam.split(",") : undefined;
3267
+ const exclude = excludeParam ? excludeParam.split(",") : undefined;
3268
+
3269
+ if (select && exclude) {
3270
+ return c.json({ error: "Cannot specify both 'select' and 'exclude' parameters" }, 400);
3271
+ }
3272
+
3273
+ if (deps.onRequest) {
3274
+ await deps.onRequest(c, deps.pg);
3275
+ }
3276
+
3277
+ const ctx = { ...baseCtx, select, exclude };
3278
+ const result = await coreOps.upsertRecord(ctx, parsed.data);
3279
+
3280
+ if (result.error) {
3281
+ return c.json({ error: result.error }, result.status as any);
3282
+ }
3283
+
3284
+ return c.json(result.data, result.status as any);
3285
+ });
3286
+
3225
3287
  // UPDATE
3226
3288
  app.patch(\`\${base}/${pkPath}\`, async (c) => {
3227
3289
  ${getPkParams}
@@ -3261,13 +3323,15 @@ ${hasAuth ? `
3261
3323
  app.delete(\`\${base}/${pkPath}\`, async (c) => {
3262
3324
  ${getPkParams}
3263
3325
 
3264
- // Parse query params for select/exclude
3326
+ // Parse query params for select/exclude${exposeHard ? " and hard" : ""}
3265
3327
  const selectParam = c.req.query("select");
3266
3328
  const excludeParam = c.req.query("exclude");
3267
3329
  const queryData: any = {};
3268
3330
  if (selectParam) queryData.select = selectParam.split(",");
3269
3331
  if (excludeParam) queryData.exclude = excludeParam.split(",");
3270
-
3332
+ ${exposeHard ? ` const hardParam = c.req.query("hard");
3333
+ if (hardParam !== undefined) queryData.hard = hardParam === "true";
3334
+ ` : ""}
3271
3335
  const queryParsed = deleteSchema.safeParse(queryData);
3272
3336
  if (!queryParsed.success) {
3273
3337
  const issues = queryParsed.error.flatten();
@@ -3279,7 +3343,7 @@ ${hasAuth ? `
3279
3343
  }
3280
3344
 
3281
3345
  const ctx = { ...baseCtx, select: queryParsed.data.select, exclude: queryParsed.data.exclude };
3282
- const result = await coreOps.deleteRecord(ctx, pkValues);
3346
+ const result = await coreOps.deleteRecord(ctx, pkValues${exposeHard ? ", { hard: queryParsed.data.hard }" : ""});
3283
3347
 
3284
3348
  if (result.error) {
3285
3349
  return c.json({ error: result.error }, result.status as any);
@@ -3294,14 +3358,6 @@ ${hasAuth ? `
3294
3358
  // src/emit-client.ts
3295
3359
  init_utils();
3296
3360
  init_emit_include_methods();
3297
- function isVectorType2(pgType) {
3298
- const t = pgType.toLowerCase();
3299
- return t === "vector" || t === "halfvec" || t === "sparsevec" || t === "bit";
3300
- }
3301
- function isJsonbType2(pgType) {
3302
- const t = pgType.toLowerCase();
3303
- return t === "json" || t === "jsonb";
3304
- }
3305
3361
  function toIncludeParamName(relationKey) {
3306
3362
  return `${relationKey}Include`;
3307
3363
  }
@@ -3324,10 +3380,12 @@ function emitClient(table, graph, opts, model) {
3324
3380
  const Type = pascal(table.name);
3325
3381
  const ext = opts.useJsExtensions ? ".js" : "";
3326
3382
  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 }`;
3327
- const hasVectorColumns = table.columns.some((c) => isVectorType2(c.pgType));
3328
- const hasJsonbColumns = table.columns.some((c) => isJsonbType2(c.pgType));
3383
+ const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
3384
+ const hasJsonbColumns = table.columns.some((c) => isJsonbType(c.pgType));
3385
+ const hasSoftDelete = !!opts.softDeleteColumn;
3386
+ const exposeHard = !hasSoftDelete || (opts.exposeHardDelete ?? true);
3329
3387
  if (process.env.SDK_DEBUG) {
3330
- const vectorCols = table.columns.filter((c) => isVectorType2(c.pgType));
3388
+ const vectorCols = table.columns.filter((c) => isVectorType(c.pgType));
3331
3389
  if (vectorCols.length > 0) {
3332
3390
  console.log(`[DEBUG] Table ${table.name}: Found ${vectorCols.length} vector columns:`, vectorCols.map((c) => `${c.name} (${c.pgType})`));
3333
3391
  }
@@ -3557,6 +3615,68 @@ function emitClient(table, graph, opts, model) {
3557
3615
  `;
3558
3616
  }
3559
3617
  }
3618
+ const buildDeleteMethod = (methodName, setHardTrue) => {
3619
+ const actionName = methodName === "softDelete" ? "Soft-delete" : "Hard-delete (permanently remove)";
3620
+ const hardLine = setHardTrue ? `
3621
+ queryParams.set('hard', 'true');` : "";
3622
+ const pksLabel = hasCompositePk ? "s" : "";
3623
+ const G = hasJsonbColumns ? `<TJsonb extends Partial<Select${Type}> = {}>` : "";
3624
+ const RBase = hasJsonbColumns ? `Select${Type}<TJsonb>` : `Select${Type}`;
3625
+ const RPart = hasJsonbColumns ? `Partial<Select${Type}<TJsonb>>` : `Partial<Select${Type}>`;
3626
+ return ` /**
3627
+ ` + ` * ${actionName} a ${table.name} record by primary key with field selection
3628
+ ` + ` * @param pk - The primary key value${pksLabel}
3629
+ ` + ` * @param options - Select specific fields to return
3630
+ ` + ` * @returns The deleted record with only selected fields if found, null otherwise
3631
+ ` + ` */
3632
+ ` + ` async ${methodName}${G}(pk: ${pkType}, options: { select: string[] }): Promise<${RPart} | null>;
3633
+ ` + ` /**
3634
+ ` + ` * ${actionName} a ${table.name} record by primary key with field exclusion
3635
+ ` + ` * @param pk - The primary key value${pksLabel}
3636
+ ` + ` * @param options - Exclude specific fields from return
3637
+ ` + ` * @returns The deleted record without excluded fields if found, null otherwise
3638
+ ` + ` */
3639
+ ` + ` async ${methodName}${G}(pk: ${pkType}, options: { exclude: string[] }): Promise<${RPart} | null>;
3640
+ ` + ` /**
3641
+ ` + ` * ${actionName} a ${table.name} record by primary key
3642
+ ` + ` * @param pk - The primary key value${pksLabel}
3643
+ ` + ` * @returns The deleted record with all fields if found, null otherwise
3644
+ ` + ` */
3645
+ ` + ` async ${methodName}${G}(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<${RBase} | null>;
3646
+ ` + ` async ${methodName}${G}(
3647
+ ` + ` pk: ${pkType},
3648
+ ` + ` options?: { select?: string[]; exclude?: string[] }
3649
+ ` + ` ): Promise<${RBase} | ${RPart} | null> {
3650
+ ` + ` const path = ${pkPathExpr};
3651
+ ` + ` const queryParams = new URLSearchParams();${hardLine}
3652
+ ` + ` if (options?.select) queryParams.set('select', options.select.join(','));
3653
+ ` + ` if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
3654
+ ` + ` const query = queryParams.toString();
3655
+ ` + " const url = query ? `${this.resource}/${path}?${query}` : `${this.resource}/${path}`;\n" + ` return this.del<${RBase} | null>(url);
3656
+ ` + ` }`;
3657
+ };
3658
+ const deleteMethodParts = [];
3659
+ if (hasSoftDelete)
3660
+ deleteMethodParts.push(buildDeleteMethod("softDelete", false));
3661
+ if (exposeHard)
3662
+ deleteMethodParts.push(buildDeleteMethod("hardDelete", hasSoftDelete));
3663
+ const deleteMethodsCode = deleteMethodParts.join(`
3664
+
3665
+ `);
3666
+ const txDeleteParts = [];
3667
+ if (hasSoftDelete)
3668
+ txDeleteParts.push(` /** Build a lazy soft-DELETE descriptor for use with sdk.$transaction([...]) */
3669
+ ` + ` $softDelete(pk: ${pkType}): TxOp<Select${Type} | null> {
3670
+ ` + ` return { _table: "${table.name}", _op: "softDelete", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"} };
3671
+ ` + ` }`);
3672
+ if (exposeHard)
3673
+ txDeleteParts.push(` /** Build a lazy hard-DELETE descriptor for use with sdk.$transaction([...]) */
3674
+ ` + ` $hardDelete(pk: ${pkType}): TxOp<Select${Type} | null> {
3675
+ ` + ` return { _table: "${table.name}", _op: "hardDelete", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"} };
3676
+ ` + ` }`);
3677
+ const txDeleteMethodsCode = txDeleteParts.join(`
3678
+
3679
+ `);
3560
3680
  return `/**
3561
3681
  * AUTO-GENERATED FILE - DO NOT EDIT
3562
3682
  *
@@ -3566,6 +3686,7 @@ function emitClient(table, graph, opts, model) {
3566
3686
  * To make changes, modify your schema or configuration and regenerate.
3567
3687
  */
3568
3688
  import { BaseClient } from "./base-client${ext}";
3689
+ import type { TxOp } from "./base-client${ext}";
3569
3690
  import type { Where } from "./where-types${ext}";
3570
3691
  import type { PaginatedResponse } from "./types/shared${ext}";
3571
3692
  ${typeImports}
@@ -3647,6 +3768,78 @@ ${hasJsonbColumns ? ` /**
3647
3768
  return this.post<Select${Type}>(url, data);
3648
3769
  }`}
3649
3770
 
3771
+ ${hasJsonbColumns ? ` /**
3772
+ * Upsert a ${table.name} record with field selection
3773
+ */
3774
+ async upsert<TJsonb extends Partial<Select${Type}> = {}>(
3775
+ args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
3776
+ options: { select: string[] }
3777
+ ): Promise<Partial<Select${Type}<TJsonb>>>;
3778
+ /**
3779
+ * Upsert a ${table.name} record with field exclusion
3780
+ */
3781
+ async upsert<TJsonb extends Partial<Select${Type}> = {}>(
3782
+ args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
3783
+ options: { exclude: string[] }
3784
+ ): Promise<Partial<Select${Type}<TJsonb>>>;
3785
+ /**
3786
+ * Upsert a ${table.name} record — insert if no conflict on 'where' columns, update otherwise.
3787
+ * @param args.where - Conflict target column(s) (must be a unique constraint)
3788
+ * @param args.create - Full insert data used when no conflict occurs
3789
+ * @param args.update - Partial data applied when a conflict occurs
3790
+ * @returns The resulting record
3791
+ */
3792
+ async upsert<TJsonb extends Partial<Select${Type}> = {}>(
3793
+ args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
3794
+ options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>
3795
+ ): Promise<Select${Type}<TJsonb>>;
3796
+ async upsert<TJsonb extends Partial<Select${Type}> = {}>(
3797
+ args: { where: Update${Type}<TJsonb>; create: NoInfer<Insert${Type}<TJsonb>>; update: NoInfer<Update${Type}<TJsonb>> },
3798
+ options?: { select?: string[]; exclude?: string[] }
3799
+ ): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>> {
3800
+ const queryParams = new URLSearchParams();
3801
+ if (options?.select) queryParams.set('select', options.select.join(','));
3802
+ if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
3803
+ const query = queryParams.toString();
3804
+ const url = query ? \`\${this.resource}/upsert?\${query}\` : \`\${this.resource}/upsert\`;
3805
+ return this.post<Select${Type}<TJsonb>>(url, args);
3806
+ }` : ` /**
3807
+ * Upsert a ${table.name} record with field selection
3808
+ */
3809
+ async upsert(
3810
+ args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
3811
+ options: { select: string[] }
3812
+ ): Promise<Partial<Select${Type}>>;
3813
+ /**
3814
+ * Upsert a ${table.name} record with field exclusion
3815
+ */
3816
+ async upsert(
3817
+ args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
3818
+ options: { exclude: string[] }
3819
+ ): Promise<Partial<Select${Type}>>;
3820
+ /**
3821
+ * Upsert a ${table.name} record — insert if no conflict on 'where' columns, update otherwise.
3822
+ * @param args.where - Conflict target column(s) (must be a unique constraint)
3823
+ * @param args.create - Full insert data used when no conflict occurs
3824
+ * @param args.update - Partial data applied when a conflict occurs
3825
+ * @returns The resulting record
3826
+ */
3827
+ async upsert(
3828
+ args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
3829
+ options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>
3830
+ ): Promise<Select${Type}>;
3831
+ async upsert(
3832
+ args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} },
3833
+ options?: { select?: string[]; exclude?: string[] }
3834
+ ): Promise<Select${Type} | Partial<Select${Type}>> {
3835
+ const queryParams = new URLSearchParams();
3836
+ if (options?.select) queryParams.set('select', options.select.join(','));
3837
+ if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
3838
+ const query = queryParams.toString();
3839
+ const url = query ? \`\${this.resource}/upsert?\${query}\` : \`\${this.resource}/upsert\`;
3840
+ return this.post<Select${Type}>(url, args);
3841
+ }`}
3842
+
3650
3843
  ${hasJsonbColumns ? ` /**
3651
3844
  * Get a ${table.name} record by primary key with field selection
3652
3845
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
@@ -3987,72 +4180,24 @@ ${hasJsonbColumns ? ` /**
3987
4180
  return this.patch<Select${Type} | null>(url, patch);
3988
4181
  }`}
3989
4182
 
3990
- ${hasJsonbColumns ? ` /**
3991
- * Delete a ${table.name} record by primary key with field selection
3992
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3993
- * @param options - Select specific fields to return
3994
- * @returns The deleted record with only selected fields if found, null otherwise
3995
- */
3996
- async delete<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
3997
- /**
3998
- * Delete a ${table.name} record by primary key with field exclusion
3999
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
4000
- * @param options - Exclude specific fields from return
4001
- * @returns The deleted record without excluded fields if found, null otherwise
4002
- */
4003
- async delete<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
4004
- /**
4005
- * Delete a ${table.name} record by primary key
4006
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
4007
- * @returns The deleted record with all fields if found, null otherwise
4008
- * @example
4009
- * // With JSONB type override:
4010
- * const user = await client.delete<{ metadata: Metadata }>('user-id');
4011
- */
4012
- async delete<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type}<TJsonb> | null>;
4013
- async delete<TJsonb extends Partial<Select${Type}> = {}>(
4014
- pk: ${pkType},
4015
- options?: { select?: string[]; exclude?: string[] }
4016
- ): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>> | null> {
4017
- const path = ${pkPathExpr};
4018
- const queryParams = new URLSearchParams();
4019
- if (options?.select) queryParams.set('select', options.select.join(','));
4020
- if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
4021
- const query = queryParams.toString();
4022
- const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
4023
- return this.del<Select${Type}<TJsonb> | null>(url);
4024
- }` : ` /**
4025
- * Delete a ${table.name} record by primary key with field selection
4026
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
4027
- * @param options - Select specific fields to return
4028
- * @returns The deleted record with only selected fields if found, null otherwise
4029
- */
4030
- async delete(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}> | null>;
4031
- /**
4032
- * Delete a ${table.name} record by primary key with field exclusion
4033
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
4034
- * @param options - Exclude specific fields from return
4035
- * @returns The deleted record without excluded fields if found, null otherwise
4036
- */
4037
- async delete(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}> | null>;
4038
- /**
4039
- * Delete a ${table.name} record by primary key
4040
- * @param pk - The primary key value${hasCompositePk ? "s" : ""}
4041
- * @returns The deleted record with all fields if found, null otherwise
4042
- */
4043
- async delete(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type} | null>;
4044
- async delete(
4045
- pk: ${pkType},
4046
- options?: { select?: string[]; exclude?: string[] }
4047
- ): Promise<Select${Type} | Partial<Select${Type}> | null> {
4048
- const path = ${pkPathExpr};
4049
- const queryParams = new URLSearchParams();
4050
- if (options?.select) queryParams.set('select', options.select.join(','));
4051
- if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
4052
- const query = queryParams.toString();
4053
- const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
4054
- return this.del<Select${Type} | null>(url);
4055
- }`}
4183
+ ${deleteMethodsCode}
4184
+
4185
+ /** Build a lazy CREATE descriptor for use with sdk.$transaction([...]) */
4186
+ $create(data: Insert${Type}): TxOp<Select${Type}> {
4187
+ return { _table: "${table.name}", _op: "create", _data: data as Record<string, unknown> };
4188
+ }
4189
+
4190
+ /** Build a lazy UPDATE descriptor for use with sdk.$transaction([...]) */
4191
+ $update(pk: ${pkType}, data: Update${Type}): TxOp<Select${Type} | null> {
4192
+ return { _table: "${table.name}", _op: "update", _pk: ${hasCompositePk ? "pk as Record<string, unknown>" : "pk"}, _data: data as Record<string, unknown> };
4193
+ }
4194
+
4195
+ ${txDeleteMethodsCode}
4196
+
4197
+ /** Build a lazy UPSERT descriptor for use with sdk.$transaction([...]) */
4198
+ $upsert(args: { where: Update${Type}; create: Insert${Type}; update: Update${Type} }): TxOp<Select${Type}> {
4199
+ return { _table: "${table.name}", _op: "upsert", _data: args as Record<string, unknown> };
4200
+ }
4056
4201
  ${includeMethodsCode}}
4057
4202
  `;
4058
4203
  }
@@ -4067,7 +4212,9 @@ function emitClientIndex(tables, useJsExtensions, graph, includeOpts) {
4067
4212
  * To make changes, modify your schema or configuration and regenerate.
4068
4213
  */
4069
4214
  `;
4070
- out += `import type { AuthConfig } from "./base-client${ext}";
4215
+ out += `import { BaseClient } from "./base-client${ext}";
4216
+ `;
4217
+ out += `import type { AuthConfig, TxOp } from "./base-client${ext}";
4071
4218
  `;
4072
4219
  for (const t of tables) {
4073
4220
  out += `import { ${pascal(t.name)}Client } from "./${t.name}${ext}";
@@ -4083,7 +4230,7 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
4083
4230
  `;
4084
4231
  out += ` */
4085
4232
  `;
4086
- out += `export class SDK {
4233
+ out += `export class SDK extends BaseClient {
4087
4234
  `;
4088
4235
  for (const t of tables) {
4089
4236
  out += ` public ${t.name}: ${pascal(t.name)}Client;
@@ -4093,17 +4240,101 @@ export type { AuthConfig, HeaderMap, AuthHeadersProvider } from "./base-client${
4093
4240
  constructor(cfg: { baseUrl: string; fetch?: typeof fetch; auth?: AuthConfig }) {
4094
4241
  `;
4095
4242
  out += ` const f = cfg.fetch ?? fetch;
4243
+ `;
4244
+ out += ` super(cfg.baseUrl, f, cfg.auth);
4096
4245
  `;
4097
4246
  for (const t of tables) {
4098
4247
  out += ` this.${t.name} = new ${pascal(t.name)}Client(cfg.baseUrl, f, cfg.auth);
4099
4248
  `;
4100
4249
  }
4101
4250
  out += ` }
4251
+ `;
4252
+ out += `
4253
+ `;
4254
+ out += ` /**
4255
+ `;
4256
+ out += ` * Execute multiple operations atomically in one PostgreSQL transaction.
4257
+ `;
4258
+ out += ` * All ops are validated before BEGIN is issued — fail-fast on bad input.
4259
+ `;
4260
+ out += ` *
4261
+ `;
4262
+ out += ` * @example
4263
+ `;
4264
+ out += ` * const [order, user] = await sdk.$transaction([
4265
+ `;
4266
+ out += ` * sdk.orders.$create({ user_id: 1, total: 99 }),
4267
+ `;
4268
+ out += ` * sdk.users.$update('1', { last_order_at: new Date().toISOString() }),
4269
+ `;
4270
+ out += ` * ]);
4271
+ `;
4272
+ out += ` */
4273
+ `;
4274
+ out += ` async $transaction<const T extends readonly TxOp<unknown>[]>(
4275
+ `;
4276
+ out += ` ops: [...T]
4277
+ `;
4278
+ out += ` ): Promise<{ [K in keyof T]: T[K] extends TxOp<infer R> ? R : never }> {
4279
+ `;
4280
+ out += ` const payload = ops.map(op => ({
4281
+ `;
4282
+ out += ` op: op._op,
4283
+ `;
4284
+ out += ` table: op._table,
4285
+ `;
4286
+ out += ` ...(op._data !== undefined ? { data: op._data } : {}),
4287
+ `;
4288
+ out += ` ...(op._pk !== undefined ? { pk: op._pk } : {}),
4289
+ `;
4290
+ out += ` }));
4291
+ `;
4292
+ out += `
4293
+ `;
4294
+ out += ` const res = await this.fetchFn(\`\${this.baseUrl}/v1/transaction\`, {
4295
+ `;
4296
+ out += ` method: "POST",
4297
+ `;
4298
+ out += ` headers: await this.headers(true),
4299
+ `;
4300
+ out += ` body: JSON.stringify({ ops: payload }),
4301
+ `;
4302
+ out += ` });
4303
+ `;
4304
+ out += `
4305
+ `;
4306
+ out += ` if (!res.ok) {
4307
+ `;
4308
+ out += ` let errBody: Record<string, unknown> = {};
4309
+ `;
4310
+ out += ` try { errBody = await res.json() as Record<string, unknown>; } catch {}
4311
+ `;
4312
+ out += ` const err = Object.assign(
4313
+ `;
4314
+ out += ` new Error((errBody.error as string | undefined) ?? \`$transaction failed: \${res.status}\`),
4315
+ `;
4316
+ out += ` { failedAt: errBody.failedAt as number | undefined, issues: errBody.issues }
4317
+ `;
4318
+ out += ` );
4319
+ `;
4320
+ out += ` throw err;
4321
+ `;
4322
+ out += ` }
4323
+ `;
4324
+ out += `
4325
+ `;
4326
+ out += ` const json = await res.json() as { results: unknown[] };
4327
+ `;
4328
+ out += ` return json.results as unknown as { [K in keyof T]: T[K] extends TxOp<infer R> ? R : never };
4329
+ `;
4330
+ out += ` }
4102
4331
  `;
4103
4332
  out += `}
4104
4333
 
4105
4334
  `;
4106
4335
  out += `export { BaseClient } from "./base-client${ext}";
4336
+ `;
4337
+ out += `export type { TxOp } from "./base-client${ext}";
4107
4338
  `;
4108
4339
  out += `
4109
4340
  // Include specification types for custom queries
@@ -4304,15 +4535,29 @@ export abstract class BaseClient {
4304
4535
  method: "DELETE",
4305
4536
  headers: await this.headers(),
4306
4537
  });
4307
-
4538
+
4308
4539
  if (res.status === 404) {
4309
4540
  return null as T;
4310
4541
  }
4311
-
4542
+
4312
4543
  await this.okOrThrow(res, "DELETE", path);
4313
4544
  return (await res.json()) as T;
4314
4545
  }
4315
4546
  }
4547
+
4548
+ /**
4549
+ * Lazy operation descriptor returned by $create/$update/$softDelete/$hardDelete/$upsert.
4550
+ * \`__resultType\` is a phantom field — never assigned at runtime, exists only
4551
+ * so TypeScript can infer the correct tuple element type inside \`$transaction\`.
4552
+ */
4553
+ export type TxOp<T = unknown> = {
4554
+ readonly _table: string;
4555
+ readonly _op: "create" | "update" | "softDelete" | "hardDelete" | "upsert";
4556
+ readonly _data?: Record<string, unknown>;
4557
+ readonly _pk?: string | Record<string, unknown>;
4558
+ /** @internal */
4559
+ readonly __resultType?: T;
4560
+ };
4316
4561
  `;
4317
4562
  }
4318
4563
 
@@ -4856,14 +5101,14 @@ function tsTypeFor(pgType, opts, enums) {
4856
5101
  return "number[]";
4857
5102
  return "string";
4858
5103
  }
4859
- function isJsonbType3(pgType) {
5104
+ function isJsonbType2(pgType) {
4860
5105
  const t = pgType.toLowerCase();
4861
5106
  return t === "json" || t === "jsonb";
4862
5107
  }
4863
5108
  var pascal2 = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
4864
5109
  function emitTypes(table, opts, enums) {
4865
5110
  const Type = pascal2(table.name);
4866
- const hasJsonbColumns = table.columns.some((col) => isJsonbType3(col.pgType));
5111
+ const hasJsonbColumns = table.columns.some((col) => isJsonbType2(col.pgType));
4867
5112
  const insertFields = table.columns.map((col) => {
4868
5113
  const base = tsTypeFor(col.pgType, opts, enums);
4869
5114
  const optional = col.hasDefault || col.nullable ? "?" : "";
@@ -5314,9 +5559,12 @@ export async function authMiddleware(c: Context, next: Next) {
5314
5559
 
5315
5560
  // src/emit-router-hono.ts
5316
5561
  init_utils();
5317
- function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
5562
+ function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken, opts) {
5318
5563
  const tableNames = tables.map((t) => t.name).sort();
5319
5564
  const ext = useJsExtensions ? ".js" : "";
5565
+ const apiPathPrefix = opts?.apiPathPrefix ?? "/v1";
5566
+ const softDeleteCols = opts?.softDeleteCols ?? {};
5567
+ const includeMethodsDepth = opts?.includeMethodsDepth ?? 2;
5320
5568
  let resolvedPullToken;
5321
5569
  let pullTokenEnvVar;
5322
5570
  if (pullToken) {
@@ -5342,6 +5590,93 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions, pullToken) {
5342
5590
  const Type = pascal(name);
5343
5591
  return `export { register${Type}Routes } from "./routes/${name}${ext}";`;
5344
5592
  }).join(`
5593
+ `);
5594
+ const txSchemaImports = tableNames.map((name) => {
5595
+ const Type = pascal(name);
5596
+ return `import { Insert${Type}Schema, Update${Type}Schema } from "./zod/${name}${ext}";`;
5597
+ }).join(`
5598
+ `);
5599
+ function txRouteBlock(appVar) {
5600
+ const authLine = hasAuth ? ` ${appVar}.use(\`${apiPathPrefix}/transaction\`, authMiddleware);
5601
+ ` : "";
5602
+ return `${authLine} ${appVar}.post(\`${apiPathPrefix}/transaction\`, async (c) => {
5603
+ const body = await c.req.json().catch(() => ({}));
5604
+ const rawOps: unknown[] = Array.isArray(body.ops) ? body.ops : [];
5605
+
5606
+ if (rawOps.length === 0) {
5607
+ return c.json({ error: "ops must be a non-empty array" }, 400);
5608
+ }
5609
+
5610
+ // Validate all ops against their table schemas BEFORE opening a transaction
5611
+ const validatedOps: coreOps.TransactionOperation[] = [];
5612
+ for (let i = 0; i < rawOps.length; i++) {
5613
+ const item = rawOps[i] as any;
5614
+ const entry = TABLE_TX_METADATA[item?.table as string];
5615
+ if (!entry) {
5616
+ return c.json({ error: \`Unknown table "\${item?.table}" at index \${i}\`, failedAt: i }, 400);
5617
+ }
5618
+ if (item.op === "create") {
5619
+ const parsed = entry.insertSchema.safeParse(item.data ?? {});
5620
+ if (!parsed.success) {
5621
+ return c.json({ error: "Validation failed", issues: parsed.error.flatten(), failedAt: i }, 400);
5622
+ }
5623
+ validatedOps.push({ op: "create", table: item.table, data: parsed.data });
5624
+ } else if (item.op === "update") {
5625
+ const parsed = entry.updateSchema.safeParse(item.data ?? {});
5626
+ if (!parsed.success) {
5627
+ return c.json({ error: "Validation failed", issues: parsed.error.flatten(), failedAt: i }, 400);
5628
+ }
5629
+ if (item.pk == null) {
5630
+ return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
5631
+ }
5632
+ validatedOps.push({ op: "update", table: item.table, pk: item.pk, data: parsed.data });
5633
+ } else if (item.op === "softDelete") {
5634
+ if (item.pk == null) {
5635
+ return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
5636
+ }
5637
+ validatedOps.push({ op: "softDelete", table: item.table, pk: item.pk });
5638
+ } else if (item.op === "hardDelete") {
5639
+ if (item.pk == null) {
5640
+ return c.json({ error: \`Missing pk at index \${i}\`, failedAt: i }, 400);
5641
+ }
5642
+ validatedOps.push({ op: "hardDelete", table: item.table, pk: item.pk });
5643
+ } else {
5644
+ return c.json({ error: \`Unknown op "\${item?.op}" at index \${i}\`, failedAt: i }, 400);
5645
+ }
5646
+ }
5647
+
5648
+ const onBegin = deps.onRequest
5649
+ ? (txClient: typeof deps.pg) => deps.onRequest!(c, txClient)
5650
+ : undefined;
5651
+
5652
+ const result = await coreOps.executeTransaction(deps.pg, validatedOps, TABLE_TX_METADATA, onBegin);
5653
+
5654
+ if (!result.ok) {
5655
+ return c.json({ error: result.error, failedAt: result.failedAt }, 400);
5656
+ }
5657
+ return c.json({ results: result.results.map(r => r.data) }, 200);
5658
+ });`;
5659
+ }
5660
+ const txMetadataEntries = tables.slice().sort((a, b) => a.name.localeCompare(b.name)).map((t) => {
5661
+ const rawPk = t.pk;
5662
+ const pkCols = Array.isArray(rawPk) ? rawPk : rawPk ? [rawPk] : ["id"];
5663
+ const softDel = softDeleteCols[t.name] ?? null;
5664
+ const allCols = t.columns.map((c) => `"${c.name}"`).join(", ");
5665
+ const vecCols = t.columns.filter((c) => isVectorType(c.pgType)).map((c) => `"${c.name}"`).join(", ");
5666
+ const jsonCols = t.columns.filter((c) => isJsonbType(c.pgType)).map((c) => `"${c.name}"`).join(", ");
5667
+ const Type = pascal(t.name);
5668
+ return ` "${t.name}": {
5669
+ table: "${t.name}",
5670
+ pkColumns: ${JSON.stringify(pkCols)},
5671
+ softDeleteColumn: ${softDel ? `"${softDel}"` : "null"},
5672
+ allColumnNames: [${allCols}],
5673
+ vectorColumns: [${vecCols}],
5674
+ jsonbColumns: [${jsonCols}],
5675
+ includeMethodsDepth: ${includeMethodsDepth},
5676
+ insertSchema: Insert${Type}Schema,
5677
+ updateSchema: Update${Type}Schema,
5678
+ }`;
5679
+ }).join(`,
5345
5680
  `);
5346
5681
  return `/**
5347
5682
  * AUTO-GENERATED FILE - DO NOT EDIT
@@ -5355,8 +5690,27 @@ import { Hono } from "hono";
5355
5690
  import type { Context } from "hono";
5356
5691
  import { SDK_MANIFEST } from "./sdk-bundle${ext}";
5357
5692
  import { getContract } from "./contract${ext}";
5693
+ import * as coreOps from "./core/operations${ext}";
5694
+ ${txSchemaImports}
5358
5695
  ${imports}
5359
- ${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
5696
+ ${hasAuth ? `import { authMiddleware } from "./auth${ext}";
5697
+ export { authMiddleware };` : ""}
5698
+
5699
+ /** Discriminated result from safeParse — mirrors Zod's actual return shape */
5700
+ type SchemaParseResult =
5701
+ | { success: true; data: Record<string, unknown> }
5702
+ | { success: false; error: { flatten: () => unknown } };
5703
+
5704
+ /** Registry entry — core metadata + Zod schemas for request validation */
5705
+ interface TxTableRegistry extends coreOps.TransactionTableMetadata {
5706
+ insertSchema: { safeParse: (v: unknown) => SchemaParseResult };
5707
+ updateSchema: { safeParse: (v: unknown) => SchemaParseResult };
5708
+ }
5709
+
5710
+ // Registry used by POST /v1/transaction — maps table name to metadata + Zod schemas
5711
+ const TABLE_TX_METADATA: Record<string, TxTableRegistry> = {
5712
+ ${txMetadataEntries}
5713
+ };
5360
5714
 
5361
5715
  /**
5362
5716
  * Creates a Hono router with all generated routes that can be mounted into your existing app.
@@ -5426,7 +5780,7 @@ ${pullToken ? `
5426
5780
  if (!expectedToken) {
5427
5781
  // Token not configured in environment - reject request
5428
5782
  return c.json({
5429
- error: "SDK endpoints are protected but pullToken environment variable not set. ${pullTokenEnvVar ? `Set ${pullTokenEnvVar} in your environment or remove pullToken from config.` : "Set the pullToken environment variable or remove pullToken from config."}"
5783
+ 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."}"
5430
5784
  }, 500);
5431
5785
  }
5432
5786
 
@@ -5443,6 +5797,9 @@ ${pullToken ? `
5443
5797
  await next();
5444
5798
  });
5445
5799
  ` : ""}
5800
+ // Transaction endpoint — executes multiple operations atomically
5801
+ ${txRouteBlock("router")}
5802
+
5446
5803
  // SDK distribution endpoints
5447
5804
  router.get("/_psdk/sdk/manifest", (c) => {
5448
5805
  return c.json({
@@ -5526,6 +5883,7 @@ export function registerAllRoutes(
5526
5883
  }
5527
5884
  ) {
5528
5885
  ${registrations.replace(/router/g, "app")}
5886
+ ${txRouteBlock("app")}
5529
5887
  }
5530
5888
 
5531
5889
  // Individual route registrations (for selective use)
@@ -5713,6 +6071,96 @@ export async function createRecord(
5713
6071
  }
5714
6072
  }
5715
6073
 
6074
+ /**
6075
+ * UPSERT operation - Insert or update a record based on a conflict target
6076
+ */
6077
+ export async function upsertRecord(
6078
+ ctx: OperationContext,
6079
+ args: {
6080
+ where: Record<string, any>; // conflict target columns (keys only matter)
6081
+ create: Record<string, any>; // full insert data
6082
+ update: Record<string, any>; // update data on conflict
6083
+ }
6084
+ ): Promise<{ data?: any; error?: string; issues?: any; status: number }> {
6085
+ try {
6086
+ const { where, create: createData, update: updateData } = args;
6087
+
6088
+ const conflictCols = Object.keys(where);
6089
+ if (!conflictCols.length) {
6090
+ return { error: "where must specify at least one column", status: 400 };
6091
+ }
6092
+
6093
+ const insertCols = Object.keys(createData);
6094
+ const insertVals = Object.values(createData);
6095
+ if (!insertCols.length) {
6096
+ return { error: "No fields provided in create", status: 400 };
6097
+ }
6098
+
6099
+ // Filter PK columns from update (mirrors updateRecord behaviour)
6100
+ const filteredUpdate = Object.fromEntries(
6101
+ Object.entries(updateData).filter(([k]) => !ctx.pkColumns.includes(k))
6102
+ );
6103
+ if (!Object.keys(filteredUpdate).length) {
6104
+ return { error: "update must include at least one non-PK field", status: 400 };
6105
+ }
6106
+
6107
+ // Serialise JSONB/vector values for create (same pattern as createRecord)
6108
+ const preparedInsertVals = insertVals.map((v, i) =>
6109
+ v !== null && v !== undefined && typeof v === 'object' &&
6110
+ (ctx.jsonbColumns?.includes(insertCols[i]!) || ctx.vectorColumns?.includes(insertCols[i]!))
6111
+ ? JSON.stringify(v) : v
6112
+ );
6113
+
6114
+ // Serialise JSONB/vector values for update
6115
+ const updateCols = Object.keys(filteredUpdate);
6116
+ const updateVals = Object.values(filteredUpdate);
6117
+ const preparedUpdateVals = updateVals.map((v, i) =>
6118
+ v !== null && v !== undefined && typeof v === 'object' &&
6119
+ (ctx.jsonbColumns?.includes(updateCols[i]!) || ctx.vectorColumns?.includes(updateCols[i]!))
6120
+ ? JSON.stringify(v) : v
6121
+ );
6122
+
6123
+ const placeholders = insertCols.map((_, i) => \`$\${i + 1}\`).join(', ');
6124
+ const conflictSQL = conflictCols.map(c => \`"\${c}"\`).join(', ');
6125
+ const setSql = updateCols.map((k, i) => \`"\${k}" = $\${insertCols.length + i + 1}\`).join(', ');
6126
+ const returningClause = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
6127
+
6128
+ const text = \`INSERT INTO "\${ctx.table}" (\${insertCols.map(c => \`"\${c}"\`).join(', ')})
6129
+ VALUES (\${placeholders})
6130
+ ON CONFLICT (\${conflictSQL}) DO UPDATE SET \${setSql}
6131
+ RETURNING \${returningClause}\`;
6132
+ const params = [...preparedInsertVals, ...preparedUpdateVals];
6133
+
6134
+ log.debug("UPSERT SQL:", text, "params:", params);
6135
+ const { rows } = await ctx.pg.query(text, params);
6136
+ const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
6137
+
6138
+ if (!parsedRows[0]) {
6139
+ return { data: null, status: 500 };
6140
+ }
6141
+
6142
+ return { data: parsedRows[0], status: 200 };
6143
+ } catch (e: any) {
6144
+ const errorMsg = e?.message ?? "";
6145
+ const isJsonError = errorMsg.includes("invalid input syntax for type json");
6146
+ if (isJsonError) {
6147
+ log.error(\`UPSERT \${ctx.table} - Invalid JSON input detected!\`);
6148
+ log.error("Input args that caused error:", JSON.stringify(args, null, 2));
6149
+ log.error("Filtered update data (sent to DB):", JSON.stringify(Object.fromEntries(
6150
+ Object.entries(args.update).filter(([k]) => !ctx.pkColumns.includes(k))
6151
+ ), null, 2));
6152
+ log.error("PostgreSQL error:", errorMsg);
6153
+ } else {
6154
+ log.error(\`UPSERT \${ctx.table} error:\`, e?.stack ?? e);
6155
+ }
6156
+ return {
6157
+ error: e?.message ?? "Internal error",
6158
+ ...(DEBUG ? { stack: e?.stack } : {}),
6159
+ status: 500
6160
+ };
6161
+ }
6162
+ }
6163
+
5716
6164
  /**
5717
6165
  * READ operation - Get a record by primary key
5718
6166
  */
@@ -6389,7 +6837,8 @@ export async function updateRecord(
6389
6837
  */
6390
6838
  export async function deleteRecord(
6391
6839
  ctx: OperationContext,
6392
- pkValues: any[]
6840
+ pkValues: any[],
6841
+ opts?: { hard?: boolean }
6393
6842
  ): Promise<{ data?: any; error?: string; status: number }> {
6394
6843
  try {
6395
6844
  const hasCompositePk = ctx.pkColumns.length > 1;
@@ -6398,11 +6847,12 @@ export async function deleteRecord(
6398
6847
  : \`"\${ctx.pkColumns[0]}" = $1\`;
6399
6848
 
6400
6849
  const returningClause = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
6401
- const text = ctx.softDeleteColumn
6850
+ const doSoftDelete = ctx.softDeleteColumn && !opts?.hard;
6851
+ const text = doSoftDelete
6402
6852
  ? \`UPDATE "\${ctx.table}" SET "\${ctx.softDeleteColumn}" = NOW() WHERE \${wherePkSql} RETURNING \${returningClause}\`
6403
6853
  : \`DELETE FROM "\${ctx.table}" WHERE \${wherePkSql} RETURNING \${returningClause}\`;
6404
6854
 
6405
- log.debug(\`DELETE \${ctx.softDeleteColumn ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
6855
+ log.debug(\`DELETE \${doSoftDelete ? '(soft)' : ''} \${ctx.table} SQL:\`, text, "pk:", pkValues);
6406
6856
  const { rows } = await ctx.pg.query(text, pkValues);
6407
6857
  const parsedRows = parseVectorColumns(rows, ctx.vectorColumns);
6408
6858
 
@@ -6413,12 +6863,126 @@ export async function deleteRecord(
6413
6863
  return { data: parsedRows[0], status: 200 };
6414
6864
  } catch (e: any) {
6415
6865
  log.error(\`DELETE \${ctx.table} error:\`, e?.stack ?? e);
6416
- return {
6417
- error: e?.message ?? "Internal error",
6866
+ return {
6867
+ error: e?.message ?? "Internal error",
6418
6868
  ...(DEBUG ? { stack: e?.stack } : {}),
6419
- status: 500
6869
+ status: 500
6420
6870
  };
6421
6871
  }
6872
+ }
6873
+
6874
+ /**
6875
+ * Static metadata for a table used during transaction execution.
6876
+ * Mirrors the fields needed to construct an OperationContext (minus the pg client,
6877
+ * which is injected at transaction time).
6878
+ */
6879
+ export interface TransactionTableMetadata {
6880
+ table: string;
6881
+ pkColumns: string[];
6882
+ softDeleteColumn: string | null;
6883
+ allColumnNames: string[];
6884
+ vectorColumns: string[];
6885
+ jsonbColumns: string[];
6886
+ includeMethodsDepth: number;
6887
+ }
6888
+
6889
+ export type TransactionOperation =
6890
+ | { op: "create"; table: string; data: Record<string, unknown> }
6891
+ | { op: "update"; table: string; pk: string | Record<string, unknown>; data: Record<string, unknown> }
6892
+ | { op: "softDelete"; table: string; pk: string | Record<string, unknown> }
6893
+ | { op: "hardDelete"; table: string; pk: string | Record<string, unknown> }
6894
+ | { op: "upsert"; table: string; data: { where: Record<string, unknown>; create: Record<string, unknown>; update: Record<string, unknown> } };
6895
+
6896
+ /**
6897
+ * Executes a list of operations atomically inside a single PostgreSQL transaction.
6898
+ *
6899
+ * - When \`pg\` has a \`.connect()\` method (Pool), acquires a dedicated connection.
6900
+ * - Otherwise uses \`pg\` directly (already a single connected Client).
6901
+ * - Calls \`onBegin(txClient)\` after BEGIN and before any operations (for SET LOCAL etc.).
6902
+ * - Any status >= 400 or thrown error triggers ROLLBACK.
6903
+ * - \`failedAt: -1\` indicates an unexpected exception (e.g. connectivity failure).
6904
+ */
6905
+ export async function executeTransaction(
6906
+ pg: DatabaseClient & { connect?: () => Promise<DatabaseClient & { release?: () => void }> },
6907
+ ops: TransactionOperation[],
6908
+ metadata: Record<string, TransactionTableMetadata>,
6909
+ onBegin?: (txClient: DatabaseClient) => Promise<void>
6910
+ ): Promise<
6911
+ | { ok: true; results: Array<{ data: unknown }> }
6912
+ | { ok: false; error: string; failedAt: number }
6913
+ > {
6914
+ // Fail-fast: validate all table names before touching the DB
6915
+ for (let i = 0; i < ops.length; i++) {
6916
+ if (!metadata[ops[i]!.table]) {
6917
+ return { ok: false, error: \`Unknown table "\${ops[i]!.table}"\`, failedAt: i };
6918
+ }
6919
+ }
6920
+
6921
+ // Pool gives a dedicated connection; plain Client is used directly
6922
+ const isPool = typeof (pg as any).connect === "function";
6923
+ const txClient: DatabaseClient & { release?: () => void } = isPool
6924
+ ? await (pg as any).connect()
6925
+ : pg;
6926
+
6927
+ try {
6928
+ await txClient.query("BEGIN");
6929
+ if (onBegin) await onBegin(txClient);
6930
+
6931
+ const results: Array<{ data: unknown }> = [];
6932
+
6933
+ for (let i = 0; i < ops.length; i++) {
6934
+ const op = ops[i]!;
6935
+ const meta = metadata[op.table]!;
6936
+ const ctx: OperationContext = {
6937
+ pg: txClient,
6938
+ table: meta.table,
6939
+ pkColumns: meta.pkColumns,
6940
+ softDeleteColumn: meta.softDeleteColumn,
6941
+ allColumnNames: meta.allColumnNames,
6942
+ vectorColumns: meta.vectorColumns,
6943
+ jsonbColumns: meta.jsonbColumns,
6944
+ includeMethodsDepth: meta.includeMethodsDepth,
6945
+ };
6946
+
6947
+ let result: { data?: unknown; error?: string; status: number };
6948
+
6949
+ if (op.op === "create") {
6950
+ result = await createRecord(ctx, op.data);
6951
+ } else if (op.op === "upsert") {
6952
+ result = await upsertRecord(ctx, op.data);
6953
+ } else {
6954
+ const pkValues = Array.isArray(op.pk)
6955
+ ? op.pk
6956
+ : typeof op.pk === "object" && op.pk !== null
6957
+ ? meta.pkColumns.map(c => (op.pk as Record<string, unknown>)[c])
6958
+ : [op.pk];
6959
+ result = op.op === "update"
6960
+ ? await updateRecord(ctx, pkValues as string[], op.data)
6961
+ : op.op === "hardDelete"
6962
+ ? await deleteRecord(ctx, pkValues as string[], { hard: true })
6963
+ : await deleteRecord(ctx, pkValues as string[]);
6964
+ }
6965
+
6966
+ if (result.status >= 400) {
6967
+ try { await txClient.query("ROLLBACK"); } catch {}
6968
+ txClient.release?.();
6969
+ return {
6970
+ ok: false,
6971
+ error: result.error ?? \`Op \${i} returned status \${result.status}\`,
6972
+ failedAt: i,
6973
+ };
6974
+ }
6975
+ results.push({ data: result.data ?? null });
6976
+ }
6977
+
6978
+ await txClient.query("COMMIT");
6979
+ txClient.release?.();
6980
+ return { ok: true, results };
6981
+ } catch (e: unknown) {
6982
+ try { await txClient.query("ROLLBACK"); } catch {}
6983
+ txClient.release?.();
6984
+ return { ok: false, error: (e instanceof Error ? e.message : String(e)) ?? "Transaction error", failedAt: -1 };
6985
+ }
6422
6986
  }`;
6423
6987
  }
6424
6988
 
@@ -6850,7 +7414,7 @@ function generateForeignKeySetup(table, model, clientPath) {
6850
7414
  // Clean up parent ${foreignTableName} record
6851
7415
  if (${foreignTableName}Id) {
6852
7416
  try {
6853
- await sdk.${foreignTableName}.delete(${foreignTableName}Id);
7417
+ await sdk.${foreignTableName}.hardDelete(${foreignTableName}Id);
6854
7418
  } catch (e) {
6855
7419
  // Parent might already be deleted due to cascading
6856
7420
  }
@@ -6867,7 +7431,7 @@ function generateForeignKeySetup(table, model, clientPath) {
6867
7431
  // Clean up parent ${foreignTableName} record
6868
7432
  if (${foreignTableName}Key) {
6869
7433
  try {
6870
- await sdk.${foreignTableName}.delete(${foreignTableName}Key);
7434
+ await sdk.${foreignTableName}.hardDelete(${foreignTableName}Key);
6871
7435
  } catch (e) {
6872
7436
  // Parent might already be deleted due to cascading
6873
7437
  }
@@ -7132,19 +7696,34 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
7132
7696
  console.warn('No ID from create test, skipping update test');
7133
7697
  return;
7134
7698
  }
7135
-
7699
+
7136
7700
  const updateData: Update${Type} = ${updateData};
7137
7701
  const updated = await sdk.${table.name}.update(createdId, updateData);
7138
7702
  expect(updated).toBeDefined();
7703
+ });
7704
+
7705
+ it('should upsert (update path) ${table.name}', async () => {
7706
+ if (!createdId) {
7707
+ console.warn('No ID from create test, skipping upsert test');
7708
+ return;
7709
+ }
7710
+ // where: use PK as conflict target; create includes PK so conflict is guaranteed
7711
+ const result = await sdk.${table.name}.upsert({
7712
+ where: { ${table.pk[0]}: createdId },
7713
+ create: { ${table.pk[0]}: createdId, ...${sampleData} },
7714
+ update: ${updateData},
7715
+ });
7716
+ expect(result).toBeDefined();
7717
+ expect(result.${table.pk[0]}).toBe(createdId);
7139
7718
  });` : ""}
7140
-
7719
+
7141
7720
  it('should delete ${table.name}', async () => {
7142
7721
  if (!createdId) {
7143
7722
  console.warn('No ID from create test, skipping delete test');
7144
7723
  return;
7145
7724
  }
7146
7725
 
7147
- const deleted = await sdk.${table.name}.delete(createdId);
7726
+ const deleted = await sdk.${table.name}.hardDelete(createdId);
7148
7727
  expect(deleted).toBeDefined();
7149
7728
 
7150
7729
  // Verify deletion
@@ -7161,10 +7740,16 @@ var __filename2 = fileURLToPath(import.meta.url);
7161
7740
  var __dirname2 = dirname2(__filename2);
7162
7741
  var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
7163
7742
  function resolveSoftDeleteColumn(cfg, tableName) {
7164
- const overrides = cfg.softDeleteColumnOverrides;
7743
+ const del = cfg.delete;
7744
+ if (!del)
7745
+ return null;
7746
+ const overrides = del.softDeleteColumnOverrides;
7165
7747
  if (overrides && tableName in overrides)
7166
7748
  return overrides[tableName] ?? null;
7167
- return cfg.softDeleteColumn ?? null;
7749
+ return del.softDeleteColumn ?? null;
7750
+ }
7751
+ function resolveExposeHardDelete(cfg) {
7752
+ return cfg.delete?.exposeHardDelete ?? true;
7168
7753
  }
7169
7754
  async function generate(configPath, options) {
7170
7755
  if (!existsSync2(configPath)) {
@@ -7254,6 +7839,7 @@ async function generate(configPath, options) {
7254
7839
  path: join2(serverDir, "core", "operations.ts"),
7255
7840
  content: emitCoreOperations()
7256
7841
  });
7842
+ const exposeHardDelete = resolveExposeHardDelete(cfg);
7257
7843
  if (process.env.SDK_DEBUG) {
7258
7844
  console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
7259
7845
  }
@@ -7271,6 +7857,7 @@ async function generate(configPath, options) {
7271
7857
  if (serverFramework === "hono") {
7272
7858
  routeContent = emitHonoRoutes(table, graph, {
7273
7859
  softDeleteColumn: softDeleteCols[table.name] ?? null,
7860
+ exposeHardDelete,
7274
7861
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
7275
7862
  authStrategy: getAuthStrategy(normalizedAuth),
7276
7863
  useJsExtensions: cfg.useJsExtensions,
@@ -7286,6 +7873,8 @@ async function generate(configPath, options) {
7286
7873
  files.push({
7287
7874
  path: join2(clientDir, `${table.name}.ts`),
7288
7875
  content: emitClient(table, graph, {
7876
+ softDeleteColumn: softDeleteCols[table.name] ?? null,
7877
+ exposeHardDelete,
7289
7878
  useJsExtensions: cfg.useJsExtensionsClient,
7290
7879
  includeMethodsDepth: cfg.includeMethodsDepth ?? 2,
7291
7880
  skipJunctionTables: cfg.skipJunctionTables ?? true
@@ -7299,7 +7888,11 @@ async function generate(configPath, options) {
7299
7888
  if (serverFramework === "hono") {
7300
7889
  files.push({
7301
7890
  path: join2(serverDir, "router.ts"),
7302
- content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken)
7891
+ content: emitHonoRouter(Object.values(model.tables), getAuthStrategy(normalizedAuth) !== "none", cfg.useJsExtensions, cfg.pullToken, {
7892
+ apiPathPrefix: cfg.apiPathPrefix || "/v1",
7893
+ softDeleteCols: Object.fromEntries(Object.values(model.tables).map((t) => [t.name, resolveSoftDeleteColumn(cfg, t.name)])),
7894
+ includeMethodsDepth: cfg.includeMethodsDepth ?? 2
7895
+ })
7303
7896
  });
7304
7897
  }
7305
7898
  const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));