postgresdk 0.18.22 → 0.18.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/cli.js +284 -608
- package/dist/emit-include-loader.d.ts +6 -2
- package/dist/emit-sdk-contract.d.ts +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +285 -608
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
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 =
|
|
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:
|
|
890
|
-
|
|
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
|
-
|
|
905
|
-
|
|
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:
|
|
922
|
-
const
|
|
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: `
|
|
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: `
|
|
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('
|
|
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 =
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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(
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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(
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
lines.push(" SelectTableName, // Full record type");
|
|
1684
|
-
lines.push(" InsertTableName, // Create payload type");
|
|
1685
|
-
lines.push(" UpdateTableName // Update payload type");
|
|
1686
|
-
lines.push("} from './client/types/table_name';");
|
|
1687
|
-
lines.push("");
|
|
1688
|
-
lines.push("// Import all types");
|
|
1689
|
-
lines.push("import type * as Types from './client/types';");
|
|
1690
|
-
lines.push("```");
|
|
1691
|
-
lines.push("");
|
|
1346
|
+
lines.push(`## Type Imports
|
|
1347
|
+
|
|
1348
|
+
\`\`\`typescript
|
|
1349
|
+
import { SDK } from './client';
|
|
1350
|
+
import type { SelectTableName, InsertTableName, UpdateTableName } from './client/types/table_name';
|
|
1351
|
+
import type * as Types from './client/types';
|
|
1352
|
+
\`\`\`
|
|
1353
|
+
`);
|
|
1692
1354
|
return lines.join(`
|
|
1693
1355
|
`);
|
|
1694
1356
|
}
|
|
@@ -3273,7 +2935,8 @@ const deleteSchema = z.object({
|
|
|
3273
2935
|
|
|
3274
2936
|
const getByPkQuerySchema = z.object({
|
|
3275
2937
|
select: z.array(z.string()).min(1).optional(),
|
|
3276
|
-
exclude: z.array(z.string()).min(1).optional()
|
|
2938
|
+
exclude: z.array(z.string()).min(1).optional(),
|
|
2939
|
+
includeSoftDeleted: z.boolean().optional()
|
|
3277
2940
|
}).strict().refine(
|
|
3278
2941
|
(data) => !(data.select && data.exclude),
|
|
3279
2942
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -3288,7 +2951,8 @@ const listSchema = z.object({
|
|
|
3288
2951
|
offset: z.number().int().min(0).optional(),
|
|
3289
2952
|
orderBy: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
3290
2953
|
order: z.union([z.enum(["asc", "desc"]), z.array(z.enum(["asc", "desc"]))]).optional(),
|
|
3291
|
-
distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional()
|
|
2954
|
+
distinctOn: z.union([columnEnum, z.array(columnEnum)]).optional(),
|
|
2955
|
+
includeSoftDeleted: z.boolean().optional(),${hasVectorColumns ? `
|
|
3292
2956
|
vector: z.object({
|
|
3293
2957
|
field: z.string(),
|
|
3294
2958
|
query: z.array(z.number()),
|
|
@@ -3385,12 +3049,15 @@ ${hasAuth ? `
|
|
|
3385
3049
|
app.get(\`\${base}/${pkPath}\`, async (c) => {
|
|
3386
3050
|
${getPkParams}
|
|
3387
3051
|
|
|
3388
|
-
// Parse query params
|
|
3052
|
+
// Parse query params — coerce includeSoftDeleted string to boolean before Zod validation
|
|
3053
|
+
// (avoids Boolean("false")=true pitfall while keeping schema as single source of truth)
|
|
3389
3054
|
const selectParam = c.req.query("select");
|
|
3390
3055
|
const excludeParam = c.req.query("exclude");
|
|
3056
|
+
const includeSoftDeletedParam = c.req.query("includeSoftDeleted");
|
|
3391
3057
|
const queryData: any = {};
|
|
3392
3058
|
if (selectParam) queryData.select = selectParam.split(",");
|
|
3393
3059
|
if (excludeParam) queryData.exclude = excludeParam.split(",");
|
|
3060
|
+
if (includeSoftDeletedParam !== undefined) queryData.includeSoftDeleted = includeSoftDeletedParam === "true";
|
|
3394
3061
|
|
|
3395
3062
|
const queryParsed = getByPkQuerySchema.safeParse(queryData);
|
|
3396
3063
|
if (!queryParsed.success) {
|
|
@@ -3403,7 +3070,7 @@ ${hasAuth ? `
|
|
|
3403
3070
|
}
|
|
3404
3071
|
|
|
3405
3072
|
const ctx = { ...baseCtx, select: queryParsed.data.select, exclude: queryParsed.data.exclude };
|
|
3406
|
-
const result = await coreOps.getByPk(ctx, pkValues);
|
|
3073
|
+
const result = await coreOps.getByPk(ctx, pkValues, { includeSoftDeleted: queryParsed.data.includeSoftDeleted });
|
|
3407
3074
|
|
|
3408
3075
|
if (result.error) {
|
|
3409
3076
|
return c.json({ error: result.error }, result.status as any);
|
|
@@ -3909,14 +3576,14 @@ ${hasJsonbColumns ? ` /**
|
|
|
3909
3576
|
* @param options - Select specific fields to return
|
|
3910
3577
|
* @returns The record with only selected fields if found, null otherwise
|
|
3911
3578
|
*/
|
|
3912
|
-
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
|
|
3579
|
+
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
|
|
3913
3580
|
/**
|
|
3914
3581
|
* Get a ${table.name} record by primary key with field exclusion
|
|
3915
3582
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3916
3583
|
* @param options - Exclude specific fields from return
|
|
3917
3584
|
* @returns The record without excluded fields if found, null otherwise
|
|
3918
3585
|
*/
|
|
3919
|
-
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}<TJsonb>> | null>;
|
|
3586
|
+
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}<TJsonb>> | null>;
|
|
3920
3587
|
/**
|
|
3921
3588
|
* Get a ${table.name} record by primary key
|
|
3922
3589
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
@@ -3925,15 +3592,16 @@ ${hasJsonbColumns ? ` /**
|
|
|
3925
3592
|
* // With JSONB type override:
|
|
3926
3593
|
* const user = await client.getByPk<{ metadata: Metadata }>('user-id');
|
|
3927
3594
|
*/
|
|
3928
|
-
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?:
|
|
3595
|
+
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type}<TJsonb> | null>;
|
|
3929
3596
|
async getByPk<TJsonb extends Partial<Select${Type}> = {}>(
|
|
3930
3597
|
pk: ${pkType},
|
|
3931
|
-
options?: { select?: string[]; exclude?: string[] }
|
|
3598
|
+
options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
|
|
3932
3599
|
): Promise<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>> | null> {
|
|
3933
3600
|
const path = ${pkPathExpr};
|
|
3934
3601
|
const queryParams = new URLSearchParams();
|
|
3935
3602
|
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
3936
3603
|
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
3604
|
+
if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
|
|
3937
3605
|
const query = queryParams.toString();
|
|
3938
3606
|
const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
|
|
3939
3607
|
return this.get<Select${Type}<TJsonb> | null>(url);
|
|
@@ -3943,28 +3611,29 @@ ${hasJsonbColumns ? ` /**
|
|
|
3943
3611
|
* @param options - Select specific fields to return
|
|
3944
3612
|
* @returns The record with only selected fields if found, null otherwise
|
|
3945
3613
|
*/
|
|
3946
|
-
async getByPk(pk: ${pkType}, options: { select: string[] }): Promise<Partial<Select${Type}> | null>;
|
|
3614
|
+
async getByPk(pk: ${pkType}, options: { select: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
|
|
3947
3615
|
/**
|
|
3948
3616
|
* Get a ${table.name} record by primary key with field exclusion
|
|
3949
3617
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3950
3618
|
* @param options - Exclude specific fields from return
|
|
3951
3619
|
* @returns The record without excluded fields if found, null otherwise
|
|
3952
3620
|
*/
|
|
3953
|
-
async getByPk(pk: ${pkType}, options: { exclude: string[] }): Promise<Partial<Select${Type}> | null>;
|
|
3621
|
+
async getByPk(pk: ${pkType}, options: { exclude: string[]; includeSoftDeleted?: boolean }): Promise<Partial<Select${Type}> | null>;
|
|
3954
3622
|
/**
|
|
3955
3623
|
* Get a ${table.name} record by primary key
|
|
3956
3624
|
* @param pk - The primary key value${hasCompositePk ? "s" : ""}
|
|
3957
3625
|
* @returns The record with all fields if found, null otherwise
|
|
3958
3626
|
*/
|
|
3959
|
-
async getByPk(pk: ${pkType}, options?:
|
|
3627
|
+
async getByPk(pk: ${pkType}, options?: { includeSoftDeleted?: boolean }): Promise<Select${Type} | null>;
|
|
3960
3628
|
async getByPk(
|
|
3961
3629
|
pk: ${pkType},
|
|
3962
|
-
options?: { select?: string[]; exclude?: string[] }
|
|
3630
|
+
options?: { select?: string[]; exclude?: string[]; includeSoftDeleted?: boolean }
|
|
3963
3631
|
): Promise<Select${Type} | Partial<Select${Type}> | null> {
|
|
3964
3632
|
const path = ${pkPathExpr};
|
|
3965
3633
|
const queryParams = new URLSearchParams();
|
|
3966
3634
|
if (options?.select) queryParams.set('select', options.select.join(','));
|
|
3967
3635
|
if (options?.exclude) queryParams.set('exclude', options.exclude.join(','));
|
|
3636
|
+
if (options?.includeSoftDeleted) queryParams.set('includeSoftDeleted', 'true');
|
|
3968
3637
|
const query = queryParams.toString();
|
|
3969
3638
|
const url = query ? \`\${this.resource}/\${path}?\${query}\` : \`\${this.resource}/\${path}\`;
|
|
3970
3639
|
return this.get<Select${Type} | null>(url);
|
|
@@ -3991,6 +3660,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
3991
3660
|
orderBy?: string | string[];
|
|
3992
3661
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3993
3662
|
distinctOn?: string | string[];
|
|
3663
|
+
includeSoftDeleted?: boolean;
|
|
3994
3664
|
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3995
3665
|
/**
|
|
3996
3666
|
* List ${table.name} records with field exclusion
|
|
@@ -4013,6 +3683,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4013
3683
|
orderBy?: string | string[];
|
|
4014
3684
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4015
3685
|
distinctOn?: string | string[];
|
|
3686
|
+
includeSoftDeleted?: boolean;
|
|
4016
3687
|
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4017
3688
|
/**
|
|
4018
3689
|
* List ${table.name} records with pagination and filtering
|
|
@@ -4046,6 +3717,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4046
3717
|
orderBy?: string | string[];
|
|
4047
3718
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4048
3719
|
distinctOn?: string | string[];
|
|
3720
|
+
includeSoftDeleted?: boolean;
|
|
4049
3721
|
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4050
3722
|
async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
|
|
4051
3723
|
include?: ${Type}IncludeSpec;
|
|
@@ -4064,6 +3736,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4064
3736
|
orderBy?: string | string[];
|
|
4065
3737
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4066
3738
|
distinctOn?: string | string[];
|
|
3739
|
+
includeSoftDeleted?: boolean;
|
|
4067
3740
|
}): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
|
|
4068
3741
|
return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4069
3742
|
}` : ` /**
|
|
@@ -4087,6 +3760,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4087
3760
|
orderBy?: string | string[];
|
|
4088
3761
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4089
3762
|
distinctOn?: string | string[];
|
|
3763
|
+
includeSoftDeleted?: boolean;
|
|
4090
3764
|
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4091
3765
|
/**
|
|
4092
3766
|
* List ${table.name} records with field exclusion
|
|
@@ -4109,6 +3783,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4109
3783
|
orderBy?: string | string[];
|
|
4110
3784
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4111
3785
|
distinctOn?: string | string[];
|
|
3786
|
+
includeSoftDeleted?: boolean;
|
|
4112
3787
|
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4113
3788
|
/**
|
|
4114
3789
|
* List ${table.name} records with pagination and filtering
|
|
@@ -4136,6 +3811,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4136
3811
|
orderBy?: string | string[];
|
|
4137
3812
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4138
3813
|
distinctOn?: string | string[];
|
|
3814
|
+
includeSoftDeleted?: boolean;
|
|
4139
3815
|
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4140
3816
|
async list(params?: {
|
|
4141
3817
|
include?: ${Type}IncludeSpec;
|
|
@@ -4154,6 +3830,7 @@ ${hasJsonbColumns ? ` /**
|
|
|
4154
3830
|
orderBy?: string | string[];
|
|
4155
3831
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4156
3832
|
distinctOn?: string | string[];
|
|
3833
|
+
includeSoftDeleted?: boolean;
|
|
4157
3834
|
}): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
|
|
4158
3835
|
return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4159
3836
|
}`}
|
|
@@ -4563,7 +4240,8 @@ export abstract class BaseClient {
|
|
|
4563
4240
|
}
|
|
4564
4241
|
|
|
4565
4242
|
// src/emit-include-loader.ts
|
|
4566
|
-
function emitIncludeLoader(
|
|
4243
|
+
function emitIncludeLoader(model, maxDepth, opts = {}) {
|
|
4244
|
+
const { softDeleteCols = {}, useJsExtensions } = opts;
|
|
4567
4245
|
const fkIndex = {};
|
|
4568
4246
|
for (const t of Object.values(model.tables)) {
|
|
4569
4247
|
fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
|
|
@@ -4602,8 +4280,18 @@ const log = {
|
|
|
4602
4280
|
};
|
|
4603
4281
|
|
|
4604
4282
|
// Helpers for PK/FK discovery from model (inlined)
|
|
4605
|
-
const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)}
|
|
4606
|
-
const PKS = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)}
|
|
4283
|
+
const FK_INDEX: Record<string, Array<{from: string[]; toTable: string; to: string[]}>> = ${JSON.stringify(fkIndex, null, 2)};
|
|
4284
|
+
const PKS: Record<string, string[]> = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)};
|
|
4285
|
+
/** Soft-delete column per table, null if not applicable. Baked in at generation time. */
|
|
4286
|
+
const SOFT_DELETE_COLS: Record<string, string | null> = ${JSON.stringify(softDeleteCols, null, 2)};
|
|
4287
|
+
|
|
4288
|
+
/** Returns a SQL fragment like ' AND "col" IS NULL' for the given table, or "" if none. */
|
|
4289
|
+
function softDeleteFilter(table: string, alias?: string): string {
|
|
4290
|
+
const col = SOFT_DELETE_COLS[table];
|
|
4291
|
+
if (!col) return "";
|
|
4292
|
+
const ref = alias ? \`\${alias}."\${col}"\` : \`"\${col}"\`;
|
|
4293
|
+
return \` AND \${ref} IS NULL\`;
|
|
4294
|
+
}
|
|
4607
4295
|
|
|
4608
4296
|
// Build WHERE predicate for OR-of-AND on composite values
|
|
4609
4297
|
function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
|
|
@@ -4693,6 +4381,49 @@ function filterFields<T extends Record<string, any>>(
|
|
|
4693
4381
|
});
|
|
4694
4382
|
}
|
|
4695
4383
|
|
|
4384
|
+
/** Sentinel used as window-function LIMIT when no per-parent limit is requested. */
|
|
4385
|
+
const NO_LIMIT = 999_999_999;
|
|
4386
|
+
|
|
4387
|
+
/**
|
|
4388
|
+
* FK_INDEX and PKS are fully populated at code-gen time for all schema tables,
|
|
4389
|
+
* so lookups by TableName are always defined. These helpers assert non-null
|
|
4390
|
+
* in one place rather than scattering \`!\` or \`as any\` at every call site.
|
|
4391
|
+
*/
|
|
4392
|
+
function fksOf(table: string): Array<{from: string[]; toTable: string; to: string[]}> {
|
|
4393
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4394
|
+
return FK_INDEX[table]!;
|
|
4395
|
+
}
|
|
4396
|
+
function pkOf(table: string): string[] {
|
|
4397
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4398
|
+
return PKS[table]!;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
/** Parse relation options and nested child spec from a specValue entry. */
|
|
4402
|
+
function parseSpecOptions(specValue: any): { options: RelationOptions; childSpec: any } {
|
|
4403
|
+
const options: RelationOptions = {};
|
|
4404
|
+
let childSpec: any = undefined;
|
|
4405
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4406
|
+
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4407
|
+
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4408
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4409
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4410
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4411
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4412
|
+
if (specValue.include !== undefined) {
|
|
4413
|
+
childSpec = specValue.include;
|
|
4414
|
+
} else {
|
|
4415
|
+
// Support legacy format: { tags: true } alongside new: { include: { tags: true } }
|
|
4416
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4417
|
+
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4418
|
+
);
|
|
4419
|
+
if (nonOptionKeys.length > 0) {
|
|
4420
|
+
childSpec = Object.fromEntries(nonOptionKeys.map(k => [k, specValue[k]]));
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
return { options, childSpec };
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4696
4427
|
// Public entry
|
|
4697
4428
|
export async function loadIncludes(
|
|
4698
4429
|
root: TableName,
|
|
@@ -4732,37 +4463,7 @@ export async function loadIncludes(
|
|
|
4732
4463
|
// Safely run each loader; never let one bad edge 500 the route
|
|
4733
4464
|
if (rel.via) {
|
|
4734
4465
|
// M:N via junction
|
|
4735
|
-
const
|
|
4736
|
-
const options: RelationOptions = {};
|
|
4737
|
-
let childSpec: any = undefined;
|
|
4738
|
-
|
|
4739
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4740
|
-
// Extract options
|
|
4741
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4742
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4743
|
-
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4744
|
-
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4745
|
-
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4746
|
-
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4747
|
-
|
|
4748
|
-
// Extract nested spec - support both formats:
|
|
4749
|
-
// New: { limit: 3, include: { tags: true } }
|
|
4750
|
-
// Old: { tags: true } (backward compatibility)
|
|
4751
|
-
if (specValue.include !== undefined) {
|
|
4752
|
-
childSpec = specValue.include;
|
|
4753
|
-
} else {
|
|
4754
|
-
// Build childSpec from non-option keys
|
|
4755
|
-
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4756
|
-
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4757
|
-
);
|
|
4758
|
-
if (nonOptionKeys.length > 0) {
|
|
4759
|
-
childSpec = {};
|
|
4760
|
-
for (const k of nonOptionKeys) {
|
|
4761
|
-
childSpec[k] = specValue[k];
|
|
4762
|
-
}
|
|
4763
|
-
}
|
|
4764
|
-
}
|
|
4765
|
-
}
|
|
4466
|
+
const { options, childSpec } = parseSpecOptions(s[key]);
|
|
4766
4467
|
|
|
4767
4468
|
try {
|
|
4768
4469
|
await loadManyToMany(table, target, rel.via as string, rows, key, options);
|
|
@@ -4784,37 +4485,7 @@ export async function loadIncludes(
|
|
|
4784
4485
|
|
|
4785
4486
|
if (rel.kind === "many") {
|
|
4786
4487
|
// 1:N target has FK to current
|
|
4787
|
-
const
|
|
4788
|
-
const options: RelationOptions = {};
|
|
4789
|
-
let childSpec: any = undefined;
|
|
4790
|
-
|
|
4791
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4792
|
-
// Extract options
|
|
4793
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4794
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4795
|
-
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4796
|
-
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4797
|
-
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4798
|
-
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4799
|
-
|
|
4800
|
-
// Extract nested spec - support both formats:
|
|
4801
|
-
// New: { limit: 3, include: { tags: true } }
|
|
4802
|
-
// Old: { tags: true } (backward compatibility)
|
|
4803
|
-
if (specValue.include !== undefined) {
|
|
4804
|
-
childSpec = specValue.include;
|
|
4805
|
-
} else {
|
|
4806
|
-
// Build childSpec from non-option keys
|
|
4807
|
-
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4808
|
-
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4809
|
-
);
|
|
4810
|
-
if (nonOptionKeys.length > 0) {
|
|
4811
|
-
childSpec = {};
|
|
4812
|
-
for (const k of nonOptionKeys) {
|
|
4813
|
-
childSpec[k] = specValue[k];
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
}
|
|
4817
|
-
}
|
|
4488
|
+
const { options, childSpec } = parseSpecOptions(s[key]);
|
|
4818
4489
|
|
|
4819
4490
|
try {
|
|
4820
4491
|
await loadOneToMany(table, target, rows, key, options);
|
|
@@ -4834,20 +4505,11 @@ export async function loadIncludes(
|
|
|
4834
4505
|
} else {
|
|
4835
4506
|
// kind === "one"
|
|
4836
4507
|
// Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
|
|
4837
|
-
|
|
4838
|
-
const options:
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4842
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4843
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4844
|
-
// Support { include: TargetIncludeSpec } — mirrors the many/via handler
|
|
4845
|
-
if (specValue.include !== undefined) {
|
|
4846
|
-
childSpec = specValue.include;
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4508
|
+
// Destructure only select/exclude/childSpec — limit/offset/orderBy don't apply to 1:1 relations
|
|
4509
|
+
const { options: { select, exclude }, childSpec } = parseSpecOptions(s[key]);
|
|
4510
|
+
const options: RelationOptions = { select, exclude };
|
|
4849
4511
|
|
|
4850
|
-
const currFks = (
|
|
4512
|
+
const currFks = fksOf(table);
|
|
4851
4513
|
const toTarget = currFks.find(f => f.toTable === target);
|
|
4852
4514
|
if (toTarget) {
|
|
4853
4515
|
try {
|
|
@@ -4879,16 +4541,16 @@ export async function loadIncludes(
|
|
|
4879
4541
|
|
|
4880
4542
|
async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4881
4543
|
// current has FK cols referencing target PK
|
|
4882
|
-
const fk = (
|
|
4544
|
+
const fk = fksOf(curr).find(f => f.toTable === target);
|
|
4883
4545
|
if (!fk) { for (const r of rows) r[key] = null; return; }
|
|
4884
4546
|
const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
|
|
4885
4547
|
if (!tuples.length) { for (const r of rows) r[key] = null; return; }
|
|
4886
4548
|
|
|
4887
4549
|
// Query target WHERE target.pk IN tuples
|
|
4888
|
-
const pkCols = (
|
|
4550
|
+
const pkCols = pkOf(target);
|
|
4889
4551
|
const where = buildOrAndPredicate(pkCols, tuples.length, 1);
|
|
4890
4552
|
const params = tuples.flat();
|
|
4891
|
-
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4553
|
+
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4892
4554
|
log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
|
|
4893
4555
|
const { rows: targets } = await pg.query(sql, params);
|
|
4894
4556
|
|
|
@@ -4905,17 +4567,17 @@ export async function loadIncludes(
|
|
|
4905
4567
|
|
|
4906
4568
|
async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4907
4569
|
// target has FK cols referencing current PK (unique)
|
|
4908
|
-
const fk = (
|
|
4570
|
+
const fk = fksOf(target).find(f => f.toTable === curr);
|
|
4909
4571
|
if (!fk) { for (const r of rows) r[key] = null; return; }
|
|
4910
4572
|
|
|
4911
|
-
const pkCols = (
|
|
4573
|
+
const pkCols = pkOf(curr);
|
|
4912
4574
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4913
4575
|
if (!tuples.length) { for (const r of rows) r[key] = null; return; }
|
|
4914
4576
|
|
|
4915
4577
|
// SELECT target WHERE fk IN tuples
|
|
4916
4578
|
const where = buildOrAndPredicate(fk.from, tuples.length, 1);
|
|
4917
4579
|
const params = tuples.flat();
|
|
4918
|
-
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4580
|
+
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4919
4581
|
log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
|
|
4920
4582
|
const { rows: targets } = await pg.query(sql, params);
|
|
4921
4583
|
|
|
@@ -4931,10 +4593,10 @@ export async function loadIncludes(
|
|
|
4931
4593
|
|
|
4932
4594
|
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4933
4595
|
// target has FK cols referencing current PK
|
|
4934
|
-
const fk = (
|
|
4596
|
+
const fk = fksOf(target).find(f => f.toTable === curr);
|
|
4935
4597
|
if (!fk) { for (const r of rows) r[key] = []; return; }
|
|
4936
4598
|
|
|
4937
|
-
const pkCols = (
|
|
4599
|
+
const pkCols = pkOf(curr);
|
|
4938
4600
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4939
4601
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
4940
4602
|
|
|
@@ -4942,7 +4604,7 @@ export async function loadIncludes(
|
|
|
4942
4604
|
const params = tuples.flat();
|
|
4943
4605
|
|
|
4944
4606
|
// Build SQL with optional ORDER BY, LIMIT, OFFSET
|
|
4945
|
-
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4607
|
+
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4946
4608
|
|
|
4947
4609
|
// If limit/offset are needed, use window functions to limit per parent
|
|
4948
4610
|
if (options.limit !== undefined || options.offset !== undefined) {
|
|
@@ -4952,13 +4614,13 @@ export async function loadIncludes(
|
|
|
4952
4614
|
|
|
4953
4615
|
const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
|
|
4954
4616
|
const offset = options.offset ?? 0;
|
|
4955
|
-
const limit = options.limit ??
|
|
4617
|
+
const limit = options.limit ?? NO_LIMIT;
|
|
4956
4618
|
|
|
4957
4619
|
sql = \`
|
|
4958
4620
|
SELECT * FROM (
|
|
4959
4621
|
SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
|
|
4960
4622
|
FROM "\${target}"
|
|
4961
|
-
WHERE \${where}
|
|
4623
|
+
WHERE \${where}\${softDeleteFilter(target)}
|
|
4962
4624
|
) __sub
|
|
4963
4625
|
WHERE __rn > \${offset} AND __rn <= \${offset + limit}
|
|
4964
4626
|
\`;
|
|
@@ -4988,11 +4650,11 @@ export async function loadIncludes(
|
|
|
4988
4650
|
|
|
4989
4651
|
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4990
4652
|
// via has two FKs: one to curr, one to target
|
|
4991
|
-
const toCurr = (
|
|
4992
|
-
const toTarget = (
|
|
4653
|
+
const toCurr = fksOf(via).find(f => f.toTable === curr);
|
|
4654
|
+
const toTarget = fksOf(via).find(f => f.toTable === target);
|
|
4993
4655
|
if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
|
|
4994
4656
|
|
|
4995
|
-
const pkCols = (
|
|
4657
|
+
const pkCols = pkOf(curr);
|
|
4996
4658
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4997
4659
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
4998
4660
|
|
|
@@ -5007,9 +4669,9 @@ export async function loadIncludes(
|
|
|
5007
4669
|
|
|
5008
4670
|
const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
|
|
5009
4671
|
const offset = options.offset ?? 0;
|
|
5010
|
-
const limit = options.limit ??
|
|
4672
|
+
const limit = options.limit ?? NO_LIMIT;
|
|
5011
4673
|
|
|
5012
|
-
const targetPkCols = (
|
|
4674
|
+
const targetPkCols = pkOf(target);
|
|
5013
4675
|
const joinConditions = toTarget.from.map((jCol: string, i: number) => {
|
|
5014
4676
|
return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
|
|
5015
4677
|
}).join(' AND ');
|
|
@@ -5022,7 +4684,7 @@ export async function loadIncludes(
|
|
|
5022
4684
|
\${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
|
|
5023
4685
|
FROM "\${via}" j
|
|
5024
4686
|
INNER JOIN "\${target}" t ON \${joinConditions}
|
|
5025
|
-
WHERE \${whereVia}
|
|
4687
|
+
WHERE \${whereVia}\${softDeleteFilter(target, "t")}
|
|
5026
4688
|
) __numbered
|
|
5027
4689
|
WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
|
|
5028
4690
|
\`;
|
|
@@ -5065,8 +4727,8 @@ export async function loadIncludes(
|
|
|
5065
4727
|
|
|
5066
4728
|
// 2) Load targets by distinct target fk tuples in junction
|
|
5067
4729
|
const tTuples = distinctTuples(jrows, toTarget.from);
|
|
5068
|
-
const whereT = buildOrAndPredicate((
|
|
5069
|
-
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
|
|
4730
|
+
const whereT = buildOrAndPredicate(pkOf(target), tTuples.length, 1);
|
|
4731
|
+
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\${softDeleteFilter(target)}\`;
|
|
5070
4732
|
const paramsT = tTuples.flat();
|
|
5071
4733
|
log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
|
|
5072
4734
|
const { rows: targets } = await pg.query(sqlT, paramsT);
|
|
@@ -5074,7 +4736,7 @@ export async function loadIncludes(
|
|
|
5074
4736
|
// Apply select/exclude filtering
|
|
5075
4737
|
const filteredTargets = filterFields(targets, options.select, options.exclude);
|
|
5076
4738
|
|
|
5077
|
-
const tIdx = indexByTuple(filteredTargets, (
|
|
4739
|
+
const tIdx = indexByTuple(filteredTargets, pkOf(target));
|
|
5078
4740
|
|
|
5079
4741
|
// 3) Group junction rows by current pk tuple, map to target rows
|
|
5080
4742
|
const byCurr = groupByTuple(jrows, toCurr.from);
|
|
@@ -5980,7 +5642,8 @@ export async function createRecord(
|
|
|
5980
5642
|
*/
|
|
5981
5643
|
export async function getByPk(
|
|
5982
5644
|
ctx: OperationContext,
|
|
5983
|
-
pkValues: any[]
|
|
5645
|
+
pkValues: any[],
|
|
5646
|
+
opts?: { includeSoftDeleted?: boolean }
|
|
5984
5647
|
): Promise<{ data?: any; error?: string; status: number }> {
|
|
5985
5648
|
try {
|
|
5986
5649
|
const hasCompositePk = ctx.pkColumns.length > 1;
|
|
@@ -5989,7 +5652,8 @@ export async function getByPk(
|
|
|
5989
5652
|
: \`"\${ctx.pkColumns[0]}" = $1\`;
|
|
5990
5653
|
|
|
5991
5654
|
const columns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
5992
|
-
const
|
|
5655
|
+
const softDeleteFilter = ctx.softDeleteColumn && !opts?.includeSoftDeleted ? \` AND "\${ctx.softDeleteColumn}" IS NULL\` : "";
|
|
5656
|
+
const text = \`SELECT \${columns} FROM "\${ctx.table}" WHERE \${wherePkSql}\${softDeleteFilter} LIMIT 1\`;
|
|
5993
5657
|
log.debug(\`GET \${ctx.table} by PK:\`, pkValues, "SQL:", text);
|
|
5994
5658
|
|
|
5995
5659
|
const { rows } = await ctx.pg.query(text, pkValues);
|
|
@@ -6362,6 +6026,7 @@ export async function listRecords(
|
|
|
6362
6026
|
orderBy?: string | string[];
|
|
6363
6027
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
6364
6028
|
distinctOn?: string | string[];
|
|
6029
|
+
includeSoftDeleted?: boolean;
|
|
6365
6030
|
vector?: {
|
|
6366
6031
|
field: string;
|
|
6367
6032
|
query: number[];
|
|
@@ -6372,7 +6037,7 @@ export async function listRecords(
|
|
|
6372
6037
|
}
|
|
6373
6038
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
6374
6039
|
try {
|
|
6375
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
|
|
6040
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn, includeSoftDeleted } = params;
|
|
6376
6041
|
|
|
6377
6042
|
// DISTINCT ON support
|
|
6378
6043
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
@@ -6408,8 +6073,8 @@ export async function listRecords(
|
|
|
6408
6073
|
const whereParts: string[] = [];
|
|
6409
6074
|
let whereParams: any[] = [];
|
|
6410
6075
|
|
|
6411
|
-
// Add soft delete filter if applicable
|
|
6412
|
-
if (ctx.softDeleteColumn) {
|
|
6076
|
+
// Add soft delete filter if applicable (skip if caller opts into seeing soft-deleted records)
|
|
6077
|
+
if (ctx.softDeleteColumn && !includeSoftDeleted) {
|
|
6413
6078
|
whereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
6414
6079
|
}
|
|
6415
6080
|
|
|
@@ -6447,7 +6112,7 @@ export async function listRecords(
|
|
|
6447
6112
|
|
|
6448
6113
|
if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
|
|
6449
6114
|
const countWhereParts: string[] = [];
|
|
6450
|
-
if (ctx.softDeleteColumn) {
|
|
6115
|
+
if (ctx.softDeleteColumn && !includeSoftDeleted) {
|
|
6451
6116
|
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
6452
6117
|
}
|
|
6453
6118
|
if (whereClause) {
|
|
@@ -7418,6 +7083,12 @@ init_utils();
|
|
|
7418
7083
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
7419
7084
|
var __dirname2 = dirname2(__filename2);
|
|
7420
7085
|
var { version: CLI_VERSION } = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
|
|
7086
|
+
function resolveSoftDeleteColumn(cfg, tableName) {
|
|
7087
|
+
const overrides = cfg.softDeleteColumnOverrides;
|
|
7088
|
+
if (overrides && tableName in overrides)
|
|
7089
|
+
return overrides[tableName] ?? null;
|
|
7090
|
+
return cfg.softDeleteColumn ?? null;
|
|
7091
|
+
}
|
|
7421
7092
|
async function generate(configPath) {
|
|
7422
7093
|
if (!existsSync2(configPath)) {
|
|
7423
7094
|
throw new Error(`Config file not found: ${configPath}
|
|
@@ -7489,9 +7160,14 @@ async function generate(configPath) {
|
|
|
7489
7160
|
path: join2(serverDir, "include-builder.ts"),
|
|
7490
7161
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
7491
7162
|
});
|
|
7163
|
+
const softDeleteCols = Object.fromEntries(Object.values(model.tables).map((t) => {
|
|
7164
|
+
const col = resolveSoftDeleteColumn(cfg, t.name);
|
|
7165
|
+
const validated = col && t.columns.some((c) => c.name === col) ? col : null;
|
|
7166
|
+
return [t.name, validated];
|
|
7167
|
+
}));
|
|
7492
7168
|
files.push({
|
|
7493
7169
|
path: join2(serverDir, "include-loader.ts"),
|
|
7494
|
-
content: emitIncludeLoader(
|
|
7170
|
+
content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
|
|
7495
7171
|
});
|
|
7496
7172
|
files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
|
|
7497
7173
|
if (getAuthStrategy(normalizedAuth) !== "none") {
|
|
@@ -7517,7 +7193,7 @@ async function generate(configPath) {
|
|
|
7517
7193
|
let routeContent;
|
|
7518
7194
|
if (serverFramework === "hono") {
|
|
7519
7195
|
routeContent = emitHonoRoutes(table, graph, {
|
|
7520
|
-
softDeleteColumn:
|
|
7196
|
+
softDeleteColumn: softDeleteCols[table.name] ?? null,
|
|
7521
7197
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
7522
7198
|
authStrategy: getAuthStrategy(normalizedAuth),
|
|
7523
7199
|
useJsExtensions: cfg.useJsExtensions,
|