@dyrected/core 2.17.0 → 2.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-CLdT_0se.d.cts → app-x7J5BIqK.d.cts} +171 -5
- package/dist/{app-CLdT_0se.d.ts → app-x7J5BIqK.d.ts} +171 -5
- package/dist/index.cjs +436 -6
- package/dist/index.d.cts +105 -4
- package/dist/index.d.ts +105 -4
- package/dist/index.js +424 -6
- package/dist/server.cjs +3526 -3312
- package/dist/server.d.cts +20 -4
- package/dist/server.d.ts +20 -4
- package/dist/server.js +3525 -3312
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1866,6 +1866,296 @@ import { SignJWT, jwtVerify, decodeJwt } from "jose";
|
|
|
1866
1866
|
// src/auth/sessions.ts
|
|
1867
1867
|
var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
|
|
1868
1868
|
|
|
1869
|
+
// src/tasks.ts
|
|
1870
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
1871
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
1872
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
1873
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
1874
|
+
fields: [
|
|
1875
|
+
{ name: "name", type: "text", required: true },
|
|
1876
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
1877
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
1878
|
+
{ name: "lockedBy", type: "text" },
|
|
1879
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
1880
|
+
{ name: "lastStartedAt", type: "date" },
|
|
1881
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
1882
|
+
{ name: "lastStatus", type: "text" },
|
|
1883
|
+
{ name: "lastError", type: "textarea" }
|
|
1884
|
+
],
|
|
1885
|
+
access: {
|
|
1886
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
1887
|
+
create: () => false,
|
|
1888
|
+
update: () => false,
|
|
1889
|
+
delete: () => false
|
|
1890
|
+
},
|
|
1891
|
+
admin: { hidden: true }
|
|
1892
|
+
};
|
|
1893
|
+
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
1894
|
+
var DEFAULT_TICK_MS = 3e4;
|
|
1895
|
+
var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
1896
|
+
var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
1897
|
+
function parseCronField(raw, min2, max2, names, nameOffset, label) {
|
|
1898
|
+
const values = /* @__PURE__ */ new Set();
|
|
1899
|
+
const toNumber = (token) => {
|
|
1900
|
+
const named = names?.indexOf(token.toLowerCase());
|
|
1901
|
+
if (named !== void 0 && named >= 0) return named + nameOffset;
|
|
1902
|
+
if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
|
|
1903
|
+
return Number(token);
|
|
1904
|
+
};
|
|
1905
|
+
for (const part of raw.split(",")) {
|
|
1906
|
+
const [rangePart, stepPart] = part.split("/");
|
|
1907
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
1908
|
+
if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
|
|
1909
|
+
let start;
|
|
1910
|
+
let end;
|
|
1911
|
+
if (rangePart === "*") {
|
|
1912
|
+
start = min2;
|
|
1913
|
+
end = max2;
|
|
1914
|
+
} else if (rangePart.includes("-")) {
|
|
1915
|
+
const [a, b] = rangePart.split("-");
|
|
1916
|
+
start = toNumber(a);
|
|
1917
|
+
end = toNumber(b);
|
|
1918
|
+
} else {
|
|
1919
|
+
start = toNumber(rangePart);
|
|
1920
|
+
end = stepPart === void 0 ? start : max2;
|
|
1921
|
+
}
|
|
1922
|
+
if (start < min2 || end > max2 || start > end) {
|
|
1923
|
+
throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
|
|
1924
|
+
}
|
|
1925
|
+
for (let v = start; v <= end; v += step) values.add(v);
|
|
1926
|
+
}
|
|
1927
|
+
return values;
|
|
1928
|
+
}
|
|
1929
|
+
function parseCron(expression) {
|
|
1930
|
+
const fields = expression.trim().split(/\s+/);
|
|
1931
|
+
if (fields.length !== 5) {
|
|
1932
|
+
throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
|
|
1933
|
+
}
|
|
1934
|
+
const [min2, hour, dom, mon, dow] = fields;
|
|
1935
|
+
const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
|
|
1936
|
+
if (daysOfWeek.has(7)) daysOfWeek.add(0);
|
|
1937
|
+
daysOfWeek.delete(7);
|
|
1938
|
+
return {
|
|
1939
|
+
minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
|
|
1940
|
+
hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
|
|
1941
|
+
daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
|
|
1942
|
+
months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
|
|
1943
|
+
daysOfWeek,
|
|
1944
|
+
dayFieldsBothRestricted: dom !== "*" && dow !== "*"
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
function dayMatches(schedule, date) {
|
|
1948
|
+
const domOk = schedule.daysOfMonth.has(date.getUTCDate());
|
|
1949
|
+
const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
|
|
1950
|
+
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
1951
|
+
}
|
|
1952
|
+
function nextCronRun(expression, after) {
|
|
1953
|
+
const schedule = typeof expression === "string" ? parseCron(expression) : expression;
|
|
1954
|
+
const cursor = new Date(after.getTime());
|
|
1955
|
+
cursor.setUTCSeconds(0, 0);
|
|
1956
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
|
|
1957
|
+
const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
1958
|
+
while (cursor.getTime() <= limit) {
|
|
1959
|
+
if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
|
|
1960
|
+
cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
|
|
1961
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
if (!dayMatches(schedule, cursor)) {
|
|
1965
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
1966
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
if (!schedule.hours.has(cursor.getUTCHours())) {
|
|
1970
|
+
cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
if (!schedule.minutes.has(cursor.getUTCMinutes())) {
|
|
1974
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
|
|
1975
|
+
continue;
|
|
1976
|
+
}
|
|
1977
|
+
return cursor;
|
|
1978
|
+
}
|
|
1979
|
+
throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
|
|
1980
|
+
}
|
|
1981
|
+
function defineTask(task) {
|
|
1982
|
+
if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
|
|
1983
|
+
if (task.cron !== void 0) parseCron(task.cron);
|
|
1984
|
+
if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
|
|
1985
|
+
throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
|
|
1986
|
+
}
|
|
1987
|
+
return task;
|
|
1988
|
+
}
|
|
1989
|
+
function resolveLogger(config, taskName) {
|
|
1990
|
+
const base = config.logger;
|
|
1991
|
+
if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
|
|
1992
|
+
const prefix = `[dyrected/tasks] ${taskName}`;
|
|
1993
|
+
return {
|
|
1994
|
+
info: (obj, msg) => console.info(prefix, msg ?? "", obj),
|
|
1995
|
+
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
1996
|
+
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
function floorToMinute(date) {
|
|
2000
|
+
const d = new Date(date.getTime());
|
|
2001
|
+
d.setUTCSeconds(0, 0);
|
|
2002
|
+
return d;
|
|
2003
|
+
}
|
|
2004
|
+
function createTaskRunner(config) {
|
|
2005
|
+
const db = config.db;
|
|
2006
|
+
if (!db) throw new Error("createTaskRunner requires config.db.");
|
|
2007
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
2008
|
+
for (const task of config.tasks ?? []) {
|
|
2009
|
+
if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
|
|
2010
|
+
tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
|
|
2011
|
+
}
|
|
2012
|
+
const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
2013
|
+
let timer;
|
|
2014
|
+
let inFlight = Promise.resolve();
|
|
2015
|
+
let ticking = false;
|
|
2016
|
+
let ready;
|
|
2017
|
+
function ensureReady() {
|
|
2018
|
+
ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
|
|
2019
|
+
ready.catch(() => {
|
|
2020
|
+
ready = void 0;
|
|
2021
|
+
});
|
|
2022
|
+
return ready;
|
|
2023
|
+
}
|
|
2024
|
+
async function ensureLockRow(name, now2) {
|
|
2025
|
+
await ensureReady();
|
|
2026
|
+
const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2027
|
+
if (existing) return existing;
|
|
2028
|
+
try {
|
|
2029
|
+
return await db.create({
|
|
2030
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2031
|
+
data: {
|
|
2032
|
+
id: name,
|
|
2033
|
+
name,
|
|
2034
|
+
lockedUntil: 0,
|
|
2035
|
+
lockedBy: null,
|
|
2036
|
+
lastScheduledFor: floorToMinute(now2).toISOString(),
|
|
2037
|
+
lastStartedAt: null,
|
|
2038
|
+
lastFinishedAt: null,
|
|
2039
|
+
lastStatus: null,
|
|
2040
|
+
lastError: null
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2045
|
+
if (created) return created;
|
|
2046
|
+
throw error;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
async function execute(task, row, scheduledFor) {
|
|
2050
|
+
const started = Date.now();
|
|
2051
|
+
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
2052
|
+
const token = `${instanceId}:${started}`;
|
|
2053
|
+
const where = {
|
|
2054
|
+
id: { equals: task.name },
|
|
2055
|
+
lockedUntil: { less_than: started }
|
|
2056
|
+
};
|
|
2057
|
+
if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
|
|
2058
|
+
const claimed = await db.update({
|
|
2059
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2060
|
+
where,
|
|
2061
|
+
data: {
|
|
2062
|
+
lockedUntil: started + lockMs,
|
|
2063
|
+
lockedBy: token,
|
|
2064
|
+
lastStartedAt: new Date(started).toISOString(),
|
|
2065
|
+
...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
if (claimed.affectedRows !== 1) {
|
|
2069
|
+
return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
|
|
2070
|
+
}
|
|
2071
|
+
const logger = resolveLogger(config, task.name);
|
|
2072
|
+
const controller = new AbortController();
|
|
2073
|
+
let timeout;
|
|
2074
|
+
let status = "completed";
|
|
2075
|
+
let errorMessage;
|
|
2076
|
+
try {
|
|
2077
|
+
await Promise.race([
|
|
2078
|
+
Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
|
|
2079
|
+
new Promise((_, reject) => {
|
|
2080
|
+
timeout = setTimeout(() => {
|
|
2081
|
+
reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
|
|
2082
|
+
controller.abort();
|
|
2083
|
+
}, lockMs);
|
|
2084
|
+
})
|
|
2085
|
+
]);
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
status = "failed";
|
|
2088
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2089
|
+
logger.error({ err: error }, "Task failed");
|
|
2090
|
+
} finally {
|
|
2091
|
+
if (timeout) clearTimeout(timeout);
|
|
2092
|
+
}
|
|
2093
|
+
try {
|
|
2094
|
+
await db.update({
|
|
2095
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2096
|
+
where: { id: { equals: task.name }, lockedBy: { equals: token } },
|
|
2097
|
+
data: {
|
|
2098
|
+
lockedUntil: 0,
|
|
2099
|
+
lockedBy: null,
|
|
2100
|
+
lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2101
|
+
lastStatus: status,
|
|
2102
|
+
lastError: errorMessage ?? null
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
|
|
2107
|
+
}
|
|
2108
|
+
return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
|
|
2109
|
+
}
|
|
2110
|
+
function track(work) {
|
|
2111
|
+
inFlight = inFlight.then(() => work).catch(() => void 0);
|
|
2112
|
+
return work;
|
|
2113
|
+
}
|
|
2114
|
+
const runner = {
|
|
2115
|
+
async runTask(name) {
|
|
2116
|
+
const task = tasks.get(name);
|
|
2117
|
+
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
2118
|
+
const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
2119
|
+
return track(execute(task, row, void 0));
|
|
2120
|
+
},
|
|
2121
|
+
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
2122
|
+
const results = [];
|
|
2123
|
+
for (const task of tasks.values()) {
|
|
2124
|
+
if (!task.schedule) continue;
|
|
2125
|
+
const row = await ensureLockRow(task.name, now2);
|
|
2126
|
+
const reference = new Date(String(row.lastScheduledFor));
|
|
2127
|
+
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
2128
|
+
if (!due) {
|
|
2129
|
+
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
results.push(await track(execute(task, row, floorToMinute(now2))));
|
|
2133
|
+
}
|
|
2134
|
+
return results;
|
|
2135
|
+
},
|
|
2136
|
+
start(options) {
|
|
2137
|
+
if (timer) return;
|
|
2138
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
|
|
2139
|
+
const tick = () => {
|
|
2140
|
+
if (ticking) return;
|
|
2141
|
+
ticking = true;
|
|
2142
|
+
runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
|
|
2143
|
+
ticking = false;
|
|
2144
|
+
});
|
|
2145
|
+
};
|
|
2146
|
+
timer = setInterval(tick, intervalMs);
|
|
2147
|
+
timer.unref?.();
|
|
2148
|
+
setTimeout(tick, 0);
|
|
2149
|
+
},
|
|
2150
|
+
async stop() {
|
|
2151
|
+
if (timer) clearInterval(timer);
|
|
2152
|
+
timer = void 0;
|
|
2153
|
+
await inFlight;
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
2156
|
+
return runner;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1869
2159
|
// src/workflows.ts
|
|
1870
2160
|
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
1871
2161
|
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
@@ -2081,7 +2371,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
|
|
|
2081
2371
|
return due.length;
|
|
2082
2372
|
}
|
|
2083
2373
|
async function saveWorkflowDraft(args) {
|
|
2084
|
-
const { config, collection, id, originalDoc, data, user } = args;
|
|
2374
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
2085
2375
|
const db = config.db;
|
|
2086
2376
|
const workflow = collection.workflow;
|
|
2087
2377
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -2115,13 +2405,14 @@ async function saveWorkflowDraft(args) {
|
|
|
2115
2405
|
}
|
|
2116
2406
|
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
2117
2407
|
await persistEvent(tx, event);
|
|
2408
|
+
await beforeCommit?.(tx, updated);
|
|
2118
2409
|
return updated;
|
|
2119
2410
|
});
|
|
2120
2411
|
void dispatchLifecycleEvent(config, event);
|
|
2121
2412
|
return { doc, event };
|
|
2122
2413
|
}
|
|
2123
2414
|
async function createWorkflowDocument(args) {
|
|
2124
|
-
const { config, collection, data, user } = args;
|
|
2415
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
2125
2416
|
const db = config.db;
|
|
2126
2417
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
2127
2418
|
let event;
|
|
@@ -2135,13 +2426,14 @@ async function createWorkflowDocument(args) {
|
|
|
2135
2426
|
payload: { revision: 1, previousRevision: null }
|
|
2136
2427
|
});
|
|
2137
2428
|
await persistEvent(tx, event);
|
|
2429
|
+
await beforeCommit?.(tx, created);
|
|
2138
2430
|
return created;
|
|
2139
2431
|
});
|
|
2140
2432
|
void dispatchLifecycleEvent(config, event);
|
|
2141
2433
|
return { doc, event };
|
|
2142
2434
|
}
|
|
2143
2435
|
async function transitionWorkflow(args) {
|
|
2144
|
-
const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
|
|
2436
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
2145
2437
|
const db = config.db;
|
|
2146
2438
|
const workflow = collection.workflow;
|
|
2147
2439
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -2167,7 +2459,7 @@ async function transitionWorkflow(args) {
|
|
|
2167
2459
|
if (transition.requireComment && !comment?.trim()) {
|
|
2168
2460
|
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
2169
2461
|
}
|
|
2170
|
-
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
|
|
2462
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
2171
2463
|
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
2172
2464
|
const events = [
|
|
2173
2465
|
createLifecycleEvent({
|
|
@@ -2221,6 +2513,19 @@ async function transitionWorkflow(args) {
|
|
|
2221
2513
|
if (targetState.published) data.__published = working;
|
|
2222
2514
|
if (transition.unpublish) data.__published = null;
|
|
2223
2515
|
const next = await tx.update({ collection: collection.slug, id, data });
|
|
2516
|
+
if (transition.onTransition) {
|
|
2517
|
+
await transition.onTransition({
|
|
2518
|
+
transition,
|
|
2519
|
+
from: lockedMeta.state,
|
|
2520
|
+
to: transition.to,
|
|
2521
|
+
doc: locked,
|
|
2522
|
+
input,
|
|
2523
|
+
comment,
|
|
2524
|
+
user,
|
|
2525
|
+
req,
|
|
2526
|
+
tx
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2224
2529
|
await tx.create({
|
|
2225
2530
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
2226
2531
|
data: {
|
|
@@ -3657,6 +3962,7 @@ function normalizeConfig(config) {
|
|
|
3657
3962
|
const globals = schemaAwareConfig?.globals || [];
|
|
3658
3963
|
const needsAudit = collections.some((col) => col.audit);
|
|
3659
3964
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
3965
|
+
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
3660
3966
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
3661
3967
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
3662
3968
|
collections,
|
|
@@ -3889,6 +4195,9 @@ function normalizeConfig(config) {
|
|
|
3889
4195
|
if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
|
|
3890
4196
|
systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
|
|
3891
4197
|
}
|
|
4198
|
+
if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
|
|
4199
|
+
systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
|
|
4200
|
+
}
|
|
3892
4201
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
3893
4202
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
3894
4203
|
}
|
|
@@ -3973,7 +4282,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
|
|
|
3973
4282
|
params.push("");
|
|
3974
4283
|
return `(${c})::text = ${next()}`;
|
|
3975
4284
|
}
|
|
3976
|
-
|
|
4285
|
+
if (typeof value === "boolean") {
|
|
4286
|
+
params.push(placeholder === "?" ? value ? 1 : 0 : isJsonExtract ? String(value) : value);
|
|
4287
|
+
} else {
|
|
4288
|
+
params.push(value);
|
|
4289
|
+
}
|
|
3977
4290
|
return `${c} = ${next()}`;
|
|
3978
4291
|
}
|
|
3979
4292
|
const entries = Object.entries(value);
|
|
@@ -4173,6 +4486,38 @@ function parseMongoWhere(where) {
|
|
|
4173
4486
|
}
|
|
4174
4487
|
return buildClause(where);
|
|
4175
4488
|
}
|
|
4489
|
+
function coerceBooleanWhere(where, fields) {
|
|
4490
|
+
const booleanFields = new Set(
|
|
4491
|
+
(fields ?? []).filter((f) => f.type === "boolean" && f.name).map((f) => f.name)
|
|
4492
|
+
);
|
|
4493
|
+
if (booleanFields.size === 0) return where;
|
|
4494
|
+
const toBool = (v) => v === "true" ? true : v === "false" ? false : v;
|
|
4495
|
+
const walk = (node) => {
|
|
4496
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
4497
|
+
if (!node || typeof node !== "object") return node;
|
|
4498
|
+
const out = {};
|
|
4499
|
+
for (const [key, value] of Object.entries(node)) {
|
|
4500
|
+
if (key.toUpperCase() === "AND" || key.toUpperCase() === "OR") {
|
|
4501
|
+
out[key] = walk(value);
|
|
4502
|
+
} else if (booleanFields.has(key)) {
|
|
4503
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4504
|
+
out[key] = Object.fromEntries(
|
|
4505
|
+
Object.entries(value).map(([op, operand]) => [
|
|
4506
|
+
op,
|
|
4507
|
+
Array.isArray(operand) ? operand.map(toBool) : toBool(operand)
|
|
4508
|
+
])
|
|
4509
|
+
);
|
|
4510
|
+
} else {
|
|
4511
|
+
out[key] = toBool(value);
|
|
4512
|
+
}
|
|
4513
|
+
} else {
|
|
4514
|
+
out[key] = value;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
return out;
|
|
4518
|
+
};
|
|
4519
|
+
return walk(where);
|
|
4520
|
+
}
|
|
4176
4521
|
|
|
4177
4522
|
// src/utils/parse-sort.ts
|
|
4178
4523
|
var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
|
|
@@ -4454,7 +4799,12 @@ function generateOpenApi(config) {
|
|
|
4454
4799
|
type: "object",
|
|
4455
4800
|
properties: {
|
|
4456
4801
|
expectedRevision: { type: "integer", minimum: 1 },
|
|
4457
|
-
comment: { type: "string" }
|
|
4802
|
+
comment: { type: "string" },
|
|
4803
|
+
input: {
|
|
4804
|
+
type: "object",
|
|
4805
|
+
additionalProperties: true,
|
|
4806
|
+
description: "Extra data passed to the transition's onTransition handler."
|
|
4807
|
+
}
|
|
4458
4808
|
}
|
|
4459
4809
|
},
|
|
4460
4810
|
WorkflowHistoryEntry: {
|
|
@@ -5617,6 +5967,9 @@ function fieldToSchema(field) {
|
|
|
5617
5967
|
case "number":
|
|
5618
5968
|
schema = { type: "number" };
|
|
5619
5969
|
break;
|
|
5970
|
+
case "money":
|
|
5971
|
+
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
5972
|
+
break;
|
|
5620
5973
|
case "boolean":
|
|
5621
5974
|
schema = { type: "boolean" };
|
|
5622
5975
|
break;
|
|
@@ -6569,6 +6922,36 @@ function sanitizeDocForAI(options) {
|
|
|
6569
6922
|
return result;
|
|
6570
6923
|
}
|
|
6571
6924
|
|
|
6925
|
+
// src/utils/index-name.ts
|
|
6926
|
+
import { createHash } from "crypto";
|
|
6927
|
+
var MAX_INDEX_NAME_LENGTH = 60;
|
|
6928
|
+
function resolveIndexName(prefix, table, fields) {
|
|
6929
|
+
const full = `${prefix}_${table}_${fields.join("_")}`;
|
|
6930
|
+
if (full.length <= MAX_INDEX_NAME_LENGTH) return full;
|
|
6931
|
+
const hash = createHash("sha1").update(full).digest("hex").slice(0, 10);
|
|
6932
|
+
const tail = (fields[fields.length - 1] ?? "").slice(0, 30);
|
|
6933
|
+
return `${prefix}_${hash}_${tail}`;
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
// src/utils/group-key.ts
|
|
6937
|
+
var UNASSIGNED_GROUP_KEY = "__unassigned__";
|
|
6938
|
+
var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
6939
|
+
function normalizeGroupKey(raw, fieldType) {
|
|
6940
|
+
if (raw === null || raw === void 0) return UNASSIGNED_GROUP_KEY;
|
|
6941
|
+
if (fieldType === "boolean") {
|
|
6942
|
+
if (raw === true || raw === 1 || raw === "1" || raw === "true" || raw === "t") return "true";
|
|
6943
|
+
if (raw === false || raw === 0 || raw === "0" || raw === "false" || raw === "f") return "false";
|
|
6944
|
+
}
|
|
6945
|
+
if ((fieldType === "number" || fieldType === "money") && raw !== "" && !Number.isNaN(Number(raw))) {
|
|
6946
|
+
return String(Number(raw));
|
|
6947
|
+
}
|
|
6948
|
+
if (fieldType === "date" || fieldType === "datetime") {
|
|
6949
|
+
if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? String(raw) : raw.toISOString();
|
|
6950
|
+
if (typeof raw === "string" && MYSQL_DATETIME.test(raw)) return `${raw.replace(" ", "T")}Z`;
|
|
6951
|
+
}
|
|
6952
|
+
return String(raw);
|
|
6953
|
+
}
|
|
6954
|
+
|
|
6572
6955
|
// src/utils/navigation-reconciler.ts
|
|
6573
6956
|
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
6574
6957
|
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
@@ -7087,6 +7470,28 @@ function pruneNavigationForUser(tree, user) {
|
|
|
7087
7470
|
ungrouped: tree.ungrouped.filter(isAllowed)
|
|
7088
7471
|
};
|
|
7089
7472
|
}
|
|
7473
|
+
function resolveAllCollectionViews(collection, config) {
|
|
7474
|
+
const views = [...collection.views ?? []];
|
|
7475
|
+
const seenSlugs = new Set(views.map((v) => v.slug));
|
|
7476
|
+
const explicitNav = config?.admin?.navigation;
|
|
7477
|
+
if (Array.isArray(explicitNav)) {
|
|
7478
|
+
for (const item of explicitNav) {
|
|
7479
|
+
if (!item) continue;
|
|
7480
|
+
const itemCol = item.collection || item.addToCollection;
|
|
7481
|
+
if (Array.isArray(item.views)) {
|
|
7482
|
+
for (const view of item.views) {
|
|
7483
|
+
if (!view || !view.slug) continue;
|
|
7484
|
+
const targetCol = view.collection || itemCol;
|
|
7485
|
+
if (targetCol === collection.slug && !seenSlugs.has(view.slug)) {
|
|
7486
|
+
views.push(view);
|
|
7487
|
+
seenSlugs.add(view.slug);
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
return views;
|
|
7494
|
+
}
|
|
7090
7495
|
|
|
7091
7496
|
// src/index.ts
|
|
7092
7497
|
import { z } from "zod";
|
|
@@ -7112,6 +7517,7 @@ var defineTextField = createFieldDefiner("text");
|
|
|
7112
7517
|
var defineTextareaField = createFieldDefiner("textarea");
|
|
7113
7518
|
var defineRichTextField = createFieldDefiner("richText");
|
|
7114
7519
|
var defineNumberField = createFieldDefiner("number");
|
|
7520
|
+
var defineMoneyField = createFieldDefiner("money");
|
|
7115
7521
|
var defineBooleanField = createFieldDefiner("boolean");
|
|
7116
7522
|
var defineDateField = createFieldDefiner("date");
|
|
7117
7523
|
var defineDateTimeField = createFieldDefiner("datetime");
|
|
@@ -7171,6 +7577,9 @@ export {
|
|
|
7171
7577
|
NotFoundError,
|
|
7172
7578
|
PII_REGEX_PATTERNS,
|
|
7173
7579
|
RESERVED_NAVIGATION_SLUGS,
|
|
7580
|
+
TASK_LOCKS_COLLECTION,
|
|
7581
|
+
TASK_LOCKS_COLLECTION_CONFIG,
|
|
7582
|
+
UNASSIGNED_GROUP_KEY,
|
|
7174
7583
|
ValidationError,
|
|
7175
7584
|
WORKFLOW_HISTORY_COLLECTION,
|
|
7176
7585
|
abs,
|
|
@@ -7190,6 +7599,7 @@ export {
|
|
|
7190
7599
|
ceil,
|
|
7191
7600
|
clamp,
|
|
7192
7601
|
coalesce,
|
|
7602
|
+
coerceBooleanWhere,
|
|
7193
7603
|
collectAdminConditionValidationIssues,
|
|
7194
7604
|
collectConfigDiagnostics,
|
|
7195
7605
|
collectDeclarativeAccessValidationIssues,
|
|
@@ -7199,6 +7609,7 @@ export {
|
|
|
7199
7609
|
compileNavigation,
|
|
7200
7610
|
count,
|
|
7201
7611
|
createLifecycleEvent,
|
|
7612
|
+
createTaskRunner,
|
|
7202
7613
|
createWorkflowDocument,
|
|
7203
7614
|
defaultValue,
|
|
7204
7615
|
defineAction,
|
|
@@ -7221,6 +7632,7 @@ export {
|
|
|
7221
7632
|
defineJoinField,
|
|
7222
7633
|
defineJsonField,
|
|
7223
7634
|
defineKanbanView,
|
|
7635
|
+
defineMoneyField,
|
|
7224
7636
|
defineMultiSelectField,
|
|
7225
7637
|
defineNumberField,
|
|
7226
7638
|
defineObjectField,
|
|
@@ -7232,6 +7644,7 @@ export {
|
|
|
7232
7644
|
defineSelectField,
|
|
7233
7645
|
defineTab,
|
|
7234
7646
|
defineTableView,
|
|
7647
|
+
defineTask,
|
|
7235
7648
|
defineTextField,
|
|
7236
7649
|
defineTextareaField,
|
|
7237
7650
|
defineTimeField,
|
|
@@ -7295,10 +7708,13 @@ export {
|
|
|
7295
7708
|
max,
|
|
7296
7709
|
min,
|
|
7297
7710
|
nanoid,
|
|
7711
|
+
nextCronRun,
|
|
7298
7712
|
normalizeConfig,
|
|
7299
7713
|
normalizeDetailItem,
|
|
7714
|
+
normalizeGroupKey,
|
|
7300
7715
|
now,
|
|
7301
7716
|
numericFieldNames,
|
|
7717
|
+
parseCron,
|
|
7302
7718
|
parseMongoWhere,
|
|
7303
7719
|
parseSort,
|
|
7304
7720
|
parseSqlWhere,
|
|
@@ -7312,7 +7728,9 @@ export {
|
|
|
7312
7728
|
registerJexlHelpers,
|
|
7313
7729
|
replace,
|
|
7314
7730
|
resolveAdminAuthCollection,
|
|
7731
|
+
resolveAllCollectionViews,
|
|
7315
7732
|
resolveAuthorizedSiteId,
|
|
7733
|
+
resolveIndexName,
|
|
7316
7734
|
resolvePrefix,
|
|
7317
7735
|
round,
|
|
7318
7736
|
runCollectionHooks,
|