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/cli.js CHANGED
@@ -750,7 +750,7 @@ __export(exports_emit_sdk_contract, {
750
750
  function generateUnifiedContract(model, config, graph) {
751
751
  const resources = [];
752
752
  const relationships = [];
753
- const tables = model && model.tables ? Object.values(model.tables) : [];
753
+ const tables = Object.values(model.tables);
754
754
  if (process.env.SDK_DEBUG) {
755
755
  console.log(`[SDK Contract] Processing ${tables.length} tables`);
756
756
  }
@@ -880,30 +880,23 @@ function generateResourceWithSDK(table, model, graph, config) {
880
880
  const hasSinglePK = table.pk.length === 1;
881
881
  const pkField = hasSinglePK ? table.pk[0] : "id";
882
882
  const enums = model.enums || {};
883
+ const overrides = config?.softDeleteColumnOverrides;
884
+ const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.softDeleteColumn ?? null;
885
+ const softDeleteCol = resolvedSoftDeleteCol && table.columns.some((c) => c.name === resolvedSoftDeleteCol) ? resolvedSoftDeleteCol : null;
883
886
  const sdkMethods = [];
884
887
  const endpoints = [];
885
888
  sdkMethods.push({
886
889
  name: "list",
887
890
  signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
888
891
  description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
889
- example: `// Get all ${tableName}
890
- const result = await sdk.${tableName}.list();
891
- console.log(result.data); // array of records
892
- console.log(result.total); // total matching records
893
- console.log(result.hasMore); // true if more pages available
894
-
895
- // With filters and pagination
896
- const filtered = await sdk.${tableName}.list({
897
- limit: 20,
898
- offset: 0,
899
- where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
892
+ example: `const result = await sdk.${tableName}.list({
893
+ where: { ${table.columns[0]?.name || "id"}: { $ilike: '%value%' } },
900
894
  orderBy: '${table.columns[0]?.name || "created_at"}',
901
- order: 'desc'
902
- });
903
-
904
- // Calculate total pages
905
- const totalPages = Math.ceil(filtered.total / filtered.limit);
906
- const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
895
+ order: 'desc',
896
+ limit: 20,
897
+ offset: 0${softDeleteCol ? `,
898
+ includeSoftDeleted: false` : ""}
899
+ }); // result.data, result.total, result.hasMore`,
907
900
  correspondsTo: `GET ${basePath}`
908
901
  });
909
902
  endpoints.push({
@@ -918,13 +911,8 @@ const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
918
911
  name: "getByPk",
919
912
  signature: `getByPk(${pkField}: string): Promise<${Type} | null>`,
920
913
  description: `Get a single ${tableName} by primary key`,
921
- example: `// Get by ID
922
- const item = await sdk.${tableName}.getByPk('123e4567-e89b-12d3-a456-426614174000');
923
-
924
- // Check if exists
925
- if (item === null) {
926
- console.log('Not found');
927
- }`,
914
+ example: `const item = await sdk.${tableName}.getByPk('id');${softDeleteCol ? `
915
+ const withDeleted = await sdk.${tableName}.getByPk('id', { includeSoftDeleted: true });` : ""} // null if not found`,
928
916
  correspondsTo: `GET ${basePath}/:${pkField}`
929
917
  });
930
918
  endpoints.push({
@@ -938,14 +926,9 @@ if (item === null) {
938
926
  name: "create",
939
927
  signature: `create(data: Insert${Type}): Promise<${Type}>`,
940
928
  description: `Create a new ${tableName}`,
941
- example: `import type { Insert${Type} } from './client/types/${tableName}';
942
-
943
- const newItem: Insert${Type} = {
929
+ example: `const created = await sdk.${tableName}.create({
944
930
  ${generateExampleFields(table, "create")}
945
- };
946
-
947
- const created = await sdk.${tableName}.create(newItem);
948
- console.log('Created:', created.${pkField});`,
931
+ });`,
949
932
  correspondsTo: `POST ${basePath}`
950
933
  });
951
934
  endpoints.push({
@@ -960,13 +943,9 @@ console.log('Created:', created.${pkField});`,
960
943
  name: "update",
961
944
  signature: `update(${pkField}: string, data: Update${Type}): Promise<${Type}>`,
962
945
  description: `Update an existing ${tableName}`,
963
- example: `import type { Update${Type} } from './client/types/${tableName}';
964
-
965
- const updates: Update${Type} = {
946
+ example: `const updated = await sdk.${tableName}.update('id', {
966
947
  ${generateExampleFields(table, "update")}
967
- };
968
-
969
- const updated = await sdk.${tableName}.update('123', updates);`,
948
+ });`,
970
949
  correspondsTo: `PATCH ${basePath}/:${pkField}`
971
950
  });
972
951
  endpoints.push({
@@ -982,8 +961,7 @@ const updated = await sdk.${tableName}.update('123', updates);`,
982
961
  name: "delete",
983
962
  signature: `delete(${pkField}: string): Promise<${Type}>`,
984
963
  description: `Delete a ${tableName}`,
985
- example: `const deleted = await sdk.${tableName}.delete('123');
986
- console.log('Deleted:', deleted);`,
964
+ example: `const deleted = await sdk.${tableName}.delete('id');`,
987
965
  correspondsTo: `DELETE ${basePath}/:${pkField}`
988
966
  });
989
967
  endpoints.push({
@@ -994,29 +972,17 @@ console.log('Deleted:', deleted);`,
994
972
  });
995
973
  }
996
974
  if (graph && config) {
997
- const allTables = model && model.tables ? Object.values(model.tables) : undefined;
975
+ const allTables = Object.values(model.tables);
998
976
  const includeMethods = generateIncludeMethods(table, graph, {
999
977
  maxDepth: config.includeMethodsDepth ?? 2,
1000
978
  skipJunctionTables: config.skipJunctionTables ?? true
1001
979
  }, allTables);
1002
980
  for (const method of includeMethods) {
1003
981
  const isGetByPk = method.name.startsWith("getByPk");
1004
- const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const result = await sdk.${tableName}.${method.name}();
1005
- console.log(result.data); // array of records with includes
1006
- console.log(result.total); // total count
1007
- console.log(result.hasMore); // more pages available
1008
-
1009
- // With filters and pagination
1010
- const filtered = await sdk.${tableName}.${method.name}({
1011
- limit: 20,
1012
- offset: 0,
1013
- where: { /* filter conditions */ }
1014
- });`;
1015
982
  sdkMethods.push({
1016
983
  name: method.name,
1017
984
  signature: `${method.name}(${isGetByPk ? `${pkField}: string` : "params?: ListParams"}): ${method.returnType}`,
1018
985
  description: `Get ${tableName} with included ${method.path.join(", ")} data`,
1019
- example: exampleCall,
1020
986
  correspondsTo: `POST ${basePath}/list`
1021
987
  });
1022
988
  }
@@ -1053,79 +1019,68 @@ function generateFieldContract(column, table, enums) {
1053
1019
  }
1054
1020
  return field;
1055
1021
  }
1022
+ function pgTypeCategory(t) {
1023
+ switch (t) {
1024
+ case "int":
1025
+ case "int2":
1026
+ case "int4":
1027
+ case "int8":
1028
+ case "integer":
1029
+ case "smallint":
1030
+ case "bigint":
1031
+ case "decimal":
1032
+ case "numeric":
1033
+ case "real":
1034
+ case "float4":
1035
+ case "float8":
1036
+ case "double precision":
1037
+ case "float":
1038
+ return "number";
1039
+ case "boolean":
1040
+ case "bool":
1041
+ return "boolean";
1042
+ case "date":
1043
+ case "timestamp":
1044
+ case "timestamptz":
1045
+ return "date";
1046
+ case "json":
1047
+ case "jsonb":
1048
+ return "json";
1049
+ case "uuid":
1050
+ return "uuid";
1051
+ case "text[]":
1052
+ case "varchar[]":
1053
+ case "_text":
1054
+ case "_varchar":
1055
+ return "string[]";
1056
+ case "int[]":
1057
+ case "integer[]":
1058
+ case "_int":
1059
+ case "_int2":
1060
+ case "_int4":
1061
+ case "_int8":
1062
+ case "_integer":
1063
+ return "number[]";
1064
+ case "vector":
1065
+ return "number[]";
1066
+ default:
1067
+ return "string";
1068
+ }
1069
+ }
1056
1070
  function postgresTypeToTsType(column, enums) {
1057
1071
  const pgType = column.pgType.toLowerCase();
1058
1072
  if (enums[pgType]) {
1059
1073
  const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
1060
- if (column.nullable) {
1061
- return `${enumType} | null`;
1062
- }
1063
- return enumType;
1064
- }
1065
- if (pgType.startsWith("_")) {
1066
- const enumName = pgType.slice(1);
1067
- const enumValues = enums[enumName];
1068
- if (enumValues) {
1069
- const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
1070
- const arrayType = `(${enumType})[]`;
1071
- if (column.nullable) {
1072
- return `${arrayType} | null`;
1073
- }
1074
- return arrayType;
1075
- }
1076
- }
1077
- const baseType = (() => {
1078
- switch (pgType) {
1079
- case "int":
1080
- case "int2":
1081
- case "int4":
1082
- case "int8":
1083
- case "integer":
1084
- case "smallint":
1085
- case "bigint":
1086
- case "decimal":
1087
- case "numeric":
1088
- case "real":
1089
- case "float4":
1090
- case "float8":
1091
- case "double precision":
1092
- case "float":
1093
- return "number";
1094
- case "boolean":
1095
- case "bool":
1096
- return "boolean";
1097
- case "date":
1098
- case "timestamp":
1099
- case "timestamptz":
1100
- return "string";
1101
- case "json":
1102
- case "jsonb":
1103
- return "JsonValue";
1104
- case "uuid":
1105
- return "string";
1106
- case "text[]":
1107
- case "varchar[]":
1108
- case "_text":
1109
- case "_varchar":
1110
- return "string[]";
1111
- case "int[]":
1112
- case "integer[]":
1113
- case "_int":
1114
- case "_int2":
1115
- case "_int4":
1116
- case "_int8":
1117
- case "_integer":
1118
- return "number[]";
1119
- case "vector":
1120
- return "number[]";
1121
- default:
1122
- return "string";
1123
- }
1124
- })();
1125
- if (column.nullable) {
1126
- return `${baseType} | null`;
1074
+ return column.nullable ? `${enumType} | null` : enumType;
1075
+ }
1076
+ const enumArrayValues = enums[pgType.slice(1)];
1077
+ if (pgType.startsWith("_") && enumArrayValues) {
1078
+ const arrayType = `(${enumArrayValues.map((v) => `"${v}"`).join(" | ")})[]`;
1079
+ return column.nullable ? `${arrayType} | null` : arrayType;
1127
1080
  }
1128
- return baseType;
1081
+ const cat = pgTypeCategory(pgType);
1082
+ const baseType = cat === "date" || cat === "uuid" ? "string" : cat === "json" ? "JsonValue" : cat;
1083
+ return column.nullable ? `${baseType} | null` : baseType;
1129
1084
  }
1130
1085
  function generateExampleFields(table, operation) {
1131
1086
  const fields = [];
@@ -1224,76 +1179,25 @@ function generateQueryParams(table, enums) {
1224
1179
  }
1225
1180
  function postgresTypeToJsonType(pgType, enums) {
1226
1181
  const t = pgType.toLowerCase();
1227
- if (enums[t]) {
1182
+ if (enums[t])
1228
1183
  return t;
1229
- }
1230
- if (t.startsWith("_") && enums[t.slice(1)]) {
1184
+ if (t.startsWith("_") && enums[t.slice(1)])
1231
1185
  return `${t.slice(1)}[]`;
1232
- }
1233
- switch (t) {
1234
- case "int":
1235
- case "int2":
1236
- case "int4":
1237
- case "int8":
1238
- case "integer":
1239
- case "smallint":
1240
- case "bigint":
1241
- case "decimal":
1242
- case "numeric":
1243
- case "real":
1244
- case "float4":
1245
- case "float8":
1246
- case "double precision":
1247
- case "float":
1248
- return "number";
1249
- case "boolean":
1250
- case "bool":
1251
- return "boolean";
1252
- case "date":
1253
- case "timestamp":
1254
- case "timestamptz":
1255
- return "date/datetime";
1256
- case "json":
1257
- case "jsonb":
1258
- return "object";
1259
- case "uuid":
1260
- return "uuid";
1261
- case "text[]":
1262
- case "varchar[]":
1263
- case "_text":
1264
- case "_varchar":
1265
- return "string[]";
1266
- case "int[]":
1267
- case "integer[]":
1268
- case "_int":
1269
- case "_int2":
1270
- case "_int4":
1271
- case "_int8":
1272
- case "_integer":
1273
- return "number[]";
1274
- case "vector":
1275
- return "number[]";
1276
- default:
1277
- return "string";
1278
- }
1186
+ const cat = pgTypeCategory(t);
1187
+ return cat === "date" ? "date/datetime" : cat === "json" ? "object" : cat;
1279
1188
  }
1280
1189
  function generateFieldDescription(column, table) {
1281
- const descriptions = [];
1282
- if (column.name === "id") {
1283
- descriptions.push("Primary key");
1284
- } else if (column.name === "created_at") {
1285
- descriptions.push("Creation timestamp");
1286
- } else if (column.name === "updated_at") {
1287
- descriptions.push("Last update timestamp");
1288
- } else if (column.name === "deleted_at") {
1289
- descriptions.push("Soft delete timestamp");
1290
- } else if (column.name.endsWith("_id")) {
1291
- const relatedTable = column.name.slice(0, -3);
1292
- descriptions.push(`Foreign key to ${relatedTable}`);
1293
- } else {
1294
- descriptions.push(column.name.replace(/_/g, " "));
1295
- }
1296
- return descriptions.join(", ");
1190
+ if (column.name === "id")
1191
+ return "Primary key";
1192
+ if (column.name === "created_at")
1193
+ return "Creation timestamp";
1194
+ if (column.name === "updated_at")
1195
+ return "Last update timestamp";
1196
+ if (column.name === "deleted_at")
1197
+ return "Soft delete timestamp";
1198
+ if (column.name.endsWith("_id"))
1199
+ return `Foreign key to ${column.name.slice(0, -3)}`;
1200
+ return column.name.replace(/_/g, " ");
1297
1201
  }
1298
1202
  function generateUnifiedContractMarkdown(contract) {
1299
1203
  const lines = [];
@@ -1334,288 +1238,53 @@ function generateUnifiedContractMarkdown(contract) {
1334
1238
  lines.push("");
1335
1239
  }
1336
1240
  }
1337
- lines.push("## Filtering with WHERE Clauses");
1338
- lines.push("");
1339
- lines.push("The SDK provides type-safe WHERE clause filtering with support for various operators.");
1340
- lines.push("");
1341
- lines.push("### Basic Filtering");
1342
- lines.push("");
1343
- lines.push("**Direct equality:**");
1344
- lines.push("");
1345
- lines.push("```typescript");
1346
- lines.push("// Find users with specific email");
1347
- lines.push("const users = await sdk.users.list({");
1348
- lines.push(" where: { email: 'user@example.com' }");
1349
- lines.push("});");
1350
- lines.push("");
1351
- lines.push("// Multiple conditions (AND)");
1352
- lines.push("const activeUsers = await sdk.users.list({");
1353
- lines.push(" where: {");
1354
- lines.push(" status: 'active',");
1355
- lines.push(" role: 'admin'");
1356
- lines.push(" }");
1357
- lines.push("});");
1358
- lines.push("```");
1359
- lines.push("");
1360
- lines.push("### Comparison Operators");
1361
- lines.push("");
1362
- lines.push("Use comparison operators for numeric, date, and other comparable fields:");
1363
- lines.push("");
1364
- lines.push("```typescript");
1365
- lines.push("// Greater than / Less than");
1366
- lines.push("const adults = await sdk.users.list({");
1367
- lines.push(" where: { age: { $gt: 18 } }");
1368
- lines.push("});");
1369
- lines.push("");
1370
- lines.push("// Range queries");
1371
- lines.push("const workingAge = await sdk.users.list({");
1372
- lines.push(" where: {");
1373
- lines.push(" age: { $gte: 18, $lte: 65 }");
1374
- lines.push(" }");
1375
- lines.push("});");
1376
- lines.push("");
1377
- lines.push("// Not equal");
1378
- lines.push("const notPending = await sdk.orders.list({");
1379
- lines.push(" where: { status: { $ne: 'pending' } }");
1380
- lines.push("});");
1381
- lines.push("```");
1382
- lines.push("");
1383
- lines.push("### String Operators");
1384
- lines.push("");
1385
- lines.push("Pattern matching for string fields:");
1386
- lines.push("");
1387
- lines.push("```typescript");
1388
- lines.push("// Case-sensitive LIKE");
1389
- lines.push("const johnsmiths = await sdk.users.list({");
1390
- lines.push(" where: { name: { $like: '%Smith%' } }");
1391
- lines.push("});");
1392
- lines.push("");
1393
- lines.push("// Case-insensitive ILIKE");
1394
- lines.push("const gmailUsers = await sdk.users.list({");
1395
- lines.push(" where: { email: { $ilike: '%@gmail.com' } }");
1396
- lines.push("});");
1397
- lines.push("```");
1398
- lines.push("");
1399
- lines.push("### Array Operators");
1400
- lines.push("");
1401
- lines.push("Filter by multiple possible values:");
1402
- lines.push("");
1403
- lines.push("```typescript");
1404
- lines.push("// IN - match any value in array");
1405
- lines.push("const specificUsers = await sdk.users.list({");
1406
- lines.push(" where: {");
1407
- lines.push(" id: { $in: ['id1', 'id2', 'id3'] }");
1408
- lines.push(" }");
1409
- lines.push("});");
1410
- lines.push("");
1411
- lines.push("// NOT IN - exclude values");
1412
- lines.push("const nonSystemUsers = await sdk.users.list({");
1413
- lines.push(" where: {");
1414
- lines.push(" role: { $nin: ['admin', 'system'] }");
1415
- lines.push(" }");
1416
- lines.push("});");
1417
- lines.push("```");
1418
- lines.push("");
1419
- lines.push("### NULL Checks");
1420
- lines.push("");
1421
- lines.push("Check for null or non-null values:");
1422
- lines.push("");
1423
- lines.push("```typescript");
1424
- lines.push("// IS NULL");
1425
- lines.push("const activeRecords = await sdk.records.list({");
1426
- lines.push(" where: { deleted_at: { $is: null } }");
1427
- lines.push("});");
1428
- lines.push("");
1429
- lines.push("// IS NOT NULL");
1430
- lines.push("const deletedRecords = await sdk.records.list({");
1431
- lines.push(" where: { deleted_at: { $isNot: null } }");
1432
- lines.push("});");
1433
- lines.push("```");
1434
- lines.push("");
1435
- lines.push("### Combining Operators");
1436
- lines.push("");
1437
- lines.push("Mix multiple operators for complex queries:");
1438
- lines.push("");
1439
- lines.push("```typescript");
1440
- lines.push("const filteredUsers = await sdk.users.list({");
1441
- lines.push(" where: {");
1442
- lines.push(" age: { $gte: 18, $lt: 65 },");
1443
- lines.push(" email: { $ilike: '%@company.com' },");
1444
- lines.push(" status: { $in: ['active', 'pending'] },");
1445
- lines.push(" deleted_at: { $is: null }");
1446
- lines.push(" },");
1447
- lines.push(" limit: 50,");
1448
- lines.push(" offset: 0");
1449
- lines.push("});");
1450
- lines.push("```");
1451
- lines.push("");
1452
- lines.push("### Available Operators");
1453
- lines.push("");
1454
- lines.push("| Operator | Description | Example | Types |");
1455
- lines.push("|----------|-------------|---------|-------|");
1456
- lines.push("| `$eq` | Equal to | `{ age: { $eq: 25 } }` | All |");
1457
- lines.push("| `$ne` | Not equal to | `{ status: { $ne: 'inactive' } }` | All |");
1458
- lines.push("| `$gt` | Greater than | `{ price: { $gt: 100 } }` | Number, Date |");
1459
- lines.push("| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | Number, Date |");
1460
- lines.push("| `$lt` | Less than | `{ quantity: { $lt: 10 } }` | Number, Date |");
1461
- lines.push("| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | Number, Date |");
1462
- lines.push("| `$in` | In array | `{ id: { $in: ['a', 'b'] } }` | All |");
1463
- lines.push("| `$nin` | Not in array | `{ role: { $nin: ['admin'] } }` | All |");
1464
- lines.push("| `$like` | Pattern match (case-sensitive) | `{ name: { $like: '%John%' } }` | String |");
1465
- lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
1466
- lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
1467
- lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
1468
- lines.push("| `$jsonbContains` | JSONB contains | `{ metadata: { $jsonbContains: { tags: ['premium'] } } }` | JSONB/JSON |");
1469
- lines.push("| `$jsonbContainedBy` | JSONB contained by | `{ metadata: { $jsonbContainedBy: {...} } }` | JSONB/JSON |");
1470
- lines.push("| `$jsonbHasKey` | JSONB has key | `{ settings: { $jsonbHasKey: 'theme' } }` | JSONB/JSON |");
1471
- lines.push("| `$jsonbHasAnyKeys` | JSONB has any keys | `{ settings: { $jsonbHasAnyKeys: ['dark', 'light'] } }` | JSONB/JSON |");
1472
- lines.push("| `$jsonbHasAllKeys` | JSONB has all keys | `{ config: { $jsonbHasAllKeys: ['api', 'db'] } }` | JSONB/JSON |");
1473
- lines.push("| `$jsonbPath` | JSONB nested value | `{ meta: { $jsonbPath: { path: ['user', 'age'], operator: '$gte', value: 18 } } }` | JSONB/JSON |");
1474
- lines.push("");
1475
- lines.push("### Logical Operators");
1476
- lines.push("");
1477
- lines.push("Combine conditions using `$or` and `$and` (supports 2 levels of nesting):");
1478
- lines.push("");
1479
- lines.push("| Operator | Description | Example |");
1480
- lines.push("|----------|-------------|---------|");
1481
- lines.push("| `$or` | Match any condition | `{ $or: [{ status: 'active' }, { role: 'admin' }] }` |");
1482
- lines.push("| `$and` | Match all conditions (explicit) | `{ $and: [{ age: { $gte: 18 } }, { status: 'verified' }] }` |");
1483
- lines.push("");
1484
- lines.push("```typescript");
1485
- lines.push("// OR - match any condition");
1486
- lines.push("const results = await sdk.users.list({");
1487
- lines.push(" where: {");
1488
- lines.push(" $or: [");
1489
- lines.push(" { email: { $ilike: '%@gmail.com' } },");
1490
- lines.push(" { status: 'premium' }");
1491
- lines.push(" ]");
1492
- lines.push(" }");
1493
- lines.push("});");
1494
- lines.push("");
1495
- lines.push("// Mixed AND + OR (implicit AND at root level)");
1496
- lines.push("const complex = await sdk.users.list({");
1497
- lines.push(" where: {");
1498
- lines.push(" status: 'active', // AND");
1499
- lines.push(" $or: [");
1500
- lines.push(" { age: { $lt: 18 } },");
1501
- lines.push(" { age: { $gt: 65 } }");
1502
- lines.push(" ]");
1503
- lines.push(" }");
1504
- lines.push("});");
1505
- lines.push("");
1506
- lines.push("// Nested (2 levels max)");
1507
- lines.push("const nested = await sdk.users.list({");
1508
- lines.push(" where: {");
1509
- lines.push(" $and: [");
1510
- lines.push(" {");
1511
- lines.push(" $or: [");
1512
- lines.push(" { firstName: { $ilike: '%john%' } },");
1513
- lines.push(" { lastName: { $ilike: '%john%' } }");
1514
- lines.push(" ]");
1515
- lines.push(" },");
1516
- lines.push(" { status: 'active' }");
1517
- lines.push(" ]");
1518
- lines.push(" }");
1519
- lines.push("});");
1520
- lines.push("```");
1521
- lines.push("");
1522
- lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
1523
- lines.push("");
1524
- lines.push("## Sorting");
1525
- lines.push("");
1526
- lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
1527
- lines.push("");
1528
- lines.push("### Single Column Sorting");
1529
- lines.push("");
1530
- lines.push("```typescript");
1531
- lines.push("// Sort by one column ascending");
1532
- lines.push("const users = await sdk.users.list({");
1533
- lines.push(" orderBy: 'created_at',");
1534
- lines.push(" order: 'asc'");
1535
- lines.push("});");
1536
- lines.push("");
1537
- lines.push("// Sort descending");
1538
- lines.push("const latest = await sdk.users.list({");
1539
- lines.push(" orderBy: 'created_at',");
1540
- lines.push(" order: 'desc'");
1541
- lines.push("});");
1542
- lines.push("");
1543
- lines.push("// Order defaults to 'asc' if not specified");
1544
- lines.push("const sorted = await sdk.users.list({");
1545
- lines.push(" orderBy: 'name'");
1546
- lines.push("});");
1547
- lines.push("```");
1548
- lines.push("");
1549
- lines.push("### Multi-Column Sorting");
1550
- lines.push("");
1551
- lines.push("```typescript");
1552
- lines.push("// Sort by multiple columns (all same direction)");
1553
- lines.push("const users = await sdk.users.list({");
1554
- lines.push(" orderBy: ['status', 'created_at'],");
1555
- lines.push(" order: 'desc'");
1556
- lines.push("});");
1557
- lines.push("");
1558
- lines.push("// Different direction per column");
1559
- lines.push("const sorted = await sdk.users.list({");
1560
- lines.push(" orderBy: ['status', 'created_at'],");
1561
- lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
1562
- lines.push("});");
1563
- lines.push("```");
1564
- lines.push("");
1565
- lines.push("### Combining Sorting with Filters");
1566
- lines.push("");
1567
- lines.push("```typescript");
1568
- lines.push("const results = await sdk.users.list({");
1569
- lines.push(" where: {");
1570
- lines.push(" status: 'active',");
1571
- lines.push(" age: { $gte: 18 }");
1572
- lines.push(" },");
1573
- lines.push(" orderBy: 'created_at',");
1574
- lines.push(" order: 'desc',");
1575
- lines.push(" limit: 50,");
1576
- lines.push(" offset: 0");
1577
- lines.push("});");
1578
- lines.push("```");
1579
- lines.push("");
1580
- lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
1581
- lines.push("");
1582
- lines.push("## Vector Search");
1583
- lines.push("");
1584
- lines.push("For tables with `vector` columns (requires pgvector extension), use the `vector` parameter for similarity search:");
1585
- lines.push("");
1586
- lines.push("```typescript");
1587
- lines.push("// Basic similarity search");
1588
- lines.push("const results = await sdk.embeddings.list({");
1589
- lines.push(" vector: {");
1590
- lines.push(" field: 'embedding',");
1591
- lines.push(" query: [0.1, 0.2, 0.3, ...], // Your embedding vector");
1592
- lines.push(" metric: 'cosine' // 'cosine' (default), 'l2', or 'inner'");
1593
- lines.push(" },");
1594
- lines.push(" limit: 10");
1595
- lines.push("});");
1596
- lines.push("");
1597
- lines.push("// Results include _distance field");
1598
- lines.push("results.data[0]._distance; // Similarity distance");
1599
- lines.push("");
1600
- lines.push("// Distance threshold filtering");
1601
- lines.push("const closeMatches = await sdk.embeddings.list({");
1602
- lines.push(" vector: {");
1603
- lines.push(" field: 'embedding',");
1604
- lines.push(" query: queryVector,");
1605
- lines.push(" maxDistance: 0.5 // Only return results within this distance");
1606
- lines.push(" }");
1607
- lines.push("});");
1608
- lines.push("");
1609
- lines.push("// Hybrid search: vector + WHERE filters");
1610
- lines.push("const filtered = await sdk.embeddings.list({");
1611
- lines.push(" vector: { field: 'embedding', query: queryVector },");
1612
- lines.push(" where: {");
1613
- lines.push(" status: 'published',");
1614
- lines.push(" embedding: { $isNot: null }");
1615
- lines.push(" }");
1616
- lines.push("});");
1617
- lines.push("```");
1618
- lines.push("");
1241
+ lines.push(`## Filtering
1242
+
1243
+ Type-safe WHERE clauses. Root-level keys are AND'd; use \`$or\`/\`$and\` for logic (2 levels max).
1244
+
1245
+ \`\`\`typescript
1246
+ await sdk.users.list({
1247
+ where: {
1248
+ status: { $in: ['active', 'pending'] },
1249
+ age: { $gte: 18, $lt: 65 },
1250
+ email: { $ilike: '%@company.com' },
1251
+ deleted_at: { $is: null },
1252
+ meta: { $jsonbContains: { tag: 'vip' } },
1253
+ $or: [{ role: 'admin' }, { role: 'mod' }]
1254
+ }
1255
+ });
1256
+ \`\`\`
1257
+
1258
+ | Operator | SQL | Types |
1259
+ |----------|-----|-------|
1260
+ | \`$eq\` \`$ne\` | = ≠ | All |
1261
+ | \`$gt\` \`$gte\` \`$lt\` \`$lte\` | > ≥ < ≤ | Number, Date |
1262
+ | \`$in\` \`$nin\` | IN / NOT IN | All |
1263
+ | \`$like\` \`$ilike\` | LIKE / ILIKE | String |
1264
+ | \`$is\` \`$isNot\` | IS NULL / IS NOT NULL | Nullable |
1265
+ | \`$jsonbContains\` \`$jsonbContainedBy\` \`$jsonbHasKey\` \`$jsonbHasAnyKeys\` \`$jsonbHasAllKeys\` \`$jsonbPath\` | JSONB ops | JSONB |
1266
+ | \`$or\` \`$and\` | OR / AND (2 levels) | — |
1267
+
1268
+ ## Sorting
1269
+
1270
+ \`orderBy\` accepts a column name or array; \`order\` accepts \`'asc'\`/\`'desc'\` or a per-column array.
1271
+
1272
+ \`\`\`typescript
1273
+ await sdk.users.list({ orderBy: ['status', 'created_at'], order: ['asc', 'desc'] });
1274
+ \`\`\`
1275
+
1276
+ ## Vector Search
1277
+
1278
+ For tables with \`vector\` columns (requires pgvector). Results include a \`_distance\` field.
1279
+
1280
+ \`\`\`typescript
1281
+ const results = await sdk.embeddings.list({
1282
+ vector: { field: 'embedding', query: [0.1, 0.2, 0.3, /* ... */], metric: 'cosine', maxDistance: 0.5 },
1283
+ where: { status: 'published' },
1284
+ limit: 10
1285
+ }); // results.data[0]._distance
1286
+ \`\`\`
1287
+ `);
1619
1288
  lines.push("## Resources");
1620
1289
  lines.push("");
1621
1290
  for (const resource of contract.resources) {
@@ -1635,10 +1304,12 @@ function generateUnifiedContractMarkdown(contract) {
1635
1304
  lines.push(`- API: \`${method.correspondsTo}\``);
1636
1305
  }
1637
1306
  lines.push("");
1638
- lines.push("```typescript");
1639
- lines.push(method.example);
1640
- lines.push("```");
1641
- lines.push("");
1307
+ if (method.example) {
1308
+ lines.push("```typescript");
1309
+ lines.push(method.example);
1310
+ lines.push("```");
1311
+ lines.push("");
1312
+ }
1642
1313
  }
1643
1314
  lines.push("#### API Endpoints");
1644
1315
  lines.push("");
@@ -1672,23 +1343,14 @@ function generateUnifiedContractMarkdown(contract) {
1672
1343
  }
1673
1344
  lines.push("");
1674
1345
  }
1675
- lines.push("## Type Imports");
1676
- lines.push("");
1677
- lines.push("```typescript");
1678
- lines.push("// Import SDK and types");
1679
- lines.push("import { SDK } from './client';");
1680
- lines.push("");
1681
- lines.push("// Import types for a specific table");
1682
- lines.push("import type {");
1683
- lines.push(" SelectTableName, // Full record type");
1684
- lines.push(" InsertTableName, // Create payload type");
1685
- lines.push(" UpdateTableName // Update payload type");
1686
- lines.push("} from './client/types/table_name';");
1687
- lines.push("");
1688
- lines.push("// Import all types");
1689
- lines.push("import type * as Types from './client/types';");
1690
- lines.push("```");
1691
- lines.push("");
1346
+ lines.push(`## Type Imports
1347
+
1348
+ \`\`\`typescript
1349
+ import { SDK } from './client';
1350
+ import type { SelectTableName, InsertTableName, UpdateTableName } from './client/types/table_name';
1351
+ import type * as Types from './client/types';
1352
+ \`\`\`
1353
+ `);
1692
1354
  return lines.join(`
1693
1355
  `);
1694
1356
  }
@@ -3273,7 +2935,8 @@ const deleteSchema = z.object({
3273
2935
 
3274
2936
  const getByPkQuerySchema = z.object({
3275
2937
  select: z.array(z.string()).min(1).optional(),
3276
- exclude: z.array(z.string()).min(1).optional()
2938
+ exclude: z.array(z.string()).min(1).optional(),
2939
+ includeSoftDeleted: z.boolean().optional()
3277
2940
  }).strict().refine(
3278
2941
  (data) => !(data.select && data.exclude),
3279
2942
  { message: "Cannot specify both 'select' and 'exclude' parameters" }
@@ -3288,7 +2951,8 @@ const listSchema = z.object({
3288
2951
  offset: z.number().int().min(0).optional(),
3289
2952
  orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
3290
2953
  order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
3291
- distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional(),${hasVectorColumns ? `
2954
+ distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional(),
2955
+ includeSoftDeleted: z.boolean().optional(),${hasVectorColumns ? `
3292
2956
  vector: z.object({
3293
2957
  field: z.string(),
3294
2958
  query: z.array(z.number()),
@@ -3385,12 +3049,15 @@ ${hasAuth ? `
3385
3049
  app.get(\`\${base}/${pkPath}\`, async (c) => {
3386
3050
  ${getPkParams}
3387
3051
 
3388
- // Parse query params for select/exclude
3052
+ // Parse query params coerce includeSoftDeleted string to boolean before Zod validation
3053
+ // (avoids Boolean("false")=true pitfall while keeping schema as single source of truth)
3389
3054
  const selectParam = c.req.query("select");
3390
3055
  const excludeParam = c.req.query("exclude");
3056
+ const includeSoftDeletedParam = c.req.query("includeSoftDeleted");
3391
3057
  const queryData: any = {};
3392
3058
  if (selectParam) queryData.select = selectParam.split(",");
3393
3059
  if (excludeParam) queryData.exclude = excludeParam.split(",");
3060
+ if (includeSoftDeletedParam !== undefined) queryData.includeSoftDeleted = includeSoftDeletedParam === "true";
3394
3061
 
3395
3062
  const queryParsed = getByPkQuerySchema.safeParse(queryData);
3396
3063
  if (!queryParsed.success) {
@@ -3403,7 +3070,7 @@ ${hasAuth ? `
3403
3070
  }
3404
3071
 
3405
3072
  const ctx = { ...baseCtx, select: queryParsed.data.select, exclude: queryParsed.data.exclude };
3406
- const result = await coreOps.getByPk(ctx, pkValues);
3073
+ const result = await coreOps.getByPk(ctx, pkValues, { includeSoftDeleted: queryParsed.data.includeSoftDeleted });
3407
3074
 
3408
3075
  if (result.error) {
3409
3076
  return c.json({ error: result.error }, result.status as any);
@@ -3909,14 +3576,14 @@ ${hasJsonbColumns ? ` /**
3909
3576
  * @param options - Select specific fields to return
3910
3577
  * @returns The record with only selected fields if found, null otherwise
3911
3578
  */
3912
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
3579
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
3913
3580
  /**
3914
3581
  * Get a ${table.name} record by primary key with field exclusion
3915
3582
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3916
3583
  * @param options - Exclude specific fields from return
3917
3584
  * @returns The record without excluded fields if found, null otherwise
3918
3585
  */
3919
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
3586
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
3920
3587
  /**
3921
3588
  * Get a ${table.name} record by primary key
3922
3589
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
@@ -3925,15 +3592,16 @@ ${hasJsonbColumns ? ` /**
3925
3592
  * // With JSONB type override:
3926
3593
  * const user = await client.getByPk<{ metadata: Metadata }>('user-id');
3927
3594
  */
3928
- async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type}<TJsonb> | null>;
3595
+ async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type}<TJsonb> | null>;
3929
3596
  async getByPk<TJsonb extends Partial<Select${Type}> = {}>(
3930
3597
  pk: ${pkType},
3931
- options?: { select?: string[]; exclude?: string[] }
3598
+ options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
3932
3599
  ): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>> | null> {
3933
3600
  const path = ${pkPathExpr};
3934
3601
  const queryParams = new URLSearchParams();
3935
3602
  if (options?.select) queryParams.set('select', options.select.join(','));
3936
3603
  if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
3604
+ if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
3937
3605
  const query = queryParams.toString();
3938
3606
  const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
3939
3607
  return this.get<Select${Type}<TJsonb> | null>(url);
@@ -3943,28 +3611,29 @@ ${hasJsonbColumns ? ` /**
3943
3611
  * @param options - Select specific fields to return
3944
3612
  * @returns The record with only selected fields if found, null otherwise
3945
3613
  */
3946
- async getByPk(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}> | null>;
3614
+ async getByPk(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
3947
3615
  /**
3948
3616
  * Get a ${table.name} record by primary key with field exclusion
3949
3617
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3950
3618
  * @param options - Exclude specific fields from return
3951
3619
  * @returns The record without excluded fields if found, null otherwise
3952
3620
  */
3953
- async getByPk(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}> | null>;
3621
+ async getByPk(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
3954
3622
  /**
3955
3623
  * Get a ${table.name} record by primary key
3956
3624
  * @param pk - The primary key value${hasCompositePk ? "s" : ""}
3957
3625
  * @returns The record with all fields if found, null otherwise
3958
3626
  */
3959
- async getByPk(pk: ${pkType}, options?: Omit<{ select?: string[]; exclude?: string[] }, 'select' | 'exclude'>): Promise<Select${Type} | null>;
3627
+ async getByPk(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type} | null>;
3960
3628
  async getByPk(
3961
3629
  pk: ${pkType},
3962
- options?: { select?: string[]; exclude?: string[] }
3630
+ options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
3963
3631
  ): Promise<Select${Type} | Partial<Select${Type}> | null> {
3964
3632
  const path = ${pkPathExpr};
3965
3633
  const queryParams = new URLSearchParams();
3966
3634
  if (options?.select) queryParams.set('select', options.select.join(','));
3967
3635
  if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
3636
+ if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
3968
3637
  const query = queryParams.toString();
3969
3638
  const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
3970
3639
  return this.get<Select${Type} | null>(url);
@@ -3991,6 +3660,7 @@ ${hasJsonbColumns ? ` /**
3991
3660
  orderBy?: string | string[];
3992
3661
  order?: "asc" | "desc" | ("asc" | "desc")[];
3993
3662
  distinctOn?: string | string[];
3663
+ includeSoftDeleted?: boolean;
3994
3664
  }): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
3995
3665
  /**
3996
3666
  * List ${table.name} records with field exclusion
@@ -4013,6 +3683,7 @@ ${hasJsonbColumns ? ` /**
4013
3683
  orderBy?: string | string[];
4014
3684
  order?: "asc" | "desc" | ("asc" | "desc")[];
4015
3685
  distinctOn?: string | string[];
3686
+ includeSoftDeleted?: boolean;
4016
3687
  }): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
4017
3688
  /**
4018
3689
  * List ${table.name} records with pagination and filtering
@@ -4046,6 +3717,7 @@ ${hasJsonbColumns ? ` /**
4046
3717
  orderBy?: string | string[];
4047
3718
  order?: "asc" | "desc" | ("asc" | "desc")[];
4048
3719
  distinctOn?: string | string[];
3720
+ includeSoftDeleted?: boolean;
4049
3721
  }): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
4050
3722
  async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
4051
3723
  include?: ${Type}IncludeSpec;
@@ -4064,6 +3736,7 @@ ${hasJsonbColumns ? ` /**
4064
3736
  orderBy?: string | string[];
4065
3737
  order?: "asc" | "desc" | ("asc" | "desc")[];
4066
3738
  distinctOn?: string | string[];
3739
+ includeSoftDeleted?: boolean;
4067
3740
  }): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
4068
3741
  return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
4069
3742
  }` : ` /**
@@ -4087,6 +3760,7 @@ ${hasJsonbColumns ? ` /**
4087
3760
  orderBy?: string | string[];
4088
3761
  order?: "asc" | "desc" | ("asc" | "desc")[];
4089
3762
  distinctOn?: string | string[];
3763
+ includeSoftDeleted?: boolean;
4090
3764
  }): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
4091
3765
  /**
4092
3766
  * List ${table.name} records with field exclusion
@@ -4109,6 +3783,7 @@ ${hasJsonbColumns ? ` /**
4109
3783
  orderBy?: string | string[];
4110
3784
  order?: "asc" | "desc" | ("asc" | "desc")[];
4111
3785
  distinctOn?: string | string[];
3786
+ includeSoftDeleted?: boolean;
4112
3787
  }): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
4113
3788
  /**
4114
3789
  * List ${table.name} records with pagination and filtering
@@ -4136,6 +3811,7 @@ ${hasJsonbColumns ? ` /**
4136
3811
  orderBy?: string | string[];
4137
3812
  order?: "asc" | "desc" | ("asc" | "desc")[];
4138
3813
  distinctOn?: string | string[];
3814
+ includeSoftDeleted?: boolean;
4139
3815
  }): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
4140
3816
  async list(params?: {
4141
3817
  include?: ${Type}IncludeSpec;
@@ -4154,6 +3830,7 @@ ${hasJsonbColumns ? ` /**
4154
3830
  orderBy?: string | string[];
4155
3831
  order?: "asc" | "desc" | ("asc" | "desc")[];
4156
3832
  distinctOn?: string | string[];
3833
+ includeSoftDeleted?: boolean;
4157
3834
  }): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
4158
3835
  return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
4159
3836
  }`}
@@ -4563,7 +4240,8 @@ export abstract class BaseClient {
4563
4240
  }
4564
4241
 
4565
4242
  // src/emit-include-loader.ts
4566
- function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
4243
+ function emitIncludeLoader(model, maxDepth, opts = {}) {
4244
+ const { softDeleteCols = {}, useJsExtensions } = opts;
4567
4245
  const fkIndex = {};
4568
4246
  for (const t of Object.values(model.tables)) {
4569
4247
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
@@ -4602,8 +4280,18 @@ const log = {
4602
4280
  };
4603
4281
 
4604
4282
  // Helpers for PK/FK discovery from model (inlined)
4605
- const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)} as const;
4606
- const PKS = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)} as const;
4283
+ const FK_INDEX: Record<string, Array<{from: string[]; toTable: string; to: string[]}>> = ${JSON.stringify(fkIndex, null, 2)};
4284
+ const PKS: Record<string, string[]> = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)};
4285
+ /** Soft-delete column per table, null if not applicable. Baked in at generation time. */
4286
+ const SOFT_DELETE_COLS: Record<string, string | null> = ${JSON.stringify(softDeleteCols, null, 2)};
4287
+
4288
+ /** Returns a SQL fragment like ' AND "col" IS NULL' for the given table, or "" if none. */
4289
+ function softDeleteFilter(table: string, alias?: string): string {
4290
+ const col = SOFT_DELETE_COLS[table];
4291
+ if (!col) return "";
4292
+ const ref = alias ? \`\${alias}."\${col}"\` : \`"\${col}"\`;
4293
+ return \` AND \${ref} IS NULL\`;
4294
+ }
4607
4295
 
4608
4296
  // Build WHERE predicate for OR-of-AND on composite values
4609
4297
  function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
@@ -4693,6 +4381,49 @@ function filterFields<T extends Record<string, any>>(
4693
4381
  });
4694
4382
  }
4695
4383
 
4384
+ /** Sentinel used as window-function LIMIT when no per-parent limit is requested. */
4385
+ const NO_LIMIT = 999_999_999;
4386
+
4387
+ /**
4388
+ * FK_INDEX and PKS are fully populated at code-gen time for all schema tables,
4389
+ * so lookups by TableName are always defined. These helpers assert non-null
4390
+ * in one place rather than scattering \`!\` or \`as any\` at every call site.
4391
+ */
4392
+ function fksOf(table: string): Array<{from: string[]; toTable: string; to: string[]}> {
4393
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4394
+ return FK_INDEX[table]!;
4395
+ }
4396
+ function pkOf(table: string): string[] {
4397
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4398
+ return PKS[table]!;
4399
+ }
4400
+
4401
+ /** Parse relation options and nested child spec from a specValue entry. */
4402
+ function parseSpecOptions(specValue: any): { options: RelationOptions; childSpec: any } {
4403
+ const options: RelationOptions = {};
4404
+ let childSpec: any = undefined;
4405
+ if (specValue && typeof specValue === "object" && specValue !== true) {
4406
+ if (specValue.select !== undefined) options.select = specValue.select;
4407
+ if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4408
+ if (specValue.limit !== undefined) options.limit = specValue.limit;
4409
+ if (specValue.offset !== undefined) options.offset = specValue.offset;
4410
+ if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4411
+ if (specValue.order !== undefined) options.order = specValue.order;
4412
+ if (specValue.include !== undefined) {
4413
+ childSpec = specValue.include;
4414
+ } else {
4415
+ // Support legacy format: { tags: true } alongside new: { include: { tags: true } }
4416
+ const nonOptionKeys = Object.keys(specValue).filter(
4417
+ k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4418
+ );
4419
+ if (nonOptionKeys.length > 0) {
4420
+ childSpec = Object.fromEntries(nonOptionKeys.map(k => [k, specValue[k]]));
4421
+ }
4422
+ }
4423
+ }
4424
+ return { options, childSpec };
4425
+ }
4426
+
4696
4427
  // Public entry
4697
4428
  export async function loadIncludes(
4698
4429
  root: TableName,
@@ -4732,37 +4463,7 @@ export async function loadIncludes(
4732
4463
  // Safely run each loader; never let one bad edge 500 the route
4733
4464
  if (rel.via) {
4734
4465
  // M:N via junction
4735
- const specValue = s[key];
4736
- const options: RelationOptions = {};
4737
- let childSpec: any = undefined;
4738
-
4739
- if (specValue && typeof specValue === "object" && specValue !== true) {
4740
- // Extract options
4741
- if (specValue.select !== undefined) options.select = specValue.select;
4742
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4743
- if (specValue.limit !== undefined) options.limit = specValue.limit;
4744
- if (specValue.offset !== undefined) options.offset = specValue.offset;
4745
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4746
- if (specValue.order !== undefined) options.order = specValue.order;
4747
-
4748
- // Extract nested spec - support both formats:
4749
- // New: { limit: 3, include: { tags: true } }
4750
- // Old: { tags: true } (backward compatibility)
4751
- if (specValue.include !== undefined) {
4752
- childSpec = specValue.include;
4753
- } else {
4754
- // Build childSpec from non-option keys
4755
- const nonOptionKeys = Object.keys(specValue).filter(
4756
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4757
- );
4758
- if (nonOptionKeys.length > 0) {
4759
- childSpec = {};
4760
- for (const k of nonOptionKeys) {
4761
- childSpec[k] = specValue[k];
4762
- }
4763
- }
4764
- }
4765
- }
4466
+ const { options, childSpec } = parseSpecOptions(s[key]);
4766
4467
 
4767
4468
  try {
4768
4469
  await loadManyToMany(table, target, rel.via as string, rows, key, options);
@@ -4784,37 +4485,7 @@ export async function loadIncludes(
4784
4485
 
4785
4486
  if (rel.kind === "many") {
4786
4487
  // 1:N target has FK to current
4787
- const specValue = s[key];
4788
- const options: RelationOptions = {};
4789
- let childSpec: any = undefined;
4790
-
4791
- if (specValue && typeof specValue === "object" && specValue !== true) {
4792
- // Extract options
4793
- if (specValue.select !== undefined) options.select = specValue.select;
4794
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4795
- if (specValue.limit !== undefined) options.limit = specValue.limit;
4796
- if (specValue.offset !== undefined) options.offset = specValue.offset;
4797
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4798
- if (specValue.order !== undefined) options.order = specValue.order;
4799
-
4800
- // Extract nested spec - support both formats:
4801
- // New: { limit: 3, include: { tags: true } }
4802
- // Old: { tags: true } (backward compatibility)
4803
- if (specValue.include !== undefined) {
4804
- childSpec = specValue.include;
4805
- } else {
4806
- // Build childSpec from non-option keys
4807
- const nonOptionKeys = Object.keys(specValue).filter(
4808
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4809
- );
4810
- if (nonOptionKeys.length > 0) {
4811
- childSpec = {};
4812
- for (const k of nonOptionKeys) {
4813
- childSpec[k] = specValue[k];
4814
- }
4815
- }
4816
- }
4817
- }
4488
+ const { options, childSpec } = parseSpecOptions(s[key]);
4818
4489
 
4819
4490
  try {
4820
4491
  await loadOneToMany(table, target, rows, key, options);
@@ -4834,20 +4505,11 @@ export async function loadIncludes(
4834
4505
  } else {
4835
4506
  // kind === "one"
4836
4507
  // Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
4837
- const specValue = s[key];
4838
- const options: RelationOptions = {};
4839
- let childSpec: any = undefined;
4840
-
4841
- if (specValue && typeof specValue === "object" && specValue !== true) {
4842
- if (specValue.select !== undefined) options.select = specValue.select;
4843
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4844
- // Support { include: TargetIncludeSpec } — mirrors the many/via handler
4845
- if (specValue.include !== undefined) {
4846
- childSpec = specValue.include;
4847
- }
4848
- }
4508
+ // Destructure only select/exclude/childSpec — limit/offset/orderBy don't apply to 1:1 relations
4509
+ const { options: { select, exclude }, childSpec } = parseSpecOptions(s[key]);
4510
+ const options: RelationOptions = { select, exclude };
4849
4511
 
4850
- const currFks = (FK_INDEX as any)[table] as Array<{from:string[];toTable:string;to:string[]}>;
4512
+ const currFks = fksOf(table);
4851
4513
  const toTarget = currFks.find(f => f.toTable === target);
4852
4514
  if (toTarget) {
4853
4515
  try {
@@ -4879,16 +4541,16 @@ export async function loadIncludes(
4879
4541
 
4880
4542
  async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4881
4543
  // current has FK cols referencing target PK
4882
- const fk = (FK_INDEX as any)[curr].find((f: any) => f.toTable === target);
4544
+ const fk = fksOf(curr).find(f => f.toTable === target);
4883
4545
  if (!fk) { for (const r of rows) r[key] = null; return; }
4884
4546
  const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
4885
4547
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
4886
4548
 
4887
4549
  // Query target WHERE target.pk IN tuples
4888
- const pkCols = (PKS as any)[target] as string[];
4550
+ const pkCols = pkOf(target);
4889
4551
  const where = buildOrAndPredicate(pkCols, tuples.length, 1);
4890
4552
  const params = tuples.flat();
4891
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4553
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
4892
4554
  log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
4893
4555
  const { rows: targets } = await pg.query(sql, params);
4894
4556
 
@@ -4905,17 +4567,17 @@ export async function loadIncludes(
4905
4567
 
4906
4568
  async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4907
4569
  // target has FK cols referencing current PK (unique)
4908
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
4570
+ const fk = fksOf(target).find(f => f.toTable === curr);
4909
4571
  if (!fk) { for (const r of rows) r[key] = null; return; }
4910
4572
 
4911
- const pkCols = (PKS as any)[curr] as string[];
4573
+ const pkCols = pkOf(curr);
4912
4574
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4913
4575
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
4914
4576
 
4915
4577
  // SELECT target WHERE fk IN tuples
4916
4578
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
4917
4579
  const params = tuples.flat();
4918
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4580
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
4919
4581
  log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
4920
4582
  const { rows: targets } = await pg.query(sql, params);
4921
4583
 
@@ -4931,10 +4593,10 @@ export async function loadIncludes(
4931
4593
 
4932
4594
  async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4933
4595
  // target has FK cols referencing current PK
4934
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
4596
+ const fk = fksOf(target).find(f => f.toTable === curr);
4935
4597
  if (!fk) { for (const r of rows) r[key] = []; return; }
4936
4598
 
4937
- const pkCols = (PKS as any)[curr] as string[];
4599
+ const pkCols = pkOf(curr);
4938
4600
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4939
4601
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4940
4602
 
@@ -4942,7 +4604,7 @@ export async function loadIncludes(
4942
4604
  const params = tuples.flat();
4943
4605
 
4944
4606
  // Build SQL with optional ORDER BY, LIMIT, OFFSET
4945
- let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4607
+ let sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
4946
4608
 
4947
4609
  // If limit/offset are needed, use window functions to limit per parent
4948
4610
  if (options.limit !== undefined || options.offset !== undefined) {
@@ -4952,13 +4614,13 @@ export async function loadIncludes(
4952
4614
 
4953
4615
  const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
4954
4616
  const offset = options.offset ?? 0;
4955
- const limit = options.limit ?? 999999999;
4617
+ const limit = options.limit ?? NO_LIMIT;
4956
4618
 
4957
4619
  sql = \`
4958
4620
  SELECT * FROM (
4959
4621
  SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
4960
4622
  FROM "\${target}"
4961
- WHERE \${where}
4623
+ WHERE \${where}\${softDeleteFilter(target)}
4962
4624
  ) __sub
4963
4625
  WHERE __rn > \${offset} AND __rn <= \${offset + limit}
4964
4626
  \`;
@@ -4988,11 +4650,11 @@ export async function loadIncludes(
4988
4650
 
4989
4651
  async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
4990
4652
  // via has two FKs: one to curr, one to target
4991
- const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
4992
- const toTarget = (FK_INDEX as any)[via].find((f: any) => f.toTable === target);
4653
+ const toCurr = fksOf(via).find(f => f.toTable === curr);
4654
+ const toTarget = fksOf(via).find(f => f.toTable === target);
4993
4655
  if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
4994
4656
 
4995
- const pkCols = (PKS as any)[curr] as string[];
4657
+ const pkCols = pkOf(curr);
4996
4658
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4997
4659
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4998
4660
 
@@ -5007,9 +4669,9 @@ export async function loadIncludes(
5007
4669
 
5008
4670
  const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
5009
4671
  const offset = options.offset ?? 0;
5010
- const limit = options.limit ?? 999999999;
4672
+ const limit = options.limit ?? NO_LIMIT;
5011
4673
 
5012
- const targetPkCols = (PKS as any)[target] as string[];
4674
+ const targetPkCols = pkOf(target);
5013
4675
  const joinConditions = toTarget.from.map((jCol: string, i: number) => {
5014
4676
  return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
5015
4677
  }).join(' AND ');
@@ -5022,7 +4684,7 @@ export async function loadIncludes(
5022
4684
  \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
5023
4685
  FROM "\${via}" j
5024
4686
  INNER JOIN "\${target}" t ON \${joinConditions}
5025
- WHERE \${whereVia}
4687
+ WHERE \${whereVia}\${softDeleteFilter(target, "t")}
5026
4688
  ) __numbered
5027
4689
  WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
5028
4690
  \`;
@@ -5065,8 +4727,8 @@ export async function loadIncludes(
5065
4727
 
5066
4728
  // 2) Load targets by distinct target fk tuples in junction
5067
4729
  const tTuples = distinctTuples(jrows, toTarget.from);
5068
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
5069
- const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
4730
+ const whereT = buildOrAndPredicate(pkOf(target), tTuples.length, 1);
4731
+ const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\${softDeleteFilter(target)}\`;
5070
4732
  const paramsT = tTuples.flat();
5071
4733
  log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
5072
4734
  const { rows: targets } = await pg.query(sqlT, paramsT);
@@ -5074,7 +4736,7 @@ export async function loadIncludes(
5074
4736
  // Apply select/exclude filtering
5075
4737
  const filteredTargets = filterFields(targets, options.select, options.exclude);
5076
4738
 
5077
- const tIdx = indexByTuple(filteredTargets, (PKS as any)[target]);
4739
+ const tIdx = indexByTuple(filteredTargets, pkOf(target));
5078
4740
 
5079
4741
  // 3) Group junction rows by current pk tuple, map to target rows
5080
4742
  const byCurr = groupByTuple(jrows, toCurr.from);
@@ -5980,7 +5642,8 @@ export async function createRecord(
5980
5642
  */
5981
5643
  export async function getByPk(
5982
5644
  ctx: OperationContext,
5983
- pkValues: any[]
5645
+ pkValues: any[],
5646
+ opts?: { includeSoftDeleted?: boolean }
5984
5647
  ): Promise<{ data?: any; error?: string; status: number }> {
5985
5648
  try {
5986
5649
  const hasCompositePk = ctx.pkColumns.length > 1;
@@ -5989,7 +5652,8 @@ export async function getByPk(
5989
5652
  : \`"\${ctx.pkColumns[0]}" = $1\`;
5990
5653
 
5991
5654
  const columns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
5992
- const text = \`SELECT \${columns} FROM "\${ctx.table}" WHERE \${wherePkSql} LIMIT 1\`;
5655
+ const softDeleteFilter = ctx.softDeleteColumn && !opts?.includeSoftDeleted ? \` AND "\${ctx.softDeleteColumn}" IS NULL\` : "";
5656
+ const text = \`SELECT \${columns} FROM "\${ctx.table}" WHERE \${wherePkSql}\${softDeleteFilter} LIMIT 1\`;
5993
5657
  log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
5994
5658
 
5995
5659
  const { rows } = await ctx.pg.query(text, pkValues);
@@ -6362,6 +6026,7 @@ export async function listRecords(
6362
6026
  orderBy?: string | string[];
6363
6027
  order?: "asc" | "desc" | ("asc" | "desc")[];
6364
6028
  distinctOn?: string | string[];
6029
+ includeSoftDeleted?: boolean;
6365
6030
  vector?: {
6366
6031
  field: string;
6367
6032
  query: number[];
@@ -6372,7 +6037,7 @@ export async function listRecords(
6372
6037
  }
6373
6038
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
6374
6039
  try {
6375
- const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
6040
+ const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
6376
6041
 
6377
6042
  // DISTINCT ON support
6378
6043
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -6408,8 +6073,8 @@ export async function listRecords(
6408
6073
  const whereParts: string[] = [];
6409
6074
  let whereParams: any[] = [];
6410
6075
 
6411
- // Add soft delete filter if applicable
6412
- if (ctx.softDeleteColumn) {
6076
+ // Add soft delete filter if applicable (skip if caller opts into seeing soft-deleted records)
6077
+ if (ctx.softDeleteColumn && !includeSoftDeleted) {
6413
6078
  whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
6414
6079
  }
6415
6080
 
@@ -6447,7 +6112,7 @@ export async function listRecords(
6447
6112
 
6448
6113
  if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
6449
6114
  const countWhereParts: string[] = [];
6450
- if (ctx.softDeleteColumn) {
6115
+ if (ctx.softDeleteColumn && !includeSoftDeleted) {
6451
6116
  countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
6452
6117
  }
6453
6118
  if (whereClause) {
@@ -7418,6 +7083,12 @@ init_utils();
7418
7083
  var __filename2 = fileURLToPath(import.meta.url);
7419
7084
  var __dirname2 = dirname2(__filename2);
7420
7085
  var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
7086
+ function resolveSoftDeleteColumn(cfg, tableName) {
7087
+ const overrides = cfg.softDeleteColumnOverrides;
7088
+ if (overrides && tableName in overrides)
7089
+ return overrides[tableName] ?? null;
7090
+ return cfg.softDeleteColumn ?? null;
7091
+ }
7421
7092
  async function generate(configPath) {
7422
7093
  if (!existsSync2(configPath)) {
7423
7094
  throw new Error(`Config file not found: ${configPath}
@@ -7489,9 +7160,14 @@ async function generate(configPath) {
7489
7160
  path: join2(serverDir, "include-builder.ts"),
7490
7161
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
7491
7162
  });
7163
+ const softDeleteCols = Object.fromEntries(Object.values(model.tables).map((t) => {
7164
+ const col = resolveSoftDeleteColumn(cfg, t.name);
7165
+ const validated = col && t.columns.some((c) => c.name === col) ? col : null;
7166
+ return [t.name, validated];
7167
+ }));
7492
7168
  files.push({
7493
7169
  path: join2(serverDir, "include-loader.ts"),
7494
- content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
7170
+ content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
7495
7171
  });
7496
7172
  files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
7497
7173
  if (getAuthStrategy(normalizedAuth) !== "none") {
@@ -7517,7 +7193,7 @@ async function generate(configPath) {
7517
7193
  let routeContent;
7518
7194
  if (serverFramework === "hono") {
7519
7195
  routeContent = emitHonoRoutes(table, graph, {
7520
- softDeleteColumn: cfg.softDeleteColumn || null,
7196
+ softDeleteColumn: softDeleteCols[table.name] ?? null,
7521
7197
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
7522
7198
  authStrategy: getAuthStrategy(normalizedAuth),
7523
7199
  useJsExtensions: cfg.useJsExtensions,