@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.js CHANGED
@@ -1845,12 +1845,317 @@ var DyrectedAIError = class _DyrectedAIError extends Error {
1845
1845
  }
1846
1846
  };
1847
1847
 
1848
+ // src/types/navigation.ts
1849
+ function defineWorkspace(options) {
1850
+ return options;
1851
+ }
1852
+ var CURRENT_NAV_PREFERENCES_VERSION = 1;
1853
+ var DEFAULT_USER_NAV_PREFERENCES = {
1854
+ _version: CURRENT_NAV_PREFERENCES_VERSION,
1855
+ pinned: [],
1856
+ hidden: [],
1857
+ groupOrder: [],
1858
+ itemOrder: {},
1859
+ groups: [],
1860
+ items: []
1861
+ };
1862
+
1848
1863
  // src/auth/token.ts
1849
1864
  import { SignJWT, jwtVerify, decodeJwt } from "jose";
1850
1865
 
1851
1866
  // src/auth/sessions.ts
1852
1867
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
1853
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
+
1854
2159
  // src/workflows.ts
1855
2160
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
1856
2161
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -2066,7 +2371,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
2066
2371
  return due.length;
2067
2372
  }
2068
2373
  async function saveWorkflowDraft(args) {
2069
- const { config, collection, id, originalDoc, data, user } = args;
2374
+ const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
2070
2375
  const db = config.db;
2071
2376
  const workflow = collection.workflow;
2072
2377
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2100,13 +2405,14 @@ async function saveWorkflowDraft(args) {
2100
2405
  }
2101
2406
  const updated = await tx.update({ collection: collection.slug, id, data: updateData });
2102
2407
  await persistEvent(tx, event);
2408
+ await beforeCommit?.(tx, updated);
2103
2409
  return updated;
2104
2410
  });
2105
2411
  void dispatchLifecycleEvent(config, event);
2106
2412
  return { doc, event };
2107
2413
  }
2108
2414
  async function createWorkflowDocument(args) {
2109
- const { config, collection, data, user } = args;
2415
+ const { config, collection, data, user, beforeCommit } = args;
2110
2416
  const db = config.db;
2111
2417
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
2112
2418
  let event;
@@ -2120,13 +2426,14 @@ async function createWorkflowDocument(args) {
2120
2426
  payload: { revision: 1, previousRevision: null }
2121
2427
  });
2122
2428
  await persistEvent(tx, event);
2429
+ await beforeCommit?.(tx, created);
2123
2430
  return created;
2124
2431
  });
2125
2432
  void dispatchLifecycleEvent(config, event);
2126
2433
  return { doc, event };
2127
2434
  }
