@dudousxd/nestjs-durable-store-mikro-orm 0.23.0 → 0.24.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.js CHANGED
@@ -547,7 +547,7 @@ __name(durableEntities, "durableEntities");
547
547
  var ENTITIES = durableEntities();
548
548
 
549
549
  // src/mikro-orm-state-store.ts
550
- import { attributeColumnFor, attributeOperand, mergeRunFacetRows, normalizeAttributeRows, parseDuration, sqlComparator } from "@dudousxd/nestjs-durable-core";
550
+ import { RUN_VALUE_FACET_SCAN, attributePredicateOperands, axisIsRunColumn, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns } from "@dudousxd/nestjs-durable-core";
551
551
  import { raw } from "@mikro-orm/core";
552
552
 
553
553
  // src/schema.ts
@@ -1031,20 +1031,47 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1031
1031
  * applied by the QueryBuilder path in both callers. */
1032
1032
  runWhere(query) {
1033
1033
  const where = {};
1034
+ const and = [];
1034
1035
  if (query.workflow) where.workflow = query.workflow;
1036
+ if (query.workflows) and.push({
1037
+ workflow: {
1038
+ $in: query.workflows
1039
+ }
1040
+ });
1035
1041
  if (query.namespace !== void 0) where.namespace = query.namespace;
1042
+ if (query.namespaces) and.push({
1043
+ namespace: {
1044
+ $in: query.namespaces
1045
+ }
1046
+ });
1036
1047
  if (query.origin !== void 0) where.origin = query.origin;
1048
+ if (query.origins) {
1049
+ const named = query.origins.filter((o) => o !== null);
1050
+ const branches = [];
1051
+ if (named.length) branches.push({
1052
+ origin: {
1053
+ $in: named
1054
+ }
1055
+ });
1056
+ if (query.origins.some((o) => o === null)) branches.push({
1057
+ origin: null
1058
+ });
1059
+ and.push(branches.length ? {
1060
+ $or: branches
1061
+ } : {
1062
+ origin: {
1063
+ $in: []
1064
+ }
1065
+ });
1066
+ }
1037
1067
  if (query.status && query.statuses) {
1038
- where.$and = [
1039
- {
1040
- status: query.status
1041
- },
1042
- {
1043
- status: {
1044
- $in: query.statuses
1045
- }
1068
+ and.push({
1069
+ status: query.status
1070
+ }, {
1071
+ status: {
1072
+ $in: query.statuses
1046
1073
  }
1047
- ];
1074
+ });
1048
1075
  } else if (query.status) {
1049
1076
  where.status = query.status;
1050
1077
  } else if (query.statuses) {
@@ -1052,6 +1079,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1052
1079
  $in: query.statuses
1053
1080
  };
1054
1081
  }
1082
+ if (and.length) where.$and = and;
1055
1083
  return where;
1056
1084
  }
