@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 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
- where.$and = [
1079
- {
1080
- status: query.status
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
- const tagsCol = this.tagsColumn(em);
1131
- const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1132
- const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`);
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. Identifiers are quoted per
1164
- * driver and operands are inlined as literals (string operands are escaped by doubling quotes). */
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 col = (0, import_nestjs_durable_core.attributeColumnFor)(f) === "numValue" ? cols.numValue : cols.strValue;
1167
- const cmp = (0, import_nestjs_durable_core.sqlComparator)(f.op);
1168
- const operand = (0, import_nestjs_durable_core.attributeOperand)(f);
1169
- const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
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
- return `EXISTS (SELECT 1 FROM ${a} ${sub} WHERE ${sub}.${quote(cols.runId)} = ${outerId} AND ${sub}.${quote(cols.key)} = '${f.key.replace(/'/g, "''")}' AND ${sub}.${quote(col)} ${cmp} ${literal})`;
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