@dyrected/core 2.17.0 → 2.18.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
@@ -1809,6 +1809,9 @@ __export(index_exports, {
1809
1809
  NotFoundError: () => NotFoundError,
1810
1810
  PII_REGEX_PATTERNS: () => PII_REGEX_PATTERNS,
1811
1811
  RESERVED_NAVIGATION_SLUGS: () => RESERVED_NAVIGATION_SLUGS,
1812
+ TASK_LOCKS_COLLECTION: () => TASK_LOCKS_COLLECTION,
1813
+ TASK_LOCKS_COLLECTION_CONFIG: () => TASK_LOCKS_COLLECTION_CONFIG,
1814
+ UNASSIGNED_GROUP_KEY: () => UNASSIGNED_GROUP_KEY,
1812
1815
  ValidationError: () => ValidationError,
1813
1816
  WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
1814
1817
  abs: () => abs,
@@ -1828,6 +1831,7 @@ __export(index_exports, {
1828
1831
  ceil: () => ceil,
1829
1832
  clamp: () => clamp,
1830
1833
  coalesce: () => coalesce,
1834
+ coerceBooleanWhere: () => coerceBooleanWhere,
1831
1835
  collectAdminConditionValidationIssues: () => collectAdminConditionValidationIssues,
1832
1836
  collectConfigDiagnostics: () => collectConfigDiagnostics,
1833
1837
  collectDeclarativeAccessValidationIssues: () => collectDeclarativeAccessValidationIssues,
@@ -1837,6 +1841,7 @@ __export(index_exports, {
1837
1841
  compileNavigation: () => compileNavigation,
1838
1842
  count: () => count,
1839
1843
  createLifecycleEvent: () => createLifecycleEvent,
1844
+ createTaskRunner: () => createTaskRunner,
1840
1845
  createWorkflowDocument: () => createWorkflowDocument,
1841
1846
  defaultValue: () => defaultValue,
1842
1847
  defineAction: () => defineAction,
@@ -1859,6 +1864,7 @@ __export(index_exports, {
1859
1864
  defineJoinField: () => defineJoinField,
1860
1865
  defineJsonField: () => defineJsonField,
1861
1866
  defineKanbanView: () => defineKanbanView,
1867
+ defineMoneyField: () => defineMoneyField,
1862
1868
  defineMultiSelectField: () => defineMultiSelectField,
1863
1869
  defineNumberField: () => defineNumberField,
1864
1870
  defineObjectField: () => defineObjectField,
@@ -1870,6 +1876,7 @@ __export(index_exports, {
1870
1876
  defineSelectField: () => defineSelectField,
1871
1877
  defineTab: () => defineTab,
1872
1878
  defineTableView: () => defineTableView,
1879
+ defineTask: () => defineTask,
1873
1880
  defineTextField: () => defineTextField,
1874
1881
  defineTextareaField: () => defineTextareaField,
1875
1882
  defineTimeField: () => defineTimeField,
@@ -1933,10 +1940,13 @@ __export(index_exports, {
1933
1940
  max: () => max,
1934
1941
  min: () => min,
1935
1942
  nanoid: () => nanoid,
1943
+ nextCronRun: () => nextCronRun,
1936
1944
  normalizeConfig: () => normalizeConfig,
1937
1945
  normalizeDetailItem: () => normalizeDetailItem,
1946
+ normalizeGroupKey: () => normalizeGroupKey,
1938
1947
  now: () => now,
1939
1948
  numericFieldNames: () => numericFieldNames,
1949
+ parseCron: () => parseCron,
1940
1950
  parseMongoWhere: () => parseMongoWhere,
1941
1951
  parseSort: () => parseSort,
1942
1952
  parseSqlWhere: () => parseSqlWhere,
@@ -1951,6 +1961,7 @@ __export(index_exports, {
1951
1961
  replace: () => replace,
1952
1962
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
1953
1963
  resolveAuthorizedSiteId: () => resolveAuthorizedSiteId,
1964
+ resolveIndexName: () => resolveIndexName,
1954
1965
  resolvePrefix: () => resolvePrefix,
1955
1966
  round: () => round,
1956
1967
  runCollectionHooks: () => runCollectionHooks,
@@ -2071,6 +2082,296 @@ var import_jose = require("jose");
2071
2082
  // src/auth/sessions.ts
2072
2083
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
2073
2084
 
2085
+ // src/tasks.ts
2086
+ var TASK_LOCKS_COLLECTION = "__task_locks";
2087
+ var TASK_LOCKS_COLLECTION_CONFIG = {
2088
+ slug: TASK_LOCKS_COLLECTION,
2089
+ labels: { singular: "Task lock", plural: "Task locks" },
2090
+ fields: [
2091
+ { name: "name", type: "text", required: true },
2092
+ // Numeric and promoted so the lock compare-and-set is a real column comparison.
2093
+ { name: "lockedUntil", type: "number", required: true, promoted: true },
2094
+ { name: "lockedBy", type: "text" },
2095
+ { name: "lastScheduledFor", type: "date" },
2096
+ { name: "lastStartedAt", type: "date" },
2097
+ { name: "lastFinishedAt", type: "date" },
2098
+ { name: "lastStatus", type: "text" },
2099
+ { name: "lastError", type: "textarea" }
2100
+ ],
2101
+ access: {
2102
+ read: ({ user }) => !!user?.roles?.includes("admin"),
2103
+ create: () => false,
2104
+ update: () => false,
2105
+ delete: () => false
2106
+ },
2107
+ admin: { hidden: true }
2108
+ };
2109
+ var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
2110
+ var DEFAULT_TICK_MS = 3e4;
2111
+ var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
2112
+ var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
2113
+ function parseCronField(raw, min2, max2, names, nameOffset, label) {
2114
+ const values = /* @__PURE__ */ new Set();
2115
+ const toNumber = (token) => {
2116
+ const named = names?.indexOf(token.toLowerCase());
2117
+ if (named !== void 0 && named >= 0) return named + nameOffset;
2118
+ if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
2119
+ return Number(token);
2120
+ };
2121
+ for (const part of raw.split(",")) {
2122
+ const [rangePart, stepPart] = part.split("/");
2123
+ const step = stepPart === void 0 ? 1 : Number(stepPart);
2124
+ if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
2125
+ let start;
2126
+ let end;
2127
+ if (rangePart === "*") {
2128
+ start = min2;
2129
+ end = max2;
2130
+ } else if (rangePart.includes("-")) {
2131
+ const [a, b] = rangePart.split("-");
2132
+ start = toNumber(a);
2133
+ end = toNumber(b);
2134
+ } else {
2135
+ start = toNumber(rangePart);
2136
+ end = stepPart === void 0 ? start : max2;
2137
+ }
2138
+ if (start < min2 || end > max2 || start > end) {
2139
+ throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
2140
+ }
2141
+ for (let v = start; v <= end; v += step) values.add(v);
2142
+ }
2143
+ return values;
2144
+ }
2145
+ function parseCron(expression) {
2146
+ const fields = expression.trim().split(/\s+/);
2147
+ if (fields.length !== 5) {
2148
+ throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
2149
+ }
2150
+ const [min2, hour, dom, mon, dow] = fields;
2151
+ const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
2152
+ if (daysOfWeek.has(7)) daysOfWeek.add(0);
2153
+ daysOfWeek.delete(7);
2154
+ return {
2155
+ minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
2156
+ hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
2157
+ daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
2158
+ months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
2159
+ daysOfWeek,
2160
+ dayFieldsBothRestricted: dom !== "*" && dow !== "*"
2161
+ };
2162
+ }
2163
+ function dayMatches(schedule, date) {
2164
+ const domOk = schedule.daysOfMonth.has(date.getUTCDate());
2165
+ const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
2166
+ return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
2167
+ }
2168
+ function nextCronRun(expression, after) {
2169
+ const schedule = typeof expression === "string" ? parseCron(expression) : expression;
2170
+ const cursor = new Date(after.getTime());
2171
+ cursor.setUTCSeconds(0, 0);
2172
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
2173
+ const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
2174
+ while (cursor.getTime() <= limit) {
2175
+ if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
2176
+ cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
2177
+ cursor.setUTCHours(0, 0, 0, 0);
2178
+ continue;
2179
+ }
2180
+ if (!dayMatches(schedule, cursor)) {
2181
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
2182
+ cursor.setUTCHours(0, 0, 0, 0);
2183
+ continue;
2184
+ }
2185
+ if (!schedule.hours.has(cursor.getUTCHours())) {
2186
+ cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
2187
+ continue;
2188
+ }
2189
+ if (!schedule.minutes.has(cursor.getUTCMinutes())) {
2190
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
2191
+ continue;
2192
+ }
2193
+ return cursor;
2194
+ }
2195
+ throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
2196
+ }
2197
+ function defineTask(task) {
2198
+ if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
2199
+ if (task.cron !== void 0) parseCron(task.cron);
2200
+ if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
2201
+ throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
2202
+ }
2203
+ return task;
2204
+ }
2205
+ function resolveLogger(config, taskName) {
2206
+ const base = config.logger;
2207
+ if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
2208
+ const prefix = `[dyrected/tasks] ${taskName}`;
2209
+ return {
2210
+ info: (obj, msg) => console.info(prefix, msg ?? "", obj),
2211
+ warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
2212
+ error: (obj, msg) => console.error(prefix, msg ?? "", obj)
2213
+ };
2214
+ }
2215
+ function floorToMinute(date) {
2216
+ const d = new Date(date.getTime());
2217
+ d.setUTCSeconds(0, 0);
2218
+ return d;
2219
+ }
2220
+ function createTaskRunner(config) {
2221
+ const db = config.db;
2222
+ if (!db) throw new Error("createTaskRunner requires config.db.");
2223
+ const tasks = /* @__PURE__ */ new Map();
2224
+ for (const task of config.tasks ?? []) {
2225
+ if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
2226
+ tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
2227
+ }
2228
+ const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2229
+ let timer;
2230
+ let inFlight = Promise.resolve();
2231
+ let ticking = false;
2232
+ let ready;
2233
+ function ensureReady() {
2234
+ ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
2235
+ ready.catch(() => {
2236
+ ready = void 0;
2237
+ });
2238
+ return ready;
2239
+ }
2240
+ async function ensureLockRow(name, now2) {
2241
+ await ensureReady();
2242
+ const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
2243
+ if (existing) return existing;
2244
+ try {
2245
+ return await db.create({
2246
+ collection: TASK_LOCKS_COLLECTION,
2247
+ data: {
2248
+ id: name,
2249
+ name,
2250
+ lockedUntil: 0,
2251
+ lockedBy: null,
2252
+ lastScheduledFor: floorToMinute(now2).toISOString(),
2253
+ lastStartedAt: null,
2254
+ lastFinishedAt: null,
2255
+ lastStatus: null,
2256
+ lastError: null
2257
+ }
2258
+ });
2259
+ } catch (error) {
2260
+ const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
2261
+ if (created) return created;
2262
+ throw error;
2263
+ }
2264
+ }
2265
+ async function execute(task, row, scheduledFor) {
2266
+ const started = Date.now();
2267
+ const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
2268
+ const token = `${instanceId}:${started}`;
2269
+ const where = {
2270
+ id: { equals: task.name },
2271
+ lockedUntil: { less_than: started }
2272
+ };
2273
+ if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
2274
+ const claimed = await db.update({
2275
+ collection: TASK_LOCKS_COLLECTION,
2276
+ where,
2277
+ data: {
2278
+ lockedUntil: started + lockMs,
2279
+ lockedBy: token,
2280
+ lastStartedAt: new Date(started).toISOString(),
2281
+ ...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
2282
+ }
2283
+ });
2284
+ if (claimed.affectedRows !== 1) {
2285
+ return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
2286
+ }
2287
+ const logger = resolveLogger(config, task.name);
2288
+ const controller = new AbortController();
2289
+ let timeout;
2290
+ let status = "completed";
2291
+ let errorMessage;
2292
+ try {
2293
+ await Promise.race([
2294
+ Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
2295
+ new Promise((_, reject) => {
2296
+ timeout = setTimeout(() => {
2297
+ reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
2298
+ controller.abort();
2299
+ }, lockMs);
2300
+ })
2301
+ ]);
2302
+ } catch (error) {
2303
+ status = "failed";
2304
+ errorMessage = error instanceof Error ? error.message : String(error);
2305
+ logger.error({ err: error }, "Task failed");
2306
+ } finally {
2307
+ if (timeout) clearTimeout(timeout);
2308
+ }
2309
+ try {
2310
+ await db.update({
2311
+ collection: TASK_LOCKS_COLLECTION,
2312
+ where: { id: { equals: task.name }, lockedBy: { equals: token } },
2313
+ data: {
2314
+ lockedUntil: 0,
2315
+ lockedBy: null,
2316
+ lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2317
+ lastStatus: status,
2318
+ lastError: errorMessage ?? null
2319
+ }
2320
+ });
2321
+ } catch (error) {
2322
+ logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
2323
+ }
2324
+ return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
2325
+ }
2326
+ function track(work) {
2327
+ inFlight = inFlight.then(() => work).catch(() => void 0);
2328
+ return work;
2329
+ }
2330
+ const runner = {
2331
+ async runTask(name) {
2332
+ const task = tasks.get(name);
2333
+ if (!task) throw new Error(`Unknown task "${name}".`);
2334
+ const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
2335
+ return track(execute(task, row, void 0));
2336
+ },
2337
+ async runDue(now2 = /* @__PURE__ */ new Date()) {
2338
+ const results = [];
2339
+ for (const task of tasks.values()) {
2340
+ if (!task.schedule) continue;
2341
+ const row = await ensureLockRow(task.name, now2);
2342
+ const reference = new Date(String(row.lastScheduledFor));
2343
+ const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
2344
+ if (!due) {
2345
+ results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
2346
+ continue;
2347
+ }
2348
+ results.push(await track(execute(task, row, floorToMinute(now2))));
2349
+ }
2350
+ return results;
2351
+ },
2352
+ start(options) {
2353
+ if (timer) return;
2354
+ const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
2355
+ const tick = () => {
2356
+ if (ticking) return;
2357
+ ticking = true;
2358
+ runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
2359
+ ticking = false;
2360
+ });
2361
+ };
2362
+ timer = setInterval(tick, intervalMs);
2363
+ timer.unref?.();
2364
+ setTimeout(tick, 0);
2365
+ },
2366
+ async stop() {
2367
+ if (timer) clearInterval(timer);
2368
+ timer = void 0;
2369
+ await inFlight;
2370
+ }
2371
+ };
2372
+ return runner;
2373
+ }
2374
+
2074
2375
  // src/workflows.ts
2075
2376
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
2076
2377
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -2286,7 +2587,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
2286
2587
  return due.length;
2287
2588
  }
2288
2589
  async function saveWorkflowDraft(args) {
2289
- const { config, collection, id, originalDoc, data, user } = args;
2590
+ const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
2290
2591
  const db = config.db;
2291
2592
  const workflow = collection.workflow;
2292
2593
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2320,13 +2621,14 @@ async function saveWorkflowDraft(args) {
2320
2621
  }
2321
2622
  const updated = await tx.update({ collection: collection.slug, id, data: updateData });
2322
2623
  await persistEvent(tx, event);
2624
+ await beforeCommit?.(tx, updated);
2323
2625
  return updated;
2324
2626
  });
2325
2627
  void dispatchLifecycleEvent(config, event);
2326
2628
  return { doc, event };
2327
2629
  }
2328
2630
  async function createWorkflowDocument(args) {
2329
- const { config, collection, data, user } = args;
2631
+ const { config, collection, data, user, beforeCommit } = args;
2330
2632
  const db = config.db;
2331
2633
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
2332
2634
  let event;
@@ -2340,13 +2642,14 @@ async function createWorkflowDocument(args) {
2340
2642
  payload: { revision: 1, previousRevision: null }
2341
2643
  });
2342
2644
  await persistEvent(tx, event);
2645
+ await beforeCommit?.(tx, created);
2343
2646
  return created;
2344
2647
  });
2345
2648
  void dispatchLifecycleEvent(config, event);
2346
2649
  return { doc, event };
2347
2650
  }
2348
2651
  async function transitionWorkflow(args) {
2349
- const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
2652
+ const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
2350
2653
  const db = config.db;
2351
2654
  const workflow = collection.workflow;
2352
2655
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2372,7 +2675,7 @@ async function transitionWorkflow(args) {
2372
2675
  if (transition.requireComment && !comment?.trim()) {
2373
2676
  throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
2374
2677
  }
2375
- const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
2678
+ const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
2376
2679
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
2377
2680
  const events = [
2378
2681
  createLifecycleEvent({
@@ -2426,6 +2729,19 @@ async function transitionWorkflow(args) {
2426
2729
  if (targetState.published) data.__published = working;
2427
2730
  if (transition.unpublish) data.__published = null;
2428
2731
  const next = await tx.update({ collection: collection.slug, id, data });
2732
+ if (transition.onTransition) {
2733
+ await transition.onTransition({
2734
+ transition,
2735
+ from: lockedMeta.state,
2736
+ to: transition.to,
2737
+ doc: locked,
2738
+ input,
2739
+ comment,
2740
+ user,
2741
+ req,
2742
+ tx
2743
+ });
2744
+ }
2429
2745
  await tx.create({
2430
2746
  collection: WORKFLOW_HISTORY_COLLECTION,
2431
2747
  data: {
@@ -3862,6 +4178,7 @@ function normalizeConfig(config) {
3862
4178
  const globals = schemaAwareConfig?.globals || [];
3863
4179
  const needsAudit = collections.some((col) => col.audit);
3864
4180
  const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
4181
+ const needsTasks = (config.tasks?.length ?? 0) > 0;
3865
4182
  const needsAuthSessions = collections.some((col) => !!col.auth);
3866
4183
  const adminAuthCollectionSlug = getAdminAuthCollection({
3867
4184
  collections,
@@ -4094,6 +4411,9 @@ function normalizeConfig(config) {
4094
4411
  if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
4095
4412
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
4096
4413
  }
4414
+ if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
4415
+ systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
4416
+ }
4097
4417
  if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
4098
4418
  systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
4099
4419
  }
@@ -4178,7 +4498,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
4178
4498
  params.push("");
4179
4499
  return `(${c})::text = ${next()}`;
4180
4500
  }
4181
- params.push(typeof value === "boolean" && isJsonExtract ? String(value) : value);
4501
+ if (typeof value === "boolean") {
4502
+ params.push(placeholder === "?" ? value ? 1 : 0 : isJsonExtract ? String(value) : value);
4503
+ } else {
4504
+ params.push(value);
4505
+ }
4182
4506
  return `${c} = ${next()}`;
4183
4507
  }
4184
4508
  const entries = Object.entries(value);
@@ -4378,6 +4702,38 @@ function parseMongoWhere(where) {
4378
4702
  }
4379
4703
  return buildClause(where);
4380
4704
  }
4705
+ function coerceBooleanWhere(where, fields) {
4706
+ const booleanFields = new Set(
4707
+ (fields ?? []).filter((f) => f.type === "boolean" && f.name).map((f) => f.name)
4708
+ );
4709
+ if (booleanFields.size === 0) return where;
4710
+ const toBool = (v) => v === "true" ? true : v === "false" ? false : v;
4711
+ const walk = (node) => {
4712
+ if (Array.isArray(node)) return node.map(walk);
4713
+ if (!node || typeof node !== "object") return node;
4714
+ const out = {};
4715
+ for (const [key, value] of Object.entries(node)) {
4716
+ if (key.toUpperCase() === "AND" || key.toUpperCase() === "OR") {
4717
+ out[key] = walk(value);
4718
+ } else if (booleanFields.has(key)) {
4719
+ if (value && typeof value === "object" && !Array.isArray(value)) {
4720
+ out[key] = Object.fromEntries(
4721
+ Object.entries(value).map(([op, operand]) => [
4722
+ op,
4723
+ Array.isArray(operand) ? operand.map(toBool) : toBool(operand)
4724
+ ])
4725
+ );
4726
+ } else {
4727
+ out[key] = toBool(value);
4728
+ }
4729
+ } else {
4730
+ out[key] = value;
4731
+ }
4732
+ }
4733
+ return out;
4734
+ };
4735
+ return walk(where);
4736
+ }
4381
4737
 
4382
4738
  // src/utils/parse-sort.ts
4383
4739
  var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
@@ -4659,7 +5015,12 @@ function generateOpenApi(config) {
4659
5015
  type: "object",
4660
5016
  properties: {
4661
5017
  expectedRevision: { type: "integer", minimum: 1 },
4662
- comment: { type: "string" }
5018
+ comment: { type: "string" },
5019
+ input: {
5020
+ type: "object",
5021
+ additionalProperties: true,
5022
+ description: "Extra data passed to the transition's onTransition handler."
5023
+ }
4663
5024
  }
4664
5025
  },
4665
5026
  WorkflowHistoryEntry: {
@@ -5822,6 +6183,9 @@ function fieldToSchema(field) {
5822
6183
  case "number":
5823
6184
  schema = { type: "number" };
5824
6185
  break;
6186
+ case "money":
6187
+ schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
6188
+ break;
5825
6189
  case "boolean":
5826
6190
  schema = { type: "boolean" };
5827
6191
  break;
@@ -6774,6 +7138,36 @@ function sanitizeDocForAI(options) {
6774
7138
  return result;
6775
7139
  }
6776
7140
 
7141
+ // src/utils/index-name.ts
7142
+ var import_node_crypto2 = require("crypto");
7143
+ var MAX_INDEX_NAME_LENGTH = 60;
7144
+ function resolveIndexName(prefix, table, fields) {
7145
+ const full = `${prefix}_${table}_${fields.join("_")}`;
7146
+ if (full.length <= MAX_INDEX_NAME_LENGTH) return full;
7147
+ const hash = (0, import_node_crypto2.createHash)("sha1").update(full).digest("hex").slice(0, 10);
7148
+ const tail = (fields[fields.length - 1] ?? "").slice(0, 30);
7149
+ return `${prefix}_${hash}_${tail}`;
7150
+ }
7151
+
7152
+ // src/utils/group-key.ts
7153
+ var UNASSIGNED_GROUP_KEY = "__unassigned__";
7154
+ var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
7155
+ function normalizeGroupKey(raw, fieldType) {
7156
+ if (raw === null || raw === void 0) return UNASSIGNED_GROUP_KEY;
7157
+ if (fieldType === "boolean") {
7158
+ if (raw === true || raw === 1 || raw === "1" || raw === "true" || raw === "t") return "true";
7159
+ if (raw === false || raw === 0 || raw === "0" || raw === "false" || raw === "f") return "false";
7160
+ }
7161
+ if ((fieldType === "number" || fieldType === "money") && raw !== "" && !Number.isNaN(Number(raw))) {
7162
+ return String(Number(raw));
7163
+ }
7164
+ if (fieldType === "date" || fieldType === "datetime") {
7165
+ if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? String(raw) : raw.toISOString();
7166
+ if (typeof raw === "string" && MYSQL_DATETIME.test(raw)) return `${raw.replace(" ", "T")}Z`;
7167
+ }
7168
+ return String(raw);
7169
+ }
7170
+
6777
7171
  // src/utils/navigation-reconciler.ts
6778
7172
  var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
6779
7173
  function reconcileNavigation(baseTree, prefs, schemas, options) {
@@ -7317,6 +7711,7 @@ var defineTextField = createFieldDefiner("text");
7317
7711
  var defineTextareaField = createFieldDefiner("textarea");
7318
7712
  var defineRichTextField = createFieldDefiner("richText");
7319
7713
  var defineNumberField = createFieldDefiner("number");
7714
+ var defineMoneyField = createFieldDefiner("money");
7320
7715
  var defineBooleanField = createFieldDefiner("boolean");
7321
7716
  var defineDateField = createFieldDefiner("date");
7322
7717
  var defineDateTimeField = createFieldDefiner("datetime");
@@ -7377,6 +7772,9 @@ function defineTab(args) {
7377
7772
  NotFoundError,
7378
7773
  PII_REGEX_PATTERNS,
7379
7774
  RESERVED_NAVIGATION_SLUGS,
7775
+ TASK_LOCKS_COLLECTION,
7776
+ TASK_LOCKS_COLLECTION_CONFIG,
7777
+ UNASSIGNED_GROUP_KEY,
7380
7778
  ValidationError,
7381
7779
  WORKFLOW_HISTORY_COLLECTION,
7382
7780
  abs,
@@ -7396,6 +7794,7 @@ function defineTab(args) {
7396
7794
  ceil,
7397
7795
  clamp,
7398
7796
  coalesce,
7797
+ coerceBooleanWhere,
7399
7798
  collectAdminConditionValidationIssues,
7400
7799
  collectConfigDiagnostics,
7401
7800
  collectDeclarativeAccessValidationIssues,
@@ -7405,6 +7804,7 @@ function defineTab(args) {
7405
7804
  compileNavigation,
7406
7805
  count,
7407
7806
  createLifecycleEvent,
7807
+ createTaskRunner,
7408
7808
  createWorkflowDocument,
7409
7809
  defaultValue,
7410
7810
  defineAction,
@@ -7427,6 +7827,7 @@ function defineTab(args) {
7427
7827
  defineJoinField,
7428
7828
  defineJsonField,
7429
7829
  defineKanbanView,
7830
+ defineMoneyField,
7430
7831
  defineMultiSelectField,
7431
7832
  defineNumberField,
7432
7833
  defineObjectField,
@@ -7438,6 +7839,7 @@ function defineTab(args) {
7438
7839
  defineSelectField,
7439
7840
  defineTab,
7440
7841
  defineTableView,
7842
+ defineTask,
7441
7843
  defineTextField,
7442
7844
  defineTextareaField,
7443
7845
  defineTimeField,
@@ -7501,10 +7903,13 @@ function defineTab(args) {
7501
7903
  max,
7502
7904
  min,
7503
7905
  nanoid,
7906
+ nextCronRun,
7504
7907
  normalizeConfig,
7505
7908
  normalizeDetailItem,
7909
+ normalizeGroupKey,
7506
7910
  now,
7507
7911
  numericFieldNames,
7912
+ parseCron,
7508
7913
  parseMongoWhere,
7509
7914
  parseSort,
7510
7915
  parseSqlWhere,
@@ -7519,6 +7924,7 @@ function defineTab(args) {
7519
7924
  replace,
7520
7925
  resolveAdminAuthCollection,
7521
7926
  resolveAuthorizedSiteId,
7927
+ resolveIndexName,
7522
7928
  resolvePrefix,
7523
7929
  round,
7524
7930
  runCollectionHooks,