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