@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.js CHANGED
@@ -1866,6 +1866,296 @@ import { SignJWT, jwtVerify, decodeJwt } from "jose";
1866
1866
  // src/auth/sessions.ts
1867
1867
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
1868
1868
 
1869
+ // src/tasks.ts
1870
+ var TASK_LOCKS_COLLECTION = "__task_locks";
1871
+ var TASK_LOCKS_COLLECTION_CONFIG = {
1872
+ slug: TASK_LOCKS_COLLECTION,
1873
+ labels: { singular: "Task lock", plural: "Task locks" },
1874
+ fields: [
1875
+ { name: "name", type: "text", required: true },
1876
+ // Numeric and promoted so the lock compare-and-set is a real column comparison.
1877
+ { name: "lockedUntil", type: "number", required: true, promoted: true },
1878
+ { name: "lockedBy", type: "text" },
1879
+ { name: "lastScheduledFor", type: "date" },
1880
+ { name: "lastStartedAt", type: "date" },
1881
+ { name: "lastFinishedAt", type: "date" },
1882
+ { name: "lastStatus", type: "text" },
1883
+ { name: "lastError", type: "textarea" }
1884
+ ],
1885
+ access: {
1886
+ read: ({ user }) => !!user?.roles?.includes("admin"),
1887
+ create: () => false,
1888
+ update: () => false,
1889
+ delete: () => false
1890
+ },
1891
+ admin: { hidden: true }
1892
+ };
1893
+ var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
1894
+ var DEFAULT_TICK_MS = 3e4;
1895
+ var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
1896
+ var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
1897
+ function parseCronField(raw, min2, max2, names, nameOffset, label) {
1898
+ const values = /* @__PURE__ */ new Set();
1899
+ const toNumber = (token) => {
1900
+ const named = names?.indexOf(token.toLowerCase());
1901
+ if (named !== void 0 && named >= 0) return named + nameOffset;
1902
+ if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
1903
+ return Number(token);
1904
+ };
1905
+ for (const part of raw.split(",")) {
1906
+ const [rangePart, stepPart] = part.split("/");
1907
+ const step = stepPart === void 0 ? 1 : Number(stepPart);
1908
+ if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
1909
+ let start;
1910
+ let end;
1911
+ if (rangePart === "*") {
1912
+ start = min2;
1913
+ end = max2;
1914
+ } else if (rangePart.includes("-")) {
1915
+ const [a, b] = rangePart.split("-");
1916
+ start = toNumber(a);
1917
+ end = toNumber(b);
1918
+ } else {
1919
+ start = toNumber(rangePart);
1920
+ end = stepPart === void 0 ? start : max2;
1921
+ }
1922
+ if (start < min2 || end > max2 || start > end) {
1923
+ throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
1924
+ }
1925
+ for (let v = start; v <= end; v += step) values.add(v);
1926
+ }
1927
+ return values;
1928
+ }
1929
+ function parseCron(expression) {
1930
+ const fields = expression.trim().split(/\s+/);
1931
+ if (fields.length !== 5) {
1932
+ throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
1933
+ }
1934
+ const [min2, hour, dom, mon, dow] = fields;
1935
+ const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
1936
+ if (daysOfWeek.has(7)) daysOfWeek.add(0);
1937
+ daysOfWeek.delete(7);
1938
+ return {
1939
+ minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
1940
+ hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
1941
+ daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
1942
+ months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
1943
+ daysOfWeek,
1944
+ dayFieldsBothRestricted: dom !== "*" && dow !== "*"
1945
+ };
1946
+ }
1947
+ function dayMatches(schedule, date) {
1948
+ const domOk = schedule.daysOfMonth.has(date.getUTCDate());
1949
+ const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
1950
+ return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
1951
+ }
1952
+ function nextCronRun(expression, after) {
1953
+ const schedule = typeof expression === "string" ? parseCron(expression) : expression;
1954
+ const cursor = new Date(after.getTime());
1955
+ cursor.setUTCSeconds(0, 0);
1956
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
1957
+ const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
1958
+ while (cursor.getTime() <= limit) {
1959
+ if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
1960
+ cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
1961
+ cursor.setUTCHours(0, 0, 0, 0);
1962
+ continue;
1963
+ }
1964
+ if (!dayMatches(schedule, cursor)) {
1965
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
1966
+ cursor.setUTCHours(0, 0, 0, 0);
1967
+ continue;
1968
+ }
1969
+ if (!schedule.hours.has(cursor.getUTCHours())) {
1970
+ cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
1971
+ continue;
1972
+ }
1973
+ if (!schedule.minutes.has(cursor.getUTCMinutes())) {
1974
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
1975
+ continue;
1976
+ }
1977
+ return cursor;
1978
+ }
1979
+ throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
1980
+ }
1981
+ function defineTask(task) {
1982
+ if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
1983
+ if (task.cron !== void 0) parseCron(task.cron);
1984
+ if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
1985
+ throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
1986
+ }
1987
+ return task;
1988
+ }
1989
+ function resolveLogger(config, taskName) {
1990
+ const base = config.logger;
1991
+ if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
1992
+ const prefix = `[dyrected/tasks] ${taskName}`;
1993
+ return {
1994
+ info: (obj, msg) => console.info(prefix, msg ?? "", obj),
1995
+ warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
1996
+ error: (obj, msg) => console.error(prefix, msg ?? "", obj)
1997
+ };
1998
+ }
1999
+ function floorToMinute(date) {
2000
+ const d = new Date(date.getTime());
2001
+ d.setUTCSeconds(0, 0);
2002
+ return d;
2003
+ }
2004
+ function createTaskRunner(config) {
2005
+ const db = config.db;
2006
+ if (!db) throw new Error("createTaskRunner requires config.db.");
2007
+ const tasks = /* @__PURE__ */ new Map();
2008
+ for (const task of config.tasks ?? []) {
2009
+ if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
2010
+ tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
2011
+ }
2012
+ const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2013
+ let timer;
2014
+ let inFlight = Promise.resolve();
2015
+ let ticking = false;
2016
+ let ready;
2017
+ function ensureReady() {
2018
+ ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
2019
+ ready.catch(() => {
2020
+ ready = void 0;
2021
+ });
2022
+ return ready;
2023
+ }
2024
+ async function ensureLockRow(name, now2) {
2025
+ await ensureReady();
2026
+ const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
2027
+ if (existing) return existing;
2028
+ try {
2029
+ return await db.create({
2030
+ collection: TASK_LOCKS_COLLECTION,
2031
+ data: {
2032
+ id: name,
2033
+ name,
2034
+ lockedUntil: 0,
2035
+ lockedBy: null,
2036
+ lastScheduledFor: floorToMinute(now2).toISOString(),
2037
+ lastStartedAt: null,
2038
+ lastFinishedAt: null,
2039
+ lastStatus: null,
2040
+ lastError: null
2041
+ }
2042
+ });
2043
+ } catch (error) {
2044
+ const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
2045
+ if (created) return created;
2046
+ throw error;
2047
+ }
2048
+ }
2049
+ async function execute(task, row, scheduledFor) {
2050
+ const started = Date.now();
2051
+ const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
2052
+ const token = `${instanceId}:${started}`;
2053
+ const where = {
2054
+ id: { equals: task.name },
2055
+ lockedUntil: { less_than: started }
2056
+ };
2057
+ if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
2058
+ const claimed = await db.update({
2059
+ collection: TASK_LOCKS_COLLECTION,
2060
+ where,
2061
+ data: {
2062
+ lockedUntil: started + lockMs,
2063
+ lockedBy: token,
2064
+ lastStartedAt: new Date(started).toISOString(),
2065
+ ...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
2066
+ }
2067
+ });
2068
+ if (claimed.affectedRows !== 1) {
2069
+ return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
2070
+ }
2071
+ const logger = resolveLogger(config, task.name);
2072
+ const controller = new AbortController();
2073
+ let timeout;
2074
+ let status = "completed";
2075
+ let errorMessage;
2076
+ try {
2077
+ await Promise.race([
2078
+ Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
2079
+ new Promise((_, reject) => {
2080
+ timeout = setTimeout(() => {
2081
+ reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
2082
+ controller.abort();
2083
+ }, lockMs);
2084
+ })
2085
+ ]);
2086
+ } catch (error) {
2087
+ status = "failed";
2088
+ errorMessage = error instanceof Error ? error.message : String(error);
2089
+ logger.error({ err: error }, "Task failed");
2090
+ } finally {
2091
+ if (timeout) clearTimeout(timeout);
2092
+ }
2093
+ try {
2094
+ await db.update({
2095
+ collection: TASK_LOCKS_COLLECTION,
2096
+ where: { id: { equals: task.name }, lockedBy: { equals: token } },
2097
+ data: {
2098
+ lockedUntil: 0,
2099
+ lockedBy: null,
2100
+ lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
2101
+ lastStatus: status,
2102
+ lastError: errorMessage ?? null
2103
+ }
2104
+ });
2105
+ } catch (error) {
2106
+ logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
2107
+ }
2108
+ return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
2109
+ }
2110
+ function track(work) {
2111
+ inFlight = inFlight.then(() => work).catch(() => void 0);
2112
+ return work;
2113
+ }
2114
+ const runner = {
2115
+ async runTask(name) {
2116
+ const task = tasks.get(name);
2117
+ if (!task) throw new Error(`Unknown task "${name}".`);
2118
+ const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
2119
+ return track(execute(task, row, void 0));
2120
+ },
2121
+ async runDue(now2 = /* @__PURE__ */ new Date()) {
2122
+ const results = [];
2123
+ for (const task of tasks.values()) {
2124
+ if (!task.schedule) continue;
2125
+ const row = await ensureLockRow(task.name, now2);
2126
+ const reference = new Date(String(row.lastScheduledFor));
2127
+ const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
2128
+ if (!due) {
2129
+ results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
2130
+ continue;
2131
+ }
2132
+ results.push(await track(execute(task, row, floorToMinute(now2))));
2133
+ }
2134
+ return results;
2135
+ },
2136
+ start(options) {
2137
+ if (timer) return;
2138
+ const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
2139
+ const tick = () => {
2140
+ if (ticking) return;
2141
+ ticking = true;
2142
+ runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
2143
+ ticking = false;
2144
+ });
2145
+ };
2146
+ timer = setInterval(tick, intervalMs);
2147
+ timer.unref?.();
2148
+ setTimeout(tick, 0);
2149
+ },
2150
+ async stop() {
2151
+ if (timer) clearInterval(timer);
2152
+ timer = void 0;
2153
+ await inFlight;
2154
+ }
2155
+ };
2156
+ return runner;
2157
+ }
2158
+
1869
2159
  // src/workflows.ts