1057
1085
  async listRuns(query) {
@@ -1060,7 +1088,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1060
1088
  const orderBy = {
1061
1089
  createdAt: "desc"
1062
1090
  };
1063
- if (query.tag || query.attributes?.length) {
1091
+ if (query.tag || query.tags || query.attributes?.length) {
1064
1092
  const qb = this.runQueryBuilder(em, query, where).orderBy(orderBy);
1065
1093
  if (query.limit != null) qb.limit(query.limit);
1066
1094
  if (query.offset != null) qb.offset(query.offset);
@@ -1086,17 +1114,23 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1086
1114
  const quote = this.idQuote(em);
1087
1115
  const cols = this.attributeColumns(em);
1088
1116
  const qb = em.createQueryBuilder(WorkflowRunEntity, "r").where(where);
1089
- if (query.tag) {
1090
- const tagsCol = this.tagsColumn(em);
1091
- const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
1092
- const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`);
1093
- qb.andWhere(`${colExpr} LIKE '${pattern}'`);
1117
+ if (query.tag) qb.andWhere(this.tagLikeSql(em, query.tag, quote));
1118
+ if (query.tags) {
1119
+ const branches = query.tags.map((t) => this.tagLikeSql(em, t, quote));
1120
+ qb.andWhere(branches.length ? `(${branches.join(" OR ")})` : "1 = 0");
1094
1121
  }
1095
1122
  for (const f of query.attributes ?? []) {
1096
1123
  qb.andWhere(this.attributeExistsSql(f, quote, "r", cols));
1097
1124
  }
1098
1125
  return qb;
1099
1126
  }
1127
+ /** `tags LIKE '%"etl"%'` — the quoted token, so `etl` doesn't match `etl-foo`. See the note in
1128
+ * {@link listRuns} for why this is raw SQL rather than a MikroORM condition. */
1129
+ tagLikeSql(em, tag, quote) {
1130
+ const pattern = `%"${tag.replace(/'/g, "''")}"%`;
1131
+ const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
1132
+ return `${colExpr} LIKE '${pattern}'`;
1133
+ }
1100
1134
  /** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
1101
1135
  * console can show whole-set counts next to a bounded page instead of downloading every run to
1102
1136
  * count them in the browser. */
@@ -1117,20 +1151,50 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1117
1151
  count: Number(r.count)
1118
1152
  })));
1119
1153
  }
1154
+ /** Distinct values of one filter axis over the same predicates {@link listRuns} pages. A run-table
1155
+ * column is a `GROUP BY` like {@link runFacets}, exact over the whole matching set; the tag and
1156
+ * attribute axes live outside the row (json array / side table) and are counted from a bounded
1157
+ * page of runs instead — see {@link RunValueFacetOptions.scan}. */
1158
+ async runValueFacets(axis, query, opts) {
1159
+ if (!axisIsRunColumn(axis)) {
1160
+ const runs = await this.listRuns({
1161
+ ...query,
1162
+ limit: opts?.scan ?? RUN_VALUE_FACET_SCAN
1163
+ });
1164
+ return runValueFacetsFromRuns(runs, axis, opts);
1165
+ }
1166
+ const em = this.fork();
1167
+ const meta = em.getMetadata().get(WorkflowRunEntity);
1168
+ const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
1169
+ const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([
1170
+ `r.${col}`
1171
+ ]).addSelect(raw("count(*) as count")).groupBy([
1172
+ `r.${col}`
1173
+ ]).execute();
1174
+ return mergeRunValueFacetRows(rows.map((row) => ({
1175
+ value: row[axis.field] ?? null,
1176
+ count: Number(row.count)
1177
+ })), opts);
1178
+ }
1120
1179
  /** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
1121
1180
  * alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
1122
1181
  * EXISTS already requires a present row, so EXISTS(... <> ...) is exactly ne-with-present. Numeric
1123
- * operands compare the num column, everything else the str column. Identifiers are quoted per
1124
- * driver and operands are inlined as literals (string operands are escaped by doubling quotes). */
1182
+ * operands compare the num column, everything else the str column. `in` contributes one comparison
1183
+ * per member, ORed inside the single EXISTS — one subquery either way, and an `in` over no values
1184
+ * yields no comparison and so matches nothing. Identifiers are quoted per driver and operands are
1185
+ * inlined as literals (string operands are escaped by doubling quotes). */
1125
1186
  attributeExistsSql(f, quote, alias, cols) {
1126
- const col = attributeColumnFor(f) === "numValue" ? cols.numValue : cols.strValue;
1127
- const cmp = sqlComparator(f.op);
1128
- const operand = attributeOperand(f);
1129
- const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
1187
+ const comparisons = attributePredicateOperands(f).map(({ column, comparator, operand }) => {
1188
+ const col = column === "numValue" ? cols.numValue : cols.strValue;
1189
+ const literal = typeof operand === "number" ? String(operand) : `'${String(operand).replace(/'/g, "''")}'`;
1190
+ return `${quote("a")}.${quote(col)} ${comparator} ${literal}`;
1191
+ });
1192
+ if (comparisons.length === 0) return "1 = 0";
1130
1193
  const a = quote(cols.table);
1131
1194
  const sub = quote("a");
1132
1195
  const outerId = `${quote(alias)}.${quote(cols.runPk)}`;
1133
- 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})`;
1196
+ const match = comparisons.length === 1 ? comparisons[0] : `(${comparisons.join(" OR ")})`;
1197
+ return `EXISTS (SELECT 1 FROM ${a} ${sub} WHERE ${sub}.${quote(cols.runId)} = ${outerId} AND ${sub}.${quote(cols.key)} = '${f.key.replace(/'/g, "''")}' AND ${match})`;
1134
1198
  }
1135
1199
  /** Resolve the side-table's actual column names from MikroORM metadata, so the raw EXISTS matches
1136
1200
  * the active naming strategy (underscore vs camelCase) rather than guessing. The run PK column is