postgresdk 0.18.23 → 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
  }
@@ -3617,7 +3279,8 @@ export abstract class BaseClient {
3617
3279
  }
3618
3280
 
3619
3281
  // src/emit-include-loader.ts
3620
- function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
3282
+ function emitIncludeLoader(model, maxDepth, opts = {}) {
3283
+ const { softDeleteCols = {}, useJsExtensions } = opts;
3621
3284
  const fkIndex = {};
3622
3285
  for (const t of Object.values(model.tables)) {
3623
3286
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
@@ -3656,8 +3319,18 @@ const log = {
3656
3319
  };
3657
3320
 
3658
3321
  // Helpers for PK/FK discovery from model (inlined)
3659
- const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)} as const;
3660
- 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
+ }
3661
3334
 
3662
3335
  // Build WHERE predicate for OR-of-AND on composite values
3663
3336
  function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
@@ -3747,6 +3420,49 @@ function filterFields<T extends Record<string, any>>(
3747
3420
  });
3748
3421
  }
3749
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
+
3750
3466
  // Public entry
3751
3467
  export async function loadIncludes(
3752
3468
  root: TableName,
@@ -3786,37 +3502,7 @@ export async function loadIncludes(
3786
3502
  // Safely run each loader; never let one bad edge 500 the route
3787
3503
  if (rel.via) {
3788
3504
  // M:N via junction
3789
- const specValue = s[key];
3790
- const options: RelationOptions = {};
3791
- let childSpec: any = undefined;
3792
-
3793
- if (specValue && typeof specValue === "object" && specValue !== true) {
3794
- // Extract options
3795
- if (specValue.select !== undefined) options.select = specValue.select;
3796
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3797
- if (specValue.limit !== undefined) options.limit = specValue.limit;
3798
- if (specValue.offset !== undefined) options.offset = specValue.offset;
3799
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3800
- if (specValue.order !== undefined) options.order = specValue.order;
3801
-
3802
- // Extract nested spec - support both formats:
3803
- // New: { limit: 3, include: { tags: true } }
3804
- // Old: { tags: true } (backward compatibility)
3805
- if (specValue.include !== undefined) {
3806
- childSpec = specValue.include;
3807
- } else {
3808
- // Build childSpec from non-option keys
3809
- const nonOptionKeys = Object.keys(specValue).filter(
3810
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3811
- );
3812
- if (nonOptionKeys.length > 0) {
3813
- childSpec = {};
3814
- for (const k of nonOptionKeys) {
3815
- childSpec[k] = specValue[k];
3816
- }
3817
- }
3818
- }
3819
- }
3505
+ const { options, childSpec } = parseSpecOptions(s[key]);
3820
3506
 
3821
3507
  try {
3822
3508
  await loadManyToMany(table, target, rel.via as string, rows, key, options);
@@ -3838,37 +3524,7 @@ export async function loadIncludes(
3838
3524
 
3839
3525
  if (rel.kind === "many") {
3840
3526
  // 1:N target has FK to current
3841
- const specValue = s[key];
3842
- const options: RelationOptions = {};
3843
- let childSpec: any = undefined;
3844
-
3845
- if (specValue && typeof specValue === "object" && specValue !== true) {
3846
- // Extract options
3847
- if (specValue.select !== undefined) options.select = specValue.select;
3848
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3849
- if (specValue.limit !== undefined) options.limit = specValue.limit;
3850
- if (specValue.offset !== undefined) options.offset = specValue.offset;
3851
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
3852
- if (specValue.order !== undefined) options.order = specValue.order;
3853
-
3854
- // Extract nested spec - support both formats:
3855
- // New: { limit: 3, include: { tags: true } }
3856
- // Old: { tags: true } (backward compatibility)
3857
- if (specValue.include !== undefined) {
3858
- childSpec = specValue.include;
3859
- } else {
3860
- // Build childSpec from non-option keys
3861
- const nonOptionKeys = Object.keys(specValue).filter(
3862
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
3863
- );
3864
- if (nonOptionKeys.length > 0) {
3865
- childSpec = {};
3866
- for (const k of nonOptionKeys) {
3867
- childSpec[k] = specValue[k];
3868
- }
3869
- }
3870
- }
3871
- }
3527
+ const { options, childSpec } = parseSpecOptions(s[key]);
3872
3528
 
3873
3529
  try {
3874
3530
  await loadOneToMany(table, target, rows, key, options);
@@ -3888,20 +3544,11 @@ export async function loadIncludes(
3888
3544
  } else {
3889
3545
  // kind === "one"
3890
3546
  // Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
3891
- const specValue = s[key];
3892
- const options: RelationOptions = {};
3893
- let childSpec: any = undefined;
3894
-
3895
- if (specValue && typeof specValue === "object" && specValue !== true) {
3896
- if (specValue.select !== undefined) options.select = specValue.select;
3897
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
3898
- // Support { include: TargetIncludeSpec } — mirrors the many/via handler
3899
- if (specValue.include !== undefined) {
3900
- childSpec = specValue.include;
3901
- }
3902
- }
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 };
3903
3550
 
3904
- const currFks = (FK_INDEX as any)[table] as Array<{from:string[];toTable:string;to:string[]}>;
3551
+ const currFks = fksOf(table);
3905
3552
  const toTarget = currFks.find(f => f.toTable === target);
3906
3553
  if (toTarget) {
3907
3554
  try {
@@ -3933,16 +3580,16 @@ export async function loadIncludes(
3933
3580
 
3934
3581
  async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3935
3582
  // current has FK cols referencing target PK
3936
- const fk = (FK_INDEX as any)[curr].find((f: any) => f.toTable === target);
3583
+ const fk = fksOf(curr).find(f => f.toTable === target);
3937
3584
  if (!fk) { for (const r of rows) r[key] = null; return; }
3938
3585
  const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
3939
3586
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
3940
3587
 
3941
3588
  // Query target WHERE target.pk IN tuples
3942
- const pkCols = (PKS as any)[target] as string[];
3589
+ const pkCols = pkOf(target);
3943
3590
  const where = buildOrAndPredicate(pkCols, tuples.length, 1);
3944
3591
  const params = tuples.flat();
3945
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3592
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
3946
3593
  log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
3947
3594
  const { rows: targets } = await pg.query(sql, params);
3948
3595
 
@@ -3959,17 +3606,17 @@ export async function loadIncludes(
3959
3606
 
3960
3607
  async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3961
3608
  // target has FK cols referencing current PK (unique)
3962
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3609
+ const fk = fksOf(target).find(f => f.toTable === curr);
3963
3610
  if (!fk) { for (const r of rows) r[key] = null; return; }
3964
3611
 
3965
- const pkCols = (PKS as any)[curr] as string[];
3612
+ const pkCols = pkOf(curr);
3966
3613
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
3967
3614
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
3968
3615
 
3969
3616
  // SELECT target WHERE fk IN tuples
3970
3617
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
3971
3618
  const params = tuples.flat();
3972
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3619
+ const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
3973
3620
  log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
3974
3621
  const { rows: targets } = await pg.query(sql, params);
3975
3622
 
@@ -3985,10 +3632,10 @@ export async function loadIncludes(
3985
3632
 
3986
3633
  async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
3987
3634
  // target has FK cols referencing current PK
3988
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
3635
+ const fk = fksOf(target).find(f => f.toTable === curr);
3989
3636
  if (!fk) { for (const r of rows) r[key] = []; return; }
3990
3637
 
3991
- const pkCols = (PKS as any)[curr] as string[];
3638
+ const pkCols = pkOf(curr);
3992
3639
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
3993
3640
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
3994
3641
 
@@ -3996,7 +3643,7 @@ export async function loadIncludes(
3996
3643
  const params = tuples.flat();
3997
3644
 
3998
3645
  // Build SQL with optional ORDER BY, LIMIT, OFFSET
3999
- let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
3646
+ let sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
4000
3647
 
4001
3648
  // If limit/offset are needed, use window functions to limit per parent
4002
3649
  if (options.limit !== undefined || options.offset !== undefined) {
@@ -4006,13 +3653,13 @@ export async function loadIncludes(
4006
3653
 
4007
3654
  const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
4008
3655
  const offset = options.offset ?? 0;
4009
- const limit = options.limit ?? 999999999;
3656
+ const limit = options.limit ?? NO_LIMIT;
4010
3657
 
4011
3658
  sql = \`
4012
3659
  SELECT * FROM (
4013
3660
  SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
4014
3661
  FROM "\${target}"
4015
- WHERE \${where}
3662
+ WHERE \${where}\${softDeleteFilter(target)}
4016
3663
  ) __sub
4017
3664
  WHERE __rn > \${offset} AND __rn <= \${offset + limit}
4018
3665
  \`;
@@ -4042,11 +3689,11 @@ export async function loadIncludes(
4042
3689
 
4043
3690
  async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
4044
3691
  // via has two FKs: one to curr, one to target
4045
- const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
4046
- 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);
4047
3694
  if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
4048
3695
 
4049
- const pkCols = (PKS as any)[curr] as string[];
3696
+ const pkCols = pkOf(curr);
4050
3697
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4051
3698
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4052
3699
 
@@ -4061,9 +3708,9 @@ export async function loadIncludes(
4061
3708
 
4062
3709
  const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
4063
3710
  const offset = options.offset ?? 0;
4064
- const limit = options.limit ?? 999999999;
3711
+ const limit = options.limit ?? NO_LIMIT;
4065
3712
 
4066
- const targetPkCols = (PKS as any)[target] as string[];
3713
+ const targetPkCols = pkOf(target);
4067
3714
  const joinConditions = toTarget.from.map((jCol: string, i: number) => {
4068
3715
  return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
4069
3716
  }).join(' AND ');
@@ -4076,7 +3723,7 @@ export async function loadIncludes(
4076
3723
  \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
4077
3724
  FROM "\${via}" j
4078
3725
  INNER JOIN "\${target}" t ON \${joinConditions}
4079
- WHERE \${whereVia}
3726
+ WHERE \${whereVia}\${softDeleteFilter(target, "t")}
4080
3727
  ) __numbered
4081
3728
  WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
4082
3729
  \`;
@@ -4119,8 +3766,8 @@ export async function loadIncludes(
4119
3766
 
4120
3767
  // 2) Load targets by distinct target fk tuples in junction
4121
3768
  const tTuples = distinctTuples(jrows, toTarget.from);
4122
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
4123
- 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)}\`;
4124
3771
  const paramsT = tTuples.flat();
4125
3772
  log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
4126
3773
  const { rows: targets } = await pg.query(sqlT, paramsT);
@@ -4128,7 +3775,7 @@ export async function loadIncludes(
4128
3775
  // Apply select/exclude filtering
4129
3776
  const filteredTargets = filterFields(targets, options.select, options.exclude);
4130
3777
 
4131
- const tIdx = indexByTuple(filteredTargets, (PKS as any)[target]);
3778
+ const tIdx = indexByTuple(filteredTargets, pkOf(target));
4132
3779
 
4133
3780
  // 3) Group junction rows by current pk tuple, map to target rows
4134
3781
  const byCurr = groupByTuple(jrows, toCurr.from);
@@ -6552,9 +6199,14 @@ async function generate(configPath) {
6552
6199
  path: join2(serverDir, "include-builder.ts"),
6553
6200
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
6554
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
+ }));
6555
6207
  files.push({
6556
6208
  path: join2(serverDir, "include-loader.ts"),
6557
- content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
6209
+ content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
6558
6210
  });
6559
6211
  files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
6560
6212
  if (getAuthStrategy(normalizedAuth) !== "none") {
@@ -6580,7 +6232,7 @@ async function generate(configPath) {
6580
6232
  let routeContent;
6581
6233
  if (serverFramework === "hono") {
6582
6234
  routeContent = emitHonoRoutes(table, graph, {
6583
- softDeleteColumn: resolveSoftDeleteColumn(cfg, table.name),
6235
+ softDeleteColumn: softDeleteCols[table.name] ?? null,
6584
6236
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
6585
6237
  authStrategy: getAuthStrategy(normalizedAuth),
6586
6238
  useJsExtensions: cfg.useJsExtensions,