@dudousxd/nestjs-durable-store-mikro-orm 0.23.0 → 0.25.0
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/index.cjs +139 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -3
- package/dist/index.d.ts +24 -3
- package/dist/index.js +140 -23
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -1071,20 +1071,47 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1071
1071
|
* applied by the QueryBuilder path in both callers. */
|
|
1072
1072
|
runWhere(query) {
|
|
1073
1073
|
const where = {};
|
|
1074
|
+
const and = [];
|
|
1074
1075
|
if (query.workflow) where.workflow = query.workflow;
|
|
1076
|
+
if (query.workflows) and.push({
|
|
1077
|
+
workflow: {
|
|
1078
|
+
$in: query.workflows
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1075
1081
|
if (query.namespace !== void 0) where.namespace = query.namespace;
|
|
1082
|
+
if (query.namespaces) and.push({
|
|
1083
|
+
namespace: {
|
|
1084
|
+
$in: query.namespaces
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1076
1087
|
if (query.origin !== void 0) where.origin = query.origin;
|
|
1088
|
+
if (query.origins) {
|
|
1089
|
+
const named = query.origins.filter((o) => o !== null);
|
|
1090
|
+
const branches = [];
|
|
1091
|
+
if (named.length) branches.push({
|
|
1092
|
+
origin: {
|
|
1093
|
+
$in: named
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
if (query.origins.some((o) => o === null)) branches.push({
|
|
1097
|
+
origin: null
|
|
1098
|
+
});
|
|
1099
|
+
and.push(branches.length ? {
|
|
1100
|
+
$or: branches
|
|
1101
|
+
} : {
|
|
1102
|
+
origin: {
|
|
1103
|
+
$in: []
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1077
1107
|
if (query.status && query.statuses) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
status: {
|
|
1084
|
-
$in: query.statuses
|
|
1085
|
-
}
|
|
1108
|
+
and.push({
|
|
1109
|
+
status: query.status
|
|
1110
|
+
}, {
|
|
1111
|
+
status: {
|
|
1112
|
+
$in: query.statuses
|
|
1086
1113
|
}
|
|
1087
|
-
|
|
1114
|
+
});
|
|
1088
1115
|
} else if (query.status) {
|
|
1089
1116
|
where.status = query.status;
|
|
1090
1117
|
} else if (query.statuses) {
|
|
@@ -1092,6 +1119,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1092
1119
|
$in: query.statuses
|
|
1093
1120
|
};
|
|
1094
1121
|
}
|
|
1122
|
+
if (and.length) where.$and = and;
|
|
1095
1123
|
return where;
|
|
1096
1124
|
}
|
|
1097
1125
|
async listRuns(query) {
|
|
@@ -1100,7 +1128,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1100
1128
|
const orderBy = {
|
|
1101
1129
|
createdAt: "desc"
|
|
1102
1130
|
};
|
|
1103
|
-
if (query.tag || query.attributes?.length) {
|
|
1131
|
+
if (query.tag || query.tags || query.attributes?.length) {
|
|
1104
1132
|
const qb = this.runQueryBuilder(em, query, where).orderBy(orderBy);
|
|
1105
1133
|
if (query.limit != null) qb.limit(query.limit);
|
|
1106
1134
|
if (query.offset != null) qb.offset(query.offset);
|
|
@@ -1126,17 +1154,23 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1126
1154
|
const quote = this.idQuote(em);
|
|
1127
1155
|
const cols = this.attributeColumns(em);
|
|
1128
1156
|
const qb = em.createQueryBuilder(WorkflowRunEntity, "r").where(where);
|
|
1129
|
-
if (query.tag)
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
qb.andWhere(`${colExpr} LIKE '${pattern}'`);
|
|
1157
|
+
if (query.tag) qb.andWhere(this.tagLikeSql(em, query.tag, quote));
|
|
1158
|
+
if (query.tags) {
|
|
1159
|
+
const branches = query.tags.map((t) => this.tagLikeSql(em, t, quote));
|
|
1160
|
+
qb.andWhere(branches.length ? `(${branches.join(" OR ")})` : "1 = 0");
|
|
1134
1161
|
}
|
|
1135
1162
|
for (const f of query.attributes ?? []) {
|
|
1136
1163
|
qb.andWhere(this.attributeExistsSql(f, quote, "r", cols));
|
|
1137
1164
|
}
|
|
1138
1165
|
return qb;
|
|
1139
1166
|
}
|
|
1167
|
+
/** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
|
|
1168
|
+
* {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
|
|
1169
|
+
tagLikeSql(em, tag, quote) {
|
|
1170
|
+
const pattern = `%"${tag.replace(/'/g, "''")}"%`;
|
|
1171
|
+
const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
|
|
1172
|
+
return `${colExpr} LIKE '${pattern}'`;
|
|
1173
|
+
}
|
|
1140
1174
|
/** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
|
|
1141
1175
|
* console can show whole-set counts next to a bounded page instead of downloading every run to
|
|
1142
1176
|
* count them in the browser. */
|
|
@@ -1157,20 +1191,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1157
1191
|
count: Number(r.count)
|
|
1158
1192
|
})));
|
|
1159
1193
|
}
|
|
1194
|
+
/** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
|
|
1195
|
+
* column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
|
|
1196
|
+
* attribute axes live outside the row (json array / side table) and are counted from a bounded
|
|
1197
|
+
* page of runs instead — see {@link RunValueFacetOptions.scan}. */
|
|
1198
|
+
async runValueFacets(axis, query, opts) {
|
|
1199
|
+
if (!(0, import_nestjs_durable_core.axisIsRunColumn)(axis)) {
|
|
1200
|
+
return this.scannedValueFacets(axis, query, opts);
|
|
1201
|
+
}
|
|
1202
|
+
const em = this.fork();
|
|
1203
|
+
const meta = em.getMetadata().get(WorkflowRunEntity);
|
|
1204
|
+
const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
|
|
1205
|
+
const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([
|
|
1206
|
+
`r.${col}`
|
|
1207
|
+
]).addSelect((0, import_core3.raw)("count(*) as count")).groupBy([
|
|
1208
|
+
`r.${col}`
|
|
1209
|
+
]).execute();
|
|
1210
|
+
return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(rows.map((row) => ({
|
|
1211
|
+
value: row[axis.field] ?? null,
|
|
1212
|
+
count: Number(row.count)
|
|
1213
|
+
})), opts);
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* The axes whose values do not live in a column of the run row: `tag` (inside a JSON array) and the
|
|
1217
|
+
* two search-attribute axes (a side table). Both read the newest `scan` matching runs.
|
|
1218
|
+
*
|
|
1219
|
+
* Neither reads a run's PAYLOAD. The obvious implementation — page `listRuns` and count in
|
|
1220
|
+
* memory — pulls `input`, `output` and `error` for every run in the window, which on a control
|
|
1221
|
+
* plane with real payloads is tens of megabytes fetched to produce a list of a hundred short
|
|
1222
|
+
* strings, every time a picker opens. These project the one column each question needs instead:
|
|
1223
|
+
* the tags column, or the run ids that then drive one grouped read of the side table.
|
|
1224
|
+
*/
|
|
1225
|
+
async scannedValueFacets(axis, query, opts) {
|
|
1226
|
+
const em = this.fork();
|
|
1227
|
+
const scan = opts?.scan ?? import_nestjs_durable_core.RUN_VALUE_FACET_SCAN;
|
|
1228
|
+
const where = this.runWhere(query);
|
|
1229
|
+
const runMeta = em.getMetadata().get(WorkflowRunEntity);
|
|
1230
|
+
const idCol = runMeta.properties.id?.fieldNames?.[0] ?? "id";
|
|
1231
|
+
if (axis.field === "tag") {
|
|
1232
|
+
const tagsCol = this.tagsColumn(em);
|
|
1233
|
+
const rows = await this.runQueryBuilder(em, query, where).select([
|
|
1234
|
+
`r.${tagsCol}`
|
|
1235
|
+
]).orderBy({
|
|
1236
|
+
createdAt: "desc"
|
|
1237
|
+
}).limit(scan).execute();
|
|
1238
|
+
const values = [];
|
|
1239
|
+
for (const row of rows) {
|
|
1240
|
+
const raw2 = row.tags ?? row[tagsCol];
|
|
1241
|
+
const tags = typeof raw2 === "string" ? JSON.parse(raw2) : raw2;
|
|
1242
|
+
if (!Array.isArray(tags)) continue;
|
|
1243
|
+
for (const tag of tags) values.push({
|
|
1244
|
+
value: String(tag),
|
|
1245
|
+
count: 1
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(values, opts);
|
|
1249
|
+
}
|
|
1250
|
+
const idRows = await this.runQueryBuilder(em, query, where).select([
|
|
1251
|
+
`r.${idCol}`
|
|
1252
|
+
]).orderBy({
|
|
1253
|
+
createdAt: "desc"
|
|
1254
|
+
}).limit(scan).execute();
|
|
1255
|
+
const runIds = idRows.map((row) => String(row.id ?? row[idCol]));
|
|
1256
|
+
if (runIds.length === 0) return [];
|
|
1257
|
+
const attributes = await em.find(RunAttributeEntity, axis.field === "attributeValue" ? {
|
|
1258
|
+
runId: {
|
|
1259
|
+
$in: runIds
|
|
1260
|
+
},
|
|
1261
|
+
key: axis.key
|
|
1262
|
+
} : {
|
|
1263
|
+
runId: {
|
|
1264
|
+
$in: runIds
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
return (0, import_nestjs_durable_core.mergeRunValueFacetRows)(attributes.map((row) => ({
|
|
1268
|
+
value: axis.field === "attributeKey" ? row.key : row.strValue ?? (row.numValue == null ? null : String(row.numValue)),
|
|
1269
|
+
count: 1
|
|
1270
|
+
})), opts);
|
|
1271
|
+
}
|
|
1160
1272
|
/** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
|
|
1161
1273
|
* alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
|
|
1162
1274
|
* EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
|
|
1163
|
-
* operands compare the num column, everything else the str column.
|
|
1164
|
-
*
|
|
1275
|
+
* operands compare the num column, everything else the str column. `in` contributes one comparison
|
|
1276
|
+
* per member, ORed inside the single EXISTS — one subquery either way, and an `in` over no values
|
|
1277
|
+
* yields no comparison and so matches nothing. Identifiers are quoted per driver and operands are
|
|
1278
|
+
* inlined as literals (string operands are escaped by doubling quotes). */
|
|
1165
1279
|
attributeExistsSql(f, quote, alias, cols) {
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1280
|
+
const comparisons = (0, import_nestjs_durable_core.attributePredicateOperands)(f).map(({ column, comparator, operand }) => {
|
|
1281
|
+
const col = column === "numValue" ? cols.numValue : cols.strValue;
|
|
1282
|
+
const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
|
|
1283
|
+
return `${quote("a")}.${quote(col)} ${comparator} ${literal}`;
|
|
1284
|
+
});
|
|
1285
|
+
if (comparisons.length === 0) return "1 = 0";
|
|
1170
1286
|
const a = quote(cols.table);
|
|
1171
1287
|
const sub = quote("a");
|
|
1172
1288
|
const outerId = `${quote(alias)}.${quote(cols.runPk)}`;
|
|
1173
|
-
|
|
1289
|
+
const match = comparisons.length === 1 ? comparisons[0] : `(${comparisons.join(" OR ")})`;
|
|
1290
|
+
return `EXISTS (SELECT 1 FROM ${a} ${sub} WHERE ${sub}.${quote(cols.runId)} = ${outerId} AND ${sub}.${quote(cols.key)} = '${f.key.replace(/'/g, "''")}' AND ${match})`;
|
|
1174
1291
|
}
|
|
1175
1292
|
/** Resolve the side-table's actual column names from MikroORM metadata, so the raw EXISTS matches
|
|
1176
1293
|
* the active naming strategy (underscore vs camelCase) rather than guessing. The run PK column is
|