2128
2435
  async function transitionWorkflow(args) {
2129
- const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
2436
+ const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
2130
2437
  const db = config.db;
2131
2438
  const workflow = collection.workflow;
2132
2439
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
@@ -2152,7 +2459,7 @@ async function transitionWorkflow(args) {
2152
2459
  if (transition.requireComment && !comment?.trim()) {
2153
2460
  throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
2154
2461
  }
2155
- 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 };
2156
2463
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
2157
2464
  const events = [
2158
2465
  createLifecycleEvent({
@@ -2206,6 +2513,19 @@ async function transitionWorkflow(args) {
2206
2513
  if (targetState.published) data.__published = working;
2207
2514
  if (transition.unpublish) data.__published = null;
2208
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
+ }
2209
2529
  await tx.create({
2210
2530
  collection: WORKFLOW_HISTORY_COLLECTION,
2211
2531
  data: {
@@ -3642,6 +3962,7 @@ function normalizeConfig(config) {
3642
3962
  const globals = schemaAwareConfig?.globals || [];
3643
3963
  const needsAudit = collections.some((col) => col.audit);
3644
3964
  const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
3965
+ const needsTasks = (config.tasks?.length ?? 0) > 0;
3645
3966
  const needsAuthSessions = collections.some((col) => !!col.auth);
3646
3967
  const adminAuthCollectionSlug = getAdminAuthCollection({
3647
3968
  collections,
@@ -3721,7 +4042,33 @@ function normalizeConfig(config) {
3721
4042
  }
3722
4043
  ];
3723
4044
  }
4045
+ if (!existingFieldNames.has("__preferences")) {
4046
+ fields = [
4047
+ ...fields,
4048
+ {
4049
+ name: "__preferences",
4050
+ type: "json",
4051
+ label: "Preferences",
4052
+ promoted: true,
4053
+ admin: {
4054
+ hidden: true,
4055
+ readOnly: true
4056
+ }
4057
+ }
4058
+ ];
4059
+ }
3724
4060
  fields = fields.map((field) => {
4061
+ if (field.name === "__preferences") {
4062
+ return {
4063
+ ...field,
4064
+ promoted: true,
4065
+ admin: {
4066
+ ...field.admin || {},
4067
+ hidden: true,
4068
+ readOnly: true
4069
+ }
4070
+ };
4071
+ }
3725
4072
  if (field.name === "email") {
3726
4073
  return {
3727
4074
  ...field,
@@ -3848,6 +4195,9 @@ function normalizeConfig(config) {
3848
4195
  if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
3849
4196
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
3850
4197
  }
4198
+ if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
4199
+ systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
4200
+ }
3851
4201
  if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
3852
4202
  systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
3853
4203
  }
@@ -3932,7 +4282,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
3932
4282
  params.push("");
3933
4283
  return `(${c})::text = ${next()}`;
3934
4284
  }
3935
- 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
+ }
3936
4290
  return `${c} = ${next()}`;
3937
4291
  }
3938
4292
  const entries = Object.entries(value);
@@ -4132,6 +4486,38 @@ function parseMongoWhere(where) {
4132
4486
  }
4133
4487
  return buildClause(where);
4134
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
+ }
4135
4521
 
4136
4522
  // src/utils/parse-sort.ts
4137
4523
  var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
@@ -4413,7 +4799,12 @@ function generateOpenApi(config) {
4413
4799
  type: "object",
4414
4800
  properties: {
4415
4801
  expectedRevision: { type: "integer", minimum: 1 },
4416
- 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
+ }
4417
4808
  }
4418
4809
  },
4419
4810
  WorkflowHistoryEntry: {
@@ -5576,6 +5967,9 @@ function fieldToSchema(field) {
5576
5967
  case "number":
5577
5968
  schema = { type: "number" };
5578
5969
  break;
5970
+ case "money":
5971
+ schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
5972
+ break;
5579
5973
  case "boolean":
5580
5974
  schema = { type: "boolean" };
5581
5975
  break;
@@ -6528,6 +6922,555 @@ function sanitizeDocForAI(options) {
6528
6922
  return result;
6529
6923
  }
6530
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
+
6955
+ // src/utils/navigation-reconciler.ts
6956
+ var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
6957
+ function reconcileNavigation(baseTree, prefs, schemas, options) {
6958
+ if (!baseTree) {
6959
+ return {
6960
+ groups: [],
6961
+ ungrouped: [],
6962
+ pinnedItems: []
6963
+ };
6964
+ }
6965
+ const includeHidden = options?.includeHidden ?? false;
6966
+ const effectivePrefs = prefs || DEFAULT_USER_NAV_PREFERENCES;
6967
+ const hiddenSet = new Set(effectivePrefs.hidden || []);
6968
+ const knownCollectionSlugs = new Set((schemas?.collections || []).map((c) => c.slug));
6969
+ const knownGlobalSlugs = new Set((schemas?.globals || []).map((g) => g.slug));
6970
+ const groupsList = [];
6971
+ const findGroup = (query) => {
6972
+ const qNorm = normalizeKey(query);
6973
+ return groupsList.find(
6974
+ (g) => g.id === query || g.slug === query || g.name === query || normalizeKey(g.id) === qNorm || normalizeKey(g.slug) === qNorm || normalizeKey(g.name) === qNorm
6975
+ );
6976
+ };
6977
+ for (const g of baseTree.groups || []) {
6978
+ groupsList.push({
6979
+ ...g,
6980
+ items: (g.items || []).map((item) => ({ ...item }))
6981
+ });
6982
+ }
6983
+ for (const ug of effectivePrefs.groups || []) {
6984
+ const existing = findGroup(ug.slug || ug.name);
6985
+ if (!existing) {
6986
+ groupsList.push({
6987
+ id: ug.slug ? `group_${ug.slug}` : `group_${normalizeKey(ug.name)}`,
6988
+ name: ug.name,
6989
+ slug: ug.slug || normalizeKey(ug.name),
6990
+ icon: ug.icon || "Folder",
6991
+ defaultExpanded: ug.defaultExpanded ?? true,
6992
+ order: ug.order ?? 100,
6993
+ items: []
6994
+ });
6995
+ }
6996
+ }
6997
+ for (const itemOpt of effectivePrefs.items || []) {
6998
+ const targetGroup = typeof itemOpt.group === "object" ? itemOpt.group?.name : itemOpt.group;
6999
+ const groupKey = targetGroup || "Custom";
7000
+ let group = findGroup(groupKey);
7001
+ if (!group) {
7002
+ group = {
7003
+ id: `group_${normalizeKey(groupKey)}`,
7004
+ name: groupKey,
7005
+ slug: normalizeKey(groupKey),
7006
+ icon: "Folder",
7007
+ defaultExpanded: true,
7008
+ order: 99,
7009
+ items: []
7010
+ };
7011
+ groupsList.push(group);
7012
+ }
7013
+ const itemId = itemOpt.slug ? `workspace_${itemOpt.slug}` : itemOpt.collection ? `collection_${itemOpt.collection}` : `custom_${Math.random()}`;
7014
+ for (const otherGroup of groupsList) {
7015
+ if (otherGroup !== group) {
7016
+ otherGroup.items = otherGroup.items.filter((i) => i.slug !== itemOpt.slug && i.id !== itemId);
7017
+ }
7018
+ }
7019
+ const existingIndex = group.items.findIndex((i) => i.id === itemId || i.slug === itemOpt.slug);
7020
+ const existingItem = existingIndex >= 0 ? group.items[existingIndex] : void 0;
7021
+ const col = schemas?.collections?.find((c) => c.slug === itemOpt.collection);
7022
+ const colViews = col?.views || [];
7023
+ const baseViews = existingItem?.views || [];
7024
+ const userViews = itemOpt.views || [];
7025
+ const userViewSlugs = new Set(userViews.map((v) => v.slug));
7026
+ let mergedViews = [
7027
+ ...baseViews.filter((v) => !userViewSlugs.has(v.slug)),
7028
+ ...userViews
7029
+ ];
7030
+ if (mergedViews.length === 0 && itemOpt.collection) {
7031
+ if (colViews.length > 0) {
7032
+ mergedViews = [...colViews];
7033
+ } else {
7034
+ mergedViews = [
7035
+ {
7036
+ slug: "default",
7037
+ label: col?.label || col?.labels?.plural || itemOpt.label || "All Records",
7038
+ layout: "table",
7039
+ collection: itemOpt.collection
7040
+ }
7041
+ ];
7042
+ }
7043
+ }
7044
+ mergedViews = mergedViews.map((v) => ({
7045
+ ...v,
7046
+ collection: v.collection || itemOpt.collection || existingItem?.collection
7047
+ }));
7048
+ const compiledItem = {
7049
+ id: itemId,
7050
+ type: itemOpt.slug ? "workspace" : itemOpt.collection ? "collection" : itemOpt.global ? "global" : "workspace",
7051
+ slug: itemOpt.slug || itemOpt.collection || itemOpt.global || "item",
7052
+ label: itemOpt.label || itemOpt.slug || "Item",
7053
+ icon: itemOpt.icon || existingItem?.icon || col?.admin?.icon || "Briefcase",
7054
+ group: group.name,
7055
+ order: itemOpt.order ?? existingItem?.order ?? 100,
7056
+ views: mergedViews,
7057
+ collection: itemOpt.collection || existingItem?.collection,
7058
+ global: itemOpt.global || existingItem?.global,
7059
+ badge: itemOpt.badge || existingItem?.badge,
7060
+ access: itemOpt.access || existingItem?.access
7061
+ };
7062
+ if (existingIndex >= 0) {
7063
+ group.items[existingIndex] = compiledItem;
7064
+ } else {
7065
+ group.items.push(compiledItem);
7066
+ }
7067
+ }
7068
+ const allAvailableItems = [];
7069
+ const reconciledGroups = [];
7070
+ for (const group of groupsList) {
7071
+ const isGroupHidden = hiddenSet.has(group.id) || !!group.slug && hiddenSet.has(group.slug) || hiddenSet.has(group.name);
7072
+ if (!includeHidden && isGroupHidden) {
7073
+ continue;
7074
+ }
7075
+ const visibleItems = [];
7076
+ for (const item of group.items) {
7077
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
7078
+ if (!includeHidden && isItemHidden) {
7079
+ continue;
7080
+ }
7081
+ const allViews = item.views || [];
7082
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
7083
+ const compoundKey = `${item.slug}_${v.slug}`;
7084
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
7085
+ });
7086
+ let isTombstone = false;
7087
+ if (schemas && schemas.collections && schemas.collections.length > 0) {
7088
+ if (item.collection && !knownCollectionSlugs.has(item.collection)) {
7089
+ isTombstone = true;
7090
+ }
7091
+ }
7092
+ if (schemas && schemas.globals && schemas.globals.length > 0) {
7093
+ if (item.global && !knownGlobalSlugs.has(item.global)) {
7094
+ isTombstone = true;
7095
+ }
7096
+ }
7097
+ const processedItem = {
7098
+ ...item,
7099
+ views: visibleViews,
7100
+ ...isTombstone ? { isTombstone: true } : {}
7101
+ };
7102
+ visibleItems.push(processedItem);
7103
+ allAvailableItems.push(processedItem);
7104
+ }
7105
+ const groupKey = group.slug || group.name;
7106
+ const itemOrderList = effectivePrefs.itemOrder?.[group.id] || effectivePrefs.itemOrder?.[groupKey];
7107
+ if (itemOrderList && itemOrderList.length > 0) {
7108
+ visibleItems.sort((a, b) => {
7109
+ const aIdx = itemOrderList.indexOf(a.id) >= 0 ? itemOrderList.indexOf(a.id) : a.slug ? itemOrderList.indexOf(a.slug) : -1;
7110
+ const bIdx = itemOrderList.indexOf(b.id) >= 0 ? itemOrderList.indexOf(b.id) : b.slug ? itemOrderList.indexOf(b.slug) : -1;
7111
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
7112
+ if (aIdx >= 0) return -1;
7113
+ if (bIdx >= 0) return 1;
7114
+ return (a.order ?? 100) - (b.order ?? 100);
7115
+ });
7116
+ } else {
7117
+ visibleItems.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
7118
+ }
7119
+ reconciledGroups.push({
7120
+ ...group,
7121
+ items: visibleItems
7122
+ });
7123
+ }
7124
+ if (effectivePrefs.groupOrder && effectivePrefs.groupOrder.length > 0) {
7125
+ reconciledGroups.sort((a, b) => {
7126
+ 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);
7127
+ 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);
7128
+ if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
7129
+ if (aIdx >= 0) return -1;
7130
+ if (bIdx >= 0) return 1;
7131
+ return (a.order ?? 100) - (b.order ?? 100);
7132
+ });
7133
+ } else {
7134
+ reconciledGroups.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
7135
+ }
7136
+ const reconciledUngrouped = (baseTree.ungrouped || []).filter((item) => {
7137
+ const isItemHidden = hiddenSet.has(item.id) || !!item.slug && hiddenSet.has(item.slug);
7138
+ return includeHidden || !isItemHidden;
7139
+ }).map((item) => {
7140
+ const allViews = item.views || [];
7141
+ const visibleViews = includeHidden ? allViews : allViews.filter((v) => {
7142
+ const compoundKey = `${item.slug}_${v.slug}`;
7143
+ return !hiddenSet.has(compoundKey) && !hiddenSet.has(v.slug);
7144
+ });
7145
+ const processed = { ...item, views: visibleViews };
7146
+ allAvailableItems.push(processed);
7147
+ return processed;
7148
+ });
7149
+ const pinnedItems = [];
7150
+ for (const pin of effectivePrefs.pinned || []) {
7151
+ const match = allAvailableItems.find((i) => i.slug === pin.slug || i.id === pin.slug);
7152
+ if (match) {
7153
+ pinnedItems.push({
7154
+ ...match,
7155
+ id: `pin_${match.id}`,
7156
+ label: pin.label || match.label,
7157
+ icon: pin.icon || match.icon
7158
+ });
7159
+ } else {
7160
+ pinnedItems.push({
7161
+ id: `pin_${pin.slug}`,
7162
+ type: pin.type,
7163
+ slug: pin.slug,
7164
+ label: pin.label || pin.slug,
7165
+ icon: pin.icon || "Star",
7166
+ order: 0,
7167
+ views: []
7168
+ });
7169
+ }
7170
+ }
7171
+ return {
7172
+ groups: reconciledGroups,
7173
+ ungrouped: reconciledUngrouped,
7174
+ pinnedItems
7175
+ };
7176
+ }
7177
+
7178
+ // src/utils/navigation.ts
7179
+ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
7180
+ "collections",
7181
+ "globals",
7182
+ "setup",
7183
+ "api",
7184
+ "login",
7185
+ "logout",
7186
+ "settings",
7187
+ "admin",
7188
+ "preferences",
7189
+ "auth"
7190
+ ]);
7191
+ function assertValidNavigationSlugs(navigation) {
7192
+ for (const item of navigation) {
7193
+ if (item.slug) {
7194
+ const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
7195
+ if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
7196
+ throw new Error(
7197
+ `[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.`
7198
+ );
7199
+ }
7200
+ }
7201
+ }
7202
+ }
7203
+ function resolveGroupMeta(groupProp, defaultGroupName = "Collections") {
7204
+ if (!groupProp) {
7205
+ return { name: defaultGroupName };
7206
+ }
7207
+ if (typeof groupProp === "string") {
7208
+ return { name: groupProp };
7209
+ }
7210
+ return { name: groupProp.name, meta: groupProp };
7211
+ }
7212
+ function compileNavigation(config) {
7213
+ const collections = config.collections || [];
7214
+ const globals = config.globals || [];
7215
+ const explicitNav = config.admin?.navigation || [];
7216
+ assertValidNavigationSlugs(explicitNav);
7217
+ const collectionsBySlug = /* @__PURE__ */ new Map();
7218
+ for (const col of collections) {
7219
+ collectionsBySlug.set(col.slug, col);
7220
+ }
7221
+ const globalsBySlug = /* @__PURE__ */ new Map();
7222
+ for (const gl of globals) {
7223
+ globalsBySlug.set(gl.slug, gl);
7224
+ }
7225
+ const mentionedCollections = /* @__PURE__ */ new Set();
7226
+ const mentionedGlobals = /* @__PURE__ */ new Set();
7227
+ const injectedViewsByCollection = /* @__PURE__ */ new Map();
7228
+ for (const item of explicitNav) {
7229
+ if (item.addToCollection) {
7230
+ const existing = injectedViewsByCollection.get(item.addToCollection) || [];
7231
+ if (item.views && item.views.length > 0) {
7232
+ injectedViewsByCollection.set(item.addToCollection, [...existing, ...item.views]);
7233
+ }
7234
+ }
7235
+ if (item.collection) mentionedCollections.add(item.collection);
7236
+ if (item.global) mentionedGlobals.add(item.global);
7237
+ }
7238
+ const groupMetadataMap = /* @__PURE__ */ new Map();
7239
+ const itemsByGroup = /* @__PURE__ */ new Map();
7240
+ const ungroupedItems = [];
7241
+ const registerItem = (groupName, item) => {
7242
+ if (!groupName) {
7243
+ ungroupedItems.push(item);
7244
+ } else {
7245
+ const list = itemsByGroup.get(groupName) || [];
7246
+ list.push(item);
7247
+ itemsByGroup.set(groupName, list);
7248
+ }
7249
+ };
7250
+ for (const item of explicitNav) {
7251
+ if (item.addToCollection && !item.slug && !item.collection && !item.global && !item.dashboard && !item.href) {
7252
+ continue;
7253
+ }
7254
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(item.group, void 0);
7255
+ if (groupName && groupMeta) {
7256
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
7257
+ }
7258
+ if (item.dashboard) {
7259
+ registerItem(groupName, {
7260
+ id: "dashboard",
7261
+ type: "dashboard",
7262
+ slug: "dashboard",
7263
+ label: item.label || "Dashboard",
7264
+ icon: item.icon || "LayoutDashboard",
7265
+ group: groupName,
7266
+ order: item.order ?? 0,
7267
+ views: [],
7268
+ access: item.access
7269
+ });
7270
+ continue;
7271
+ }
7272
+ if (item.collection) {
7273
+ const col = collectionsBySlug.get(item.collection);
7274
+ const combinedViews = [
7275
+ ...col?.views || [],
7276
+ ...injectedViewsByCollection.get(item.collection) || [],
7277
+ ...item.views || []
7278
+ ];
7279
+ registerItem(groupName, {
7280
+ id: item.slug || `collection_${item.collection}`,
7281
+ type: item.slug ? "workspace" : "collection",
7282
+ slug: item.slug || item.collection,
7283
+ collection: item.collection,
7284
+ label: item.label || col?.labels?.plural || item.collection,
7285
+ icon: item.icon || col?.admin?.icon || "Folder",
7286
+ group: groupName,
7287
+ order: item.order ?? col?.admin?.order ?? 100,
7288
+ views: combinedViews,
7289
+ badge: item.badge,
7290
+ access: item.access || col?.access?.read
7291
+ });
7292
+ continue;
7293
+ }
7294
+ if (item.global) {
7295
+ const gl = globalsBySlug.get(item.global);
7296
+ registerItem(groupName, {
7297
+ id: item.slug || `global_${item.global}`,
7298
+ type: "global",
7299
+ slug: item.slug || item.global,
7300
+ global: item.global,
7301
+ label: item.label || gl?.label || item.global,
7302
+ icon: item.icon || "Settings",
7303
+ group: groupName,
7304
+ order: item.order ?? 100,
7305
+ views: [],
7306
+ badge: item.badge,
7307
+ access: item.access
7308
+ });
7309
+ continue;
7310
+ }
7311
+ if (item.href) {
7312
+ registerItem(groupName, {
7313
+ id: `link_${item.slug || item.href}`,
7314
+ type: "link",
7315
+ slug: item.slug || "link",
7316
+ label: item.label || "Link",
7317
+ href: item.href,
7318
+ icon: item.icon || "ExternalLink",
7319
+ group: groupName,
7320
+ order: item.order ?? 100,
7321
+ views: [],
7322
+ badge: item.badge,
7323
+ access: item.access
7324
+ });
7325
+ continue;
7326
+ }
7327
+ const workspaceSlug = item.slug || "workspace";
7328
+ registerItem(groupName, {
7329
+ id: `workspace_${workspaceSlug}`,
7330
+ type: "workspace",
7331
+ slug: workspaceSlug,
7332
+ label: item.label || workspaceSlug,
7333
+ icon: item.icon || "Briefcase",
7334
+ group: groupName,
7335
+ order: item.order ?? 100,
7336
+ views: item.views || [],
7337
+ badge: item.badge,
7338
+ access: item.access
7339
+ });
7340
+ }
7341
+ for (const col of collections) {
7342
+ if (mentionedCollections.has(col.slug)) continue;
7343
+ if (col.admin?.hidden) continue;
7344
+ const groupProp = col.admin?.group;
7345
+ const { name: groupName, meta: groupMeta } = resolveGroupMeta(groupProp, "Collections");
7346
+ if (groupMeta) {
7347
+ groupMetadataMap.set(groupName, { ...groupMetadataMap.get(groupName) || {}, ...groupMeta });
7348
+ }
7349
+ const combinedViews = [
7350
+ ...col.views || [],
7351
+ ...injectedViewsByCollection.get(col.slug) || []
7352
+ ];
7353
+ registerItem(groupName, {
7354
+ id: `collection_${col.slug}`,
7355
+ type: "collection",
7356
+ slug: col.slug,
7357
+ collection: col.slug,
7358
+ label: col.labels?.plural || col.slug,
7359
+ icon: col.admin?.icon || "Folder",
7360
+ group: groupName,
7361
+ order: col?.admin?.order ?? 100,
7362
+ views: combinedViews,
7363
+ access: col.access?.read
7364
+ });
7365
+ }
7366
+ for (const gl of globals) {
7367
+ if (mentionedGlobals.has(gl.slug)) continue;
7368
+ const groupName = "Configuration";
7369
+ registerItem(groupName, {
7370
+ id: `global_${gl.slug}`,
7371
+ type: "global",
7372
+ slug: gl.slug,
7373
+ global: gl.slug,
7374
+ label: gl.label || gl.slug,
7375
+ icon: "Settings",
7376
+ group: groupName,
7377
+ order: 100,
7378
+ views: []
7379
+ });
7380
+ }
7381
+ const sortAndSpliceItems = (items) => {
7382
+ const sorted = [...items].sort((a, b) => a.order - b.order);
7383
+ const explicitMap = /* @__PURE__ */ new Map();
7384
+ for (const opt of explicitNav) {
7385
+ if (opt.slug) explicitMap.set(opt.slug, opt);
7386
+ if (opt.collection) explicitMap.set(opt.collection, opt);
7387
+ if (opt.global) explicitMap.set(opt.global, opt);
7388
+ }
7389
+ for (const opt of explicitNav) {
7390
+ const itemKey = opt.slug || opt.collection || opt.global;
7391
+ if (!itemKey) continue;
7392
+ if (opt.after) {
7393
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7394
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.after);
7395
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7396
+ const [moved] = sorted.splice(itemIdx, 1);
7397
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.after);
7398
+ sorted.splice(newTargetIdx + 1, 0, moved);
7399
+ }
7400
+ } else if (opt.before) {
7401
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7402
+ const targetIdx = sorted.findIndex((i) => i.slug === opt.before);
7403
+ if (itemIdx !== -1 && targetIdx !== -1 && itemIdx !== targetIdx) {
7404
+ const [moved] = sorted.splice(itemIdx, 1);
7405
+ const newTargetIdx = sorted.findIndex((i) => i.slug === opt.before);
7406
+ sorted.splice(newTargetIdx, 0, moved);
7407
+ }
7408
+ } else if (opt.position === "first") {
7409
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7410
+ if (itemIdx > 0) {
7411
+ const [moved] = sorted.splice(itemIdx, 1);
7412
+ sorted.unshift(moved);
7413
+ }
7414
+ } else if (opt.position === "last") {
7415
+ const itemIdx = sorted.findIndex((i) => i.slug === itemKey);
7416
+ if (itemIdx !== -1 && itemIdx < sorted.length - 1) {
7417
+ const [moved] = sorted.splice(itemIdx, 1);
7418
+ sorted.push(moved);
7419
+ }
7420
+ }
7421
+ }
7422
+ return sorted;
7423
+ };
7424
+ const compiledGroups = [];
7425
+ for (const [groupName, rawItems] of itemsByGroup.entries()) {
7426
+ const meta = groupMetadataMap.get(groupName);
7427
+ const sortedItems = sortAndSpliceItems(rawItems);
7428
+ compiledGroups.push({
7429
+ id: `group_${groupName.toLowerCase().replace(/\s+/g, "_")}`,
7430
+ name: groupName,
7431
+ slug: meta?.slug || groupName.toLowerCase().replace(/\s+/g, "-"),
7432
+ icon: meta?.icon,
7433
+ defaultExpanded: meta?.defaultExpanded ?? true,
7434
+ order: meta?.order ?? 100,
7435
+ items: sortedItems
7436
+ });
7437
+ }
7438
+ compiledGroups.sort((a, b) => a.order - b.order);
7439
+ return {
7440
+ groups: compiledGroups,
7441
+ ungrouped: sortAndSpliceItems(ungroupedItems)
7442
+ };
7443
+ }
7444
+ function pruneNavigationForUser(tree, user) {
7445
+ const userRoles = /* @__PURE__ */ new Set();
7446
+ if (user?.role) userRoles.add(user.role);
7447
+ if (Array.isArray(user?.roles)) {
7448
+ for (const r of user.roles) userRoles.add(r);
7449
+ }
7450
+ const isAllowed = (item) => {
7451
+ if (!item.access) return true;
7452
+ if (Array.isArray(item.access)) {
7453
+ if (item.access.length === 0) return true;
7454
+ return item.access.some((role) => userRoles.has(role));
7455
+ }
7456
+ return true;
7457
+ };
7458
+ const prunedGroups = [];
7459
+ for (const group of tree.groups) {
7460
+ const visibleItems = group.items.filter(isAllowed);
7461
+ if (visibleItems.length > 0) {
7462
+ prunedGroups.push({
7463
+ ...group,
7464
+ items: visibleItems
7465
+ });
7466
+ }
7467
+ }
7468
+ return {
7469
+ groups: prunedGroups,
7470
+ ungrouped: tree.ungrouped.filter(isAllowed)
7471
+ };
7472
+ }
7473
+
6531
7474
  // src/index.ts
