@dyrected/core 2.16.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
@@ -1792,8 +1792,10 @@ __export(index_exports, {
1792
1792
  BASE62_ALPHABET: () => BASE62_ALPHABET,
1793
1793
  BUILTIN_JEXL_HELPERS: () => BUILTIN_JEXL_HELPERS,
1794
1794
  CROCKFORD_BASE32: () => CROCKFORD_BASE32,
1795
+ CURRENT_NAV_PREFERENCES_VERSION: () => CURRENT_NAV_PREFERENCES_VERSION,
1795
1796
  ConfigValidationError: () => ConfigValidationError,
1796
1797
  ConflictError: () => ConflictError,
1798
+ DEFAULT_USER_NAV_PREFERENCES: () => DEFAULT_USER_NAV_PREFERENCES,
1797
1799
  DuplicateKeyError: () => DuplicateKeyError,
1798
1800
  DyrectedAIError: () => DyrectedAIError,
1799
1801
  DyrectedError: () => DyrectedError,
@@ -1806,6 +1808,10 @@ __export(index_exports, {
1806
1808
  NON_NEGOTIABLE_CREDENTIAL_FIELDS: () => NON_NEGOTIABLE_CREDENTIAL_FIELDS,
1807
1809
  NotFoundError: () => NotFoundError,
1808
1810
  PII_REGEX_PATTERNS: () => PII_REGEX_PATTERNS,
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,
1809
1815
  ValidationError: () => ValidationError,
1810
1816
  WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
1811
1817
  abs: () => abs,
@@ -1816,6 +1822,7 @@ __export(index_exports, {
1816
1822
  assertValidDeclarativeHookExpression: () => assertValidDeclarativeHookExpression,
1817
1823
  assertValidDeclarativeHookResult: () => assertValidDeclarativeHookResult,
1818
1824
  assertValidDeclarativeHooksInConfig: () => assertValidDeclarativeHooksInConfig,
1825
+ assertValidNavigationSlugs: () => assertValidNavigationSlugs,
1819
1826
  assertValidPreviewUrlsInConfig: () => assertValidPreviewUrlsInConfig,
1820
1827
  availableWorkflowTransitions: () => availableWorkflowTransitions,
1821
1828
  buildSqlOrderBy: () => buildSqlOrderBy,
@@ -1824,14 +1831,17 @@ __export(index_exports, {
1824
1831
  ceil: () => ceil,
1825
1832
  clamp: () => clamp,
1826
1833
  coalesce: () => coalesce,
1834
+ coerceBooleanWhere: () => coerceBooleanWhere,
1827
1835
  collectAdminConditionValidationIssues: () => collectAdminConditionValidationIssues,
1828
1836
  collectConfigDiagnostics: () => collectConfigDiagnostics,
1829
1837
  collectDeclarativeAccessValidationIssues: () => collectDeclarativeAccessValidationIssues,
1830
1838
  collectDeclarativeHookValidationIssues: () => collectDeclarativeHookValidationIssues,
1831
1839
  collectPreviewUrlValidationIssues: () => collectPreviewUrlValidationIssues,
1832
1840
  compact: () => compact,
1841
+ compileNavigation: () => compileNavigation,
1833
1842
  count: () => count,
1834
1843
  createLifecycleEvent: () => createLifecycleEvent,
1844
+ createTaskRunner: () => createTaskRunner,
1835
1845
  createWorkflowDocument: () => createWorkflowDocument,
1836
1846
  defaultValue: () => defaultValue,
1837
1847
  defineAction: () => defineAction,
@@ -1854,6 +1864,7 @@ __export(index_exports, {
1854
1864
  defineJoinField: () => defineJoinField,
1855
1865
  defineJsonField: () => defineJsonField,
1856
1866
  defineKanbanView: () => defineKanbanView,
1867
+ defineMoneyField: () => defineMoneyField,
1857
1868
  defineMultiSelectField: () => defineMultiSelectField,
1858
1869
  defineNumberField: () => defineNumberField,
1859
1870
  defineObjectField: () => defineObjectField,
@@ -1865,11 +1876,13 @@ __export(index_exports, {
1865
1876
  defineSelectField: () => defineSelectField,
1866
1877
  defineTab: () => defineTab,
1867
1878
  defineTableView: () => defineTableView,
1879
+ defineTask: () => defineTask,
1868
1880
  defineTextField: () => defineTextField,
1869
1881
  defineTextareaField: () => defineTextareaField,
1870
1882
  defineTimeField: () => defineTimeField,
1871
1883
  defineUrlField: () => defineUrlField,
1872
1884
  defineView: () => defineView,
1885
+ defineWorkspace: () => defineWorkspace,
1873
1886
  diffDays: () => diffDays,
1874
1887
  dispatchLifecycleEvent: () => dispatchLifecycleEvent,
1875
1888
  dispatchPendingLifecycleEvents: () => dispatchPendingLifecycleEvents,
@@ -1927,22 +1940,28 @@ __export(index_exports, {
1927
1940
  max: () => max,
1928
1941
  min: () => min,
1929
1942
  nanoid: () => nanoid,
1943
+ nextCronRun: () => nextCronRun,
1930
1944
  normalizeConfig: () => normalizeConfig,
1931
1945
  normalizeDetailItem: () => normalizeDetailItem,
1946
+ normalizeGroupKey: () => normalizeGroupKey,
1932
1947
  now: () => now,
1933
1948
  numericFieldNames: () => numericFieldNames,
1949
+ parseCron: () => parseCron,
1934
1950
  parseMongoWhere: () => parseMongoWhere,
1935
1951
  parseSort: () => parseSort,
1936
1952
  parseSqlWhere: () => parseSqlWhere,
1937
1953
  pow: () => pow,
1938
1954
  prefixedId: () => prefixedId,
1955
+ pruneNavigationForUser: () => pruneNavigationForUser,
1939
1956
  publishedStateName: () => publishedStateName,
1940
1957
  publishingWorkflow: () => publishingWorkflow,
1941
1958
  readingTime: () => readingTime,
1959
+ reconcileNavigation: () => reconcileNavigation,
1942
1960
  registerJexlHelpers: () => registerJexlHelpers,
1943
1961
  replace: () => replace,
1944
1962
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
1945
1963
  resolveAuthorizedSiteId: () => resolveAuthorizedSiteId,
1964
+ resolveIndexName: () => resolveIndexName,
1946
1965
  resolvePrefix: () => resolvePrefix,
1947
1966
  round: () => round,
1948
1967
  runCollectionHooks: () => runCollectionHooks,
@@ -2042,12 +2061,317 @@ var DyrectedAIError = class _DyrectedAIError extends Error {
2042
2061
  }
2043
2062
  };
2044
2063
 
2064
+ // src/types/navigation.ts
2065
+ function defineWorkspace(options) {
2066
+ return options;
2067
+ }
2068
+ var CURRENT_NAV_PREFERENCES_VERSION = 1;
2069
+ var DEFAULT_USER_NAV_PREFERENCES = {
2070
+ _version: CURRENT_NAV_PREFERENCES_VERSION,
2071
+ pinned: [],
2072
+ hidden: [],
2073
+ groupOrder: [],
2074
+ itemOrder: {},
2075
+ groups: [],
2076
+ items: []
2077
+ };
2078
+
2045
2079
  // src/auth/token.ts
2046
2080
  var import_jose = require("jose");
2047
2081
 
2048
2082
  // src/auth/sessions.ts
2049
2083
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
2050
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
+
2051
2375
  // src/workflows.ts
2052
2376
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
2053
2377
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -2263,7 +2587,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
2263
2587
  return due.length;
2264
2588
  }
2265
2589
  async function saveWorkflowDraft(args) {
2266
- const { config, collection, id, originalDoc, data, user } = args;
2590
+ const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
2267
2591
  const db = config.db;
2268
2592
  const workflow = collection.workflow;
2269
2593
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2297,13 +2621,14 @@ async function saveWorkflowDraft(args) {
2297
2621
  }
2298
2622
  const updated = await tx.update({ collection: collection.slug, id, data: updateData });
2299
2623
  await persistEvent(tx, event);
2624
+ await beforeCommit?.(tx, updated);
2300
2625
  return updated;
2301
2626
  });
2302
2627
  void dispatchLifecycleEvent(config, event);
2303
2628
  return { doc, event };
2304
2629
  }
2305
2630
  async function createWorkflowDocument(args) {
2306
- const { config, collection, data, user } = args;
2631
+ const { config, collection, data, user, beforeCommit } = args;
2307
2632
  const db = config.db;
2308
2633
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
2309
2634
  let event;
@@ -2317,13 +2642,14 @@ async function createWorkflowDocument(args) {
2317
2642
  payload: { revision: 1, previousRevision: null }
2318
2643
  });
2319
2644
  await persistEvent(tx, event);
2645
+ await beforeCommit?.(tx, created);
2320
2646
  return created;
2321
2647
  });
2322
2648
  void dispatchLifecycleEvent(config, event);
2323
2649
  return { doc, event };
2324
2650
  }
2325
2651
  async function transitionWorkflow(args) {
2326
- const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
2652
+ const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
2327
2653
  const db = config.db;
2328
2654
  const workflow = collection.workflow;
2329
2655
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2349,7 +2675,7 @@ async function transitionWorkflow(args) {
2349
2675
  if (transition.requireComment && !comment?.trim()) {
2350
2676
  throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
2351
2677
  }
2352
- 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 };
2353
2679
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
2354
2680
  const events = [
2355
2681
  createLifecycleEvent({
@@ -2403,6 +2729,19 @@ async function transitionWorkflow(args) {
2403
2729
  if (targetState.published) data.__published = working;
2404
2730
  if (transition.unpublish) data.__published = null;
2405
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
+ }
2406
2745
  await tx.create({
2407
2746
  collection: WORKFLOW_HISTORY_COLLECTION,
2408
2747
  data: {
@@ -3839,6 +4178,7 @@ function normalizeConfig(config) {
3839
4178
  const globals = schemaAwareConfig?.globals || [];
3840
4179
  const needsAudit = collections.some((col) => col.audit);
3841
4180
  const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
4181
+ const needsTasks = (config.tasks?.length ?? 0) > 0;
3842
4182
  const needsAuthSessions = collections.some((col) => !!col.auth);
3843
4183
  const adminAuthCollectionSlug = getAdminAuthCollection({
3844
4184
  collections,
@@ -3918,7 +4258,33 @@ function normalizeConfig(config) {
3918
4258
  }
3919
4259
  ];
3920
4260
  }
4261
+ if (!existingFieldNames.has("__preferences")) {
4262
+ fields = [
4263
+ ...fields,
4264
+ {
4265
+ name: "__preferences",
4266
+ type: "json",
4267
+ label: "Preferences",
4268
+ promoted: true,
4269
+ admin: {
4270
+ hidden: true,
4271
+ readOnly: true
4272
+ }
4273
+ }
4274
+ ];
4275
+ }
3921
4276
  fields = fields.map((field) => {
4277
+ if (field.name === "__preferences") {
4278
+ return {
4279
+ ...field,
4280
+ promoted: true,
4281
+ admin: {
4282
+ ...field.admin || {},
4283
+ hidden: true,
4284
+ readOnly: true
4285
+ }
4286
+ };
4287
+ }
3922
4288
  if (field.name === "email") {
3923
4289
  return {
3924
4290
  ...field,
@@ -4045,6 +4411,9 @@ function normalizeConfig(config) {
4045
4411
  if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
4046
4412
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
4047
4413
  }
4414
+ if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
4415
+ systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
4416
+ }
4048
4417
  if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
4049
4418
  systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
4050
4419
  }
@@ -4129,7 +4498,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
4129
4498
  params.push("");
4130
4499
  return `(${c})::text = ${next()}`;
4131
4500
  }
4132
- 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
+ }
4133
4506
  return `${c} = ${next()}`;
4134
4507
  }
4135
4508
  const entries = Object.entries(value);
@@ -4329,6 +4702,38 @@ function parseMongoWhere(where) {
4329
4702
  }
4330
4703
  return buildClause(where);
4331
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
+ }
4332
4737
 
4333
4738
  // src/utils/parse-sort.ts
4334
4739
  var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
@@ -4610,7 +5015,12 @@ function generateOpenApi(config) {
4610
5015
  type: "object",
4611
5016
  properties: {
4612
5017
  expectedRevision: { type: "integer", minimum: 1 },
4613
- 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
+ }
4614
5024
  }
4615
5025
  },
4616
5026
  WorkflowHistoryEntry: {
@@ -5773,6 +6183,9 @@ function fieldToSchema(field) {
5773
6183
  case "number":
5774
6184
  schema = { type: "number" };
5775
6185
  break;
6186
+ case "money":
6187
+ schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
6188
+ break;
5776
6189
  case "boolean":
5777
6190
  schema = { type: "boolean" };
5778
6191
  break;
@@ -6725,6 +7138,555 @@ function sanitizeDocForAI(options) {
6725
7138
  return result;
6726
7139
  }
6727
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
+
7171
+ // src/utils/navigation-reconciler.ts
7172
+ var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
7173
+ function reconcileNavigation(baseTree, prefs, schemas, options) {
7174
+ if (!baseTree) {
7175
+ return {
7176
+ groups: [],
7177
+ ungrouped: [],
7178
+ pinnedItems: []
7179
+ };
7180
+ }
7181
+ const includeHidden = options?.includeHidden ?? false;
7182
+ const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
7183
+ const hiddenSet = new Set(effectivePrefs.hidden || []);
7184
+ const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
7185
+ const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
7186
+ const groupsList = [];
7187
+ const findGroup = (query) => {
7188
+ const qNorm = normalizeKey(query);
7189
+ return groupsList.find(
7190
+ (g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
7191
+ );
7192
+ };
7193
+ for (const g of baseTree.groups || []) {
7194
+ groupsList.push({
7195
+ ...g,
7196
+ items: (g.items || []).map((item) => ({ ...item }))
7197
+ });
7198
+ }
7199
+ for (const ug of effectivePrefs.groups || []) {
7200
+ const existing = findGroup(ug.slug || ug.name);
7201
+ if (!existing) {
7202
+ groupsList.push({
7203
+ id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
7204
+ name: ug.name,
7205
+ slug: ug.slug || normalizeKey(ug.name),
7206
+ icon: ug.icon || "Folder",
7207
+ defaultExpanded: ug.defaultExpanded ?? true,
7208
+ order: ug.order ?? 100,
7209
+ items: []
7210
+ });
7211
+ }
7212
+ }
7213
+ for (const itemOpt of effectivePrefs.items || []) {
7214
+ const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
7215
+ const groupKey = targetGroup || "Custom";
7216
+ let group = findGroup(groupKey);
7217
+ if (!group) {
7218
+ group = {
7219
+ id: `group_${normalizeKey(groupKey)}`,
7220
+ name: groupKey,
7221
+ slug: normalizeKey(groupKey),
7222
+ icon: "Folder",
7223
+ defaultExpanded: true,
7224
+ order: 99,
7225
+ items: []
7226
+ };
7227
+ groupsList.push(group);
7228
+ }
7229
+ const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
7230
+ for (const otherGroup of groupsList) {
7231
+ if (otherGroup !== group) {
7232
+ otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
7233
+ }
7234
+ }
7235
+ const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
7236
+ const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
7237
+ const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
7238
+ const colViews = col?.views || [];
7239
+ const baseViews = existingItem?.views || [];
7240
+ const userViews = itemOpt.views || [];
7241
+ const userViewSlugs = new Set(userViews.map((v) => v.slug));
7242
+ let mergedViews = [
7243
+ ...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
7244
+ ...userViews
7245
+ ];
7246
+ if (mergedViews.length === 0 && itemOpt.collection) {
7247
+ if (colViews.length > 0) {
7248
+ mergedViews = [...colViews];
7249
+ } else {
7250
+ mergedViews = [
7251
+ {
7252
+ slug: "default",
7253
+ label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
7254
+ layout: "table",
7255
+ collection: itemOpt.collection
7256
+ }
7257
+ ];
7258
+ }
7259
+ }
7260
+ mergedViews = mergedViews.map((v) => ({
7261
+ ...v,
7262
+ collection: v.collection || itemOpt.collection || existingItem?.collection
7263
+ }));
7264
+ const compiledItem = {
7265
+ id: itemId,
7266
+ type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
7267
+ slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
7268
+ label: itemOpt.label || itemOpt.slug || "Item",
7269
+ icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
7270
+ group: group.name,
7271
+ order: itemOpt.order ?? existingItem?.order ?? 100,
7272
+ views: mergedViews,
7273
+ collection: itemOpt.collection || existingItem?.collection,
7274
+ global: itemOpt.global || existingItem?.global,
7275
+ badge: itemOpt.badge || existingItem?.badge,
7276
+ access: itemOpt.access || existingItem?.access
7277
+ };
7278
+ if (existingIndex >= 0) {
7279
+ group.items[existingIndex] = compiledItem;
7280
+ } else {
7281
+ group.items.push(compiledItem);
7282
+ }
7283
+ }
7284
+ const allAvailableItems = [];
7285
+ const reconciledGroups = [];
7286
+ for (const group of groupsList) {
7287
+ const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
7288
+ if (!includeHidden && isGroupHidden) {
7289
+ continue;
7290
+ }
7291
+ const visibleItems = [];
7292
+ for (const item of group.items) {
7293
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
7294
+ if (!includeHidden && isItemHidden) {
7295
+ continue;
7296
+ }
7297
+ const allViews = item.views || [];
7298
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
7299
+ const compoundKey = `${item.slug}_${v.slug}`;
7300
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
7301
+ });
7302
+ let isTombstone = false;
7303
+ if (schemas && schemas.collections && schemas.collections.length > 0) {
7304
+ if (item.collection && !knownCollectionSlugs.has(item.collection)) {
7305
+ isTombstone = true;
7306
+ }
7307
+ }
7308
+ if (schemas && schemas.globals && schemas.globals.length > 0) {
7309
+ if (item.global && !knownGlobalSlugs.has(item.global)) {
7310
+ isTombstone = true;
7311
+ }
7312
+ }
7313
+ const processedItem = {
7314
+ ...item,
7315
+ views: visibleViews,
7316
+ ...isTombstone ? { isTombstone: true } : {}
7317
+ };
7318
+ visibleItems.push(processedItem);
7319
+ allAvailableItems.push(processedItem);
7320
+ }
7321
+ const groupKey = group.slug || group.name;
7322
+ const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
7323
+ if (itemOrderList && itemOrderList.length > 0) {
7324
+ visibleItems.sort((a, b) => {
7325
+ const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
7326
+ const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
7327
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
7328
+ if (aIdx >= 0) return -1;
7329
+ if (bIdx >= 0) return 1;
7330
+ return (a.order ?? 100) - (b.order ?? 100);
7331
+ });
7332
+ } else {
7333
+ visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
7334
+ }
7335
+ reconciledGroups.push({
7336
+ ...group,
7337
+ items: visibleItems
7338
+ });
7339
+ }
7340
+ if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
7341
+ reconciledGroups.sort((a, b) => {
7342
+ const aIdx = effectivePrefs.groupOrder.indexOf(a.id) >= 0 ? effectivePrefs.groupOrder.indexOf(a.id) : a.slug ? effectivePrefs.groupOrder.indexOf(a.slug) : effectivePrefs.groupOrder.indexOf(a.name);
7343
+ const bIdx = effectivePrefs.groupOrder.indexOf(b.id) >= 0 ? effectivePrefs.groupOrder.indexOf(b.id) : b.slug ? effectivePrefs.groupOrder.indexOf(b.slug) : effectivePrefs.groupOrder.indexOf(b.name);
7344
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
7345
+ if (aIdx >= 0) return -1;
7346
+ if (bIdx >= 0) return 1;
7347
+ return (a.order ?? 100) - (b.order ?? 100);
7348
+ });
7349
+ } else {
7350
+ reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
7351
+ }
7352
+ const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
7353
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
7354
+ return includeHidden || !isItemHidden;
7355
+ }).map((item) => {
7356
+ const allViews = item.views || [];
7357
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
7358
+ const compoundKey = `${item.slug}_${v.slug}`;
7359
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
7360
+ });
7361
+ const processed = { ...item, views: visibleViews };
7362
+ allAvailableItems.push(processed);
7363
+ return processed;
7364
+ });
7365
+ const pinnedItems = [];
7366
+ for (const pin of effectivePrefs.pinned || []) {
7367
+ const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
7368
+ if (match) {
7369
+ pinnedItems.push({
7370
+ ...match,
7371
+ id: `pin_${match.id}`,
7372
+ label: pin.label || match.label,
7373
+ icon: pin.icon || match.icon
7374
+ });
7375
+ } else {
7376
+ pinnedItems.push({
7377
+ id: `pin_${pin.slug}`,
7378
+ type: pin.type,
7379
+ slug: pin.slug,
7380
+ label: pin.label || pin.slug,
7381
+ icon: pin.icon || "Star",
7382
+ order: 0,
7383
+ views: []
7384
+ });
7385
+ }
7386
+ }
7387
+ return {
7388
+ groups: reconciledGroups,
7389
+ ungrouped: reconciledUngrouped,
7390
+ pinnedItems
7391
+ };
7392
+ }
7393
+
7394
+ // src/utils/navigation.ts
7395
+ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
7396
+ "collections",
7397
+ "globals",
7398
+ "setup",
7399
+ "api",
7400
+ "login",
7401
+ "logout",
7402
+ "settings",
7403
+ "admin",
7404
+ "preferences",
7405
+ "auth"
7406
+ ]);
7407
+ function assertValidNavigationSlugs(navigation) {
7408
+ for (const item of navigation) {
7409
+ if (item.slug) {
7410
+ const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
7411
+ if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
7412
+ throw new Error(
7413
+ `[Dyrected Config] The slug '${item.slug}' in navigation item '${item.label || item.slug}' is a reserved system path and cannot be used as an operational workspace slug.`
7414
+ );
7415
+ }
7416
+ }
7417
+ }
7418
+ }
7419
+ function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
7420
+ if (!groupProp) {
7421
+ return { name: defaultGroupName };
7422
+ }
7423
+ if (typeof groupProp === "string") {
7424
+ return { name: groupProp };
7425
+ }
7426
+ return { name: groupProp.name, meta: groupProp };
7427
+ }
7428
+ function compileNavigation(config) {
7429
+ const collections = config.collections || [];
7430
+ const globals = config.globals || [];
7431
+ const explicitNav = config.admin?.navigation || [];
7432
+ assertValidNavigationSlugs(explicitNav);
7433
+ const collectionsBySlug = /* @__PURE__ */ new Map();
7434
+ for (const col of collections) {
7435
+ collectionsBySlug.set(col.slug, col);
7436
+ }
7437
+ const globalsBySlug = /* @__PURE__ */ new Map();
7438
+ for (const gl of globals) {
7439
+ globalsBySlug.set(gl.slug, gl);
7440
+ }
7441
+ const mentionedCollections = /* @__PURE__ */ new Set();
7442
+ const mentionedGlobals = /* @__PURE__ */ new Set();
7443
+ const injectedViewsByCollection = /* @__PURE__ */ new Map();
7444
+ for (const item of explicitNav) {
7445
+ if (item.addToCollection) {
7446
+ const existing = injectedViewsByCollection.get(item.addToCollection) || [];
7447
+ if (item.views && item.views.length > 0) {
7448
+ injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
7449
+ }
7450
+ }
7451
+ if (item.collection) mentionedCollections.add(item.collection);
7452
+ if (item.global) mentionedGlobals.add(item.global);
7453
+ }
7454
+ const groupMetadataMap = /* @__PURE__ */ new Map();
7455
+ const itemsByGroup = /* @__PURE__ */ new Map();
7456
+ const ungroupedItems = [];
7457
+ const registerItem = (groupName, item) => {
7458
+ if (!groupName) {
7459
+ ungroupedItems.push(item);
7460
+ } else {
7461
+ const list = itemsByGroup.get(groupName) || [];
7462
+ list.push(item);
7463
+ itemsByGroup.set(groupName, list);
7464
+ }
7465
+ };
7466
+ for (const item of explicitNav) {
7467
+ if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
7468
+ continue;
7469
+ }
7470
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
7471
+ if (groupName && groupMeta) {
7472
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
7473
+ }
7474
+ if (item.dashboard) {
7475
+ registerItem(groupName, {
7476
+ id: "dashboard",
7477
+ type: "dashboard",
7478
+ slug: "dashboard",
7479
+ label: item.label || "Dashboard",
7480
+ icon: item.icon || "LayoutDashboard",
7481
+ group: groupName,
7482
+ order: item.order ?? 0,
7483
+ views: [],
7484
+ access: item.access
7485
+ });
7486
+ continue;
7487
+ }
7488
+ if (item.collection) {
7489
+ const col = collectionsBySlug.get(item.collection);
7490
+ const combinedViews = [
7491
+ ...col?.views || [],
7492
+ ...injectedViewsByCollection.get(item.collection) || [],
7493
+ ...item.views || []
7494
+ ];
7495
+ registerItem(groupName, {
7496
+ id: item.slug || `collection_${item.collection}`,
7497
+ type: item.slug ? "workspace" : "collection",
7498
+ slug: item.slug || item.collection,
7499
+ collection: item.collection,
7500
+ label: item.label || col?.labels?.plural || item.collection,
7501
+ icon: item.icon || col?.admin?.icon || "Folder",
7502
+ group: groupName,
7503
+ order: item.order ?? col?.admin?.order ?? 100,
7504
+ views: combinedViews,
7505
+ badge: item.badge,
7506
+ access: item.access || col?.access?.read
7507
+ });
7508
+ continue;
7509
+ }
7510
+ if (item.global) {
7511
+ const gl = globalsBySlug.get(item.global);
7512
+ registerItem(groupName, {
7513
+ id: item.slug || `global_${item.global}`,
7514
+ type: "global",
7515
+ slug: item.slug || item.global,
7516
+ global: item.global,
7517
+ label: item.label || gl?.label || item.global,
7518
+ icon: item.icon || "Settings",
7519
+ group: groupName,
7520
+ order: item.order ?? 100,
7521
+ views: [],
7522
+ badge: item.badge,
7523
+ access: item.access
7524
+ });
7525
+ continue;
7526
+ }
7527
+ if (item.href) {
7528
+ registerItem(groupName, {
7529
+ id: `link_${item.slug || item.href}`,
7530
+ type: "link",
7531
+ slug: item.slug || "link",
7532
+ label: item.label || "Link",
7533
+ href: item.href,
7534
+ icon: item.icon || "ExternalLink",
7535
+ group: groupName,
7536
+ order: item.order ?? 100,
7537
+ views: [],
7538
+ badge: item.badge,
7539
+ access: item.access
7540
+ });
7541
+ continue;
7542
+ }
7543
+ const workspaceSlug = item.slug || "workspace";
7544
+ registerItem(groupName, {
7545
+ id: `workspace_${workspaceSlug}`,
7546
+ type: "workspace",
7547
+ slug: workspaceSlug,
7548
+ label: item.label || workspaceSlug,
7549
+ icon: item.icon || "Briefcase",
7550
+ group: groupName,
7551
+ order: item.order ?? 100,
7552
+ views: item.views || [],
7553
+ badge: item.badge,
7554
+ access: item.access
7555
+ });
7556
+ }
7557
+ for (const col of collections) {
7558
+ if (mentionedCollections.has(col.slug)) continue;
7559
+ if (col.admin?.hidden) continue;
7560
+ const groupProp = col.admin?.group;
7561
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
7562
+ if (groupMeta) {
7563
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
7564
+ }
7565
+ const combinedViews = [
7566
+ ...col.views || [],
7567
+ ...injectedViewsByCollection.get(col.slug) || []
7568
+ ];
7569
+ registerItem(groupName, {
7570
+ id: `collection_${col.slug}`,
7571
+ type: "collection",
7572
+ slug: col.slug,
7573
+ collection: col.slug,
7574
+ label: col.labels?.plural || col.slug,
7575
+ icon: col.admin?.icon || "Folder",
7576
+ group: groupName,
7577
+ order: col?.admin?.order ?? 100,
7578
+ views: combinedViews,
7579
+ access: col.access?.read
7580
+ });
7581
+ }
7582
+ for (const gl of globals) {
7583
+ if (mentionedGlobals.has(gl.slug)) continue;
7584
+ const groupName = "Configuration";
7585
+ registerItem(groupName, {
7586
+ id: `global_${gl.slug}`,
7587
+ type: "global",
7588
+ slug: gl.slug,
7589
+ global: gl.slug,
7590
+ label: gl.label || gl.slug,
7591
+ icon: "Settings",
7592
+ group: groupName,
7593
+ order: 100,
7594
+ views: []
7595
+ });
7596
+ }
7597
+ const sortAndSpliceItems = (items) => {
7598
+ const sorted = [...items].sort((a, b) => a.order - b.order);
7599
+ const explicitMap = /* @__PURE__ */ new Map();
7600
+ for (const opt of explicitNav) {
7601
+ if (opt.slug) explicitMap.set(opt.slug, opt);
7602
+ if (opt.collection) explicitMap.set(opt.collection, opt);
7603
+ if (opt.global) explicitMap.set(opt.global, opt);
7604
+ }
7605
+ for (const opt of explicitNav) {
7606
+ const itemKey = opt.slug || opt.collection || opt.global;
7607
+ if (!itemKey) continue;
7608
+ if (opt.after) {
7609
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7610
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
7611
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7612
+ const [moved] = sorted.splice(itemIdx, 1);
7613
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
7614
+ sorted.splice(newTargetIdx + 1, 0, moved);
7615
+ }
7616
+ } else if (opt.before) {
7617
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7618
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
7619
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7620
+ const [moved] = sorted.splice(itemIdx, 1);
7621
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
7622
+ sorted.splice(newTargetIdx, 0, moved);
7623
+ }
7624
+ } else if (opt.position === "first") {
7625
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7626
+ if (itemIdx > 0) {
7627
+ const [moved] = sorted.splice(itemIdx, 1);
7628
+ sorted.unshift(moved);
7629
+ }
7630
+ } else if (opt.position === "last") {
7631
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7632
+ if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
7633
+ const [moved] = sorted.splice(itemIdx, 1);
7634
+ sorted.push(moved);
7635
+ }
7636
+ }
7637
+ }
7638
+ return sorted;
7639
+ };
7640
+ const compiledGroups = [];
7641
+ for (const [groupName, rawItems] of itemsByGroup.entries()) {
7642
+ const meta = groupMetadataMap.get(groupName);
7643
+ const sortedItems = sortAndSpliceItems(rawItems);
7644
+ compiledGroups.push({
7645
+ id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
7646
+ name: groupName,
7647
+ slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
7648
+ icon: meta?.icon,
7649
+ defaultExpanded: meta?.defaultExpanded ?? true,
7650
+ order: meta?.order ?? 100,
7651
+ items: sortedItems
7652
+ });
7653
+ }
7654
+ compiledGroups.sort((a, b) => a.order - b.order);
7655
+ return {
7656
+ groups: compiledGroups,
7657
+ ungrouped: sortAndSpliceItems(ungroupedItems)
7658
+ };
7659
+ }
7660
+ function pruneNavigationForUser(tree, user) {
7661
+ const userRoles = /* @__PURE__ */ new Set();
7662
+ if (user?.role) userRoles.add(user.role);
7663
+ if (Array.isArray(user?.roles)) {
7664
+ for (const r of user.roles) userRoles.add(r);
7665
+ }
7666
+ const isAllowed = (item) => {
7667
+ if (!item.access) return true;
7668
+ if (Array.isArray(item.access)) {
7669
+ if (item.access.length === 0) return true;
7670
+ return item.access.some((role) => userRoles.has(role));
7671
+ }
7672
+ return true;
7673
+ };
7674
+ const prunedGroups = [];
7675
+ for (const group of tree.groups) {
7676
+ const visibleItems = group.items.filter(isAllowed);
7677
+ if (visibleItems.length > 0) {
7678
+ prunedGroups.push({
7679
+ ...group,
7680
+ items: visibleItems
7681
+ });
7682
+ }
7683
+ }
7684
+ return {
7685
+ groups: prunedGroups,
7686
+ ungrouped: tree.ungrouped.filter(isAllowed)
7687
+ };
7688
+ }
7689
+
6728
7690
  // src/index.ts
