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/cli.js
CHANGED
|
@@ -750,7 +750,7 @@ __export(exports_emit_sdk_contract, {
|
|
|
750
750
|
function generateUnifiedContract(model, config, graph) {
|
|
751
751
|
const resources = [];
|
|
752
752
|
const relationships = [];
|
|
753
|
-
const tables =
|
|
753
|
+
const tables = Object.values(model.tables);
|
|
754
754
|
if (process.env.SDK_DEBUG) {
|
|
755
755
|
console.log(`[SDK Contract] Processing ${tables.length} tables`);
|
|
756
756
|
}
|
|
@@ -880,30 +880,23 @@ function generateResourceWithSDK(table, model, graph, config) {
|
|
|
880
880
|
const hasSinglePK = table.pk.length === 1;
|
|
881
881
|
const pkField = hasSinglePK ? table.pk[0] : "id";
|
|
882
882
|
const enums = model.enums || {};
|
|
883
|
+
const overrides = config?.softDeleteColumnOverrides;
|
|
884
|
+
const resolvedSoftDeleteCol = overrides && tableName in overrides ? overrides[tableName] ?? null : config?.softDeleteColumn ?? null;
|
|
885
|
+
const softDeleteCol = resolvedSoftDeleteCol && table.columns.some((c) => c.name === resolvedSoftDeleteCol) ? resolvedSoftDeleteCol : null;
|
|
883
886
|
const sdkMethods = [];
|
|
884
887
|
const endpoints = [];
|
|
885
888
|
sdkMethods.push({
|
|
886
889
|
name: "list",
|
|
887
890
|
signature: `list(params?: ListParams): Promise<PaginatedResponse<${Type}>>`,
|
|
888
891
|
description: `List ${tableName} with filtering, sorting, and pagination. Returns paginated results with metadata.`,
|
|
889
|
-
example:
|
|
890
|
-
|
|
891
|
-
console.log(result.data); // array of records
|
|
892
|
-
console.log(result.total); // total matching records
|
|
893
|
-
console.log(result.hasMore); // true if more pages available
|
|
894
|
-
|
|
895
|
-
// With filters and pagination
|
|
896
|
-
const filtered = await sdk.${tableName}.list({
|
|
897
|
-
limit: 20,
|
|
898
|
-
offset: 0,
|
|
899
|
-
where: { ${table.columns[0]?.name || "field"}: { $like: '%search%' } },
|
|
892
|
+
example: `const result = await sdk.${tableName}.list({
|
|
893
|
+
where: { ${table.columns[0]?.name || "id"}: { $ilike: '%value%' } },
|
|
900
894
|
orderBy: '${table.columns[0]?.name || "created_at"}',
|
|
901
|
-
order: 'desc'
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
895
|
+
order: 'desc',
|
|
896
|
+
limit: 20,
|
|
897
|
+
offset: 0${softDeleteCol ? `,
|
|
898
|
+
includeSoftDeleted: false` : ""}
|
|
899
|
+
}); // result.data, result.total, result.hasMore`,
|
|
907
900
|
correspondsTo: `GET ${basePath}`
|
|
908
901
|
});
|
|
909
902
|
endpoints.push({
|
|
@@ -918,13 +911,8 @@ const currentPage = Math.floor(filtered.offset / filtered.limit) + 1;`,
|
|
|
918
911
|
name: "getByPk",
|
|
919
912
|
signature: `getByPk(${pkField}: string): Promise<${Type} | null>`,
|
|
920
913
|
description: `Get a single ${tableName} by primary key`,
|
|
921
|
-
example:
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
// Check if exists
|
|
925
|
-
if (item === null) {
|
|
926
|
-
console.log('Not found');
|
|
927
|
-
}`,
|
|
914
|
+
example: `const item = await sdk.${tableName}.getByPk('id');${softDeleteCol ? `
|
|
915
|
+
const withDeleted = await sdk.${tableName}.getByPk('id', { includeSoftDeleted: true });` : ""} // null if not found`,
|
|
928
916
|
correspondsTo: `GET ${basePath}/:${pkField}`
|
|
929
917
|
});
|
|
930
918
|
endpoints.push({
|
|
@@ -938,14 +926,9 @@ if (item === null) {
|
|
|
938
926
|
name: "create",
|
|
939
927
|
signature: `create(data: Insert${Type}): Promise<${Type}>`,
|
|
940
928
|
description: `Create a new ${tableName}`,
|
|
941
|
-
example: `
|
|
942
|
-
|
|
943
|
-
const newItem: Insert${Type} = {
|
|
929
|
+
example: `const created = await sdk.${tableName}.create({
|
|
944
930
|
${generateExampleFields(table, "create")}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
const created = await sdk.${tableName}.create(newItem);
|
|
948
|
-
console.log('Created:', created.${pkField});`,
|
|
931
|
+
});`,
|
|
949
932
|
correspondsTo: `POST ${basePath}`
|
|
950
933
|
});
|
|
951
934
|
endpoints.push({
|
|
@@ -960,13 +943,9 @@ console.log('Created:', created.${pkField});`,
|
|
|
960
943
|
name: "update",
|
|
961
944
|
signature: `update(${pkField}: string, data: Update${Type}): Promise<${Type}>`,
|
|
962
945
|
description: `Update an existing ${tableName}`,
|
|
963
|
-
example: `
|
|
964
|
-
|
|
965
|
-
const updates: Update${Type} = {
|
|
946
|
+
example: `const updated = await sdk.${tableName}.update('id', {
|
|
966
947
|
${generateExampleFields(table, "update")}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const updated = await sdk.${tableName}.update('123', updates);`,
|
|
948
|
+
});`,
|
|
970
949
|
correspondsTo: `PATCH ${basePath}/:${pkField}`
|
|
971
950
|
});
|
|
972
951
|
endpoints.push({
|
|
@@ -982,8 +961,7 @@ const updated = await sdk.${tableName}.update('123', updates);`,
|
|
|
982
961
|
name: "delete",
|
|
983
962
|
signature: `delete(${pkField}: string): Promise<${Type}>`,
|
|
984
963
|
description: `Delete a ${tableName}`,
|
|
985
|
-
example: `const deleted = await sdk.${tableName}.delete('
|
|
986
|
-
console.log('Deleted:', deleted);`,
|
|
964
|
+
example: `const deleted = await sdk.${tableName}.delete('id');`,
|
|
987
965
|
correspondsTo: `DELETE ${basePath}/:${pkField}`
|
|
988
966
|
});
|
|
989
967
|
endpoints.push({
|
|
@@ -994,29 +972,17 @@ console.log('Deleted:', deleted);`,
|
|
|
994
972
|
});
|
|
995
973
|
}
|
|
996
974
|
if (graph && config) {
|
|
997
|
-
const allTables =
|
|
975
|
+
const allTables = Object.values(model.tables);
|
|
998
976
|
const includeMethods = generateIncludeMethods(table, graph, {
|
|
999
977
|
maxDepth: config.includeMethodsDepth ?? 2,
|
|
1000
978
|
skipJunctionTables: config.skipJunctionTables ?? true
|
|
1001
979
|
}, allTables);
|
|
1002
980
|
for (const method of includeMethods) {
|
|
1003
981
|
const isGetByPk = method.name.startsWith("getByPk");
|
|
1004
|
-
const exampleCall = isGetByPk ? `const result = await sdk.${tableName}.${method.name}('123e4567-e89b-12d3-a456-426614174000');` : `const result = await sdk.${tableName}.${method.name}();
|
|
1005
|
-
console.log(result.data); // array of records with includes
|
|
1006
|
-
console.log(result.total); // total count
|
|
1007
|
-
console.log(result.hasMore); // more pages available
|
|
1008
|
-
|
|
1009
|
-
// With filters and pagination
|
|
1010
|
-
const filtered = await sdk.${tableName}.${method.name}({
|
|
1011
|
-
limit: 20,
|
|
1012
|
-
offset: 0,
|
|
1013
|
-
where: { /* filter conditions */ }
|
|
1014
|
-
});`;
|
|
1015
982
|
sdkMethods.push({
|
|
1016
983
|
name: method.name,
|
|
1017
984
|
signature: `${method.name}(${isGetByPk ? `${pkField}: string` : "params?: ListParams"}): ${method.returnType}`,
|
|
1018
985
|
description: `Get ${tableName} with included ${method.path.join(", ")} data`,
|
|
1019
|
-
example: exampleCall,
|
|
1020
986
|
correspondsTo: `POST ${basePath}/list`
|
|
1021
987
|
});
|
|
1022
988
|
}
|
|
@@ -1053,79 +1019,68 @@ function generateFieldContract(column, table, enums) {
|
|
|
1053
1019
|
}
|
|
1054
1020
|
return field;
|
|
1055
1021
|
}
|
|
1022
|
+
function pgTypeCategory(t) {
|
|
1023
|
+
switch (t) {
|
|
1024
|
+
case "int":
|
|
1025
|
+
case "int2":
|
|
1026
|
+
case "int4":
|
|
1027
|
+
case "int8":
|
|
1028
|
+
case "integer":
|
|
1029
|
+
case "smallint":
|
|
1030
|
+
case "bigint":
|
|
1031
|
+
case "decimal":
|
|
1032
|
+
case "numeric":
|
|
1033
|
+
case "real":
|
|
1034
|
+
case "float4":
|
|
1035
|
+
case "float8":
|
|
1036
|
+
case "double precision":
|
|
1037
|
+
case "float":
|
|
1038
|
+
return "number";
|
|
1039
|
+
case "boolean":
|
|
1040
|
+
case "bool":
|
|
1041
|
+
return "boolean";
|
|
1042
|
+
case "date":
|
|
1043
|
+
case "timestamp":
|
|
1044
|
+
case "timestamptz":
|
|
1045
|
+
return "date";
|
|
1046
|
+
case "json":
|
|
1047
|
+
case "jsonb":
|
|
1048
|
+
return "json";
|
|
1049
|
+
case "uuid":
|
|
1050
|
+
return "uuid";
|
|
1051
|
+
case "text[]":
|
|
1052
|
+
case "varchar[]":
|
|
1053
|
+
case "_text":
|
|
1054
|
+
case "_varchar":
|
|
1055
|
+
return "string[]";
|
|
1056
|
+
case "int[]":
|
|
1057
|
+
case "integer[]":
|
|
1058
|
+
case "_int":
|
|
1059
|
+
case "_int2":
|
|
1060
|
+
case "_int4":
|
|
1061
|
+
case "_int8":
|
|
1062
|
+
case "_integer":
|
|
1063
|
+
return "number[]";
|
|
1064
|
+
case "vector":
|
|
1065
|
+
return "number[]";
|
|
1066
|
+
default:
|
|
1067
|
+
return "string";
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1056
1070
|
function postgresTypeToTsType(column, enums) {
|
|
1057
1071
|
const pgType = column.pgType.toLowerCase();
|
|
1058
1072
|
if (enums[pgType]) {
|
|
1059
1073
|
const enumType = enums[pgType].map((v) => `"${v}"`).join(" | ");
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const enumName = pgType.slice(1);
|
|
1067
|
-
const enumValues = enums[enumName];
|
|
1068
|
-
if (enumValues) {
|
|
1069
|
-
const enumType = enumValues.map((v) => `"${v}"`).join(" | ");
|
|
1070
|
-
const arrayType = `(${enumType})[]`;
|
|
1071
|
-
if (column.nullable) {
|
|
1072
|
-
return `${arrayType} | null`;
|
|
1073
|
-
}
|
|
1074
|
-
return arrayType;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
const baseType = (() => {
|
|
1078
|
-
switch (pgType) {
|
|
1079
|
-
case "int":
|
|
1080
|
-
case "int2":
|
|
1081
|
-
case "int4":
|
|
1082
|
-
case "int8":
|
|
1083
|
-
case "integer":
|
|
1084
|
-
case "smallint":
|
|
1085
|
-
case "bigint":
|
|
1086
|
-
case "decimal":
|
|
1087
|
-
case "numeric":
|
|
1088
|
-
case "real":
|
|
1089
|
-
case "float4":
|
|
1090
|
-
case "float8":
|
|
1091
|
-
case "double precision":
|
|
1092
|
-
case "float":
|
|
1093
|
-
return "number";
|
|
1094
|
-
case "boolean":
|
|
1095
|
-
case "bool":
|
|
1096
|
-
return "boolean";
|
|
1097
|
-
case "date":
|
|
1098
|
-
case "timestamp":
|
|
1099
|
-
case "timestamptz":
|
|
1100
|
-
return "string";
|
|
1101
|
-
case "json":
|
|
1102
|
-
case "jsonb":
|
|
1103
|
-
return "JsonValue";
|
|
1104
|
-
case "uuid":
|
|
1105
|
-
return "string";
|
|
1106
|
-
case "text[]":
|
|
1107
|
-
case "varchar[]":
|
|
1108
|
-
case "_text":
|
|
1109
|
-
case "_varchar":
|
|
1110
|
-
return "string[]";
|
|
1111
|
-
case "int[]":
|
|
1112
|
-
case "integer[]":
|
|
1113
|
-
case "_int":
|
|
1114
|
-
case "_int2":
|
|
1115
|
-
case "_int4":
|
|
1116
|
-
case "_int8":
|
|
1117
|
-
case "_integer":
|
|
1118
|
-
return "number[]";
|
|
1119
|
-
case "vector":
|
|
1120
|
-
return "number[]";
|
|
1121
|
-
default:
|
|
1122
|
-
return "string";
|
|
1123
|
-
}
|
|
1124
|
-
})();
|
|
1125
|
-
if (column.nullable) {
|
|
1126
|
-
return `${baseType} | null`;
|
|
1074
|
+
return column.nullable ? `${enumType} | null` : enumType;
|
|
1075
|
+
}
|
|
1076
|
+
const enumArrayValues = enums[pgType.slice(1)];
|
|
1077
|
+
if (pgType.startsWith("_") && enumArrayValues) {
|
|
1078
|
+
const arrayType = `(${enumArrayValues.map((v) => `"${v}"`).join(" | ")})[]`;
|
|
1079
|
+
return column.nullable ? `${arrayType} | null` : arrayType;
|
|
1127
1080
|
}
|
|
1128
|
-
|
|
1081
|
+
const cat = pgTypeCategory(pgType);
|
|
1082
|
+
const baseType = cat === "date" || cat === "uuid" ? "string" : cat === "json" ? "JsonValue" : cat;
|
|
1083
|
+
return column.nullable ? `${baseType} | null` : baseType;
|
|
1129
1084
|
}
|
|
1130
1085
|
function generateExampleFields(table, operation) {
|
|
1131
1086
|
const fields = [];
|
|
@@ -1224,76 +1179,25 @@ function generateQueryParams(table, enums) {
|
|
|
1224
1179
|
}
|
|
1225
1180
|
function postgresTypeToJsonType(pgType, enums) {
|
|
1226
1181
|
const t = pgType.toLowerCase();
|
|
1227
|
-
if (enums[t])
|
|
1182
|
+
if (enums[t])
|
|
1228
1183
|
return t;
|
|
1229
|
-
|
|
1230
|
-
if (t.startsWith("_") && enums[t.slice(1)]) {
|
|
1184
|
+
if (t.startsWith("_") && enums[t.slice(1)])
|
|
1231
1185
|
return `${t.slice(1)}[]`;
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
case "int":
|
|
1235
|
-
case "int2":
|
|
1236
|
-
case "int4":
|
|
1237
|
-
case "int8":
|
|
1238
|
-
case "integer":
|
|
1239
|
-
case "smallint":
|
|
1240
|
-
case "bigint":
|
|
1241
|
-
case "decimal":
|
|
1242
|
-
case "numeric":
|
|
1243
|
-
case "real":
|
|
1244
|
-
case "float4":
|
|
1245
|
-
case "float8":
|
|
1246
|
-
case "double precision":
|
|
1247
|
-
case "float":
|
|
1248
|
-
return "number";
|
|
1249
|
-
case "boolean":
|
|
1250
|
-
case "bool":
|
|
1251
|
-
return "boolean";
|
|
1252
|
-
case "date":
|
|
1253
|
-
case "timestamp":
|
|
1254
|
-
case "timestamptz":
|
|
1255
|
-
return "date/datetime";
|
|
1256
|
-
case "json":
|
|
1257
|
-
case "jsonb":
|
|
1258
|
-
return "object";
|
|
1259
|
-
case "uuid":
|
|
1260
|
-
return "uuid";
|
|
1261
|
-
case "text[]":
|
|
1262
|
-
case "varchar[]":
|
|
1263
|
-
case "_text":
|
|
1264
|
-
case "_varchar":
|
|
1265
|
-
return "string[]";
|
|
1266
|
-
case "int[]":
|
|
1267
|
-
case "integer[]":
|
|
1268
|
-
case "_int":
|
|
1269
|
-
case "_int2":
|
|
1270
|
-
case "_int4":
|
|
1271
|
-
case "_int8":
|
|
1272
|
-
case "_integer":
|
|
1273
|
-
return "number[]";
|
|
1274
|
-
case "vector":
|
|
1275
|
-
return "number[]";
|
|
1276
|
-
default:
|
|
1277
|
-
return "string";
|
|
1278
|
-
}
|
|
1186
|
+
const cat = pgTypeCategory(t);
|
|
1187
|
+
return cat === "date" ? "date/datetime" : cat === "json" ? "object" : cat;
|
|
1279
1188
|
}
|
|
1280
1189
|
function generateFieldDescription(column, table) {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
descriptions.push(`Foreign key to ${relatedTable}`);
|
|
1293
|
-
} else {
|
|
1294
|
-
descriptions.push(column.name.replace(/_/g, " "));
|
|
1295
|
-
}
|
|
1296
|
-
return descriptions.join(", ");
|
|
1190
|
+
if (column.name === "id")
|
|
1191
|
+
return "Primary key";
|
|
1192
|
+
if (column.name === "created_at")
|
|
1193
|
+
return "Creation timestamp";
|
|
1194
|
+
if (column.name === "updated_at")
|
|
1195
|
+
return "Last update timestamp";
|
|
1196
|
+
if (column.name === "deleted_at")
|
|
1197
|
+
return "Soft delete timestamp";
|
|
1198
|
+
if (column.name.endsWith("_id"))
|
|
1199
|
+
return `Foreign key to ${column.name.slice(0, -3)}`;
|
|
1200
|
+
return column.name.replace(/_/g, " ");
|
|
1297
1201
|
}
|
|
1298
1202
|
function generateUnifiedContractMarkdown(contract) {
|
|
1299
1203
|
const lines = [];
|
|
@@ -1334,288 +1238,53 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1334
1238
|
lines.push("");
|
|
1335
1239
|
}
|
|
1336
1240
|
}
|
|
1337
|
-
lines.push(
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
lines.push("");
|
|
1385
|
-
lines.push("Pattern matching for string fields:");
|
|
1386
|
-
lines.push("");
|
|
1387
|
-
lines.push("```typescript");
|
|
1388
|
-
lines.push("// Case-sensitive LIKE");
|
|
1389
|
-
lines.push("const johnsmiths = await sdk.users.list({");
|
|
1390
|
-
lines.push(" where: { name: { $like: '%Smith%' } }");
|
|
1391
|
-
lines.push("});");
|
|
1392
|
-
lines.push("");
|
|
1393
|
-
lines.push("// Case-insensitive ILIKE");
|
|
1394
|
-
lines.push("const gmailUsers = await sdk.users.list({");
|
|
1395
|
-
lines.push(" where: { email: { $ilike: '%@gmail.com' } }");
|
|
1396
|
-
lines.push("});");
|
|
1397
|
-
lines.push("```");
|
|
1398
|
-
lines.push("");
|
|
1399
|
-
lines.push("### Array Operators");
|
|
1400
|
-
lines.push("");
|
|
1401
|
-
lines.push("Filter by multiple possible values:");
|
|
1402
|
-
lines.push("");
|
|
1403
|
-
lines.push("```typescript");
|
|
1404
|
-
lines.push("// IN - match any value in array");
|
|
1405
|
-
lines.push("const specificUsers = await sdk.users.list({");
|
|
1406
|
-
lines.push(" where: {");
|
|
1407
|
-
lines.push(" id: { $in: ['id1', 'id2', 'id3'] }");
|
|
1408
|
-
lines.push(" }");
|
|
1409
|
-
lines.push("});");
|
|
1410
|
-
lines.push("");
|
|
1411
|
-
lines.push("// NOT IN - exclude values");
|
|
1412
|
-
lines.push("const nonSystemUsers = await sdk.users.list({");
|
|
1413
|
-
lines.push(" where: {");
|
|
1414
|
-
lines.push(" role: { $nin: ['admin', 'system'] }");
|
|
1415
|
-
lines.push(" }");
|
|
1416
|
-
lines.push("});");
|
|
1417
|
-
lines.push("```");
|
|
1418
|
-
lines.push("");
|
|
1419
|
-
lines.push("### NULL Checks");
|
|
1420
|
-
lines.push("");
|
|
1421
|
-
lines.push("Check for null or non-null values:");
|
|
1422
|
-
lines.push("");
|
|
1423
|
-
lines.push("```typescript");
|
|
1424
|
-
lines.push("// IS NULL");
|
|
1425
|
-
lines.push("const activeRecords = await sdk.records.list({");
|
|
1426
|
-
lines.push(" where: { deleted_at: { $is: null } }");
|
|
1427
|
-
lines.push("});");
|
|
1428
|
-
lines.push("");
|
|
1429
|
-
lines.push("// IS NOT NULL");
|
|
1430
|
-
lines.push("const deletedRecords = await sdk.records.list({");
|
|
1431
|
-
lines.push(" where: { deleted_at: { $isNot: null } }");
|
|
1432
|
-
lines.push("});");
|
|
1433
|
-
lines.push("```");
|
|
1434
|
-
lines.push("");
|
|
1435
|
-
lines.push("### Combining Operators");
|
|
1436
|
-
lines.push("");
|
|
1437
|
-
lines.push("Mix multiple operators for complex queries:");
|
|
1438
|
-
lines.push("");
|
|
1439
|
-
lines.push("```typescript");
|
|
1440
|
-
lines.push("const filteredUsers = await sdk.users.list({");
|
|
1441
|
-
lines.push(" where: {");
|
|
1442
|
-
lines.push(" age: { $gte: 18, $lt: 65 },");
|
|
1443
|
-
lines.push(" email: { $ilike: '%@company.com' },");
|
|
1444
|
-
lines.push(" status: { $in: ['active', 'pending'] },");
|
|
1445
|
-
lines.push(" deleted_at: { $is: null }");
|
|
1446
|
-
lines.push(" },");
|
|
1447
|
-
lines.push(" limit: 50,");
|
|
1448
|
-
lines.push(" offset: 0");
|
|
1449
|
-
lines.push("});");
|
|
1450
|
-
lines.push("```");
|
|
1451
|
-
lines.push("");
|
|
1452
|
-
lines.push("### Available Operators");
|
|
1453
|
-
lines.push("");
|
|
1454
|
-
lines.push("| Operator | Description | Example | Types |");
|
|
1455
|
-
lines.push("|----------|-------------|---------|-------|");
|
|
1456
|
-
lines.push("| `$eq` | Equal to | `{ age: { $eq: 25 } }` | All |");
|
|
1457
|
-
lines.push("| `$ne` | Not equal to | `{ status: { $ne: 'inactive' } }` | All |");
|
|
1458
|
-
lines.push("| `$gt` | Greater than | `{ price: { $gt: 100 } }` | Number, Date |");
|
|
1459
|
-
lines.push("| `$gte` | Greater than or equal | `{ age: { $gte: 18 } }` | Number, Date |");
|
|
1460
|
-
lines.push("| `$lt` | Less than | `{ quantity: { $lt: 10 } }` | Number, Date |");
|
|
1461
|
-
lines.push("| `$lte` | Less than or equal | `{ age: { $lte: 65 } }` | Number, Date |");
|
|
1462
|
-
lines.push("| `$in` | In array | `{ id: { $in: ['a', 'b'] } }` | All |");
|
|
1463
|
-
lines.push("| `$nin` | Not in array | `{ role: { $nin: ['admin'] } }` | All |");
|
|
1464
|
-
lines.push("| `$like` | Pattern match (case-sensitive) | `{ name: { $like: '%John%' } }` | String |");
|
|
1465
|
-
lines.push("| `$ilike` | Pattern match (case-insensitive) | `{ email: { $ilike: '%@GMAIL%' } }` | String |");
|
|
1466
|
-
lines.push("| `$is` | IS NULL | `{ deleted_at: { $is: null } }` | Nullable fields |");
|
|
1467
|
-
lines.push("| `$isNot` | IS NOT NULL | `{ created_by: { $isNot: null } }` | Nullable fields |");
|
|
1468
|
-
lines.push("| `$jsonbContains` | JSONB contains | `{ metadata: { $jsonbContains: { tags: ['premium'] } } }` | JSONB/JSON |");
|
|
1469
|
-
lines.push("| `$jsonbContainedBy` | JSONB contained by | `{ metadata: { $jsonbContainedBy: {...} } }` | JSONB/JSON |");
|
|
1470
|
-
lines.push("| `$jsonbHasKey` | JSONB has key | `{ settings: { $jsonbHasKey: 'theme' } }` | JSONB/JSON |");
|
|
1471
|
-
lines.push("| `$jsonbHasAnyKeys` | JSONB has any keys | `{ settings: { $jsonbHasAnyKeys: ['dark', 'light'] } }` | JSONB/JSON |");
|
|
1472
|
-
lines.push("| `$jsonbHasAllKeys` | JSONB has all keys | `{ config: { $jsonbHasAllKeys: ['api', 'db'] } }` | JSONB/JSON |");
|
|
1473
|
-
lines.push("| `$jsonbPath` | JSONB nested value | `{ meta: { $jsonbPath: { path: ['user', 'age'], operator: '$gte', value: 18 } } }` | JSONB/JSON |");
|
|
1474
|
-
lines.push("");
|
|
1475
|
-
lines.push("### Logical Operators");
|
|
1476
|
-
lines.push("");
|
|
1477
|
-
lines.push("Combine conditions using `$or` and `$and` (supports 2 levels of nesting):");
|
|
1478
|
-
lines.push("");
|
|
1479
|
-
lines.push("| Operator | Description | Example |");
|
|
1480
|
-
lines.push("|----------|-------------|---------|");
|
|
1481
|
-
lines.push("| `$or` | Match any condition | `{ $or: [{ status: 'active' }, { role: 'admin' }] }` |");
|
|
1482
|
-
lines.push("| `$and` | Match all conditions (explicit) | `{ $and: [{ age: { $gte: 18 } }, { status: 'verified' }] }` |");
|
|
1483
|
-
lines.push("");
|
|
1484
|
-
lines.push("```typescript");
|
|
1485
|
-
lines.push("// OR - match any condition");
|
|
1486
|
-
lines.push("const results = await sdk.users.list({");
|
|
1487
|
-
lines.push(" where: {");
|
|
1488
|
-
lines.push(" $or: [");
|
|
1489
|
-
lines.push(" { email: { $ilike: '%@gmail.com' } },");
|
|
1490
|
-
lines.push(" { status: 'premium' }");
|
|
1491
|
-
lines.push(" ]");
|
|
1492
|
-
lines.push(" }");
|
|
1493
|
-
lines.push("});");
|
|
1494
|
-
lines.push("");
|
|
1495
|
-
lines.push("// Mixed AND + OR (implicit AND at root level)");
|
|
1496
|
-
lines.push("const complex = await sdk.users.list({");
|
|
1497
|
-
lines.push(" where: {");
|
|
1498
|
-
lines.push(" status: 'active', // AND");
|
|
1499
|
-
lines.push(" $or: [");
|
|
1500
|
-
lines.push(" { age: { $lt: 18 } },");
|
|
1501
|
-
lines.push(" { age: { $gt: 65 } }");
|
|
1502
|
-
lines.push(" ]");
|
|
1503
|
-
lines.push(" }");
|
|
1504
|
-
lines.push("});");
|
|
1505
|
-
lines.push("");
|
|
1506
|
-
lines.push("// Nested (2 levels max)");
|
|
1507
|
-
lines.push("const nested = await sdk.users.list({");
|
|
1508
|
-
lines.push(" where: {");
|
|
1509
|
-
lines.push(" $and: [");
|
|
1510
|
-
lines.push(" {");
|
|
1511
|
-
lines.push(" $or: [");
|
|
1512
|
-
lines.push(" { firstName: { $ilike: '%john%' } },");
|
|
1513
|
-
lines.push(" { lastName: { $ilike: '%john%' } }");
|
|
1514
|
-
lines.push(" ]");
|
|
1515
|
-
lines.push(" },");
|
|
1516
|
-
lines.push(" { status: 'active' }");
|
|
1517
|
-
lines.push(" ]");
|
|
1518
|
-
lines.push(" }");
|
|
1519
|
-
lines.push("});");
|
|
1520
|
-
lines.push("```");
|
|
1521
|
-
lines.push("");
|
|
1522
|
-
lines.push("**Note:** The WHERE clause types are fully type-safe. TypeScript will only allow operators that are valid for each field type.");
|
|
1523
|
-
lines.push("");
|
|
1524
|
-
lines.push("## Sorting");
|
|
1525
|
-
lines.push("");
|
|
1526
|
-
lines.push("Sort query results using the `orderBy` and `order` parameters. Supports both single and multi-column sorting.");
|
|
1527
|
-
lines.push("");
|
|
1528
|
-
lines.push("### Single Column Sorting");
|
|
1529
|
-
lines.push("");
|
|
1530
|
-
lines.push("```typescript");
|
|
1531
|
-
lines.push("// Sort by one column ascending");
|
|
1532
|
-
lines.push("const users = await sdk.users.list({");
|
|
1533
|
-
lines.push(" orderBy: 'created_at',");
|
|
1534
|
-
lines.push(" order: 'asc'");
|
|
1535
|
-
lines.push("});");
|
|
1536
|
-
lines.push("");
|
|
1537
|
-
lines.push("// Sort descending");
|
|
1538
|
-
lines.push("const latest = await sdk.users.list({");
|
|
1539
|
-
lines.push(" orderBy: 'created_at',");
|
|
1540
|
-
lines.push(" order: 'desc'");
|
|
1541
|
-
lines.push("});");
|
|
1542
|
-
lines.push("");
|
|
1543
|
-
lines.push("// Order defaults to 'asc' if not specified");
|
|
1544
|
-
lines.push("const sorted = await sdk.users.list({");
|
|
1545
|
-
lines.push(" orderBy: 'name'");
|
|
1546
|
-
lines.push("});");
|
|
1547
|
-
lines.push("```");
|
|
1548
|
-
lines.push("");
|
|
1549
|
-
lines.push("### Multi-Column Sorting");
|
|
1550
|
-
lines.push("");
|
|
1551
|
-
lines.push("```typescript");
|
|
1552
|
-
lines.push("// Sort by multiple columns (all same direction)");
|
|
1553
|
-
lines.push("const users = await sdk.users.list({");
|
|
1554
|
-
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1555
|
-
lines.push(" order: 'desc'");
|
|
1556
|
-
lines.push("});");
|
|
1557
|
-
lines.push("");
|
|
1558
|
-
lines.push("// Different direction per column");
|
|
1559
|
-
lines.push("const sorted = await sdk.users.list({");
|
|
1560
|
-
lines.push(" orderBy: ['status', 'created_at'],");
|
|
1561
|
-
lines.push(" order: ['asc', 'desc'] // status ASC, created_at DESC");
|
|
1562
|
-
lines.push("});");
|
|
1563
|
-
lines.push("```");
|
|
1564
|
-
lines.push("");
|
|
1565
|
-
lines.push("### Combining Sorting with Filters");
|
|
1566
|
-
lines.push("");
|
|
1567
|
-
lines.push("```typescript");
|
|
1568
|
-
lines.push("const results = await sdk.users.list({");
|
|
1569
|
-
lines.push(" where: {");
|
|
1570
|
-
lines.push(" status: 'active',");
|
|
1571
|
-
lines.push(" age: { $gte: 18 }");
|
|
1572
|
-
lines.push(" },");
|
|
1573
|
-
lines.push(" orderBy: 'created_at',");
|
|
1574
|
-
lines.push(" order: 'desc',");
|
|
1575
|
-
lines.push(" limit: 50,");
|
|
1576
|
-
lines.push(" offset: 0");
|
|
1577
|
-
lines.push("});");
|
|
1578
|
-
lines.push("```");
|
|
1579
|
-
lines.push("");
|
|
1580
|
-
lines.push("**Note:** Column names are validated by Zod schemas. Only valid table columns are accepted, preventing SQL injection.");
|
|
1581
|
-
lines.push("");
|
|
1582
|
-
lines.push("## Vector Search");
|
|
1583
|
-
lines.push("");
|
|
1584
|
-
lines.push("For tables with `vector` columns (requires pgvector extension), use the `vector` parameter for similarity search:");
|
|
1585
|
-
lines.push("");
|
|
1586
|
-
lines.push("```typescript");
|
|
1587
|
-
lines.push("// Basic similarity search");
|
|
1588
|
-
lines.push("const results = await sdk.embeddings.list({");
|
|
1589
|
-
lines.push(" vector: {");
|
|
1590
|
-
lines.push(" field: 'embedding',");
|
|
1591
|
-
lines.push(" query: [0.1, 0.2, 0.3, ...], // Your embedding vector");
|
|
1592
|
-
lines.push(" metric: 'cosine' // 'cosine' (default), 'l2', or 'inner'");
|
|
1593
|
-
lines.push(" },");
|
|
1594
|
-
lines.push(" limit: 10");
|
|
1595
|
-
lines.push("});");
|
|
1596
|
-
lines.push("");
|
|
1597
|
-
lines.push("// Results include _distance field");
|
|
1598
|
-
lines.push("results.data[0]._distance; // Similarity distance");
|
|
1599
|
-
lines.push("");
|
|
1600
|
-
lines.push("// Distance threshold filtering");
|
|
1601
|
-
lines.push("const closeMatches = await sdk.embeddings.list({");
|
|
1602
|
-
lines.push(" vector: {");
|
|
1603
|
-
lines.push(" field: 'embedding',");
|
|
1604
|
-
lines.push(" query: queryVector,");
|
|
1605
|
-
lines.push(" maxDistance: 0.5 // Only return results within this distance");
|
|
1606
|
-
lines.push(" }");
|
|
1607
|
-
lines.push("});");
|
|
1608
|
-
lines.push("");
|
|
1609
|
-
lines.push("// Hybrid search: vector + WHERE filters");
|
|
1610
|
-
lines.push("const filtered = await sdk.embeddings.list({");
|
|
1611
|
-
lines.push(" vector: { field: 'embedding', query: queryVector },");
|
|
1612
|
-
lines.push(" where: {");
|
|
1613
|
-
lines.push(" status: 'published',");
|
|
1614
|
-
lines.push(" embedding: { $isNot: null }");
|
|
1615
|
-
lines.push(" }");
|
|
1616
|
-
lines.push("});");
|
|
1617
|
-
lines.push("```");
|
|
1618
|
-
lines.push("");
|
|
1241
|
+
lines.push(`## Filtering
|
|
1242
|
+
|
|
1243
|
+
Type-safe WHERE clauses. Root-level keys are AND'd; use \`$or\`/\`$and\` for logic (2 levels max).
|
|
1244
|
+
|
|
1245
|
+
\`\`\`typescript
|
|
1246
|
+
await sdk.users.list({
|
|
1247
|
+
where: {
|
|
1248
|
+
status: { $in: ['active', 'pending'] },
|
|
1249
|
+
age: { $gte: 18, $lt: 65 },
|
|
1250
|
+
email: { $ilike: '%@company.com' },
|
|
1251
|
+
deleted_at: { $is: null },
|
|
1252
|
+
meta: { $jsonbContains: { tag: 'vip' } },
|
|
1253
|
+
$or: [{ role: 'admin' }, { role: 'mod' }]
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
\`\`\`
|
|
1257
|
+
|
|
1258
|
+
| Operator | SQL | Types |
|
|
1259
|
+
|----------|-----|-------|
|
|
1260
|
+
| \`$eq\` \`$ne\` | = ≠ | All |
|
|
1261
|
+
| \`$gt\` \`$gte\` \`$lt\` \`$lte\` | > ≥ < ≤ | Number, Date |
|
|
1262
|
+
| \`$in\` \`$nin\` | IN / NOT IN | All |
|
|
1263
|
+
| \`$like\` \`$ilike\` | LIKE / ILIKE | String |
|
|
1264
|
+
| \`$is\` \`$isNot\` | IS NULL / IS NOT NULL | Nullable |
|
|
1265
|
+
| \`$jsonbContains\` \`$jsonbContainedBy\` \`$jsonbHasKey\` \`$jsonbHasAnyKeys\` \`$jsonbHasAllKeys\` \`$jsonbPath\` | JSONB ops | JSONB |
|
|
1266
|
+
| \`$or\` \`$and\` | OR / AND (2 levels) | — |
|
|
1267
|
+
|
|
1268
|
+
## Sorting
|
|
1269
|
+
|
|
1270
|
+
\`orderBy\` accepts a column name or array; \`order\` accepts \`'asc'\`/\`'desc'\` or a per-column array.
|
|
1271
|
+
|
|
1272
|
+
\`\`\`typescript
|
|
1273
|
+
await sdk.users.list({ orderBy: ['status', 'created_at'], order: ['asc', 'desc'] });
|
|
1274
|
+
\`\`\`
|
|
1275
|
+
|
|
1276
|
+
## Vector Search
|
|
1277
|
+
|
|
1278
|
+
For tables with \`vector\` columns (requires pgvector). Results include a \`_distance\` field.
|
|
1279
|
+
|
|
1280
|
+
\`\`\`typescript
|
|
1281
|
+
const results = await sdk.embeddings.list({
|
|
1282
|
+
vector: { field: 'embedding', query: [0.1, 0.2, 0.3, /* ... */], metric: 'cosine', maxDistance: 0.5 },
|
|
1283
|
+
where: { status: 'published' },
|
|
1284
|
+
limit: 10
|
|
1285
|
+
}); // results.data[0]._distance
|
|
1286
|
+
\`\`\`
|
|
1287
|
+
`);
|
|
1619
1288
|
lines.push("## Resources");
|
|
1620
1289
|
lines.push("");
|
|
1621
1290
|
for (const resource of contract.resources) {
|
|
@@ -1635,10 +1304,12 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1635
1304
|
lines.push(`- API: \`${method.correspondsTo}\``);
|
|
1636
1305
|
}
|
|
1637
1306
|
lines.push("");
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1307
|
+
if (method.example) {
|
|
1308
|
+
lines.push("```typescript");
|
|
1309
|
+
lines.push(method.example);
|
|
1310
|
+
lines.push("```");
|
|
1311
|
+
lines.push("");
|
|
1312
|
+
}
|
|
1642
1313
|
}
|
|
1643
1314
|
lines.push("#### API Endpoints");
|
|
1644
1315
|
lines.push("");
|
|
@@ -1672,23 +1343,14 @@ function generateUnifiedContractMarkdown(contract) {
|
|
|
1672
1343
|
}
|
|
1673
1344
|
lines.push("");
|
|
1674
1345
|
}
|
|
1675
|
-
lines.push(
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
lines.push(" SelectTableName, // Full record type");
|
|
1684
|
-
lines.push(" InsertTableName, // Create payload type");
|
|
1685
|
-
lines.push(" UpdateTableName // Update payload type");
|
|
1686
|
-
lines.push("} from './client/types/table_name';");
|
|
1687
|
-
lines.push("");
|
|
1688
|
-
lines.push("// Import all types");
|
|
1689
|
-
lines.push("import type * as Types from './client/types';");
|
|
1690
|
-
lines.push("```");
|
|
1691
|
-
lines.push("");
|
|
1346
|
+
lines.push(`## Type Imports
|
|
1347
|
+
|
|
1348
|
+
\`\`\`typescript
|
|
1349
|
+
import { SDK } from './client';
|
|
1350
|
+
import type { SelectTableName, InsertTableName, UpdateTableName } from './client/types/table_name';
|
|
1351
|
+
import type * as Types from './client/types';
|
|
1352
|
+
\`\`\`
|
|
1353
|
+
`);
|
|
1692
1354
|
return lines.join(`
|
|
1693
1355
|
`);
|
|
1694
1356
|
}
|
|
@@ -4578,7 +4240,8 @@ export abstract class BaseClient {
|
|
|
4578
4240
|
}
|
|
4579
4241
|
|
|
4580
4242
|
// src/emit-include-loader.ts
|
|
4581
|
-
function emitIncludeLoader(
|
|
4243
|
+
function emitIncludeLoader(model, maxDepth, opts = {}) {
|
|
4244
|
+
const { softDeleteCols = {}, useJsExtensions } = opts;
|
|
4582
4245
|
const fkIndex = {};
|
|
4583
4246
|
for (const t of Object.values(model.tables)) {
|
|
4584
4247
|
fkIndex[t.name] = t.fks.map((f) => ({ from: f.from, toTable: f.toTable, to: f.to }));
|
|
@@ -4617,8 +4280,18 @@ const log = {
|
|
|
4617
4280
|
};
|
|
4618
4281
|
|
|
4619
4282
|
// Helpers for PK/FK discovery from model (inlined)
|
|
4620
|
-
const FK_INDEX = ${JSON.stringify(fkIndex, null, 2)}
|
|
4621
|
-
const PKS = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)}
|
|
4283
|
+
const FK_INDEX: Record<string, Array<{from: string[]; toTable: string; to: string[]}>> = ${JSON.stringify(fkIndex, null, 2)};
|
|
4284
|
+
const PKS: Record<string, string[]> = ${JSON.stringify(Object.fromEntries(Object.values(model.tables).map((t) => [t.name, t.pk])), null, 2)};
|
|
4285
|
+
/** Soft-delete column per table, null if not applicable. Baked in at generation time. */
|
|
4286
|
+
const SOFT_DELETE_COLS: Record<string, string | null> = ${JSON.stringify(softDeleteCols, null, 2)};
|
|
4287
|
+
|
|
4288
|
+
/** Returns a SQL fragment like ' AND "col" IS NULL' for the given table, or "" if none. */
|
|
4289
|
+
function softDeleteFilter(table: string, alias?: string): string {
|
|
4290
|
+
const col = SOFT_DELETE_COLS[table];
|
|
4291
|
+
if (!col) return "";
|
|
4292
|
+
const ref = alias ? \`\${alias}."\${col}"\` : \`"\${col}"\`;
|
|
4293
|
+
return \` AND \${ref} IS NULL\`;
|
|
4294
|
+
}
|
|
4622
4295
|
|
|
4623
4296
|
// Build WHERE predicate for OR-of-AND on composite values
|
|
4624
4297
|
function buildOrAndPredicate(cols: string[], count: number, startIndex: number) {
|
|
@@ -4708,6 +4381,49 @@ function filterFields<T extends Record<string, any>>(
|
|
|
4708
4381
|
});
|
|
4709
4382
|
}
|
|
4710
4383
|
|
|
4384
|
+
/** Sentinel used as window-function LIMIT when no per-parent limit is requested. */
|
|
4385
|
+
const NO_LIMIT = 999_999_999;
|
|
4386
|
+
|
|
4387
|
+
/**
|
|
4388
|
+
* FK_INDEX and PKS are fully populated at code-gen time for all schema tables,
|
|
4389
|
+
* so lookups by TableName are always defined. These helpers assert non-null
|
|
4390
|
+
* in one place rather than scattering \`!\` or \`as any\` at every call site.
|
|
4391
|
+
*/
|
|
4392
|
+
function fksOf(table: string): Array<{from: string[]; toTable: string; to: string[]}> {
|
|
4393
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4394
|
+
return FK_INDEX[table]!;
|
|
4395
|
+
}
|
|
4396
|
+
function pkOf(table: string): string[] {
|
|
4397
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
4398
|
+
return PKS[table]!;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
/** Parse relation options and nested child spec from a specValue entry. */
|
|
4402
|
+
function parseSpecOptions(specValue: any): { options: RelationOptions; childSpec: any } {
|
|
4403
|
+
const options: RelationOptions = {};
|
|
4404
|
+
let childSpec: any = undefined;
|
|
4405
|
+
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4406
|
+
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4407
|
+
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4408
|
+
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4409
|
+
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4410
|
+
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4411
|
+
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4412
|
+
if (specValue.include !== undefined) {
|
|
4413
|
+
childSpec = specValue.include;
|
|
4414
|
+
} else {
|
|
4415
|
+
// Support legacy format: { tags: true } alongside new: { include: { tags: true } }
|
|
4416
|
+
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4417
|
+
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4418
|
+
);
|
|
4419
|
+
if (nonOptionKeys.length > 0) {
|
|
4420
|
+
childSpec = Object.fromEntries(nonOptionKeys.map(k => [k, specValue[k]]));
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
return { options, childSpec };
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4711
4427
|
// Public entry
|
|
4712
4428
|
export async function loadIncludes(
|
|
4713
4429
|
root: TableName,
|
|
@@ -4747,37 +4463,7 @@ export async function loadIncludes(
|
|
|
4747
4463
|
// Safely run each loader; never let one bad edge 500 the route
|
|
4748
4464
|
if (rel.via) {
|
|
4749
4465
|
// M:N via junction
|
|
4750
|
-
const
|
|
4751
|
-
const options: RelationOptions = {};
|
|
4752
|
-
let childSpec: any = undefined;
|
|
4753
|
-
|
|
4754
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4755
|
-
// Extract options
|
|
4756
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4757
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4758
|
-
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4759
|
-
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4760
|
-
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4761
|
-
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4762
|
-
|
|
4763
|
-
// Extract nested spec - support both formats:
|
|
4764
|
-
// New: { limit: 3, include: { tags: true } }
|
|
4765
|
-
// Old: { tags: true } (backward compatibility)
|
|
4766
|
-
if (specValue.include !== undefined) {
|
|
4767
|
-
childSpec = specValue.include;
|
|
4768
|
-
} else {
|
|
4769
|
-
// Build childSpec from non-option keys
|
|
4770
|
-
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4771
|
-
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4772
|
-
);
|
|
4773
|
-
if (nonOptionKeys.length > 0) {
|
|
4774
|
-
childSpec = {};
|
|
4775
|
-
for (const k of nonOptionKeys) {
|
|
4776
|
-
childSpec[k] = specValue[k];
|
|
4777
|
-
}
|
|
4778
|
-
}
|
|
4779
|
-
}
|
|
4780
|
-
}
|
|
4466
|
+
const { options, childSpec } = parseSpecOptions(s[key]);
|
|
4781
4467
|
|
|
4782
4468
|
try {
|
|
4783
4469
|
await loadManyToMany(table, target, rel.via as string, rows, key, options);
|
|
@@ -4799,37 +4485,7 @@ export async function loadIncludes(
|
|
|
4799
4485
|
|
|
4800
4486
|
if (rel.kind === "many") {
|
|
4801
4487
|
// 1:N target has FK to current
|
|
4802
|
-
const
|
|
4803
|
-
const options: RelationOptions = {};
|
|
4804
|
-
let childSpec: any = undefined;
|
|
4805
|
-
|
|
4806
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4807
|
-
// Extract options
|
|
4808
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4809
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4810
|
-
if (specValue.limit !== undefined) options.limit = specValue.limit;
|
|
4811
|
-
if (specValue.offset !== undefined) options.offset = specValue.offset;
|
|
4812
|
-
if (specValue.orderBy !== undefined) options.orderBy = specValue.orderBy;
|
|
4813
|
-
if (specValue.order !== undefined) options.order = specValue.order;
|
|
4814
|
-
|
|
4815
|
-
// Extract nested spec - support both formats:
|
|
4816
|
-
// New: { limit: 3, include: { tags: true } }
|
|
4817
|
-
// Old: { tags: true } (backward compatibility)
|
|
4818
|
-
if (specValue.include !== undefined) {
|
|
4819
|
-
childSpec = specValue.include;
|
|
4820
|
-
} else {
|
|
4821
|
-
// Build childSpec from non-option keys
|
|
4822
|
-
const nonOptionKeys = Object.keys(specValue).filter(
|
|
4823
|
-
k => k !== 'select' && k !== 'exclude' && k !== 'limit' && k !== 'offset' && k !== 'orderBy' && k !== 'order'
|
|
4824
|
-
);
|
|
4825
|
-
if (nonOptionKeys.length > 0) {
|
|
4826
|
-
childSpec = {};
|
|
4827
|
-
for (const k of nonOptionKeys) {
|
|
4828
|
-
childSpec[k] = specValue[k];
|
|
4829
|
-
}
|
|
4830
|
-
}
|
|
4831
|
-
}
|
|
4832
|
-
}
|
|
4488
|
+
const { options, childSpec } = parseSpecOptions(s[key]);
|
|
4833
4489
|
|
|
4834
4490
|
try {
|
|
4835
4491
|
await loadOneToMany(table, target, rows, key, options);
|
|
@@ -4849,20 +4505,11 @@ export async function loadIncludes(
|
|
|
4849
4505
|
} else {
|
|
4850
4506
|
// kind === "one"
|
|
4851
4507
|
// Could be belongs-to (current has FK to target) OR has-one (target unique-FK to current)
|
|
4852
|
-
|
|
4853
|
-
const options:
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
if (specValue && typeof specValue === "object" && specValue !== true) {
|
|
4857
|
-
if (specValue.select !== undefined) options.select = specValue.select;
|
|
4858
|
-
if (specValue.exclude !== undefined) options.exclude = specValue.exclude;
|
|
4859
|
-
// Support { include: TargetIncludeSpec } — mirrors the many/via handler
|
|
4860
|
-
if (specValue.include !== undefined) {
|
|
4861
|
-
childSpec = specValue.include;
|
|
4862
|
-
}
|
|
4863
|
-
}
|
|
4508
|
+
// Destructure only select/exclude/childSpec — limit/offset/orderBy don't apply to 1:1 relations
|
|
4509
|
+
const { options: { select, exclude }, childSpec } = parseSpecOptions(s[key]);
|
|
4510
|
+
const options: RelationOptions = { select, exclude };
|
|
4864
4511
|
|
|
4865
|
-
const currFks = (
|
|
4512
|
+
const currFks = fksOf(table);
|
|
4866
4513
|
const toTarget = currFks.find(f => f.toTable === target);
|
|
4867
4514
|
if (toTarget) {
|
|
4868
4515
|
try {
|
|
@@ -4894,16 +4541,16 @@ export async function loadIncludes(
|
|
|
4894
4541
|
|
|
4895
4542
|
async function loadBelongsTo(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4896
4543
|
// current has FK cols referencing target PK
|
|
4897
|
-
const fk = (
|
|
4544
|
+
const fk = fksOf(curr).find(f => f.toTable === target);
|
|
4898
4545
|
if (!fk) { for (const r of rows) r[key] = null; return; }
|
|
4899
4546
|
const tuples = distinctTuples(rows, fk.from).filter(t => t.every((v: any) => v != null));
|
|
4900
4547
|
if (!tuples.length) { for (const r of rows) r[key] = null; return; }
|
|
4901
4548
|
|
|
4902
4549
|
// Query target WHERE target.pk IN tuples
|
|
4903
|
-
const pkCols = (
|
|
4550
|
+
const pkCols = pkOf(target);
|
|
4904
4551
|
const where = buildOrAndPredicate(pkCols, tuples.length, 1);
|
|
4905
4552
|
const params = tuples.flat();
|
|
4906
|
-
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4553
|
+
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4907
4554
|
log.debug("belongsTo SQL", { curr, target, key, sql, paramsCount: params.length });
|
|
4908
4555
|
const { rows: targets } = await pg.query(sql, params);
|
|
4909
4556
|
|
|
@@ -4920,17 +4567,17 @@ export async function loadIncludes(
|
|
|
4920
4567
|
|
|
4921
4568
|
async function loadHasOne(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4922
4569
|
// target has FK cols referencing current PK (unique)
|
|
4923
|
-
const fk = (
|
|
4570
|
+
const fk = fksOf(target).find(f => f.toTable === curr);
|
|
4924
4571
|
if (!fk) { for (const r of rows) r[key] = null; return; }
|
|
4925
4572
|
|
|
4926
|
-
const pkCols = (
|
|
4573
|
+
const pkCols = pkOf(curr);
|
|
4927
4574
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4928
4575
|
if (!tuples.length) { for (const r of rows) r[key] = null; return; }
|
|
4929
4576
|
|
|
4930
4577
|
// SELECT target WHERE fk IN tuples
|
|
4931
4578
|
const where = buildOrAndPredicate(fk.from, tuples.length, 1);
|
|
4932
4579
|
const params = tuples.flat();
|
|
4933
|
-
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4580
|
+
const sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4934
4581
|
log.debug("hasOne SQL", { curr, target, key, sql, paramsCount: params.length });
|
|
4935
4582
|
const { rows: targets } = await pg.query(sql, params);
|
|
4936
4583
|
|
|
@@ -4946,10 +4593,10 @@ export async function loadIncludes(
|
|
|
4946
4593
|
|
|
4947
4594
|
async function loadOneToMany(curr: TableName, target: TableName, rows: any[], key: string, options: RelationOptions = {}) {
|
|
4948
4595
|
// target has FK cols referencing current PK
|
|
4949
|
-
const fk = (
|
|
4596
|
+
const fk = fksOf(target).find(f => f.toTable === curr);
|
|
4950
4597
|
if (!fk) { for (const r of rows) r[key] = []; return; }
|
|
4951
4598
|
|
|
4952
|
-
const pkCols = (
|
|
4599
|
+
const pkCols = pkOf(curr);
|
|
4953
4600
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
4954
4601
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
4955
4602
|
|
|
@@ -4957,7 +4604,7 @@ export async function loadIncludes(
|
|
|
4957
4604
|
const params = tuples.flat();
|
|
4958
4605
|
|
|
4959
4606
|
// Build SQL with optional ORDER BY, LIMIT, OFFSET
|
|
4960
|
-
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\`;
|
|
4607
|
+
let sql = \`SELECT * FROM "\${target}" WHERE \${where}\${softDeleteFilter(target)}\`;
|
|
4961
4608
|
|
|
4962
4609
|
// If limit/offset are needed, use window functions to limit per parent
|
|
4963
4610
|
if (options.limit !== undefined || options.offset !== undefined) {
|
|
@@ -4967,13 +4614,13 @@ export async function loadIncludes(
|
|
|
4967
4614
|
|
|
4968
4615
|
const partitionCols = fk.from.map((c: string) => \`"\${c}"\`).join(', ');
|
|
4969
4616
|
const offset = options.offset ?? 0;
|
|
4970
|
-
const limit = options.limit ??
|
|
4617
|
+
const limit = options.limit ?? NO_LIMIT;
|
|
4971
4618
|
|
|
4972
4619
|
sql = \`
|
|
4973
4620
|
SELECT * FROM (
|
|
4974
4621
|
SELECT *, ROW_NUMBER() OVER (PARTITION BY \${partitionCols} \${orderByClause}) as __rn
|
|
4975
4622
|
FROM "\${target}"
|
|
4976
|
-
WHERE \${where}
|
|
4623
|
+
WHERE \${where}\${softDeleteFilter(target)}
|
|
4977
4624
|
) __sub
|
|
4978
4625
|
WHERE __rn > \${offset} AND __rn <= \${offset + limit}
|
|
4979
4626
|
\`;
|
|
@@ -5003,11 +4650,11 @@ export async function loadIncludes(
|
|
|
5003
4650
|
|
|
5004
4651
|
async function loadManyToMany(curr: TableName, target: TableName, via: string, rows: any[], key: string, options: RelationOptions = {}) {
|
|
5005
4652
|
// via has two FKs: one to curr, one to target
|
|
5006
|
-
const toCurr = (
|
|
5007
|
-
const toTarget = (
|
|
4653
|
+
const toCurr = fksOf(via).find(f => f.toTable === curr);
|
|
4654
|
+
const toTarget = fksOf(via).find(f => f.toTable === target);
|
|
5008
4655
|
if (!toCurr || !toTarget) { for (const r of rows) r[key] = []; return; }
|
|
5009
4656
|
|
|
5010
|
-
const pkCols = (
|
|
4657
|
+
const pkCols = pkOf(curr);
|
|
5011
4658
|
const tuples = distinctTuples(rows, pkCols).filter(t => t.every((v: any) => v != null));
|
|
5012
4659
|
if (!tuples.length) { for (const r of rows) r[key] = []; return; }
|
|
5013
4660
|
|
|
@@ -5022,9 +4669,9 @@ export async function loadIncludes(
|
|
|
5022
4669
|
|
|
5023
4670
|
const partitionCols = toCurr.from.map((c: string) => \`j."\${c}"\`).join(', ');
|
|
5024
4671
|
const offset = options.offset ?? 0;
|
|
5025
|
-
const limit = options.limit ??
|
|
4672
|
+
const limit = options.limit ?? NO_LIMIT;
|
|
5026
4673
|
|
|
5027
|
-
const targetPkCols = (
|
|
4674
|
+
const targetPkCols = pkOf(target);
|
|
5028
4675
|
const joinConditions = toTarget.from.map((jCol: string, i: number) => {
|
|
5029
4676
|
return \`j."\${jCol}" = t."\${targetPkCols[i]}"\`;
|
|
5030
4677
|
}).join(' AND ');
|
|
@@ -5037,7 +4684,7 @@ export async function loadIncludes(
|
|
|
5037
4684
|
\${toCurr.from.map((c: string) => \`j."\${c}"\`).join(' || \\',\\' || ')} as __parent_fk
|
|
5038
4685
|
FROM "\${via}" j
|
|
5039
4686
|
INNER JOIN "\${target}" t ON \${joinConditions}
|
|
5040
|
-
WHERE \${whereVia}
|
|
4687
|
+
WHERE \${whereVia}\${softDeleteFilter(target, "t")}
|
|
5041
4688
|
) __numbered
|
|
5042
4689
|
WHERE __numbered.__rn > \${offset} AND __numbered.__rn <= \${offset + limit}
|
|
5043
4690
|
\`;
|
|
@@ -5080,8 +4727,8 @@ export async function loadIncludes(
|
|
|
5080
4727
|
|
|
5081
4728
|
// 2) Load targets by distinct target fk tuples in junction
|
|
5082
4729
|
const tTuples = distinctTuples(jrows, toTarget.from);
|
|
5083
|
-
const whereT = buildOrAndPredicate((
|
|
5084
|
-
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\`;
|
|
4730
|
+
const whereT = buildOrAndPredicate(pkOf(target), tTuples.length, 1);
|
|
4731
|
+
const sqlT = \`SELECT * FROM "\${target}" WHERE \${whereT}\${softDeleteFilter(target)}\`;
|
|
5085
4732
|
const paramsT = tTuples.flat();
|
|
5086
4733
|
log.debug("manyToMany target SQL", { curr, target, via, key, sql: sqlT, paramsCount: paramsT.length });
|
|
5087
4734
|
const { rows: targets } = await pg.query(sqlT, paramsT);
|
|
@@ -5089,7 +4736,7 @@ export async function loadIncludes(
|
|
|
5089
4736
|
// Apply select/exclude filtering
|
|
5090
4737
|
const filteredTargets = filterFields(targets, options.select, options.exclude);
|
|
5091
4738
|
|
|
5092
|
-
const tIdx = indexByTuple(filteredTargets, (
|
|
4739
|
+
const tIdx = indexByTuple(filteredTargets, pkOf(target));
|
|
5093
4740
|
|
|
5094
4741
|
// 3) Group junction rows by current pk tuple, map to target rows
|
|
5095
4742
|
const byCurr = groupByTuple(jrows, toCurr.from);
|
|
@@ -7513,9 +7160,14 @@ async function generate(configPath) {
|
|
|
7513
7160
|
path: join2(serverDir, "include-builder.ts"),
|
|
7514
7161
|
content: emitIncludeBuilder(graph, cfg.includeMethodsDepth || 2)
|
|
7515
7162
|
});
|
|
7163
|
+
const softDeleteCols = Object.fromEntries(Object.values(model.tables).map((t) => {
|
|
7164
|
+
const col = resolveSoftDeleteColumn(cfg, t.name);
|
|
7165
|
+
const validated = col && t.columns.some((c) => c.name === col) ? col : null;
|
|
7166
|
+
return [t.name, validated];
|
|
7167
|
+
}));
|
|
7516
7168
|
files.push({
|
|
7517
7169
|
path: join2(serverDir, "include-loader.ts"),
|
|
7518
|
-
content: emitIncludeLoader(
|
|
7170
|
+
content: emitIncludeLoader(model, cfg.includeMethodsDepth || 2, { softDeleteCols, useJsExtensions: cfg.useJsExtensions })
|
|
7519
7171
|
});
|
|
7520
7172
|
files.push({ path: join2(serverDir, "logger.ts"), content: emitLogger() });
|
|
7521
7173
|
if (getAuthStrategy(normalizedAuth) !== "none") {
|
|
@@ -7541,7 +7193,7 @@ async function generate(configPath) {
|
|
|
7541
7193
|
let routeContent;
|
|
7542
7194
|
if (serverFramework === "hono") {
|
|
7543
7195
|
routeContent = emitHonoRoutes(table, graph, {
|
|
7544
|
-
softDeleteColumn:
|
|
7196
|
+
softDeleteColumn: softDeleteCols[table.name] ?? null,
|
|
7545
7197
|
includeMethodsDepth: cfg.includeMethodsDepth || 2,
|
|
7546
7198
|
authStrategy: getAuthStrategy(normalizedAuth),
|
|
7547
7199
|
useJsExtensions: cfg.useJsExtensions,
|