@dyrected/core 2.18.1 → 2.19.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-x7J5BIqK.d.cts → app-CXF4MdJc.d.cts} +97 -6
- package/dist/{app-x7J5BIqK.d.ts → app-CXF4MdJc.d.ts} +97 -6
- package/dist/index.cjs +2371 -1878
- package/dist/index.d.cts +231 -6
- package/dist/index.d.ts +231 -6
- package/dist/index.js +2356 -1878
- package/dist/server.cjs +8196 -6673
- package/dist/server.d.cts +22 -2
- package/dist/server.d.ts +22 -2
- package/dist/server.js +8217 -6694
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1866,1821 +1866,2318 @@ import { SignJWT, jwtVerify, decodeJwt } from "jose";
|
|
|
1866
1866
|
// src/auth/sessions.ts
|
|
1867
1867
|
var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
|
|
1868
1868
|
|
|
1869
|
-
// src/
|
|
1870
|
-
var
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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;
|
|
1869
|
+
// src/utils/hooks.ts
|
|
1870
|
+
var import_jexl3 = __toESM(require_Jexl(), 1);
|
|
1871
|
+
|
|
1872
|
+
// src/utils/declarative-hooks.ts
|
|
1873
|
+
var import_jexl2 = __toESM(require_Jexl(), 1);
|
|
1874
|
+
|
|
1875
|
+
// src/utils/jexl-helpers.ts
|
|
1876
|
+
var import_jexl = __toESM(require_Jexl(), 1);
|
|
1877
|
+
function slugify(val) {
|
|
1878
|
+
if (val === void 0 || val === null) return "";
|
|
1879
|
+
return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1928
1880
|
}
|
|
1929
|
-
function
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
-
};
|
|
1881
|
+
function lower(val) {
|
|
1882
|
+
if (val === void 0 || val === null) return "";
|
|
1883
|
+
return String(val).toLowerCase();
|
|
1946
1884
|
}
|
|
1947
|
-
function
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
1885
|
+
function upper(val) {
|
|
1886
|
+
if (val === void 0 || val === null) return "";
|
|
1887
|
+
return String(val).toUpperCase();
|
|
1951
1888
|
}
|
|
1952
|
-
function
|
|
1953
|
-
|
|
1954
|
-
|
|
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.`);
|
|
1889
|
+
function trim(val) {
|
|
1890
|
+
if (val === void 0 || val === null) return "";
|
|
1891
|
+
return String(val).trim();
|
|
1980
1892
|
}
|
|
1981
|
-
function
|
|
1982
|
-
if (
|
|
1983
|
-
|
|
1984
|
-
if (
|
|
1985
|
-
|
|
1986
|
-
}
|
|
1987
|
-
return task;
|
|
1893
|
+
function capitalize(val) {
|
|
1894
|
+
if (val === void 0 || val === null) return "";
|
|
1895
|
+
const str = String(val).trim();
|
|
1896
|
+
if (!str) return "";
|
|
1897
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
1988
1898
|
}
|
|
1989
|
-
function
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
const
|
|
1993
|
-
return
|
|
1994
|
-
|
|
1995
|
-
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
1996
|
-
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
1997
|
-
};
|
|
1899
|
+
function truncate(val, length2, ellipsis = "...") {
|
|
1900
|
+
if (val === void 0 || val === null) return "";
|
|
1901
|
+
const str = String(val);
|
|
1902
|
+
const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
|
|
1903
|
+
if (str.length <= maxLen) return str;
|
|
1904
|
+
return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
|
|
1998
1905
|
}
|
|
1999
|
-
function
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
return
|
|
1906
|
+
function readingTime(val, wpm = 200) {
|
|
1907
|
+
if (val === void 0 || val === null) return 1;
|
|
1908
|
+
const str = String(val).trim();
|
|
1909
|
+
if (!str) return 1;
|
|
1910
|
+
const words = str.split(/\s+/).filter(Boolean).length;
|
|
1911
|
+
return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
|
|
2003
1912
|
}
|
|
2004
|
-
function
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
1913
|
+
function wordCount(val) {
|
|
1914
|
+
if (val === void 0 || val === null) return 0;
|
|
1915
|
+
const str = String(val).trim();
|
|
1916
|
+
if (!str) return 0;
|
|
1917
|
+
return str.split(/\s+/).filter(Boolean).length;
|
|
1918
|
+
}
|
|
1919
|
+
function replace(val, search, replacement) {
|
|
1920
|
+
if (val === void 0 || val === null) return "";
|
|
1921
|
+
return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
|
|
1922
|
+
}
|
|
1923
|
+
function startsWith(str, prefix) {
|
|
1924
|
+
if (str === void 0 || str === null) return false;
|
|
1925
|
+
return String(str).startsWith(String(prefix ?? ""));
|
|
1926
|
+
}
|
|
1927
|
+
function endsWith(str, suffix) {
|
|
1928
|
+
if (str === void 0 || str === null) return false;
|
|
1929
|
+
return String(str).endsWith(String(suffix ?? ""));
|
|
1930
|
+
}
|
|
1931
|
+
function now() {
|
|
1932
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1933
|
+
}
|
|
1934
|
+
function today() {
|
|
1935
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1936
|
+
}
|
|
1937
|
+
function formatDate(val, style = "short") {
|
|
1938
|
+
if (!val) return "";
|
|
1939
|
+
const d = new Date(val);
|
|
1940
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1941
|
+
const styleStr = String(style).toLowerCase();
|
|
1942
|
+
if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
|
|
1943
|
+
if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
|
|
1944
|
+
return d.toLocaleDateString("en-US", {
|
|
1945
|
+
year: "numeric",
|
|
1946
|
+
month: "short",
|
|
1947
|
+
day: "numeric"
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
function addDays(val, days) {
|
|
1951
|
+
const d = val ? new Date(val) : /* @__PURE__ */ new Date();
|
|
1952
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1953
|
+
const daysNum = typeof days === "number" ? days : Number(days) || 0;
|
|
1954
|
+
d.setDate(d.getDate() + daysNum);
|
|
1955
|
+
return d.toISOString();
|
|
1956
|
+
}
|
|
1957
|
+
function diffDays(dateA, dateB) {
|
|
1958
|
+
const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
|
|
1959
|
+
const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
|
|
1960
|
+
if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
|
|
1961
|
+
const diffMs = Math.abs(dA.getTime() - dB.getTime());
|
|
1962
|
+
return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
1963
|
+
}
|
|
1964
|
+
function isPast(val) {
|
|
1965
|
+
if (!val) return false;
|
|
1966
|
+
const d = new Date(val);
|
|
1967
|
+
if (Number.isNaN(d.getTime())) return false;
|
|
1968
|
+
return d.getTime() < Date.now();
|
|
1969
|
+
}
|
|
1970
|
+
function isFuture(val) {
|
|
1971
|
+
if (!val) return false;
|
|
1972
|
+
const d = new Date(val);
|
|
1973
|
+
if (Number.isNaN(d.getTime())) return false;
|
|
1974
|
+
return d.getTime() > Date.now();
|
|
1975
|
+
}
|
|
1976
|
+
function includes(arrOrStr, item) {
|
|
1977
|
+
if (Array.isArray(arrOrStr)) {
|
|
1978
|
+
return arrOrStr.includes(item);
|
|
2011
1979
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
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;
|
|
1980
|
+
if (typeof arrOrStr === "string") {
|
|
1981
|
+
return arrOrStr.includes(String(item ?? ""));
|
|
2023
1982
|
}
|
|
2024
|
-
|
|
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;
|
|
1983
|
+
return false;
|
|
2157
1984
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
2162
|
-
var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
|
|
2163
|
-
var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
|
|
2164
|
-
function definePublishingWorkflow(options = {}) {
|
|
2165
|
-
const editors = options.editors ?? ["editor"];
|
|
2166
|
-
const publishers = options.publishers ?? ["publisher", "admin"];
|
|
2167
|
-
return {
|
|
2168
|
-
initialState: "draft",
|
|
2169
|
-
draftState: "draft",
|
|
2170
|
-
states: [
|
|
2171
|
-
{ name: "draft", label: "Draft", color: "neutral" },
|
|
2172
|
-
{ name: "in_review", label: "In review", color: "warning" },
|
|
2173
|
-
{ name: "published", label: "Published", color: "success", published: true }
|
|
2174
|
-
],
|
|
2175
|
-
transitions: [
|
|
2176
|
-
{
|
|
2177
|
-
name: "submit",
|
|
2178
|
-
label: "Submit for review",
|
|
2179
|
-
from: "draft",
|
|
2180
|
-
to: "in_review",
|
|
2181
|
-
requiredCapabilities: ["entry.submit"]
|
|
2182
|
-
},
|
|
2183
|
-
{
|
|
2184
|
-
name: "publish",
|
|
2185
|
-
label: "Publish",
|
|
2186
|
-
from: "in_review",
|
|
2187
|
-
to: "published",
|
|
2188
|
-
requiredCapabilities: ["entry.publish"]
|
|
2189
|
-
},
|
|
2190
|
-
{
|
|
2191
|
-
name: "reject",
|
|
2192
|
-
label: "Request changes",
|
|
2193
|
-
from: "in_review",
|
|
2194
|
-
to: "draft",
|
|
2195
|
-
requiredCapabilities: ["entry.publish"],
|
|
2196
|
-
requireComment: true
|
|
2197
|
-
},
|
|
2198
|
-
{
|
|
2199
|
-
name: "unpublish",
|
|
2200
|
-
label: "Unpublish",
|
|
2201
|
-
from: "published",
|
|
2202
|
-
to: "draft",
|
|
2203
|
-
requiredCapabilities: ["entry.unpublish"],
|
|
2204
|
-
unpublish: true
|
|
2205
|
-
}
|
|
2206
|
-
],
|
|
2207
|
-
roles: [
|
|
2208
|
-
...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
|
|
2209
|
-
...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
|
|
2210
|
-
]
|
|
2211
|
-
};
|
|
1985
|
+
function join(arr, separator = ", ") {
|
|
1986
|
+
if (!Array.isArray(arr)) return "";
|
|
1987
|
+
return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
|
|
2212
1988
|
}
|
|
2213
|
-
function
|
|
2214
|
-
|
|
1989
|
+
function first(arr) {
|
|
1990
|
+
if (Array.isArray(arr)) return arr[0] ?? null;
|
|
1991
|
+
return null;
|
|
2215
1992
|
}
|
|
2216
|
-
function
|
|
2217
|
-
return
|
|
2218
|
-
|
|
2219
|
-
draftState: "draft",
|
|
2220
|
-
states: [
|
|
2221
|
-
{ name: "draft", label: "Draft", color: "neutral" },
|
|
2222
|
-
{ name: "published", label: "Published", color: "success", published: true }
|
|
2223
|
-
],
|
|
2224
|
-
transitions: [
|
|
2225
|
-
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
2226
|
-
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
2227
|
-
]
|
|
2228
|
-
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
2229
|
-
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
2230
|
-
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
2231
|
-
};
|
|
1993
|
+
function last(arr) {
|
|
1994
|
+
if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
|
|
1995
|
+
return null;
|
|
2232
1996
|
}
|
|
2233
|
-
function
|
|
2234
|
-
|
|
2235
|
-
return
|
|
1997
|
+
function compact(arr) {
|
|
1998
|
+
if (!Array.isArray(arr)) return [];
|
|
1999
|
+
return arr.filter((item) => item !== void 0 && item !== null && item !== "");
|
|
2236
2000
|
}
|
|
2237
|
-
function
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
for (const mapping of workflow.roles ?? []) {
|
|
2241
|
-
if (roles.includes(mapping.role)) mapping.capabilities.forEach((capability) => capabilities.add(capability));
|
|
2242
|
-
}
|
|
2243
|
-
return capabilities;
|
|
2001
|
+
function unique(arr) {
|
|
2002
|
+
if (!Array.isArray(arr)) return [];
|
|
2003
|
+
return Array.from(new Set(arr));
|
|
2244
2004
|
}
|
|
2245
|
-
function
|
|
2246
|
-
if (
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
if (workflow.transitions.some(
|
|
2250
|
-
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
2251
|
-
)) {
|
|
2252
|
-
return true;
|
|
2253
|
-
}
|
|
2254
|
-
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
2255
|
-
return false;
|
|
2005
|
+
function length(val) {
|
|
2006
|
+
if (Array.isArray(val) || typeof val === "string") return val.length;
|
|
2007
|
+
if (val && typeof val === "object") return Object.keys(val).length;
|
|
2008
|
+
return 0;
|
|
2256
2009
|
}
|
|
2257
|
-
function
|
|
2258
|
-
const
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2010
|
+
function round(val, decimals = 0) {
|
|
2011
|
+
const num = Number(val);
|
|
2012
|
+
if (Number.isNaN(num)) return 0;
|
|
2013
|
+
const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
|
|
2014
|
+
const factor = Math.pow(10, dec);
|
|
2015
|
+
return Math.round(num * factor) / factor;
|
|
2263
2016
|
}
|
|
2264
|
-
function
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2017
|
+
function clamp(val, minVal, maxVal) {
|
|
2018
|
+
const num = Number(val) || 0;
|
|
2019
|
+
const min2 = Number(minVal) || 0;
|
|
2020
|
+
const max2 = Number(maxVal) || 0;
|
|
2021
|
+
return Math.min(Math.max(num, min2), max2);
|
|
2269
2022
|
}
|
|
2270
|
-
function
|
|
2271
|
-
return
|
|
2023
|
+
function ceil(val) {
|
|
2024
|
+
return Math.ceil(Number(val) || 0);
|
|
2272
2025
|
}
|
|
2273
|
-
function
|
|
2274
|
-
|
|
2275
|
-
const publishedState = publishedStateName(workflow);
|
|
2276
|
-
if (!meta) {
|
|
2277
|
-
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
2278
|
-
const state = publishedStateName(workflow);
|
|
2279
|
-
return {
|
|
2280
|
-
...working2,
|
|
2281
|
-
_workflow: {
|
|
2282
|
-
state,
|
|
2283
|
-
revision: 1,
|
|
2284
|
-
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
2285
|
-
}
|
|
2286
|
-
};
|
|
2287
|
-
}
|
|
2288
|
-
const { __published, __workflow, ...working } = doc;
|
|
2289
|
-
if (!canViewWorkflowDraft(workflow, user)) {
|
|
2290
|
-
if (!__published || typeof __published !== "object") {
|
|
2291
|
-
if (meta.state === publishedState) {
|
|
2292
|
-
return { ...working, _workflow: publicMetadata(meta) };
|
|
2293
|
-
}
|
|
2294
|
-
return null;
|
|
2295
|
-
}
|
|
2296
|
-
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
2297
|
-
}
|
|
2298
|
-
return {
|
|
2299
|
-
...working,
|
|
2300
|
-
_workflow: {
|
|
2301
|
-
...meta,
|
|
2302
|
-
availableTransitions: availableWorkflowTransitions(workflow, meta.state, user).map((item) => item.name)
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2026
|
+
function floor(val) {
|
|
2027
|
+
return Math.floor(Number(val) || 0);
|
|
2305
2028
|
}
|
|
2306
|
-
function
|
|
2307
|
-
return
|
|
2029
|
+
function abs(val) {
|
|
2030
|
+
return Math.abs(Number(val) || 0);
|
|
2308
2031
|
}
|
|
2309
|
-
function
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
name: args.name,
|
|
2313
|
-
collection: args.collection,
|
|
2314
|
-
documentId: args.documentId,
|
|
2315
|
-
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2316
|
-
actorId: args.actorId,
|
|
2317
|
-
payload: args.payload,
|
|
2318
|
-
attempts: 0,
|
|
2319
|
-
status: "pending"
|
|
2320
|
-
};
|
|
2032
|
+
function min(...args) {
|
|
2033
|
+
const nums = args.flat().map((n) => Number(n) || 0);
|
|
2034
|
+
return nums.length ? Math.min(...nums) : 0;
|
|
2321
2035
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2036
|
+
function max(...args) {
|
|
2037
|
+
const nums = args.flat().map((n) => Number(n) || 0);
|
|
2038
|
+
return nums.length ? Math.max(...nums) : 0;
|
|
2324
2039
|
}
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
if (!db || !config.events?.handlers.length) return;
|
|
2328
|
-
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
2329
|
-
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
2330
|
-
const attempts = event.attempts + 1;
|
|
2331
|
-
try {
|
|
2332
|
-
await db.update({
|
|
2333
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2334
|
-
id: event.id,
|
|
2335
|
-
data: { status: "processing", attempts }
|
|
2336
|
-
});
|
|
2337
|
-
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
2338
|
-
await db.update({
|
|
2339
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2340
|
-
id: event.id,
|
|
2341
|
-
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
2342
|
-
});
|
|
2343
|
-
} catch (error) {
|
|
2344
|
-
const exhausted = attempts >= maxAttempts;
|
|
2345
|
-
await db.update({
|
|
2346
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2347
|
-
id: event.id,
|
|
2348
|
-
data: {
|
|
2349
|
-
status: "failed",
|
|
2350
|
-
attempts,
|
|
2351
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
2352
|
-
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
2353
|
-
}
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2040
|
+
function pow(base, exp) {
|
|
2041
|
+
return Math.pow(Number(base) || 0, Number(exp) || 0);
|
|
2356
2042
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
2360
|
-
const result = await config.db.find({
|
|
2361
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2362
|
-
where: { status: { in: ["pending", "failed"] } },
|
|
2363
|
-
sort: "occurredAt",
|
|
2364
|
-
limit
|
|
2365
|
-
});
|
|
2366
|
-
const now2 = Date.now();
|
|
2367
|
-
const due = result.docs.filter(
|
|
2368
|
-
(doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
|
|
2369
|
-
);
|
|
2370
|
-
for (const event of due) await dispatchLifecycleEvent(config, event);
|
|
2371
|
-
return due.length;
|
|
2043
|
+
function sqrt(val) {
|
|
2044
|
+
return Math.sqrt(Number(val) || 0);
|
|
2372
2045
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
const workflow = collection.workflow;
|
|
2377
|
-
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
2378
|
-
const previous = originalDoc.__workflow;
|
|
2379
|
-
const isLegacyPublishedDoc = !previous;
|
|
2380
|
-
const baselineRevision = previous?.revision ?? 1;
|
|
2381
|
-
const event = createLifecycleEvent({
|
|
2382
|
-
name: "revision.created",
|
|
2383
|
-
collection: collection.slug,
|
|
2384
|
-
documentId: id,
|
|
2385
|
-
actorId: user?.sub,
|
|
2386
|
-
payload: {
|
|
2387
|
-
revision: baselineRevision + 1,
|
|
2388
|
-
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
2389
|
-
}
|
|
2390
|
-
});
|
|
2391
|
-
const doc = await db.transaction(async (tx) => {
|
|
2392
|
-
const nextMeta = {
|
|
2393
|
-
...previous ?? {},
|
|
2394
|
-
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
2395
|
-
revision: baselineRevision + 1,
|
|
2396
|
-
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
2397
|
-
};
|
|
2398
|
-
const updateData = {
|
|
2399
|
-
...data,
|
|
2400
|
-
__workflow: nextMeta
|
|
2401
|
-
};
|
|
2402
|
-
if (isLegacyPublishedDoc) {
|
|
2403
|
-
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
2404
|
-
updateData.__published = publishedSnapshot;
|
|
2405
|
-
}
|
|
2406
|
-
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
2407
|
-
await persistEvent(tx, event);
|
|
2408
|
-
await beforeCommit?.(tx, updated);
|
|
2409
|
-
return updated;
|
|
2410
|
-
});
|
|
2411
|
-
void dispatchLifecycleEvent(config, event);
|
|
2412
|
-
return { doc, event };
|
|
2046
|
+
function sum(arr) {
|
|
2047
|
+
if (!Array.isArray(arr)) return Number(arr) || 0;
|
|
2048
|
+
return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
|
|
2413
2049
|
}
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
if (
|
|
2418
|
-
|
|
2419
|
-
const doc = await db.transaction(async (tx) => {
|
|
2420
|
-
const created = await tx.create({ collection: collection.slug, data });
|
|
2421
|
-
event = createLifecycleEvent({
|
|
2422
|
-
name: "revision.created",
|
|
2423
|
-
collection: collection.slug,
|
|
2424
|
-
documentId: created.id,
|
|
2425
|
-
actorId: user?.sub,
|
|
2426
|
-
payload: { revision: 1, previousRevision: null }
|
|
2427
|
-
});
|
|
2428
|
-
await persistEvent(tx, event);
|
|
2429
|
-
await beforeCommit?.(tx, created);
|
|
2430
|
-
return created;
|
|
2431
|
-
});
|
|
2432
|
-
void dispatchLifecycleEvent(config, event);
|
|
2433
|
-
return { doc, event };
|
|
2050
|
+
function count(val) {
|
|
2051
|
+
if (Array.isArray(val)) return val.length;
|
|
2052
|
+
if (typeof val === "string") return val.length;
|
|
2053
|
+
if (val !== void 0 && val !== null && val !== false) return 1;
|
|
2054
|
+
return 0;
|
|
2434
2055
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2442
|
-
const meta = original.__workflow;
|
|
2443
|
-
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
2444
|
-
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
2445
|
-
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
2446
|
-
statusCode: 409
|
|
2447
|
-
});
|
|
2448
|
-
}
|
|
2449
|
-
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
2450
|
-
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
2451
|
-
if (!transition || !fromStates.includes(meta.state)) {
|
|
2452
|
-
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
2453
|
-
statusCode: 409
|
|
2454
|
-
});
|
|
2455
|
-
}
|
|
2456
|
-
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
2457
|
-
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
2056
|
+
function defaultValue(val, fallback) {
|
|
2057
|
+
if (val === void 0 || val === null || val === "") return fallback;
|
|
2058
|
+
return val;
|
|
2059
|
+
}
|
|
2060
|
+
function coalesce(...args) {
|
|
2061
|
+
for (const arg of args) {
|
|
2062
|
+
if (arg !== void 0 && arg !== null && arg !== "") return arg;
|
|
2458
2063
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2064
|
+
return null;
|
|
2065
|
+
}
|
|
2066
|
+
function isEmpty(val) {
|
|
2067
|
+
if (val === void 0 || val === null || val === "") return true;
|
|
2068
|
+
if (Array.isArray(val)) return val.length === 0;
|
|
2069
|
+
if (typeof val === "object") return Object.keys(val).length === 0;
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
function get(obj, path, fallback = null) {
|
|
2073
|
+
if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
|
|
2074
|
+
if (typeof path !== "string") return fallback;
|
|
2075
|
+
const parts = path.split(".").filter(Boolean);
|
|
2076
|
+
let curr = obj;
|
|
2077
|
+
for (const part of parts) {
|
|
2078
|
+
if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
|
|
2079
|
+
curr = curr[part];
|
|
2461
2080
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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
|
-
}
|
|
2529
|
-
await tx.create({
|
|
2530
|
-
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
2531
|
-
data: {
|
|
2532
|
-
collection: collection.slug,
|
|
2533
|
-
documentId: id,
|
|
2534
|
-
transition: transition.name,
|
|
2535
|
-
from: lockedMeta.state,
|
|
2536
|
-
to: transition.to,
|
|
2537
|
-
revision: lockedMeta.revision,
|
|
2538
|
-
comment: comment ?? null,
|
|
2539
|
-
actorId: user?.sub ?? null,
|
|
2540
|
-
createdAt: now2
|
|
2541
|
-
}
|
|
2542
|
-
});
|
|
2543
|
-
for (const event of events) await persistEvent(tx, event);
|
|
2544
|
-
if (collection.audit) {
|
|
2545
|
-
await tx.create({
|
|
2546
|
-
collection: "__audit",
|
|
2547
|
-
data: {
|
|
2548
|
-
collection: collection.slug,
|
|
2549
|
-
documentId: id,
|
|
2550
|
-
operation: "workflow.transition",
|
|
2551
|
-
user: user?.sub ?? null,
|
|
2552
|
-
timestamp: now2,
|
|
2553
|
-
changes: JSON.stringify({
|
|
2554
|
-
transition: transition.name,
|
|
2555
|
-
from: lockedMeta.state,
|
|
2556
|
-
to: transition.to,
|
|
2557
|
-
revision: lockedMeta.revision
|
|
2558
|
-
})
|
|
2559
|
-
}
|
|
2560
|
-
});
|
|
2081
|
+
return curr === void 0 || curr === null ? fallback : curr;
|
|
2082
|
+
}
|
|
2083
|
+
function registerJexlHelpers(jexlInstance = import_jexl.default) {
|
|
2084
|
+
const helpers = {
|
|
2085
|
+
// Strings
|
|
2086
|
+
slugify,
|
|
2087
|
+
lower,
|
|
2088
|
+
upper,
|
|
2089
|
+
trim,
|
|
2090
|
+
capitalize,
|
|
2091
|
+
truncate,
|
|
2092
|
+
readingTime,
|
|
2093
|
+
wordCount,
|
|
2094
|
+
replace,
|
|
2095
|
+
startsWith,
|
|
2096
|
+
endsWith,
|
|
2097
|
+
// Dates
|
|
2098
|
+
now,
|
|
2099
|
+
today,
|
|
2100
|
+
formatDate,
|
|
2101
|
+
addDays,
|
|
2102
|
+
diffDays,
|
|
2103
|
+
isPast,
|
|
2104
|
+
isFuture,
|
|
2105
|
+
// Arrays
|
|
2106
|
+
includes,
|
|
2107
|
+
join,
|
|
2108
|
+
first,
|
|
2109
|
+
last,
|
|
2110
|
+
compact,
|
|
2111
|
+
unique,
|
|
2112
|
+
length,
|
|
2113
|
+
// Math
|
|
2114
|
+
round,
|
|
2115
|
+
clamp,
|
|
2116
|
+
ceil,
|
|
2117
|
+
floor,
|
|
2118
|
+
abs,
|
|
2119
|
+
min,
|
|
2120
|
+
max,
|
|
2121
|
+
pow,
|
|
2122
|
+
sqrt,
|
|
2123
|
+
sum,
|
|
2124
|
+
count,
|
|
2125
|
+
// Objects & Logical
|
|
2126
|
+
default: defaultValue,
|
|
2127
|
+
coalesce,
|
|
2128
|
+
isEmpty,
|
|
2129
|
+
get
|
|
2130
|
+
};
|
|
2131
|
+
for (const [name, fn] of Object.entries(helpers)) {
|
|
2132
|
+
try {
|
|
2133
|
+
jexlInstance.addFunction(name, fn);
|
|
2134
|
+
} catch {
|
|
2561
2135
|
}
|
|
2562
|
-
return next;
|
|
2563
|
-
});
|
|
2564
|
-
for (const event of events) void dispatchLifecycleEvent(config, event);
|
|
2565
|
-
for (const hook of workflow.hooks?.afterTransition ?? []) {
|
|
2566
2136
|
try {
|
|
2567
|
-
|
|
2568
|
-
} catch
|
|
2569
|
-
try {
|
|
2570
|
-
const obs = config?.__observability;
|
|
2571
|
-
obs?.recordWorkflowHookFailure?.({
|
|
2572
|
-
collection: collection.slug,
|
|
2573
|
-
transition: transition.name
|
|
2574
|
-
});
|
|
2575
|
-
const logger = config?.logger;
|
|
2576
|
-
if (logger && typeof logger.child === "function") {
|
|
2577
|
-
logger.child({ component: "workflow" }).error({
|
|
2578
|
-
err: error,
|
|
2579
|
-
msg: "afterTransition hook failed",
|
|
2580
|
-
collection: collection.slug,
|
|
2581
|
-
transition: transition.name,
|
|
2582
|
-
documentId: id
|
|
2583
|
-
});
|
|
2584
|
-
} else if (logger && typeof logger.error === "function") {
|
|
2585
|
-
logger.error({
|
|
2586
|
-
err: error,
|
|
2587
|
-
msg: "afterTransition hook failed",
|
|
2588
|
-
collection: collection.slug,
|
|
2589
|
-
transition: transition.name,
|
|
2590
|
-
documentId: id
|
|
2591
|
-
});
|
|
2592
|
-
} else {
|
|
2593
|
-
console.error("afterTransition hook failed:", error);
|
|
2594
|
-
}
|
|
2595
|
-
} catch {
|
|
2596
|
-
console.error("afterTransition hook failed:", error);
|
|
2597
|
-
}
|
|
2137
|
+
jexlInstance.addTransform(name, fn);
|
|
2138
|
+
} catch {
|
|
2598
2139
|
}
|
|
2599
2140
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
const adminsCollection = collections.find((collection) => collection.slug === "__admins");
|
|
2610
|
-
if (adminsCollection) return adminsCollection;
|
|
2611
|
-
if (collectionSlug) {
|
|
2612
|
-
const requestedCollection = collections.find((collection) => collection.slug === collectionSlug);
|
|
2613
|
-
if (requestedCollection) return requestedCollection;
|
|
2141
|
+
try {
|
|
2142
|
+
jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
|
|
2143
|
+
jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
|
|
2144
|
+
jexlInstance.addBinaryOp("in", 30, (left, right) => {
|
|
2145
|
+
if (Array.isArray(right)) return right.includes(left);
|
|
2146
|
+
if (typeof right === "string") return right.includes(String(left ?? ""));
|
|
2147
|
+
return false;
|
|
2148
|
+
});
|
|
2149
|
+
} catch {
|
|
2614
2150
|
}
|
|
2615
|
-
return collections.find((collection) => collection.auth);
|
|
2616
|
-
}
|
|
2617
|
-
function getPublicAdminAuthConfig(adminAuth, collections) {
|
|
2618
|
-
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
2619
|
-
id: provider.id,
|
|
2620
|
-
type: provider.type,
|
|
2621
|
-
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
2622
|
-
autoRedirect: provider.autoRedirect
|
|
2623
|
-
}));
|
|
2624
|
-
const resolvedCollectionSlug = collections ? resolveAdminAuthCollection(collections, adminAuth?.collectionSlug)?.slug : adminAuth?.collectionSlug;
|
|
2625
|
-
return {
|
|
2626
|
-
mode: adminAuth?.mode ?? "local",
|
|
2627
|
-
collectionSlug: resolvedCollectionSlug,
|
|
2628
|
-
provisioningMode: adminAuth?.provisioningMode,
|
|
2629
|
-
providers
|
|
2630
|
-
};
|
|
2631
2151
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
if (!
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2152
|
+
registerJexlHelpers(import_jexl.default);
|
|
2153
|
+
async function evaluateJexl(expression, context = {}) {
|
|
2154
|
+
if (!expression || typeof expression !== "string") return void 0;
|
|
2155
|
+
try {
|
|
2156
|
+
const evalContext = {
|
|
2157
|
+
null: null,
|
|
2158
|
+
undefined: void 0,
|
|
2159
|
+
...context,
|
|
2160
|
+
math: Math,
|
|
2161
|
+
Math
|
|
2162
|
+
};
|
|
2163
|
+
return await import_jexl.default.eval(expression, evalContext);
|
|
2164
|
+
} catch (_err) {
|
|
2165
|
+
return void 0;
|
|
2641
2166
|
}
|
|
2642
|
-
return "admin";
|
|
2643
2167
|
}
|
|
2644
|
-
function
|
|
2645
|
-
if (!
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
if (existing && existing !== block) {
|
|
2659
|
-
throw new Error(
|
|
2660
|
-
`Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
|
|
2661
|
-
);
|
|
2662
|
-
}
|
|
2663
|
-
if (!existing) seen.set(block.slug, block);
|
|
2664
|
-
}
|
|
2665
|
-
return Array.from(seen.values());
|
|
2666
|
-
}
|
|
2667
|
-
function buildRegistry(blocks) {
|
|
2668
|
-
return new Map(blocks.map((block) => [block.slug, block]));
|
|
2669
|
-
}
|
|
2670
|
-
function resolveField(field, registry, blockCache) {
|
|
2671
|
-
const next = { ...field };
|
|
2672
|
-
if (next.fields) {
|
|
2673
|
-
next.fields = next.fields.map(
|
|
2674
|
-
(child) => resolveField(child, registry, blockCache)
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
if (next.type !== "blocks") return next;
|
|
2678
|
-
const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
|
|
2679
|
-
const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
|
|
2680
|
-
if (hasInlineBlocks && hasReferences) {
|
|
2681
|
-
throw new Error(
|
|
2682
|
-
`Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
|
|
2683
|
-
);
|
|
2684
|
-
}
|
|
2685
|
-
if (hasReferences) {
|
|
2686
|
-
next.blocks = next.blockReferences.map((slug) => {
|
|
2687
|
-
const block = registry.get(slug);
|
|
2688
|
-
if (!block) {
|
|
2689
|
-
throw new Error(
|
|
2690
|
-
`Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
|
|
2691
|
-
);
|
|
2692
|
-
}
|
|
2693
|
-
return resolveBlock(block, registry, blockCache);
|
|
2694
|
-
});
|
|
2695
|
-
return next;
|
|
2696
|
-
}
|
|
2697
|
-
if (hasInlineBlocks) {
|
|
2698
|
-
next.blocks = next.blocks.map(
|
|
2699
|
-
(block) => resolveBlock(block, registry, blockCache)
|
|
2700
|
-
);
|
|
2701
|
-
}
|
|
2702
|
-
return next;
|
|
2703
|
-
}
|
|
2704
|
-
function resolveBlock(block, registry, blockCache) {
|
|
2705
|
-
const cached = blockCache.get(block);
|
|
2706
|
-
if (cached) return cached;
|
|
2707
|
-
const resolved = {
|
|
2708
|
-
...block,
|
|
2709
|
-
fields: []
|
|
2710
|
-
};
|
|
2711
|
-
blockCache.set(block, resolved);
|
|
2712
|
-
resolved.fields = block.fields.map(
|
|
2713
|
-
(field) => resolveField(field, registry, blockCache)
|
|
2714
|
-
);
|
|
2715
|
-
return resolved;
|
|
2716
|
-
}
|
|
2717
|
-
function resolveFields(fields, registry, blockCache) {
|
|
2718
|
-
return fields.map((field) => resolveField(field, registry, blockCache));
|
|
2719
|
-
}
|
|
2720
|
-
function normalizeSchemaFragment(fragment) {
|
|
2721
|
-
const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
|
|
2722
|
-
const registry = buildRegistry(reusableBlocks);
|
|
2723
|
-
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
2724
|
-
const resolvedBlocks = reusableBlocks.map(
|
|
2725
|
-
(block) => resolveBlock(block, registry, blockCache)
|
|
2726
|
-
);
|
|
2727
|
-
const resolvedCollections = (fragment.collections ?? []).map(
|
|
2728
|
-
(collection) => ({
|
|
2729
|
-
...collection,
|
|
2730
|
-
fields: resolveFields(collection.fields ?? [], registry, blockCache)
|
|
2731
|
-
})
|
|
2732
|
-
);
|
|
2733
|
-
const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
|
|
2734
|
-
...global,
|
|
2735
|
-
fields: resolveFields(global.fields ?? [], registry, blockCache)
|
|
2736
|
-
}));
|
|
2737
|
-
return {
|
|
2738
|
-
...fragment,
|
|
2739
|
-
...fragment.blocks ? { blocks: resolvedBlocks } : {},
|
|
2740
|
-
...fragment.collections ? { collections: resolvedCollections } : {},
|
|
2741
|
-
...fragment.globals ? { globals: resolvedGlobals } : {}
|
|
2742
|
-
};
|
|
2168
|
+
function evaluateJexlSync(expression, context = {}) {
|
|
2169
|
+
if (!expression || typeof expression !== "string") return void 0;
|
|
2170
|
+
try {
|
|
2171
|
+
const evalContext = {
|
|
2172
|
+
null: null,
|
|
2173
|
+
undefined: void 0,
|
|
2174
|
+
...context,
|
|
2175
|
+
math: Math,
|
|
2176
|
+
Math
|
|
2177
|
+
};
|
|
2178
|
+
return import_jexl.default.evalSync(expression, evalContext);
|
|
2179
|
+
} catch (_err) {
|
|
2180
|
+
return void 0;
|
|
2181
|
+
}
|
|
2743
2182
|
}
|
|
2183
|
+
var BUILTIN_JEXL_HELPERS = [
|
|
2184
|
+
// Strings
|
|
2185
|
+
"slugify",
|
|
2186
|
+
"lower",
|
|
2187
|
+
"upper",
|
|
2188
|
+
"trim",
|
|
2189
|
+
"capitalize",
|
|
2190
|
+
"truncate",
|
|
2191
|
+
"readingTime",
|
|
2192
|
+
"wordCount",
|
|
2193
|
+
"replace",
|
|
2194
|
+
"startsWith",
|
|
2195
|
+
"endsWith",
|
|
2196
|
+
// Dates
|
|
2197
|
+
"now",
|
|
2198
|
+
"today",
|
|
2199
|
+
"formatDate",
|
|
2200
|
+
"addDays",
|
|
2201
|
+
"diffDays",
|
|
2202
|
+
"isPast",
|
|
2203
|
+
"isFuture",
|
|
2204
|
+
// Arrays
|
|
2205
|
+
"includes",
|
|
2206
|
+
"join",
|
|
2207
|
+
"first",
|
|
2208
|
+
"last",
|
|
2209
|
+
"compact",
|
|
2210
|
+
"unique",
|
|
2211
|
+
"length",
|
|
2212
|
+
// Math
|
|
2213
|
+
"round",
|
|
2214
|
+
"clamp",
|
|
2215
|
+
// Objects & Logical
|
|
2216
|
+
"default",
|
|
2217
|
+
"coalesce",
|
|
2218
|
+
"isEmpty",
|
|
2219
|
+
"get"
|
|
2220
|
+
];
|
|
2744
2221
|
|
|
2745
2222
|
// src/utils/declarative-hooks.ts
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
if (
|
|
2764
|
-
|
|
2223
|
+
registerJexlHelpers(import_jexl2.default);
|
|
2224
|
+
var SURFACE_ROOTS = {
|
|
2225
|
+
"collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
|
|
2226
|
+
"collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
|
|
2227
|
+
"collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
|
|
2228
|
+
"global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
|
|
2229
|
+
"global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
|
|
2230
|
+
"global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
|
|
2231
|
+
"field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
|
|
2232
|
+
"field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
|
|
2233
|
+
};
|
|
2234
|
+
var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
|
|
2235
|
+
var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
|
|
2236
|
+
var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
|
|
2237
|
+
var COMPILE_CACHE = /* @__PURE__ */ new Map();
|
|
2238
|
+
function compileExpression(expression) {
|
|
2239
|
+
const cached = COMPILE_CACHE.get(expression);
|
|
2240
|
+
if (cached) return cached;
|
|
2241
|
+
const compiled = import_jexl2.default.compile(expression);
|
|
2242
|
+
const next = { ast: compiled._ast };
|
|
2243
|
+
COMPILE_CACHE.set(expression, next);
|
|
2244
|
+
return next;
|
|
2765
2245
|
}
|
|
2766
|
-
function
|
|
2767
|
-
|
|
2768
|
-
const str = String(val).trim();
|
|
2769
|
-
if (!str) return "";
|
|
2770
|
-
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
2246
|
+
function isPlainObject(value) {
|
|
2247
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2771
2248
|
}
|
|
2772
|
-
function
|
|
2773
|
-
|
|
2774
|
-
const str = String(val);
|
|
2775
|
-
const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
|
|
2776
|
-
if (str.length <= maxLen) return str;
|
|
2777
|
-
return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
|
|
2249
|
+
function quoteSegment(value) {
|
|
2250
|
+
return `["${value.replace(/"/g, '\\"')}"]`;
|
|
2778
2251
|
}
|
|
2779
|
-
function
|
|
2780
|
-
|
|
2781
|
-
const str = String(val).trim();
|
|
2782
|
-
if (!str) return 1;
|
|
2783
|
-
const words = str.split(/\s+/).filter(Boolean).length;
|
|
2784
|
-
return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
|
|
2252
|
+
function fieldSegment(field, index) {
|
|
2253
|
+
return typeof field.name === "string" && field.name.length > 0 ? `fields${quoteSegment(field.name)}` : `fields[${index}]`;
|
|
2785
2254
|
}
|
|
2786
|
-
function
|
|
2787
|
-
|
|
2788
|
-
const str = String(val).trim();
|
|
2789
|
-
if (!str) return 0;
|
|
2790
|
-
return str.split(/\s+/).filter(Boolean).length;
|
|
2255
|
+
function blockSegment(block, index) {
|
|
2256
|
+
return typeof block.slug === "string" && block.slug.length > 0 ? `blocks${quoteSegment(block.slug)}` : `blocks[${index}]`;
|
|
2791
2257
|
}
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2794
|
-
return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
|
|
2258
|
+
function collectionSegment(collection, index) {
|
|
2259
|
+
return typeof collection.slug === "string" && collection.slug.length > 0 ? `collections${quoteSegment(collection.slug)}` : `collections[${index}]`;
|
|
2795
2260
|
}
|
|
2796
|
-
function
|
|
2797
|
-
|
|
2798
|
-
return String(str).startsWith(String(prefix ?? ""));
|
|
2261
|
+
function globalSegment(global, index) {
|
|
2262
|
+
return typeof global.slug === "string" && global.slug.length > 0 ? `globals${quoteSegment(global.slug)}` : `globals[${index}]`;
|
|
2799
2263
|
}
|
|
2800
|
-
function
|
|
2801
|
-
|
|
2802
|
-
return String(str).endsWith(String(suffix ?? ""));
|
|
2264
|
+
function colorize(value, code, enabled) {
|
|
2265
|
+
return enabled ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
2803
2266
|
}
|
|
2804
|
-
function
|
|
2805
|
-
|
|
2267
|
+
function collectRootIdentifiers(node, roots, seen) {
|
|
2268
|
+
if (!node || typeof node !== "object" || seen.has(node)) return;
|
|
2269
|
+
seen.add(node);
|
|
2270
|
+
const candidate = node;
|
|
2271
|
+
if (candidate.type === "Identifier") {
|
|
2272
|
+
if (candidate.from) {
|
|
2273
|
+
collectRootIdentifiers(candidate.from, roots, seen);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (typeof candidate.value === "string" && candidate.value !== "null") {
|
|
2277
|
+
roots.add(candidate.value);
|
|
2278
|
+
}
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
if (Array.isArray(node)) {
|
|
2282
|
+
for (const entry of node) collectRootIdentifiers(entry, roots, seen);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
for (const value of Object.values(node)) {
|
|
2286
|
+
if (Array.isArray(value)) {
|
|
2287
|
+
for (const entry of value) collectRootIdentifiers(entry, roots, seen);
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
if (value && typeof value === "object") {
|
|
2291
|
+
collectRootIdentifiers(value, roots, seen);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2806
2294
|
}
|
|
2807
|
-
function
|
|
2808
|
-
|
|
2295
|
+
function getInvalidRoots(expression, allowedRoots) {
|
|
2296
|
+
const { ast } = compileExpression(expression);
|
|
2297
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2298
|
+
collectRootIdentifiers(ast, roots, /* @__PURE__ */ new Set());
|
|
2299
|
+
const allowed = new Set(allowedRoots);
|
|
2300
|
+
return [...roots].filter((root) => !allowed.has(root));
|
|
2809
2301
|
}
|
|
2810
|
-
function
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
|
|
2832
|
-
const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
|
|
2833
|
-
if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
|
|
2834
|
-
const diffMs = Math.abs(dA.getTime() - dB.getTime());
|
|
2835
|
-
return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
2836
|
-
}
|
|
2837
|
-
function isPast(val) {
|
|
2838
|
-
if (!val) return false;
|
|
2839
|
-
const d = new Date(val);
|
|
2840
|
-
if (Number.isNaN(d.getTime())) return false;
|
|
2841
|
-
return d.getTime() < Date.now();
|
|
2842
|
-
}
|
|
2843
|
-
function isFuture(val) {
|
|
2844
|
-
if (!val) return false;
|
|
2845
|
-
const d = new Date(val);
|
|
2846
|
-
if (Number.isNaN(d.getTime())) return false;
|
|
2847
|
-
return d.getTime() > Date.now();
|
|
2848
|
-
}
|
|
2849
|
-
function includes(arrOrStr, item) {
|
|
2850
|
-
if (Array.isArray(arrOrStr)) {
|
|
2851
|
-
return arrOrStr.includes(item);
|
|
2852
|
-
}
|
|
2853
|
-
if (typeof arrOrStr === "string") {
|
|
2854
|
-
return arrOrStr.includes(String(item ?? ""));
|
|
2302
|
+
function validateDeclarativeHookExpression(expression, surface, path) {
|
|
2303
|
+
try {
|
|
2304
|
+
const invalidRoots = getInvalidRoots(expression, SURFACE_ROOTS[surface]);
|
|
2305
|
+
if (invalidRoots.length === 0) return [];
|
|
2306
|
+
return [
|
|
2307
|
+
{
|
|
2308
|
+
path,
|
|
2309
|
+
surface,
|
|
2310
|
+
expression,
|
|
2311
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${SURFACE_ROOTS[surface].map((root) => `"${root}"`).join(", ")}`
|
|
2312
|
+
}
|
|
2313
|
+
];
|
|
2314
|
+
} catch (error) {
|
|
2315
|
+
return [
|
|
2316
|
+
{
|
|
2317
|
+
path,
|
|
2318
|
+
surface,
|
|
2319
|
+
expression,
|
|
2320
|
+
message: `has invalid syntax: ${error.message}`
|
|
2321
|
+
}
|
|
2322
|
+
];
|
|
2855
2323
|
}
|
|
2856
|
-
return false;
|
|
2857
|
-
}
|
|
2858
|
-
function join(arr, separator = ", ") {
|
|
2859
|
-
if (!Array.isArray(arr)) return "";
|
|
2860
|
-
return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
|
|
2861
|
-
}
|
|
2862
|
-
function first(arr) {
|
|
2863
|
-
if (Array.isArray(arr)) return arr[0] ?? null;
|
|
2864
|
-
return null;
|
|
2865
|
-
}
|
|
2866
|
-
function last(arr) {
|
|
2867
|
-
if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
|
|
2868
|
-
return null;
|
|
2869
2324
|
}
|
|
2870
|
-
function
|
|
2871
|
-
|
|
2872
|
-
|
|
2325
|
+
function validateDeclarativeAccessExpression(expression, surface, path) {
|
|
2326
|
+
try {
|
|
2327
|
+
const invalidRoots = getInvalidRoots(expression, ACCESS_ROOTS);
|
|
2328
|
+
if (invalidRoots.length === 0) return [];
|
|
2329
|
+
return [
|
|
2330
|
+
{
|
|
2331
|
+
path,
|
|
2332
|
+
surface,
|
|
2333
|
+
expression,
|
|
2334
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${ACCESS_ROOTS.map((root) => `"${root}"`).join(", ")}`
|
|
2335
|
+
}
|
|
2336
|
+
];
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
return [
|
|
2339
|
+
{
|
|
2340
|
+
path,
|
|
2341
|
+
surface,
|
|
2342
|
+
expression,
|
|
2343
|
+
message: `has invalid syntax: ${error.message}`
|
|
2344
|
+
}
|
|
2345
|
+
];
|
|
2346
|
+
}
|
|
2873
2347
|
}
|
|
2874
|
-
function
|
|
2875
|
-
|
|
2876
|
-
|
|
2348
|
+
function validateAdminConditionExpression(expression, path, allowedRoots) {
|
|
2349
|
+
try {
|
|
2350
|
+
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
2351
|
+
if (invalidRoots.length === 0) return [];
|
|
2352
|
+
return [
|
|
2353
|
+
{
|
|
2354
|
+
path,
|
|
2355
|
+
surface: "field.admin.condition",
|
|
2356
|
+
expression,
|
|
2357
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
2358
|
+
}
|
|
2359
|
+
];
|
|
2360
|
+
} catch (error) {
|
|
2361
|
+
return [
|
|
2362
|
+
{
|
|
2363
|
+
path,
|
|
2364
|
+
surface: "field.admin.condition",
|
|
2365
|
+
expression,
|
|
2366
|
+
message: `has invalid syntax: ${error.message}`
|
|
2367
|
+
}
|
|
2368
|
+
];
|
|
2369
|
+
}
|
|
2877
2370
|
}
|
|
2878
|
-
function
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2371
|
+
function validatePreviewUrlExpression(expression, path, allowedRoots) {
|
|
2372
|
+
try {
|
|
2373
|
+
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
2374
|
+
if (invalidRoots.length === 0) return [];
|
|
2375
|
+
return [
|
|
2376
|
+
{
|
|
2377
|
+
path,
|
|
2378
|
+
surface: "collection.admin.previewUrl",
|
|
2379
|
+
expression,
|
|
2380
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
2381
|
+
}
|
|
2382
|
+
];
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
return [
|
|
2385
|
+
{
|
|
2386
|
+
path,
|
|
2387
|
+
surface: "collection.admin.previewUrl",
|
|
2388
|
+
expression,
|
|
2389
|
+
message: `has invalid syntax: ${error.message}`
|
|
2390
|
+
}
|
|
2391
|
+
];
|
|
2392
|
+
}
|
|
2882
2393
|
}
|
|
2883
|
-
function
|
|
2884
|
-
const
|
|
2885
|
-
if (
|
|
2886
|
-
|
|
2887
|
-
const factor = Math.pow(10, dec);
|
|
2888
|
-
return Math.round(num * factor) / factor;
|
|
2394
|
+
function assertValidDeclarativeHookExpression(expression, surface, path) {
|
|
2395
|
+
const issues = validateDeclarativeHookExpression(expression, surface, path);
|
|
2396
|
+
if (issues.length === 0) return;
|
|
2397
|
+
throw new Error(`Invalid declarative hook at ${path}: ${issues[0].message}`);
|
|
2889
2398
|
}
|
|
2890
|
-
function
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
const max2 = Number(maxVal) || 0;
|
|
2894
|
-
return Math.min(Math.max(num, min2), max2);
|
|
2399
|
+
function assertValidDeclarativeHookResult(result, surface, path) {
|
|
2400
|
+
if (isPlainObject(result)) return;
|
|
2401
|
+
throw new Error(`Invalid declarative hook result at ${path}: ${surface} expressions must return an object`);
|
|
2895
2402
|
}
|
|
2896
|
-
function
|
|
2897
|
-
|
|
2403
|
+
function collectFieldIssues(fields, path, issues) {
|
|
2404
|
+
fields.forEach((field, index) => {
|
|
2405
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2406
|
+
field.hooks?.beforeChange?.forEach((hook, hookIndex) => {
|
|
2407
|
+
if (typeof hook !== "string") return;
|
|
2408
|
+
issues.push(
|
|
2409
|
+
...validateDeclarativeHookExpression(
|
|
2410
|
+
hook,
|
|
2411
|
+
"field.beforeChange",
|
|
2412
|
+
`${fieldPath}.hooks.beforeChange[${hookIndex}]`
|
|
2413
|
+
)
|
|
2414
|
+
);
|
|
2415
|
+
});
|
|
2416
|
+
field.hooks?.afterRead?.forEach((hook, hookIndex) => {
|
|
2417
|
+
if (typeof hook !== "string") return;
|
|
2418
|
+
issues.push({
|
|
2419
|
+
path: `${fieldPath}.hooks.afterRead[${hookIndex}]`,
|
|
2420
|
+
surface: "unsupported",
|
|
2421
|
+
expression: hook,
|
|
2422
|
+
message: "declarative strings are not supported on field hooks.afterRead"
|
|
2423
|
+
});
|
|
2424
|
+
});
|
|
2425
|
+
if (typeof field.admin?.hooks?.onChange === "string") {
|
|
2426
|
+
issues.push(
|
|
2427
|
+
...validateDeclarativeHookExpression(
|
|
2428
|
+
field.admin.hooks.onChange,
|
|
2429
|
+
"field.admin.onChange",
|
|
2430
|
+
`${fieldPath}.admin.hooks.onChange`
|
|
2431
|
+
)
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof field.admin?.hooks?.options === "string") {
|
|
2435
|
+
issues.push({
|
|
2436
|
+
path: `${fieldPath}.admin.hooks.options`,
|
|
2437
|
+
surface: "unsupported",
|
|
2438
|
+
expression: field.admin.hooks.options,
|
|
2439
|
+
message: "declarative strings are not supported on field admin.hooks.options"
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
if (Array.isArray(field.fields)) {
|
|
2443
|
+
collectFieldIssues(field.fields, fieldPath, issues);
|
|
2444
|
+
}
|
|
2445
|
+
if (Array.isArray(field.blocks)) {
|
|
2446
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2447
|
+
if (!Array.isArray(block.fields)) return;
|
|
2448
|
+
collectFieldIssues(block.fields, `${fieldPath}.${blockSegment(block, blockIndex)}`, issues);
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2898
2452
|
}
|
|
2899
|
-
function
|
|
2900
|
-
return
|
|
2453
|
+
function collectFieldNames(fields) {
|
|
2454
|
+
return fields.flatMap(
|
|
2455
|
+
(field) => typeof field.name === "string" && field.name.length > 0 ? [field.name] : []
|
|
2456
|
+
);
|
|
2901
2457
|
}
|
|
2902
|
-
function
|
|
2903
|
-
|
|
2458
|
+
function collectAdminConditionIssues(fields, path, rootFieldNames, issues) {
|
|
2459
|
+
const scopeFieldNames = collectFieldNames(fields);
|
|
2460
|
+
const allowedRoots = [.../* @__PURE__ */ new Set([...ADMIN_CONDITION_RESERVED_ROOTS, ...rootFieldNames, ...scopeFieldNames])];
|
|
2461
|
+
fields.forEach((field, index) => {
|
|
2462
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2463
|
+
if (typeof field.admin?.condition === "string") {
|
|
2464
|
+
issues.push(
|
|
2465
|
+
...validateAdminConditionExpression(
|
|
2466
|
+
field.admin.condition,
|
|
2467
|
+
`${fieldPath}.admin.condition`,
|
|
2468
|
+
allowedRoots
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
if (Array.isArray(field.fields)) {
|
|
2473
|
+
collectAdminConditionIssues(field.fields, fieldPath, rootFieldNames, issues);
|
|
2474
|
+
}
|
|
2475
|
+
if (Array.isArray(field.blocks)) {
|
|
2476
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2477
|
+
if (!Array.isArray(block.fields)) return;
|
|
2478
|
+
collectAdminConditionIssues(
|
|
2479
|
+
block.fields,
|
|
2480
|
+
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
2481
|
+
rootFieldNames,
|
|
2482
|
+
issues
|
|
2483
|
+
);
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2904
2487
|
}
|
|
2905
|
-
function
|
|
2906
|
-
|
|
2907
|
-
|
|
2488
|
+
function collectFieldAccessIssues(fields, path, issues) {
|
|
2489
|
+
fields.forEach((field, index) => {
|
|
2490
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2491
|
+
if (typeof field.access?.read === "string") {
|
|
2492
|
+
issues.push(...validateDeclarativeAccessExpression(field.access.read, "field.access", `${fieldPath}.access.read`));
|
|
2493
|
+
}
|
|
2494
|
+
if (typeof field.access?.create === "string") {
|
|
2495
|
+
issues.push(
|
|
2496
|
+
...validateDeclarativeAccessExpression(field.access.create, "field.access", `${fieldPath}.access.create`)
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
if (typeof field.access?.update === "string") {
|
|
2500
|
+
issues.push(
|
|
2501
|
+
...validateDeclarativeAccessExpression(field.access.update, "field.access", `${fieldPath}.access.update`)
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
if (Array.isArray(field.fields)) {
|
|
2505
|
+
collectFieldAccessIssues(field.fields, fieldPath, issues);
|
|
2506
|
+
}
|
|
2507
|
+
if (Array.isArray(field.blocks)) {
|
|
2508
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2509
|
+
if (!Array.isArray(block.fields)) return;
|
|
2510
|
+
collectFieldAccessIssues(
|
|
2511
|
+
block.fields,
|
|
2512
|
+
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
2513
|
+
issues
|
|
2514
|
+
);
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2908
2518
|
}
|
|
2909
|
-
function
|
|
2910
|
-
|
|
2911
|
-
|
|
2519
|
+
function collectHookIssues(hooks, scope, path, issues) {
|
|
2520
|
+
if (!hooks) return;
|
|
2521
|
+
const supported = {
|
|
2522
|
+
beforeRead: `${scope}.beforeRead`,
|
|
2523
|
+
afterRead: `${scope}.afterRead`,
|
|
2524
|
+
beforeChange: `${scope}.beforeChange`
|
|
2525
|
+
};
|
|
2526
|
+
for (const [key, value] of Object.entries(hooks)) {
|
|
2527
|
+
if (!Array.isArray(value)) continue;
|
|
2528
|
+
value.forEach((hook, index) => {
|
|
2529
|
+
if (typeof hook !== "string") return;
|
|
2530
|
+
const surface = supported[key];
|
|
2531
|
+
if (!surface) {
|
|
2532
|
+
issues.push({
|
|
2533
|
+
path: `${path}.${key}[${index}]`,
|
|
2534
|
+
surface: "unsupported",
|
|
2535
|
+
expression: hook,
|
|
2536
|
+
message: `declarative strings are not supported on ${scope} hooks.${key}`
|
|
2537
|
+
});
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
issues.push(...validateDeclarativeHookExpression(hook, surface, `${path}.${key}[${index}]`));
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2912
2543
|
}
|
|
2913
|
-
function
|
|
2914
|
-
|
|
2544
|
+
function collectDeclarativeHookValidationIssues(config) {
|
|
2545
|
+
const issues = [];
|
|
2546
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2547
|
+
if (!Array.isArray(block.fields)) return;
|
|
2548
|
+
collectFieldIssues(
|
|
2549
|
+
block.fields,
|
|
2550
|
+
blockSegment(block, blockIndex),
|
|
2551
|
+
issues
|
|
2552
|
+
);
|
|
2553
|
+
});
|
|
2554
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2555
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2556
|
+
collectHookIssues(
|
|
2557
|
+
collection.hooks,
|
|
2558
|
+
"collection",
|
|
2559
|
+
`${collectionPath}.hooks`,
|
|
2560
|
+
issues
|
|
2561
|
+
);
|
|
2562
|
+
collectFieldIssues(collection.fields, collectionPath, issues);
|
|
2563
|
+
});
|
|
2564
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2565
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2566
|
+
collectHookIssues(
|
|
2567
|
+
global.hooks,
|
|
2568
|
+
"global",
|
|
2569
|
+
`${globalPath}.hooks`,
|
|
2570
|
+
issues
|
|
2571
|
+
);
|
|
2572
|
+
collectFieldIssues(global.fields, globalPath, issues);
|
|
2573
|
+
});
|
|
2574
|
+
return issues;
|
|
2915
2575
|
}
|
|
2916
|
-
function
|
|
2917
|
-
|
|
2576
|
+
function collectDeclarativeAccessValidationIssues(config) {
|
|
2577
|
+
const issues = [];
|
|
2578
|
+
Object.entries(config.accessPolicies ?? {}).forEach(([name, value]) => {
|
|
2579
|
+
if (typeof value !== "string") return;
|
|
2580
|
+
issues.push(
|
|
2581
|
+
...validateDeclarativeAccessExpression(value, "config.accessPolicy", `accessPolicies.${name}`)
|
|
2582
|
+
);
|
|
2583
|
+
});
|
|
2584
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2585
|
+
if (!Array.isArray(block.fields)) return;
|
|
2586
|
+
collectFieldAccessIssues(
|
|
2587
|
+
block.fields,
|
|
2588
|
+
blockSegment(block, blockIndex),
|
|
2589
|
+
issues
|
|
2590
|
+
);
|
|
2591
|
+
});
|
|
2592
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2593
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2594
|
+
if (typeof collection.access?.read === "string") {
|
|
2595
|
+
issues.push(
|
|
2596
|
+
...validateDeclarativeAccessExpression(
|
|
2597
|
+
collection.access.read,
|
|
2598
|
+
"collection.access",
|
|
2599
|
+
`${collectionPath}.access.read`
|
|
2600
|
+
)
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
if (typeof collection.access?.create === "string") {
|
|
2604
|
+
issues.push(
|
|
2605
|
+
...validateDeclarativeAccessExpression(
|
|
2606
|
+
collection.access.create,
|
|
2607
|
+
"collection.access",
|
|
2608
|
+
`${collectionPath}.access.create`
|
|
2609
|
+
)
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
if (typeof collection.access?.update === "string") {
|
|
2613
|
+
issues.push(
|
|
2614
|
+
...validateDeclarativeAccessExpression(
|
|
2615
|
+
collection.access.update,
|
|
2616
|
+
"collection.access",
|
|
2617
|
+
`${collectionPath}.access.update`
|
|
2618
|
+
)
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
if (typeof collection.access?.delete === "string") {
|
|
2622
|
+
issues.push(
|
|
2623
|
+
...validateDeclarativeAccessExpression(
|
|
2624
|
+
collection.access.delete,
|
|
2625
|
+
"collection.access",
|
|
2626
|
+
`${collectionPath}.access.delete`
|
|
2627
|
+
)
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
if (typeof collection.access?.readAudit === "string") {
|
|
2631
|
+
issues.push(
|
|
2632
|
+
...validateDeclarativeAccessExpression(
|
|
2633
|
+
collection.access.readAudit,
|
|
2634
|
+
"collection.access",
|
|
2635
|
+
`${collectionPath}.access.readAudit`
|
|
2636
|
+
)
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
collectFieldAccessIssues(collection.fields, collectionPath, issues);
|
|
2640
|
+
});
|
|
2641
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2642
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2643
|
+
if (typeof global.access?.read === "string") {
|
|
2644
|
+
issues.push(
|
|
2645
|
+
...validateDeclarativeAccessExpression(global.access.read, "global.access", `${globalPath}.access.read`)
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
if (typeof global.access?.update === "string") {
|
|
2649
|
+
issues.push(
|
|
2650
|
+
...validateDeclarativeAccessExpression(
|
|
2651
|
+
global.access.update,
|
|
2652
|
+
"global.access",
|
|
2653
|
+
`${globalPath}.access.update`
|
|
2654
|
+
)
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
collectFieldAccessIssues(global.fields, globalPath, issues);
|
|
2658
|
+
});
|
|
2659
|
+
return issues;
|
|
2918
2660
|
}
|
|
2919
|
-
function
|
|
2920
|
-
|
|
2921
|
-
|
|
2661
|
+
function collectAdminConditionValidationIssues(config) {
|
|
2662
|
+
const issues = [];
|
|
2663
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2664
|
+
if (!Array.isArray(block.fields)) return;
|
|
2665
|
+
collectAdminConditionIssues(
|
|
2666
|
+
block.fields,
|
|
2667
|
+
blockSegment(block, blockIndex),
|
|
2668
|
+
collectFieldNames(block.fields),
|
|
2669
|
+
issues
|
|
2670
|
+
);
|
|
2671
|
+
});
|
|
2672
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2673
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2674
|
+
const rootFieldNames = collectFieldNames(collection.fields);
|
|
2675
|
+
collectAdminConditionIssues(collection.fields, collectionPath, rootFieldNames, issues);
|
|
2676
|
+
});
|
|
2677
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2678
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2679
|
+
const rootFieldNames = collectFieldNames(global.fields);
|
|
2680
|
+
collectAdminConditionIssues(global.fields, globalPath, rootFieldNames, issues);
|
|
2681
|
+
});
|
|
2682
|
+
return issues;
|
|
2922
2683
|
}
|
|
2923
|
-
function
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2684
|
+
function collectPreviewUrlValidationIssues(config) {
|
|
2685
|
+
const issues = [];
|
|
2686
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2687
|
+
if (typeof collection.admin?.previewUrl !== "string") return;
|
|
2688
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2689
|
+
const allowedRoots = [
|
|
2690
|
+
.../* @__PURE__ */ new Set([...PREVIEW_URL_RESERVED_ROOTS, ...collectFieldNames(collection.fields)])
|
|
2691
|
+
];
|
|
2692
|
+
issues.push(
|
|
2693
|
+
...validatePreviewUrlExpression(
|
|
2694
|
+
collection.admin.previewUrl,
|
|
2695
|
+
`${collectionPath}.admin.previewUrl`,
|
|
2696
|
+
allowedRoots
|
|
2697
|
+
)
|
|
2698
|
+
);
|
|
2699
|
+
});
|
|
2700
|
+
return issues;
|
|
2928
2701
|
}
|
|
2929
|
-
function
|
|
2930
|
-
|
|
2931
|
-
|
|
2702
|
+
function assertValidAdminConditionsInConfig(config, source = "config") {
|
|
2703
|
+
const diagnostics = collectAdminConditionValidationIssues(config).map((issue) => ({
|
|
2704
|
+
severity: "error",
|
|
2705
|
+
source: "adminCondition",
|
|
2706
|
+
path: issue.path,
|
|
2707
|
+
surface: issue.surface,
|
|
2708
|
+
message: issue.message,
|
|
2709
|
+
expression: issue.expression
|
|
2710
|
+
}));
|
|
2711
|
+
if (diagnostics.length === 0) return;
|
|
2712
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
2932
2713
|
}
|
|
2933
|
-
function
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2714
|
+
function assertValidPreviewUrlsInConfig(config, source = "config") {
|
|
2715
|
+
const diagnostics = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
2716
|
+
severity: "error",
|
|
2717
|
+
source: "previewUrl",
|
|
2718
|
+
path: issue.path,
|
|
2719
|
+
surface: issue.surface,
|
|
2720
|
+
message: issue.message,
|
|
2721
|
+
expression: issue.expression
|
|
2722
|
+
}));
|
|
2723
|
+
if (diagnostics.length === 0) return;
|
|
2724
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
2938
2725
|
}
|
|
2939
|
-
function
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2726
|
+
function collectConfigDiagnostics(config) {
|
|
2727
|
+
const hookIssues = collectDeclarativeHookValidationIssues(config).map((issue) => ({
|
|
2728
|
+
severity: "error",
|
|
2729
|
+
source: "declarativeHook",
|
|
2730
|
+
path: issue.path,
|
|
2731
|
+
surface: issue.surface,
|
|
2732
|
+
message: issue.message,
|
|
2733
|
+
expression: issue.expression
|
|
2734
|
+
}));
|
|
2735
|
+
const accessIssues = collectDeclarativeAccessValidationIssues(config).map((issue) => ({
|
|
2736
|
+
severity: "error",
|
|
2737
|
+
source: "declarativeAccess",
|
|
2738
|
+
path: issue.path,
|
|
2739
|
+
surface: issue.surface,
|
|
2740
|
+
message: issue.message,
|
|
2741
|
+
expression: issue.expression
|
|
2742
|
+
}));
|
|
2743
|
+
const adminConditionIssues = collectAdminConditionValidationIssues(config).map((issue) => ({
|
|
2744
|
+
severity: "error",
|
|
2745
|
+
source: "adminCondition",
|
|
2746
|
+
path: issue.path,
|
|
2747
|
+
surface: issue.surface,
|
|
2748
|
+
message: issue.message,
|
|
2749
|
+
expression: issue.expression
|
|
2750
|
+
}));
|
|
2751
|
+
const previewUrlIssues = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
2752
|
+
severity: "error",
|
|
2753
|
+
source: "previewUrl",
|
|
2754
|
+
path: issue.path,
|
|
2755
|
+
surface: issue.surface,
|
|
2756
|
+
message: issue.message,
|
|
2757
|
+
expression: issue.expression
|
|
2758
|
+
}));
|
|
2759
|
+
return [...hookIssues, ...accessIssues, ...adminConditionIssues, ...previewUrlIssues];
|
|
2760
|
+
}
|
|
2761
|
+
function formatConfigDiagnostics(source, diagnostics, options = {}) {
|
|
2762
|
+
const color = options.color ?? false;
|
|
2763
|
+
const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
|
|
2764
|
+
const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
|
|
2765
|
+
const lines = diagnostics.flatMap((issue, index) => [
|
|
2766
|
+
`${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
|
|
2767
|
+
` ${issue.message}`,
|
|
2768
|
+
` ${colorize("surface:", 90, color)} ${issue.surface}`
|
|
2769
|
+
]);
|
|
2770
|
+
return `${header}
|
|
2771
|
+
${lines.join("\n")}
|
|
2772
|
+
|
|
2773
|
+
${docLink}`;
|
|
2774
|
+
}
|
|
2775
|
+
var ConfigValidationError = class extends Error {
|
|
2776
|
+
diagnostics;
|
|
2777
|
+
source;
|
|
2778
|
+
constructor(source, diagnostics) {
|
|
2779
|
+
super(formatConfigDiagnostics(source, diagnostics));
|
|
2780
|
+
this.name = "ConfigValidationError";
|
|
2781
|
+
this.source = source;
|
|
2782
|
+
this.diagnostics = diagnostics;
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
function isConfigValidationError(error) {
|
|
2786
|
+
return error instanceof ConfigValidationError;
|
|
2787
|
+
}
|
|
2788
|
+
function assertValidDeclarativeHooksInConfig(config, source = "config") {
|
|
2789
|
+
const diagnostics = collectDeclarativeHookValidationIssues(config).map((issue) => ({
|
|
2790
|
+
severity: "error",
|
|
2791
|
+
source: "declarativeHook",
|
|
2792
|
+
path: issue.path,
|
|
2793
|
+
surface: issue.surface,
|
|
2794
|
+
message: issue.message,
|
|
2795
|
+
expression: issue.expression
|
|
2796
|
+
}));
|
|
2797
|
+
if (diagnostics.length === 0) return;
|
|
2798
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
2799
|
+
}
|
|
2800
|
+
function assertValidDeclarativeAccessInConfig(config, source = "config") {
|
|
2801
|
+
const diagnostics = collectDeclarativeAccessValidationIssues(config).map((issue) => ({
|
|
2802
|
+
severity: "error",
|
|
2803
|
+
source: "declarativeAccess",
|
|
2804
|
+
path: issue.path,
|
|
2805
|
+
surface: issue.surface,
|
|
2806
|
+
message: issue.message,
|
|
2807
|
+
expression: issue.expression
|
|
2808
|
+
}));
|
|
2809
|
+
if (diagnostics.length === 0) return;
|
|
2810
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/utils/hooks.ts
|
|
2814
|
+
async function runCollectionHooks(hooks, args, options = {}) {
|
|
2815
|
+
if (!hooks || hooks.length === 0) {
|
|
2816
|
+
return args.data ?? args.doc ?? args.query ?? void 0;
|
|
2817
|
+
}
|
|
2818
|
+
const payloadKey = args.data !== void 0 ? "data" : args.doc !== void 0 ? "doc" : "query" in args ? "query" : void 0;
|
|
2819
|
+
let currentPayload = payloadKey === void 0 ? void 0 : args[payloadKey];
|
|
2820
|
+
for (const hook of hooks) {
|
|
2821
|
+
try {
|
|
2822
|
+
const hookArgs = {
|
|
2823
|
+
...args,
|
|
2824
|
+
...payloadKey !== void 0 ? { [payloadKey]: currentPayload } : {}
|
|
2825
|
+
};
|
|
2826
|
+
const result = typeof hook === "string" ? await (() => {
|
|
2827
|
+
if (options.surface) {
|
|
2828
|
+
assertValidDeclarativeHookExpression(
|
|
2829
|
+
hook,
|
|
2830
|
+
options.surface,
|
|
2831
|
+
options.path ?? options.surface
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
return import_jexl3.default.eval(hook, hookArgs);
|
|
2835
|
+
})() : await hook(hookArgs);
|
|
2836
|
+
if (result !== void 0) {
|
|
2837
|
+
if (typeof hook === "string" && options.surface) {
|
|
2838
|
+
assertValidDeclarativeHookResult(
|
|
2839
|
+
result,
|
|
2840
|
+
options.surface,
|
|
2841
|
+
options.path ?? options.surface
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
currentPayload = typeof hook === "string" && currentPayload && typeof currentPayload === "object" && !Array.isArray(currentPayload) && result && typeof result === "object" && !Array.isArray(result) ? {
|
|
2845
|
+
...currentPayload,
|
|
2846
|
+
...result
|
|
2847
|
+
} : result;
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
if (options.isolated) {
|
|
2851
|
+
console.error("Side-effect hook failed after a successful write", err);
|
|
2852
|
+
} else {
|
|
2853
|
+
throw err;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return currentPayload;
|
|
2858
|
+
}
|
|
2859
|
+
async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pathPrefix = "") {
|
|
2860
|
+
if (!data || typeof data !== "object") return data;
|
|
2861
|
+
const result = { ...data };
|
|
2862
|
+
for (const field of fields) {
|
|
2863
|
+
if (!field.name) continue;
|
|
2864
|
+
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
2865
|
+
const value = result[field.name];
|
|
2866
|
+
const origValue = originalDoc?.[field.name];
|
|
2867
|
+
let updatedValue = value;
|
|
2868
|
+
if (field.hooks?.beforeChange) {
|
|
2869
|
+
for (const hook of field.hooks.beforeChange) {
|
|
2870
|
+
const hookArgs = {
|
|
2871
|
+
value: updatedValue,
|
|
2872
|
+
originalDoc: originalDoc ?? void 0,
|
|
2873
|
+
data: result,
|
|
2874
|
+
user,
|
|
2875
|
+
db
|
|
2876
|
+
};
|
|
2877
|
+
updatedValue = typeof hook === "string" ? await (() => {
|
|
2878
|
+
assertValidDeclarativeHookExpression(
|
|
2879
|
+
hook,
|
|
2880
|
+
"field.beforeChange",
|
|
2881
|
+
fieldPath
|
|
2882
|
+
);
|
|
2883
|
+
return import_jexl3.default.eval(hook, hookArgs);
|
|
2884
|
+
})() : await hook(hookArgs);
|
|
2885
|
+
}
|
|
2886
|
+
result[field.name] = updatedValue;
|
|
2887
|
+
}
|
|
2888
|
+
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
2889
|
+
if (field.type === "object" && field.fields) {
|
|
2890
|
+
result[field.name] = await executeFieldBeforeChange(
|
|
2891
|
+
field.fields,
|
|
2892
|
+
updatedValue,
|
|
2893
|
+
origValue,
|
|
2894
|
+
user,
|
|
2895
|
+
db,
|
|
2896
|
+
fieldPath
|
|
2897
|
+
);
|
|
2898
|
+
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
2899
|
+
const arrayResult = [];
|
|
2900
|
+
for (let i = 0; i < updatedValue.length; i++) {
|
|
2901
|
+
const item = updatedValue[i];
|
|
2902
|
+
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
2903
|
+
arrayResult.push(
|
|
2904
|
+
await executeFieldBeforeChange(
|
|
2905
|
+
field.fields,
|
|
2906
|
+
item,
|
|
2907
|
+
origItem,
|
|
2908
|
+
user,
|
|
2909
|
+
db,
|
|
2910
|
+
fieldPath
|
|
2911
|
+
)
|
|
2912
|
+
);
|
|
2913
|
+
}
|
|
2914
|
+
result[field.name] = arrayResult;
|
|
2915
|
+
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
2916
|
+
const blocksResult = [];
|
|
2917
|
+
for (let i = 0; i < updatedValue.length; i++) {
|
|
2918
|
+
const blockData = updatedValue[i];
|
|
2919
|
+
const origBlock = Array.isArray(origValue) ? origValue[i] : null;
|
|
2920
|
+
const blockConfig = field.blocks.find(
|
|
2921
|
+
(b) => b.slug === blockData.blockType
|
|
2922
|
+
);
|
|
2923
|
+
if (blockConfig) {
|
|
2924
|
+
blocksResult.push(
|
|
2925
|
+
await executeFieldBeforeChange(
|
|
2926
|
+
blockConfig.fields,
|
|
2927
|
+
blockData,
|
|
2928
|
+
origBlock,
|
|
2929
|
+
user,
|
|
2930
|
+
db,
|
|
2931
|
+
fieldPath
|
|
2932
|
+
)
|
|
2933
|
+
);
|
|
2934
|
+
} else {
|
|
2935
|
+
blocksResult.push(blockData);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
result[field.name] = blocksResult;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return result;
|
|
2943
|
+
}
|
|
2944
|
+
async function executeFieldAfterRead(fields, doc, user, db) {
|
|
2945
|
+
if (!doc || typeof doc !== "object") return doc;
|
|
2946
|
+
const result = { ...doc };
|
|
2947
|
+
for (const field of fields) {
|
|
2948
|
+
if (!field.name) continue;
|
|
2949
|
+
const value = result[field.name];
|
|
2950
|
+
let updatedValue = value;
|
|
2951
|
+
if (field.hooks?.afterRead) {
|
|
2952
|
+
for (const hook of field.hooks.afterRead) {
|
|
2953
|
+
const typedHook = hook;
|
|
2954
|
+
const hookArgs = {
|
|
2955
|
+
value: updatedValue,
|
|
2956
|
+
doc: result,
|
|
2957
|
+
user,
|
|
2958
|
+
db
|
|
2959
|
+
};
|
|
2960
|
+
updatedValue = await typedHook(hookArgs);
|
|
2961
|
+
}
|
|
2962
|
+
result[field.name] = updatedValue;
|
|
2963
|
+
}
|
|
2964
|
+
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
2965
|
+
if (field.type === "object" && field.fields) {
|
|
2966
|
+
result[field.name] = await executeFieldAfterRead(
|
|
2967
|
+
field.fields,
|
|
2968
|
+
updatedValue,
|
|
2969
|
+
user,
|
|
2970
|
+
db
|
|
2971
|
+
);
|
|
2972
|
+
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
2973
|
+
const arrayResult = [];
|
|
2974
|
+
for (const item of updatedValue) {
|
|
2975
|
+
arrayResult.push(
|
|
2976
|
+
await executeFieldAfterRead(
|
|
2977
|
+
field.fields,
|
|
2978
|
+
item,
|
|
2979
|
+
user,
|
|
2980
|
+
db
|
|
2981
|
+
)
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
result[field.name] = arrayResult;
|
|
2985
|
+
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
2986
|
+
const blocksResult = [];
|
|
2987
|
+
for (const blockData of updatedValue) {
|
|
2988
|
+
const typedBlock = blockData;
|
|
2989
|
+
const blockConfig = field.blocks.find(
|
|
2990
|
+
(b) => b.slug === typedBlock.blockType
|
|
2991
|
+
);
|
|
2992
|
+
if (blockConfig) {
|
|
2993
|
+
blocksResult.push(
|
|
2994
|
+
await executeFieldAfterRead(
|
|
2995
|
+
blockConfig.fields,
|
|
2996
|
+
typedBlock,
|
|
2997
|
+
user,
|
|
2998
|
+
db
|
|
2999
|
+
)
|
|
3000
|
+
);
|
|
3001
|
+
} else {
|
|
3002
|
+
blocksResult.push(blockData);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
result[field.name] = blocksResult;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return result;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/trash.ts
|
|
3013
|
+
var TRASH_COLLECTION = "__trash";
|
|
3014
|
+
var TRASH_COLLECTION_CONFIG = {
|
|
3015
|
+
slug: TRASH_COLLECTION,
|
|
3016
|
+
labels: { singular: "Trash entry", plural: "Trash entries" },
|
|
3017
|
+
fields: [
|
|
3018
|
+
{ name: "collection", type: "text", required: true, promoted: true },
|
|
3019
|
+
{ name: "docId", type: "text", required: true, promoted: true },
|
|
3020
|
+
{ name: "deletedAt", type: "number", required: true, promoted: true },
|
|
3021
|
+
// epoch ms
|
|
3022
|
+
{ name: "purgeAt", type: "number", promoted: true },
|
|
3023
|
+
// epoch ms, null = never
|
|
3024
|
+
{ name: "deletedBy", type: "text", promoted: true },
|
|
3025
|
+
{ name: "title", type: "text" },
|
|
3026
|
+
// resolved useAsTitle, for listing
|
|
3027
|
+
{ name: "snapshot", type: "json", required: true },
|
|
3028
|
+
// the raw stored document
|
|
3029
|
+
{ name: "createdAt", type: "date" }
|
|
3030
|
+
// original doc timestamps live in snapshot
|
|
3031
|
+
],
|
|
3032
|
+
indexes: [
|
|
3033
|
+
{ fields: ["purgeAt"] },
|
|
3034
|
+
{ fields: ["collection", "docId"] }
|
|
3035
|
+
],
|
|
3036
|
+
access: {
|
|
3037
|
+
read: () => false,
|
|
3038
|
+
create: () => false,
|
|
3039
|
+
update: () => false,
|
|
3040
|
+
delete: () => false
|
|
3041
|
+
},
|
|
3042
|
+
admin: { hidden: true }
|
|
3043
|
+
};
|
|
3044
|
+
function getTrashEntryId(collection, docId) {
|
|
3045
|
+
return `${collection}:${docId}`;
|
|
3046
|
+
}
|
|
3047
|
+
function resolveTrashConfig(collection, appConfig) {
|
|
3048
|
+
const colTrash = collection.trash;
|
|
3049
|
+
const appTrash = appConfig?.trash;
|
|
3050
|
+
let enabled = true;
|
|
3051
|
+
if (colTrash === false) {
|
|
3052
|
+
enabled = false;
|
|
3053
|
+
} else if (colTrash === true || typeof colTrash === "object" && colTrash !== null) {
|
|
3054
|
+
enabled = true;
|
|
3055
|
+
} else if (appTrash?.enabled === false) {
|
|
3056
|
+
enabled = false;
|
|
3057
|
+
}
|
|
3058
|
+
if (!enabled) {
|
|
3059
|
+
return {
|
|
3060
|
+
enabled: false,
|
|
3061
|
+
retentionDays: null,
|
|
3062
|
+
allowPermanentDelete: true
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
let retentionDays = 30;
|
|
3066
|
+
if (typeof colTrash === "object" && colTrash !== null && "retentionDays" in colTrash) {
|
|
3067
|
+
retentionDays = colTrash.retentionDays ?? null;
|
|
3068
|
+
} else if (appTrash?.retentionDays !== void 0) {
|
|
3069
|
+
retentionDays = appTrash.retentionDays ?? null;
|
|
3070
|
+
}
|
|
3071
|
+
const allowPermanentDelete = typeof colTrash === "object" && colTrash !== null && colTrash.allowPermanentDelete !== void 0 ? colTrash.allowPermanentDelete : appTrash?.allowPermanentDelete !== void 0 ? appTrash.allowPermanentDelete : true;
|
|
3072
|
+
return {
|
|
3073
|
+
enabled: true,
|
|
3074
|
+
retentionDays,
|
|
3075
|
+
allowPermanentDelete
|
|
3076
|
+
};
|
|
2944
3077
|
}
|
|
2945
|
-
function
|
|
2946
|
-
if (
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
for (const part of parts) {
|
|
2951
|
-
if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
|
|
2952
|
-
curr = curr[part];
|
|
3078
|
+
function logTrashWarning(config, payload) {
|
|
3079
|
+
if (config?.logger && typeof config.logger.warn === "function") {
|
|
3080
|
+
config.logger.warn(payload);
|
|
3081
|
+
} else if (typeof console !== "undefined" && console.warn) {
|
|
3082
|
+
console.warn(`[dyrected] ${payload.msg}`);
|
|
2953
3083
|
}
|
|
2954
|
-
return curr === void 0 || curr === null ? fallback : curr;
|
|
2955
3084
|
}
|
|
2956
|
-
function
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
// Dates
|
|
2971
|
-
now,
|
|
2972
|
-
today,
|
|
2973
|
-
formatDate,
|
|
2974
|
-
addDays,
|
|
2975
|
-
diffDays,
|
|
2976
|
-
isPast,
|
|
2977
|
-
isFuture,
|
|
2978
|
-
// Arrays
|
|
2979
|
-
includes,
|
|
2980
|
-
join,
|
|
2981
|
-
first,
|
|
2982
|
-
last,
|
|
2983
|
-
compact,
|
|
2984
|
-
unique,
|
|
2985
|
-
length,
|
|
2986
|
-
// Math
|
|
2987
|
-
round,
|
|
2988
|
-
clamp,
|
|
2989
|
-
ceil,
|
|
2990
|
-
floor,
|
|
2991
|
-
abs,
|
|
2992
|
-
min,
|
|
2993
|
-
max,
|
|
2994
|
-
pow,
|
|
2995
|
-
sqrt,
|
|
2996
|
-
sum,
|
|
2997
|
-
count,
|
|
2998
|
-
// Objects & Logical
|
|
2999
|
-
default: defaultValue,
|
|
3000
|
-
coalesce,
|
|
3001
|
-
isEmpty,
|
|
3002
|
-
get
|
|
3003
|
-
};
|
|
3004
|
-
for (const [name, fn] of Object.entries(helpers)) {
|
|
3005
|
-
try {
|
|
3006
|
-
jexlInstance.addFunction(name, fn);
|
|
3007
|
-
} catch {
|
|
3085
|
+
function assertValidTrashInConfig(config, source = "config") {
|
|
3086
|
+
if (config.trash) {
|
|
3087
|
+
const { retentionDays } = config.trash;
|
|
3088
|
+
if (retentionDays !== void 0 && retentionDays !== null) {
|
|
3089
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 1) {
|
|
3090
|
+
throw new Error(
|
|
3091
|
+
`Invalid trash retentionDays in ${source}: must be an integer >= 1. Received ${retentionDays}.`
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
if (retentionDays > 3650) {
|
|
3095
|
+
logTrashWarning(config, {
|
|
3096
|
+
msg: `Trash retentionDays in ${source} is unusually large (${retentionDays} days > 10 years). Check if value was provided in hours or seconds.`
|
|
3097
|
+
});
|
|
3098
|
+
}
|
|
3008
3099
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3100
|
+
}
|
|
3101
|
+
for (const col of config.collections || []) {
|
|
3102
|
+
if (col.slug === "trash") {
|
|
3103
|
+
throw new Error(`Collection slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3104
|
+
}
|
|
3105
|
+
const isSystem = col.slug.startsWith("__");
|
|
3106
|
+
if (isSystem && col.trash) {
|
|
3107
|
+
throw new Error(`System collection "${col.slug}" cannot enable trash.`);
|
|
3108
|
+
}
|
|
3109
|
+
if (col.trash === false) {
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
if (typeof col.trash === "object" && col.trash !== null) {
|
|
3113
|
+
const { retentionDays } = col.trash;
|
|
3114
|
+
if (retentionDays !== void 0 && retentionDays !== null) {
|
|
3115
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 1) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
`Invalid trash retentionDays for collection "${col.slug}" in ${source}: must be an integer >= 1. Received ${retentionDays}. Use trash: false for immediate hard delete.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
if (retentionDays > 3650) {
|
|
3121
|
+
logTrashWarning(config, {
|
|
3122
|
+
msg: `Trash retentionDays for collection "${col.slug}" is unusually large (${retentionDays} days > 10 years). Check if value was provided in hours or seconds.`,
|
|
3123
|
+
collection: col.slug
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3012
3127
|
}
|
|
3013
3128
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
if (Array.isArray(right)) return right.includes(left);
|
|
3019
|
-
if (typeof right === "string") return right.includes(String(left ?? ""));
|
|
3020
|
-
return false;
|
|
3021
|
-
});
|
|
3022
|
-
} catch {
|
|
3129
|
+
for (const glob of config.globals || []) {
|
|
3130
|
+
if (glob.slug === "trash") {
|
|
3131
|
+
throw new Error(`Global slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3132
|
+
}
|
|
3023
3133
|
}
|
|
3024
3134
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
return
|
|
3037
|
-
} catch (_err) {
|
|
3038
|
-
return void 0;
|
|
3135
|
+
function resolveDocumentTitle(collection, doc) {
|
|
3136
|
+
if (!doc || typeof doc !== "object") return "";
|
|
3137
|
+
const titleField = collection.admin?.useAsTitle || "title";
|
|
3138
|
+
const val = doc[titleField];
|
|
3139
|
+
if (val !== void 0 && val !== null && String(val).trim().length > 0) {
|
|
3140
|
+
return String(val);
|
|
3141
|
+
}
|
|
3142
|
+
if (doc.name !== void 0 && doc.name !== null && String(doc.name).trim().length > 0) {
|
|
3143
|
+
return String(doc.name);
|
|
3144
|
+
}
|
|
3145
|
+
if (doc.id !== void 0 && doc.id !== null) {
|
|
3146
|
+
return String(doc.id);
|
|
3039
3147
|
}
|
|
3148
|
+
return "";
|
|
3040
3149
|
}
|
|
3041
|
-
function
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3150
|
+
function createTrashPurgeTask(config) {
|
|
3151
|
+
return {
|
|
3152
|
+
name: "dyrected:trash-purge",
|
|
3153
|
+
cron: config.trash?.purge?.cron ?? "0 3 * * *",
|
|
3154
|
+
run: async ({ db, logger, signal }) => {
|
|
3155
|
+
const batchSize = Math.max(1, config.trash?.purge?.batchSize ?? 200);
|
|
3156
|
+
while (true) {
|
|
3157
|
+
if (signal?.aborted) return;
|
|
3158
|
+
const now2 = Date.now();
|
|
3159
|
+
const due = await db.find({
|
|
3160
|
+
collection: TRASH_COLLECTION,
|
|
3161
|
+
where: {
|
|
3162
|
+
purgeAt: { lte: now2, not_equals: null }
|
|
3163
|
+
},
|
|
3164
|
+
limit: batchSize,
|
|
3165
|
+
sort: "purgeAt"
|
|
3166
|
+
});
|
|
3167
|
+
if (!due.docs.length) break;
|
|
3168
|
+
for (const entry of due.docs) {
|
|
3169
|
+
if (signal?.aborted) return;
|
|
3170
|
+
if (entry.purgeAt == null || typeof entry.purgeAt !== "number") continue;
|
|
3171
|
+
try {
|
|
3172
|
+
const colSlug = entry.collection;
|
|
3173
|
+
const colConfig = config.collections.find((c) => c.slug === colSlug);
|
|
3174
|
+
const snapshot = typeof entry.snapshot === "string" ? JSON.parse(entry.snapshot) : entry.snapshot;
|
|
3175
|
+
if (colConfig?.upload && config.storage && snapshot?.filename) {
|
|
3176
|
+
try {
|
|
3177
|
+
await config.storage.delete({ filename: snapshot.filename });
|
|
3178
|
+
if (snapshot.sizes && typeof snapshot.sizes === "object") {
|
|
3179
|
+
for (const size of Object.values(snapshot.sizes)) {
|
|
3180
|
+
if (size?.filename) {
|
|
3181
|
+
await config.storage.delete({ filename: size.filename });
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
} catch (storageErr) {
|
|
3186
|
+
logger.warn({ err: storageErr?.message || storageErr, docId: entry.docId, collection: colSlug }, "Failed to delete storage file on purge");
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
if (colConfig?.hooks?.afterDelete) {
|
|
3190
|
+
await runCollectionHooks(
|
|
3191
|
+
colConfig.hooks.afterDelete,
|
|
3192
|
+
{
|
|
3193
|
+
id: entry.docId,
|
|
3194
|
+
doc: snapshot,
|
|
3195
|
+
user: void 0,
|
|
3196
|
+
req: {},
|
|
3197
|
+
db,
|
|
3198
|
+
mode: "permanent"
|
|
3199
|
+
},
|
|
3200
|
+
{ isolated: true }
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
if (colConfig?.audit) {
|
|
3204
|
+
try {
|
|
3205
|
+
await db.create({
|
|
3206
|
+
collection: "__audit",
|
|
3207
|
+
data: {
|
|
3208
|
+
collection: colSlug,
|
|
3209
|
+
documentId: entry.docId,
|
|
3210
|
+
operation: "purge",
|
|
3211
|
+
user: null,
|
|
3212
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3213
|
+
changes: JSON.stringify({
|
|
3214
|
+
before: snapshot,
|
|
3215
|
+
after: null
|
|
3216
|
+
})
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
} catch (auditErr) {
|
|
3220
|
+
logger.warn({ err: auditErr?.message || auditErr, docId: entry.docId, collection: colSlug }, "Failed to write audit log on purge");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
await db.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3224
|
+
} catch (entryErr) {
|
|
3225
|
+
logger.error({ err: entryErr?.message || entryErr, entryId: entry.id }, "Error purging trash entry");
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
if (due.docs.length < batchSize) break;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
async function softDeleteDocument(options) {
|
|
3234
|
+
const { db, config, collection: collectionSlug, id, deletedBy } = options;
|
|
3235
|
+
const colConfig = config.collections.find((c) => c.slug === collectionSlug);
|
|
3236
|
+
if (!colConfig) throw new Error(`Collection ${collectionSlug} not found`);
|
|
3237
|
+
const doc = await db.findOne({ collection: collectionSlug, id });
|
|
3238
|
+
if (!doc) throw new Error(`Document ${id} in ${collectionSlug} not found`);
|
|
3239
|
+
const resolvedTrash = resolveTrashConfig(colConfig, config);
|
|
3240
|
+
const deletedAt = Date.now();
|
|
3241
|
+
const purgeAt = resolvedTrash.retentionDays !== null && resolvedTrash.retentionDays !== void 0 ? deletedAt + resolvedTrash.retentionDays * 864e5 : null;
|
|
3242
|
+
const title = resolveDocumentTitle(colConfig, doc);
|
|
3243
|
+
const trashId = getTrashEntryId(collectionSlug, id);
|
|
3244
|
+
const trashData = {
|
|
3245
|
+
id: trashId,
|
|
3246
|
+
collection: collectionSlug,
|
|
3247
|
+
docId: id,
|
|
3248
|
+
deletedAt,
|
|
3249
|
+
purgeAt,
|
|
3250
|
+
deletedBy: deletedBy ?? null,
|
|
3251
|
+
title,
|
|
3252
|
+
snapshot: doc,
|
|
3253
|
+
createdAt: doc.createdAt ?? new Date(deletedAt).toISOString()
|
|
3254
|
+
};
|
|
3255
|
+
if (db.transaction) {
|
|
3256
|
+
await db.transaction(async (tx) => {
|
|
3257
|
+
await tx.create({ collection: TRASH_COLLECTION, data: trashData });
|
|
3258
|
+
await tx.delete({ collection: collectionSlug, id });
|
|
3259
|
+
});
|
|
3260
|
+
} else {
|
|
3261
|
+
await db.create({ collection: TRASH_COLLECTION, data: trashData });
|
|
3262
|
+
await db.delete({ collection: collectionSlug, id });
|
|
3263
|
+
}
|
|
3264
|
+
return { id: trashId, purgeAt };
|
|
3265
|
+
}
|
|
3266
|
+
async function restoreDocument(options) {
|
|
3267
|
+
const { db, config, collection: collectionSlug, trashId: param, overrides } = options;
|
|
3268
|
+
const colConfig = config.collections.find((c) => c.slug === collectionSlug);
|
|
3269
|
+
if (!colConfig) throw new Error(`Collection ${collectionSlug} not found`);
|
|
3270
|
+
const trashId = param.includes(":") ? param : getTrashEntryId(collectionSlug, param);
|
|
3271
|
+
const entry = await db.findOne({ collection: TRASH_COLLECTION, id: trashId }) || await db.findOne({ collection: TRASH_COLLECTION, id: param });
|
|
3272
|
+
if (!entry || entry.collection !== collectionSlug) {
|
|
3273
|
+
throw new Error(`Trash entry ${param} not found for collection ${collectionSlug}`);
|
|
3274
|
+
}
|
|
3275
|
+
const snapshot = typeof entry.snapshot === "string" ? JSON.parse(entry.snapshot) : entry.snapshot;
|
|
3276
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3277
|
+
const docToRestore = {
|
|
3278
|
+
...snapshot,
|
|
3279
|
+
...overrides || {},
|
|
3280
|
+
id: entry.docId,
|
|
3281
|
+
createdAt: snapshot.createdAt ?? entry.createdAt ?? now2,
|
|
3282
|
+
updatedAt: now2
|
|
3283
|
+
};
|
|
3284
|
+
if (db.transaction) {
|
|
3285
|
+
await db.transaction(async (tx) => {
|
|
3286
|
+
await tx.create({ collection: collectionSlug, data: docToRestore });
|
|
3287
|
+
await tx.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3288
|
+
});
|
|
3289
|
+
} else {
|
|
3290
|
+
await db.create({ collection: collectionSlug, data: docToRestore });
|
|
3291
|
+
await db.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3054
3292
|
}
|
|
3293
|
+
return { restoredDoc: docToRestore };
|
|
3055
3294
|
}
|
|
3056
|
-
var BUILTIN_JEXL_HELPERS = [
|
|
3057
|
-
// Strings
|
|
3058
|
-
"slugify",
|
|
3059
|
-
"lower",
|
|
3060
|
-
"upper",
|
|
3061
|
-
"trim",
|
|
3062
|
-
"capitalize",
|
|
3063
|
-
"truncate",
|
|
3064
|
-
"readingTime",
|
|
3065
|
-
"wordCount",
|
|
3066
|
-
"replace",
|
|
3067
|
-
"startsWith",
|
|
3068
|
-
"endsWith",
|
|
3069
|
-
// Dates
|
|
3070
|
-
"now",
|
|
3071
|
-
"today",
|
|
3072
|
-
"formatDate",
|
|
3073
|
-
"addDays",
|
|
3074
|
-
"diffDays",
|
|
3075
|
-
"isPast",
|
|
3076
|
-
"isFuture",
|
|
3077
|
-
// Arrays
|
|
3078
|
-
"includes",
|
|
3079
|
-
"join",
|
|
3080
|
-
"first",
|
|
3081
|
-
"last",
|
|
3082
|
-
"compact",
|
|
3083
|
-
"unique",
|
|
3084
|
-
"length",
|
|
3085
|
-
// Math
|
|
3086
|
-
"round",
|
|
3087
|
-
"clamp",
|
|
3088
|
-
// Objects & Logical
|
|
3089
|
-
"default",
|
|
3090
|
-
"coalesce",
|
|
3091
|
-
"isEmpty",
|
|
3092
|
-
"get"
|
|
3093
|
-
];
|
|
3094
3295
|
|
|
3095
|
-
// src/
|
|
3096
|
-
|
|
3097
|
-
var
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3296
|
+
// src/tasks.ts
|
|
3297
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
3298
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
3299
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
3300
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
3301
|
+
fields: [
|
|
3302
|
+
{ name: "name", type: "text", required: true },
|
|
3303
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
3304
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
3305
|
+
{ name: "lockedBy", type: "text" },
|
|
3306
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
3307
|
+
{ name: "lastStartedAt", type: "date" },
|
|
3308
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
3309
|
+
{ name: "lastStatus", type: "text" },
|
|
3310
|
+
{ name: "lastError", type: "textarea" }
|
|
3311
|
+
],
|
|
3312
|
+
access: {
|
|
3313
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
3314
|
+
create: () => false,
|
|
3315
|
+
update: () => false,
|
|
3316
|
+
delete: () => false
|
|
3317
|
+
},
|
|
3318
|
+
admin: { hidden: true }
|
|
3106
3319
|
};
|
|
3107
|
-
var
|
|
3108
|
-
var
|
|
3109
|
-
var
|
|
3110
|
-
var
|
|
3111
|
-
function
|
|
3112
|
-
const
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3320
|
+
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
3321
|
+
var DEFAULT_TICK_MS = 3e4;
|
|
3322
|
+
var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
3323
|
+
var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
3324
|
+
function parseCronField(raw, min2, max2, names, nameOffset, label) {
|
|
3325
|
+
const values = /* @__PURE__ */ new Set();
|
|
3326
|
+
const toNumber = (token) => {
|
|
3327
|
+
const named = names?.indexOf(token.toLowerCase());
|
|
3328
|
+
if (named !== void 0 && named >= 0) return named + nameOffset;
|
|
3329
|
+
if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
|
|
3330
|
+
return Number(token);
|
|
3331
|
+
};
|
|
3332
|
+
for (const part of raw.split(",")) {
|
|
3333
|
+
const [rangePart, stepPart] = part.split("/");
|
|
3334
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
3335
|
+
if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
|
|
3336
|
+
let start;
|
|
3337
|
+
let end;
|
|
3338
|
+
if (rangePart === "*") {
|
|
3339
|
+
start = min2;
|
|
3340
|
+
end = max2;
|
|
3341
|
+
} else if (rangePart.includes("-")) {
|
|
3342
|
+
const [a, b] = rangePart.split("-");
|
|
3343
|
+
start = toNumber(a);
|
|
3344
|
+
end = toNumber(b);
|
|
3345
|
+
} else {
|
|
3346
|
+
start = toNumber(rangePart);
|
|
3347
|
+
end = stepPart === void 0 ? start : max2;
|
|
3348
|
+
}
|
|
3349
|
+
if (start < min2 || end > max2 || start > end) {
|
|
3350
|
+
throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
|
|
3351
|
+
}
|
|
3352
|
+
for (let v = start; v <= end; v += step) values.add(v);
|
|
3353
|
+
}
|
|
3354
|
+
return values;
|
|
3121
3355
|
}
|
|
3122
|
-
function
|
|
3123
|
-
|
|
3356
|
+
function parseCron(expression) {
|
|
3357
|
+
const fields = expression.trim().split(/\s+/);
|
|
3358
|
+
if (fields.length !== 5) {
|
|
3359
|
+
throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
|
|
3360
|
+
}
|
|
3361
|
+
const [min2, hour, dom, mon, dow] = fields;
|
|
3362
|
+
const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
|
|
3363
|
+
if (daysOfWeek.has(7)) daysOfWeek.add(0);
|
|
3364
|
+
daysOfWeek.delete(7);
|
|
3365
|
+
return {
|
|
3366
|
+
minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
|
|
3367
|
+
hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
|
|
3368
|
+
daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
|
|
3369
|
+
months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
|
|
3370
|
+
daysOfWeek,
|
|
3371
|
+
dayFieldsBothRestricted: dom !== "*" && dow !== "*"
|
|
3372
|
+
};
|
|
3124
3373
|
}
|
|
3125
|
-
function
|
|
3126
|
-
|
|
3374
|
+
function dayMatches(schedule, date) {
|
|
3375
|
+
const domOk = schedule.daysOfMonth.has(date.getUTCDate());
|
|
3376
|
+
const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
|
|
3377
|
+
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
3127
3378
|
}
|
|
3128
|
-
function
|
|
3129
|
-
|
|
3379
|
+
function nextCronRun(expression, after) {
|
|
3380
|
+
const schedule = typeof expression === "string" ? parseCron(expression) : expression;
|
|
3381
|
+
const cursor = new Date(after.getTime());
|
|
3382
|
+
cursor.setUTCSeconds(0, 0);
|
|
3383
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
|
|
3384
|
+
const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
3385
|
+
while (cursor.getTime() <= limit) {
|
|
3386
|
+
if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
|
|
3387
|
+
cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
|
|
3388
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
3389
|
+
continue;
|
|
3390
|
+
}
|
|
3391
|
+
if (!dayMatches(schedule, cursor)) {
|
|
3392
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
3393
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
if (!schedule.hours.has(cursor.getUTCHours())) {
|
|
3397
|
+
cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
if (!schedule.minutes.has(cursor.getUTCMinutes())) {
|
|
3401
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
|
|
3402
|
+
continue;
|
|
3403
|
+
}
|
|
3404
|
+
return cursor;
|
|
3405
|
+
}
|
|
3406
|
+
throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
|
|
3130
3407
|
}
|
|
3131
|
-
function
|
|
3132
|
-
|
|
3408
|
+
function defineTask(task) {
|
|
3409
|
+
if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
|
|
3410
|
+
if (task.cron !== void 0) parseCron(task.cron);
|
|
3411
|
+
if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
|
|
3412
|
+
throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
|
|
3413
|
+
}
|
|
3414
|
+
return task;
|
|
3133
3415
|
}
|
|
3134
|
-
function
|
|
3135
|
-
|
|
3416
|
+
function resolveLogger(config, taskName) {
|
|
3417
|
+
const base = config.logger;
|
|
3418
|
+
if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
|
|
3419
|
+
const prefix = `[dyrected/tasks] ${taskName}`;
|
|
3420
|
+
return {
|
|
3421
|
+
info: (obj, msg) => console.info(prefix, msg ?? "", obj),
|
|
3422
|
+
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
3423
|
+
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
3424
|
+
};
|
|
3136
3425
|
}
|
|
3137
|
-
function
|
|
3138
|
-
|
|
3426
|
+
function floorToMinute(date) {
|
|
3427
|
+
const d = new Date(date.getTime());
|
|
3428
|
+
d.setUTCSeconds(0, 0);
|
|
3429
|
+
return d;
|
|
3139
3430
|
}
|
|
3140
|
-
function
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
if (
|
|
3146
|
-
|
|
3147
|
-
|
|
3431
|
+
function createTaskRunner(config) {
|
|
3432
|
+
const db = config.db;
|
|
3433
|
+
if (!db) throw new Error("createTaskRunner requires config.db.");
|
|
3434
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
3435
|
+
for (const task of config.tasks ?? []) {
|
|
3436
|
+
if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
|
|
3437
|
+
tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
|
|
3438
|
+
}
|
|
3439
|
+
const hasRetention = (config.collections ?? []).some((col) => {
|
|
3440
|
+
const resolved = resolveTrashConfig(col, config);
|
|
3441
|
+
return resolved.enabled && resolved.retentionDays !== null && resolved.retentionDays > 0;
|
|
3442
|
+
});
|
|
3443
|
+
if (hasRetention && !tasks.has("dyrected:trash-purge")) {
|
|
3444
|
+
const purgeTask = createTrashPurgeTask(config);
|
|
3445
|
+
tasks.set(purgeTask.name, { ...purgeTask, schedule: purgeTask.cron ? parseCron(purgeTask.cron) : void 0 });
|
|
3446
|
+
}
|
|
3447
|
+
const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3448
|
+
let timer;
|
|
3449
|
+
let inFlight = Promise.resolve();
|
|
3450
|
+
let ticking = false;
|
|
3451
|
+
let ready;
|
|
3452
|
+
function ensureReady() {
|
|
3453
|
+
ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
|
|
3454
|
+
ready.catch(() => {
|
|
3455
|
+
ready = void 0;
|
|
3456
|
+
});
|
|
3457
|
+
return ready;
|
|
3458
|
+
}
|
|
3459
|
+
async function ensureLockRow(name, now2) {
|
|
3460
|
+
await ensureReady();
|
|
3461
|
+
const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
3462
|
+
if (existing) return existing;
|
|
3463
|
+
try {
|
|
3464
|
+
return await db.create({
|
|
3465
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3466
|
+
data: {
|
|
3467
|
+
id: name,
|
|
3468
|
+
name,
|
|
3469
|
+
lockedUntil: 0,
|
|
3470
|
+
lockedBy: null,
|
|
3471
|
+
lastScheduledFor: floorToMinute(now2).toISOString(),
|
|
3472
|
+
lastStartedAt: null,
|
|
3473
|
+
lastFinishedAt: null,
|
|
3474
|
+
lastStatus: null,
|
|
3475
|
+
lastError: null
|
|
3476
|
+
}
|
|
3477
|
+
});
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
3480
|
+
if (created) return created;
|
|
3481
|
+
throw error;
|
|
3148
3482
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3483
|
+
}
|
|
3484
|
+
async function execute(task, row, scheduledFor) {
|
|
3485
|
+
const started = Date.now();
|
|
3486
|
+
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
3487
|
+
const token = `${instanceId}:${started}`;
|
|
3488
|
+
const where = {
|
|
3489
|
+
id: { equals: task.name },
|
|
3490
|
+
lockedUntil: { less_than: started }
|
|
3491
|
+
};
|
|
3492
|
+
if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
|
|
3493
|
+
const claimed = await db.update({
|
|
3494
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3495
|
+
where,
|
|
3496
|
+
data: {
|
|
3497
|
+
lockedUntil: started + lockMs,
|
|
3498
|
+
lockedBy: token,
|
|
3499
|
+
lastStartedAt: new Date(started).toISOString(),
|
|
3500
|
+
...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
if (claimed.affectedRows !== 1) {
|
|
3504
|
+
return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
|
|
3151
3505
|
}
|
|
3152
|
-
|
|
3506
|
+
const logger = resolveLogger(config, task.name);
|
|
3507
|
+
const controller = new AbortController();
|
|
3508
|
+
let timeout;
|
|
3509
|
+
let status = "completed";
|
|
3510
|
+
let errorMessage;
|
|
3511
|
+
try {
|
|
3512
|
+
await Promise.race([
|
|
3513
|
+
Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
|
|
3514
|
+
new Promise((_, reject) => {
|
|
3515
|
+
timeout = setTimeout(() => {
|
|
3516
|
+
reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
|
|
3517
|
+
controller.abort();
|
|
3518
|
+
}, lockMs);
|
|
3519
|
+
})
|
|
3520
|
+
]);
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
status = "failed";
|
|
3523
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3524
|
+
logger.error({ err: error }, "Task failed");
|
|
3525
|
+
} finally {
|
|
3526
|
+
if (timeout) clearTimeout(timeout);
|
|
3527
|
+
}
|
|
3528
|
+
try {
|
|
3529
|
+
await db.update({
|
|
3530
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3531
|
+
where: { id: { equals: task.name }, lockedBy: { equals: token } },
|
|
3532
|
+
data: {
|
|
3533
|
+
lockedUntil: 0,
|
|
3534
|
+
lockedBy: null,
|
|
3535
|
+
lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3536
|
+
lastStatus: status,
|
|
3537
|
+
lastError: errorMessage ?? null
|
|
3538
|
+
}
|
|
3539
|
+
});
|
|
3540
|
+
} catch (error) {
|
|
3541
|
+
logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
|
|
3542
|
+
}
|
|
3543
|
+
return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
|
|
3153
3544
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
return;
|
|
3545
|
+
function track(work) {
|
|
3546
|
+
inFlight = inFlight.then(() => work).catch(() => void 0);
|
|
3547
|
+
return work;
|
|
3157
3548
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3549
|
+
const runner = {
|
|
3550
|
+
async runTask(name) {
|
|
3551
|
+
const task = tasks.get(name);
|
|
3552
|
+
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
3553
|
+
const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
3554
|
+
return track(execute(task, row, void 0));
|
|
3555
|
+
},
|
|
3556
|
+
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
3557
|
+
const results = [];
|
|
3558
|
+
for (const task of tasks.values()) {
|
|
3559
|
+
if (!task.schedule) continue;
|
|
3560
|
+
const row = await ensureLockRow(task.name, now2);
|
|
3561
|
+
const reference = new Date(String(row.lastScheduledFor));
|
|
3562
|
+
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
3563
|
+
if (!due) {
|
|
3564
|
+
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
3565
|
+
continue;
|
|
3566
|
+
}
|
|
3567
|
+
results.push(await track(execute(task, row, floorToMinute(now2))));
|
|
3568
|
+
}
|
|
3569
|
+
return results;
|
|
3570
|
+
},
|
|
3571
|
+
start(options) {
|
|
3572
|
+
if (timer) return;
|
|
3573
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
|
|
3574
|
+
const tick = () => {
|
|
3575
|
+
if (ticking) return;
|
|
3576
|
+
ticking = true;
|
|
3577
|
+
runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
|
|
3578
|
+
ticking = false;
|
|
3579
|
+
});
|
|
3580
|
+
};
|
|
3581
|
+
timer = setInterval(tick, intervalMs);
|
|
3582
|
+
timer.unref?.();
|
|
3583
|
+
setTimeout(tick, 0);
|
|
3584
|
+
},
|
|
3585
|
+
async stop() {
|
|
3586
|
+
if (timer) clearInterval(timer);
|
|
3587
|
+
timer = void 0;
|
|
3588
|
+
await inFlight;
|
|
3589
|
+
},
|
|
3590
|
+
getTasks() {
|
|
3591
|
+
return Array.from(tasks.values()).map((t) => ({ name: t.name, cron: t.cron }));
|
|
3165
3592
|
}
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
function getInvalidRoots(expression, allowedRoots) {
|
|
3169
|
-
const { ast } = compileExpression(expression);
|
|
3170
|
-
const roots = /* @__PURE__ */ new Set();
|
|
3171
|
-
collectRootIdentifiers(ast, roots, /* @__PURE__ */ new Set());
|
|
3172
|
-
const allowed = new Set(allowedRoots);
|
|
3173
|
-
return [...roots].filter((root) => !allowed.has(root));
|
|
3593
|
+
};
|
|
3594
|
+
return runner;
|
|
3174
3595
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3596
|
+
|
|
3597
|
+
// src/workflows.ts
|
|
3598
|
+
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
3599
|
+
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
3600
|
+
var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
|
|
3601
|
+
var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
|
|
3602
|
+
function definePublishingWorkflow(options = {}) {
|
|
3603
|
+
const editors = options.editors ?? ["editor"];
|
|
3604
|
+
const publishers = options.publishers ?? ["publisher", "admin"];
|
|
3605
|
+
return {
|
|
3606
|
+
initialState: "draft",
|
|
3607
|
+
draftState: "draft",
|
|
3608
|
+
states: [
|
|
3609
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
3610
|
+
{ name: "in_review", label: "In review", color: "warning" },
|
|
3611
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
3612
|
+
],
|
|
3613
|
+
transitions: [
|
|
3180
3614
|
{
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
} catch (error) {
|
|
3188
|
-
return [
|
|
3615
|
+
name: "submit",
|
|
3616
|
+
label: "Submit for review",
|
|
3617
|
+
from: "draft",
|
|
3618
|
+
to: "in_review",
|
|
3619
|
+
requiredCapabilities: ["entry.submit"]
|
|
3620
|
+
},
|
|
3189
3621
|
{
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
}
|
|
3197
|
-
}
|
|
3198
|
-
function validateDeclarativeAccessExpression(expression, surface, path) {
|
|
3199
|
-
try {
|
|
3200
|
-
const invalidRoots = getInvalidRoots(expression, ACCESS_ROOTS);
|
|
3201
|
-
if (invalidRoots.length === 0) return [];
|
|
3202
|
-
return [
|
|
3622
|
+
name: "publish",
|
|
3623
|
+
label: "Publish",
|
|
3624
|
+
from: "in_review",
|
|
3625
|
+
to: "published",
|
|
3626
|
+
requiredCapabilities: ["entry.publish"]
|
|
3627
|
+
},
|
|
3203
3628
|
{
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
return [
|
|
3629
|
+
name: "reject",
|
|
3630
|
+
label: "Request changes",
|
|
3631
|
+
from: "in_review",
|
|
3632
|
+
to: "draft",
|
|
3633
|
+
requiredCapabilities: ["entry.publish"],
|
|
3634
|
+
requireComment: true
|
|
3635
|
+
},
|
|
3212
3636
|
{
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3637
|
+
name: "unpublish",
|
|
3638
|
+
label: "Unpublish",
|
|
3639
|
+
from: "published",
|
|
3640
|
+
to: "draft",
|
|
3641
|
+
requiredCapabilities: ["entry.unpublish"],
|
|
3642
|
+
unpublish: true
|
|
3217
3643
|
}
|
|
3218
|
-
]
|
|
3219
|
-
|
|
3644
|
+
],
|
|
3645
|
+
roles: [
|
|
3646
|
+
...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
|
|
3647
|
+
...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
|
|
3648
|
+
]
|
|
3649
|
+
};
|
|
3220
3650
|
}
|
|
3221
|
-
function
|
|
3222
|
-
|
|
3223
|
-
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
3224
|
-
if (invalidRoots.length === 0) return [];
|
|
3225
|
-
return [
|
|
3226
|
-
{
|
|
3227
|
-
path,
|
|
3228
|
-
surface: "field.admin.condition",
|
|
3229
|
-
expression,
|
|
3230
|
-
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
3231
|
-
}
|
|
3232
|
-
];
|
|
3233
|
-
} catch (error) {
|
|
3234
|
-
return [
|
|
3235
|
-
{
|
|
3236
|
-
path,
|
|
3237
|
-
surface: "field.admin.condition",
|
|
3238
|
-
expression,
|
|
3239
|
-
message: `has invalid syntax: ${error.message}`
|
|
3240
|
-
}
|
|
3241
|
-
];
|
|
3242
|
-
}
|
|
3651
|
+
function publishingWorkflow() {
|
|
3652
|
+
return definePublishingWorkflow();
|
|
3243
3653
|
}
|
|
3244
|
-
function
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
{
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
}
|
|
3255
|
-
]
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
surface: "collection.admin.previewUrl",
|
|
3261
|
-
expression,
|
|
3262
|
-
message: `has invalid syntax: ${error.message}`
|
|
3263
|
-
}
|
|
3264
|
-
];
|
|
3265
|
-
}
|
|
3654
|
+
function simplePublishingWorkflow() {
|
|
3655
|
+
return {
|
|
3656
|
+
initialState: "draft",
|
|
3657
|
+
draftState: "draft",
|
|
3658
|
+
states: [
|
|
3659
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
3660
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
3661
|
+
],
|
|
3662
|
+
transitions: [
|
|
3663
|
+
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
3664
|
+
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
3665
|
+
]
|
|
3666
|
+
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
3667
|
+
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
3668
|
+
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
3669
|
+
};
|
|
3266
3670
|
}
|
|
3267
|
-
function
|
|
3268
|
-
const
|
|
3269
|
-
|
|
3270
|
-
throw new Error(`Invalid declarative hook at ${path}: ${issues[0].message}`);
|
|
3671
|
+
function publicMetadata(meta) {
|
|
3672
|
+
const { availableTransitions: _availableTransitions, ...safe } = meta;
|
|
3673
|
+
return safe;
|
|
3271
3674
|
}
|
|
3272
|
-
function
|
|
3273
|
-
|
|
3274
|
-
|
|
3675
|
+
function workflowCapabilities(workflow, user) {
|
|
3676
|
+
const capabilities = new Set(Array.isArray(user?.capabilities) ? user.capabilities : []);
|
|
3677
|
+
const roles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
3678
|
+
for (const mapping of workflow.roles ?? []) {
|
|
3679
|
+
if (roles.includes(mapping.role)) mapping.capabilities.forEach((capability) => capabilities.add(capability));
|
|
3680
|
+
}
|
|
3681
|
+
return capabilities;
|
|
3275
3682
|
}
|
|
3276
|
-
function
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
surface: "unsupported",
|
|
3294
|
-
expression: hook,
|
|
3295
|
-
message: "declarative strings are not supported on field hooks.afterRead"
|
|
3296
|
-
});
|
|
3297
|
-
});
|
|
3298
|
-
if (typeof field.admin?.hooks?.onChange === "string") {
|
|
3299
|
-
issues.push(
|
|
3300
|
-
...validateDeclarativeHookExpression(
|
|
3301
|
-
field.admin.hooks.onChange,
|
|
3302
|
-
"field.admin.onChange",
|
|
3303
|
-
`${fieldPath}.admin.hooks.onChange`
|
|
3304
|
-
)
|
|
3305
|
-
);
|
|
3306
|
-
}
|
|
3307
|
-
if (typeof field.admin?.hooks?.options === "string") {
|
|
3308
|
-
issues.push({
|
|
3309
|
-
path: `${fieldPath}.admin.hooks.options`,
|
|
3310
|
-
surface: "unsupported",
|
|
3311
|
-
expression: field.admin.hooks.options,
|
|
3312
|
-
message: "declarative strings are not supported on field admin.hooks.options"
|
|
3313
|
-
});
|
|
3314
|
-
}
|
|
3315
|
-
if (Array.isArray(field.fields)) {
|
|
3316
|
-
collectFieldIssues(field.fields, fieldPath, issues);
|
|
3317
|
-
}
|
|
3318
|
-
if (Array.isArray(field.blocks)) {
|
|
3319
|
-
field.blocks.forEach((block, blockIndex) => {
|
|
3320
|
-
if (!Array.isArray(block.fields)) return;
|
|
3321
|
-
collectFieldIssues(block.fields, `${fieldPath}.${blockSegment(block, blockIndex)}`, issues);
|
|
3322
|
-
});
|
|
3323
|
-
}
|
|
3683
|
+
function canViewWorkflowDraft(workflow, user) {
|
|
3684
|
+
if (!user) return false;
|
|
3685
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
3686
|
+
if (capabilities.has("entry.edit")) return true;
|
|
3687
|
+
if (workflow.transitions.some(
|
|
3688
|
+
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
3689
|
+
)) {
|
|
3690
|
+
return true;
|
|
3691
|
+
}
|
|
3692
|
+
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
3693
|
+
return false;
|
|
3694
|
+
}
|
|
3695
|
+
function availableWorkflowTransitions(workflow, state, user) {
|
|
3696
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
3697
|
+
return workflow.transitions.filter((transition) => {
|
|
3698
|
+
const from = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
3699
|
+
return from.includes(state) && (transition.requiredCapabilities ?? []).every((item) => capabilities.has(item));
|
|
3324
3700
|
});
|
|
3325
3701
|
}
|
|
3326
|
-
function
|
|
3327
|
-
return
|
|
3328
|
-
|
|
3329
|
-
|
|
3702
|
+
function initializeWorkflowDocument(data, workflow) {
|
|
3703
|
+
return {
|
|
3704
|
+
...data,
|
|
3705
|
+
__workflow: { state: workflow.initialState, revision: 1 }
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
function publishedStateName(workflow) {
|
|
3709
|
+
return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
|
|
3330
3710
|
}
|
|
3331
|
-
function
|
|
3332
|
-
const
|
|
3333
|
-
const
|
|
3334
|
-
|
|
3335
|
-
const
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
)
|
|
3343
|
-
|
|
3344
|
-
}
|
|
3345
|
-
|
|
3346
|
-
|
|
3711
|
+
function materializeWorkflowDocument(doc, workflow, user) {
|
|
3712
|
+
const meta = doc.__workflow;
|
|
3713
|
+
const publishedState = publishedStateName(workflow);
|
|
3714
|
+
if (!meta) {
|
|
3715
|
+
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
3716
|
+
const state = publishedStateName(workflow);
|
|
3717
|
+
return {
|
|
3718
|
+
...working2,
|
|
3719
|
+
_workflow: {
|
|
3720
|
+
state,
|
|
3721
|
+
revision: 1,
|
|
3722
|
+
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
const { __published, __workflow, ...working } = doc;
|
|
3727
|
+
if (!canViewWorkflowDraft(workflow, user)) {
|
|
3728
|
+
if (!__published || typeof __published !== "object") {
|
|
3729
|
+
if (meta.state === publishedState) {
|
|
3730
|
+
return { ...working, _workflow: publicMetadata(meta) };
|
|
3731
|
+
}
|
|
3732
|
+
return null;
|
|
3347
3733
|
}
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
issues
|
|
3356
|
-
);
|
|
3357
|
-
});
|
|
3734
|
+
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
3735
|
+
}
|
|
3736
|
+
return {
|
|
3737
|
+
...working,
|
|
3738
|
+
_workflow: {
|
|
3739
|
+
...meta,
|
|
3740
|
+
availableTransitions: availableWorkflowTransitions(workflow, meta.state, user).map((item) => item.name)
|
|
3358
3741
|
}
|
|
3359
|
-
}
|
|
3742
|
+
};
|
|
3360
3743
|
}
|
|
3361
|
-
function
|
|
3362
|
-
|
|
3363
|
-
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
3364
|
-
if (typeof field.access?.read === "string") {
|
|
3365
|
-
issues.push(...validateDeclarativeAccessExpression(field.access.read, "field.access", `${fieldPath}.access.read`));
|
|
3366
|
-
}
|
|
3367
|
-
if (typeof field.access?.create === "string") {
|
|
3368
|
-
issues.push(
|
|
3369
|
-
...validateDeclarativeAccessExpression(field.access.create, "field.access", `${fieldPath}.access.create`)
|
|
3370
|
-
);
|
|
3371
|
-
}
|
|
3372
|
-
if (typeof field.access?.update === "string") {
|
|
3373
|
-
issues.push(
|
|
3374
|
-
...validateDeclarativeAccessExpression(field.access.update, "field.access", `${fieldPath}.access.update`)
|
|
3375
|
-
);
|
|
3376
|
-
}
|
|
3377
|
-
if (Array.isArray(field.fields)) {
|
|
3378
|
-
collectFieldAccessIssues(field.fields, fieldPath, issues);
|
|
3379
|
-
}
|
|
3380
|
-
if (Array.isArray(field.blocks)) {
|
|
3381
|
-
field.blocks.forEach((block, blockIndex) => {
|
|
3382
|
-
if (!Array.isArray(block.fields)) return;
|
|
3383
|
-
collectFieldAccessIssues(
|
|
3384
|
-
block.fields,
|
|
3385
|
-
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
3386
|
-
issues
|
|
3387
|
-
);
|
|
3388
|
-
});
|
|
3389
|
-
}
|
|
3390
|
-
});
|
|
3744
|
+
function eventId() {
|
|
3745
|
+
return globalThis.crypto?.randomUUID?.() ?? `evt_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3391
3746
|
}
|
|
3392
|
-
function
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3747
|
+
function createLifecycleEvent(args) {
|
|
3748
|
+
return {
|
|
3749
|
+
id: eventId(),
|
|
3750
|
+
name: args.name,
|
|
3751
|
+
collection: args.collection,
|
|
3752
|
+
documentId: args.documentId,
|
|
3753
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3754
|
+
actorId: args.actorId,
|
|
3755
|
+
payload: args.payload,
|
|
3756
|
+
attempts: 0,
|
|
3757
|
+
status: "pending"
|
|
3398
3758
|
};
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3759
|
+
}
|
|
3760
|
+
async function persistEvent(db, event) {
|
|
3761
|
+
await db.create({ collection: LIFECYCLE_EVENTS_COLLECTION, data: event });
|
|
3762
|
+
}
|
|
3763
|
+
async function dispatchLifecycleEvent(config, event) {
|
|
3764
|
+
const db = config.db;
|
|
3765
|
+
if (!db || !config.events?.handlers.length) return;
|
|
3766
|
+
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
3767
|
+
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
3768
|
+
const attempts = event.attempts + 1;
|
|
3769
|
+
try {
|
|
3770
|
+
await db.update({
|
|
3771
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
3772
|
+
id: event.id,
|
|
3773
|
+
data: { status: "processing", attempts }
|
|
3774
|
+
});
|
|
3775
|
+
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
3776
|
+
await db.update({
|
|
3777
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
3778
|
+
id: event.id,
|
|
3779
|
+
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
3780
|
+
});
|
|
3781
|
+
} catch (error) {
|
|
3782
|
+
const exhausted = attempts >= maxAttempts;
|
|
3783
|
+
await db.update({
|
|
3784
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
3785
|
+
id: event.id,
|
|
3786
|
+
data: {
|
|
3787
|
+
status: "failed",
|
|
3788
|
+
attempts,
|
|
3789
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
3790
|
+
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
3412
3791
|
}
|
|
3413
|
-
issues.push(...validateDeclarativeHookExpression(hook, surface, `${path}.${key}[${index}]`));
|
|
3414
3792
|
});
|
|
3415
3793
|
}
|
|
3416
3794
|
}
|
|
3417
|
-
function
|
|
3418
|
-
|
|
3419
|
-
config.
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
);
|
|
3795
|
+
async function dispatchPendingLifecycleEvents(config, limit = 50) {
|
|
3796
|
+
if (!config.db || !config.events?.handlers.length) return 0;
|
|
3797
|
+
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
3798
|
+
const result = await config.db.find({
|
|
3799
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
3800
|
+
where: { status: { in: ["pending", "failed"] } },
|
|
3801
|
+
sort: "occurredAt",
|
|
3802
|
+
limit
|
|
3426
3803
|
});
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3804
|
+
const now2 = Date.now();
|
|
3805
|
+
const due = result.docs.filter(
|
|
3806
|
+
(doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
|
|
3807
|
+
);
|
|
3808
|
+
for (const event of due) await dispatchLifecycleEvent(config, event);
|
|
3809
|
+
return due.length;
|
|
3810
|
+
}
|
|
3811
|
+
async function saveWorkflowDraft(args) {
|
|
3812
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
3813
|
+
const db = config.db;
|
|
3814
|
+
const workflow = collection.workflow;
|
|
3815
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
3816
|
+
const previous = originalDoc.__workflow;
|
|
3817
|
+
const isLegacyPublishedDoc = !previous;
|
|
3818
|
+
const baselineRevision = previous?.revision ?? 1;
|
|
3819
|
+
const event = createLifecycleEvent({
|
|
3820
|
+
name: "revision.created",
|
|
3821
|
+
collection: collection.slug,
|
|
3822
|
+
documentId: id,
|
|
3823
|
+
actorId: user?.sub,
|
|
3824
|
+
payload: {
|
|
3825
|
+
revision: baselineRevision + 1,
|
|
3826
|
+
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
3827
|
+
}
|
|
3436
3828
|
});
|
|
3437
|
-
|
|
3438
|
-
const
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3829
|
+
const doc = await db.transaction(async (tx) => {
|
|
3830
|
+
const nextMeta = {
|
|
3831
|
+
...previous ?? {},
|
|
3832
|
+
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
3833
|
+
revision: baselineRevision + 1,
|
|
3834
|
+
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
3835
|
+
};
|
|
3836
|
+
const updateData = {
|
|
3837
|
+
...data,
|
|
3838
|
+
__workflow: nextMeta
|
|
3839
|
+
};
|
|
3840
|
+
if (isLegacyPublishedDoc) {
|
|
3841
|
+
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
3842
|
+
updateData.__published = publishedSnapshot;
|
|
3843
|
+
}
|
|
3844
|
+
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
3845
|
+
await persistEvent(tx, event);
|
|
3846
|
+
await beforeCommit?.(tx, updated);
|
|
3847
|
+
return updated;
|
|
3848
|
+
});
|
|
3849
|
+
void dispatchLifecycleEvent(config, event);
|
|
3850
|
+
return { doc, event };
|
|
3851
|
+
}
|
|
3852
|
+
async function createWorkflowDocument(args) {
|
|
3853
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
3854
|
+
const db = config.db;
|
|
3855
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
3856
|
+
let event;
|
|
3857
|
+
const doc = await db.transaction(async (tx) => {
|
|
3858
|
+
const created = await tx.create({ collection: collection.slug, data });
|
|
3859
|
+
event = createLifecycleEvent({
|
|
3860
|
+
name: "revision.created",
|
|
3861
|
+
collection: collection.slug,
|
|
3862
|
+
documentId: created.id,
|
|
3863
|
+
actorId: user?.sub,
|
|
3864
|
+
payload: { revision: 1, previousRevision: null }
|
|
3865
|
+
});
|
|
3866
|
+
await persistEvent(tx, event);
|
|
3867
|
+
await beforeCommit?.(tx, created);
|
|
3868
|
+
return created;
|
|
3446
3869
|
});
|
|
3447
|
-
|
|
3870
|
+
void dispatchLifecycleEvent(config, event);
|
|
3871
|
+
return { doc, event };
|
|
3448
3872
|
}
|
|
3449
|
-
function
|
|
3450
|
-
const
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3873
|
+
async function transitionWorkflow(args) {
|
|
3874
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
3875
|
+
const db = config.db;
|
|
3876
|
+
const workflow = collection.workflow;
|
|
3877
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
3878
|
+
const original = await db.findOne({ collection: collection.slug, id });
|
|
3879
|
+
if (!original) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
3880
|
+
const meta = original.__workflow;
|
|
3881
|
+
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
3882
|
+
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
3883
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
3884
|
+
statusCode: 409
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
3888
|
+
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
3889
|
+
if (!transition || !fromStates.includes(meta.state)) {
|
|
3890
|
+
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
3891
|
+
statusCode: 409
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
3895
|
+
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
3896
|
+
}
|
|
3897
|
+
if (transition.requireComment && !comment?.trim()) {
|
|
3898
|
+
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
3899
|
+
}
|
|
3900
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
3901
|
+
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
3902
|
+
const events = [
|
|
3903
|
+
createLifecycleEvent({
|
|
3904
|
+
name: "workflow.transitioned",
|
|
3905
|
+
collection: collection.slug,
|
|
3906
|
+
documentId: id,
|
|
3907
|
+
actorId: user?.sub,
|
|
3908
|
+
payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
|
|
3909
|
+
})
|
|
3910
|
+
];
|
|
3911
|
+
const targetState = workflow.states.find((state) => state.name === transition.to);
|
|
3912
|
+
if (targetState.published) {
|
|
3913
|
+
events.push(
|
|
3914
|
+
createLifecycleEvent({
|
|
3915
|
+
name: "entry.published",
|
|
3916
|
+
collection: collection.slug,
|
|
3917
|
+
documentId: id,
|
|
3918
|
+
actorId: user?.sub,
|
|
3919
|
+
payload: { revision: meta.revision }
|
|
3920
|
+
})
|
|
3455
3921
|
);
|
|
3456
|
-
})
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3922
|
+
} else if (transition.unpublish) {
|
|
3923
|
+
events.push(
|
|
3924
|
+
createLifecycleEvent({
|
|
3925
|
+
name: "entry.unpublished",
|
|
3926
|
+
collection: collection.slug,
|
|
3927
|
+
documentId: id,
|
|
3928
|
+
actorId: user?.sub,
|
|
3929
|
+
payload: { revision: meta.publishedRevision }
|
|
3930
|
+
})
|
|
3463
3931
|
);
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
const
|
|
3467
|
-
if (
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
)
|
|
3474
|
-
);
|
|
3475
|
-
}
|
|
3476
|
-
if (typeof collection.access?.create === "string") {
|
|
3477
|
-
issues.push(
|
|
3478
|
-
...validateDeclarativeAccessExpression(
|
|
3479
|
-
collection.access.create,
|
|
3480
|
-
"collection.access",
|
|
3481
|
-
`${collectionPath}.access.create`
|
|
3482
|
-
)
|
|
3483
|
-
);
|
|
3484
|
-
}
|
|
3485
|
-
if (typeof collection.access?.update === "string") {
|
|
3486
|
-
issues.push(
|
|
3487
|
-
...validateDeclarativeAccessExpression(
|
|
3488
|
-
collection.access.update,
|
|
3489
|
-
"collection.access",
|
|
3490
|
-
`${collectionPath}.access.update`
|
|
3491
|
-
)
|
|
3492
|
-
);
|
|
3493
|
-
}
|
|
3494
|
-
if (typeof collection.access?.delete === "string") {
|
|
3495
|
-
issues.push(
|
|
3496
|
-
...validateDeclarativeAccessExpression(
|
|
3497
|
-
collection.access.delete,
|
|
3498
|
-
"collection.access",
|
|
3499
|
-
`${collectionPath}.access.delete`
|
|
3500
|
-
)
|
|
3501
|
-
);
|
|
3502
|
-
}
|
|
3503
|
-
if (typeof collection.access?.readAudit === "string") {
|
|
3504
|
-
issues.push(
|
|
3505
|
-
...validateDeclarativeAccessExpression(
|
|
3506
|
-
collection.access.readAudit,
|
|
3507
|
-
"collection.access",
|
|
3508
|
-
`${collectionPath}.access.readAudit`
|
|
3509
|
-
)
|
|
3510
|
-
);
|
|
3932
|
+
}
|
|
3933
|
+
const updated = await db.transaction(async (tx) => {
|
|
3934
|
+
const locked = await tx.findOne({ collection: collection.slug, id });
|
|
3935
|
+
if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
3936
|
+
const lockedMeta = locked.__workflow;
|
|
3937
|
+
if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
|
|
3938
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
3939
|
+
statusCode: 409
|
|
3940
|
+
});
|
|
3511
3941
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3942
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3943
|
+
const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
|
|
3944
|
+
const nextMeta = {
|
|
3945
|
+
...lockedMeta,
|
|
3946
|
+
state: transition.to,
|
|
3947
|
+
...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
|
|
3948
|
+
...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
|
|
3949
|
+
};
|
|
3950
|
+
const data = { __workflow: nextMeta };
|
|
3951
|
+
if (targetState.published) data.__published = working;
|
|
3952
|
+
if (transition.unpublish) data.__published = null;
|
|
3953
|
+
const next = await tx.update({ collection: collection.slug, id, data });
|
|
3954
|
+
if (transition.onTransition) {
|
|
3955
|
+
await transition.onTransition({
|
|
3956
|
+
transition,
|
|
3957
|
+
from: lockedMeta.state,
|
|
3958
|
+
to: transition.to,
|
|
3959
|
+
doc: locked,
|
|
3960
|
+
input,
|
|
3961
|
+
comment,
|
|
3962
|
+
user,
|
|
3963
|
+
req,
|
|
3964
|
+
tx
|
|
3965
|
+
});
|
|
3520
3966
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3967
|
+
await tx.create({
|
|
3968
|
+
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
3969
|
+
data: {
|
|
3970
|
+
collection: collection.slug,
|
|
3971
|
+
documentId: id,
|
|
3972
|
+
transition: transition.name,
|
|
3973
|
+
from: lockedMeta.state,
|
|
3974
|
+
to: transition.to,
|
|
3975
|
+
revision: lockedMeta.revision,
|
|
3976
|
+
comment: comment ?? null,
|
|
3977
|
+
actorId: user?.sub ?? null,
|
|
3978
|
+
createdAt: now2
|
|
3979
|
+
}
|
|
3980
|
+
});
|
|
3981
|
+
for (const event of events) await persistEvent(tx, event);
|
|
3982
|
+
if (collection.audit) {
|
|
3983
|
+
await tx.create({
|
|
3984
|
+
collection: "__audit",
|
|
3985
|
+
data: {
|
|
3986
|
+
collection: collection.slug,
|
|
3987
|
+
documentId: id,
|
|
3988
|
+
operation: "workflow.transition",
|
|
3989
|
+
user: user?.sub ?? null,
|
|
3990
|
+
timestamp: now2,
|
|
3991
|
+
changes: JSON.stringify({
|
|
3992
|
+
transition: transition.name,
|
|
3993
|
+
from: lockedMeta.state,
|
|
3994
|
+
to: transition.to,
|
|
3995
|
+
revision: lockedMeta.revision
|
|
3996
|
+
})
|
|
3997
|
+
}
|
|
3998
|
+
});
|
|
3529
3999
|
}
|
|
3530
|
-
|
|
3531
|
-
});
|
|
3532
|
-
return issues;
|
|
3533
|
-
}
|
|
3534
|
-
function collectAdminConditionValidationIssues(config) {
|
|
3535
|
-
const issues = [];
|
|
3536
|
-
config.blocks?.forEach((block, blockIndex) => {
|
|
3537
|
-
if (!Array.isArray(block.fields)) return;
|
|
3538
|
-
collectAdminConditionIssues(
|
|
3539
|
-
block.fields,
|
|
3540
|
-
blockSegment(block, blockIndex),
|
|
3541
|
-
collectFieldNames(block.fields),
|
|
3542
|
-
issues
|
|
3543
|
-
);
|
|
3544
|
-
});
|
|
3545
|
-
config.collections.forEach((collection, collectionIndex) => {
|
|
3546
|
-
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
3547
|
-
const rootFieldNames = collectFieldNames(collection.fields);
|
|
3548
|
-
collectAdminConditionIssues(collection.fields, collectionPath, rootFieldNames, issues);
|
|
3549
|
-
});
|
|
3550
|
-
config.globals?.forEach((global, globalIndex) => {
|
|
3551
|
-
const globalPath = globalSegment(global, globalIndex);
|
|
3552
|
-
const rootFieldNames = collectFieldNames(global.fields);
|
|
3553
|
-
collectAdminConditionIssues(global.fields, globalPath, rootFieldNames, issues);
|
|
4000
|
+
return next;
|
|
3554
4001
|
});
|
|
3555
|
-
|
|
4002
|
+
for (const event of events) void dispatchLifecycleEvent(config, event);
|
|
4003
|
+
for (const hook of workflow.hooks?.afterTransition ?? []) {
|
|
4004
|
+
try {
|
|
4005
|
+
await hook({ ...hookContext, doc: updated, event: events[0] });
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
try {
|
|
4008
|
+
const obs = config?.__observability;
|
|
4009
|
+
obs?.recordWorkflowHookFailure?.({
|
|
4010
|
+
collection: collection.slug,
|
|
4011
|
+
transition: transition.name
|
|
4012
|
+
});
|
|
4013
|
+
const logger = config?.logger;
|
|
4014
|
+
if (logger && typeof logger.child === "function") {
|
|
4015
|
+
logger.child({ component: "workflow" }).error({
|
|
4016
|
+
err: error,
|
|
4017
|
+
msg: "afterTransition hook failed",
|
|
4018
|
+
collection: collection.slug,
|
|
4019
|
+
transition: transition.name,
|
|
4020
|
+
documentId: id
|
|
4021
|
+
});
|
|
4022
|
+
} else if (logger && typeof logger.error === "function") {
|
|
4023
|
+
logger.error({
|
|
4024
|
+
err: error,
|
|
4025
|
+
msg: "afterTransition hook failed",
|
|
4026
|
+
collection: collection.slug,
|
|
4027
|
+
transition: transition.name,
|
|
4028
|
+
documentId: id
|
|
4029
|
+
});
|
|
4030
|
+
} else {
|
|
4031
|
+
console.error("afterTransition hook failed:", error);
|
|
4032
|
+
}
|
|
4033
|
+
} catch {
|
|
4034
|
+
console.error("afterTransition hook failed:", error);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
return updated;
|
|
3556
4039
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
const allowedRoots = [
|
|
3563
|
-
.../* @__PURE__ */ new Set([...PREVIEW_URL_RESERVED_ROOTS, ...collectFieldNames(collection.fields)])
|
|
3564
|
-
];
|
|
3565
|
-
issues.push(
|
|
3566
|
-
...validatePreviewUrlExpression(
|
|
3567
|
-
collection.admin.previewUrl,
|
|
3568
|
-
`${collectionPath}.admin.previewUrl`,
|
|
3569
|
-
allowedRoots
|
|
3570
|
-
)
|
|
3571
|
-
);
|
|
3572
|
-
});
|
|
3573
|
-
return issues;
|
|
4040
|
+
|
|
4041
|
+
// src/utils/admin-auth.ts
|
|
4042
|
+
function getAdminAuthCollection(config) {
|
|
4043
|
+
const adminCollection = resolveAdminAuthCollection(config.collections, config.adminAuth?.collectionSlug);
|
|
4044
|
+
return adminCollection ?? null;
|
|
3574
4045
|
}
|
|
3575
|
-
function
|
|
3576
|
-
const
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
}));
|
|
3584
|
-
if (diagnostics.length === 0) return;
|
|
3585
|
-
throw new ConfigValidationError(source, diagnostics);
|
|
4046
|
+
function resolveAdminAuthCollection(collections, collectionSlug) {
|
|
4047
|
+
const adminsCollection = collections.find((collection) => collection.slug === "__admins");
|
|
4048
|
+
if (adminsCollection) return adminsCollection;
|
|
4049
|
+
if (collectionSlug) {
|
|
4050
|
+
const requestedCollection = collections.find((collection) => collection.slug === collectionSlug);
|
|
4051
|
+
if (requestedCollection) return requestedCollection;
|
|
4052
|
+
}
|
|
4053
|
+
return collections.find((collection) => collection.auth);
|
|
3586
4054
|
}
|
|
3587
|
-
function
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
message: issue.message,
|
|
3594
|
-
expression: issue.expression
|
|
4055
|
+
function getPublicAdminAuthConfig(adminAuth, collections) {
|
|
4056
|
+
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
4057
|
+
id: provider.id,
|
|
4058
|
+
type: provider.type,
|
|
4059
|
+
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
4060
|
+
autoRedirect: provider.autoRedirect
|
|
3595
4061
|
}));
|
|
3596
|
-
|
|
3597
|
-
|
|
4062
|
+
const resolvedCollectionSlug = collections ? resolveAdminAuthCollection(collections, adminAuth?.collectionSlug)?.slug : adminAuth?.collectionSlug;
|
|
4063
|
+
return {
|
|
4064
|
+
mode: adminAuth?.mode ?? "local",
|
|
4065
|
+
collectionSlug: resolvedCollectionSlug,
|
|
4066
|
+
provisioningMode: adminAuth?.provisioningMode,
|
|
4067
|
+
providers
|
|
4068
|
+
};
|
|
3598
4069
|
}
|
|
3599
|
-
function
|
|
3600
|
-
const
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
source: "adminCondition",
|
|
3619
|
-
path: issue.path,
|
|
3620
|
-
surface: issue.surface,
|
|
3621
|
-
message: issue.message,
|
|
3622
|
-
expression: issue.expression
|
|
3623
|
-
}));
|
|
3624
|
-
const previewUrlIssues = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
3625
|
-
severity: "error",
|
|
3626
|
-
source: "previewUrl",
|
|
3627
|
-
path: issue.path,
|
|
3628
|
-
surface: issue.surface,
|
|
3629
|
-
message: issue.message,
|
|
3630
|
-
expression: issue.expression
|
|
3631
|
-
}));
|
|
3632
|
-
return [...hookIssues, ...accessIssues, ...adminConditionIssues, ...previewUrlIssues];
|
|
4070
|
+
function humanizeProviderName(id, type) {
|
|
4071
|
+
const cleaned = id.replace(/[-_]+/g, " ").trim();
|
|
4072
|
+
if (!cleaned) return type.toUpperCase();
|
|
4073
|
+
return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
4074
|
+
}
|
|
4075
|
+
function getAdminRoleForCollection(collection) {
|
|
4076
|
+
if (!collection?.auth) return "admin";
|
|
4077
|
+
if (typeof collection.auth === "object" && collection.auth.adminRole) {
|
|
4078
|
+
return collection.auth.adminRole;
|
|
4079
|
+
}
|
|
4080
|
+
return "admin";
|
|
4081
|
+
}
|
|
4082
|
+
function isUserAdmin(user, collection) {
|
|
4083
|
+
if (!user) return false;
|
|
4084
|
+
const roles = Array.isArray(user.roles) ? user.roles : typeof user.role === "string" ? [user.role] : [];
|
|
4085
|
+
if (roles.length === 0) return false;
|
|
4086
|
+
const configuredRole = getAdminRoleForCollection(collection);
|
|
4087
|
+
if (roles.includes(configuredRole)) return true;
|
|
4088
|
+
return roles.includes("admin") || roles.includes("super_admin");
|
|
3633
4089
|
}
|
|
3634
|
-
function formatConfigDiagnostics(source, diagnostics, options = {}) {
|
|
3635
|
-
const color = options.color ?? false;
|
|
3636
|
-
const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
|
|
3637
|
-
const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
|
|
3638
|
-
const lines = diagnostics.flatMap((issue, index) => [
|
|
3639
|
-
`${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
|
|
3640
|
-
` ${issue.message}`,
|
|
3641
|
-
` ${colorize("surface:", 90, color)} ${issue.surface}`
|
|
3642
|
-
]);
|
|
3643
|
-
return `${header}
|
|
3644
|
-
${lines.join("\n")}
|
|
3645
4090
|
|
|
3646
|
-
|
|
4091
|
+
// src/utils/block-references.ts
|
|
4092
|
+
function mergeUniqueBlocks(blocks) {
|
|
4093
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4094
|
+
for (const block of blocks) {
|
|
4095
|
+
const existing = seen.get(block.slug);
|
|
4096
|
+
if (existing && existing !== block) {
|
|
4097
|
+
throw new Error(
|
|
4098
|
+
`Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
|
|
4099
|
+
);
|
|
4100
|
+
}
|
|
4101
|
+
if (!existing) seen.set(block.slug, block);
|
|
4102
|
+
}
|
|
4103
|
+
return Array.from(seen.values());
|
|
3647
4104
|
}
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4105
|
+
function buildRegistry(blocks) {
|
|
4106
|
+
return new Map(blocks.map((block) => [block.slug, block]));
|
|
4107
|
+
}
|
|
4108
|
+
function resolveField(field, registry, blockCache) {
|
|
4109
|
+
const next = { ...field };
|
|
4110
|
+
if (next.fields) {
|
|
4111
|
+
next.fields = next.fields.map(
|
|
4112
|
+
(child) => resolveField(child, registry, blockCache)
|
|
4113
|
+
);
|
|
3656
4114
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
4115
|
+
if (next.type !== "blocks") return next;
|
|
4116
|
+
const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
|
|
4117
|
+
const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
|
|
4118
|
+
if (hasInlineBlocks && hasReferences) {
|
|
4119
|
+
throw new Error(
|
|
4120
|
+
`Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
if (hasReferences) {
|
|
4124
|
+
next.blocks = next.blockReferences.map((slug) => {
|
|
4125
|
+
const block = registry.get(slug);
|
|
4126
|
+
if (!block) {
|
|
4127
|
+
throw new Error(
|
|
4128
|
+
`Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
return resolveBlock(block, registry, blockCache);
|
|
4132
|
+
});
|
|
4133
|
+
return next;
|
|
4134
|
+
}
|
|
4135
|
+
if (hasInlineBlocks) {
|
|
4136
|
+
next.blocks = next.blocks.map(
|
|
4137
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
4138
|
+
);
|
|
4139
|
+
}
|
|
4140
|
+
return next;
|
|
3660
4141
|
}
|
|
3661
|
-
function
|
|
3662
|
-
const
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
4142
|
+
function resolveBlock(block, registry, blockCache) {
|
|
4143
|
+
const cached = blockCache.get(block);
|
|
4144
|
+
if (cached) return cached;
|
|
4145
|
+
const resolved = {
|
|
4146
|
+
...block,
|
|
4147
|
+
fields: []
|
|
4148
|
+
};
|
|
4149
|
+
blockCache.set(block, resolved);
|
|
4150
|
+
resolved.fields = block.fields.map(
|
|
4151
|
+
(field) => resolveField(field, registry, blockCache)
|
|
4152
|
+
);
|
|
4153
|
+
return resolved;
|
|
3672
4154
|
}
|
|
3673
|
-
function
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4155
|
+
function resolveFields(fields, registry, blockCache) {
|
|
4156
|
+
return fields.map((field) => resolveField(field, registry, blockCache));
|
|
4157
|
+
}
|
|
4158
|
+
function normalizeSchemaFragment(fragment) {
|
|
4159
|
+
const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
|
|
4160
|
+
const registry = buildRegistry(reusableBlocks);
|
|
4161
|
+
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
4162
|
+
const resolvedBlocks = reusableBlocks.map(
|
|
4163
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
4164
|
+
);
|
|
4165
|
+
const resolvedCollections = (fragment.collections ?? []).map(
|
|
4166
|
+
(collection) => ({
|
|
4167
|
+
...collection,
|
|
4168
|
+
fields: resolveFields(collection.fields ?? [], registry, blockCache)
|
|
4169
|
+
})
|
|
4170
|
+
);
|
|
4171
|
+
const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
|
|
4172
|
+
...global,
|
|
4173
|
+
fields: resolveFields(global.fields ?? [], registry, blockCache)
|
|
3681
4174
|
}));
|
|
3682
|
-
|
|
3683
|
-
|
|
4175
|
+
return {
|
|
4176
|
+
...fragment,
|
|
4177
|
+
...fragment.blocks ? { blocks: resolvedBlocks } : {},
|
|
4178
|
+
...fragment.collections ? { collections: resolvedCollections } : {},
|
|
4179
|
+
...fragment.globals ? { globals: resolvedGlobals } : {}
|
|
4180
|
+
};
|
|
3684
4181
|
}
|
|
3685
4182
|
|
|
3686
4183
|
// src/utils/id.ts
|
|
@@ -3864,7 +4361,7 @@ var AUDIT_COLLECTION = {
|
|
|
3864
4361
|
name: "operation",
|
|
3865
4362
|
type: "select",
|
|
3866
4363
|
label: "Operation",
|
|
3867
|
-
options: ["create", "update", "delete"],
|
|
4364
|
+
options: ["create", "update", "delete", "trash", "restore", "purge", "trash-empty"],
|
|
3868
4365
|
required: true
|
|
3869
4366
|
},
|
|
3870
4367
|
{ name: "user", type: "text", label: "User ID" },
|
|
@@ -3958,12 +4455,14 @@ function normalizeConfig(config) {
|
|
|
3958
4455
|
assertValidDeclarativeAccessInConfig(schemaAwareConfig, "config");
|
|
3959
4456
|
assertValidAdminConditionsInConfig(schemaAwareConfig, "config");
|
|
3960
4457
|
assertValidPreviewUrlsInConfig(schemaAwareConfig, "config");
|
|
4458
|
+
assertValidTrashInConfig(schemaAwareConfig, "config");
|
|
3961
4459
|
const collections = schemaAwareConfig?.collections || [];
|
|
3962
4460
|
const globals = schemaAwareConfig?.globals || [];
|
|
3963
4461
|
const needsAudit = collections.some((col) => col.audit);
|
|
3964
4462
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
3965
4463
|
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
3966
4464
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
4465
|
+
const needsTrash = schemaAwareConfig.trash?.enabled === true || collections.some((col) => resolveTrashConfig(col, schemaAwareConfig).enabled);
|
|
3967
4466
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
3968
4467
|
collections,
|
|
3969
4468
|
adminAuth: schemaAwareConfig.adminAuth
|
|
@@ -4201,6 +4700,9 @@ function normalizeConfig(config) {
|
|
|
4201
4700
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
4202
4701
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
4203
4702
|
}
|
|
4703
|
+
if (needsTrash && !normalizedCollections.some((col) => col.slug === TRASH_COLLECTION)) {
|
|
4704
|
+
systemCollections.push(TRASH_COLLECTION_CONFIG);
|
|
4705
|
+
}
|
|
4204
4706
|
return {
|
|
4205
4707
|
...schemaAwareConfig,
|
|
4206
4708
|
collections: [...normalizedCollections, ...systemCollections],
|
|
@@ -4563,206 +5065,6 @@ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
|
|
|
4563
5065
|
}).join(", ");
|
|
4564
5066
|
}
|
|
4565
5067
|
|
|
4566
|
-
// src/utils/hooks.ts
|
|
4567
|
-
var import_jexl3 = __toESM(require_Jexl(), 1);
|
|
4568
|
-
async function runCollectionHooks(hooks, args, options = {}) {
|
|
4569
|
-
if (!hooks || hooks.length === 0) {
|
|
4570
|
-
return args.data ?? args.doc ?? args.query ?? void 0;
|
|
4571
|
-
}
|
|
4572
|
-
const payloadKey = args.data !== void 0 ? "data" : args.doc !== void 0 ? "doc" : "query" in args ? "query" : void 0;
|
|
4573
|
-
let currentPayload = payloadKey === void 0 ? void 0 : args[payloadKey];
|
|
4574
|
-
for (const hook of hooks) {
|
|
4575
|
-
try {
|
|
4576
|
-
const hookArgs = {
|
|
4577
|
-
...args,
|
|
4578
|
-
...payloadKey !== void 0 ? { [payloadKey]: currentPayload } : {}
|
|
4579
|
-
};
|
|
4580
|
-
const result = typeof hook === "string" ? await (() => {
|
|
4581
|
-
if (options.surface) {
|
|
4582
|
-
assertValidDeclarativeHookExpression(
|
|
4583
|
-
hook,
|
|
4584
|
-
options.surface,
|
|
4585
|
-
options.path ?? options.surface
|
|
4586
|
-
);
|
|
4587
|
-
}
|
|
4588
|
-
return import_jexl3.default.eval(hook, hookArgs);
|
|
4589
|
-
})() : await hook(hookArgs);
|
|
4590
|
-
if (result !== void 0) {
|
|
4591
|
-
if (typeof hook === "string" && options.surface) {
|
|
4592
|
-
assertValidDeclarativeHookResult(
|
|
4593
|
-
result,
|
|
4594
|
-
options.surface,
|
|
4595
|
-
options.path ?? options.surface
|
|
4596
|
-
);
|
|
4597
|
-
}
|
|
4598
|
-
currentPayload = typeof hook === "string" && currentPayload && typeof currentPayload === "object" && !Array.isArray(currentPayload) && result && typeof result === "object" && !Array.isArray(result) ? {
|
|
4599
|
-
...currentPayload,
|
|
4600
|
-
...result
|
|
4601
|
-
} : result;
|
|
4602
|
-
}
|
|
4603
|
-
} catch (err) {
|
|
4604
|
-
if (options.isolated) {
|
|
4605
|
-
console.error("Side-effect hook failed after a successful write", err);
|
|
4606
|
-
} else {
|
|
4607
|
-
throw err;
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
return currentPayload;
|
|
4612
|
-
}
|
|
4613
|
-
async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pathPrefix = "") {
|
|
4614
|
-
if (!data || typeof data !== "object") return data;
|
|
4615
|
-
const result = { ...data };
|
|
4616
|
-
for (const field of fields) {
|
|
4617
|
-
if (!field.name) continue;
|
|
4618
|
-
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
4619
|
-
const value = result[field.name];
|
|
4620
|
-
const origValue = originalDoc?.[field.name];
|
|
4621
|
-
let updatedValue = value;
|
|
4622
|
-
if (field.hooks?.beforeChange) {
|
|
4623
|
-
for (const hook of field.hooks.beforeChange) {
|
|
4624
|
-
const hookArgs = {
|
|
4625
|
-
value: updatedValue,
|
|
4626
|
-
originalDoc: originalDoc ?? void 0,
|
|
4627
|
-
data: result,
|
|
4628
|
-
user,
|
|
4629
|
-
db
|
|
4630
|
-
};
|
|
4631
|
-
updatedValue = typeof hook === "string" ? await (() => {
|
|
4632
|
-
assertValidDeclarativeHookExpression(
|
|
4633
|
-
hook,
|
|
4634
|
-
"field.beforeChange",
|
|
4635
|
-
fieldPath
|
|
4636
|
-
);
|
|
4637
|
-
return import_jexl3.default.eval(hook, hookArgs);
|
|
4638
|
-
})() : await hook(hookArgs);
|
|
4639
|
-
}
|
|
4640
|
-
result[field.name] = updatedValue;
|
|
4641
|
-
}
|
|
4642
|
-
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
4643
|
-
if (field.type === "object" && field.fields) {
|
|
4644
|
-
result[field.name] = await executeFieldBeforeChange(
|
|
4645
|
-
field.fields,
|
|
4646
|
-
updatedValue,
|
|
4647
|
-
origValue,
|
|
4648
|
-
user,
|
|
4649
|
-
db,
|
|
4650
|
-
fieldPath
|
|
4651
|
-
);
|
|
4652
|
-
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
4653
|
-
const arrayResult = [];
|
|
4654
|
-
for (let i = 0; i < updatedValue.length; i++) {
|
|
4655
|
-
const item = updatedValue[i];
|
|
4656
|
-
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
4657
|
-
arrayResult.push(
|
|
4658
|
-
await executeFieldBeforeChange(
|
|
4659
|
-
field.fields,
|
|
4660
|
-
item,
|
|
4661
|
-
origItem,
|
|
4662
|
-
user,
|
|
4663
|
-
db,
|
|
4664
|
-
fieldPath
|
|
4665
|
-
)
|
|
4666
|
-
);
|
|
4667
|
-
}
|
|
4668
|
-
result[field.name] = arrayResult;
|
|
4669
|
-
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
4670
|
-
const blocksResult = [];
|
|
4671
|
-
for (let i = 0; i < updatedValue.length; i++) {
|
|
4672
|
-
const blockData = updatedValue[i];
|
|
4673
|
-
const origBlock = Array.isArray(origValue) ? origValue[i] : null;
|
|
4674
|
-
const blockConfig = field.blocks.find(
|
|
4675
|
-
(b) => b.slug === blockData.blockType
|
|
4676
|
-
);
|
|
4677
|
-
if (blockConfig) {
|
|
4678
|
-
blocksResult.push(
|
|
4679
|
-
await executeFieldBeforeChange(
|
|
4680
|
-
blockConfig.fields,
|
|
4681
|
-
blockData,
|
|
4682
|
-
origBlock,
|
|
4683
|
-
user,
|
|
4684
|
-
db,
|
|
4685
|
-
fieldPath
|
|
4686
|
-
)
|
|
4687
|
-
);
|
|
4688
|
-
} else {
|
|
4689
|
-
blocksResult.push(blockData);
|
|
4690
|
-
}
|
|
4691
|
-
}
|
|
4692
|
-
result[field.name] = blocksResult;
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
}
|
|
4696
|
-
return result;
|
|
4697
|
-
}
|
|
4698
|
-
async function executeFieldAfterRead(fields, doc, user, db) {
|
|
4699
|
-
if (!doc || typeof doc !== "object") return doc;
|
|
4700
|
-
const result = { ...doc };
|
|
4701
|
-
for (const field of fields) {
|
|
4702
|
-
if (!field.name) continue;
|
|
4703
|
-
const value = result[field.name];
|
|
4704
|
-
let updatedValue = value;
|
|
4705
|
-
if (field.hooks?.afterRead) {
|
|
4706
|
-
for (const hook of field.hooks.afterRead) {
|
|
4707
|
-
const typedHook = hook;
|
|
4708
|
-
const hookArgs = {
|
|
4709
|
-
value: updatedValue,
|
|
4710
|
-
doc: result,
|
|
4711
|
-
user,
|
|
4712
|
-
db
|
|
4713
|
-
};
|
|
4714
|
-
updatedValue = await typedHook(hookArgs);
|
|
4715
|
-
}
|
|
4716
|
-
result[field.name] = updatedValue;
|
|
4717
|
-
}
|
|
4718
|
-
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
4719
|
-
if (field.type === "object" && field.fields) {
|
|
4720
|
-
result[field.name] = await executeFieldAfterRead(
|
|
4721
|
-
field.fields,
|
|
4722
|
-
updatedValue,
|
|
4723
|
-
user,
|
|
4724
|
-
db
|
|
4725
|
-
);
|
|
4726
|
-
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
4727
|
-
const arrayResult = [];
|
|
4728
|
-
for (const item of updatedValue) {
|
|
4729
|
-
arrayResult.push(
|
|
4730
|
-
await executeFieldAfterRead(
|
|
4731
|
-
field.fields,
|
|
4732
|
-
item,
|
|
4733
|
-
user,
|
|
4734
|
-
db
|
|
4735
|
-
)
|
|
4736
|
-
);
|
|
4737
|
-
}
|
|
4738
|
-
result[field.name] = arrayResult;
|
|
4739
|
-
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
4740
|
-
const blocksResult = [];
|
|
4741
|
-
for (const blockData of updatedValue) {
|
|
4742
|
-
const typedBlock = blockData;
|
|
4743
|
-
const blockConfig = field.blocks.find(
|
|
4744
|
-
(b) => b.slug === typedBlock.blockType
|
|
4745
|
-
);
|
|
4746
|
-
if (blockConfig) {
|
|
4747
|
-
blocksResult.push(
|
|
4748
|
-
await executeFieldAfterRead(
|
|
4749
|
-
blockConfig.fields,
|
|
4750
|
-
typedBlock,
|
|
4751
|
-
user,
|
|
4752
|
-
db
|
|
4753
|
-
)
|
|
4754
|
-
);
|
|
4755
|
-
} else {
|
|
4756
|
-
blocksResult.push(blockData);
|
|
4757
|
-
}
|
|
4758
|
-
}
|
|
4759
|
-
result[field.name] = blocksResult;
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
return result;
|
|
4764
|
-
}
|
|
4765
|
-
|
|
4766
5068
|
// src/utils/openapi.ts
|
|
4767
5069
|
function getCollectionLabels(collection) {
|
|
4768
5070
|
return collection.labels || { singular: collection.slug, plural: collection.slug };
|
|
@@ -7186,10 +7488,12 @@ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
|
7186
7488
|
"settings",
|
|
7187
7489
|
"admin",
|
|
7188
7490
|
"preferences",
|
|
7189
|
-
"auth"
|
|
7491
|
+
"auth",
|
|
7492
|
+
"trash"
|
|
7190
7493
|
]);
|
|
7191
7494
|
function assertValidNavigationSlugs(navigation) {
|
|
7192
7495
|
for (const item of navigation) {
|
|
7496
|
+
if (item.dashboard || item.trash) continue;
|
|
7193
7497
|
if (item.slug) {
|
|
7194
7498
|
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
7195
7499
|
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
@@ -7269,6 +7573,21 @@ function compileNavigation(config) {
|
|
|
7269
7573
|
});
|
|
7270
7574
|
continue;
|
|
7271
7575
|
}
|
|
7576
|
+
if (item.trash) {
|
|
7577
|
+
registerItem(groupName, {
|
|
7578
|
+
id: "trash",
|
|
7579
|
+
type: "trash",
|
|
7580
|
+
slug: "trash",
|
|
7581
|
+
label: item.label || "Trash",
|
|
7582
|
+
icon: item.icon || "Trash2",
|
|
7583
|
+
group: groupName,
|
|
7584
|
+
order: item.order ?? 950,
|
|
7585
|
+
views: [],
|
|
7586
|
+
badge: item.badge,
|
|
7587
|
+
access: item.access
|
|
7588
|
+
});
|
|
7589
|
+
continue;
|
|
7590
|
+
}
|
|
7272
7591
|
if (item.collection) {
|
|
7273
7592
|
const col = collectionsBySlug.get(item.collection);
|
|
7274
7593
|
const combinedViews = [
|
|
@@ -7378,6 +7697,23 @@ function compileNavigation(config) {
|
|
|
7378
7697
|
views: []
|
|
7379
7698
|
});
|
|
7380
7699
|
}
|
|
7700
|
+
const mentionedTrash = explicitNav.some((i) => i.trash || i.slug === "trash");
|
|
7701
|
+
if (!mentionedTrash) {
|
|
7702
|
+
const hasTrash = collections.some(
|
|
7703
|
+
(col) => !col.slug.startsWith("__") && resolveTrashConfig(col, config).enabled
|
|
7704
|
+
);
|
|
7705
|
+
if (hasTrash) {
|
|
7706
|
+
registerItem(void 0, {
|
|
7707
|
+
id: "trash",
|
|
7708
|
+
type: "trash",
|
|
7709
|
+
slug: "trash",
|
|
7710
|
+
label: "Trash",
|
|
7711
|
+
icon: "Trash2",
|
|
7712
|
+
order: 950,
|
|
7713
|
+
views: []
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7381
7717
|
const sortAndSpliceItems = (items) => {
|
|
7382
7718
|
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
7383
7719
|
const explicitMap = /* @__PURE__ */ new Map();
|
|
@@ -7545,6 +7881,133 @@ function defineImageField(config) {
|
|
|
7545
7881
|
function defineJoinField(config) {
|
|
7546
7882
|
return _defineJoinFieldBase(config);
|
|
7547
7883
|
}
|
|
7884
|
+
function defineCreatedAtField(config) {
|
|
7885
|
+
const type = config?.type ?? "datetime";
|
|
7886
|
+
const name = config?.name ?? "createdAt";
|
|
7887
|
+
return {
|
|
7888
|
+
name,
|
|
7889
|
+
label: "Created At",
|
|
7890
|
+
...config,
|
|
7891
|
+
type,
|
|
7892
|
+
admin: {
|
|
7893
|
+
readOnly: true,
|
|
7894
|
+
...config?.admin
|
|
7895
|
+
}
|
|
7896
|
+
};
|
|
7897
|
+
}
|
|
7898
|
+
function defineUpdatedAtField(config) {
|
|
7899
|
+
const type = config?.type ?? "datetime";
|
|
7900
|
+
const name = config?.name ?? "updatedAt";
|
|
7901
|
+
return {
|
|
7902
|
+
name,
|
|
7903
|
+
label: "Updated At",
|
|
7904
|
+
...config,
|
|
7905
|
+
type,
|
|
7906
|
+
admin: {
|
|
7907
|
+
readOnly: true,
|
|
7908
|
+
...config?.admin
|
|
7909
|
+
}
|
|
7910
|
+
};
|
|
7911
|
+
}
|
|
7912
|
+
function defineCreatedByField(config) {
|
|
7913
|
+
const name = config?.name ?? "createdBy";
|
|
7914
|
+
if (config?.type === "text") {
|
|
7915
|
+
const { type: _2, ...rest2 } = config;
|
|
7916
|
+
return {
|
|
7917
|
+
name,
|
|
7918
|
+
label: "Created By",
|
|
7919
|
+
...rest2,
|
|
7920
|
+
type: "text",
|
|
7921
|
+
admin: {
|
|
7922
|
+
readOnly: true,
|
|
7923
|
+
...rest2?.admin
|
|
7924
|
+
}
|
|
7925
|
+
};
|
|
7926
|
+
}
|
|
7927
|
+
const { type: _, relationTo = "users", ...rest } = config || {};
|
|
7928
|
+
return {
|
|
7929
|
+
name,
|
|
7930
|
+
label: "Created By",
|
|
7931
|
+
...rest,
|
|
7932
|
+
type: "relationship",
|
|
7933
|
+
relationTo,
|
|
7934
|
+
admin: {
|
|
7935
|
+
readOnly: true,
|
|
7936
|
+
...rest?.admin
|
|
7937
|
+
},
|
|
7938
|
+
access: {
|
|
7939
|
+
update: () => false,
|
|
7940
|
+
...rest?.access
|
|
7941
|
+
}
|
|
7942
|
+
};
|
|
7943
|
+
}
|
|
7944
|
+
function defineUpdatedByField(config) {
|
|
7945
|
+
const name = config?.name ?? "updatedBy";
|
|
7946
|
+
if (config?.type === "text") {
|
|
7947
|
+
const { type: _2, ...rest2 } = config;
|
|
7948
|
+
return {
|
|
7949
|
+
name,
|
|
7950
|
+
label: "Updated By",
|
|
7951
|
+
...rest2,
|
|
7952
|
+
type: "text",
|
|
7953
|
+
admin: {
|
|
7954
|
+
readOnly: true,
|
|
7955
|
+
...rest2?.admin
|
|
7956
|
+
}
|
|
7957
|
+
};
|
|
7958
|
+
}
|
|
7959
|
+
const { type: _, relationTo = "users", ...rest } = config || {};
|
|
7960
|
+
return {
|
|
7961
|
+
name,
|
|
7962
|
+
label: "Updated By",
|
|
7963
|
+
...rest,
|
|
7964
|
+
type: "relationship",
|
|
7965
|
+
relationTo,
|
|
7966
|
+
admin: {
|
|
7967
|
+
readOnly: true,
|
|
7968
|
+
...rest?.admin
|
|
7969
|
+
},
|
|
7970
|
+
access: {
|
|
7971
|
+
update: () => false,
|
|
7972
|
+
...rest?.access
|
|
7973
|
+
}
|
|
7974
|
+
};
|
|
7975
|
+
}
|
|
7976
|
+
var DEFAULT_ROLES = [
|
|
7977
|
+
{ value: "admin", label: "Admin" },
|
|
7978
|
+
{ value: "editor", label: "Editor" },
|
|
7979
|
+
{ value: "viewer", label: "Viewer" }
|
|
7980
|
+
];
|
|
7981
|
+
function defineRolesField(config) {
|
|
7982
|
+
const isMultiple = Boolean(config?.multiple || config?.hasMany || config?.type === "multiSelect");
|
|
7983
|
+
const type = isMultiple ? "multiSelect" : "select";
|
|
7984
|
+
const name = config?.name ?? "roles";
|
|
7985
|
+
const label = config?.label ?? (name === "role" ? "Role" : "Roles");
|
|
7986
|
+
const rawRoles = config?.roles ?? config?.options ?? DEFAULT_ROLES;
|
|
7987
|
+
const resolvedOptions = Array.isArray(rawRoles) ? rawRoles.map(
|
|
7988
|
+
(r) => typeof r === "string" ? { value: r, label: r.charAt(0).toUpperCase() + r.slice(1).replace(/[-_]/g, " ") } : r
|
|
7989
|
+
) : rawRoles;
|
|
7990
|
+
const lastOption = Array.isArray(resolvedOptions) && resolvedOptions.length > 0 ? resolvedOptions[resolvedOptions.length - 1] : void 0;
|
|
7991
|
+
const defaultRoleValue = lastOption ? typeof lastOption === "string" ? lastOption : lastOption.value : "viewer";
|
|
7992
|
+
const defaultValue2 = config?.defaultValue !== void 0 ? config.defaultValue : isMultiple ? [defaultRoleValue] : defaultRoleValue;
|
|
7993
|
+
const adminRoles = Array.isArray(config?.adminRole) ? config.adminRole : typeof config?.adminRole === "string" ? [config.adminRole] : ["admin"];
|
|
7994
|
+
const defaultUpdateAccess = adminRoles.map((r) => `user.role == '${r}' || (user.roles != null && '${r}' in user.roles)`).join(" || ");
|
|
7995
|
+
const { multiple: _, roles: __, adminRole: ___, ...rest } = config || {};
|
|
7996
|
+
return {
|
|
7997
|
+
name,
|
|
7998
|
+
label,
|
|
7999
|
+
defaultValue: defaultValue2,
|
|
8000
|
+
options: resolvedOptions,
|
|
8001
|
+
access: {
|
|
8002
|
+
update: defaultUpdateAccess,
|
|
8003
|
+
...rest.access
|
|
8004
|
+
},
|
|
8005
|
+
...rest,
|
|
8006
|
+
type,
|
|
8007
|
+
...isMultiple ? { hasMany: true } : {}
|
|
8008
|
+
};
|
|
8009
|
+
}
|
|
8010
|
+
var defineRoles = defineRolesField;
|
|
7548
8011
|
function defineTab(args) {
|
|
7549
8012
|
return args.fields.map((field) => ({
|
|
7550
8013
|
...field,
|
|
@@ -7579,6 +8042,8 @@ export {
|
|
|
7579
8042
|
RESERVED_NAVIGATION_SLUGS,
|
|
7580
8043
|
TASK_LOCKS_COLLECTION,
|
|
7581
8044
|
TASK_LOCKS_COLLECTION_CONFIG,
|
|
8045
|
+
TRASH_COLLECTION,
|
|
8046
|
+
TRASH_COLLECTION_CONFIG,
|
|
7582
8047
|
UNASSIGNED_GROUP_KEY,
|
|
7583
8048
|
ValidationError,
|
|
7584
8049
|
WORKFLOW_HISTORY_COLLECTION,
|
|
@@ -7592,6 +8057,7 @@ export {
|
|
|
7592
8057
|
assertValidDeclarativeHooksInConfig,
|
|
7593
8058
|
assertValidNavigationSlugs,
|
|
7594
8059
|
assertValidPreviewUrlsInConfig,
|
|
8060
|
+
assertValidTrashInConfig,
|
|
7595
8061
|
availableWorkflowTransitions,
|
|
7596
8062
|
buildSqlOrderBy,
|
|
7597
8063
|
canViewWorkflowDraft,
|
|
@@ -7610,6 +8076,7 @@ export {
|
|
|
7610
8076
|
count,
|
|
7611
8077
|
createLifecycleEvent,
|
|
7612
8078
|
createTaskRunner,
|
|
8079
|
+
createTrashPurgeTask,
|
|
7613
8080
|
createWorkflowDocument,
|
|
7614
8081
|
defaultValue,
|
|
7615
8082
|
defineAction,
|
|
@@ -7621,6 +8088,8 @@ export {
|
|
|
7621
8088
|
defineCardsView,
|
|
7622
8089
|
defineCollection,
|
|
7623
8090
|
defineConfig,
|
|
8091
|
+
defineCreatedAtField,
|
|
8092
|
+
defineCreatedByField,
|
|
7624
8093
|
defineDateField,
|
|
7625
8094
|
defineDateTimeField,
|
|
7626
8095
|
defineEmailField,
|
|
@@ -7640,6 +8109,8 @@ export {
|
|
|
7640
8109
|
defineRadioField,
|
|
7641
8110
|
defineRelationshipField,
|
|
7642
8111
|
defineRichTextField,
|
|
8112
|
+
defineRoles,
|
|
8113
|
+
defineRolesField,
|
|
7643
8114
|
defineRowField,
|
|
7644
8115
|
defineSelectField,
|
|
7645
8116
|
defineTab,
|
|
@@ -7648,6 +8119,8 @@ export {
|
|
|
7648
8119
|
defineTextField,
|
|
7649
8120
|
defineTextareaField,
|
|
7650
8121
|
defineTimeField,
|
|
8122
|
+
defineUpdatedAtField,
|
|
8123
|
+
defineUpdatedByField,
|
|
7651
8124
|
defineUrlField,
|
|
7652
8125
|
defineView,
|
|
7653
8126
|
defineWorkspace,
|
|
@@ -7687,6 +8160,7 @@ export {
|
|
|
7687
8160
|
getAdminRoleForCollection,
|
|
7688
8161
|
getAllowedSitesForUser,
|
|
7689
8162
|
getPublicAdminAuthConfig,
|
|
8163
|
+
getTrashEntryId,
|
|
7690
8164
|
includes,
|
|
7691
8165
|
initializeWorkflowDocument,
|
|
7692
8166
|
isAICollection,
|
|
@@ -7730,14 +8204,18 @@ export {
|
|
|
7730
8204
|
resolveAdminAuthCollection,
|
|
7731
8205
|
resolveAllCollectionViews,
|
|
7732
8206
|
resolveAuthorizedSiteId,
|
|
8207
|
+
resolveDocumentTitle,
|
|
7733
8208
|
resolveIndexName,
|
|
7734
8209
|
resolvePrefix,
|
|
8210
|
+
resolveTrashConfig,
|
|
8211
|
+
restoreDocument,
|
|
7735
8212
|
round,
|
|
7736
8213
|
runCollectionHooks,
|
|
7737
8214
|
sanitizeDocForAI,
|
|
7738
8215
|
saveWorkflowDraft,
|
|
7739
8216
|
simplePublishingWorkflow,
|
|
7740
8217
|
slugify,
|
|
8218
|
+
softDeleteDocument,
|
|
7741
8219
|
sqrt,
|
|
7742
8220
|
startsWith,
|
|
7743
8221
|
sum,
|