@dyrected/core 2.17.0 → 2.18.1

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) {
@@ -7087,6 +7470,28 @@ function pruneNavigationForUser(tree, user) {
7087
7470
  ungrouped: tree.ungrouped.filter(isAllowed)
7088
7471
  };
7089
7472
  }
7473
+ function resolveAllCollectionViews(collection, config) {
7474
+ const views = [...collection.views ?? []];
7475
+ const seenSlugs = new Set(views.map((v) => v.slug));
7476
+ const explicitNav = config?.admin?.navigation;
7477
+ if (Array.isArray(explicitNav)) {
7478
+ for (const item of explicitNav) {
7479
+ if (!item) continue;
7480
+ const itemCol = item.collection || item.addToCollection;
7481
+ if (Array.isArray(item.views)) {
7482
+ for (const view of item.views) {
7483
+ if (!view || !view.slug) continue;
7484
+ const targetCol = view.collection || itemCol;
7485
+ if (targetCol === collection.slug && !seenSlugs.has(view.slug)) {
7486
+ views.push(view);
7487
+ seenSlugs.add(view.slug);
7488
+ }
7489
+ }
7490
+ }
7491
+ }
7492
+ }
7493
+ return views;
7494
+ }
7090
7495
 
7091
7496
  // src/index.ts
7092
7497
  import { z } from "zod";
@@ -7112,6 +7517,7 @@ var defineTextField = createFieldDefiner("text");
7112
7517
  var defineTextareaField = createFieldDefiner("textarea");
7113
7518
  var defineRichTextField = createFieldDefiner("richText");
7114
7519
  var defineNumberField = createFieldDefiner("number");
7520
+ var defineMoneyField = createFieldDefiner("money");
7115
7521
  var defineBooleanField = createFieldDefiner("boolean");
7116
7522
  var defineDateField = createFieldDefiner("date");
7117
7523
  var defineDateTimeField = createFieldDefiner("datetime");
@@ -7171,6 +7577,9 @@ export {
7171
7577
  NotFoundError,
7172
7578
  PII_REGEX_PATTERNS,
7173
7579
  RESERVED_NAVIGATION_SLUGS,
7580
+ TASK_LOCKS_COLLECTION,
7581
+ TASK_LOCKS_COLLECTION_CONFIG,
7582
+ UNASSIGNED_GROUP_KEY,
7174
7583
  ValidationError,
7175
7584
  WORKFLOW_HISTORY_COLLECTION,
7176
7585
  abs,
@@ -7190,6 +7599,7 @@ export {
7190
7599
  ceil,
7191
7600
  clamp,
7192
7601
  coalesce,
7602
+ coerceBooleanWhere,
7193
7603
  collectAdminConditionValidationIssues,
7194
7604
  collectConfigDiagnostics,
7195
7605
  collectDeclarativeAccessValidationIssues,
@@ -7199,6 +7609,7 @@ export {
7199
7609
  compileNavigation,
7200
7610
  count,
7201
7611
  createLifecycleEvent,
7612
+ createTaskRunner,
7202
7613
  createWorkflowDocument,
7203
7614
  defaultValue,
7204
7615
  defineAction,
@@ -7221,6 +7632,7 @@ export {
7221
7632
  defineJoinField,
7222
7633
  defineJsonField,
7223
7634
  defineKanbanView,
7635
+ defineMoneyField,
7224
7636
  defineMultiSelectField,
7225
7637
  defineNumberField,
7226
7638
  defineObjectField,
@@ -7232,6 +7644,7 @@ export {
7232
7644
  defineSelectField,
7233
7645
  defineTab,
7234
7646
  defineTableView,
7647
+ defineTask,
7235
7648
  defineTextField,
7236
7649
  defineTextareaField,
7237
7650
  defineTimeField,
@@ -7295,10 +7708,13 @@ export {
7295
7708
  max,
7296
7709
  min,
7297
7710
  nanoid,
7711
+ nextCronRun,
7298
7712
  normalizeConfig,
7299
7713
  normalizeDetailItem,
7714
+ normalizeGroupKey,
7300
7715
  now,
7301
7716
  numericFieldNames,
7717
+ parseCron,
7302
7718
  parseMongoWhere,
7303
7719
  parseSort,
7304
7720
  parseSqlWhere,
@@ -7312,7 +7728,9 @@ export {
7312
7728
  registerJexlHelpers,
7313
7729
  replace,
7314
7730
  resolveAdminAuthCollection,
7731
+ resolveAllCollectionViews,
7315
7732
  resolveAuthorizedSiteId,
7733
+ resolveIndexName,
7316
7734
  resolvePrefix,
7317
7735
  round,
7318
7736
  runCollectionHooks,