postgresdk 0.18.22 → 0.18.24

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/index.js CHANGED
@@ -749,7 +749,7 @@ __export(exports_emit_sdk_contract, {
749
749
  function generateUnifiedContract(model, config, graph) {
750
750
  const resources = [];
751
751
  const relationships = [];
752
- const tables = model && model.tables ? Object.values(model.tables) : [];
752
+ const tables = Object.values(model.tables);
753
753
  if (process.env.SDK_DEBUG) {
754
754
  console.log(`[SDK Contract] Processing ${tables.length} tables`);
755
755
  }
@@ -879,30 +879,23 @@ function generateResourceWithSDK(table, model, graph, config) {
879
879
  const hasSinglePK = table.pk.length === 1;
880
880
  const pkField = hasSinglePK ? table.pk[0] : "id";
881
881
  const enums = model.enums || {};
882
+ const overrides = config?.softDeleteColumnOverrides;
883
+ const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.softDeleteColumn ?? null;
884
+ const softDeleteCol = resolvedSoftDeleteCol && table.columns.some((c) => c.name === resolvedSoftDeleteCol) ? resolvedSoftDeleteCol : null;
882
885
  const sdkMethods = [];
883
886
  const endpoints = [];
884
887
  sdkMethods.push({
885
888
  name: "list",
886
889
  signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
887
890
  description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
888
- example: `// Get all ${tableName}
889
- const result = await sdk.${tableName}.list();
890
- console.log(result.data); // array of records
891
- console.log(result.total); // total matching records
892
- console.log(result.hasMore); // true if more pages available
893
-
894
- // With filters and pagination
895
- const filtered = await sdk.${tableName}.list({
896
- limit: 20,
897
- offset: 0,
898
- where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
891
+ example: `const result = await sdk.${tableName}.list({
892
+ where: { ${table.columns[0]?.name || "id"}: { $ilike: '%value%' } },
899
893
  orderBy: '${table.columns[0]?.name || "created_at"}',
900
- order: 'desc'
901
- });
902
-
903
- // Calculate total pages
904
- const totalPages = Math.ceil(filtered.total / filtered.limit);
905
- const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
894
+ order: 'desc',
895
+ limit: 20,
896
+ offset: 0${softDeleteCol ? `,
897
+ includeSoftDeleted: false` : ""}
898
+ }); // result.data, result.total, result.hasMore`,
906
899
  correspondsTo: `GET ${basePath}`
907
900
  });
908
901
  endpoints.push({
@@ -917,13 +910,8 @@ const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
917
910
  name: "getByPk",
918
911
  signature: `getByPk(${pkField}: string): Promise<${Type} | null>`,
919
912
  description: `Get a single ${tableName} by primary key`,
920
- example: `// Get by ID
921
- const item = await sdk.${tableName}.getByPk('123e4567-e89b-12d3-a456-426614174000');
922
-
923
- // Check if exists
924
- if (item === null) {
925
- console.log('Not found');
926
- }`,
913
+ example: `const item = await sdk.${tableName}.getByPk('id');${softDeleteCol ? `
914
+ const withDeleted = await sdk.${tableName}.getByPk('id', { includeSoftDeleted: true });` : ""} // null if not found`,
927
915
  correspondsTo: `GET ${basePath}/:${pkField}`
928
916
  });
929
917
  endpoints.push({
@@ -937,14 +925,9 @@ if (item === null) {
937
925
  name: "create",
938
926
  signature: `create(data: Insert${Type}): Promise<${Type}>`,
939
927
  description: `Create a new ${tableName}`,
940
- example: `import type { Insert${Type} } from './client/types/${tableName}';
941
-
942
- const newItem: Insert${Type} = {
928
+ example: `const created = await sdk.${tableName}.create({
943
929
  ${generateExampleFields(table, "create")}
944
- };
945
-
946
- const created = await sdk.${tableName}.create(newItem);
947
- console.log('Created:', created.${pkField});`,
930
+ });`,
948
931
  correspondsTo: `POST ${basePath}`
949
932
  });
950
933
  endpoints.push({
@@ -959,13 +942,9 @@ console.log('Created:', created.${pkField});`,
959
942
  name: "update",
960
943
  signature: `update(${pkField}: string, data: Update${Type}): Promise<${Type}>`,
961
944
  description: `Update an existing ${tableName}`,
962
- example: `import type { Update${Type} } from './client/types/${tableName}';
963
-
964
- const updates: Update${Type} = {
945
+ example: `const updated = await sdk.${tableName}.update('id', {
965
946
  ${generateExampleFields(table, "update")}
966
- };
967
-
968
- const updated = await sdk.${tableName}.update('123', updates);`,
947
+ });`,
969
948
  correspondsTo: `PATCH ${basePath}/:${pkField}`
970
949
  });
971
950
  endpoints.push({
@@ -981,8 +960,7 @@ const updated = await sdk.${tableName}.update('123', updates);`,
981
960
  name: "delete",
982
961
  signature: `delete(${pkField}: string): Promise<${Type}>`,
983
962
  description: `Delete a ${tableName}`,
984
- example: `const deleted = await sdk.${tableName}.delete('123');
985
- console.log('Deleted:', deleted);`,
963
+ example: `const deleted = await sdk.${tableName}.delete('id');`,
986
964
  correspondsTo: `DELETE ${basePath}/:${pkField}`
987
965
  });
988
966
  endpoints.push({
@@ -993,29 +971,17 @@ console.log('Deleted:', deleted);`,
993
971
  });
994
972
  }
995
973
  if (graph && config) {
996
- const allTables = model && model.tables ? Object.values(model.tables) : undefined;
974
+ const allTables = Object.values(model.tables);
997
975
  const includeMethods = generateIncludeMethods(table, graph, {
998
976
  maxDepth: config.includeMethodsDepth ?? 2,
999
977
  skipJunctionTables: config.skipJunctionTables ?? true
1000
978
  }, allTables);
1001
979
  for (const method of includeMethods) {
1002
980
  const isGetByPk = method.name.startsWith("getByPk");
1003
- const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const result = await sdk.${tableName}.${method.name}();
1004
- console.log(result.data); // array of records with includes
1005
- console.log(result.total); // total count
1006
- console.log(result.hasMore); // more pages available
1007
-
1008
- // With filters and pagination
1009
- const filtered = await sdk.${tableName}.${method.name}({
1010
- limit: 20,
1011
- offset: 0,
1012
- where: { /* filter conditions */ }
1013
- });`;
1014
981
  sdkMethods.push({
1015
982
  name: method.name,
1016
983
  signature: `${method.name}(${isGetByPk ? `${pkField}: string` : "params?: ListParams"}): ${method.returnType}`,
1017
984
  description: `Get ${tableName} with included ${method.path.join(", ")} data`,
1018
- example: exampleCall,
1019
985
  correspondsTo: `POST ${basePath}/list`
1020
986
  });
1021
987
  }
@@ -1052,79 +1018,68 @@ function generateFieldContract(column, table, enums) {
1052
1018
  }
1053
1019
  return field;
1054
1020
  }
1021
+ function pgTypeCategory(t) {
1022
+ switch (t) {
1023
+ case "int":
1024
+ case "int2":
1025
+ case "int4":
1026
+ case "int8":
1027
+ case "integer":
1028
+ case "smallint":
1029
+ case "bigint":
1030
+ case "decimal":
1031
+ case "numeric":
1032
+ case "real":
1033
+ case "float4":
1034
+ case "float8":
1035
+ case "double precision":
1036
+ case "float":
1037
+ return "number";
1038
+ case "boolean":
1039
+ case "bool":
1040
+ return "boolean";
1041
+ case "date":
1042
+ case "timestamp":
1043
+ case "timestamptz":
1044
+ return "date";
1045
+ case "json":
1046
+ case "jsonb":
1047
+ return "json";
1048
+ case "uuid":
1049
+ return "uuid";
1050
+ case "text[]":
1051
+ case "varchar[]":
1052
+ case "_text":
1053
+ case "_varchar":
1054
+ return "string[]";
1055
+ case "int[]":
1056
+ case "integer[]":
1057
+ case "_int":
1058
+ case "_int2":
1059
+ case "_int4":
1060
+ case "_int8":
1061
+ case "_integer":
1062
+ return "number[]";
1063
+ case "vector":
1064
+ return "number[]";
1065
+ default:
1066
+ return "string";
1067
+ }
1068
+ }
1055
1069
  function postgresTypeToTsType(column, enums) {
1056
1070
  const pgType = column.pgType.toLowerCase();
1057
1071
  if (enums[pgType]) {
1058
1072
  const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
1059
- if (column.nullable) {
1060
- return `${enumType} | null`;
1061
- }
1062
- return enumType;
1063
- }
1064
- if (pgType.startsWith("_")) {
1065
- const enumName = pgType.slice(1);
1066
- const enumValues = enums[enumName];
1067
- if (enumValues) {
1068
- const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
1069
- const arrayType = `(${enumType})[]`;
1070
- if (column.nullable) {
1071
- return `${arrayType} | null`;
1072
- }
1073
- return arrayType;
1074
- }
1075
- }
1076
- const baseType = (() => {
1077
- switch (pgType) {
1078
- case "int":
1079
- case "int2":
1080
- case "int4":
1081
- case "int8":
1082
- case "integer":
1083
- case "smallint":
1084
- case "bigint":
1085
- case "decimal":
1086
- case "numeric":
1087
- case "real":
1088
- case "float4":
1089
- case "float8":
1090
- case "double precision":
1091
- case "float":
1092
- return "number";
1093
- case "boolean":
1094
- case "bool":
1095
- return "boolean";
1096
- case "date":
1097
- case "timestamp":
1098
- case "timestamptz":
1099
- return "string";
1100
- case "json":
1101
- case "jsonb":
1102
- return "JsonValue";
1103
- case "uuid":
1104
- return "string";
1105
- case "text[]":
1106
- case "varchar[]":
1107
- case "_text":
1108
- case "_varchar":
1109
- return "string[]";
1110
- case "int[]":
1111
- case "integer[]":
1112
- case "_int":
1113
- case "_int2":
1114
- case "_int4":
1115
- case "_int8":
1116
- case "_integer":
1117
- return "number[]";
1118
- case "vector":
1119
- return "number[]";
1120
- default:
1121
- return "string";
1122
- }
1123
- })();
1124
- if (column.nullable) {
1125
- return `${baseType} | null`;
1073
+ return column.nullable ? `${enumType} | null` : enumType;
1074
+ }
1075
+ const enumArrayValues = enums[pgType.slice(1)];
1076
+ if (pgType.startsWith("_") && enumArrayValues) {
1077
+ const arrayType = `(${enumArrayValues.map((v) => `"${v}"`).join(" | ")})[]`;
1078
+ return column.nullable ? `${arrayType} | null` : arrayType;
1126
1079
  }
1127
- return baseType;
1080
+ const cat = pgTypeCategory(pgType);
1081
+ const baseType = cat === "date" || cat === "uuid" ? "string" : cat === "json" ? "JsonValue" : cat;
1082
+ return column.nullable ? `${baseType} | null` : baseType;
1128
1083
  }
1129
1084
  function generateExampleFields(table, operation) {
1130
1085
  const fields = [];
@@ -1223,76 +1178,25 @@ function generateQueryParams(table, enums) {
1223
1178
  }
1224
1179
  function postgresTypeToJsonType(pgType, enums) {
1225
1180
  const t = pgType.toLowerCase();
1226
- if (enums[t]) {
1181
+ if (enums[t])
1227
1182
  return t;
1228
- }
1229
- if (t.startsWith("_") && enums[t.slice(1)]) {
1183
+ if (t.startsWith("_") && enums[t.slice(1)])
1230
1184
  return `${t.slice(1)}[]`;
1231
- }
1232
- switch (t) {
1233
- case "int":
1234
- case "int2":
1235
- case "int4":
1236
- case "int8":
1237
- case "integer":
1238
- case "smallint":
1239
- case "bigint":
1240
- case "decimal":
1241
- case "numeric":
1242
- case "real":
1243
- case "float4":
1244
- case "float8":
1245
- case "double precision":
1246
- case "float":
1247
- return "number";
1248
- case "boolean":
1249
- case "bool":
1250
- return "boolean";
1251
- case "date":
1252
- case "timestamp":
1253
- case "timestamptz":
1254
- return "date/datetime";
1255
- case "json":
1256
- case "jsonb":
1257
- return "object";
1258
- case "uuid":
1259
- return "uuid";
1260
- case "text[]":
1261
- case "varchar[]":
1262
- case "_text":
1263
- case "_varchar":
1264
- return "string[]";
1265
- case "int[]":
1266
- case "integer[]":
1267
- case "_int":
1268
- case "_int2":
1269
- case "_int4":
1270
- case "_int8":
1271
- case "_integer":
1272
- return "number[]";
1273
- case "vector":
1274
- return "number[]";
1275
- default:
1276
- return "string";
1277
- }
1185
+ const cat = pgTypeCategory(t);
1186
+ return cat === "date" ? "date/datetime" : cat === "json" ? "object" : cat;
1278
1187
  }
1279
1188
  function generateFieldDescription(column, table) {
1280
- const descriptions = [];
1281
- if (column.name === "id") {
1282
- descriptions.push("Primary key");
1283
- } else if (column.name === "created_at") {
1284
- descriptions.push("Creation timestamp");
1285
- } else if (column.name === "updated_at") {
1286
- descriptions.push("Last update timestamp");
1287
- } else if (column.name === "deleted_at") {
1288
- descriptions.push("Soft delete timestamp");
1289
- } else if (column.name.endsWith("_id")) {
1290
- const relatedTable = column.name.slice(0, -3);
1291
- descriptions.push(`Foreign key to ${relatedTable}`);
1292
- } else {
1293
- descriptions.push(column.name.replace(/_/g, " "));
1294
- }
1295
- return descriptions.join(", ");
1189
+ if (column.name === "id")
1190
+ return "Primary key";
1191
+ if (column.name === "created_at")
1192
+ return "Creation timestamp";
1193
+ if (column.name === "updated_at")
1194
+ return "Last update timestamp";
1195
+ if (column.name === "deleted_at")
1196
+ return "Soft delete timestamp";
1197
+ if (column.name.endsWith("_id"))
1198
+ return `Foreign key to ${column.name.slice(0, -3)}`;
1199
+ return column.name.replace(/_/g, " ");
1296
1200
  }
1297
1201
  function generateUnifiedContractMarkdown(contract) {
1298
1202
  const lines = [];
@@ -1333,288 +1237,53 @@ function generateUnifiedContractMarkdown(contract) {
1333
1237
  lines.push("");
1334
1238
  }
1335
1239
  }
1336
- lines.push("## Filtering with WHERE Clauses");
1337
- lines.push("");
1338
- lines.push("The SDK provides type-safe WHERE clause filtering with support for various operators.");
1339
- lines.push("");
1340
- lines.push("### Basic Filtering");
1341
- lines.push("");
1342
- lines.push("**Direct equality:**");
1343
- lines.push("");
1344
- lines.push("```typescript");
1345
- lines.push("// Find users with specific email");
1346
- lines.push("const users = await sdk.users.list({");
1347
- lines.push(" where: { email: 'user@example.com' }");
1348
- lines.push("});");
1349
- lines.push("");
1350
- lines.push("// Multiple conditions (AND)");
1351
- lines.push("const activeUsers = await sdk.users.list({");
1352
- lines.push(" where: {");
1353
- lines.push(" status: 'active',");
1354
- lines.push(" role: 'admin'");
1355
- lines.push(" }");
1356
- lines.push("});");
1357
- lines.push("```");
1358
- lines.push("");
1359
- lines.push("### Comparison Operators");
1360
- lines.push("");
1361
- lines.push("Use comparison operators for numeric, date, and other comparable fields:");
1362
- lines.push("");
1363
- lines.push("```typescript");
1364
- lines.push("// Greater than / Less than");
1365
- lines.push("const adults = await sdk.users.list({");
1366
- lines.push(" where: { age: { $gt: 18 } }");
1367
- lines.push("});");
1368
- lines.push("");
1369
- lines.push("// Range queries");
1370
- lines.push("const workingAge = await sdk.users.list({");
1371
- lines.push(" where: {");
1372
- lines.push(" age: { $gte: 18, $lte: 65 }");
1373
- lines.push(" }");
1374
- lines.push("});");
1375
- lines.push("");
1376
- lines.push("// Not equal");
1377
- lines.push("const notPending = await sdk.orders.list({");
1378
- lines.push(" where: { status: { $ne: 'pending' } }");
1379
- lines.push("});");
1380
- lines.push("```");
1381
- lines.push("");
1382
- lines.push("### String Operators");
1383
- lines.push("");
1384
- lines.push("Pattern matching for string fields:");
1385
- lines.push("");
1386
- lines.push("```typescript");
1387
- lines.push("// Case-sensitive LIKE");
1388
- lines.push("const johnsmiths = await sdk.users.list({");
1389
- lines.push(" where: { name: { $like: '%Smith%' } }");
1390
- lines.push("});");
1391
- lines.push("");
1392
- lines.push("// Case-insensitive ILIKE");
1393
- lines.push("const gmailUsers = await sdk.users.list({");
1394
- lines.push(" where: { email: { $ilike: '%@gmail.com' } }");
1395
- lines.push("});");
1396
- lines.push("```");
1397
- lines.push("");
1398
- lines.push("### Array Operators");
1399
- lines.push("");
1400
- lines.push("Filter by multiple possible values:");
1401
- lines.push("");
1402
- lines.push("```typescript");
1403
- lines.push("// IN - match any value in array");
1404
- lines.push("const specificUsers = await sdk.users.list({");
1405
- lines.push(" where: {");
1406
- lines.push(" id: { $in: ['id1', 'id2', 'id3'] }");
1407
- lines.push(" }");
1408
- lines.push("});");
1409
- lines.push("");
1410
- lines.push("// NOT IN - exclude values");
1411
- lines.push("const nonSystemUsers = await sdk.users.list({");
1412
- lines.push(" where: {");
1413
- lines.push(" role: { $nin: ['admin', 'system'] }");
1414
- lines.push(" }");
1415
- lines.push("});");
1416
- lines.push("```");
1417
- lines.push("");
1418
- lines.push("### NULL Checks");
1419
- lines.push("");
1420
- lines.push("Check for null or non-null values:");
1421
- lines.push("");
1422
- lines.push("```typescript");
1423
- lines.push("// IS NULL");
1424
- lines.push("const activeRecords = await sdk.records.list({");
1425
- lines.push(" where: { deleted_at: { $is: null } }");
1426
- lines.push("});");
1427
- lines.push("");
1428
- lines.push("// IS NOT NULL");
1429
- lines.push("const deletedRecords = await sdk.records.list({");
1430
- lines.push(" where: { deleted_at: { $isNot: null } }");
1431
- lines.push("});");
1432
- lines.push("```");
1433
- lines.push("");
1434
- lines.push("### Combining Operators");
1435
- lines.push("");
1436
- lines.push("Mix multiple operators for complex queries:");
1437
- lines.push("");
1438
- lines.push("```typescript");
1439
- lines.push("const filteredUsers = await sdk.users.list({");
1440
- lines.push(" where: {");
1441
- lines.push(" age: { $gte: 18, $lt: 65 },");
1442
- lines.push(" email: { $ilike: '%@company.com' },");
1443
- lines.push(" status: { $in: ['active', 'pending'] },");
1444
- lines.push(" deleted_at: { $is: null }");
1445
- lines.push(" },");
1446
- lines.push(" limit: 50,");
1447
- lines.push(" offset: 0");
1448
- lines.push("});");
1449
- lines.push("```");
1450
- lines.push("");
1451
- lines.push("### Available Operators");
1452
- lines.push("");
1453
- lines.push("| Operator | Description | Example | Types |");
1454
- lines.push("|----------|-------------|---------|-------|");
1455
- lines.push("| `$eq` | Equal to | `{ age: { $eq: 25 } }` | All |");
1456
- lines.push("| `$ne` | Not equal to | `{ status: { $ne: 'inactive' } }` | All |");
1457
- lines.push("| `$gt` | Greater than | `{ price: { $gt: 100 } }` | Number, Date |");
1458
- lines.push("| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | Number, Date |");
1459
- lines.push("| `$lt` | Less than | `{ quantity: { $lt: 10 } }` | Number, Date |");
1460
- lines.push("| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | Number, Date |");
1461
- lines.push("| `$in` | In array | `{ id: { $in: ['a', 'b'] } }` | All |");
1462
- lines.push("| `$nin` | Not in array | `{ role: { $nin: ['admin'] } }` | All |");
1463
- lines.push("| `$like` | Pattern match (case-sensitive) | `{ name: { $like: '%John%' } }` | String |");
1464
- lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
1465
- lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1466
- lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
1467
- lines.push("| `$jsonbContains` | JSONB contains | `{ metadata: { $jsonbContains: { tags: ['premium'] } } }` | JSONB/JSON |");
1468
- lines.push("| `$jsonbContainedBy` | JSONB contained by | `{ metadata: { $jsonbContainedBy: {...} } }` | JSONB/JSON |");
1469
- lines.push("| `$jsonbHasKey` | JSONB has key | `{ settings: { $jsonbHasKey: 'theme' } }` | JSONB/JSON |");
1470
- lines.push("| `$jsonbHasAnyKeys` | JSONB has any keys | `{ settings: { $jsonbHasAnyKeys: ['dark', 'light'] } }` | JSONB/JSON |");
1471
- lines.push("| `$jsonbHasAllKeys` | JSONB has all keys | `{ config: { $jsonbHasAllKeys: ['api', 'db'] } }` | JSONB/JSON |");
1472
- lines.push("| `$jsonbPath` | JSONB nested value | `{ meta: { $jsonbPath: { path: ['user', 'age'], operator: '$gte', value: 18 } } }` | JSONB/JSON |");
1473
- lines.push("");
1474
- lines.push("### Logical Operators");
1475
- lines.push("");
1476
- lines.push("Combine conditions using `$or` and `$and` (supports 2 levels of nesting):");
1477
- lines.push("");
1478
- lines.push("| Operator | Description | Example |");
1479
- lines.push("|----------|-------------|---------|");
1480
- lines.push("| `$or` | Match any condition | `{ $or: [{ status: 'active' }, { role: 'admin' }] }` |");
1481
- lines.push("| `$and` | Match all conditions (explicit) | `{ $and: [{ age: { $gte: 18 } }, { status: 'verified' }] }` |");
1482
- lines.push("");
1483
- lines.push("```typescript");
1484
- lines.push("// OR - match any condition");
1485
- lines.push("const results = await sdk.users.list({");
1486
- lines.push(" where: {");
1487
- lines.push(" $or: [");
1488
- lines.push(" { email: { $ilike: '%@gmail.com' } },");
1489
- lines.push(" { status: 'premium' }");
1490
- lines.push(" ]");
1491
- lines.push(" }");
1492
- lines.push("});");
1493
- lines.push("");
1494
- lines.push("// Mixed AND + OR (implicit AND at root level)");
1495
- lines.push("const complex = await sdk.users.list({");
1496
- lines.push(" where: {");
1497
- lines.push(" status: 'active', // AND");
1498
- lines.push(" $or: [");
1499
- lines.push(" { age: { $lt: 18 } },");
1500
- lines.push(" { age: { $gt: 65 } }");
1501
- lines.push(" ]");
1502
- lines.push(" }");
1503
- lines.push("});");
1504
- lines.push("");
1505
- lines.push("// Nested (2 levels max)");
1506
- lines.push("const nested = await sdk.users.list({");
1507
- lines.push(" where: {");
1508
- lines.push(" $and: [");
1509
- lines.push(" {");
1510
- lines.push(" $or: [");
1511
- lines.push(" { firstName: { $ilike: '%john%' } },");
1512
- lines.push(" { lastName: { $ilike: '%john%' } }");
1513
- lines.push(" ]");
1514
- lines.push(" },");
1515
- lines.push(" { status: 'active' }");
1516
- lines.push(" ]");
1517
- lines.push(" }");
1518
- lines.push("});");
1519
- lines.push("```");
1520
- lines.push("");
1521
- lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1522
- lines.push("");
1523
- lines.push("## Sorting");
1524
- lines.push("");
1525
- lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
1526
- lines.push("");
1527
- lines.push("### Single Column Sorting");
1528
- lines.push("");
1529
- lines.push("```typescript");
1530
- lines.push("// Sort by one column ascending");
1531
- lines.push("const users = await sdk.users.list({");
1532
- lines.push(" orderBy: 'created_at',");
1533
- lines.push(" order: 'asc'");
1534
- lines.push("});");
1535
- lines.push("");
1536
- lines.push("// Sort descending");
1537
- lines.push("const latest = await sdk.users.list({");
1538
- lines.push(" orderBy: 'created_at',");
1539
- lines.push(" order: 'desc'");
1540
- lines.push("});");
1541
- lines.push("");
1542
- lines.push("// Order defaults to 'asc' if not specified");
1543
- lines.push("const sorted = await sdk.users.list({");
1544
- lines.push(" orderBy: 'name'");
1545
- lines.push("});");
1546
- lines.push("```");
1547
- lines.push("");
1548
- lines.push("### Multi-Column Sorting");
1549
- lines.push("");
1550
- lines.push("```typescript");
1551
- lines.push("// Sort by multiple columns (all same direction)");
1552
- lines.push("const users = await sdk.users.list({");
1553
- lines.push(" orderBy: ['status', 'created_at'],");
1554
- lines.push(" order: 'desc'");
1555
- lines.push("});");
1556
- lines.push("");
1557
- lines.push("// Different direction per column");
1558
- lines.push("const sorted = await sdk.users.list({");
1559
- lines.push(" orderBy: ['status', 'created_at'],");
1560
- lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
1561
- lines.push("});");
1562
- lines.push("```");
1563
- lines.push("");
1564
- lines.push("### Combining Sorting with Filters");
1565
- lines.push("");
1566
- lines.push("```typescript");
1567
- lines.push("const results = await sdk.users.list({");
1568
- lines.push(" where: {");
1569
- lines.push(" status: 'active',");
1570
- lines.push(" age: { $gte: 18 }");
1571
- lines.push(" },");
1572
- lines.push(" orderBy: 'created_at',");
1573
- lines.push(" order: 'desc',");
1574
- lines.push(" limit: 50,");
1575
- lines.push(" offset: 0");
1576
- lines.push("});");
1577
- lines.push("```");
1578
- lines.push("");
1579
- lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
1580
- lines.push("");
1581
- lines.push("## Vector Search");
1582
- lines.push("");
1583
- lines.push("For tables with `vector` columns (requires pgvector extension), use the `vector` parameter for similarity search:");
1584
- lines.push("");
1585
- lines.push("```typescript");
1586
- lines.push("// Basic similarity search");
1587
- lines.push("const results = await sdk.embeddings.list({");
1588
- lines.push(" vector: {");
1589
- lines.push(" field: 'embedding',");
1590
- lines.push(" query: [0.1, 0.2, 0.3, ...], // Your embedding vector");
1591
- lines.push(" metric: 'cosine' // 'cosine' (default), 'l2', or 'inner'");
1592
- lines.push(" },");
1593
- lines.push(" limit: 10");
1594
- lines.push("});");
1595
- lines.push("");
1596
- lines.push("// Results include _distance field");
1597
- lines.push("results.data[0]._distance; // Similarity distance");
1598
- lines.push("");
1599
- lines.push("// Distance threshold filtering");
1600
- lines.push("const closeMatches = await sdk.embeddings.list({");
1601
- lines.push(" vector: {");
1602
- lines.push(" field: 'embedding',");
1603
- lines.push(" query: queryVector,");
1604
- lines.push(" maxDistance: 0.5 // Only return results within this distance");
1605
- lines.push(" }");
1606
- lines.push("});");
1607
- lines.push("");
1608
- lines.push("// Hybrid search: vector + WHERE filters");
1609
- lines.push("const filtered = await sdk.embeddings.list({");
1610
- lines.push(" vector: { field: 'embedding', query: queryVector },");
1611
- lines.push(" where: {");
1612
- lines.push(" status: 'published',");
1613
- lines.push(" embedding: { $isNot: null }");
1614
- lines.push(" }");
1615
- lines.push("});");
1616
- lines.push("```");
1617
- lines.push("");
1240
+ lines.push(`## Filtering
1241
+
1242
+ Type-safe WHERE clauses. Root-level keys are AND'd; use \`$or\`/\`$and\` for logic (2 levels max).
1243
+
1244
+ \`\`\`typescript
1245
+ await sdk.users.list({
1246
+ where: {
1247
+ status: { $in: ['active', 'pending'] },
1248
+ age: { $gte: 18, $lt: 65 },
1249
+ email: { $ilike: '%@company.com' },
1250
+ deleted_at: { $is: null },
1251
+ meta: { $jsonbContains: { tag: 'vip' } },
1252
+ $or: [{ role: 'admin' }, { role: 'mod' }]
1253
+ }
1254
+ });
1255
+ \`\`\`
1256
+
1257
+ | Operator | SQL | Types |
1258
+ |----------|-----|-------|
1259
+ | \`$eq\` \`$ne\` | = ≠ | All |
1260
+ | \`$gt\` \`$gte\` \`$lt\` \`$lte\` | > ≥ < ≤ | Number, Date |
1261
+ | \`$in\` \`$nin\` | IN / NOT IN | All |
1262
+ | \`$like\` \`$ilike\` | LIKE / ILIKE | String |
1263
+ | \`$is\` \`$isNot\` | IS NULL / IS NOT NULL | Nullable |
1264
+ | \`$jsonbContains\` \`$jsonbContainedBy\` \`$jsonbHasKey\` \`$jsonbHasAnyKeys\` \`$jsonbHasAllKeys\` \`$jsonbPath\` | JSONB ops | JSONB |
1265
+ | \`$or\` \`$and\` | OR / AND (2 levels) | — |
1266
+
1267
+ ## Sorting
1268
+
1269
+ \`orderBy\` accepts a column name or array; \`order\` accepts \`'asc'\`/\`'desc'\` or a per-column array.
1270
+
1271
+ \`\`\`typescript
1272
+ await sdk.users.list({ orderBy: ['status', 'created_at'], order: ['asc', 'desc'] });
1273
+ \`\`\`
1274
+
1275
+ ## Vector Search
1276
+
1277
+ For tables with \`vector\` columns (requires pgvector). Results include a \`_distance\` field.
1278
+
1279
+ \`\`\`typescript
1280
+ const results = await sdk.embeddings.list({
1281
+ vector: { field: 'embedding', query: [0.1, 0.2, 0.3, /* ... */], metric: 'cosine', maxDistance: 0.5 },
1282
+ where: { status: 'published' },
1283
+ limit: 10
1284
+ }); // results.data[0]._distance
1285
+ \`\`\`
1286
+ `);
1618
1287
  lines.push("## Resources");
1619
1288
  lines.push("");
1620
1289
  for (const resource of contract.resources) {
@@ -1634,10 +1303,12 @@ function generateUnifiedContractMarkdown(contract) {
1634
1303
  lines.push(`- API: \`${method.correspondsTo}\``);
1635
1304
  }
1636
1305
  lines.push("");
1637
- lines.push("```typescript");
1638
- lines.push(method.example);
1639
- lines.push("```");
1640
- lines.push("");
1306
+ if (method.example) {
1307
+ lines.push("```typescript");
1308
+ lines.push(method.example);
1309
+ lines.push("```");
1310
+ lines.push("");
1311
+ }
1641
1312
  }
1642
1313
  lines.push("#### API Endpoints");
1643
1314
  lines.push("");
@@ -1671,23 +1342,14 @@ function generateUnifiedContractMarkdown(contract) {
1671
1342
  }
1672
1343
  lines.push("");
1673
1344
  }
1674
- lines.push("## Type Imports");
1675
- lines.push("");
1676
- lines.push("```typescript");
1677
- lines.push("// Import SDK and types");
1678
- lines.push("import { SDK } from './client';");
1679
- lines.push("");
1680
- lines.push("// Import types for a specific table");
1681
- lines.push("import type {");
1682
- lines.push(" SelectTableName, // Full record type");
1683
- lines.push(" InsertTableName, // Create payload type");
1684
- lines.push(" UpdateTableName // Update payload type");
1685
- lines.push("} from './client/types/table_name';");
1686
- lines.push("");
1687
- lines.push("// Import all types");
1688
- lines.push("import type * as Types from './client/types';");
1689
- lines.push("```");
1690
- lines.push("");
1345
+ lines.push(`## Type Imports
1346
+
1347
+ \`\`\`typescript
1348
+ import { SDK } from './client';
1349
+ import type { SelectTableName, InsertTableName, UpdateTableName } from './client/types/table_name';
1350
+ import type * as Types from './client/types';
1351
+ \`\`\`
1352
+ `);
1691
1353
  return lines.join(`
1692
1354
  `);
1693
1355
  }
@@ -2312,7 +1974,8 @@ const deleteSchema = z.object({
2312
1974
 
2313
1975
  const getByPkQuerySchema = z.object({
2314
1976
  select: z.array(z.string()).min(1).optional(),
2315
- exclude: z.array(z.string()).min(1).optional()
1977
+ exclude: z.array(z.string()).min(1).optional(),
1978
+ includeSoftDeleted: z.boolean().optional()
2316
1979
  }).strict().refine(
2317
1980
  (data) => !(data.select && data.exclude),
2318
1981
  { message: "Cannot specify both 'select' and 'exclude' parameters" }
@@ -2327,7 +1990,8 @@ const listSchema = z.object({
2327
1990
  offset: z.number().int().min(0).optional(),
2328
1991
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
2329
1992
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
2330
- distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional(),${hasVectorColumns ? `
1993
+ distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional(),
1994
+ includeSoftDeleted: z.boolean().optional(),${hasVectorColumns ? `
2331
1995
  vector: z.object({
2332
1996
  field: z.string(),
2333
1997
  query: z.array(z.number()),
@@ -2424,12 +2088,15 @@ ${hasAuth ? `
2424
2088
  app.get(\`\${base}/${pkPath}\`, async (c) => {
2425
2089
  ${getPkParams}
2426
2090
 
2427
- // Parse query params for select/exclude
2091
+ // Parse query params coerce includeSoftDeleted string to boolean before Zod validation
2092
+ // (avoids Boolean("false")=true pitfall while keeping schema as single source of truth)
2428
2093
  const selectParam = c.req.query("select");
2429
2094
  const excludeParam = c.req.query("exclude");
2095
+ const includeSoftDeletedParam = c.req.query("includeSoftDeleted");
2430
2096
  const queryData: any = {};
2431
2097
  if (selectParam) queryData.select = selectParam.split(",");
2432
2098
  if (excludeParam) queryData.exclude = excludeParam.split(",");
2099
+ if (includeSoftDeletedParam !== undefined) queryData.includeSoftDeleted = includeSoftDeletedParam === "true";
2433
2100
 
2434
2101
  const queryParsed = getByPkQuerySchema.safeParse(queryData);
2435
2102
  if (!queryParsed.success) {
@@ -2442,7 +2109,7 @@ ${hasAuth ? `
2442
2109
  }
2443
2110
 
2444
2111
  const ctx = { ...baseCtx, select: queryParsed.data.select, exclude: queryParsed.data.exclude };
2445
- const result = await coreOps.getByPk(ctx, pkValues);
2112
+ const result = await coreOps.getByPk(ctx, pkValues, { includeSoftDeleted: queryParsed.data.includeSoftDeleted });
2446
2113
 
2447
2114
  if (result.error) {
2448
2115
  return c.json({ error: result.error }, result.status as any);
@@ -2948,14 +2615,14 @@ ${hasJsonbColumns ? ` /**
2948
2615
  * @param options - Select specific fields to return
2949
2616
  * @returns The record with only selected fields if found, null otherwise
2950
2617
  */
2951
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
2618
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
2952
2619
  /**
2953
2620
  * Get a ${table.name} record by primary key with field exclusion
2954
2621
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2955
2622
  * @param options - Exclude specific fields from return
2956
2623
  * @returns The record without excluded fields if found, null otherwise
2957
2624
  */
2958
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
2625
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
2959
2626
  /**
2960
2627
  * Get a ${table.name} record by primary key
2961
2628
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
@@ -2964,15 +2631,16 @@ ${hasJsonbColumns ? ` /**
2964
2631
  * // With JSONB type override:
2965
2632
  * const user = await client.getByPk<{ metadata: Metadata }>('user-id');
2966
2633
  */
2967
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type}<TJsonb> | null>;
2634
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type}<TJsonb> | null>;
2968
2635
  async getByPk<TJsonb extends Partial<Select${Type}> = {}>(
2969
2636
  pk: ${pkType},
2970
- options?: { select?: string[]; exclude?: string[] }
2637
+ options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
2971
2638
  ): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>> | null> {
2972
2639
  const path = ${pkPathExpr};
2973
2640
  const queryParams = new URLSearchParams();
2974
2641
  if (options?.select) queryParams.set('select', options.select.join(','));
2975
2642
  if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
2643
+ if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
2976
2644
  const query = queryParams.toString();
2977
2645
  const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
2978
2646
  return this.get<Select${Type}<TJsonb> | null>(url);
@@ -2982,28 +2650,29 @@ ${hasJsonbColumns ? ` /**
2982
2650
  * @param options - Select specific fields to return
2983
2651
  * @returns The record with only selected fields if found, null otherwise
2984
2652
  */
2985
- async getByPk(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}> | null>;
2653
+ async getByPk(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
2986
2654
  /**
2987
2655
  * Get a ${table.name} record by primary key with field exclusion
2988
2656
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2989
2657
  * @param options - Exclude specific fields from return
2990
2658
  * @returns The record without excluded fields if found, null otherwise
2991
2659
  */
2992
- async getByPk(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}> | null>;
2660
+ async getByPk(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
2993
2661
  /**
2994
2662
  * Get a ${table.name} record by primary key
2995
2663
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
2996
2664
  * @returns The record with all fields if found, null otherwise
2997
2665
  */
2998
- async getByPk(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type} | null>;
2666
+ async getByPk(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type} | null>;
2999
2667
  async getByPk(
3000
2668
  pk: ${pkType},
3001
- options?: { select?: string[]; exclude?: string[] }
2669
+ options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
3002
2670
  ): Promise<Select${Type} | Partial<Select${Type}> | null> {
3003
2671
  const path = ${pkPathExpr};
3004
2672
  const queryParams = new URLSearchParams();
3005
2673
  if (options?.select) queryParams.set('select', options.select.join(','));
3006
2674
  if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
2675
+ if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
3007
2676
  const query = queryParams.toString();
3008
2677
  const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
3009
2678
  return this.get<Select${Type} | null>(url);
@@ -3030,6 +2699,7 @@ ${hasJsonbColumns ? ` /**
3030
2699
  orderBy?: string | string[];
3031
2700
  order?: "asc" | "desc" | ("asc" | "desc")[];
3032
2701
  distinctOn?: string | string[];
2702
+ includeSoftDeleted?: boolean;
3033
2703
  }): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3034
2704
  /**
3035
2705
  * List ${table.name} records with field exclusion
@@ -3052,6 +2722,7 @@ ${hasJsonbColumns ? ` /**
3052
2722
  orderBy?: string | string[];
3053
2723
  order?: "asc" | "desc" | ("asc" | "desc")[];
3054
2724
  distinctOn?: string | string[];
2725
+ includeSoftDeleted?: boolean;
3055
2726
  }): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3056
2727
  /**
3057
2728
  * List ${table.name} records with pagination and filtering
@@ -3085,6 +2756,7 @@ ${hasJsonbColumns ? ` /**
3085
2756
  orderBy?: string | string[];
3086
2757
  order?: "asc" | "desc" | ("asc" | "desc")[];
3087
2758
  distinctOn?: string | string[];
2759
+ includeSoftDeleted?: boolean;
3088
2760
  }): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3089
2761
  async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
3090
2762
  include?: ${Type}IncludeSpec;
@@ -3103,6 +2775,7 @@ ${hasJsonbColumns ? ` /**
3103
2775
  orderBy?: string | string[];
3104
2776
  order?: "asc" | "desc" | ("asc" | "desc")[];
3105
2777
  distinctOn?: string | string[];
2778
+ includeSoftDeleted?: boolean;
3106
2779
  }): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
3107
2780
  return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
3108
2781
  }` : ` /**
@@ -3126,6 +2799,7 @@ ${hasJsonbColumns ? ` /**
3126
2799
  orderBy?: string | string[];
3127
2800
  order?: "asc" | "desc" | ("asc" | "desc")[];
3128
2801
  distinctOn?: string | string[];
2802
+ includeSoftDeleted?: boolean;
3129
2803
  }): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3130
2804
  /**
3131
2805
  * List ${table.name} records with field exclusion
@@ -3148,6 +2822,7 @@ ${hasJsonbColumns ? ` /**
3148
2822
  orderBy?: string | string[];
3149
2823
  order?: "asc" | "desc" | ("asc" | "desc")[];
3150
2824
  distinctOn?: string | string[];
2825
+ includeSoftDeleted?: boolean;
3151
2826
  }): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3152
2827
  /**
3153
2828
  * List ${table.name} records with pagination and filtering
@@ -3175,6 +2850,7 @@ ${hasJsonbColumns ? ` /**
3175
2850
  orderBy?: string | string[];
3176
2851
  order?: "asc" | "desc" | ("asc" | "desc")[];
3177
2852
  distinctOn?: string | string[];
2853
+ includeSoftDeleted?: boolean;
3178
2854
  }): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3179
2855
  async list(params?: {
3180
2856
  include?: ${Type}IncludeSpec;
@@ -3193,6 +2869,7 @@ ${hasJsonbColumns ? ` /**
3193
2869
  orderBy?: string | string[];
3194
2870
  order?: "asc" | "desc" | ("asc" | "desc")[];
3195
2871
  distinctOn?: string | string[];
2872
+ includeSoftDeleted?: boolean;
3196
2873
  }): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
3197
2874
  return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
3198
2875
  }`}
@@ -3602,7 +3279,8 @@ export abstract class BaseClient {
3602
3279
  }
3603
3280
 
3604
3281
  // src/emit-include-loader.ts
3605
- function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
3282
+ function emitIncludeLoader(model, maxDepth, opts = {}) {
3283
+ const { softDeleteCols = {}, useJsExtensions } = opts;
3606
3284
  const fkIndex = {};
3607
3285
  for (const t of Object.values(model.tables)) {
3608
3286
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
@@ -3641,8 +3319,18 @@ const log = {
3641
3319
  };
3642
3320
 
3643
3321
  // Helpers for PK/FK discovery from model (inlined)
3644
- const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)} as const;
3645
- const PKS = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)} as const;
3322
+ const FK_INDEX: Record<string, Array<{from: string[]; toTable: string; to: string[]}>> = ${JSON.stringify(fkIndex, null, 2)};
3323
+ const PKS: Record<string, string[]> = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)};
3324
+ /** Soft-delete column per table, null if not applicable. Baked in at generation time. */
3325
+ const SOFT_DELETE_COLS: Record<string, string | null> = ${JSON.stringify(softDeleteCols, null, 2)};
3326
+
3327
+ /** Returns a SQL fragment like ' AND "col" IS NULL' for the given table, or "" if none. */
3328
+ function softDeleteFilter(table: string, alias?: string): string {
3329
+ const col = SOFT_DELETE_COLS[table];
3330
+ if (!col) return "";
3331
+ const ref = alias ? \`\${alias}."\${col}"\` : \`"\${col}"\`;
3332
+ return \` AND \${ref} IS NULL\`;
3333
+ }
3646
3334
 
3647
3335
  // Build WHERE predicate for OR-of-AND on composite values
3648
3336
  function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
@@ -3732,6 +3420,49 @@ function filterFields<T extends Record<string, any>>(
3732
3420
  });
3733
3421
  }
3734
3422
 
3423
+ /** Sentinel used as window-function LIMIT when no per-parent limit is requested. */
3424
+ const NO_LIMIT = 999_999_999;
3425
+
3426
+ /**
3427
+ * FK_INDEX and PKS are fully populated at code-gen time for all schema tables,
3428
+ * so lookups by TableName are always defined. These helpers assert non-null
3429
+ * in one place rather than scattering \`!\` or \`as any\` at every call site.
3430
+ */
3431
+ function fksOf(table: string): Array<{from: string[]; toTable: string; to: string[]}> {
3432
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3433
+ return FK_INDEX[table]!;
3434
+ }
3435
+ function pkOf(table: string): string[] {
3436
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3437
+ return PKS[table]!;
3438
+ }
3439
+
3440
+ /** Parse relation options and nested child spec from a specValue entry. */
3441
+ function parseSpecOptions(specValue: any): { options: RelationOptions; childSpec: any } {
3442
+ const options: RelationOptions = {};
3443
+ let childSpec: any = undefined;
3444
+ if (specValue && typeof specValue === "object" && specValue !== true) {
3445
+ if (specValue.select !== undefined) options.select = specValue.select;
3446
+ if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3447
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
3448
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
3449
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3450
+ if (specValue.order !== undefined) options.order = specValue.order;
3451
+ if (specValue.include !== undefined) {
3452
+ childSpec = specValue.include;
3453
+ } else {
3454
+ // Support legacy format: { tags: true } alongside new: { include: { tags: true } }
3455
+ const nonOptionKeys = Object.keys(specValue).filter(
3456
+ k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3457
+ );
3458
+ if (nonOptionKeys.length > 0) {
3459
+ childSpec = Object.fromEntries(nonOptionKeys.map(k => [k, specValue[k]]));
3460
+ }
3461
+ }
3462
+ }
3463
+ return { options, childSpec };
3464
+ }
3465
+
3735
3466
  // Public entry
3736
3467
  export async function loadIncludes(
3737
3468
  root: TableName,
@@ -3771,37 +3502,7 @@ export async function loadIncludes(
3771
3502
  // Safely run each loader; never let one bad edge 500 the route
3772
3503
  if (rel.via) {
3773
3504
  // M:N via junction
3774
- const specValue = s[key];
3775
- const options: RelationOptions = {};
3776
- let childSpec: any = undefined;
3777
-
3778
- if (specValue && typeof specValue === "object" && specValue !== true) {
3779
- // Extract options
3780
- if (specValue.select !== undefined) options.select = specValue.select;
3781
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3782
- if (specValue.limit !== undefined) options.limit = specValue.limit;
3783
- if (specValue.offset !== undefined) options.offset = specValue.offset;
3784
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3785
- if (specValue.order !== undefined) options.order = specValue.order;
3786
-
3787
- // Extract nested spec - support both formats:
3788
- // New: { limit: 3, include: { tags: true } }
3789
- // Old: { tags: true } (backward compatibility)
3790
- if (specValue.include !== undefined) {
3791
- childSpec = specValue.include;
3792
- } else {
3793
- // Build childSpec from non-option keys
3794
- const nonOptionKeys = Object.keys(specValue).filter(
3795
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3796
- );
3797
- if (nonOptionKeys.length > 0) {
3798
- childSpec = {};
3799
- for (const k of nonOptionKeys) {
3800
- childSpec[k] = specValue[k];
3801
- }
3802
- }
3803
- }
3804
- }
3505
+ const { options, childSpec } = parseSpecOptions(s[key]);
3805
3506
 
3806
3507
  try {
3807
3508
  await loadManyToMany(table, target, rel.via as string, rows, key, options);
@@ -3823,37 +3524,7 @@ export async function loadIncludes(
3823
3524
 
3824
3525
  if (rel.kind === "many") {
3825
3526
  // 1:N target has FK to current
3826
- const specValue = s[key];
3827
- const options: RelationOptions = {};
3828
- let childSpec: any = undefined;
3829
-
3830
- if (specValue && typeof specValue === "object" && specValue !== true) {
3831
- // Extract options
3832
- if (specValue.select !== undefined) options.select = specValue.select;
3833
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3834
- if (specValue.limit !== undefined) options.limit = specValue.limit;
3835
- if (specValue.offset !== undefined) options.offset = specValue.offset;
3836
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3837
- if (specValue.order !== undefined) options.order = specValue.order;
3838
-
3839
- // Extract nested spec - support both formats:
3840
- // New: { limit: 3, include: { tags: true } }
3841
- // Old: { tags: true } (backward compatibility)
3842
- if (specValue.include !== undefined) {
3843
- childSpec = specValue.include;
3844
- } else {
3845
- // Build childSpec from non-option keys
3846
- const nonOptionKeys = Object.keys(specValue).filter(
3847
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3848
- );
3849
- if (nonOptionKeys.length > 0) {
3850
- childSpec = {};
3851
- for (const k of nonOptionKeys) {
3852
- childSpec[k] = specValue[k];
3853
- }
3854
- }
3855
- }
3856
- }
3527
+ const { options, childSpec } = parseSpecOptions(s[key]);
3857
3528
 
3858
3529
  try {
3859
3530
  await loadOneToMany(table, target, rows, key, options);
@@ -3873,20 +3544,11 @@ export async function loadIncludes(
3873
3544
  } else {
3874
3545
  // kind === "one"
3875
3546
  // Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
3876
- const specValue = s[key];
3877
- const options: RelationOptions = {};
3878
- let childSpec: any = undefined;
3879
-
3880
- if (specValue && typeof specValue === "object" && specValue !== true) {
3881
- if (specValue.select !== undefined) options.select = specValue.select;
3882
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3883
- // Support { include: TargetIncludeSpec } — mirrors the many/via handler
3884
- if (specValue.include !== undefined) {
3885
- childSpec = specValue.include;
3886
- }
3887
- }
3547
+ // Destructure only select/exclude/childSpec — limit/offset/orderBy don't apply to 1:1 relations
3548
+ const { options: { select, exclude }, childSpec } = parseSpecOptions(s[key]);
3549
+ const options: RelationOptions = { select, exclude };
3888
3550
 
3889
- const currFks = (FK_INDEX as any)[table] as Array<{from:string[];toTable:string;to:string[]}>;
3551
+ const currFks = fksOf(table);
3890
3552
  const toTarget = currFks.find(f => f.toTable === target);
3891
3553
  if (toTarget) {
3892
3554
  try {
@@ -3918,16 +3580,16 @@ export async function loadIncludes(
3918
3580
 
3919
3581
  async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3920
3582
  // current has FK cols referencing target PK
3921
- const fk = (FK_INDEX as any)[curr].find((f: any) => f.toTable === target);
3583
+ const fk = fksOf(curr).find(f => f.toTable === target);
3922
3584
  if (!fk) { for (const r of rows) r[key] = null; return; }
3923
3585
  const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
3924
3586
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
3925
3587
 
3926
3588
  // Query target WHERE target.pk IN tuples
3927
- const pkCols = (PKS as any)[target] as string[];
3589
+ const pkCols = pkOf(target);
3928
3590
  const where = buildOrAndPredicate(pkCols, tuples.length, 1);
3929
3591
  const params = tuples.flat();
3930
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3592
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
3931
3593
  log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
3932
3594
  const { rows: targets } = await pg.query(sql, params);
3933
3595
 
@@ -3944,17 +3606,17 @@ export async function loadIncludes(
3944
3606
 
3945
3607
  async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3946
3608
  // target has FK cols referencing current PK (unique)
3947
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3609
+ const fk = fksOf(target).find(f => f.toTable === curr);
3948
3610
  if (!fk) { for (const r of rows) r[key] = null; return; }
3949
3611
 
3950
- const pkCols = (PKS as any)[curr] as string[];
3612
+ const pkCols = pkOf(curr);
3951
3613
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
3952
3614
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
3953
3615
 
3954
3616
  // SELECT target WHERE fk IN tuples
3955
3617
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
3956
3618
  const params = tuples.flat();
3957
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3619
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
3958
3620
  log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
3959
3621
  const { rows: targets } = await pg.query(sql, params);
3960
3622
 
@@ -3970,10 +3632,10 @@ export async function loadIncludes(
3970
3632
 
3971
3633
  async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3972
3634
  // target has FK cols referencing current PK
3973
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3635
+ const fk = fksOf(target).find(f => f.toTable === curr);
3974
3636
  if (!fk) { for (const r of rows) r[key] = []; return; }
3975
3637
 
3976
- const pkCols = (PKS as any)[curr] as string[];
3638
+ const pkCols = pkOf(curr);
3977
3639
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
3978
3640
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
3979
3641
 
@@ -3981,7 +3643,7 @@ export async function loadIncludes(
3981
3643
  const params = tuples.flat();
3982
3644
 
3983
3645
  // Build SQL with optional ORDER BY, LIMIT, OFFSET
3984
- let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3646
+ let sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
3985
3647
 
3986
3648
  // If limit/offset are needed, use window functions to limit per parent
3987
3649
  if (options.limit !== undefined || options.offset !== undefined) {
@@ -3991,13 +3653,13 @@ export async function loadIncludes(
3991
3653
 
3992
3654
  const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
3993
3655
  const offset = options.offset ?? 0;
3994
- const limit = options.limit ?? 999999999;
3656
+ const limit = options.limit ?? NO_LIMIT;
3995
3657
 
3996
3658
  sql = \`
3997
3659
  SELECT * FROM (
3998
3660
  SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
3999
3661
  FROM "\${target}"
4000
- WHERE \${where}
3662
+ WHERE \${where}\${softDeleteFilter(target)}
4001
3663
  ) __sub
4002
3664
  WHERE __rn > \${offset} AND __rn <= \${offset + limit}
4003
3665
  \`;
@@ -4027,11 +3689,11 @@ export async function loadIncludes(
4027
3689
 
4028
3690
  async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
4029
3691
  // via has two FKs: one to curr, one to target
4030
- const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
4031
- const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
3692
+ const toCurr = fksOf(via).find(f => f.toTable === curr);
3693
+ const toTarget = fksOf(via).find(f => f.toTable === target);
4032
3694
  if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
4033
3695
 
4034
- const pkCols = (PKS as any)[curr] as string[];
3696
+ const pkCols = pkOf(curr);
4035
3697
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4036
3698
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4037
3699
 
@@ -4046,9 +3708,9 @@ export async function loadIncludes(
4046
3708
 
4047
3709
  const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
4048
3710
  const offset = options.offset ?? 0;
4049
- const limit = options.limit ?? 999999999;
3711
+ const limit = options.limit ?? NO_LIMIT;
4050
3712
 
4051
- const targetPkCols = (PKS as any)[target] as string[];
3713
+ const targetPkCols = pkOf(target);
4052
3714
  const joinConditions = toTarget.from.map((jCol: string, i: number) => {
4053
3715
  return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
4054
3716
  }).join(' AND ');
@@ -4061,7 +3723,7 @@ export async function loadIncludes(
4061
3723
  \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
4062
3724
  FROM "\${via}" j
4063
3725
  INNER JOIN "\${target}" t ON \${joinConditions}
4064
- WHERE \${whereVia}
3726
+ WHERE \${whereVia}\${softDeleteFilter(target, "t")}
4065
3727
  ) __numbered
4066
3728
  WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
4067
3729
  \`;
@@ -4104,8 +3766,8 @@ export async function loadIncludes(
4104
3766
 
4105
3767
  // 2) Load targets by distinct target fk tuples in junction
4106
3768
  const tTuples = distinctTuples(jrows, toTarget.from);
4107
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
4108
- const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
3769
+ const whereT = buildOrAndPredicate(pkOf(target), tTuples.length, 1);
3770
+ const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\${softDeleteFilter(target)}\`;
4109
3771
  const paramsT = tTuples.flat();
4110
3772
  log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
4111
3773
  const { rows: targets } = await pg.query(sqlT, paramsT);
@@ -4113,7 +3775,7 @@ export async function loadIncludes(
4113
3775
  // Apply select/exclude filtering
4114
3776
  const filteredTargets = filterFields(targets, options.select, options.exclude);
4115
3777
 
4116
- const tIdx = indexByTuple(filteredTargets, (PKS as any)[target]);
3778
+ const tIdx = indexByTuple(filteredTargets, pkOf(target));
4117
3779
 
4118
3780
  // 3) Group junction rows by current pk tuple, map to target rows
4119
3781
  const byCurr = groupByTuple(jrows, toCurr.from);
@@ -5019,7 +4681,8 @@ export async function createRecord(
5019
4681
  */
5020
4682
  export async function getByPk(
5021
4683
  ctx: OperationContext,
5022
- pkValues: any[]
4684
+ pkValues: any[],
4685
+ opts?: { includeSoftDeleted?: boolean }
5023
4686
  ): Promise<{ data?: any; error?: string; status: number }> {
5024
4687
  try {
5025
4688
  const hasCompositePk = ctx.pkColumns.length > 1;
@@ -5028,7 +4691,8 @@ export async function getByPk(
5028
4691
  : \`"\${ctx.pkColumns[0]}" = $1\`;
5029
4692
 
5030
4693
  const columns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
5031
- const text = \`SELECT \${columns} FROM "\${ctx.table}" WHERE \${wherePkSql} LIMIT 1\`;
4694
+ const softDeleteFilter = ctx.softDeleteColumn && !opts?.includeSoftDeleted ? \` AND "\${ctx.softDeleteColumn}" IS NULL\` : "";
4695
+ const text = \`SELECT \${columns} FROM "\${ctx.table}" WHERE \${wherePkSql}\${softDeleteFilter} LIMIT 1\`;
5032
4696
  log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
5033
4697
 
5034
4698
  const { rows } = await ctx.pg.query(text, pkValues);
@@ -5401,6 +5065,7 @@ export async function listRecords(
5401
5065
  orderBy?: string | string[];
5402
5066
  order?: "asc" | "desc" | ("asc" | "desc")[];
5403
5067
  distinctOn?: string | string[];
5068
+ includeSoftDeleted?: boolean;
5404
5069
  vector?: {
5405
5070
  field: string;
5406
5071
  query: number[];
@@ -5411,7 +5076,7 @@ export async function listRecords(
5411
5076
  }
5412
5077
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
5413
5078
  try {
5414
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
5079
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
5415
5080
 
5416
5081
  // DISTINCT ON support
5417
5082
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -5447,8 +5112,8 @@ export async function listRecords(
5447
5112
  const whereParts: string[] = [];
5448
5113
  let whereParams: any[] = [];
5449
5114
 
5450
- // Add soft delete filter if applicable
5451
- if (ctx.softDeleteColumn) {
5115
+ // Add soft delete filter if applicable (skip if caller opts into seeing soft-deleted records)
5116
+ if (ctx.softDeleteColumn && !includeSoftDeleted) {
5452
5117
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
5453
5118
  }
5454
5119
 
@@ -5486,7 +5151,7 @@ export async function listRecords(
5486
5151
 
5487
5152
  if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
5488
5153
  const countWhereParts: string[] = [];
5489
- if (ctx.softDeleteColumn) {
5154
+ if (ctx.softDeleteColumn && !includeSoftDeleted) {
5490
5155
  countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
5491
5156
  }
5492
5157
  if (whereClause) {
@@ -6457,6 +6122,12 @@ init_utils();
6457
6122
  var __filename2 = fileURLToPath(import.meta.url);
6458
6123
  var __dirname2 = dirname2(__filename2);
6459
6124
  var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
6125
+ function resolveSoftDeleteColumn(cfg, tableName) {
6126
+ const overrides = cfg.softDeleteColumnOverrides;
6127
+ if (overrides && tableName in overrides)
6128
+ return overrides[tableName] ?? null;
6129
+ return cfg.softDeleteColumn ?? null;
6130
+ }
6460
6131
  async function generate(configPath) {
6461
6132
  if (!existsSync2(configPath)) {
6462
6133
  throw new Error(`Config file not found: ${configPath}
@@ -6528,9 +6199,14 @@ async function generate(configPath) {
6528
6199
  path: join2(serverDir, "include-builder.ts"),
6529
6200
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
6530
6201
  });
6202
+ const softDeleteCols = Object.fromEntries(Object.values(model.tables).map((t) => {
6203
+ const col = resolveSoftDeleteColumn(cfg, t.name);
6204
+ const validated = col && t.columns.some((c) => c.name === col) ? col : null;
6205
+ return [t.name, validated];
6206
+ }));
6531
6207
  files.push({
6532
6208
  path: join2(serverDir, "include-loader.ts"),
6533
- content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
6209
+ content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
6534
6210
  });
6535
6211
  files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
6536
6212
  if (getAuthStrategy(normalizedAuth) !== "none") {
@@ -6556,7 +6232,7 @@ async function generate(configPath) {
6556
6232
  let routeContent;
6557
6233
  if (serverFramework === "hono") {
6558
6234
  routeContent = emitHonoRoutes(table, graph, {
6559
- softDeleteColumn: cfg.softDeleteColumn || null,
6235
+ softDeleteColumn: softDeleteCols[table.name] ?? null,
6560
6236
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
6561
6237
  authStrategy: getAuthStrategy(normalizedAuth),
6562
6238
  useJsExtensions: cfg.useJsExtensions,
@@ -6709,5 +6385,6 @@ async function generate(configPath) {
6709
6385
  console.log(` Then run: postgresdk pull`);
6710
6386
  }
6711
6387
  export {
6388
+ resolveSoftDeleteColumn,
6712
6389
  generate
6713
6390
  };