6729
7691
  var import_zod = require("zod");
6730
7692
  function defineCollection(config) {
@@ -6749,6 +7711,7 @@ var defineTextField = createFieldDefiner("text");
6749
7711
  var defineTextareaField = createFieldDefiner("textarea");
6750
7712
  var defineRichTextField = createFieldDefiner("richText");
6751
7713
  var defineNumberField = createFieldDefiner("number");
7714
+ var defineMoneyField = createFieldDefiner("money");
6752
7715
  var defineBooleanField = createFieldDefiner("boolean");
6753
7716
  var defineDateField = createFieldDefiner("date");
6754
7717
  var defineDateTimeField = createFieldDefiner("datetime");
@@ -6792,8 +7755,10 @@ function defineTab(args) {
6792
7755
  BASE62_ALPHABET,
6793
7756
  BUILTIN_JEXL_HELPERS,
6794
7757
  CROCKFORD_BASE32,
7758
+ CURRENT_NAV_PREFERENCES_VERSION,
6795
7759
  ConfigValidationError,
6796
7760
  ConflictError,
7761
+ DEFAULT_USER_NAV_PREFERENCES,
6797
7762
  DuplicateKeyError,
6798
7763
  DyrectedAIError,
6799
7764
  DyrectedError,
@@ -6806,6 +7771,10 @@ function defineTab(args) {
6806
7771
  NON_NEGOTIABLE_CREDENTIAL_FIELDS,
6807
7772
  NotFoundError,
6808
7773
  PII_REGEX_PATTERNS,
7774
+ RESERVED_NAVIGATION_SLUGS,
7775
+ TASK_LOCKS_COLLECTION,
7776
+ TASK_LOCKS_COLLECTION_CONFIG,
7777
+ UNASSIGNED_GROUP_KEY,
6809
7778
  ValidationError,
6810
7779
  WORKFLOW_HISTORY_COLLECTION,
6811
7780
  abs,
@@ -6816,6 +7785,7 @@ function defineTab(args) {
6816
7785
  assertValidDeclarativeHookExpression,
6817
7786
  assertValidDeclarativeHookResult,
6818
7787
  assertValidDeclarativeHooksInConfig,
7788
+ assertValidNavigationSlugs,
6819
7789
  assertValidPreviewUrlsInConfig,
6820
7790
  availableWorkflowTransitions,
6821
7791
  buildSqlOrderBy,
@@ -6824,14 +7794,17 @@ function defineTab(args) {
6824
7794
  ceil,
6825
7795
  clamp,
6826
7796
  coalesce,
7797
+ coerceBooleanWhere,
6827
7798
  collectAdminConditionValidationIssues,
6828
7799
  collectConfigDiagnostics,
6829
7800
  collectDeclarativeAccessValidationIssues,
6830
7801
  collectDeclarativeHookValidationIssues,
6831
7802
  collectPreviewUrlValidationIssues,
6832
7803
  compact,
7804
+ compileNavigation,
6833
7805
  count,
6834
7806
  createLifecycleEvent,
7807
+ createTaskRunner,
6835
7808
  createWorkflowDocument,
6836
7809
  defaultValue,
6837
7810
  defineAction,
@@ -6854,6 +7827,7 @@ function defineTab(args) {
6854
7827
  defineJoinField,
6855
7828
  defineJsonField,
6856
7829
  defineKanbanView,
7830
+ defineMoneyField,
6857
7831
  defineMultiSelectField,
6858
7832
  defineNumberField,
6859
7833
  defineObjectField,
@@ -6865,11 +7839,13 @@ function defineTab(args) {
6865
7839
  defineSelectField,
6866
7840
  defineTab,
6867
7841
  defineTableView,
7842
+ defineTask,
6868
7843
  defineTextField,
6869
7844
  defineTextareaField,
6870
7845
  defineTimeField,
6871
7846
  defineUrlField,
6872
7847
  defineView,
7848
+ defineWorkspace,
6873
7849
  diffDays,
6874
7850
  dispatchLifecycleEvent,
6875
7851
  dispatchPendingLifecycleEvents,
@@ -6927,22 +7903,28 @@ function defineTab(args) {
6927
7903
  max,
6928
7904
  min,
6929
7905
  nanoid,
7906
+ nextCronRun,
6930
7907
  normalizeConfig,
6931
7908
  normalizeDetailItem,
7909
+ normalizeGroupKey,
6932
7910
  now,
6933
7911
  numericFieldNames,
7912
+ parseCron,
6934
7913
  parseMongoWhere,
6935
7914
  parseSort,
6936
7915
  parseSqlWhere,
6937
7916
  pow,
6938
7917
  prefixedId,
7918
+ pruneNavigationForUser,
6939
7919
  publishedStateName,
6940
7920
  publishingWorkflow,
6941
7921
  readingTime,
7922
+ reconcileNavigation,
6942
7923
  registerJexlHelpers,
6943
7924
  replace,
6944
7925
  resolveAdminAuthCollection,
6945
7926
  resolveAuthorizedSiteId,
7927
+ resolveIndexName,
6946
7928
  resolvePrefix,
6947
7929
  round,
6948
7930
  runCollectionHooks,