@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/{app-KSDgfXsw.d.cts → app-Dd9cBbCs.d.cts} +1460 -1184
- package/dist/{app-KSDgfXsw.d.ts → app-Dd9cBbCs.d.ts} +1460 -1184
- package/dist/index.cjs +988 -6
- package/dist/index.d.cts +155 -4
- package/dist/index.d.ts +155 -4
- package/dist/index.js +969 -6
- package/dist/server.cjs +1079 -40
- package/dist/server.d.cts +20 -4
- package/dist/server.d.ts +20 -4
- package/dist/server.js +1078 -40
- package/package.json +1 -1
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
|
-
|
|
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,
|