1870
2160
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
1871
2161
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -2081,7 +2371,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
2081
2371
  return due.length;
2082
2372
  }
2083
2373
  async function saveWorkflowDraft(args) {
2084
- const { config, collection, id, originalDoc, data, user } = args;
2374
+ const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
2085
2375
  const db = config.db;
2086
2376
  const workflow = collection.workflow;
2087
2377
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2115,13 +2405,14 @@ async function saveWorkflowDraft(args) {
2115
2405
  }
2116
2406
  const updated = await tx.update({ collection: collection.slug, id, data: updateData });
2117
2407
  await persistEvent(tx, event);
2408
+ await beforeCommit?.(tx, updated);
2118
2409
  return updated;
2119
2410
  });
2120
2411
  void dispatchLifecycleEvent(config, event);
2121
2412
  return { doc, event };
2122
2413
  }
2123
2414
  async function createWorkflowDocument(args) {
2124
- const { config, collection, data, user } = args;
2415
+ const { config, collection, data, user, beforeCommit } = args;
2125
2416
  const db = config.db;
2126
2417
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
2127
2418
  let event;
@@ -2135,13 +2426,14 @@ async function createWorkflowDocument(args) {
2135
2426
  payload: { revision: 1, previousRevision: null }
2136
2427
  });
2137
2428
  await persistEvent(tx, event);
