@dyrected/core 2.17.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-CLdT_0se.d.cts → app-Dd9cBbCs.d.cts} +171 -5
- package/dist/{app-CLdT_0se.d.ts → app-Dd9cBbCs.d.ts} +171 -5
- package/dist/index.cjs +412 -6
- package/dist/index.d.cts +98 -4
- package/dist/index.d.ts +98 -4
- package/dist/index.js +401 -6
- package/dist/server.cjs +217 -27
- package/dist/server.d.cts +20 -4
- package/dist/server.d.ts +20 -4
- package/dist/server.js +216 -27
- 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) {
|
|
@@ -7112,6 +7495,7 @@ var defineTextField = createFieldDefiner("text");
|
|
|
7112
7495
|
var defineTextareaField = createFieldDefiner("textarea");
|
|
7113
7496
|
var defineRichTextField = createFieldDefiner("richText");
|
|
7114
7497
|
var defineNumberField = createFieldDefiner("number");
|
|
7498
|
+
var defineMoneyField = createFieldDefiner("money");
|
|
7115
7499
|
var defineBooleanField = createFieldDefiner("boolean");
|
|
7116
7500
|
var defineDateField = createFieldDefiner("date");
|
|
7117
7501
|
var defineDateTimeField = createFieldDefiner("datetime");
|
|
@@ -7171,6 +7555,9 @@ export {
|
|
|
7171
7555
|
NotFoundError,
|
|
7172
7556
|
PII_REGEX_PATTERNS,
|
|
7173
7557
|
RESERVED_NAVIGATION_SLUGS,
|
|
7558
|
+
TASK_LOCKS_COLLECTION,
|
|
7559
|
+
TASK_LOCKS_COLLECTION_CONFIG,
|
|
7560
|
+
UNASSIGNED_GROUP_KEY,
|
|
7174
7561
|
ValidationError,
|
|
7175
7562
|
WORKFLOW_HISTORY_COLLECTION,
|
|
7176
7563
|
abs,
|
|
@@ -7190,6 +7577,7 @@ export {
|
|
|
7190
7577
|
ceil,
|
|
7191
7578
|
clamp,
|
|
7192
7579
|
coalesce,
|
|
7580
|
+
coerceBooleanWhere,
|
|
7193
7581
|
collectAdminConditionValidationIssues,
|
|
7194
7582
|
collectConfigDiagnostics,
|
|
7195
7583
|
collectDeclarativeAccessValidationIssues,
|
|
@@ -7199,6 +7587,7 @@ export {
|
|
|
7199
7587
|
compileNavigation,
|
|
7200
7588
|
count,
|
|
7201
7589
|
createLifecycleEvent,
|
|
7590
|
+
createTaskRunner,
|
|
7202
7591
|
createWorkflowDocument,
|
|
7203
7592
|
defaultValue,
|
|
7204
7593
|
defineAction,
|
|
@@ -7221,6 +7610,7 @@ export {
|
|
|
7221
7610
|
defineJoinField,
|
|
7222
7611
|
defineJsonField,
|
|
7223
7612
|
defineKanbanView,
|
|
7613
|
+
defineMoneyField,
|
|
7224
7614
|
defineMultiSelectField,
|
|
7225
7615
|
defineNumberField,
|
|
7226
7616
|
defineObjectField,
|
|
@@ -7232,6 +7622,7 @@ export {
|
|
|
7232
7622
|
defineSelectField,
|
|
7233
7623
|
defineTab,
|
|
7234
7624
|
defineTableView,
|
|
7625
|
+
defineTask,
|
|
7235
7626
|
defineTextField,
|
|
7236
7627
|
defineTextareaField,
|
|
7237
7628
|
defineTimeField,
|
|
@@ -7295,10 +7686,13 @@ export {
|
|
|
7295
7686
|
max,
|
|
7296
7687
|
min,
|
|
7297
7688
|
nanoid,
|
|
7689
|
+
nextCronRun,
|
|
7298
7690
|
normalizeConfig,
|
|
7299
7691
|
normalizeDetailItem,
|
|
7692
|
+
normalizeGroupKey,
|
|
7300
7693
|
now,
|
|
7301
7694
|
numericFieldNames,
|
|
7695
|
+
parseCron,
|
|
7302
7696
|
parseMongoWhere,
|
|
7303
7697
|
parseSort,
|
|
7304
7698
|
parseSqlWhere,
|
|
@@ -7313,6 +7707,7 @@ export {
|
|
|
7313
7707
|
replace,
|
|
7314
7708
|
resolveAdminAuthCollection,
|
|
7315
7709
|
resolveAuthorizedSiteId,
|
|
7710
|
+
resolveIndexName,
|
|
7316
7711
|
resolvePrefix,
|
|
7317
7712
|
round,
|
|
7318
7713
|
runCollectionHooks,
|