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