@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.cjs
CHANGED
|
@@ -1809,6 +1809,9 @@ __export(index_exports, {
|
|
|
1809
1809
|
NotFoundError: () => NotFoundError,
|
|
1810
1810
|
PII_REGEX_PATTERNS: () => PII_REGEX_PATTERNS,
|
|
1811
1811
|
RESERVED_NAVIGATION_SLUGS: () => RESERVED_NAVIGATION_SLUGS,
|
|
1812
|
+
TASK_LOCKS_COLLECTION: () => TASK_LOCKS_COLLECTION,
|
|
1813
|
+
TASK_LOCKS_COLLECTION_CONFIG: () => TASK_LOCKS_COLLECTION_CONFIG,
|
|
1814
|
+
UNASSIGNED_GROUP_KEY: () => UNASSIGNED_GROUP_KEY,
|
|
1812
1815
|
ValidationError: () => ValidationError,
|
|
1813
1816
|
WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
|
|
1814
1817
|
abs: () => abs,
|
|
@@ -1828,6 +1831,7 @@ __export(index_exports, {
|
|
|
1828
1831
|
ceil: () => ceil,
|
|
1829
1832
|
clamp: () => clamp,
|
|
1830
1833
|
coalesce: () => coalesce,
|
|
1834
|
+
coerceBooleanWhere: () => coerceBooleanWhere,
|
|
1831
1835
|
collectAdminConditionValidationIssues: () => collectAdminConditionValidationIssues,
|
|
1832
1836
|
collectConfigDiagnostics: () => collectConfigDiagnostics,
|
|
1833
1837
|
collectDeclarativeAccessValidationIssues: () => collectDeclarativeAccessValidationIssues,
|
|
@@ -1837,6 +1841,7 @@ __export(index_exports, {
|
|
|
1837
1841
|
compileNavigation: () => compileNavigation,
|
|
1838
1842
|
count: () => count,
|
|
1839
1843
|
createLifecycleEvent: () => createLifecycleEvent,
|
|
1844
|
+
createTaskRunner: () => createTaskRunner,
|
|
1840
1845
|
createWorkflowDocument: () => createWorkflowDocument,
|
|
1841
1846
|
defaultValue: () => defaultValue,
|
|
1842
1847
|
defineAction: () => defineAction,
|
|
@@ -1859,6 +1864,7 @@ __export(index_exports, {
|
|
|
1859
1864
|
defineJoinField: () => defineJoinField,
|
|
1860
1865
|
defineJsonField: () => defineJsonField,
|
|
1861
1866
|
defineKanbanView: () => defineKanbanView,
|
|
1867
|
+
defineMoneyField: () => defineMoneyField,
|
|
1862
1868
|
defineMultiSelectField: () => defineMultiSelectField,
|
|
1863
1869
|
defineNumberField: () => defineNumberField,
|
|
1864
1870
|
defineObjectField: () => defineObjectField,
|
|
@@ -1870,6 +1876,7 @@ __export(index_exports, {
|
|
|
1870
1876
|
defineSelectField: () => defineSelectField,
|
|
1871
1877
|
defineTab: () => defineTab,
|
|
1872
1878
|
defineTableView: () => defineTableView,
|
|
1879
|
+
defineTask: () => defineTask,
|
|
1873
1880
|
defineTextField: () => defineTextField,
|
|
1874
1881
|
defineTextareaField: () => defineTextareaField,
|
|
1875
1882
|
defineTimeField: () => defineTimeField,
|
|
@@ -1933,10 +1940,13 @@ __export(index_exports, {
|
|
|
1933
1940
|
max: () => max,
|
|
1934
1941
|
min: () => min,
|
|
1935
1942
|
nanoid: () => nanoid,
|
|
1943
|
+
nextCronRun: () => nextCronRun,
|
|
1936
1944
|
normalizeConfig: () => normalizeConfig,
|
|
1937
1945
|
normalizeDetailItem: () => normalizeDetailItem,
|
|
1946
|
+
normalizeGroupKey: () => normalizeGroupKey,
|
|
1938
1947
|
now: () => now,
|
|
1939
1948
|
numericFieldNames: () => numericFieldNames,
|
|
1949
|
+
parseCron: () => parseCron,
|
|
1940
1950
|
parseMongoWhere: () => parseMongoWhere,
|
|
1941
1951
|
parseSort: () => parseSort,
|
|
1942
1952
|
parseSqlWhere: () => parseSqlWhere,
|
|
@@ -1950,7 +1960,9 @@ __export(index_exports, {
|
|
|
1950
1960
|
registerJexlHelpers: () => registerJexlHelpers,
|
|
1951
1961
|
replace: () => replace,
|
|
1952
1962
|
resolveAdminAuthCollection: () => resolveAdminAuthCollection,
|
|
1963
|
+
resolveAllCollectionViews: () => resolveAllCollectionViews,
|
|
1953
1964
|
resolveAuthorizedSiteId: () => resolveAuthorizedSiteId,
|
|
1965
|
+
resolveIndexName: () => resolveIndexName,
|
|
1954
1966
|
resolvePrefix: () => resolvePrefix,
|
|
1955
1967
|
round: () => round,
|
|
1956
1968
|
runCollectionHooks: () => runCollectionHooks,
|
|
@@ -2071,6 +2083,296 @@ var import_jose = require("jose");
|
|
|
2071
2083
|
// src/auth/sessions.ts
|
|
2072
2084
|
var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
|
|
2073
2085
|
|
|
2086
|
+
// src/tasks.ts
|
|
2087
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
2088
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
2089
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
2090
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
2091
|
+
fields: [
|
|
2092
|
+
{ name: "name", type: "text", required: true },
|
|
2093
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
2094
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
2095
|
+
{ name: "lockedBy", type: "text" },
|
|
2096
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
2097
|
+
{ name: "lastStartedAt", type: "date" },
|
|
2098
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
2099
|
+
{ name: "lastStatus", type: "text" },
|
|
2100
|
+
{ name: "lastError", type: "textarea" }
|
|
2101
|
+
],
|
|
2102
|
+
access: {
|
|
2103
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
2104
|
+
create: () => false,
|
|
2105
|
+
update: () => false,
|
|
2106
|
+
delete: () => false
|
|
2107
|
+
},
|
|
2108
|
+
admin: { hidden: true }
|
|
2109
|
+
};
|
|
2110
|
+
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
2111
|
+
var DEFAULT_TICK_MS = 3e4;
|
|
2112
|
+
var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
2113
|
+
var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
2114
|
+
function parseCronField(raw, min2, max2, names, nameOffset, label) {
|
|
2115
|
+
const values = /* @__PURE__ */ new Set();
|
|
2116
|
+
const toNumber = (token) => {
|
|
2117
|
+
const named = names?.indexOf(token.toLowerCase());
|
|
2118
|
+
if (named !== void 0 && named >= 0) return named + nameOffset;
|
|
2119
|
+
if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
|
|
2120
|
+
return Number(token);
|
|
2121
|
+
};
|
|
2122
|
+
for (const part of raw.split(",")) {
|
|
2123
|
+
const [rangePart, stepPart] = part.split("/");
|
|
2124
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
2125
|
+
if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
|
|
2126
|
+
let start;
|
|
2127
|
+
let end;
|
|
2128
|
+
if (rangePart === "*") {
|
|
2129
|
+
start = min2;
|
|
2130
|
+
end = max2;
|
|
2131
|
+
} else if (rangePart.includes("-")) {
|
|
2132
|
+
const [a, b] = rangePart.split("-");
|
|
2133
|
+
start = toNumber(a);
|
|
2134
|
+
end = toNumber(b);
|
|
2135
|
+
} else {
|
|
2136
|
+
start = toNumber(rangePart);
|
|
2137
|
+
end = stepPart === void 0 ? start : max2;
|
|
2138
|
+
}
|
|
2139
|
+
if (start < min2 || end > max2 || start > end) {
|
|
2140
|
+
throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
|
|
2141
|
+
}
|
|
2142
|
+
for (let v = start; v <= end; v += step) values.add(v);
|
|
2143
|
+
}
|
|
2144
|
+
return values;
|
|
2145
|
+
}
|
|
2146
|
+
function parseCron(expression) {
|
|
2147
|
+
const fields = expression.trim().split(/\s+/);
|
|
2148
|
+
if (fields.length !== 5) {
|
|
2149
|
+
throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
|
|
2150
|
+
}
|
|
2151
|
+
const [min2, hour, dom, mon, dow] = fields;
|
|
2152
|
+
const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
|
|
2153
|
+
if (daysOfWeek.has(7)) daysOfWeek.add(0);
|
|
2154
|
+
daysOfWeek.delete(7);
|
|
2155
|
+
return {
|
|
2156
|
+
minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
|
|
2157
|
+
hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
|
|
2158
|
+
daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
|
|
2159
|
+
months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
|
|
2160
|
+
daysOfWeek,
|
|
2161
|
+
dayFieldsBothRestricted: dom !== "*" && dow !== "*"
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
function dayMatches(schedule, date) {
|
|
2165
|
+
const domOk = schedule.daysOfMonth.has(date.getUTCDate());
|
|
2166
|
+
const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
|
|
2167
|
+
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
2168
|
+
}
|
|
2169
|
+
function nextCronRun(expression, after) {
|
|
2170
|
+
const schedule = typeof expression === "string" ? parseCron(expression) : expression;
|
|
2171
|
+
const cursor = new Date(after.getTime());
|
|
2172
|
+
cursor.setUTCSeconds(0, 0);
|
|
2173
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
|
|
2174
|
+
const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
2175
|
+
while (cursor.getTime() <= limit) {
|
|
2176
|
+
if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
|
|
2177
|
+
cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
|
|
2178
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
if (!dayMatches(schedule, cursor)) {
|
|
2182
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
2183
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
if (!schedule.hours.has(cursor.getUTCHours())) {
|
|
2187
|
+
cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
if (!schedule.minutes.has(cursor.getUTCMinutes())) {
|
|
2191
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
return cursor;
|
|
2195
|
+
}
|
|
2196
|
+
throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
|
|
2197
|
+
}
|
|
2198
|
+
function defineTask(task) {
|
|
2199
|
+
if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
|
|
2200
|
+
if (task.cron !== void 0) parseCron(task.cron);
|
|
2201
|
+
if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
|
|
2202
|
+
throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
|
|
2203
|
+
}
|
|
2204
|
+
return task;
|
|
2205
|
+
}
|
|
2206
|
+
function resolveLogger(config, taskName) {
|
|
2207
|
+
const base = config.logger;
|
|
2208
|
+
if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
|
|
2209
|
+
const prefix = `[dyrected/tasks] ${taskName}`;
|
|
2210
|
+
return {
|
|
2211
|
+
info: (obj, msg) => console.info(prefix, msg ?? "", obj),
|
|
2212
|
+
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
2213
|
+
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function floorToMinute(date) {
|
|
2217
|
+
const d = new Date(date.getTime());
|
|
2218
|
+
d.setUTCSeconds(0, 0);
|
|
2219
|
+
return d;
|
|
2220
|
+
}
|
|
2221
|
+
function createTaskRunner(config) {
|
|
2222
|
+
const db = config.db;
|
|
2223
|
+
if (!db) throw new Error("createTaskRunner requires config.db.");
|
|
2224
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
2225
|
+
for (const task of config.tasks ?? []) {
|
|
2226
|
+
if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
|
|
2227
|
+
tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
|
|
2228
|
+
}
|
|
2229
|
+
const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
2230
|
+
let timer;
|
|
2231
|
+
let inFlight = Promise.resolve();
|
|
2232
|
+
let ticking = false;
|
|
2233
|
+
let ready;
|
|
2234
|
+
function ensureReady() {
|
|
2235
|
+
ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
|
|
2236
|
+
ready.catch(() => {
|
|
2237
|
+
ready = void 0;
|
|
2238
|
+
});
|
|
2239
|
+
return ready;
|
|
2240
|
+
}
|
|
2241
|
+
async function ensureLockRow(name, now2) {
|
|
2242
|
+
await ensureReady();
|
|
2243
|
+
const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2244
|
+
if (existing) return existing;
|
|
2245
|
+
try {
|
|
2246
|
+
return await db.create({
|
|
2247
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2248
|
+
data: {
|
|
2249
|
+
id: name,
|
|
2250
|
+
name,
|
|
2251
|
+
lockedUntil: 0,
|
|
2252
|
+
lockedBy: null,
|
|
2253
|
+
lastScheduledFor: floorToMinute(now2).toISOString(),
|
|
2254
|
+
lastStartedAt: null,
|
|
2255
|
+
lastFinishedAt: null,
|
|
2256
|
+
lastStatus: null,
|
|
2257
|
+
lastError: null
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2262
|
+
if (created) return created;
|
|
2263
|
+
throw error;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
async function execute(task, row, scheduledFor) {
|
|
2267
|
+
const started = Date.now();
|
|
2268
|
+
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
2269
|
+
const token = `${instanceId}:${started}`;
|
|
2270
|
+
const where = {
|
|
2271
|
+
id: { equals: task.name },
|
|
2272
|
+
lockedUntil: { less_than: started }
|
|
2273
|
+
};
|
|
2274
|
+
if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
|
|
2275
|
+
const claimed = await db.update({
|
|
2276
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2277
|
+
where,
|
|
2278
|
+
data: {
|
|
2279
|
+
lockedUntil: started + lockMs,
|
|
2280
|
+
lockedBy: token,
|
|
2281
|
+
lastStartedAt: new Date(started).toISOString(),
|
|
2282
|
+
...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
if (claimed.affectedRows !== 1) {
|
|
2286
|
+
return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
|
|
2287
|
+
}
|
|
2288
|
+
const logger = resolveLogger(config, task.name);
|
|
2289
|
+
const controller = new AbortController();
|
|
2290
|
+
let timeout;
|
|
2291
|
+
let status = "completed";
|
|
2292
|
+
let errorMessage;
|
|
2293
|
+
try {
|
|
2294
|
+
await Promise.race([
|
|
2295
|
+
Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
|
|
2296
|
+
new Promise((_, reject) => {
|
|
2297
|
+
timeout = setTimeout(() => {
|
|
2298
|
+
reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
|
|
2299
|
+
controller.abort();
|
|
2300
|
+
}, lockMs);
|
|
2301
|
+
})
|
|
2302
|
+
]);
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
status = "failed";
|
|
2305
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2306
|
+
logger.error({ err: error }, "Task failed");
|
|
2307
|
+
} finally {
|
|
2308
|
+
if (timeout) clearTimeout(timeout);
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
await db.update({
|
|
2312
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
2313
|
+
where: { id: { equals: task.name }, lockedBy: { equals: token } },
|
|
2314
|
+
data: {
|
|
2315
|
+
lockedUntil: 0,
|
|
2316
|
+
lockedBy: null,
|
|
2317
|
+
lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2318
|
+
lastStatus: status,
|
|
2319
|
+
lastError: errorMessage ?? null
|
|
2320
|
+
}
|
|
2321
|
+
});
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
|
|
2324
|
+
}
|
|
2325
|
+
return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
|
|
2326
|
+
}
|
|
2327
|
+
function track(work) {
|
|
2328
|
+
inFlight = inFlight.then(() => work).catch(() => void 0);
|
|
2329
|
+
return work;
|
|
2330
|
+
}
|
|
2331
|
+
const runner = {
|
|
2332
|
+
async runTask(name) {
|
|
2333
|
+
const task = tasks.get(name);
|
|
2334
|
+
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
2335
|
+
const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
2336
|
+
return track(execute(task, row, void 0));
|
|
2337
|
+
},
|
|
2338
|
+
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
2339
|
+
const results = [];
|
|
2340
|
+
for (const task of tasks.values()) {
|
|
2341
|
+
if (!task.schedule) continue;
|
|
2342
|
+
const row = await ensureLockRow(task.name, now2);
|
|
2343
|
+
const reference = new Date(String(row.lastScheduledFor));
|
|
2344
|
+
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
2345
|
+
if (!due) {
|
|
2346
|
+
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
2347
|
+
continue;
|
|
2348
|
+
}
|
|
2349
|
+
results.push(await track(execute(task, row, floorToMinute(now2))));
|
|
2350
|
+
}
|
|
2351
|
+
return results;
|
|
2352
|
+
},
|
|
2353
|
+
start(options) {
|
|
2354
|
+
if (timer) return;
|
|
2355
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
|
|
2356
|
+
const tick = () => {
|
|
2357
|
+
if (ticking) return;
|
|
2358
|
+
ticking = true;
|
|
2359
|
+
runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
|
|
2360
|
+
ticking = false;
|
|
2361
|
+
});
|
|
2362
|
+
};
|
|
2363
|
+
timer = setInterval(tick, intervalMs);
|
|
2364
|
+
timer.unref?.();
|
|
2365
|
+
setTimeout(tick, 0);
|
|
2366
|
+
},
|
|
2367
|
+
async stop() {
|
|
2368
|
+
if (timer) clearInterval(timer);
|
|
2369
|
+
timer = void 0;
|
|
2370
|
+
await inFlight;
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
return runner;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2074
2376
|
// src/workflows.ts
|
|
2075
2377
|
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
2076
2378
|
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
@@ -2286,7 +2588,7 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
|
|
|
2286
2588
|
return due.length;
|
|
2287
2589
|
}
|
|
2288
2590
|
async function saveWorkflowDraft(args) {
|
|
2289
|
-
const { config, collection, id, originalDoc, data, user } = args;
|
|
2591
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
2290
2592
|
const db = config.db;
|
|
2291
2593
|
const workflow = collection.workflow;
|
|
2292
2594
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -2320,13 +2622,14 @@ async function saveWorkflowDraft(args) {
|
|
|
2320
2622
|
}
|
|
2321
2623
|
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
2322
2624
|
await persistEvent(tx, event);
|
|
2625
|
+
await beforeCommit?.(tx, updated);
|
|
2323
2626
|
return updated;
|
|
2324
2627
|
});
|
|
2325
2628
|
void dispatchLifecycleEvent(config, event);
|
|
2326
2629
|
return { doc, event };
|
|
2327
2630
|
}
|
|
2328
2631
|
async function createWorkflowDocument(args) {
|
|
2329
|
-
const { config, collection, data, user } = args;
|
|
2632
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
2330
2633
|
const db = config.db;
|
|
2331
2634
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
2332
2635
|
let event;
|
|
@@ -2340,13 +2643,14 @@ async function createWorkflowDocument(args) {
|
|
|
2340
2643
|
payload: { revision: 1, previousRevision: null }
|
|
2341
2644
|
});
|
|
2342
2645
|
await persistEvent(tx, event);
|
|
2646
|
+
await beforeCommit?.(tx, created);
|
|
2343
2647
|
return created;
|
|
2344
2648
|
});
|
|
2345
2649
|
void dispatchLifecycleEvent(config, event);
|
|
2346
2650
|
return { doc, event };
|
|
2347
2651
|
}
|
|
2348
2652
|
async function transitionWorkflow(args) {
|
|
2349
|
-
const { config, collection, id, transitionName, expectedRevision, comment, user, req } = args;
|
|
2653
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
2350
2654
|
const db = config.db;
|
|
2351
2655
|
const workflow = collection.workflow;
|
|
2352
2656
|
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
@@ -2372,7 +2676,7 @@ async function transitionWorkflow(args) {
|
|
|
2372
2676
|
if (transition.requireComment && !comment?.trim()) {
|
|
2373
2677
|
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
2374
2678
|
}
|
|
2375
|
-
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
|
|
2679
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
2376
2680
|
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
2377
2681
|
const events = [
|
|
2378
2682
|
createLifecycleEvent({
|
|
@@ -2426,6 +2730,19 @@ async function transitionWorkflow(args) {
|
|
|
2426
2730
|
if (targetState.published) data.__published = working;
|
|
2427
2731
|
if (transition.unpublish) data.__published = null;
|
|
2428
2732
|
const next = await tx.update({ collection: collection.slug, id, data });
|
|
2733
|
+
if (transition.onTransition) {
|
|
2734
|
+
await transition.onTransition({
|
|
2735
|
+
transition,
|
|
2736
|
+
from: lockedMeta.state,
|
|
2737
|
+
to: transition.to,
|
|
2738
|
+
doc: locked,
|
|
2739
|
+
input,
|
|
2740
|
+
comment,
|
|
2741
|
+
user,
|
|
2742
|
+
req,
|
|
2743
|
+
tx
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2429
2746
|
await tx.create({
|
|
2430
2747
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
2431
2748
|
data: {
|
|
@@ -3862,6 +4179,7 @@ function normalizeConfig(config) {
|
|
|
3862
4179
|
const globals = schemaAwareConfig?.globals || [];
|
|
3863
4180
|
const needsAudit = collections.some((col) => col.audit);
|
|
3864
4181
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
4182
|
+
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
3865
4183
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
3866
4184
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
3867
4185
|
collections,
|
|
@@ -4094,6 +4412,9 @@ function normalizeConfig(config) {
|
|
|
4094
4412
|
if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
|
|
4095
4413
|
systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
|
|
4096
4414
|
}
|
|
4415
|
+
if (needsTasks && !normalizedCollections.some((col) => col.slug === TASK_LOCKS_COLLECTION)) {
|
|
4416
|
+
systemCollections.push(TASK_LOCKS_COLLECTION_CONFIG);
|
|
4417
|
+
}
|
|
4097
4418
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
4098
4419
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
4099
4420
|
}
|
|
@@ -4178,7 +4499,11 @@ function parseSqlWhere(where, getJsonField, placeholder = "?", likeOperator = pl
|
|
|
4178
4499
|
params.push("");
|
|
4179
4500
|
return `(${c})::text = ${next()}`;
|
|
4180
4501
|
}
|
|
4181
|
-
|
|
4502
|
+
if (typeof value === "boolean") {
|
|
4503
|
+
params.push(placeholder === "?" ? value ? 1 : 0 : isJsonExtract ? String(value) : value);
|
|
4504
|
+
} else {
|
|
4505
|
+
params.push(value);
|
|
4506
|
+
}
|
|
4182
4507
|
return `${c} = ${next()}`;
|
|
4183
4508
|
}
|
|
4184
4509
|
const entries = Object.entries(value);
|
|
@@ -4378,6 +4703,38 @@ function parseMongoWhere(where) {
|
|
|
4378
4703
|
}
|
|
4379
4704
|
return buildClause(where);
|
|
4380
4705
|
}
|
|
4706
|
+
function coerceBooleanWhere(where, fields) {
|
|
4707
|
+
const booleanFields = new Set(
|
|
4708
|
+
(fields ?? []).filter((f) => f.type === "boolean" && f.name).map((f) => f.name)
|
|
4709
|
+
);
|
|
4710
|
+
if (booleanFields.size === 0) return where;
|
|
4711
|
+
const toBool = (v) => v === "true" ? true : v === "false" ? false : v;
|
|
4712
|
+
const walk = (node) => {
|
|
4713
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
4714
|
+
if (!node || typeof node !== "object") return node;
|
|
4715
|
+
const out = {};
|
|
4716
|
+
for (const [key, value] of Object.entries(node)) {
|
|
4717
|
+
if (key.toUpperCase() === "AND" || key.toUpperCase() === "OR") {
|
|
4718
|
+
out[key] = walk(value);
|
|
4719
|
+
} else if (booleanFields.has(key)) {
|
|
4720
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4721
|
+
out[key] = Object.fromEntries(
|
|
4722
|
+
Object.entries(value).map(([op, operand]) => [
|
|
4723
|
+
op,
|
|
4724
|
+
Array.isArray(operand) ? operand.map(toBool) : toBool(operand)
|
|
4725
|
+
])
|
|
4726
|
+
);
|
|
4727
|
+
} else {
|
|
4728
|
+
out[key] = toBool(value);
|
|
4729
|
+
}
|
|
4730
|
+
} else {
|
|
4731
|
+
out[key] = value;
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
return out;
|
|
4735
|
+
};
|
|
4736
|
+
return walk(where);
|
|
4737
|
+
}
|
|
4381
4738
|
|
|
4382
4739
|
// src/utils/parse-sort.ts
|
|
4383
4740
|
var SORT_FIELD_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$/i;
|
|
@@ -4659,7 +5016,12 @@ function generateOpenApi(config) {
|
|
|
4659
5016
|
type: "object",
|
|
4660
5017
|
properties: {
|
|
4661
5018
|
expectedRevision: { type: "integer", minimum: 1 },
|
|
4662
|
-
comment: { type: "string" }
|
|
5019
|
+
comment: { type: "string" },
|
|
5020
|
+
input: {
|
|
5021
|
+
type: "object",
|
|
5022
|
+
additionalProperties: true,
|
|
5023
|
+
description: "Extra data passed to the transition's onTransition handler."
|
|
5024
|
+
}
|
|
4663
5025
|
}
|
|
4664
5026
|
},
|
|
4665
5027
|
WorkflowHistoryEntry: {
|
|
@@ -5822,6 +6184,9 @@ function fieldToSchema(field) {
|
|
|
5822
6184
|
case "number":
|
|
5823
6185
|
schema = { type: "number" };
|
|
5824
6186
|
break;
|
|
6187
|
+
case "money":
|
|
6188
|
+
schema = { type: "integer", description: "Amount in minor units (for example kobo or cents)." };
|
|
6189
|
+
break;
|
|
5825
6190
|
case "boolean":
|
|
5826
6191
|
schema = { type: "boolean" };
|
|
5827
6192
|
break;
|
|
@@ -6774,6 +7139,36 @@ function sanitizeDocForAI(options) {
|
|
|
6774
7139
|
return result;
|
|
6775
7140
|
}
|
|
6776
7141
|
|
|
7142
|
+
// src/utils/index-name.ts
|
|
7143
|
+
var import_node_crypto2 = require("crypto");
|
|
7144
|
+
var MAX_INDEX_NAME_LENGTH = 60;
|
|
7145
|
+
function resolveIndexName(prefix, table, fields) {
|
|
7146
|
+
const full = `${prefix}_${table}_${fields.join("_")}`;
|
|
7147
|
+
if (full.length <= MAX_INDEX_NAME_LENGTH) return full;
|
|
7148
|
+
const hash = (0, import_node_crypto2.createHash)("sha1").update(full).digest("hex").slice(0, 10);
|
|
7149
|
+
const tail = (fields[fields.length - 1] ?? "").slice(0, 30);
|
|
7150
|
+
return `${prefix}_${hash}_${tail}`;
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
// src/utils/group-key.ts
|
|
7154
|
+
var UNASSIGNED_GROUP_KEY = "__unassigned__";
|
|
7155
|
+
var MYSQL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
|
|
7156
|
+
function normalizeGroupKey(raw, fieldType) {
|
|
7157
|
+
if (raw === null || raw === void 0) return UNASSIGNED_GROUP_KEY;
|
|
7158
|
+
if (fieldType === "boolean") {
|
|
7159
|
+
if (raw === true || raw === 1 || raw === "1" || raw === "true" || raw === "t") return "true";
|
|
7160
|
+
if (raw === false || raw === 0 || raw === "0" || raw === "false" || raw === "f") return "false";
|
|
7161
|
+
}
|
|
7162
|
+
if ((fieldType === "number" || fieldType === "money") && raw !== "" && !Number.isNaN(Number(raw))) {
|
|
7163
|
+
return String(Number(raw));
|
|
7164
|
+
}
|
|
7165
|
+
if (fieldType === "date" || fieldType === "datetime") {
|
|
7166
|
+
if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? String(raw) : raw.toISOString();
|
|
7167
|
+
if (typeof raw === "string" && MYSQL_DATETIME.test(raw)) return `${raw.replace(" ", "T")}Z`;
|
|
7168
|
+
}
|
|
7169
|
+
return String(raw);
|
|
7170
|
+
}
|
|
7171
|
+
|
|
6777
7172
|
// src/utils/navigation-reconciler.ts
|
|
6778
7173
|
var normalizeKey = (val) => (val || "").toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
6779
7174
|
function reconcileNavigation(baseTree, prefs, schemas, options) {
|
|
@@ -7292,6 +7687,28 @@ function pruneNavigationForUser(tree, user) {
|
|
|
7292
7687
|
ungrouped: tree.ungrouped.filter(isAllowed)
|
|
7293
7688
|
};
|
|
7294
7689
|
}
|
|
7690
|
+
function resolveAllCollectionViews(collection, config) {
|
|
7691
|
+
const views = [...collection.views ?? []];
|
|
7692
|
+
const seenSlugs = new Set(views.map((v) => v.slug));
|
|
7693
|
+
const explicitNav = config?.admin?.navigation;
|
|
7694
|
+
if (Array.isArray(explicitNav)) {
|
|
7695
|
+
for (const item of explicitNav) {
|
|
7696
|
+
if (!item) continue;
|
|
7697
|
+
const itemCol = item.collection || item.addToCollection;
|
|
7698
|
+
if (Array.isArray(item.views)) {
|
|
7699
|
+
for (const view of item.views) {
|
|
7700
|
+
if (!view || !view.slug) continue;
|
|
7701
|
+
const targetCol = view.collection || itemCol;
|
|
7702
|
+
if (targetCol === collection.slug && !seenSlugs.has(view.slug)) {
|
|
7703
|
+
views.push(view);
|
|
7704
|
+
seenSlugs.add(view.slug);
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
return views;
|
|
7711
|
+
}
|
|
7295
7712
|
|
|
7296
7713
|
// src/index.ts
|
|
7297
7714
|
var import_zod = require("zod");
|
|
@@ -7317,6 +7734,7 @@ var defineTextField = createFieldDefiner("text");
|
|
|
7317
7734
|
var defineTextareaField = createFieldDefiner("textarea");
|
|
7318
7735
|
var defineRichTextField = createFieldDefiner("richText");
|
|
7319
7736
|
var defineNumberField = createFieldDefiner("number");
|
|
7737
|
+
var defineMoneyField = createFieldDefiner("money");
|
|
7320
7738
|
var defineBooleanField = createFieldDefiner("boolean");
|
|
7321
7739
|
var defineDateField = createFieldDefiner("date");
|
|
7322
7740
|
var defineDateTimeField = createFieldDefiner("datetime");
|
|
@@ -7377,6 +7795,9 @@ function defineTab(args) {
|
|
|
7377
7795
|
NotFoundError,
|
|
7378
7796
|
PII_REGEX_PATTERNS,
|
|
7379
7797
|
RESERVED_NAVIGATION_SLUGS,
|
|
7798
|
+
TASK_LOCKS_COLLECTION,
|
|
7799
|
+
TASK_LOCKS_COLLECTION_CONFIG,
|
|
7800
|
+
UNASSIGNED_GROUP_KEY,
|
|
7380
7801
|
ValidationError,
|
|
7381
7802
|
WORKFLOW_HISTORY_COLLECTION,
|
|
7382
7803
|
abs,
|
|
@@ -7396,6 +7817,7 @@ function defineTab(args) {
|
|
|
7396
7817
|
ceil,
|
|
7397
7818
|
clamp,
|
|
7398
7819
|
coalesce,
|
|
7820
|
+
coerceBooleanWhere,
|
|
7399
7821
|
collectAdminConditionValidationIssues,
|
|
7400
7822
|
collectConfigDiagnostics,
|
|
7401
7823
|
collectDeclarativeAccessValidationIssues,
|
|
@@ -7405,6 +7827,7 @@ function defineTab(args) {
|
|
|
7405
7827
|
compileNavigation,
|
|
7406
7828
|
count,
|
|
7407
7829
|
createLifecycleEvent,
|
|
7830
|
+
createTaskRunner,
|
|
7408
7831
|
createWorkflowDocument,
|
|
7409
7832
|
defaultValue,
|
|
7410
7833
|
defineAction,
|
|
@@ -7427,6 +7850,7 @@ function defineTab(args) {
|
|
|
7427
7850
|
defineJoinField,
|
|
7428
7851
|
defineJsonField,
|
|
7429
7852
|
defineKanbanView,
|
|
7853
|
+
defineMoneyField,
|
|
7430
7854
|
defineMultiSelectField,
|
|
7431
7855
|
defineNumberField,
|
|
7432
7856
|
defineObjectField,
|
|
@@ -7438,6 +7862,7 @@ function defineTab(args) {
|
|
|
7438
7862
|
defineSelectField,
|
|
7439
7863
|
defineTab,
|
|
7440
7864
|
defineTableView,
|
|
7865
|
+
defineTask,
|
|
7441
7866
|
defineTextField,
|
|
7442
7867
|
defineTextareaField,
|
|
7443
7868
|
defineTimeField,
|
|
@@ -7501,10 +7926,13 @@ function defineTab(args) {
|
|
|
7501
7926
|
max,
|
|
7502
7927
|
min,
|
|
7503
7928
|
nanoid,
|
|
7929
|
+
nextCronRun,
|
|
7504
7930
|
normalizeConfig,
|
|
7505
7931
|
normalizeDetailItem,
|
|
7932
|
+
normalizeGroupKey,
|
|
7506
7933
|
now,
|
|
7507
7934
|
numericFieldNames,
|
|
7935
|
+
parseCron,
|
|
7508
7936
|
parseMongoWhere,
|
|
7509
7937
|
parseSort,
|
|
7510
7938
|
parseSqlWhere,
|
|
@@ -7518,7 +7946,9 @@ function defineTab(args) {
|
|
|
7518
7946
|
registerJexlHelpers,
|
|
7519
7947
|
replace,
|
|
7520
7948
|
resolveAdminAuthCollection,
|
|
7949
|
+
resolveAllCollectionViews,
|
|
7521
7950
|
resolveAuthorizedSiteId,
|
|
7951
|
+
resolveIndexName,
|
|
7522
7952
|
resolvePrefix,
|
|
7523
7953
|
round,
|
|
7524
7954
|
runCollectionHooks,
|