@dudousxd/nestjs-durable-store-mikro-orm 0.25.0 → 0.27.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 +350 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -15
- package/dist/index.d.ts +60 -15
- package/dist/index.js +349 -120
- package/dist/index.js.map +1 -1
- package/package.json +14 -3
package/dist/index.js
CHANGED
|
@@ -36,6 +36,11 @@ var BufferedEventRepository = class extends EntityRepository {
|
|
|
36
36
|
__name(this, "BufferedEventRepository");
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
+
var ScheduleRepository = class extends EntityRepository {
|
|
40
|
+
static {
|
|
41
|
+
__name(this, "ScheduleRepository");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
39
44
|
|
|
40
45
|
// src/entities.ts
|
|
41
46
|
function snakeCase(property) {
|
|
@@ -135,6 +140,24 @@ var BufferedEventEntity = class {
|
|
|
135
140
|
payload;
|
|
136
141
|
publishedAt;
|
|
137
142
|
};
|
|
143
|
+
var ScheduleEntity = class {
|
|
144
|
+
static {
|
|
145
|
+
__name(this, "ScheduleEntity");
|
|
146
|
+
}
|
|
147
|
+
[EntityRepositoryType];
|
|
148
|
+
id;
|
|
149
|
+
namespace;
|
|
150
|
+
workflow;
|
|
151
|
+
paused;
|
|
152
|
+
/** Epoch ms as a `bigint` column — exact, so `updateSchedule`'s compare-and-set can't be defeated
|
|
153
|
+
* by a DATETIME's truncated milliseconds. Drivers may hand it back as a string. */
|
|
154
|
+
nextFireAt;
|
|
155
|
+
tags;
|
|
156
|
+
spec;
|
|
157
|
+
state;
|
|
158
|
+
createdAt;
|
|
159
|
+
updatedAt;
|
|
160
|
+
};
|
|
138
161
|
function namespaceFilterCond(args) {
|
|
139
162
|
if (args?.namespace === void 0) return {};
|
|
140
163
|
return {
|
|
@@ -534,20 +557,83 @@ function durableEntities(options = {}) {
|
|
|
534
557
|
}
|
|
535
558
|
}
|
|
536
559
|
});
|
|
560
|
+
const schedules = new EntitySchema({
|
|
561
|
+
class: ScheduleEntity,
|
|
562
|
+
tableName: "durable_schedules",
|
|
563
|
+
repository: /* @__PURE__ */ __name(() => ScheduleRepository, "repository"),
|
|
564
|
+
// The poll's "due by T" scan: `paused = false AND next_fire_at <= T ORDER BY next_fire_at`.
|
|
565
|
+
indexes: [
|
|
566
|
+
{
|
|
567
|
+
name: "durable_schedules_due_idx",
|
|
568
|
+
properties: [
|
|
569
|
+
"paused",
|
|
570
|
+
"nextFireAt"
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
properties: {
|
|
575
|
+
id: {
|
|
576
|
+
type: "string",
|
|
577
|
+
primary: true,
|
|
578
|
+
fieldName: col("id")
|
|
579
|
+
},
|
|
580
|
+
namespace: {
|
|
581
|
+
type: "string",
|
|
582
|
+
default: "default",
|
|
583
|
+
fieldName: col("namespace")
|
|
584
|
+
},
|
|
585
|
+
workflow: {
|
|
586
|
+
type: "string",
|
|
587
|
+
fieldName: col("workflow")
|
|
588
|
+
},
|
|
589
|
+
paused: {
|
|
590
|
+
type: "boolean",
|
|
591
|
+
default: false,
|
|
592
|
+
fieldName: col("paused")
|
|
593
|
+
},
|
|
594
|
+
nextFireAt: {
|
|
595
|
+
type: "bigint",
|
|
596
|
+
nullable: true,
|
|
597
|
+
fieldName: col("nextFireAt")
|
|
598
|
+
},
|
|
599
|
+
tags: {
|
|
600
|
+
type: "json",
|
|
601
|
+
nullable: true,
|
|
602
|
+
fieldName: col("tags")
|
|
603
|
+
},
|
|
604
|
+
spec: {
|
|
605
|
+
type: "json",
|
|
606
|
+
fieldName: col("spec")
|
|
607
|
+
},
|
|
608
|
+
state: {
|
|
609
|
+
type: "json",
|
|
610
|
+
fieldName: col("state")
|
|
611
|
+
},
|
|
612
|
+
createdAt: {
|
|
613
|
+
type: "Date",
|
|
614
|
+
fieldName: col("createdAt")
|
|
615
|
+
},
|
|
616
|
+
updatedAt: {
|
|
617
|
+
type: "Date",
|
|
618
|
+
fieldName: col("updatedAt")
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
537
622
|
return [
|
|
538
623
|
workflowRuns,
|
|
539
624
|
stepCheckpoints,
|
|
540
625
|
runAttributes,
|
|
541
626
|
signalWaiters,
|
|
542
627
|
bufferedSignals,
|
|
543
|
-
bufferedEvents
|
|
628
|
+
bufferedEvents,
|
|
629
|
+
schedules
|
|
544
630
|
];
|
|
545
631
|
}
|
|
546
632
|
__name(durableEntities, "durableEntities");
|
|
547
633
|
var ENTITIES = durableEntities();
|
|
548
634
|
|
|
549
635
|
// src/mikro-orm-state-store.ts
|
|
550
|
-
import {
|
|
636
|
+
import { ENGINE_MINTED_TAG_PREFIXES, RUN_VALUE_FACET_LIMIT, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, facetOrigin, mergeRunFacetRows, normalizeAttributeRows, parseDuration } from "@dudousxd/nestjs-durable-core";
|
|
551
637
|
import { raw } from "@mikro-orm/core";
|
|
552
638
|
|
|
553
639
|
// src/schema.ts
|
|
@@ -558,7 +644,8 @@ var DURABLE_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
|
558
644
|
"durable_run_attributes",
|
|
559
645
|
"durable_signal_waiters",
|
|
560
646
|
"durable_buffered_signals",
|
|
561
|
-
"durable_buffered_events"
|
|
647
|
+
"durable_buffered_events",
|
|
648
|
+
"durable_schedules"
|
|
562
649
|
]);
|
|
563
650
|
function durableManagedTables() {
|
|
564
651
|
return [
|
|
@@ -855,71 +942,69 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
855
942
|
id: runId
|
|
856
943
|
});
|
|
857
944
|
}
|
|
945
|
+
async deleteRuns(runIds) {
|
|
946
|
+
if (runIds.length === 0) return;
|
|
947
|
+
const runId = {
|
|
948
|
+
$in: runIds
|
|
949
|
+
};
|
|
950
|
+
await this.fork().transactional(async (tem) => {
|
|
951
|
+
await tem.nativeDelete(StepCheckpointEntity, {
|
|
952
|
+
runId
|
|
953
|
+
});
|
|
954
|
+
await tem.nativeDelete(SignalWaiterEntity, {
|
|
955
|
+
runId
|
|
956
|
+
});
|
|
957
|
+
await tem.nativeDelete(RunAttributeEntity, {
|
|
958
|
+
runId
|
|
959
|
+
});
|
|
960
|
+
await tem.nativeDelete(WorkflowRunEntity, {
|
|
961
|
+
id: runId
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
}
|
|
858
965
|
async pruneTerminalRuns(policy, nowMs, limit) {
|
|
859
|
-
|
|
966
|
+
const statuses = policy.statuses.filter((s) => TERMINAL_RUN_STATUSES.includes(s));
|
|
967
|
+
if (statuses.length === 0 || limit <= 0) return 0;
|
|
968
|
+
if (policy.maxAge == null && policy.maxCount == null) return 0;
|
|
860
969
|
const em = this.fork();
|
|
861
|
-
const
|
|
862
|
-
|
|
970
|
+
const meta = em.getMetadata().get(WorkflowRunEntity);
|
|
971
|
+
const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
|
|
972
|
+
const query = {
|
|
973
|
+
...policy.scope ?? {},
|
|
974
|
+
statuses
|
|
863
975
|
};
|
|
976
|
+
const scoped = /* @__PURE__ */ __name((extra) => this.runQueryBuilder(em, query, {
|
|
977
|
+
...this.runWhere(query),
|
|
978
|
+
...extra ?? {}
|
|
979
|
+
}).select([
|
|
980
|
+
`r.${idCol}`
|
|
981
|
+
]), "scoped");
|
|
864
982
|
const ids = /* @__PURE__ */ new Set();
|
|
865
983
|
if (policy.maxAge != null) {
|
|
866
984
|
const cutoff = new Date(nowMs - parseDuration(policy.maxAge));
|
|
867
|
-
const rows = await
|
|
868
|
-
status,
|
|
985
|
+
const rows = await scoped({
|
|
869
986
|
updatedAt: {
|
|
870
987
|
$lt: cutoff
|
|
871
988
|
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
orderBy: {
|
|
877
|
-
updatedAt: "asc"
|
|
878
|
-
},
|
|
879
|
-
limit
|
|
880
|
-
});
|
|
881
|
-
for (const r of rows) ids.add(r.id);
|
|
989
|
+
}).orderBy({
|
|
990
|
+
"r.updatedAt": "asc"
|
|
991
|
+
}).limit(limit).execute();
|
|
992
|
+
for (const r of rows) ids.add(String(r[idCol] ?? r.id));
|
|
882
993
|
}
|
|
883
994
|
if (policy.maxCount != null && ids.size < limit) {
|
|
884
|
-
const rows = await
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
"id"
|
|
889
|
-
],
|
|
890
|
-
orderBy: {
|
|
891
|
-
updatedAt: "desc",
|
|
892
|
-
id: "desc"
|
|
893
|
-
},
|
|
894
|
-
limit,
|
|
895
|
-
offset: policy.maxCount
|
|
896
|
-
});
|
|
995
|
+
const rows = await scoped().orderBy({
|
|
996
|
+
"r.updatedAt": "desc",
|
|
997
|
+
"r.id": "desc"
|
|
998
|
+
}).limit(limit).offset(policy.maxCount).execute();
|
|
897
999
|
for (const r of rows) {
|
|
898
|
-
ids.add(r.id);
|
|
899
1000
|
if (ids.size >= limit) break;
|
|
1001
|
+
ids.add(String(r[idCol] ?? r.id));
|
|
900
1002
|
}
|
|
901
1003
|
}
|
|
902
|
-
if (ids.size === 0) return 0;
|
|
903
1004
|
const idList = [
|
|
904
1005
|
...ids
|
|
905
1006
|
].slice(0, limit);
|
|
906
|
-
|
|
907
|
-
$in: idList
|
|
908
|
-
};
|
|
909
|
-
await em.transactional(async (tem) => {
|
|
910
|
-
await tem.nativeDelete(StepCheckpointEntity, {
|
|
911
|
-
runId
|
|
912
|
-
});
|
|
913
|
-
await tem.nativeDelete(SignalWaiterEntity, {
|
|
914
|
-
runId
|
|
915
|
-
});
|
|
916
|
-
await tem.nativeDelete(RunAttributeEntity, {
|
|
917
|
-
runId
|
|
918
|
-
});
|
|
919
|
-
await tem.nativeDelete(WorkflowRunEntity, {
|
|
920
|
-
id: runId
|
|
921
|
-
});
|
|
922
|
-
});
|
|
1007
|
+
await this.deleteRuns(idList);
|
|
923
1008
|
return idList.length;
|
|
924
1009
|
}
|
|
925
1010
|
async getCheckpoint(runId, seq) {
|
|
@@ -1131,6 +1216,11 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1131
1216
|
const colExpr = this.jsonAsText(em, `${quote("r")}.${quote(this.tagsColumn(em))}`);
|
|
1132
1217
|
return `${colExpr} LIKE '${pattern}'`;
|
|
1133
1218
|
}
|
|
1219
|
+
async countRuns(query) {
|
|
1220
|
+
const em = this.fork();
|
|
1221
|
+
const rows = await this.runQueryBuilder(em, query, this.runWhere(query)).select([]).addSelect(raw("count(*) as count")).execute();
|
|
1222
|
+
return Number(rows[0]?.count ?? 0);
|
|
1223
|
+
}
|
|
1134
1224
|
/** `GROUP BY status, origin` over the same predicates {@link listRuns} pages — one aggregate, so a
|
|
1135
1225
|
* console can show whole-set counts next to a bounded page instead of downloading every run to
|
|
1136
1226
|
* count them in the browser. */
|
|
@@ -1151,83 +1241,118 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1151
1241
|
count: Number(r.count)
|
|
1152
1242
|
})));
|
|
1153
1243
|
}
|
|
1154
|
-
/**
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1244
|
+
/**
|
|
1245
|
+
* Distinct values of one filter axis over the same predicates {@link listRuns} pages, with counts —
|
|
1246
|
+
* what a console's value pickers list.
|
|
1247
|
+
*
|
|
1248
|
+
* All three axis families are answered by ONE grouped query against the filtered run set, so the
|
|
1249
|
+
* counts are exact over that whole set (not a sample), the ordering is stable, and a picker can
|
|
1250
|
+
* page and search server-side. That last part is what makes a picker usable on the axis that needs
|
|
1251
|
+
* it most: tag cardinality grows with the data, so the values an operator is looking for are
|
|
1252
|
+
* routinely outside any top slice, and a search that only filtered the fetched page could never
|
|
1253
|
+
* find them.
|
|
1254
|
+
*
|
|
1255
|
+
* The predicates come from {@link runQueryBuilder} as a subquery rather than being rewritten here,
|
|
1256
|
+
* so a picker and the list it filters can never disagree about which runs they are talking about.
|
|
1257
|
+
*/
|
|
1158
1258
|
async runValueFacets(axis, query, opts) {
|
|
1159
|
-
if (!axisIsRunColumn(axis)) {
|
|
1160
|
-
return this.scannedValueFacets(axis, query, opts);
|
|
1161
|
-
}
|
|
1162
1259
|
const em = this.fork();
|
|
1163
|
-
const
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1260
|
+
const quote = this.idQuote(em);
|
|
1261
|
+
const limit = Math.max(0, opts?.limit ?? RUN_VALUE_FACET_LIMIT);
|
|
1262
|
+
const offset = Math.max(0, opts?.offset ?? 0);
|
|
1263
|
+
if (limit === 0) return [];
|
|
1264
|
+
const runs = this.runSubquery(em, query, axis);
|
|
1265
|
+
const { source, valueExpr } = this.valueSource(em, axis, runs, quote);
|
|
1266
|
+
const conditions = [
|
|
1267
|
+
this.searchCondition(valueExpr, opts?.search)
|
|
1268
|
+
].filter((part) => part !== void 0);
|
|
1269
|
+
const where = conditions.length ? ` where ${conditions.join(" and ")}` : "";
|
|
1270
|
+
const engineLast = axis.field === "tag" ? `case when ${valueExpr} like '${ENGINE_MINTED_TAG_PREFIXES[0]}%' then 1 else 0 end, ` : "";
|
|
1271
|
+
const sql = `select ${valueExpr} as value, count(*) as count from ${source}${where} group by ${valueExpr} order by ${engineLast}count(*) desc, ${valueExpr} asc limit ${limit} offset ${offset}`;
|
|
1272
|
+
const rows = await em.getConnection().execute(sql);
|
|
1273
|
+
return rows.map((row) => ({
|
|
1274
|
+
// Blank and absent are the same thing to an operator on the origin axis — see facetOrigin.
|
|
1275
|
+
value: axis.field === "origin" ? facetOrigin(row.value) : this.text(row.value),
|
|
1172
1276
|
count: Number(row.count)
|
|
1173
|
-
}))
|
|
1277
|
+
}));
|
|
1278
|
+
}
|
|
1279
|
+
/** The filtered run set as a derived table, carrying only the columns the axis needs. One source
|
|
1280
|
+
* of truth with `listRuns`: the same builder, the same predicates. */
|
|
1281
|
+
runSubquery(em, query, axis) {
|
|
1282
|
+
const meta = em.getMetadata().get(WorkflowRunEntity);
|
|
1283
|
+
const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
|
|
1284
|
+
const columns = axis.field === "tag" ? [
|
|
1285
|
+
`r.${idCol}`,
|
|
1286
|
+
`r.${this.tagsColumn(em)}`
|
|
1287
|
+
] : axisIsRunColumn(axis) ? [
|
|
1288
|
+
`r.${meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field}`
|
|
1289
|
+
] : [
|
|
1290
|
+
`r.${idCol}`
|
|
1291
|
+
];
|
|
1292
|
+
return this.runQueryBuilder(em, query, this.runWhere(query)).select(columns).getFormattedQuery();
|
|
1293
|
+
}
|
|
1294
|
+
/** Where the values of an axis come from, and how to name one — a column of the filtered runs, an
|
|
1295
|
+
* element of their tags array, or a row of the attribute side table. */
|
|
1296
|
+
valueSource(em, axis, runs, quote) {
|
|
1297
|
+
const meta = em.getMetadata().get(WorkflowRunEntity);
|
|
1298
|
+
if (axisIsRunColumn(axis)) {
|
|
1299
|
+
const col = meta.properties[axis.field]?.fieldNames?.[0] ?? axis.field;
|
|
1300
|
+
return {
|
|
1301
|
+
source: `(${runs}) as ${quote("r")}`,
|
|
1302
|
+
valueExpr: `${quote("r")}.${quote(col)}`
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
if (axis.field === "tag") {
|
|
1306
|
+
return {
|
|
1307
|
+
source: `(${runs}) as ${quote("r")} ${this.jsonArrayJoin(em, `${quote("r")}.${quote(this.tagsColumn(em))}`, quote)}`,
|
|
1308
|
+
valueExpr: `${quote("t")}.${quote("value")}`
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const cols = this.attributeColumns(em);
|
|
1312
|
+
const idCol = meta.properties.id?.fieldNames?.[0] ?? "id";
|
|
1313
|
+
const a = quote("a");
|
|
1314
|
+
const join = `${quote(cols.table)} as ${a} join (${runs}) as ${quote("r")} on ${a}.${quote(cols.runId)} = ${quote("r")}.${quote(idCol)}`;
|
|
1315
|
+
if (axis.field === "attributeKey") {
|
|
1316
|
+
return {
|
|
1317
|
+
source: join,
|
|
1318
|
+
valueExpr: `${a}.${quote(cols.key)}`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
const key = axis.key.replace(/'/g, "''");
|
|
1322
|
+
return {
|
|
1323
|
+
source: `${join} and ${a}.${quote(cols.key)} = '${key}'`,
|
|
1324
|
+
valueExpr: `coalesce(${a}.${quote(cols.strValue)}, cast(${a}.${quote(cols.numValue)} as char))`
|
|
1325
|
+
};
|
|
1174
1326
|
}
|
|
1175
1327
|
/**
|
|
1176
|
-
* The
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
* memory — pulls `input`, `output` and `error` for every run in the window, which on a control
|
|
1181
|
-
* plane with real payloads is tens of megabytes fetched to produce a list of a hundred short
|
|
1182
|
-
* strings, every time a picker opens. These project the one column each question needs instead:
|
|
1183
|
-
* the tags column, or the run ids that then drive one grouped read of the side table.
|
|
1328
|
+
* The dialect's way of turning a JSON array column into one row per element — the only portable
|
|
1329
|
+
* thing about it being that all three have one. Without it a tag picker cannot be grouped in the
|
|
1330
|
+
* database at all, and falls back to deduping a bounded page of runs in memory: counts that
|
|
1331
|
+
* describe a sample, and an order that cannot be paged.
|
|
1184
1332
|
*/
|
|
1185
|
-
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const idCol = runMeta.properties.id?.fieldNames?.[0] ?? "id";
|
|
1191
|
-
if (axis.field === "tag") {
|
|
1192
|
-
const tagsCol = this.tagsColumn(em);
|
|
1193
|
-
const rows = await this.runQueryBuilder(em, query, where).select([
|
|
1194
|
-
`r.${tagsCol}`
|
|
1195
|
-
]).orderBy({
|
|
1196
|
-
createdAt: "desc"
|
|
1197
|
-
}).limit(scan).execute();
|
|
1198
|
-
const values = [];
|
|
1199
|
-
for (const row of rows) {
|
|
1200
|
-
const raw2 = row.tags ?? row[tagsCol];
|
|
1201
|
-
const tags = typeof raw2 === "string" ? JSON.parse(raw2) : raw2;
|
|
1202
|
-
if (!Array.isArray(tags)) continue;
|
|
1203
|
-
for (const tag of tags) values.push({
|
|
1204
|
-
value: String(tag),
|
|
1205
|
-
count: 1
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
return mergeRunValueFacetRows(values, opts);
|
|
1333
|
+
jsonArrayJoin(em, colRef, quote) {
|
|
1334
|
+
const platform = String(em.getPlatform().constructor.name).toLowerCase();
|
|
1335
|
+
const t = quote("t");
|
|
1336
|
+
if (platform.includes("postgre")) {
|
|
1337
|
+
return `cross join lateral jsonb_array_elements_text(${colRef}::jsonb) as ${t}(${quote("value")})`;
|
|
1209
1338
|
}
|
|
1210
|
-
|
|
1211
|
-
`
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
return mergeRunValueFacetRows(attributes.map((row) => ({
|
|
1228
|
-
value: axis.field === "attributeKey" ? row.key : row.strValue ?? (row.numValue == null ? null : String(row.numValue)),
|
|
1229
|
-
count: 1
|
|
1230
|
-
})), opts);
|
|
1339
|
+
if (platform.includes("mysql") || platform.includes("mariadb")) {
|
|
1340
|
+
return `join json_table(cast(${colRef} as json), '$[*]' columns (${quote("value")} varchar(512) path '$')) as ${t}`;
|
|
1341
|
+
}
|
|
1342
|
+
return `join json_each(${colRef}) as ${t}`;
|
|
1343
|
+
}
|
|
1344
|
+
/** `lower(value) like '%needle%'` — the picker's search box, applied BEFORE the page is cut so a
|
|
1345
|
+
* match outside the top slice is still reachable. */
|
|
1346
|
+
searchCondition(valueExpr, search) {
|
|
1347
|
+
const needle = search?.trim().toLowerCase();
|
|
1348
|
+
if (!needle) return void 0;
|
|
1349
|
+
const escaped = needle.replace(/'/g, "''").replace(/[%_]/g, (c) => `\\${c}`);
|
|
1350
|
+
return `lower(${valueExpr}) like '%${escaped}%'`;
|
|
1351
|
+
}
|
|
1352
|
+
/** A grouped value as the string a picker shows. Drivers return a JSON element or a numeric
|
|
1353
|
+
* attribute as something other than a string; `null` stays null (the absent bucket). */
|
|
1354
|
+
text(value) {
|
|
1355
|
+
return value === null || value === void 0 ? null : String(value);
|
|
1231
1356
|
}
|
|
1232
1357
|
/** One attribute predicate as a raw EXISTS subquery on the side-table, correlated to the outer run
|
|
1233
1358
|
* alias. `<>` (ne) also excludes runs where the attribute is absent (missing-key-never-matches):
|
|
@@ -1287,6 +1412,76 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
|
|
|
1287
1412
|
const ch = isMysql ? "`" : '"';
|
|
1288
1413
|
return (id) => `${ch}${id}${ch}`;
|
|
1289
1414
|
}
|
|
1415
|
+
// ---- persisted schedules ----------------------------------------------------------------------
|
|
1416
|
+
async saveSchedule(record) {
|
|
1417
|
+
const em = this.fork();
|
|
1418
|
+
await em.upsert(ScheduleEntity, toScheduleEntity(record));
|
|
1419
|
+
}
|
|
1420
|
+
async getSchedule(id) {
|
|
1421
|
+
const entity = await this.fork().findOne(ScheduleEntity, {
|
|
1422
|
+
id
|
|
1423
|
+
});
|
|
1424
|
+
return entity ? fromScheduleEntity(entity) : null;
|
|
1425
|
+
}
|
|
1426
|
+
/** One conditional `nativeUpdate`: the compare-and-set on `next_fire_at` (an exact bigint) is a
|
|
1427
|
+
* single statement. */
|
|
1428
|
+
async updateSchedule(id, patch, expectedNextFireAt) {
|
|
1429
|
+
const data = {};
|
|
1430
|
+
if (patch.namespace !== void 0) data.namespace = patch.namespace;
|
|
1431
|
+
if (patch.workflow !== void 0) data.workflow = patch.workflow;
|
|
1432
|
+
if (patch.paused !== void 0) data.paused = patch.paused;
|
|
1433
|
+
if ("nextFireAt" in patch) data.nextFireAt = patch.nextFireAt ?? null;
|
|
1434
|
+
if ("tags" in patch) data.tags = patch.tags ?? null;
|
|
1435
|
+
if (patch.spec !== void 0) data.spec = patch.spec;
|
|
1436
|
+
if (patch.state !== void 0) data.state = patch.state;
|
|
1437
|
+
if (patch.updatedAt !== void 0) data.updatedAt = patch.updatedAt;
|
|
1438
|
+
if (!Object.keys(data).length) return await this.getSchedule(id) !== null;
|
|
1439
|
+
const where = {
|
|
1440
|
+
id
|
|
1441
|
+
};
|
|
1442
|
+
if (expectedNextFireAt !== void 0) where.nextFireAt = expectedNextFireAt;
|
|
1443
|
+
const affected = await this.fork().nativeUpdate(ScheduleEntity, where, data);
|
|
1444
|
+
return affected > 0;
|
|
1445
|
+
}
|
|
1446
|
+
async deleteSchedule(id) {
|
|
1447
|
+
return await this.fork().nativeDelete(ScheduleEntity, {
|
|
1448
|
+
id
|
|
1449
|
+
}) > 0;
|
|
1450
|
+
}
|
|
1451
|
+
async listSchedules(query) {
|
|
1452
|
+
const em = this.fork();
|
|
1453
|
+
const where = {};
|
|
1454
|
+
if (query.namespace !== void 0) where.namespace = query.namespace;
|
|
1455
|
+
if (query.workflow !== void 0) where.workflow = query.workflow;
|
|
1456
|
+
if (query.paused !== void 0) where.paused = query.paused;
|
|
1457
|
+
if (query.dueBy !== void 0) {
|
|
1458
|
+
where.$and = [
|
|
1459
|
+
{
|
|
1460
|
+
paused: false
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
nextFireAt: {
|
|
1464
|
+
$lte: query.dueBy
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
];
|
|
1468
|
+
}
|
|
1469
|
+
const quote = this.idQuote(em);
|
|
1470
|
+
const meta = em.getMetadata().get(ScheduleEntity);
|
|
1471
|
+
const tagsCol = meta.properties.tags?.fieldNames?.[0] ?? "tags";
|
|
1472
|
+
const qb = em.createQueryBuilder(ScheduleEntity, "r").where(where);
|
|
1473
|
+
if (query.tag !== void 0) {
|
|
1474
|
+
const pattern = `%"${query.tag.replace(/'/g, "''")}"%`;
|
|
1475
|
+
qb.andWhere(`${this.jsonAsText(em, `${quote("r")}.${quote(tagsCol)}`)} LIKE '${pattern}'`);
|
|
1476
|
+
}
|
|
1477
|
+
qb.orderBy({
|
|
1478
|
+
"r.nextFireAt": "asc nulls last",
|
|
1479
|
+
"r.id": "asc"
|
|
1480
|
+
});
|
|
1481
|
+
if (query.limit != null) qb.limit(query.limit);
|
|
1482
|
+
if (query.offset != null) qb.offset(query.offset);
|
|
1483
|
+
return (await qb.getResultList()).map(fromScheduleEntity);
|
|
1484
|
+
}
|
|
1290
1485
|
async listCheckpoints(runId) {
|
|
1291
1486
|
const em = this.fork();
|
|
1292
1487
|
const rows = await em.find(StepCheckpointEntity, {
|
|
@@ -1525,6 +1720,38 @@ function fromCheckpointEntity(e) {
|
|
|
1525
1720
|
};
|
|
1526
1721
|
}
|
|
1527
1722
|
__name(fromCheckpointEntity, "fromCheckpointEntity");
|
|
1723
|
+
function toScheduleEntity(r) {
|
|
1724
|
+
const e = new ScheduleEntity();
|
|
1725
|
+
e.id = r.id;
|
|
1726
|
+
e.namespace = r.namespace;
|
|
1727
|
+
e.workflow = r.workflow;
|
|
1728
|
+
e.paused = r.paused;
|
|
1729
|
+
e.nextFireAt = r.nextFireAt;
|
|
1730
|
+
e.tags = r.tags ?? null;
|
|
1731
|
+
e.spec = r.spec;
|
|
1732
|
+
e.state = r.state;
|
|
1733
|
+
e.createdAt = r.createdAt;
|
|
1734
|
+
e.updatedAt = r.updatedAt;
|
|
1735
|
+
return e;
|
|
1736
|
+
}
|
|
1737
|
+
__name(toScheduleEntity, "toScheduleEntity");
|
|
1738
|
+
function fromScheduleEntity(e) {
|
|
1739
|
+
return {
|
|
1740
|
+
id: e.id,
|
|
1741
|
+
namespace: e.namespace,
|
|
1742
|
+
workflow: e.workflow,
|
|
1743
|
+
paused: Boolean(e.paused),
|
|
1744
|
+
nextFireAt: e.nextFireAt == null ? null : Number(e.nextFireAt),
|
|
1745
|
+
...e.tags?.length ? {
|
|
1746
|
+
tags: e.tags
|
|
1747
|
+
} : {},
|
|
1748
|
+
spec: e.spec ?? {},
|
|
1749
|
+
state: e.state ?? {},
|
|
1750
|
+
createdAt: e.createdAt,
|
|
1751
|
+
updatedAt: e.updatedAt
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
__name(fromScheduleEntity, "fromScheduleEntity");
|
|
1528
1755
|
export {
|
|
1529
1756
|
BufferedEventEntity,
|
|
1530
1757
|
BufferedEventRepository,
|
|
@@ -1534,6 +1761,8 @@ export {
|
|
|
1534
1761
|
MikroOrmStateStore,
|
|
1535
1762
|
RunAttributeEntity,
|
|
1536
1763
|
RunAttributeRepository,
|
|
1764
|
+
ScheduleEntity,
|
|
1765
|
+
ScheduleRepository,
|
|
1537
1766
|
SignalWaiterEntity,
|
|
1538
1767
|
SignalWaiterRepository,
|
|
1539
1768
|
StepCheckpointEntity,
|