postgresdk 0.18.23 → 0.18.25

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
  }
@@ -4578,7 +4240,8 @@ export abstract class BaseClient {
4578
4240
  }
4579
4241
 
4580
4242
  // src/emit-include-loader.ts
4581
- function emitIncludeLoader(graph, model, maxDepth, useJsExtensions) {
4243
+ function emitIncludeLoader(model, maxDepth, opts = {}) {
4244
+ const { softDeleteCols = {}, useJsExtensions } = opts;
4582
4245
  const fkIndex = {};
4583
4246
  for (const t of Object.values(model.tables)) {
4584
4247
  fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
@@ -4617,8 +4280,18 @@ const log = {
4617
4280
  };
4618
4281
 
4619
4282
  // Helpers for PK/FK discovery from model (inlined)
4620
- const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)} as const;
4621
- 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
+ }
4622
4295
 
4623
4296
  // Build WHERE predicate for OR-of-AND on composite values
4624
4297
  function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
@@ -4708,6 +4381,49 @@ function filterFields<T extends Record<string, any>>(
4708
4381
  });
4709
4382
  }
4710
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
+
4711
4427
  // Public entry
4712
4428
  export async function loadIncludes(
4713
4429
  root: TableName,
@@ -4747,37 +4463,7 @@ export async function loadIncludes(
4747
4463
  // Safely run each loader; never let one bad edge 500 the route
4748
4464
  if (rel.via) {
4749
4465
  // M:N via junction
4750
- const specValue = s[key];
4751
- const options: RelationOptions = {};
4752
- let childSpec: any = undefined;
4753
-
4754
- if (specValue && typeof specValue === "object" && specValue !== true) {
4755
- // Extract options
4756
- if (specValue.select !== undefined) options.select = specValue.select;
4757
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4758
- if (specValue.limit !== undefined) options.limit = specValue.limit;
4759
- if (specValue.offset !== undefined) options.offset = specValue.offset;
4760
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4761
- if (specValue.order !== undefined) options.order = specValue.order;
4762
-
4763
- // Extract nested spec - support both formats:
4764
- // New: { limit: 3, include: { tags: true } }
4765
- // Old: { tags: true } (backward compatibility)
4766
- if (specValue.include !== undefined) {
4767
- childSpec = specValue.include;
4768
- } else {
4769
- // Build childSpec from non-option keys
4770
- const nonOptionKeys = Object.keys(specValue).filter(
4771
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4772
- );
4773
- if (nonOptionKeys.length > 0) {
4774
- childSpec = {};
4775
- for (const k of nonOptionKeys) {
4776
- childSpec[k] = specValue[k];
4777
- }
4778
- }
4779
- }
4780
- }
4466
+ const { options, childSpec } = parseSpecOptions(s[key]);
4781
4467
 
4782
4468
  try {
4783
4469
  await loadManyToMany(table, target, rel.via as string, rows, key, options);
@@ -4799,37 +4485,7 @@ export async function loadIncludes(
4799
4485
 
4800
4486
  if (rel.kind === "many") {
4801
4487
  // 1:N target has FK to current
4802
- const specValue = s[key];
4803
- const options: RelationOptions = {};
4804
- let childSpec: any = undefined;
4805
-
4806
- if (specValue && typeof specValue === "object" && specValue !== true) {
4807
- // Extract options
4808
- if (specValue.select !== undefined) options.select = specValue.select;
4809
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4810
- if (specValue.limit !== undefined) options.limit = specValue.limit;
4811
- if (specValue.offset !== undefined) options.offset = specValue.offset;
4812
- if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
4813
- if (specValue.order !== undefined) options.order = specValue.order;
4814
-
4815
- // Extract nested spec - support both formats:
4816
- // New: { limit: 3, include: { tags: true } }
4817
- // Old: { tags: true } (backward compatibility)
4818
- if (specValue.include !== undefined) {
4819
- childSpec = specValue.include;
4820
- } else {
4821
- // Build childSpec from non-option keys
4822
- const nonOptionKeys = Object.keys(specValue).filter(
4823
- k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
4824
- );
4825
- if (nonOptionKeys.length > 0) {
4826
- childSpec = {};
4827
- for (const k of nonOptionKeys) {
4828
- childSpec[k] = specValue[k];
4829
- }
4830
- }
4831
- }
4832
- }
4488
+ const { options, childSpec } = parseSpecOptions(s[key]);
4833
4489
 
4834
4490
  try {
4835
4491
  await loadOneToMany(table, target, rows, key, options);
@@ -4849,20 +4505,11 @@ export async function loadIncludes(
4849
4505
  } else {
4850
4506
  // kind === "one"
4851
4507
  // Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
4852
- const specValue = s[key];
4853
- const options: RelationOptions = {};
4854
- let childSpec: any = undefined;
4855
-
4856
- if (specValue && typeof specValue === "object" && specValue !== true) {
4857
- if (specValue.select !== undefined) options.select = specValue.select;
4858
- if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
4859
- // Support { include: TargetIncludeSpec } — mirrors the many/via handler
4860
- if (specValue.include !== undefined) {
4861
- childSpec = specValue.include;
4862
- }
4863
- }
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 };
4864
4511
 
4865
- const currFks = (FK_INDEX as any)[table] as Array<{from:string[];toTable:string;to:string[]}>;
4512
+ const currFks = fksOf(table);
4866
4513
  const toTarget = currFks.find(f => f.toTable === target);
4867
4514
  if (toTarget) {
4868
4515
  try {
@@ -4894,16 +4541,16 @@ export async function loadIncludes(
4894
4541
 
4895
4542
  async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4896
4543
  // current has FK cols referencing target PK
4897
- const fk = (FK_INDEX as any)[curr].find((f: any) => f.toTable === target);
4544
+ const fk = fksOf(curr).find(f => f.toTable === target);
4898
4545
  if (!fk) { for (const r of rows) r[key] = null; return; }
4899
4546
  const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
4900
4547
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
4901
4548
 
4902
4549
  // Query target WHERE target.pk IN tuples
4903
- const pkCols = (PKS as any)[target] as string[];
4550
+ const pkCols = pkOf(target);
4904
4551
  const where = buildOrAndPredicate(pkCols, tuples.length, 1);
4905
4552
  const params = tuples.flat();
4906
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4553
+ const sql = \`SELECT * FROM "\${target}" WHERE (\${where})\${softDeleteFilter(target)}\`;
4907
4554
  log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
4908
4555
  const { rows: targets } = await pg.query(sql, params);
4909
4556
 
@@ -4920,17 +4567,17 @@ export async function loadIncludes(
4920
4567
 
4921
4568
  async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4922
4569
  // target has FK cols referencing current PK (unique)
4923
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
4570
+ const fk = fksOf(target).find(f => f.toTable === curr);
4924
4571
  if (!fk) { for (const r of rows) r[key] = null; return; }
4925
4572
 
4926
- const pkCols = (PKS as any)[curr] as string[];
4573
+ const pkCols = pkOf(curr);
4927
4574
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4928
4575
  if (!tuples.length) { for (const r of rows) r[key] = null; return; }
4929
4576
 
4930
4577
  // SELECT target WHERE fk IN tuples
4931
4578
  const where = buildOrAndPredicate(fk.from, tuples.length, 1);
4932
4579
  const params = tuples.flat();
4933
- const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4580
+ const sql = \`SELECT * FROM "\${target}" WHERE (\${where})\${softDeleteFilter(target)}\`;
4934
4581
  log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
4935
4582
  const { rows: targets } = await pg.query(sql, params);
4936
4583
 
@@ -4946,10 +4593,10 @@ export async function loadIncludes(
4946
4593
 
4947
4594
  async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
4948
4595
  // target has FK cols referencing current PK
4949
- const fk = (FK_INDEX as any)[target].find((f: any) => f.toTable === curr);
4596
+ const fk = fksOf(target).find(f => f.toTable === curr);
4950
4597
  if (!fk) { for (const r of rows) r[key] = []; return; }
4951
4598
 
4952
- const pkCols = (PKS as any)[curr] as string[];
4599
+ const pkCols = pkOf(curr);
4953
4600
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
4954
4601
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
4955
4602
 
@@ -4957,7 +4604,7 @@ export async function loadIncludes(
4957
4604
  const params = tuples.flat();
4958
4605
 
4959
4606
  // Build SQL with optional ORDER BY, LIMIT, OFFSET
4960
- let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
4607
+ let sql = \`SELECT * FROM "\${target}" WHERE (\${where})\${softDeleteFilter(target)}\`;
4961
4608
 
4962
4609
  // If limit/offset are needed, use window functions to limit per parent
4963
4610
  if (options.limit !== undefined || options.offset !== undefined) {
@@ -4967,13 +4614,13 @@ export async function loadIncludes(
4967
4614
 
4968
4615
  const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
4969
4616
  const offset = options.offset ?? 0;
4970
- const limit = options.limit ?? 999999999;
4617
+ const limit = options.limit ?? NO_LIMIT;
4971
4618
 
4972
4619
  sql = \`
4973
4620
  SELECT * FROM (
4974
4621
  SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
4975
4622
  FROM "\${target}"
4976
- WHERE \${where}
4623
+ WHERE (\${where})\${softDeleteFilter(target)}
4977
4624
  ) __sub
4978
4625
  WHERE __rn > \${offset} AND __rn <= \${offset + limit}
4979
4626
  \`;
@@ -5003,11 +4650,11 @@ export async function loadIncludes(
5003
4650
 
5004
4651
  async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
5005
4652
  // via has two FKs: one to curr, one to target
5006
- const toCurr = (FK_INDEX as any)[via].find((f: any) => f.toTable === curr);
5007
- 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);
5008
4655
  if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
5009
4656
 
5010
- const pkCols = (PKS as any)[curr] as string[];
4657
+ const pkCols = pkOf(curr);
5011
4658
  const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
5012
4659
  if (!tuples.length) { for (const r of rows) r[key] = []; return; }
5013
4660
 
@@ -5022,9 +4669,9 @@ export async function loadIncludes(
5022
4669
 
5023
4670
  const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
5024
4671
  const offset = options.offset ?? 0;
5025
- const limit = options.limit ?? 999999999;
4672
+ const limit = options.limit ?? NO_LIMIT;
5026
4673
 
5027
- const targetPkCols = (PKS as any)[target] as string[];
4674
+ const targetPkCols = pkOf(target);
5028
4675
  const joinConditions = toTarget.from.map((jCol: string, i: number) => {
5029
4676
  return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
5030
4677
  }).join(' AND ');
@@ -5037,7 +4684,7 @@ export async function loadIncludes(
5037
4684
  \${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
5038
4685
  FROM "\${via}" j
5039
4686
  INNER JOIN "\${target}" t ON \${joinConditions}
5040
- WHERE \${whereVia}
4687
+ WHERE (\${whereVia})\${softDeleteFilter(target, "t")}
5041
4688
  ) __numbered
5042
4689
  WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
5043
4690
  \`;
@@ -5080,8 +4727,8 @@ export async function loadIncludes(
5080
4727
 
5081
4728
  // 2) Load targets by distinct target fk tuples in junction
5082
4729
  const tTuples = distinctTuples(jrows, toTarget.from);
5083
- const whereT = buildOrAndPredicate((PKS as any)[target], tTuples.length, 1);
5084
- 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)}\`;
5085
4732
  const paramsT = tTuples.flat();
5086
4733
  log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
5087
4734
  const { rows: targets } = await pg.query(sqlT, paramsT);
@@ -5089,7 +4736,7 @@ export async function loadIncludes(
5089
4736
  // Apply select/exclude filtering
5090
4737
  const filteredTargets = filterFields(targets, options.select, options.exclude);
5091
4738
 
5092
- const tIdx = indexByTuple(filteredTargets, (PKS as any)[target]);
4739
+ const tIdx = indexByTuple(filteredTargets, pkOf(target));
5093
4740
 
5094
4741
  // 3) Group junction rows by current pk tuple, map to target rows
5095
4742
  const byCurr = groupByTuple(jrows, toCurr.from);
@@ -7513,9 +7160,14 @@ async function generate(configPath) {
7513
7160
  path: join2(serverDir, "include-builder.ts"),
7514
7161
  content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
7515
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
+ }));
7516
7168
  files.push({
7517
7169
  path: join2(serverDir, "include-loader.ts"),
7518
- content: emitIncludeLoader(graph, model, cfg.includeMethodsDepth || 2, cfg.useJsExtensions)
7170
+ content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
7519
7171
  });
7520
7172
  files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
7521
7173
  if (getAuthStrategy(normalizedAuth) !== "none") {
@@ -7541,7 +7193,7 @@ async function generate(configPath) {
7541
7193
  let routeContent;
7542
7194
  if (serverFramework === "hono") {
7543
7195
  routeContent = emitHonoRoutes(table, graph, {
7544
- softDeleteColumn: resolveSoftDeleteColumn(cfg, table.name),
7196
+ softDeleteColumn: softDeleteCols[table.name] ?? null,
7545
7197
  includeMethodsDepth: cfg.includeMethodsDepth || 2,
7546
7198
  authStrategy: getAuthStrategy(normalizedAuth),
7547
7199
  useJsExtensions: cfg.useJsExtensions,