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