2429
+ await beforeCommit?.(tx, created);
2138
2430
  return created;
2139
2431
  });
2140
2432
  void dispatchLifecycleEvent(config, event);
2141
2433
  return { doc, event };
2142
2434
  }
2143
2435
  async function transitionWorkflow(args) {
2144
- const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
2436
+ const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
2145
2437
  const db = config.db;
2146
2438
  const workflow = collection.workflow;
2147
2439
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2167,7 +2459,7 @@ async function transitionWorkflow(args) {
2167
2459
  if (transition.requireComment && !comment?.trim()) {
2168
2460
  throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
2169
2461
  }
2170
- const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
2462
+ const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
2171
2463
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
2172
2464
  const events = [
2173
2465
  createLifecycleEvent({
@@ -2221,6 +2513,19 @@ async function transitionWorkflow(args) {
2221
2513
  if (targetState.published) data.__published = working;
2222
2514
  if (transition.unpublish) data.__published = null;
2223
2515
  const next = await tx.update({ collection: collection.slug, id, data });
2516
+ if (transition.onTransition) {
2517
+ await transition.onTransition({
2518
+ transition,
2519
+ from: lockedMeta.state,
2520
+ to: transition.to,
2521
+ doc: locked,
2522
+ input,
2523
+ comment,
2524
+ user,
2525
+ req,
2526
+ tx
2527
+ });
2528
+ }
2224
2529
  await tx.create({
2225
2530
  collection: WORKFLOW_HISTORY_COLLECTION,
2226
2531
  data: {
@@ -3657,6 +3962,7 @@ function normalizeConfig(config) {
3657
3962
  const globals = schemaAwareConfig?.globals || [];
3658
3963
  const needsAudit = collections.some((col) => col.audit);
3659
3964
  const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
3965
+ const needsTasks = (config.tasks?.length ?? 0) > 0;
3660
3966
  const needsAuthSessions = collections.some((col) => !!col.auth);
3661
3967
  const adminAuthCollectionSlug = getAdminAuthCollection({
3662
3968
  collections,
@@ -3889,6 +4195,9 @@ function normalizeConfig(config) {
3889
4195
  if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
3890
4196
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
3891
4197
  }
4198
+ if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
4199
+ systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
4200
+ }
3892
4201
  if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
3893
4202
  systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
3894
4203
  }
@@ -3973,7 +4282,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
3973
4282
  params.push("");
3974
4283
  return `(${c})::text = ${next()}`;
3975
4284
  }
3976
- params.push(typeof value === "boolean" && isJsonExtract ? String(value) : value);
4285
+ if (typeof value === "boolean") {
4286
+ params.push(placeholder === "?" ? value ? 1 : 0 : isJsonExtract ? String(value) : value);
4287
+ } else {
4288
+ params.push(value);
4289
+ }
3977
4290
  return `${c} = ${next()}`;
3978
4291
  }
3979
4292
  const entries = Object.entries(value);
@@ -4173,6 +4486,38 @@ function parseMongoWhere(where) {
4173
4486
  }
4174
4487
  return buildClause(where);
4175
4488
  }
4489
+ function coerceBooleanWhere(where, fields) {
4490
+ const booleanFields = new Set(
4491
+ (fields ?? []).filter((f) => f.type === "boolean" && f.name).map((f) => f.name)
4492
+ );
4493
+ if (booleanFields.size === 0) return where;
4494
+ const toBool = (v) => v === "true" ? true : v === "false" ? false : v;
4495
+ const walk = (node) => {
4496
+ if (Array.isArray(node)) return node.map(walk);
4497
+ if (!node || typeof node !== "object") return node;
4498
+ const out = {};
4499
+ for (const [key, value] of Object.entries(node)) {
4500
+ if (key.toUpperCase() === "AND" || key.toUpperCase() === "OR") {
4501
+ out[key] = walk(value);
4502
+ } else if (booleanFields.has(key)) {
4503
+ if (value && typeof value === "object" && !Array.isArray(value)) {
4504
+ out[key] = Object.fromEntries(
4505
+ Object.entries(value).map(([op, operand]) => [
4506
+ op,
4507
+ Array.isArray(operand) ? operand.map(toBool) : toBool(operand)
4508
+ ])
4509
+ );
4510
+ } else {
4511
+ out[key] = toBool(value);
4512
+ }
4513
+ } else {
4514
+ out[key] = value;
4515
+ }
4516
+ }
4517
+ return out;
4518
+ };
4519
+ return walk(where);
4520
+ }
4176
4521
 
4177
4522
  // src/utils/parse-sort.ts
4178
4523
  var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
@@ -4454,7 +4799,12 @@ function generateOpenApi(config) {
4454
4799
  type: "object",
4455
4800
  properties: {
4456
4801
  expectedRevision: { type: "integer", minimum: 1 },
4457
- comment: { type: "string" }
4802
+ comment: { type: "string" },
4803
+ input: {
4804
+ type: "object",
4805
+ additionalProperties: true,
4806
+ description: "Extra data passed to the transition's onTransition handler."
4807
+ }
4458
4808
  }
4459
4809
  },
4460
4810
  WorkflowHistoryEntry: {
@@ -5617,6 +5967,9 @@ function fieldToSchema(field) {
5617
5967
  case "number":
5618
5968
  schema = { type: "number" };
5619
5969
  break;
5970
+ case "money":
5971
+ schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
5972
+ break;
5620
5973
  case "boolean":
5621
5974
  schema = { type: "boolean" };
5622
5975
  break;
@@ -6569,6 +6922,36 @@ function sanitizeDocForAI(options) {
6569
6922
  return result;
6570
6923
  }
6571
6924
 
6925
+ // src/utils/index-name.ts
6926
+ import { createHash } from "crypto";
6927
+ var MAX_INDEX_NAME_LENGTH = 60;
6928
+ function resolveIndexName(prefix, table, fields) {
6929
+ const full = `${prefix}_${table}_${fields.join("_")}`;
6930
+ if (full.length <= MAX_INDEX_NAME_LENGTH) return full;
6931
+ const hash = createHash("sha1").update(full).digest("hex").slice(0, 10);
6932
+ const tail = (fields[fields.length - 1] ?? "").slice(0, 30);
6933
+ return `${prefix}_${hash}_${tail}`;
6934
+ }
6935
+
6936
+ // src/utils/group-key.ts
6937
+ var UNASSIGNED_GROUP_KEY = "__unassigned__";
6938
+ var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
6939
+ function normalizeGroupKey(raw, fieldType) {
6940
+ if (raw === null || raw === void 0) return UNASSIGNED_GROUP_KEY;
6941
+ if (fieldType === "boolean") {
6942
+ if (raw === true || raw === 1 || raw === "1" || raw === "true" || raw === "t") return "true";
6943
+ if (raw === false || raw === 0 || raw === "0" || raw === "false" || raw === "f") return "false";
6944
+ }
6945
+ if ((fieldType === "number" || fieldType === "money") && raw !== "" && !Number.isNaN(Number(raw))) {
6946
+ return String(Number(raw));
6947
+ }
6948
+ if (fieldType === "date" || fieldType === "datetime") {
6949
+ if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? String(raw) : raw.toISOString();
6950
+ if (typeof raw === "string" && MYSQL_DATETIME.test(raw)) return `${raw.replace(" ", "T")}Z`;
6951
+ }
6952
+ return String(raw);
6953
+ }
6954
+
6572
6955
  // src/utils/navigation-reconciler.ts
6573
6956
  var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
6574
6957
  function reconcileNavigation(baseTree, prefs, schemas, options) {
@@ -7112,6 +7495,7 @@ var defineTextField = createFieldDefiner("text");
7112
7495
  var defineTextareaField = createFieldDefiner("textarea");
7113
7496
  var defineRichTextField = createFieldDefiner("richText");
7114
7497
  var defineNumberField = createFieldDefiner("number");
7498
+ var defineMoneyField = createFieldDefiner("money");
7115
7499
  var defineBooleanField = createFieldDefiner("boolean");
7116
7500
  var defineDateField = createFieldDefiner("date");
7117
7501
  var defineDateTimeField = createFieldDefiner("datetime");
@@ -7171,6 +7555,9 @@ export {
7171
7555
  NotFoundError,
7172
7556
  PII_REGEX_PATTERNS,
7173
7557
  RESERVED_NAVIGATION_SLUGS,
7558
+ TASK_LOCKS_COLLECTION,
7559
+ TASK_LOCKS_COLLECTION_CONFIG,
7560
+ UNASSIGNED_GROUP_KEY,
7174
7561
  ValidationError,
7175
7562
  WORKFLOW_HISTORY_COLLECTION,
7176
7563
  abs,
@@ -7190,6 +7577,7 @@ export {
7190
7577
  ceil,
7191
7578
  clamp,
7192
7579
  coalesce,
7580
+ coerceBooleanWhere,
7193
7581
  collectAdminConditionValidationIssues,
7194
7582
  collectConfigDiagnostics,
7195
7583
  collectDeclarativeAccessValidationIssues,
@@ -7199,6 +7587,7 @@ export {
7199
7587
  compileNavigation,
7200
7588
  count,
7201
7589
  createLifecycleEvent,
7590
+ createTaskRunner,
7202
7591
  createWorkflowDocument,
7203
7592
  defaultValue,
7204
7593
  defineAction,
@@ -7221,6 +7610,7 @@ export {
7221
7610
  defineJoinField,
7222
7611
  defineJsonField,
7223
7612
  defineKanbanView,
7613
+ defineMoneyField,
7224
7614
  defineMultiSelectField,
7225
7615
  defineNumberField,
7226
7616
  defineObjectField,
@@ -7232,6 +7622,7 @@ export {
7232
7622
  defineSelectField,
7233
7623
  defineTab,
7234
7624
  defineTableView,
7625
+ defineTask,
7235
7626
  defineTextField,
7236
7627
  defineTextareaField,
7237
7628
  defineTimeField,
@@ -7295,10 +7686,13 @@ export {
7295
7686
  max,
7296
7687
  min,
7297
7688
  nanoid,
7689
+ nextCronRun,
7298
7690
  normalizeConfig,
7299
7691
  normalizeDetailItem,
7692
+ normalizeGroupKey,
7300
7693
  now,
7301
7694
  numericFieldNames,
7695
+ parseCron,
7302
7696
  parseMongoWhere,
7303
7697
  parseSort,
7304
7698
  parseSqlWhere,
@@ -7313,6 +7707,7 @@ export {
7313
7707
  replace,
7314
7708
  resolveAdminAuthCollection,
7315
7709
  resolveAuthorizedSiteId,
7710
+ resolveIndexName,
7316
7711
  resolvePrefix,
7317
7712
  round,
7318
7713
  runCollectionHooks,