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