6532
7475
  import { z } from "zod";
6533
7476
  function defineCollection(config) {
@@ -6552,6 +7495,7 @@ var defineTextField = createFieldDefiner("text");
6552
7495
  var defineTextareaField = createFieldDefiner("textarea");
6553
7496
  var defineRichTextField = createFieldDefiner("richText");
6554
7497
  var defineNumberField = createFieldDefiner("number");
7498
+ var defineMoneyField = createFieldDefiner("money");
6555
7499
  var defineBooleanField = createFieldDefiner("boolean");
6556
7500
  var defineDateField = createFieldDefiner("date");
6557
7501
  var defineDateTimeField = createFieldDefiner("datetime");
@@ -6594,8 +7538,10 @@ export {
6594
7538
  BASE62_ALPHABET,
6595
7539
  BUILTIN_JEXL_HELPERS,
6596
7540
  CROCKFORD_BASE32,
7541
+ CURRENT_NAV_PREFERENCES_VERSION,
6597
7542
  ConfigValidationError,
6598
7543
  ConflictError,
7544
+ DEFAULT_USER_NAV_PREFERENCES,
6599
7545
  DuplicateKeyError,
6600
7546
  DyrectedAIError,
6601
7547
  DyrectedError,
@@ -6608,6 +7554,10 @@ export {
6608
7554
  NON_NEGOTIABLE_CREDENTIAL_FIELDS,
6609
7555
  NotFoundError,
6610
7556
  PII_REGEX_PATTERNS,
7557
+ RESERVED_NAVIGATION_SLUGS,
7558
+ TASK_LOCKS_COLLECTION,
7559
+ TASK_LOCKS_COLLECTION_CONFIG,
7560
+ UNASSIGNED_GROUP_KEY,
6611
7561
  ValidationError,
6612
7562
  WORKFLOW_HISTORY_COLLECTION,
6613
7563
  abs,
@@ -6618,6 +7568,7 @@ export {
6618
7568
  assertValidDeclarativeHookExpression,
6619
7569
  assertValidDeclarativeHookResult,
6620
7570
  assertValidDeclarativeHooksInConfig,
7571
+ assertValidNavigationSlugs,
6621
7572
  assertValidPreviewUrlsInConfig,
6622
7573
  availableWorkflowTransitions,
6623
7574
  buildSqlOrderBy,
@@ -6626,14 +7577,17 @@ export {
6626
7577
  ceil,
6627
7578
  clamp,
6628
7579
  coalesce,
7580
+ coerceBooleanWhere,
6629
7581
  collectAdminConditionValidationIssues,
6630
7582
  collectConfigDiagnostics,
6631
7583
  collectDeclarativeAccessValidationIssues,
6632
7584
  collectDeclarativeHookValidationIssues,
6633
7585
  collectPreviewUrlValidationIssues,
6634
7586
  compact,
7587
+ compileNavigation,
6635
7588
  count,
6636
7589
  createLifecycleEvent,
7590
+ createTaskRunner,
6637
7591
  createWorkflowDocument,
6638
7592
  defaultValue,
6639
7593
  defineAction,
@@ -6656,6 +7610,7 @@ export {
6656
7610
  defineJoinField,
6657
7611
  defineJsonField,
6658
7612
  defineKanbanView,
7613
+ defineMoneyField,
6659
7614
  defineMultiSelectField,
6660
7615
  defineNumberField,
6661
7616
  defineObjectField,
@@ -6667,11 +7622,13 @@ export {
6667
7622
  defineSelectField,
6668
7623
  defineTab,
6669
7624
  defineTableView,
7625
+ defineTask,
6670
7626
  defineTextField,
6671
7627
  defineTextareaField,
6672
7628
  defineTimeField,
6673
7629
  defineUrlField,
6674
7630
  defineView,
7631
+ defineWorkspace,
6675
7632
  diffDays,
6676
7633
  dispatchLifecycleEvent,
6677
7634
  dispatchPendingLifecycleEvents,
@@ -6729,22 +7686,28 @@ export {
6729
7686
  max,
6730
7687
  min,
6731
7688
  nanoid,
7689
+ nextCronRun,
6732
7690
  normalizeConfig,
6733
7691
  normalizeDetailItem,
7692
+ normalizeGroupKey,
6734
7693
  now,
6735
7694
  numericFieldNames,
7695
+ parseCron,
6736
7696
  parseMongoWhere,
6737
7697
  parseSort,
6738
7698
  parseSqlWhere,
6739
7699
  pow,
6740
7700
  prefixedId,
7701
+ pruneNavigationForUser,
6741
7702
  publishedStateName,
6742
7703
  publishingWorkflow,
6743
7704
  readingTime,
7705
+ reconcileNavigation,
6744
7706
  registerJexlHelpers,
6745
7707
  replace,
6746
7708
  resolveAdminAuthCollection,
6747
7709
  resolveAuthorizedSiteId,
7710
+ resolveIndexName,
6748
7711
  resolvePrefix,
6749
7712
  round,
6750
7713
  runCollectionHooks,