@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.cjs
CHANGED
|
@@ -1811,6 +1811,8 @@ __export(index_exports, {
|
|
|
1811
1811
|
RESERVED_NAVIGATION_SLUGS: () => RESERVED_NAVIGATION_SLUGS,
|
|
1812
1812
|
TASK_LOCKS_COLLECTION: () => TASK_LOCKS_COLLECTION,
|
|
1813
1813
|
TASK_LOCKS_COLLECTION_CONFIG: () => TASK_LOCKS_COLLECTION_CONFIG,
|
|
1814
|
+
TRASH_COLLECTION: () => TRASH_COLLECTION,
|
|
1815
|
+
TRASH_COLLECTION_CONFIG: () => TRASH_COLLECTION_CONFIG,
|
|
1814
1816
|
UNASSIGNED_GROUP_KEY: () => UNASSIGNED_GROUP_KEY,
|
|
1815
1817
|
ValidationError: () => ValidationError,
|
|
1816
1818
|
WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
|
|
@@ -1824,6 +1826,7 @@ __export(index_exports, {
|
|
|
1824
1826
|
assertValidDeclarativeHooksInConfig: () => assertValidDeclarativeHooksInConfig,
|
|
1825
1827
|
assertValidNavigationSlugs: () => assertValidNavigationSlugs,
|
|
1826
1828
|
assertValidPreviewUrlsInConfig: () => assertValidPreviewUrlsInConfig,
|
|
1829
|
+
assertValidTrashInConfig: () => assertValidTrashInConfig,
|
|
1827
1830
|
availableWorkflowTransitions: () => availableWorkflowTransitions,
|
|
1828
1831
|
buildSqlOrderBy: () => buildSqlOrderBy,
|
|
1829
1832
|
canViewWorkflowDraft: () => canViewWorkflowDraft,
|
|
@@ -1842,6 +1845,7 @@ __export(index_exports, {
|
|
|
1842
1845
|
count: () => count,
|
|
1843
1846
|
createLifecycleEvent: () => createLifecycleEvent,
|
|
1844
1847
|
createTaskRunner: () => createTaskRunner,
|
|
1848
|
+
createTrashPurgeTask: () => createTrashPurgeTask,
|
|
1845
1849
|
createWorkflowDocument: () => createWorkflowDocument,
|
|
1846
1850
|
defaultValue: () => defaultValue,
|
|
1847
1851
|
defineAction: () => defineAction,
|
|
@@ -1853,6 +1857,8 @@ __export(index_exports, {
|
|
|
1853
1857
|
defineCardsView: () => defineCardsView,
|
|
1854
1858
|
defineCollection: () => defineCollection,
|
|
1855
1859
|
defineConfig: () => defineConfig,
|
|
1860
|
+
defineCreatedAtField: () => defineCreatedAtField,
|
|
1861
|
+
defineCreatedByField: () => defineCreatedByField,
|
|
1856
1862
|
defineDateField: () => defineDateField,
|
|
1857
1863
|
defineDateTimeField: () => defineDateTimeField,
|
|
1858
1864
|
defineEmailField: () => defineEmailField,
|
|
@@ -1872,6 +1878,8 @@ __export(index_exports, {
|
|
|
1872
1878
|
defineRadioField: () => defineRadioField,
|
|
1873
1879
|
defineRelationshipField: () => defineRelationshipField,
|
|
1874
1880
|
defineRichTextField: () => defineRichTextField,
|
|
1881
|
+
defineRoles: () => defineRoles,
|
|
1882
|
+
defineRolesField: () => defineRolesField,
|
|
1875
1883
|
defineRowField: () => defineRowField,
|
|
1876
1884
|
defineSelectField: () => defineSelectField,
|
|
1877
1885
|
defineTab: () => defineTab,
|
|
@@ -1880,6 +1888,8 @@ __export(index_exports, {
|
|
|
1880
1888
|
defineTextField: () => defineTextField,
|
|
1881
1889
|
defineTextareaField: () => defineTextareaField,
|
|
1882
1890
|
defineTimeField: () => defineTimeField,
|
|
1891
|
+
defineUpdatedAtField: () => defineUpdatedAtField,
|
|
1892
|
+
defineUpdatedByField: () => defineUpdatedByField,
|
|
1883
1893
|
defineUrlField: () => defineUrlField,
|
|
1884
1894
|
defineView: () => defineView,
|
|
1885
1895
|
defineWorkspace: () => defineWorkspace,
|
|
@@ -1919,6 +1929,7 @@ __export(index_exports, {
|
|
|
1919
1929
|
getAdminRoleForCollection: () => getAdminRoleForCollection,
|
|
1920
1930
|
getAllowedSitesForUser: () => getAllowedSitesForUser,
|
|
1921
1931
|
getPublicAdminAuthConfig: () => getPublicAdminAuthConfig,
|
|
1932
|
+
getTrashEntryId: () => getTrashEntryId,
|
|
1922
1933
|
includes: () => includes,
|
|
1923
1934
|
initializeWorkflowDocument: () => initializeWorkflowDocument,
|
|
1924
1935
|
isAICollection: () => isAICollection,
|
|
@@ -1962,14 +1973,18 @@ __export(index_exports, {
|
|
|
1962
1973
|
resolveAdminAuthCollection: () => resolveAdminAuthCollection,
|
|
1963
1974
|
resolveAllCollectionViews: () => resolveAllCollectionViews,
|
|
1964
1975
|
resolveAuthorizedSiteId: () => resolveAuthorizedSiteId,
|
|
1976
|
+
resolveDocumentTitle: () => resolveDocumentTitle,
|
|
1965
1977
|
resolveIndexName: () => resolveIndexName,
|
|
1966
1978
|
resolvePrefix: () => resolvePrefix,
|
|
1979
|
+
resolveTrashConfig: () => resolveTrashConfig,
|
|
1980
|
+
restoreDocument: () => restoreDocument,
|
|
1967
1981
|
round: () => round,
|
|
1968
1982
|
runCollectionHooks: () => runCollectionHooks,
|
|
1969
1983
|
sanitizeDocForAI: () => sanitizeDocForAI,
|
|
1970
1984
|
saveWorkflowDraft: () => saveWorkflowDraft,
|
|
1971
1985
|
simplePublishingWorkflow: () => simplePublishingWorkflow,
|
|
1972
1986
|
slugify: () => slugify,
|
|
1987
|
+
softDeleteDocument: () => softDeleteDocument,
|
|
1973
1988
|
sqrt: () => sqrt,
|
|
1974
1989
|
startsWith: () => startsWith,
|
|
1975
1990
|
sum: () => sum,
|
|
@@ -2083,1821 +2098,2318 @@ var import_jose = require("jose");
|
|
|
2083
2098
|
// src/auth/sessions.ts
|
|
2084
2099
|
var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
|
|
2085
2100
|
|
|
2086
|
-
// src/
|
|
2087
|
-
var
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
{ name: "lastStartedAt", type: "date" },
|
|
2098
|
-
{ name: "lastFinishedAt", type: "date" },
|
|
2099
|
-
{ name: "lastStatus", type: "text" },
|
|
2100
|
-
{ name: "lastError", type: "textarea" }
|
|
2101
|
-
],
|
|
2102
|
-
access: {
|
|
2103
|
-
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
2104
|
-
create: () => false,
|
|
2105
|
-
update: () => false,
|
|
2106
|
-
delete: () => false
|
|
2107
|
-
},
|
|
2108
|
-
admin: { hidden: true }
|
|
2109
|
-
};
|
|
2110
|
-
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
2111
|
-
var DEFAULT_TICK_MS = 3e4;
|
|
2112
|
-
var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
2113
|
-
var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
2114
|
-
function parseCronField(raw, min2, max2, names, nameOffset, label) {
|
|
2115
|
-
const values = /* @__PURE__ */ new Set();
|
|
2116
|
-
const toNumber = (token) => {
|
|
2117
|
-
const named = names?.indexOf(token.toLowerCase());
|
|
2118
|
-
if (named !== void 0 && named >= 0) return named + nameOffset;
|
|
2119
|
-
if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
|
|
2120
|
-
return Number(token);
|
|
2121
|
-
};
|
|
2122
|
-
for (const part of raw.split(",")) {
|
|
2123
|
-
const [rangePart, stepPart] = part.split("/");
|
|
2124
|
-
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
2125
|
-
if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
|
|
2126
|
-
let start;
|
|
2127
|
-
let end;
|
|
2128
|
-
if (rangePart === "*") {
|
|
2129
|
-
start = min2;
|
|
2130
|
-
end = max2;
|
|
2131
|
-
} else if (rangePart.includes("-")) {
|
|
2132
|
-
const [a, b] = rangePart.split("-");
|
|
2133
|
-
start = toNumber(a);
|
|
2134
|
-
end = toNumber(b);
|
|
2135
|
-
} else {
|
|
2136
|
-
start = toNumber(rangePart);
|
|
2137
|
-
end = stepPart === void 0 ? start : max2;
|
|
2138
|
-
}
|
|
2139
|
-
if (start < min2 || end > max2 || start > end) {
|
|
2140
|
-
throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
|
|
2141
|
-
}
|
|
2142
|
-
for (let v = start; v <= end; v += step) values.add(v);
|
|
2143
|
-
}
|
|
2144
|
-
return values;
|
|
2101
|
+
// src/utils/hooks.ts
|
|
2102
|
+
var import_jexl3 = __toESM(require_Jexl(), 1);
|
|
2103
|
+
|
|
2104
|
+
// src/utils/declarative-hooks.ts
|
|
2105
|
+
var import_jexl2 = __toESM(require_Jexl(), 1);
|
|
2106
|
+
|
|
2107
|
+
// src/utils/jexl-helpers.ts
|
|
2108
|
+
var import_jexl = __toESM(require_Jexl(), 1);
|
|
2109
|
+
function slugify(val) {
|
|
2110
|
+
if (val === void 0 || val === null) return "";
|
|
2111
|
+
return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2145
2112
|
}
|
|
2146
|
-
function
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
|
|
2150
|
-
}
|
|
2151
|
-
const [min2, hour, dom, mon, dow] = fields;
|
|
2152
|
-
const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
|
|
2153
|
-
if (daysOfWeek.has(7)) daysOfWeek.add(0);
|
|
2154
|
-
daysOfWeek.delete(7);
|
|
2155
|
-
return {
|
|
2156
|
-
minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
|
|
2157
|
-
hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
|
|
2158
|
-
daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
|
|
2159
|
-
months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
|
|
2160
|
-
daysOfWeek,
|
|
2161
|
-
dayFieldsBothRestricted: dom !== "*" && dow !== "*"
|
|
2162
|
-
};
|
|
2113
|
+
function lower(val) {
|
|
2114
|
+
if (val === void 0 || val === null) return "";
|
|
2115
|
+
return String(val).toLowerCase();
|
|
2163
2116
|
}
|
|
2164
|
-
function
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
2117
|
+
function upper(val) {
|
|
2118
|
+
if (val === void 0 || val === null) return "";
|
|
2119
|
+
return String(val).toUpperCase();
|
|
2168
2120
|
}
|
|
2169
|
-
function
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
cursor.setUTCSeconds(0, 0);
|
|
2173
|
-
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
|
|
2174
|
-
const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
2175
|
-
while (cursor.getTime() <= limit) {
|
|
2176
|
-
if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
|
|
2177
|
-
cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
|
|
2178
|
-
cursor.setUTCHours(0, 0, 0, 0);
|
|
2179
|
-
continue;
|
|
2180
|
-
}
|
|
2181
|
-
if (!dayMatches(schedule, cursor)) {
|
|
2182
|
-
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
2183
|
-
cursor.setUTCHours(0, 0, 0, 0);
|
|
2184
|
-
continue;
|
|
2185
|
-
}
|
|
2186
|
-
if (!schedule.hours.has(cursor.getUTCHours())) {
|
|
2187
|
-
cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
|
|
2188
|
-
continue;
|
|
2189
|
-
}
|
|
2190
|
-
if (!schedule.minutes.has(cursor.getUTCMinutes())) {
|
|
2191
|
-
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
|
|
2192
|
-
continue;
|
|
2193
|
-
}
|
|
2194
|
-
return cursor;
|
|
2195
|
-
}
|
|
2196
|
-
throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
|
|
2121
|
+
function trim(val) {
|
|
2122
|
+
if (val === void 0 || val === null) return "";
|
|
2123
|
+
return String(val).trim();
|
|
2197
2124
|
}
|
|
2198
|
-
function
|
|
2199
|
-
if (
|
|
2200
|
-
|
|
2201
|
-
if (
|
|
2202
|
-
|
|
2203
|
-
}
|
|
2204
|
-
return task;
|
|
2125
|
+
function capitalize(val) {
|
|
2126
|
+
if (val === void 0 || val === null) return "";
|
|
2127
|
+
const str = String(val).trim();
|
|
2128
|
+
if (!str) return "";
|
|
2129
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
2205
2130
|
}
|
|
2206
|
-
function
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
return
|
|
2211
|
-
|
|
2212
|
-
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
2213
|
-
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
2214
|
-
};
|
|
2131
|
+
function truncate(val, length2, ellipsis = "...") {
|
|
2132
|
+
if (val === void 0 || val === null) return "";
|
|
2133
|
+
const str = String(val);
|
|
2134
|
+
const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
|
|
2135
|
+
if (str.length <= maxLen) return str;
|
|
2136
|
+
return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
|
|
2215
2137
|
}
|
|
2216
|
-
function
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
return
|
|
2138
|
+
function readingTime(val, wpm = 200) {
|
|
2139
|
+
if (val === void 0 || val === null) return 1;
|
|
2140
|
+
const str = String(val).trim();
|
|
2141
|
+
if (!str) return 1;
|
|
2142
|
+
const words = str.split(/\s+/).filter(Boolean).length;
|
|
2143
|
+
return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
|
|
2220
2144
|
}
|
|
2221
|
-
function
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2145
|
+
function wordCount(val) {
|
|
2146
|
+
if (val === void 0 || val === null) return 0;
|
|
2147
|
+
const str = String(val).trim();
|
|
2148
|
+
if (!str) return 0;
|
|
2149
|
+
return str.split(/\s+/).filter(Boolean).length;
|
|
2150
|
+
}
|
|
2151
|
+
function replace(val, search, replacement) {
|
|
2152
|
+
if (val === void 0 || val === null) return "";
|
|
2153
|
+
return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
|
|
2154
|
+
}
|
|
2155
|
+
function startsWith(str, prefix) {
|
|
2156
|
+
if (str === void 0 || str === null) return false;
|
|
2157
|
+
return String(str).startsWith(String(prefix ?? ""));
|
|
2158
|
+
}
|
|
2159
|
+
function endsWith(str, suffix) {
|
|
2160
|
+
if (str === void 0 || str === null) return false;
|
|
2161
|
+
return String(str).endsWith(String(suffix ?? ""));
|
|
2162
|
+
}
|
|
2163
|
+
function now() {
|
|
2164
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2165
|
+
}
|
|
2166
|
+
function today() {
|
|
2167
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2168
|
+
}
|
|
2169
|
+
function formatDate(val, style = "short") {
|
|
2170
|
+
if (!val) return "";
|
|
2171
|
+
const d = new Date(val);
|
|
2172
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
2173
|
+
const styleStr = String(style).toLowerCase();
|
|
2174
|
+
if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
|
|
2175
|
+
if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
|
|
2176
|
+
return d.toLocaleDateString("en-US", {
|
|
2177
|
+
year: "numeric",
|
|
2178
|
+
month: "short",
|
|
2179
|
+
day: "numeric"
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
function addDays(val, days) {
|
|
2183
|
+
const d = val ? new Date(val) : /* @__PURE__ */ new Date();
|
|
2184
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
2185
|
+
const daysNum = typeof days === "number" ? days : Number(days) || 0;
|
|
2186
|
+
d.setDate(d.getDate() + daysNum);
|
|
2187
|
+
return d.toISOString();
|
|
2188
|
+
}
|
|
2189
|
+
function diffDays(dateA, dateB) {
|
|
2190
|
+
const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
|
|
2191
|
+
const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
|
|
2192
|
+
if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
|
|
2193
|
+
const diffMs = Math.abs(dA.getTime() - dB.getTime());
|
|
2194
|
+
return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
2195
|
+
}
|
|
2196
|
+
function isPast(val) {
|
|
2197
|
+
if (!val) return false;
|
|
2198
|
+
const d = new Date(val);
|
|
2199
|
+
if (Number.isNaN(d.getTime())) return false;
|
|
2200
|
+
return d.getTime() < Date.now();
|
|
2201
|
+
}
|
|
2202
|
+
function isFuture(val) {
|
|
2203
|
+
if (!val) return false;
|
|
2204
|
+
const d = new Date(val);
|
|
2205
|
+
if (Number.isNaN(d.getTime())) return false;
|
|
2206
|
+
return d.getTime() > Date.now();
|
|
2207
|
+
}
|
|
2208
|
+
function includes(arrOrStr, item) {
|
|
2209
|
+
if (Array.isArray(arrOrStr)) {
|
|
2210
|
+
return arrOrStr.includes(item);
|
|
2228
2211
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
let inFlight = Promise.resolve();
|
|
2232
|
-
let ticking = false;
|
|
2233
|
-
let ready;
|
|
2234
|
-
function ensureReady() {
|
|
2235
|
-
ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
|
|
2236
|
-
ready.catch(() => {
|
|
2237
|
-
ready = void 0;
|
|
2238
|
-
});
|
|
2239
|
-
return ready;
|
|
2212
|
+
if (typeof arrOrStr === "string") {
|
|
2213
|
+
return arrOrStr.includes(String(item ?? ""));
|
|
2240
2214
|
}
|
|
2241
|
-
|
|
2242
|
-
await ensureReady();
|
|
2243
|
-
const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2244
|
-
if (existing) return existing;
|
|
2245
|
-
try {
|
|
2246
|
-
return await db.create({
|
|
2247
|
-
collection: TASK_LOCKS_COLLECTION,
|
|
2248
|
-
data: {
|
|
2249
|
-
id: name,
|
|
2250
|
-
name,
|
|
2251
|
-
lockedUntil: 0,
|
|
2252
|
-
lockedBy: null,
|
|
2253
|
-
lastScheduledFor: floorToMinute(now2).toISOString(),
|
|
2254
|
-
lastStartedAt: null,
|
|
2255
|
-
lastFinishedAt: null,
|
|
2256
|
-
lastStatus: null,
|
|
2257
|
-
lastError: null
|
|
2258
|
-
}
|
|
2259
|
-
});
|
|
2260
|
-
} catch (error) {
|
|
2261
|
-
const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
2262
|
-
if (created) return created;
|
|
2263
|
-
throw error;
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
async function execute(task, row, scheduledFor) {
|
|
2267
|
-
const started = Date.now();
|
|
2268
|
-
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
2269
|
-
const token = `${instanceId}:${started}`;
|
|
2270
|
-
const where = {
|
|
2271
|
-
id: { equals: task.name },
|
|
2272
|
-
lockedUntil: { less_than: started }
|
|
2273
|
-
};
|
|
2274
|
-
if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
|
|
2275
|
-
const claimed = await db.update({
|
|
2276
|
-
collection: TASK_LOCKS_COLLECTION,
|
|
2277
|
-
where,
|
|
2278
|
-
data: {
|
|
2279
|
-
lockedUntil: started + lockMs,
|
|
2280
|
-
lockedBy: token,
|
|
2281
|
-
lastStartedAt: new Date(started).toISOString(),
|
|
2282
|
-
...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
|
|
2283
|
-
}
|
|
2284
|
-
});
|
|
2285
|
-
if (claimed.affectedRows !== 1) {
|
|
2286
|
-
return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
|
|
2287
|
-
}
|
|
2288
|
-
const logger = resolveLogger(config, task.name);
|
|
2289
|
-
const controller = new AbortController();
|
|
2290
|
-
let timeout;
|
|
2291
|
-
let status = "completed";
|
|
2292
|
-
let errorMessage;
|
|
2293
|
-
try {
|
|
2294
|
-
await Promise.race([
|
|
2295
|
-
Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
|
|
2296
|
-
new Promise((_, reject) => {
|
|
2297
|
-
timeout = setTimeout(() => {
|
|
2298
|
-
reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
|
|
2299
|
-
controller.abort();
|
|
2300
|
-
}, lockMs);
|
|
2301
|
-
})
|
|
2302
|
-
]);
|
|
2303
|
-
} catch (error) {
|
|
2304
|
-
status = "failed";
|
|
2305
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
2306
|
-
logger.error({ err: error }, "Task failed");
|
|
2307
|
-
} finally {
|
|
2308
|
-
if (timeout) clearTimeout(timeout);
|
|
2309
|
-
}
|
|
2310
|
-
try {
|
|
2311
|
-
await db.update({
|
|
2312
|
-
collection: TASK_LOCKS_COLLECTION,
|
|
2313
|
-
where: { id: { equals: task.name }, lockedBy: { equals: token } },
|
|
2314
|
-
data: {
|
|
2315
|
-
lockedUntil: 0,
|
|
2316
|
-
lockedBy: null,
|
|
2317
|
-
lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2318
|
-
lastStatus: status,
|
|
2319
|
-
lastError: errorMessage ?? null
|
|
2320
|
-
}
|
|
2321
|
-
});
|
|
2322
|
-
} catch (error) {
|
|
2323
|
-
logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
|
|
2324
|
-
}
|
|
2325
|
-
return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
|
|
2326
|
-
}
|
|
2327
|
-
function track(work) {
|
|
2328
|
-
inFlight = inFlight.then(() => work).catch(() => void 0);
|
|
2329
|
-
return work;
|
|
2330
|
-
}
|
|
2331
|
-
const runner = {
|
|
2332
|
-
async runTask(name) {
|
|
2333
|
-
const task = tasks.get(name);
|
|
2334
|
-
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
2335
|
-
const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
2336
|
-
return track(execute(task, row, void 0));
|
|
2337
|
-
},
|
|
2338
|
-
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
2339
|
-
const results = [];
|
|
2340
|
-
for (const task of tasks.values()) {
|
|
2341
|
-
if (!task.schedule) continue;
|
|
2342
|
-
const row = await ensureLockRow(task.name, now2);
|
|
2343
|
-
const reference = new Date(String(row.lastScheduledFor));
|
|
2344
|
-
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
2345
|
-
if (!due) {
|
|
2346
|
-
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
2347
|
-
continue;
|
|
2348
|
-
}
|
|
2349
|
-
results.push(await track(execute(task, row, floorToMinute(now2))));
|
|
2350
|
-
}
|
|
2351
|
-
return results;
|
|
2352
|
-
},
|
|
2353
|
-
start(options) {
|
|
2354
|
-
if (timer) return;
|
|
2355
|
-
const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
|
|
2356
|
-
const tick = () => {
|
|
2357
|
-
if (ticking) return;
|
|
2358
|
-
ticking = true;
|
|
2359
|
-
runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
|
|
2360
|
-
ticking = false;
|
|
2361
|
-
});
|
|
2362
|
-
};
|
|
2363
|
-
timer = setInterval(tick, intervalMs);
|
|
2364
|
-
timer.unref?.();
|
|
2365
|
-
setTimeout(tick, 0);
|
|
2366
|
-
},
|
|
2367
|
-
async stop() {
|
|
2368
|
-
if (timer) clearInterval(timer);
|
|
2369
|
-
timer = void 0;
|
|
2370
|
-
await inFlight;
|
|
2371
|
-
}
|
|
2372
|
-
};
|
|
2373
|
-
return runner;
|
|
2215
|
+
return false;
|
|
2374
2216
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
2379
|
-
var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
|
|
2380
|
-
var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
|
|
2381
|
-
function definePublishingWorkflow(options = {}) {
|
|
2382
|
-
const editors = options.editors ?? ["editor"];
|
|
2383
|
-
const publishers = options.publishers ?? ["publisher", "admin"];
|
|
2384
|
-
return {
|
|
2385
|
-
initialState: "draft",
|
|
2386
|
-
draftState: "draft",
|
|
2387
|
-
states: [
|
|
2388
|
-
{ name: "draft", label: "Draft", color: "neutral" },
|
|
2389
|
-
{ name: "in_review", label: "In review", color: "warning" },
|
|
2390
|
-
{ name: "published", label: "Published", color: "success", published: true }
|
|
2391
|
-
],
|
|
2392
|
-
transitions: [
|
|
2393
|
-
{
|
|
2394
|
-
name: "submit",
|
|
2395
|
-
label: "Submit for review",
|
|
2396
|
-
from: "draft",
|
|
2397
|
-
to: "in_review",
|
|
2398
|
-
requiredCapabilities: ["entry.submit"]
|
|
2399
|
-
},
|
|
2400
|
-
{
|
|
2401
|
-
name: "publish",
|
|
2402
|
-
label: "Publish",
|
|
2403
|
-
from: "in_review",
|
|
2404
|
-
to: "published",
|
|
2405
|
-
requiredCapabilities: ["entry.publish"]
|
|
2406
|
-
},
|
|
2407
|
-
{
|
|
2408
|
-
name: "reject",
|
|
2409
|
-
label: "Request changes",
|
|
2410
|
-
from: "in_review",
|
|
2411
|
-
to: "draft",
|
|
2412
|
-
requiredCapabilities: ["entry.publish"],
|
|
2413
|
-
requireComment: true
|
|
2414
|
-
},
|
|
2415
|
-
{
|
|
2416
|
-
name: "unpublish",
|
|
2417
|
-
label: "Unpublish",
|
|
2418
|
-
from: "published",
|
|
2419
|
-
to: "draft",
|
|
2420
|
-
requiredCapabilities: ["entry.unpublish"],
|
|
2421
|
-
unpublish: true
|
|
2422
|
-
}
|
|
2423
|
-
],
|
|
2424
|
-
roles: [
|
|
2425
|
-
...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
|
|
2426
|
-
...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
|
|
2427
|
-
]
|
|
2428
|
-
};
|
|
2217
|
+
function join(arr, separator = ", ") {
|
|
2218
|
+
if (!Array.isArray(arr)) return "";
|
|
2219
|
+
return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
|
|
2429
2220
|
}
|
|
2430
|
-
function
|
|
2431
|
-
|
|
2221
|
+
function first(arr) {
|
|
2222
|
+
if (Array.isArray(arr)) return arr[0] ?? null;
|
|
2223
|
+
return null;
|
|
2432
2224
|
}
|
|
2433
|
-
function
|
|
2434
|
-
return
|
|
2435
|
-
|
|
2436
|
-
draftState: "draft",
|
|
2437
|
-
states: [
|
|
2438
|
-
{ name: "draft", label: "Draft", color: "neutral" },
|
|
2439
|
-
{ name: "published", label: "Published", color: "success", published: true }
|
|
2440
|
-
],
|
|
2441
|
-
transitions: [
|
|
2442
|
-
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
2443
|
-
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
2444
|
-
]
|
|
2445
|
-
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
2446
|
-
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
2447
|
-
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
2448
|
-
};
|
|
2225
|
+
function last(arr) {
|
|
2226
|
+
if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
|
|
2227
|
+
return null;
|
|
2449
2228
|
}
|
|
2450
|
-
function
|
|
2451
|
-
|
|
2452
|
-
return
|
|
2229
|
+
function compact(arr) {
|
|
2230
|
+
if (!Array.isArray(arr)) return [];
|
|
2231
|
+
return arr.filter((item) => item !== void 0 && item !== null && item !== "");
|
|
2453
2232
|
}
|
|
2454
|
-
function
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
for (const mapping of workflow.roles ?? []) {
|
|
2458
|
-
if (roles.includes(mapping.role)) mapping.capabilities.forEach((capability) => capabilities.add(capability));
|
|
2459
|
-
}
|
|
2460
|
-
return capabilities;
|
|
2233
|
+
function unique(arr) {
|
|
2234
|
+
if (!Array.isArray(arr)) return [];
|
|
2235
|
+
return Array.from(new Set(arr));
|
|
2461
2236
|
}
|
|
2462
|
-
function
|
|
2463
|
-
if (
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
if (workflow.transitions.some(
|
|
2467
|
-
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
2468
|
-
)) {
|
|
2469
|
-
return true;
|
|
2470
|
-
}
|
|
2471
|
-
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
2472
|
-
return false;
|
|
2237
|
+
function length(val) {
|
|
2238
|
+
if (Array.isArray(val) || typeof val === "string") return val.length;
|
|
2239
|
+
if (val && typeof val === "object") return Object.keys(val).length;
|
|
2240
|
+
return 0;
|
|
2473
2241
|
}
|
|
2474
|
-
function
|
|
2475
|
-
const
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2242
|
+
function round(val, decimals = 0) {
|
|
2243
|
+
const num = Number(val);
|
|
2244
|
+
if (Number.isNaN(num)) return 0;
|
|
2245
|
+
const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
|
|
2246
|
+
const factor = Math.pow(10, dec);
|
|
2247
|
+
return Math.round(num * factor) / factor;
|
|
2480
2248
|
}
|
|
2481
|
-
function
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2249
|
+
function clamp(val, minVal, maxVal) {
|
|
2250
|
+
const num = Number(val) || 0;
|
|
2251
|
+
const min2 = Number(minVal) || 0;
|
|
2252
|
+
const max2 = Number(maxVal) || 0;
|
|
2253
|
+
return Math.min(Math.max(num, min2), max2);
|
|
2486
2254
|
}
|
|
2487
|
-
function
|
|
2488
|
-
return
|
|
2255
|
+
function ceil(val) {
|
|
2256
|
+
return Math.ceil(Number(val) || 0);
|
|
2489
2257
|
}
|
|
2490
|
-
function
|
|
2491
|
-
|
|
2492
|
-
const publishedState = publishedStateName(workflow);
|
|
2493
|
-
if (!meta) {
|
|
2494
|
-
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
2495
|
-
const state = publishedStateName(workflow);
|
|
2496
|
-
return {
|
|
2497
|
-
...working2,
|
|
2498
|
-
_workflow: {
|
|
2499
|
-
state,
|
|
2500
|
-
revision: 1,
|
|
2501
|
-
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
2502
|
-
}
|
|
2503
|
-
};
|
|
2504
|
-
}
|
|
2505
|
-
const { __published, __workflow, ...working } = doc;
|
|
2506
|
-
if (!canViewWorkflowDraft(workflow, user)) {
|
|
2507
|
-
if (!__published || typeof __published !== "object") {
|
|
2508
|
-
if (meta.state === publishedState) {
|
|
2509
|
-
return { ...working, _workflow: publicMetadata(meta) };
|
|
2510
|
-
}
|
|
2511
|
-
return null;
|
|
2512
|
-
}
|
|
2513
|
-
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
2514
|
-
}
|
|
2515
|
-
return {
|
|
2516
|
-
...working,
|
|
2517
|
-
_workflow: {
|
|
2518
|
-
...meta,
|
|
2519
|
-
availableTransitions: availableWorkflowTransitions(workflow, meta.state, user).map((item) => item.name)
|
|
2520
|
-
}
|
|
2521
|
-
};
|
|
2258
|
+
function floor(val) {
|
|
2259
|
+
return Math.floor(Number(val) || 0);
|
|
2522
2260
|
}
|
|
2523
|
-
function
|
|
2524
|
-
return
|
|
2261
|
+
function abs(val) {
|
|
2262
|
+
return Math.abs(Number(val) || 0);
|
|
2525
2263
|
}
|
|
2526
|
-
function
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
name: args.name,
|
|
2530
|
-
collection: args.collection,
|
|
2531
|
-
documentId: args.documentId,
|
|
2532
|
-
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2533
|
-
actorId: args.actorId,
|
|
2534
|
-
payload: args.payload,
|
|
2535
|
-
attempts: 0,
|
|
2536
|
-
status: "pending"
|
|
2537
|
-
};
|
|
2264
|
+
function min(...args) {
|
|
2265
|
+
const nums = args.flat().map((n) => Number(n) || 0);
|
|
2266
|
+
return nums.length ? Math.min(...nums) : 0;
|
|
2538
2267
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2268
|
+
function max(...args) {
|
|
2269
|
+
const nums = args.flat().map((n) => Number(n) || 0);
|
|
2270
|
+
return nums.length ? Math.max(...nums) : 0;
|
|
2541
2271
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
if (!db || !config.events?.handlers.length) return;
|
|
2545
|
-
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
2546
|
-
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
2547
|
-
const attempts = event.attempts + 1;
|
|
2548
|
-
try {
|
|
2549
|
-
await db.update({
|
|
2550
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2551
|
-
id: event.id,
|
|
2552
|
-
data: { status: "processing", attempts }
|
|
2553
|
-
});
|
|
2554
|
-
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
2555
|
-
await db.update({
|
|
2556
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2557
|
-
id: event.id,
|
|
2558
|
-
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
2559
|
-
});
|
|
2560
|
-
} catch (error) {
|
|
2561
|
-
const exhausted = attempts >= maxAttempts;
|
|
2562
|
-
await db.update({
|
|
2563
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2564
|
-
id: event.id,
|
|
2565
|
-
data: {
|
|
2566
|
-
status: "failed",
|
|
2567
|
-
attempts,
|
|
2568
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
2569
|
-
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
2570
|
-
}
|
|
2571
|
-
});
|
|
2572
|
-
}
|
|
2272
|
+
function pow(base, exp) {
|
|
2273
|
+
return Math.pow(Number(base) || 0, Number(exp) || 0);
|
|
2573
2274
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
2577
|
-
const result = await config.db.find({
|
|
2578
|
-
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
2579
|
-
where: { status: { in: ["pending", "failed"] } },
|
|
2580
|
-
sort: "occurredAt",
|
|
2581
|
-
limit
|
|
2582
|
-
});
|
|
2583
|
-
const now2 = Date.now();
|
|
2584
|
-
const due = result.docs.filter(
|
|
2585
|
-
(doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
|
|
2586
|
-
);
|
|
2587
|
-
for (const event of due) await dispatchLifecycleEvent(config, event);
|
|
2588
|
-
return due.length;
|
|
2275
|
+
function sqrt(val) {
|
|
2276
|
+
return Math.sqrt(Number(val) || 0);
|
|
2589
2277
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
const workflow = collection.workflow;
|
|
2594
|
-
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
2595
|
-
const previous = originalDoc.__workflow;
|
|
2596
|
-
const isLegacyPublishedDoc = !previous;
|
|
2597
|
-
const baselineRevision = previous?.revision ?? 1;
|
|
2598
|
-
const event = createLifecycleEvent({
|
|
2599
|
-
name: "revision.created",
|
|
2600
|
-
collection: collection.slug,
|
|
2601
|
-
documentId: id,
|
|
2602
|
-
actorId: user?.sub,
|
|
2603
|
-
payload: {
|
|
2604
|
-
revision: baselineRevision + 1,
|
|
2605
|
-
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
2606
|
-
}
|
|
2607
|
-
});
|
|
2608
|
-
const doc = await db.transaction(async (tx) => {
|
|
2609
|
-
const nextMeta = {
|
|
2610
|
-
...previous ?? {},
|
|
2611
|
-
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
2612
|
-
revision: baselineRevision + 1,
|
|
2613
|
-
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
2614
|
-
};
|
|
2615
|
-
const updateData = {
|
|
2616
|
-
...data,
|
|
2617
|
-
__workflow: nextMeta
|
|
2618
|
-
};
|
|
2619
|
-
if (isLegacyPublishedDoc) {
|
|
2620
|
-
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
2621
|
-
updateData.__published = publishedSnapshot;
|
|
2622
|
-
}
|
|
2623
|
-
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
2624
|
-
await persistEvent(tx, event);
|
|
2625
|
-
await beforeCommit?.(tx, updated);
|
|
2626
|
-
return updated;
|
|
2627
|
-
});
|
|
2628
|
-
void dispatchLifecycleEvent(config, event);
|
|
2629
|
-
return { doc, event };
|
|
2278
|
+
function sum(arr) {
|
|
2279
|
+
if (!Array.isArray(arr)) return Number(arr) || 0;
|
|
2280
|
+
return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
|
|
2630
2281
|
}
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
2636
|
-
const doc = await db.transaction(async (tx) => {
|
|
2637
|
-
const created = await tx.create({ collection: collection.slug, data });
|
|
2638
|
-
event = createLifecycleEvent({
|
|
2639
|
-
name: "revision.created",
|
|
2640
|
-
collection: collection.slug,
|
|
2641
|
-
documentId: created.id,
|
|
2642
|
-
actorId: user?.sub,
|
|
2643
|
-
payload: { revision: 1, previousRevision: null }
|
|
2644
|
-
});
|
|
2645
|
-
await persistEvent(tx, event);
|
|
2646
|
-
await beforeCommit?.(tx, created);
|
|
2647
|
-
return created;
|
|
2648
|
-
});
|
|
2649
|
-
void dispatchLifecycleEvent(config, event);
|
|
2650
|
-
return { doc, event };
|
|
2282
|
+
function count(val) {
|
|
2283
|
+
if (Array.isArray(val)) return val.length;
|
|
2284
|
+
if (typeof val === "string") return val.length;
|
|
2285
|
+
if (val !== void 0 && val !== null && val !== false) return 1;
|
|
2286
|
+
return 0;
|
|
2651
2287
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const
|
|
2658
|
-
|
|
2659
|
-
const meta = original.__workflow;
|
|
2660
|
-
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
2661
|
-
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
2662
|
-
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
2663
|
-
statusCode: 409
|
|
2664
|
-
});
|
|
2665
|
-
}
|
|
2666
|
-
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
2667
|
-
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
2668
|
-
if (!transition || !fromStates.includes(meta.state)) {
|
|
2669
|
-
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
2670
|
-
statusCode: 409
|
|
2671
|
-
});
|
|
2672
|
-
}
|
|
2673
|
-
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
2674
|
-
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
2288
|
+
function defaultValue(val, fallback) {
|
|
2289
|
+
if (val === void 0 || val === null || val === "") return fallback;
|
|
2290
|
+
return val;
|
|
2291
|
+
}
|
|
2292
|
+
function coalesce(...args) {
|
|
2293
|
+
for (const arg of args) {
|
|
2294
|
+
if (arg !== void 0 && arg !== null && arg !== "") return arg;
|
|
2675
2295
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
function isEmpty(val) {
|
|
2299
|
+
if (val === void 0 || val === null || val === "") return true;
|
|
2300
|
+
if (Array.isArray(val)) return val.length === 0;
|
|
2301
|
+
if (typeof val === "object") return Object.keys(val).length === 0;
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
function get(obj, path, fallback = null) {
|
|
2305
|
+
if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
|
|
2306
|
+
if (typeof path !== "string") return fallback;
|
|
2307
|
+
const parts = path.split(".").filter(Boolean);
|
|
2308
|
+
let curr = obj;
|
|
2309
|
+
for (const part of parts) {
|
|
2310
|
+
if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
|
|
2311
|
+
curr = curr[part];
|
|
2678
2312
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
if (transition.onTransition) {
|
|
2734
|
-
await transition.onTransition({
|
|
2735
|
-
transition,
|
|
2736
|
-
from: lockedMeta.state,
|
|
2737
|
-
to: transition.to,
|
|
2738
|
-
doc: locked,
|
|
2739
|
-
input,
|
|
2740
|
-
comment,
|
|
2741
|
-
user,
|
|
2742
|
-
req,
|
|
2743
|
-
tx
|
|
2744
|
-
});
|
|
2745
|
-
}
|
|
2746
|
-
await tx.create({
|
|
2747
|
-
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
2748
|
-
data: {
|
|
2749
|
-
collection: collection.slug,
|
|
2750
|
-
documentId: id,
|
|
2751
|
-
transition: transition.name,
|
|
2752
|
-
from: lockedMeta.state,
|
|
2753
|
-
to: transition.to,
|
|
2754
|
-
revision: lockedMeta.revision,
|
|
2755
|
-
comment: comment ?? null,
|
|
2756
|
-
actorId: user?.sub ?? null,
|
|
2757
|
-
createdAt: now2
|
|
2758
|
-
}
|
|
2759
|
-
});
|
|
2760
|
-
for (const event of events) await persistEvent(tx, event);
|
|
2761
|
-
if (collection.audit) {
|
|
2762
|
-
await tx.create({
|
|
2763
|
-
collection: "__audit",
|
|
2764
|
-
data: {
|
|
2765
|
-
collection: collection.slug,
|
|
2766
|
-
documentId: id,
|
|
2767
|
-
operation: "workflow.transition",
|
|
2768
|
-
user: user?.sub ?? null,
|
|
2769
|
-
timestamp: now2,
|
|
2770
|
-
changes: JSON.stringify({
|
|
2771
|
-
transition: transition.name,
|
|
2772
|
-
from: lockedMeta.state,
|
|
2773
|
-
to: transition.to,
|
|
2774
|
-
revision: lockedMeta.revision
|
|
2775
|
-
})
|
|
2776
|
-
}
|
|
2777
|
-
});
|
|
2313
|
+
return curr === void 0 || curr === null ? fallback : curr;
|
|
2314
|
+
}
|
|
2315
|
+
function registerJexlHelpers(jexlInstance = import_jexl.default) {
|
|
2316
|
+
const helpers = {
|
|
2317
|
+
// Strings
|
|
2318
|
+
slugify,
|
|
2319
|
+
lower,
|
|
2320
|
+
upper,
|
|
2321
|
+
trim,
|
|
2322
|
+
capitalize,
|
|
2323
|
+
truncate,
|
|
2324
|
+
readingTime,
|
|
2325
|
+
wordCount,
|
|
2326
|
+
replace,
|
|
2327
|
+
startsWith,
|
|
2328
|
+
endsWith,
|
|
2329
|
+
// Dates
|
|
2330
|
+
now,
|
|
2331
|
+
today,
|
|
2332
|
+
formatDate,
|
|
2333
|
+
addDays,
|
|
2334
|
+
diffDays,
|
|
2335
|
+
isPast,
|
|
2336
|
+
isFuture,
|
|
2337
|
+
// Arrays
|
|
2338
|
+
includes,
|
|
2339
|
+
join,
|
|
2340
|
+
first,
|
|
2341
|
+
last,
|
|
2342
|
+
compact,
|
|
2343
|
+
unique,
|
|
2344
|
+
length,
|
|
2345
|
+
// Math
|
|
2346
|
+
round,
|
|
2347
|
+
clamp,
|
|
2348
|
+
ceil,
|
|
2349
|
+
floor,
|
|
2350
|
+
abs,
|
|
2351
|
+
min,
|
|
2352
|
+
max,
|
|
2353
|
+
pow,
|
|
2354
|
+
sqrt,
|
|
2355
|
+
sum,
|
|
2356
|
+
count,
|
|
2357
|
+
// Objects & Logical
|
|
2358
|
+
default: defaultValue,
|
|
2359
|
+
coalesce,
|
|
2360
|
+
isEmpty,
|
|
2361
|
+
get
|
|
2362
|
+
};
|
|
2363
|
+
for (const [name, fn] of Object.entries(helpers)) {
|
|
2364
|
+
try {
|
|
2365
|
+
jexlInstance.addFunction(name, fn);
|
|
2366
|
+
} catch {
|
|
2778
2367
|
}
|
|
2779
|
-
return next;
|
|
2780
|
-
});
|
|
2781
|
-
for (const event of events) void dispatchLifecycleEvent(config, event);
|
|
2782
|
-
for (const hook of workflow.hooks?.afterTransition ?? []) {
|
|
2783
2368
|
try {
|
|
2784
|
-
|
|
2785
|
-
} catch
|
|
2786
|
-
try {
|
|
2787
|
-
const obs = config?.__observability;
|
|
2788
|
-
obs?.recordWorkflowHookFailure?.({
|
|
2789
|
-
collection: collection.slug,
|
|
2790
|
-
transition: transition.name
|
|
2791
|
-
});
|
|
2792
|
-
const logger = config?.logger;
|
|
2793
|
-
if (logger && typeof logger.child === "function") {
|
|
2794
|
-
logger.child({ component: "workflow" }).error({
|
|
2795
|
-
err: error,
|
|
2796
|
-
msg: "afterTransition hook failed",
|
|
2797
|
-
collection: collection.slug,
|
|
2798
|
-
transition: transition.name,
|
|
2799
|
-
documentId: id
|
|
2800
|
-
});
|
|
2801
|
-
} else if (logger && typeof logger.error === "function") {
|
|
2802
|
-
logger.error({
|
|
2803
|
-
err: error,
|
|
2804
|
-
msg: "afterTransition hook failed",
|
|
2805
|
-
collection: collection.slug,
|
|
2806
|
-
transition: transition.name,
|
|
2807
|
-
documentId: id
|
|
2808
|
-
});
|
|
2809
|
-
} else {
|
|
2810
|
-
console.error("afterTransition hook failed:", error);
|
|
2811
|
-
}
|
|
2812
|
-
} catch {
|
|
2813
|
-
console.error("afterTransition hook failed:", error);
|
|
2814
|
-
}
|
|
2369
|
+
jexlInstance.addTransform(name, fn);
|
|
2370
|
+
} catch {
|
|
2815
2371
|
}
|
|
2816
2372
|
}
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
const adminsCollection = collections.find((collection) => collection.slug === "__admins");
|
|
2827
|
-
if (adminsCollection) return adminsCollection;
|
|
2828
|
-
if (collectionSlug) {
|
|
2829
|
-
const requestedCollection = collections.find((collection) => collection.slug === collectionSlug);
|
|
2830
|
-
if (requestedCollection) return requestedCollection;
|
|
2373
|
+
try {
|
|
2374
|
+
jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
|
|
2375
|
+
jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
|
|
2376
|
+
jexlInstance.addBinaryOp("in", 30, (left, right) => {
|
|
2377
|
+
if (Array.isArray(right)) return right.includes(left);
|
|
2378
|
+
if (typeof right === "string") return right.includes(String(left ?? ""));
|
|
2379
|
+
return false;
|
|
2380
|
+
});
|
|
2381
|
+
} catch {
|
|
2831
2382
|
}
|
|
2832
|
-
return collections.find((collection) => collection.auth);
|
|
2833
|
-
}
|
|
2834
|
-
function getPublicAdminAuthConfig(adminAuth, collections) {
|
|
2835
|
-
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
2836
|
-
id: provider.id,
|
|
2837
|
-
type: provider.type,
|
|
2838
|
-
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
2839
|
-
autoRedirect: provider.autoRedirect
|
|
2840
|
-
}));
|
|
2841
|
-
const resolvedCollectionSlug = collections ? resolveAdminAuthCollection(collections, adminAuth?.collectionSlug)?.slug : adminAuth?.collectionSlug;
|
|
2842
|
-
return {
|
|
2843
|
-
mode: adminAuth?.mode ?? "local",
|
|
2844
|
-
collectionSlug: resolvedCollectionSlug,
|
|
2845
|
-
provisioningMode: adminAuth?.provisioningMode,
|
|
2846
|
-
providers
|
|
2847
|
-
};
|
|
2848
2383
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
if (!
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2384
|
+
registerJexlHelpers(import_jexl.default);
|
|
2385
|
+
async function evaluateJexl(expression, context = {}) {
|
|
2386
|
+
if (!expression || typeof expression !== "string") return void 0;
|
|
2387
|
+
try {
|
|
2388
|
+
const evalContext = {
|
|
2389
|
+
null: null,
|
|
2390
|
+
undefined: void 0,
|
|
2391
|
+
...context,
|
|
2392
|
+
math: Math,
|
|
2393
|
+
Math
|
|
2394
|
+
};
|
|
2395
|
+
return await import_jexl.default.eval(expression, evalContext);
|
|
2396
|
+
} catch (_err) {
|
|
2397
|
+
return void 0;
|
|
2858
2398
|
}
|
|
2859
|
-
return "admin";
|
|
2860
2399
|
}
|
|
2861
|
-
function
|
|
2862
|
-
if (!
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
if (existing && existing !== block) {
|
|
2876
|
-
throw new Error(
|
|
2877
|
-
`Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
|
|
2878
|
-
);
|
|
2879
|
-
}
|
|
2880
|
-
if (!existing) seen.set(block.slug, block);
|
|
2881
|
-
}
|
|
2882
|
-
return Array.from(seen.values());
|
|
2883
|
-
}
|
|
2884
|
-
function buildRegistry(blocks) {
|
|
2885
|
-
return new Map(blocks.map((block) => [block.slug, block]));
|
|
2886
|
-
}
|
|
2887
|
-
function resolveField(field, registry, blockCache) {
|
|
2888
|
-
const next = { ...field };
|
|
2889
|
-
if (next.fields) {
|
|
2890
|
-
next.fields = next.fields.map(
|
|
2891
|
-
(child) => resolveField(child, registry, blockCache)
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
if (next.type !== "blocks") return next;
|
|
2895
|
-
const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
|
|
2896
|
-
const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
|
|
2897
|
-
if (hasInlineBlocks && hasReferences) {
|
|
2898
|
-
throw new Error(
|
|
2899
|
-
`Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
|
|
2900
|
-
);
|
|
2901
|
-
}
|
|
2902
|
-
if (hasReferences) {
|
|
2903
|
-
next.blocks = next.blockReferences.map((slug) => {
|
|
2904
|
-
const block = registry.get(slug);
|
|
2905
|
-
if (!block) {
|
|
2906
|
-
throw new Error(
|
|
2907
|
-
`Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
|
|
2908
|
-
);
|
|
2909
|
-
}
|
|
2910
|
-
return resolveBlock(block, registry, blockCache);
|
|
2911
|
-
});
|
|
2912
|
-
return next;
|
|
2913
|
-
}
|
|
2914
|
-
if (hasInlineBlocks) {
|
|
2915
|
-
next.blocks = next.blocks.map(
|
|
2916
|
-
(block) => resolveBlock(block, registry, blockCache)
|
|
2917
|
-
);
|
|
2918
|
-
}
|
|
2919
|
-
return next;
|
|
2920
|
-
}
|
|
2921
|
-
function resolveBlock(block, registry, blockCache) {
|
|
2922
|
-
const cached = blockCache.get(block);
|
|
2923
|
-
if (cached) return cached;
|
|
2924
|
-
const resolved = {
|
|
2925
|
-
...block,
|
|
2926
|
-
fields: []
|
|
2927
|
-
};
|
|
2928
|
-
blockCache.set(block, resolved);
|
|
2929
|
-
resolved.fields = block.fields.map(
|
|
2930
|
-
(field) => resolveField(field, registry, blockCache)
|
|
2931
|
-
);
|
|
2932
|
-
return resolved;
|
|
2933
|
-
}
|
|
2934
|
-
function resolveFields(fields, registry, blockCache) {
|
|
2935
|
-
return fields.map((field) => resolveField(field, registry, blockCache));
|
|
2936
|
-
}
|
|
2937
|
-
function normalizeSchemaFragment(fragment) {
|
|
2938
|
-
const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
|
|
2939
|
-
const registry = buildRegistry(reusableBlocks);
|
|
2940
|
-
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
2941
|
-
const resolvedBlocks = reusableBlocks.map(
|
|
2942
|
-
(block) => resolveBlock(block, registry, blockCache)
|
|
2943
|
-
);
|
|
2944
|
-
const resolvedCollections = (fragment.collections ?? []).map(
|
|
2945
|
-
(collection) => ({
|
|
2946
|
-
...collection,
|
|
2947
|
-
fields: resolveFields(collection.fields ?? [], registry, blockCache)
|
|
2948
|
-
})
|
|
2949
|
-
);
|
|
2950
|
-
const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
|
|
2951
|
-
...global,
|
|
2952
|
-
fields: resolveFields(global.fields ?? [], registry, blockCache)
|
|
2953
|
-
}));
|
|
2954
|
-
return {
|
|
2955
|
-
...fragment,
|
|
2956
|
-
...fragment.blocks ? { blocks: resolvedBlocks } : {},
|
|
2957
|
-
...fragment.collections ? { collections: resolvedCollections } : {},
|
|
2958
|
-
...fragment.globals ? { globals: resolvedGlobals } : {}
|
|
2959
|
-
};
|
|
2400
|
+
function evaluateJexlSync(expression, context = {}) {
|
|
2401
|
+
if (!expression || typeof expression !== "string") return void 0;
|
|
2402
|
+
try {
|
|
2403
|
+
const evalContext = {
|
|
2404
|
+
null: null,
|
|
2405
|
+
undefined: void 0,
|
|
2406
|
+
...context,
|
|
2407
|
+
math: Math,
|
|
2408
|
+
Math
|
|
2409
|
+
};
|
|
2410
|
+
return import_jexl.default.evalSync(expression, evalContext);
|
|
2411
|
+
} catch (_err) {
|
|
2412
|
+
return void 0;
|
|
2413
|
+
}
|
|
2960
2414
|
}
|
|
2415
|
+
var BUILTIN_JEXL_HELPERS = [
|
|
2416
|
+
// Strings
|
|
2417
|
+
"slugify",
|
|
2418
|
+
"lower",
|
|
2419
|
+
"upper",
|
|
2420
|
+
"trim",
|
|
2421
|
+
"capitalize",
|
|
2422
|
+
"truncate",
|
|
2423
|
+
"readingTime",
|
|
2424
|
+
"wordCount",
|
|
2425
|
+
"replace",
|
|
2426
|
+
"startsWith",
|
|
2427
|
+
"endsWith",
|
|
2428
|
+
// Dates
|
|
2429
|
+
"now",
|
|
2430
|
+
"today",
|
|
2431
|
+
"formatDate",
|
|
2432
|
+
"addDays",
|
|
2433
|
+
"diffDays",
|
|
2434
|
+
"isPast",
|
|
2435
|
+
"isFuture",
|
|
2436
|
+
// Arrays
|
|
2437
|
+
"includes",
|
|
2438
|
+
"join",
|
|
2439
|
+
"first",
|
|
2440
|
+
"last",
|
|
2441
|
+
"compact",
|
|
2442
|
+
"unique",
|
|
2443
|
+
"length",
|
|
2444
|
+
// Math
|
|
2445
|
+
"round",
|
|
2446
|
+
"clamp",
|
|
2447
|
+
// Objects & Logical
|
|
2448
|
+
"default",
|
|
2449
|
+
"coalesce",
|
|
2450
|
+
"isEmpty",
|
|
2451
|
+
"get"
|
|
2452
|
+
];
|
|
2961
2453
|
|
|
2962
2454
|
// src/utils/declarative-hooks.ts
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
if (
|
|
2981
|
-
|
|
2455
|
+
registerJexlHelpers(import_jexl2.default);
|
|
2456
|
+
var SURFACE_ROOTS = {
|
|
2457
|
+
"collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
|
|
2458
|
+
"collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
|
|
2459
|
+
"collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
|
|
2460
|
+
"global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
|
|
2461
|
+
"global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
|
|
2462
|
+
"global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
|
|
2463
|
+
"field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
|
|
2464
|
+
"field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
|
|
2465
|
+
};
|
|
2466
|
+
var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
|
|
2467
|
+
var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
|
|
2468
|
+
var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
|
|
2469
|
+
var COMPILE_CACHE = /* @__PURE__ */ new Map();
|
|
2470
|
+
function compileExpression(expression) {
|
|
2471
|
+
const cached = COMPILE_CACHE.get(expression);
|
|
2472
|
+
if (cached) return cached;
|
|
2473
|
+
const compiled = import_jexl2.default.compile(expression);
|
|
2474
|
+
const next = { ast: compiled._ast };
|
|
2475
|
+
COMPILE_CACHE.set(expression, next);
|
|
2476
|
+
return next;
|
|
2982
2477
|
}
|
|
2983
|
-
function
|
|
2984
|
-
|
|
2985
|
-
const str = String(val).trim();
|
|
2986
|
-
if (!str) return "";
|
|
2987
|
-
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
2478
|
+
function isPlainObject(value) {
|
|
2479
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2988
2480
|
}
|
|
2989
|
-
function
|
|
2990
|
-
|
|
2991
|
-
const str = String(val);
|
|
2992
|
-
const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
|
|
2993
|
-
if (str.length <= maxLen) return str;
|
|
2994
|
-
return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
|
|
2481
|
+
function quoteSegment(value) {
|
|
2482
|
+
return `["${value.replace(/"/g, '\\"')}"]`;
|
|
2995
2483
|
}
|
|
2996
|
-
function
|
|
2997
|
-
|
|
2998
|
-
const str = String(val).trim();
|
|
2999
|
-
if (!str) return 1;
|
|
3000
|
-
const words = str.split(/\s+/).filter(Boolean).length;
|
|
3001
|
-
return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
|
|
2484
|
+
function fieldSegment(field, index) {
|
|
2485
|
+
return typeof field.name === "string" && field.name.length > 0 ? `fields${quoteSegment(field.name)}` : `fields[${index}]`;
|
|
3002
2486
|
}
|
|
3003
|
-
function
|
|
3004
|
-
|
|
3005
|
-
const str = String(val).trim();
|
|
3006
|
-
if (!str) return 0;
|
|
3007
|
-
return str.split(/\s+/).filter(Boolean).length;
|
|
2487
|
+
function blockSegment(block, index) {
|
|
2488
|
+
return typeof block.slug === "string" && block.slug.length > 0 ? `blocks${quoteSegment(block.slug)}` : `blocks[${index}]`;
|
|
3008
2489
|
}
|
|
3009
|
-
function
|
|
3010
|
-
|
|
3011
|
-
return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
|
|
2490
|
+
function collectionSegment(collection, index) {
|
|
2491
|
+
return typeof collection.slug === "string" && collection.slug.length > 0 ? `collections${quoteSegment(collection.slug)}` : `collections[${index}]`;
|
|
3012
2492
|
}
|
|
3013
|
-
function
|
|
3014
|
-
|
|
3015
|
-
return String(str).startsWith(String(prefix ?? ""));
|
|
2493
|
+
function globalSegment(global, index) {
|
|
2494
|
+
return typeof global.slug === "string" && global.slug.length > 0 ? `globals${quoteSegment(global.slug)}` : `globals[${index}]`;
|
|
3016
2495
|
}
|
|
3017
|
-
function
|
|
3018
|
-
|
|
3019
|
-
return String(str).endsWith(String(suffix ?? ""));
|
|
2496
|
+
function colorize(value, code, enabled) {
|
|
2497
|
+
return enabled ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
3020
2498
|
}
|
|
3021
|
-
function
|
|
3022
|
-
|
|
2499
|
+
function collectRootIdentifiers(node, roots, seen) {
|
|
2500
|
+
if (!node || typeof node !== "object" || seen.has(node)) return;
|
|
2501
|
+
seen.add(node);
|
|
2502
|
+
const candidate = node;
|
|
2503
|
+
if (candidate.type === "Identifier") {
|
|
2504
|
+
if (candidate.from) {
|
|
2505
|
+
collectRootIdentifiers(candidate.from, roots, seen);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (typeof candidate.value === "string" && candidate.value !== "null") {
|
|
2509
|
+
roots.add(candidate.value);
|
|
2510
|
+
}
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
if (Array.isArray(node)) {
|
|
2514
|
+
for (const entry of node) collectRootIdentifiers(entry, roots, seen);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
for (const value of Object.values(node)) {
|
|
2518
|
+
if (Array.isArray(value)) {
|
|
2519
|
+
for (const entry of value) collectRootIdentifiers(entry, roots, seen);
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
if (value && typeof value === "object") {
|
|
2523
|
+
collectRootIdentifiers(value, roots, seen);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
3023
2526
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
2527
|
+
function getInvalidRoots(expression, allowedRoots) {
|
|
2528
|
+
const { ast } = compileExpression(expression);
|
|
2529
|
+
const roots = /* @__PURE__ */ new Set();
|
|
2530
|
+
collectRootIdentifiers(ast, roots, /* @__PURE__ */ new Set());
|
|
2531
|
+
const allowed = new Set(allowedRoots);
|
|
2532
|
+
return [...roots].filter((root) => !allowed.has(root));
|
|
3026
2533
|
}
|
|
3027
|
-
function
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
|
|
3049
|
-
const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
|
|
3050
|
-
if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
|
|
3051
|
-
const diffMs = Math.abs(dA.getTime() - dB.getTime());
|
|
3052
|
-
return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
3053
|
-
}
|
|
3054
|
-
function isPast(val) {
|
|
3055
|
-
if (!val) return false;
|
|
3056
|
-
const d = new Date(val);
|
|
3057
|
-
if (Number.isNaN(d.getTime())) return false;
|
|
3058
|
-
return d.getTime() < Date.now();
|
|
3059
|
-
}
|
|
3060
|
-
function isFuture(val) {
|
|
3061
|
-
if (!val) return false;
|
|
3062
|
-
const d = new Date(val);
|
|
3063
|
-
if (Number.isNaN(d.getTime())) return false;
|
|
3064
|
-
return d.getTime() > Date.now();
|
|
3065
|
-
}
|
|
3066
|
-
function includes(arrOrStr, item) {
|
|
3067
|
-
if (Array.isArray(arrOrStr)) {
|
|
3068
|
-
return arrOrStr.includes(item);
|
|
3069
|
-
}
|
|
3070
|
-
if (typeof arrOrStr === "string") {
|
|
3071
|
-
return arrOrStr.includes(String(item ?? ""));
|
|
2534
|
+
function validateDeclarativeHookExpression(expression, surface, path) {
|
|
2535
|
+
try {
|
|
2536
|
+
const invalidRoots = getInvalidRoots(expression, SURFACE_ROOTS[surface]);
|
|
2537
|
+
if (invalidRoots.length === 0) return [];
|
|
2538
|
+
return [
|
|
2539
|
+
{
|
|
2540
|
+
path,
|
|
2541
|
+
surface,
|
|
2542
|
+
expression,
|
|
2543
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${SURFACE_ROOTS[surface].map((root) => `"${root}"`).join(", ")}`
|
|
2544
|
+
}
|
|
2545
|
+
];
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
return [
|
|
2548
|
+
{
|
|
2549
|
+
path,
|
|
2550
|
+
surface,
|
|
2551
|
+
expression,
|
|
2552
|
+
message: `has invalid syntax: ${error.message}`
|
|
2553
|
+
}
|
|
2554
|
+
];
|
|
3072
2555
|
}
|
|
3073
|
-
return false;
|
|
3074
|
-
}
|
|
3075
|
-
function join(arr, separator = ", ") {
|
|
3076
|
-
if (!Array.isArray(arr)) return "";
|
|
3077
|
-
return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
|
|
3078
|
-
}
|
|
3079
|
-
function first(arr) {
|
|
3080
|
-
if (Array.isArray(arr)) return arr[0] ?? null;
|
|
3081
|
-
return null;
|
|
3082
|
-
}
|
|
3083
|
-
function last(arr) {
|
|
3084
|
-
if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
|
|
3085
|
-
return null;
|
|
3086
2556
|
}
|
|
3087
|
-
function
|
|
3088
|
-
|
|
3089
|
-
|
|
2557
|
+
function validateDeclarativeAccessExpression(expression, surface, path) {
|
|
2558
|
+
try {
|
|
2559
|
+
const invalidRoots = getInvalidRoots(expression, ACCESS_ROOTS);
|
|
2560
|
+
if (invalidRoots.length === 0) return [];
|
|
2561
|
+
return [
|
|
2562
|
+
{
|
|
2563
|
+
path,
|
|
2564
|
+
surface,
|
|
2565
|
+
expression,
|
|
2566
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${ACCESS_ROOTS.map((root) => `"${root}"`).join(", ")}`
|
|
2567
|
+
}
|
|
2568
|
+
];
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
return [
|
|
2571
|
+
{
|
|
2572
|
+
path,
|
|
2573
|
+
surface,
|
|
2574
|
+
expression,
|
|
2575
|
+
message: `has invalid syntax: ${error.message}`
|
|
2576
|
+
}
|
|
2577
|
+
];
|
|
2578
|
+
}
|
|
3090
2579
|
}
|
|
3091
|
-
function
|
|
3092
|
-
|
|
3093
|
-
|
|
2580
|
+
function validateAdminConditionExpression(expression, path, allowedRoots) {
|
|
2581
|
+
try {
|
|
2582
|
+
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
2583
|
+
if (invalidRoots.length === 0) return [];
|
|
2584
|
+
return [
|
|
2585
|
+
{
|
|
2586
|
+
path,
|
|
2587
|
+
surface: "field.admin.condition",
|
|
2588
|
+
expression,
|
|
2589
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
2590
|
+
}
|
|
2591
|
+
];
|
|
2592
|
+
} catch (error) {
|
|
2593
|
+
return [
|
|
2594
|
+
{
|
|
2595
|
+
path,
|
|
2596
|
+
surface: "field.admin.condition",
|
|
2597
|
+
expression,
|
|
2598
|
+
message: `has invalid syntax: ${error.message}`
|
|
2599
|
+
}
|
|
2600
|
+
];
|
|
2601
|
+
}
|
|
3094
2602
|
}
|
|
3095
|
-
function
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
2603
|
+
function validatePreviewUrlExpression(expression, path, allowedRoots) {
|
|
2604
|
+
try {
|
|
2605
|
+
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
2606
|
+
if (invalidRoots.length === 0) return [];
|
|
2607
|
+
return [
|
|
2608
|
+
{
|
|
2609
|
+
path,
|
|
2610
|
+
surface: "collection.admin.previewUrl",
|
|
2611
|
+
expression,
|
|
2612
|
+
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
2613
|
+
}
|
|
2614
|
+
];
|
|
2615
|
+
} catch (error) {
|
|
2616
|
+
return [
|
|
2617
|
+
{
|
|
2618
|
+
path,
|
|
2619
|
+
surface: "collection.admin.previewUrl",
|
|
2620
|
+
expression,
|
|
2621
|
+
message: `has invalid syntax: ${error.message}`
|
|
2622
|
+
}
|
|
2623
|
+
];
|
|
2624
|
+
}
|
|
3099
2625
|
}
|
|
3100
|
-
function
|
|
3101
|
-
const
|
|
3102
|
-
if (
|
|
3103
|
-
|
|
3104
|
-
const factor = Math.pow(10, dec);
|
|
3105
|
-
return Math.round(num * factor) / factor;
|
|
2626
|
+
function assertValidDeclarativeHookExpression(expression, surface, path) {
|
|
2627
|
+
const issues = validateDeclarativeHookExpression(expression, surface, path);
|
|
2628
|
+
if (issues.length === 0) return;
|
|
2629
|
+
throw new Error(`Invalid declarative hook at ${path}: ${issues[0].message}`);
|
|
3106
2630
|
}
|
|
3107
|
-
function
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
const max2 = Number(maxVal) || 0;
|
|
3111
|
-
return Math.min(Math.max(num, min2), max2);
|
|
2631
|
+
function assertValidDeclarativeHookResult(result, surface, path) {
|
|
2632
|
+
if (isPlainObject(result)) return;
|
|
2633
|
+
throw new Error(`Invalid declarative hook result at ${path}: ${surface} expressions must return an object`);
|
|
3112
2634
|
}
|
|
3113
|
-
function
|
|
3114
|
-
|
|
2635
|
+
function collectFieldIssues(fields, path, issues) {
|
|
2636
|
+
fields.forEach((field, index) => {
|
|
2637
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2638
|
+
field.hooks?.beforeChange?.forEach((hook, hookIndex) => {
|
|
2639
|
+
if (typeof hook !== "string") return;
|
|
2640
|
+
issues.push(
|
|
2641
|
+
...validateDeclarativeHookExpression(
|
|
2642
|
+
hook,
|
|
2643
|
+
"field.beforeChange",
|
|
2644
|
+
`${fieldPath}.hooks.beforeChange[${hookIndex}]`
|
|
2645
|
+
)
|
|
2646
|
+
);
|
|
2647
|
+
});
|
|
2648
|
+
field.hooks?.afterRead?.forEach((hook, hookIndex) => {
|
|
2649
|
+
if (typeof hook !== "string") return;
|
|
2650
|
+
issues.push({
|
|
2651
|
+
path: `${fieldPath}.hooks.afterRead[${hookIndex}]`,
|
|
2652
|
+
surface: "unsupported",
|
|
2653
|
+
expression: hook,
|
|
2654
|
+
message: "declarative strings are not supported on field hooks.afterRead"
|
|
2655
|
+
});
|
|
2656
|
+
});
|
|
2657
|
+
if (typeof field.admin?.hooks?.onChange === "string") {
|
|
2658
|
+
issues.push(
|
|
2659
|
+
...validateDeclarativeHookExpression(
|
|
2660
|
+
field.admin.hooks.onChange,
|
|
2661
|
+
"field.admin.onChange",
|
|
2662
|
+
`${fieldPath}.admin.hooks.onChange`
|
|
2663
|
+
)
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
if (typeof field.admin?.hooks?.options === "string") {
|
|
2667
|
+
issues.push({
|
|
2668
|
+
path: `${fieldPath}.admin.hooks.options`,
|
|
2669
|
+
surface: "unsupported",
|
|
2670
|
+
expression: field.admin.hooks.options,
|
|
2671
|
+
message: "declarative strings are not supported on field admin.hooks.options"
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
if (Array.isArray(field.fields)) {
|
|
2675
|
+
collectFieldIssues(field.fields, fieldPath, issues);
|
|
2676
|
+
}
|
|
2677
|
+
if (Array.isArray(field.blocks)) {
|
|
2678
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2679
|
+
if (!Array.isArray(block.fields)) return;
|
|
2680
|
+
collectFieldIssues(block.fields, `${fieldPath}.${blockSegment(block, blockIndex)}`, issues);
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
3115
2684
|
}
|
|
3116
|
-
function
|
|
3117
|
-
return
|
|
2685
|
+
function collectFieldNames(fields) {
|
|
2686
|
+
return fields.flatMap(
|
|
2687
|
+
(field) => typeof field.name === "string" && field.name.length > 0 ? [field.name] : []
|
|
2688
|
+
);
|
|
3118
2689
|
}
|
|
3119
|
-
function
|
|
3120
|
-
|
|
2690
|
+
function collectAdminConditionIssues(fields, path, rootFieldNames, issues) {
|
|
2691
|
+
const scopeFieldNames = collectFieldNames(fields);
|
|
2692
|
+
const allowedRoots = [.../* @__PURE__ */ new Set([...ADMIN_CONDITION_RESERVED_ROOTS, ...rootFieldNames, ...scopeFieldNames])];
|
|
2693
|
+
fields.forEach((field, index) => {
|
|
2694
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2695
|
+
if (typeof field.admin?.condition === "string") {
|
|
2696
|
+
issues.push(
|
|
2697
|
+
...validateAdminConditionExpression(
|
|
2698
|
+
field.admin.condition,
|
|
2699
|
+
`${fieldPath}.admin.condition`,
|
|
2700
|
+
allowedRoots
|
|
2701
|
+
)
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
if (Array.isArray(field.fields)) {
|
|
2705
|
+
collectAdminConditionIssues(field.fields, fieldPath, rootFieldNames, issues);
|
|
2706
|
+
}
|
|
2707
|
+
if (Array.isArray(field.blocks)) {
|
|
2708
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2709
|
+
if (!Array.isArray(block.fields)) return;
|
|
2710
|
+
collectAdminConditionIssues(
|
|
2711
|
+
block.fields,
|
|
2712
|
+
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
2713
|
+
rootFieldNames,
|
|
2714
|
+
issues
|
|
2715
|
+
);
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
3121
2719
|
}
|
|
3122
|
-
function
|
|
3123
|
-
|
|
3124
|
-
|
|
2720
|
+
function collectFieldAccessIssues(fields, path, issues) {
|
|
2721
|
+
fields.forEach((field, index) => {
|
|
2722
|
+
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
2723
|
+
if (typeof field.access?.read === "string") {
|
|
2724
|
+
issues.push(...validateDeclarativeAccessExpression(field.access.read, "field.access", `${fieldPath}.access.read`));
|
|
2725
|
+
}
|
|
2726
|
+
if (typeof field.access?.create === "string") {
|
|
2727
|
+
issues.push(
|
|
2728
|
+
...validateDeclarativeAccessExpression(field.access.create, "field.access", `${fieldPath}.access.create`)
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
if (typeof field.access?.update === "string") {
|
|
2732
|
+
issues.push(
|
|
2733
|
+
...validateDeclarativeAccessExpression(field.access.update, "field.access", `${fieldPath}.access.update`)
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
if (Array.isArray(field.fields)) {
|
|
2737
|
+
collectFieldAccessIssues(field.fields, fieldPath, issues);
|
|
2738
|
+
}
|
|
2739
|
+
if (Array.isArray(field.blocks)) {
|
|
2740
|
+
field.blocks.forEach((block, blockIndex) => {
|
|
2741
|
+
if (!Array.isArray(block.fields)) return;
|
|
2742
|
+
collectFieldAccessIssues(
|
|
2743
|
+
block.fields,
|
|
2744
|
+
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
2745
|
+
issues
|
|
2746
|
+
);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
3125
2750
|
}
|
|
3126
|
-
function
|
|
3127
|
-
|
|
3128
|
-
|
|
2751
|
+
function collectHookIssues(hooks, scope, path, issues) {
|
|
2752
|
+
if (!hooks) return;
|
|
2753
|
+
const supported = {
|
|
2754
|
+
beforeRead: `${scope}.beforeRead`,
|
|
2755
|
+
afterRead: `${scope}.afterRead`,
|
|
2756
|
+
beforeChange: `${scope}.beforeChange`
|
|
2757
|
+
};
|
|
2758
|
+
for (const [key, value] of Object.entries(hooks)) {
|
|
2759
|
+
if (!Array.isArray(value)) continue;
|
|
2760
|
+
value.forEach((hook, index) => {
|
|
2761
|
+
if (typeof hook !== "string") return;
|
|
2762
|
+
const surface = supported[key];
|
|
2763
|
+
if (!surface) {
|
|
2764
|
+
issues.push({
|
|
2765
|
+
path: `${path}.${key}[${index}]`,
|
|
2766
|
+
surface: "unsupported",
|
|
2767
|
+
expression: hook,
|
|
2768
|
+
message: `declarative strings are not supported on ${scope} hooks.${key}`
|
|
2769
|
+
});
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
issues.push(...validateDeclarativeHookExpression(hook, surface, `${path}.${key}[${index}]`));
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
3129
2775
|
}
|
|
3130
|
-
function
|
|
3131
|
-
|
|
2776
|
+
function collectDeclarativeHookValidationIssues(config) {
|
|
2777
|
+
const issues = [];
|
|
2778
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2779
|
+
if (!Array.isArray(block.fields)) return;
|
|
2780
|
+
collectFieldIssues(
|
|
2781
|
+
block.fields,
|
|
2782
|
+
blockSegment(block, blockIndex),
|
|
2783
|
+
issues
|
|
2784
|
+
);
|
|
2785
|
+
});
|
|
2786
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2787
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2788
|
+
collectHookIssues(
|
|
2789
|
+
collection.hooks,
|
|
2790
|
+
"collection",
|
|
2791
|
+
`${collectionPath}.hooks`,
|
|
2792
|
+
issues
|
|
2793
|
+
);
|
|
2794
|
+
collectFieldIssues(collection.fields, collectionPath, issues);
|
|
2795
|
+
});
|
|
2796
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2797
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2798
|
+
collectHookIssues(
|
|
2799
|
+
global.hooks,
|
|
2800
|
+
"global",
|
|
2801
|
+
`${globalPath}.hooks`,
|
|
2802
|
+
issues
|
|
2803
|
+
);
|
|
2804
|
+
collectFieldIssues(global.fields, globalPath, issues);
|
|
2805
|
+
});
|
|
2806
|
+
return issues;
|
|
3132
2807
|
}
|
|
3133
|
-
function
|
|
3134
|
-
|
|
2808
|
+
function collectDeclarativeAccessValidationIssues(config) {
|
|
2809
|
+
const issues = [];
|
|
2810
|
+
Object.entries(config.accessPolicies ?? {}).forEach(([name, value]) => {
|
|
2811
|
+
if (typeof value !== "string") return;
|
|
2812
|
+
issues.push(
|
|
2813
|
+
...validateDeclarativeAccessExpression(value, "config.accessPolicy", `accessPolicies.${name}`)
|
|
2814
|
+
);
|
|
2815
|
+
});
|
|
2816
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2817
|
+
if (!Array.isArray(block.fields)) return;
|
|
2818
|
+
collectFieldAccessIssues(
|
|
2819
|
+
block.fields,
|
|
2820
|
+
blockSegment(block, blockIndex),
|
|
2821
|
+
issues
|
|
2822
|
+
);
|
|
2823
|
+
});
|
|
2824
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2825
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2826
|
+
if (typeof collection.access?.read === "string") {
|
|
2827
|
+
issues.push(
|
|
2828
|
+
...validateDeclarativeAccessExpression(
|
|
2829
|
+
collection.access.read,
|
|
2830
|
+
"collection.access",
|
|
2831
|
+
`${collectionPath}.access.read`
|
|
2832
|
+
)
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
if (typeof collection.access?.create === "string") {
|
|
2836
|
+
issues.push(
|
|
2837
|
+
...validateDeclarativeAccessExpression(
|
|
2838
|
+
collection.access.create,
|
|
2839
|
+
"collection.access",
|
|
2840
|
+
`${collectionPath}.access.create`
|
|
2841
|
+
)
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
if (typeof collection.access?.update === "string") {
|
|
2845
|
+
issues.push(
|
|
2846
|
+
...validateDeclarativeAccessExpression(
|
|
2847
|
+
collection.access.update,
|
|
2848
|
+
"collection.access",
|
|
2849
|
+
`${collectionPath}.access.update`
|
|
2850
|
+
)
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
if (typeof collection.access?.delete === "string") {
|
|
2854
|
+
issues.push(
|
|
2855
|
+
...validateDeclarativeAccessExpression(
|
|
2856
|
+
collection.access.delete,
|
|
2857
|
+
"collection.access",
|
|
2858
|
+
`${collectionPath}.access.delete`
|
|
2859
|
+
)
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
if (typeof collection.access?.readAudit === "string") {
|
|
2863
|
+
issues.push(
|
|
2864
|
+
...validateDeclarativeAccessExpression(
|
|
2865
|
+
collection.access.readAudit,
|
|
2866
|
+
"collection.access",
|
|
2867
|
+
`${collectionPath}.access.readAudit`
|
|
2868
|
+
)
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
collectFieldAccessIssues(collection.fields, collectionPath, issues);
|
|
2872
|
+
});
|
|
2873
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2874
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2875
|
+
if (typeof global.access?.read === "string") {
|
|
2876
|
+
issues.push(
|
|
2877
|
+
...validateDeclarativeAccessExpression(global.access.read, "global.access", `${globalPath}.access.read`)
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
if (typeof global.access?.update === "string") {
|
|
2881
|
+
issues.push(
|
|
2882
|
+
...validateDeclarativeAccessExpression(
|
|
2883
|
+
global.access.update,
|
|
2884
|
+
"global.access",
|
|
2885
|
+
`${globalPath}.access.update`
|
|
2886
|
+
)
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
collectFieldAccessIssues(global.fields, globalPath, issues);
|
|
2890
|
+
});
|
|
2891
|
+
return issues;
|
|
3135
2892
|
}
|
|
3136
|
-
function
|
|
3137
|
-
|
|
3138
|
-
|
|
2893
|
+
function collectAdminConditionValidationIssues(config) {
|
|
2894
|
+
const issues = [];
|
|
2895
|
+
config.blocks?.forEach((block, blockIndex) => {
|
|
2896
|
+
if (!Array.isArray(block.fields)) return;
|
|
2897
|
+
collectAdminConditionIssues(
|
|
2898
|
+
block.fields,
|
|
2899
|
+
blockSegment(block, blockIndex),
|
|
2900
|
+
collectFieldNames(block.fields),
|
|
2901
|
+
issues
|
|
2902
|
+
);
|
|
2903
|
+
});
|
|
2904
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2905
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2906
|
+
const rootFieldNames = collectFieldNames(collection.fields);
|
|
2907
|
+
collectAdminConditionIssues(collection.fields, collectionPath, rootFieldNames, issues);
|
|
2908
|
+
});
|
|
2909
|
+
config.globals?.forEach((global, globalIndex) => {
|
|
2910
|
+
const globalPath = globalSegment(global, globalIndex);
|
|
2911
|
+
const rootFieldNames = collectFieldNames(global.fields);
|
|
2912
|
+
collectAdminConditionIssues(global.fields, globalPath, rootFieldNames, issues);
|
|
2913
|
+
});
|
|
2914
|
+
return issues;
|
|
3139
2915
|
}
|
|
3140
|
-
function
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
2916
|
+
function collectPreviewUrlValidationIssues(config) {
|
|
2917
|
+
const issues = [];
|
|
2918
|
+
config.collections.forEach((collection, collectionIndex) => {
|
|
2919
|
+
if (typeof collection.admin?.previewUrl !== "string") return;
|
|
2920
|
+
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
2921
|
+
const allowedRoots = [
|
|
2922
|
+
.../* @__PURE__ */ new Set([...PREVIEW_URL_RESERVED_ROOTS, ...collectFieldNames(collection.fields)])
|
|
2923
|
+
];
|
|
2924
|
+
issues.push(
|
|
2925
|
+
...validatePreviewUrlExpression(
|
|
2926
|
+
collection.admin.previewUrl,
|
|
2927
|
+
`${collectionPath}.admin.previewUrl`,
|
|
2928
|
+
allowedRoots
|
|
2929
|
+
)
|
|
2930
|
+
);
|
|
2931
|
+
});
|
|
2932
|
+
return issues;
|
|
3145
2933
|
}
|
|
3146
|
-
function
|
|
3147
|
-
|
|
3148
|
-
|
|
2934
|
+
function assertValidAdminConditionsInConfig(config, source = "config") {
|
|
2935
|
+
const diagnostics = collectAdminConditionValidationIssues(config).map((issue) => ({
|
|
2936
|
+
severity: "error",
|
|
2937
|
+
source: "adminCondition",
|
|
2938
|
+
path: issue.path,
|
|
2939
|
+
surface: issue.surface,
|
|
2940
|
+
message: issue.message,
|
|
2941
|
+
expression: issue.expression
|
|
2942
|
+
}));
|
|
2943
|
+
if (diagnostics.length === 0) return;
|
|
2944
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
3149
2945
|
}
|
|
3150
|
-
function
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
2946
|
+
function assertValidPreviewUrlsInConfig(config, source = "config") {
|
|
2947
|
+
const diagnostics = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
2948
|
+
severity: "error",
|
|
2949
|
+
source: "previewUrl",
|
|
2950
|
+
path: issue.path,
|
|
2951
|
+
surface: issue.surface,
|
|
2952
|
+
message: issue.message,
|
|
2953
|
+
expression: issue.expression
|
|
2954
|
+
}));
|
|
2955
|
+
if (diagnostics.length === 0) return;
|
|
2956
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
3155
2957
|
}
|
|
3156
|
-
function
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
2958
|
+
function collectConfigDiagnostics(config) {
|
|
2959
|
+
const hookIssues = collectDeclarativeHookValidationIssues(config).map((issue) => ({
|
|
2960
|
+
severity: "error",
|
|
2961
|
+
source: "declarativeHook",
|
|
2962
|
+
path: issue.path,
|
|
2963
|
+
surface: issue.surface,
|
|
2964
|
+
message: issue.message,
|
|
2965
|
+
expression: issue.expression
|
|
2966
|
+
}));
|
|
2967
|
+
const accessIssues = collectDeclarativeAccessValidationIssues(config).map((issue) => ({
|
|
2968
|
+
severity: "error",
|
|
2969
|
+
source: "declarativeAccess",
|
|
2970
|
+
path: issue.path,
|
|
2971
|
+
surface: issue.surface,
|
|
2972
|
+
message: issue.message,
|
|
2973
|
+
expression: issue.expression
|
|
2974
|
+
}));
|
|
2975
|
+
const adminConditionIssues = collectAdminConditionValidationIssues(config).map((issue) => ({
|
|
2976
|
+
severity: "error",
|
|
2977
|
+
source: "adminCondition",
|
|
2978
|
+
path: issue.path,
|
|
2979
|
+
surface: issue.surface,
|
|
2980
|
+
message: issue.message,
|
|
2981
|
+
expression: issue.expression
|
|
2982
|
+
}));
|
|
2983
|
+
const previewUrlIssues = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
2984
|
+
severity: "error",
|
|
2985
|
+
source: "previewUrl",
|
|
2986
|
+
path: issue.path,
|
|
2987
|
+
surface: issue.surface,
|
|
2988
|
+
message: issue.message,
|
|
2989
|
+
expression: issue.expression
|
|
2990
|
+
}));
|
|
2991
|
+
return [...hookIssues, ...accessIssues, ...adminConditionIssues, ...previewUrlIssues];
|
|
2992
|
+
}
|
|
2993
|
+
function formatConfigDiagnostics(source, diagnostics, options = {}) {
|
|
2994
|
+
const color = options.color ?? false;
|
|
2995
|
+
const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
|
|
2996
|
+
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)}`;
|
|
2997
|
+
const lines = diagnostics.flatMap((issue, index) => [
|
|
2998
|
+
`${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
|
|
2999
|
+
` ${issue.message}`,
|
|
3000
|
+
` ${colorize("surface:", 90, color)} ${issue.surface}`
|
|
3001
|
+
]);
|
|
3002
|
+
return `${header}
|
|
3003
|
+
${lines.join("\n")}
|
|
3004
|
+
|
|
3005
|
+
${docLink}`;
|
|
3006
|
+
}
|
|
3007
|
+
var ConfigValidationError = class extends Error {
|
|
3008
|
+
diagnostics;
|
|
3009
|
+
source;
|
|
3010
|
+
constructor(source, diagnostics) {
|
|
3011
|
+
super(formatConfigDiagnostics(source, diagnostics));
|
|
3012
|
+
this.name = "ConfigValidationError";
|
|
3013
|
+
this.source = source;
|
|
3014
|
+
this.diagnostics = diagnostics;
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
function isConfigValidationError(error) {
|
|
3018
|
+
return error instanceof ConfigValidationError;
|
|
3019
|
+
}
|
|
3020
|
+
function assertValidDeclarativeHooksInConfig(config, source = "config") {
|
|
3021
|
+
const diagnostics = collectDeclarativeHookValidationIssues(config).map((issue) => ({
|
|
3022
|
+
severity: "error",
|
|
3023
|
+
source: "declarativeHook",
|
|
3024
|
+
path: issue.path,
|
|
3025
|
+
surface: issue.surface,
|
|
3026
|
+
message: issue.message,
|
|
3027
|
+
expression: issue.expression
|
|
3028
|
+
}));
|
|
3029
|
+
if (diagnostics.length === 0) return;
|
|
3030
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
3031
|
+
}
|
|
3032
|
+
function assertValidDeclarativeAccessInConfig(config, source = "config") {
|
|
3033
|
+
const diagnostics = collectDeclarativeAccessValidationIssues(config).map((issue) => ({
|
|
3034
|
+
severity: "error",
|
|
3035
|
+
source: "declarativeAccess",
|
|
3036
|
+
path: issue.path,
|
|
3037
|
+
surface: issue.surface,
|
|
3038
|
+
message: issue.message,
|
|
3039
|
+
expression: issue.expression
|
|
3040
|
+
}));
|
|
3041
|
+
if (diagnostics.length === 0) return;
|
|
3042
|
+
throw new ConfigValidationError(source, diagnostics);
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// src/utils/hooks.ts
|
|
3046
|
+
async function runCollectionHooks(hooks, args, options = {}) {
|
|
3047
|
+
if (!hooks || hooks.length === 0) {
|
|
3048
|
+
return args.data ?? args.doc ?? args.query ?? void 0;
|
|
3049
|
+
}
|
|
3050
|
+
const payloadKey = args.data !== void 0 ? "data" : args.doc !== void 0 ? "doc" : "query" in args ? "query" : void 0;
|
|
3051
|
+
let currentPayload = payloadKey === void 0 ? void 0 : args[payloadKey];
|
|
3052
|
+
for (const hook of hooks) {
|
|
3053
|
+
try {
|
|
3054
|
+
const hookArgs = {
|
|
3055
|
+
...args,
|
|
3056
|
+
...payloadKey !== void 0 ? { [payloadKey]: currentPayload } : {}
|
|
3057
|
+
};
|
|
3058
|
+
const result = typeof hook === "string" ? await (() => {
|
|
3059
|
+
if (options.surface) {
|
|
3060
|
+
assertValidDeclarativeHookExpression(
|
|
3061
|
+
hook,
|
|
3062
|
+
options.surface,
|
|
3063
|
+
options.path ?? options.surface
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
return import_jexl3.default.eval(hook, hookArgs);
|
|
3067
|
+
})() : await hook(hookArgs);
|
|
3068
|
+
if (result !== void 0) {
|
|
3069
|
+
if (typeof hook === "string" && options.surface) {
|
|
3070
|
+
assertValidDeclarativeHookResult(
|
|
3071
|
+
result,
|
|
3072
|
+
options.surface,
|
|
3073
|
+
options.path ?? options.surface
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
3076
|
+
currentPayload = typeof hook === "string" && currentPayload && typeof currentPayload === "object" && !Array.isArray(currentPayload) && result && typeof result === "object" && !Array.isArray(result) ? {
|
|
3077
|
+
...currentPayload,
|
|
3078
|
+
...result
|
|
3079
|
+
} : result;
|
|
3080
|
+
}
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
if (options.isolated) {
|
|
3083
|
+
console.error("Side-effect hook failed after a successful write", err);
|
|
3084
|
+
} else {
|
|
3085
|
+
throw err;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
return currentPayload;
|
|
3090
|
+
}
|
|
3091
|
+
async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pathPrefix = "") {
|
|
3092
|
+
if (!data || typeof data !== "object") return data;
|
|
3093
|
+
const result = { ...data };
|
|
3094
|
+
for (const field of fields) {
|
|
3095
|
+
if (!field.name) continue;
|
|
3096
|
+
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
3097
|
+
const value = result[field.name];
|
|
3098
|
+
const origValue = originalDoc?.[field.name];
|
|
3099
|
+
let updatedValue = value;
|
|
3100
|
+
if (field.hooks?.beforeChange) {
|
|
3101
|
+
for (const hook of field.hooks.beforeChange) {
|
|
3102
|
+
const hookArgs = {
|
|
3103
|
+
value: updatedValue,
|
|
3104
|
+
originalDoc: originalDoc ?? void 0,
|
|
3105
|
+
data: result,
|
|
3106
|
+
user,
|
|
3107
|
+
db
|
|
3108
|
+
};
|
|
3109
|
+
updatedValue = typeof hook === "string" ? await (() => {
|
|
3110
|
+
assertValidDeclarativeHookExpression(
|
|
3111
|
+
hook,
|
|
3112
|
+
"field.beforeChange",
|
|
3113
|
+
fieldPath
|
|
3114
|
+
);
|
|
3115
|
+
return import_jexl3.default.eval(hook, hookArgs);
|
|
3116
|
+
})() : await hook(hookArgs);
|
|
3117
|
+
}
|
|
3118
|
+
result[field.name] = updatedValue;
|
|
3119
|
+
}
|
|
3120
|
+
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
3121
|
+
if (field.type === "object" && field.fields) {
|
|
3122
|
+
result[field.name] = await executeFieldBeforeChange(
|
|
3123
|
+
field.fields,
|
|
3124
|
+
updatedValue,
|
|
3125
|
+
origValue,
|
|
3126
|
+
user,
|
|
3127
|
+
db,
|
|
3128
|
+
fieldPath
|
|
3129
|
+
);
|
|
3130
|
+
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
3131
|
+
const arrayResult = [];
|
|
3132
|
+
for (let i = 0; i < updatedValue.length; i++) {
|
|
3133
|
+
const item = updatedValue[i];
|
|
3134
|
+
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
3135
|
+
arrayResult.push(
|
|
3136
|
+
await executeFieldBeforeChange(
|
|
3137
|
+
field.fields,
|
|
3138
|
+
item,
|
|
3139
|
+
origItem,
|
|
3140
|
+
user,
|
|
3141
|
+
db,
|
|
3142
|
+
fieldPath
|
|
3143
|
+
)
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
result[field.name] = arrayResult;
|
|
3147
|
+
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
3148
|
+
const blocksResult = [];
|
|
3149
|
+
for (let i = 0; i < updatedValue.length; i++) {
|
|
3150
|
+
const blockData = updatedValue[i];
|
|
3151
|
+
const origBlock = Array.isArray(origValue) ? origValue[i] : null;
|
|
3152
|
+
const blockConfig = field.blocks.find(
|
|
3153
|
+
(b) => b.slug === blockData.blockType
|
|
3154
|
+
);
|
|
3155
|
+
if (blockConfig) {
|
|
3156
|
+
blocksResult.push(
|
|
3157
|
+
await executeFieldBeforeChange(
|
|
3158
|
+
blockConfig.fields,
|
|
3159
|
+
blockData,
|
|
3160
|
+
origBlock,
|
|
3161
|
+
user,
|
|
3162
|
+
db,
|
|
3163
|
+
fieldPath
|
|
3164
|
+
)
|
|
3165
|
+
);
|
|
3166
|
+
} else {
|
|
3167
|
+
blocksResult.push(blockData);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
result[field.name] = blocksResult;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
return result;
|
|
3175
|
+
}
|
|
3176
|
+
async function executeFieldAfterRead(fields, doc, user, db) {
|
|
3177
|
+
if (!doc || typeof doc !== "object") return doc;
|
|
3178
|
+
const result = { ...doc };
|
|
3179
|
+
for (const field of fields) {
|
|
3180
|
+
if (!field.name) continue;
|
|
3181
|
+
const value = result[field.name];
|
|
3182
|
+
let updatedValue = value;
|
|
3183
|
+
if (field.hooks?.afterRead) {
|
|
3184
|
+
for (const hook of field.hooks.afterRead) {
|
|
3185
|
+
const typedHook = hook;
|
|
3186
|
+
const hookArgs = {
|
|
3187
|
+
value: updatedValue,
|
|
3188
|
+
doc: result,
|
|
3189
|
+
user,
|
|
3190
|
+
db
|
|
3191
|
+
};
|
|
3192
|
+
updatedValue = await typedHook(hookArgs);
|
|
3193
|
+
}
|
|
3194
|
+
result[field.name] = updatedValue;
|
|
3195
|
+
}
|
|
3196
|
+
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
3197
|
+
if (field.type === "object" && field.fields) {
|
|
3198
|
+
result[field.name] = await executeFieldAfterRead(
|
|
3199
|
+
field.fields,
|
|
3200
|
+
updatedValue,
|
|
3201
|
+
user,
|
|
3202
|
+
db
|
|
3203
|
+
);
|
|
3204
|
+
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
3205
|
+
const arrayResult = [];
|
|
3206
|
+
for (const item of updatedValue) {
|
|
3207
|
+
arrayResult.push(
|
|
3208
|
+
await executeFieldAfterRead(
|
|
3209
|
+
field.fields,
|
|
3210
|
+
item,
|
|
3211
|
+
user,
|
|
3212
|
+
db
|
|
3213
|
+
)
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
result[field.name] = arrayResult;
|
|
3217
|
+
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
3218
|
+
const blocksResult = [];
|
|
3219
|
+
for (const blockData of updatedValue) {
|
|
3220
|
+
const typedBlock = blockData;
|
|
3221
|
+
const blockConfig = field.blocks.find(
|
|
3222
|
+
(b) => b.slug === typedBlock.blockType
|
|
3223
|
+
);
|
|
3224
|
+
if (blockConfig) {
|
|
3225
|
+
blocksResult.push(
|
|
3226
|
+
await executeFieldAfterRead(
|
|
3227
|
+
blockConfig.fields,
|
|
3228
|
+
typedBlock,
|
|
3229
|
+
user,
|
|
3230
|
+
db
|
|
3231
|
+
)
|
|
3232
|
+
);
|
|
3233
|
+
} else {
|
|
3234
|
+
blocksResult.push(blockData);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
result[field.name] = blocksResult;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
return result;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/trash.ts
|
|
3245
|
+
var TRASH_COLLECTION = "__trash";
|
|
3246
|
+
var TRASH_COLLECTION_CONFIG = {
|
|
3247
|
+
slug: TRASH_COLLECTION,
|
|
3248
|
+
labels: { singular: "Trash entry", plural: "Trash entries" },
|
|
3249
|
+
fields: [
|
|
3250
|
+
{ name: "collection", type: "text", required: true, promoted: true },
|
|
3251
|
+
{ name: "docId", type: "text", required: true, promoted: true },
|
|
3252
|
+
{ name: "deletedAt", type: "number", required: true, promoted: true },
|
|
3253
|
+
// epoch ms
|
|
3254
|
+
{ name: "purgeAt", type: "number", promoted: true },
|
|
3255
|
+
// epoch ms, null = never
|
|
3256
|
+
{ name: "deletedBy", type: "text", promoted: true },
|
|
3257
|
+
{ name: "title", type: "text" },
|
|
3258
|
+
// resolved useAsTitle, for listing
|
|
3259
|
+
{ name: "snapshot", type: "json", required: true },
|
|
3260
|
+
// the raw stored document
|
|
3261
|
+
{ name: "createdAt", type: "date" }
|
|
3262
|
+
// original doc timestamps live in snapshot
|
|
3263
|
+
],
|
|
3264
|
+
indexes: [
|
|
3265
|
+
{ fields: ["purgeAt"] },
|
|
3266
|
+
{ fields: ["collection", "docId"] }
|
|
3267
|
+
],
|
|
3268
|
+
access: {
|
|
3269
|
+
read: () => false,
|
|
3270
|
+
create: () => false,
|
|
3271
|
+
update: () => false,
|
|
3272
|
+
delete: () => false
|
|
3273
|
+
},
|
|
3274
|
+
admin: { hidden: true }
|
|
3275
|
+
};
|
|
3276
|
+
function getTrashEntryId(collection, docId) {
|
|
3277
|
+
return `${collection}:${docId}`;
|
|
3278
|
+
}
|
|
3279
|
+
function resolveTrashConfig(collection, appConfig) {
|
|
3280
|
+
const colTrash = collection.trash;
|
|
3281
|
+
const appTrash = appConfig?.trash;
|
|
3282
|
+
let enabled = true;
|
|
3283
|
+
if (colTrash === false) {
|
|
3284
|
+
enabled = false;
|
|
3285
|
+
} else if (colTrash === true || typeof colTrash === "object" && colTrash !== null) {
|
|
3286
|
+
enabled = true;
|
|
3287
|
+
} else if (appTrash?.enabled === false) {
|
|
3288
|
+
enabled = false;
|
|
3289
|
+
}
|
|
3290
|
+
if (!enabled) {
|
|
3291
|
+
return {
|
|
3292
|
+
enabled: false,
|
|
3293
|
+
retentionDays: null,
|
|
3294
|
+
allowPermanentDelete: true
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
let retentionDays = 30;
|
|
3298
|
+
if (typeof colTrash === "object" && colTrash !== null && "retentionDays" in colTrash) {
|
|
3299
|
+
retentionDays = colTrash.retentionDays ?? null;
|
|
3300
|
+
} else if (appTrash?.retentionDays !== void 0) {
|
|
3301
|
+
retentionDays = appTrash.retentionDays ?? null;
|
|
3302
|
+
}
|
|
3303
|
+
const allowPermanentDelete = typeof colTrash === "object" && colTrash !== null && colTrash.allowPermanentDelete !== void 0 ? colTrash.allowPermanentDelete : appTrash?.allowPermanentDelete !== void 0 ? appTrash.allowPermanentDelete : true;
|
|
3304
|
+
return {
|
|
3305
|
+
enabled: true,
|
|
3306
|
+
retentionDays,
|
|
3307
|
+
allowPermanentDelete
|
|
3308
|
+
};
|
|
3161
3309
|
}
|
|
3162
|
-
function
|
|
3163
|
-
if (
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
for (const part of parts) {
|
|
3168
|
-
if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
|
|
3169
|
-
curr = curr[part];
|
|
3310
|
+
function logTrashWarning(config, payload) {
|
|
3311
|
+
if (config?.logger && typeof config.logger.warn === "function") {
|
|
3312
|
+
config.logger.warn(payload);
|
|
3313
|
+
} else if (typeof console !== "undefined" && console.warn) {
|
|
3314
|
+
console.warn(`[dyrected] ${payload.msg}`);
|
|
3170
3315
|
}
|
|
3171
|
-
return curr === void 0 || curr === null ? fallback : curr;
|
|
3172
3316
|
}
|
|
3173
|
-
function
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
// Dates
|
|
3188
|
-
now,
|
|
3189
|
-
today,
|
|
3190
|
-
formatDate,
|
|
3191
|
-
addDays,
|
|
3192
|
-
diffDays,
|
|
3193
|
-
isPast,
|
|
3194
|
-
isFuture,
|
|
3195
|
-
// Arrays
|
|
3196
|
-
includes,
|
|
3197
|
-
join,
|
|
3198
|
-
first,
|
|
3199
|
-
last,
|
|
3200
|
-
compact,
|
|
3201
|
-
unique,
|
|
3202
|
-
length,
|
|
3203
|
-
// Math
|
|
3204
|
-
round,
|
|
3205
|
-
clamp,
|
|
3206
|
-
ceil,
|
|
3207
|
-
floor,
|
|
3208
|
-
abs,
|
|
3209
|
-
min,
|
|
3210
|
-
max,
|
|
3211
|
-
pow,
|
|
3212
|
-
sqrt,
|
|
3213
|
-
sum,
|
|
3214
|
-
count,
|
|
3215
|
-
// Objects & Logical
|
|
3216
|
-
default: defaultValue,
|
|
3217
|
-
coalesce,
|
|
3218
|
-
isEmpty,
|
|
3219
|
-
get
|
|
3220
|
-
};
|
|
3221
|
-
for (const [name, fn] of Object.entries(helpers)) {
|
|
3222
|
-
try {
|
|
3223
|
-
jexlInstance.addFunction(name, fn);
|
|
3224
|
-
} catch {
|
|
3317
|
+
function assertValidTrashInConfig(config, source = "config") {
|
|
3318
|
+
if (config.trash) {
|
|
3319
|
+
const { retentionDays } = config.trash;
|
|
3320
|
+
if (retentionDays !== void 0 && retentionDays !== null) {
|
|
3321
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 1) {
|
|
3322
|
+
throw new Error(
|
|
3323
|
+
`Invalid trash retentionDays in ${source}: must be an integer >= 1. Received ${retentionDays}.`
|
|
3324
|
+
);
|
|
3325
|
+
}
|
|
3326
|
+
if (retentionDays > 3650) {
|
|
3327
|
+
logTrashWarning(config, {
|
|
3328
|
+
msg: `Trash retentionDays in ${source} is unusually large (${retentionDays} days > 10 years). Check if value was provided in hours or seconds.`
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3225
3331
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3332
|
+
}
|
|
3333
|
+
for (const col of config.collections || []) {
|
|
3334
|
+
if (col.slug === "trash") {
|
|
3335
|
+
throw new Error(`Collection slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3336
|
+
}
|
|
3337
|
+
const isSystem = col.slug.startsWith("__");
|
|
3338
|
+
if (isSystem && col.trash) {
|
|
3339
|
+
throw new Error(`System collection "${col.slug}" cannot enable trash.`);
|
|
3340
|
+
}
|
|
3341
|
+
if (col.trash === false) {
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
if (typeof col.trash === "object" && col.trash !== null) {
|
|
3345
|
+
const { retentionDays } = col.trash;
|
|
3346
|
+
if (retentionDays !== void 0 && retentionDays !== null) {
|
|
3347
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 1) {
|
|
3348
|
+
throw new Error(
|
|
3349
|
+
`Invalid trash retentionDays for collection "${col.slug}" in ${source}: must be an integer >= 1. Received ${retentionDays}. Use trash: false for immediate hard delete.`
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
if (retentionDays > 3650) {
|
|
3353
|
+
logTrashWarning(config, {
|
|
3354
|
+
msg: `Trash retentionDays for collection "${col.slug}" is unusually large (${retentionDays} days > 10 years). Check if value was provided in hours or seconds.`,
|
|
3355
|
+
collection: col.slug
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3229
3359
|
}
|
|
3230
3360
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
if (Array.isArray(right)) return right.includes(left);
|
|
3236
|
-
if (typeof right === "string") return right.includes(String(left ?? ""));
|
|
3237
|
-
return false;
|
|
3238
|
-
});
|
|
3239
|
-
} catch {
|
|
3361
|
+
for (const glob of config.globals || []) {
|
|
3362
|
+
if (glob.slug === "trash") {
|
|
3363
|
+
throw new Error(`Global slug "trash" is reserved by Dyrected for the trash system.`);
|
|
3364
|
+
}
|
|
3240
3365
|
}
|
|
3241
3366
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
return
|
|
3254
|
-
} catch (_err) {
|
|
3255
|
-
return void 0;
|
|
3367
|
+
function resolveDocumentTitle(collection, doc) {
|
|
3368
|
+
if (!doc || typeof doc !== "object") return "";
|
|
3369
|
+
const titleField = collection.admin?.useAsTitle || "title";
|
|
3370
|
+
const val = doc[titleField];
|
|
3371
|
+
if (val !== void 0 && val !== null && String(val).trim().length > 0) {
|
|
3372
|
+
return String(val);
|
|
3373
|
+
}
|
|
3374
|
+
if (doc.name !== void 0 && doc.name !== null && String(doc.name).trim().length > 0) {
|
|
3375
|
+
return String(doc.name);
|
|
3376
|
+
}
|
|
3377
|
+
if (doc.id !== void 0 && doc.id !== null) {
|
|
3378
|
+
return String(doc.id);
|
|
3256
3379
|
}
|
|
3380
|
+
return "";
|
|
3257
3381
|
}
|
|
3258
|
-
function
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3382
|
+
function createTrashPurgeTask(config) {
|
|
3383
|
+
return {
|
|
3384
|
+
name: "dyrected:trash-purge",
|
|
3385
|
+
cron: config.trash?.purge?.cron ?? "0 3 * * *",
|
|
3386
|
+
run: async ({ db, logger, signal }) => {
|
|
3387
|
+
const batchSize = Math.max(1, config.trash?.purge?.batchSize ?? 200);
|
|
3388
|
+
while (true) {
|
|
3389
|
+
if (signal?.aborted) return;
|
|
3390
|
+
const now2 = Date.now();
|
|
3391
|
+
const due = await db.find({
|
|
3392
|
+
collection: TRASH_COLLECTION,
|
|
3393
|
+
where: {
|
|
3394
|
+
purgeAt: { lte: now2, not_equals: null }
|
|
3395
|
+
},
|
|
3396
|
+
limit: batchSize,
|
|
3397
|
+
sort: "purgeAt"
|
|
3398
|
+
});
|
|
3399
|
+
if (!due.docs.length) break;
|
|
3400
|
+
for (const entry of due.docs) {
|
|
3401
|
+
if (signal?.aborted) return;
|
|
3402
|
+
if (entry.purgeAt == null || typeof entry.purgeAt !== "number") continue;
|
|
3403
|
+
try {
|
|
3404
|
+
const colSlug = entry.collection;
|
|
3405
|
+
const colConfig = config.collections.find((c) => c.slug === colSlug);
|
|
3406
|
+
const snapshot = typeof entry.snapshot === "string" ? JSON.parse(entry.snapshot) : entry.snapshot;
|
|
3407
|
+
if (colConfig?.upload && config.storage && snapshot?.filename) {
|
|
3408
|
+
try {
|
|
3409
|
+
await config.storage.delete({ filename: snapshot.filename });
|
|
3410
|
+
if (snapshot.sizes && typeof snapshot.sizes === "object") {
|
|
3411
|
+
for (const size of Object.values(snapshot.sizes)) {
|
|
3412
|
+
if (size?.filename) {
|
|
3413
|
+
await config.storage.delete({ filename: size.filename });
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
} catch (storageErr) {
|
|
3418
|
+
logger.warn({ err: storageErr?.message || storageErr, docId: entry.docId, collection: colSlug }, "Failed to delete storage file on purge");
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (colConfig?.hooks?.afterDelete) {
|
|
3422
|
+
await runCollectionHooks(
|
|
3423
|
+
colConfig.hooks.afterDelete,
|
|
3424
|
+
{
|
|
3425
|
+
id: entry.docId,
|
|
3426
|
+
doc: snapshot,
|
|
3427
|
+
user: void 0,
|
|
3428
|
+
req: {},
|
|
3429
|
+
db,
|
|
3430
|
+
mode: "permanent"
|
|
3431
|
+
},
|
|
3432
|
+
{ isolated: true }
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
if (colConfig?.audit) {
|
|
3436
|
+
try {
|
|
3437
|
+
await db.create({
|
|
3438
|
+
collection: "__audit",
|
|
3439
|
+
data: {
|
|
3440
|
+
collection: colSlug,
|
|
3441
|
+
documentId: entry.docId,
|
|
3442
|
+
operation: "purge",
|
|
3443
|
+
user: null,
|
|
3444
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3445
|
+
changes: JSON.stringify({
|
|
3446
|
+
before: snapshot,
|
|
3447
|
+
after: null
|
|
3448
|
+
})
|
|
3449
|
+
}
|
|
3450
|
+
});
|
|
3451
|
+
} catch (auditErr) {
|
|
3452
|
+
logger.warn({ err: auditErr?.message || auditErr, docId: entry.docId, collection: colSlug }, "Failed to write audit log on purge");
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
await db.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3456
|
+
} catch (entryErr) {
|
|
3457
|
+
logger.error({ err: entryErr?.message || entryErr, entryId: entry.id }, "Error purging trash entry");
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
if (due.docs.length < batchSize) break;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
}
|
|
3465
|
+
async function softDeleteDocument(options) {
|
|
3466
|
+
const { db, config, collection: collectionSlug, id, deletedBy } = options;
|
|
3467
|
+
const colConfig = config.collections.find((c) => c.slug === collectionSlug);
|
|
3468
|
+
if (!colConfig) throw new Error(`Collection ${collectionSlug} not found`);
|
|
3469
|
+
const doc = await db.findOne({ collection: collectionSlug, id });
|
|
3470
|
+
if (!doc) throw new Error(`Document ${id} in ${collectionSlug} not found`);
|
|
3471
|
+
const resolvedTrash = resolveTrashConfig(colConfig, config);
|
|
3472
|
+
const deletedAt = Date.now();
|
|
3473
|
+
const purgeAt = resolvedTrash.retentionDays !== null && resolvedTrash.retentionDays !== void 0 ? deletedAt + resolvedTrash.retentionDays * 864e5 : null;
|
|
3474
|
+
const title = resolveDocumentTitle(colConfig, doc);
|
|
3475
|
+
const trashId = getTrashEntryId(collectionSlug, id);
|
|
3476
|
+
const trashData = {
|
|
3477
|
+
id: trashId,
|
|
3478
|
+
collection: collectionSlug,
|
|
3479
|
+
docId: id,
|
|
3480
|
+
deletedAt,
|
|
3481
|
+
purgeAt,
|
|
3482
|
+
deletedBy: deletedBy ?? null,
|
|
3483
|
+
title,
|
|
3484
|
+
snapshot: doc,
|
|
3485
|
+
createdAt: doc.createdAt ?? new Date(deletedAt).toISOString()
|
|
3486
|
+
};
|
|
3487
|
+
if (db.transaction) {
|
|
3488
|
+
await db.transaction(async (tx) => {
|
|
3489
|
+
await tx.create({ collection: TRASH_COLLECTION, data: trashData });
|
|
3490
|
+
await tx.delete({ collection: collectionSlug, id });
|
|
3491
|
+
});
|
|
3492
|
+
} else {
|
|
3493
|
+
await db.create({ collection: TRASH_COLLECTION, data: trashData });
|
|
3494
|
+
await db.delete({ collection: collectionSlug, id });
|
|
3495
|
+
}
|
|
3496
|
+
return { id: trashId, purgeAt };
|
|
3497
|
+
}
|
|
3498
|
+
async function restoreDocument(options) {
|
|
3499
|
+
const { db, config, collection: collectionSlug, trashId: param, overrides } = options;
|
|
3500
|
+
const colConfig = config.collections.find((c) => c.slug === collectionSlug);
|
|
3501
|
+
if (!colConfig) throw new Error(`Collection ${collectionSlug} not found`);
|
|
3502
|
+
const trashId = param.includes(":") ? param : getTrashEntryId(collectionSlug, param);
|
|
3503
|
+
const entry = await db.findOne({ collection: TRASH_COLLECTION, id: trashId }) || await db.findOne({ collection: TRASH_COLLECTION, id: param });
|
|
3504
|
+
if (!entry || entry.collection !== collectionSlug) {
|
|
3505
|
+
throw new Error(`Trash entry ${param} not found for collection ${collectionSlug}`);
|
|
3506
|
+
}
|
|
3507
|
+
const snapshot = typeof entry.snapshot === "string" ? JSON.parse(entry.snapshot) : entry.snapshot;
|
|
3508
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
3509
|
+
const docToRestore = {
|
|
3510
|
+
...snapshot,
|
|
3511
|
+
...overrides || {},
|
|
3512
|
+
id: entry.docId,
|
|
3513
|
+
createdAt: snapshot.createdAt ?? entry.createdAt ?? now2,
|
|
3514
|
+
updatedAt: now2
|
|
3515
|
+
};
|
|
3516
|
+
if (db.transaction) {
|
|
3517
|
+
await db.transaction(async (tx) => {
|
|
3518
|
+
await tx.create({ collection: collectionSlug, data: docToRestore });
|
|
3519
|
+
await tx.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3520
|
+
});
|
|
3521
|
+
} else {
|
|
3522
|
+
await db.create({ collection: collectionSlug, data: docToRestore });
|
|
3523
|
+
await db.delete({ collection: TRASH_COLLECTION, id: entry.id });
|
|
3271
3524
|
}
|
|
3525
|
+
return { restoredDoc: docToRestore };
|
|
3272
3526
|
}
|
|
3273
|
-
var BUILTIN_JEXL_HELPERS = [
|
|
3274
|
-
// Strings
|
|
3275
|
-
"slugify",
|
|
3276
|
-
"lower",
|
|
3277
|
-
"upper",
|
|
3278
|
-
"trim",
|
|
3279
|
-
"capitalize",
|
|
3280
|
-
"truncate",
|
|
3281
|
-
"readingTime",
|
|
3282
|
-
"wordCount",
|
|
3283
|
-
"replace",
|
|
3284
|
-
"startsWith",
|
|
3285
|
-
"endsWith",
|
|
3286
|
-
// Dates
|
|
3287
|
-
"now",
|
|
3288
|
-
"today",
|
|
3289
|
-
"formatDate",
|
|
3290
|
-
"addDays",
|
|
3291
|
-
"diffDays",
|
|
3292
|
-
"isPast",
|
|
3293
|
-
"isFuture",
|
|
3294
|
-
// Arrays
|
|
3295
|
-
"includes",
|
|
3296
|
-
"join",
|
|
3297
|
-
"first",
|
|
3298
|
-
"last",
|
|
3299
|
-
"compact",
|
|
3300
|
-
"unique",
|
|
3301
|
-
"length",
|
|
3302
|
-
// Math
|
|
3303
|
-
"round",
|
|
3304
|
-
"clamp",
|
|
3305
|
-
// Objects & Logical
|
|
3306
|
-
"default",
|
|
3307
|
-
"coalesce",
|
|
3308
|
-
"isEmpty",
|
|
3309
|
-
"get"
|
|
3310
|
-
];
|
|
3311
3527
|
|
|
3312
|
-
// src/
|
|
3313
|
-
|
|
3314
|
-
var
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3528
|
+
// src/tasks.ts
|
|
3529
|
+
var TASK_LOCKS_COLLECTION = "__task_locks";
|
|
3530
|
+
var TASK_LOCKS_COLLECTION_CONFIG = {
|
|
3531
|
+
slug: TASK_LOCKS_COLLECTION,
|
|
3532
|
+
labels: { singular: "Task lock", plural: "Task locks" },
|
|
3533
|
+
fields: [
|
|
3534
|
+
{ name: "name", type: "text", required: true },
|
|
3535
|
+
// Numeric and promoted so the lock compare-and-set is a real column comparison.
|
|
3536
|
+
{ name: "lockedUntil", type: "number", required: true, promoted: true },
|
|
3537
|
+
{ name: "lockedBy", type: "text" },
|
|
3538
|
+
{ name: "lastScheduledFor", type: "date" },
|
|
3539
|
+
{ name: "lastStartedAt", type: "date" },
|
|
3540
|
+
{ name: "lastFinishedAt", type: "date" },
|
|
3541
|
+
{ name: "lastStatus", type: "text" },
|
|
3542
|
+
{ name: "lastError", type: "textarea" }
|
|
3543
|
+
],
|
|
3544
|
+
access: {
|
|
3545
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
3546
|
+
create: () => false,
|
|
3547
|
+
update: () => false,
|
|
3548
|
+
delete: () => false
|
|
3549
|
+
},
|
|
3550
|
+
admin: { hidden: true }
|
|
3323
3551
|
};
|
|
3324
|
-
var
|
|
3325
|
-
var
|
|
3326
|
-
var
|
|
3327
|
-
var
|
|
3328
|
-
function
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3552
|
+
var DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
|
|
3553
|
+
var DEFAULT_TICK_MS = 3e4;
|
|
3554
|
+
var MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
3555
|
+
var DAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
3556
|
+
function parseCronField(raw, min2, max2, names, nameOffset, label) {
|
|
3557
|
+
const values = /* @__PURE__ */ new Set();
|
|
3558
|
+
const toNumber = (token) => {
|
|
3559
|
+
const named = names?.indexOf(token.toLowerCase());
|
|
3560
|
+
if (named !== void 0 && named >= 0) return named + nameOffset;
|
|
3561
|
+
if (!/^\d+$/.test(token)) throw new Error(`Invalid cron ${label} "${token}".`);
|
|
3562
|
+
return Number(token);
|
|
3563
|
+
};
|
|
3564
|
+
for (const part of raw.split(",")) {
|
|
3565
|
+
const [rangePart, stepPart] = part.split("/");
|
|
3566
|
+
const step = stepPart === void 0 ? 1 : Number(stepPart);
|
|
3567
|
+
if (!Number.isInteger(step) || step < 1) throw new Error(`Invalid cron ${label} step "${part}".`);
|
|
3568
|
+
let start;
|
|
3569
|
+
let end;
|
|
3570
|
+
if (rangePart === "*") {
|
|
3571
|
+
start = min2;
|
|
3572
|
+
end = max2;
|
|
3573
|
+
} else if (rangePart.includes("-")) {
|
|
3574
|
+
const [a, b] = rangePart.split("-");
|
|
3575
|
+
start = toNumber(a);
|
|
3576
|
+
end = toNumber(b);
|
|
3577
|
+
} else {
|
|
3578
|
+
start = toNumber(rangePart);
|
|
3579
|
+
end = stepPart === void 0 ? start : max2;
|
|
3580
|
+
}
|
|
3581
|
+
if (start < min2 || end > max2 || start > end) {
|
|
3582
|
+
throw new Error(`Cron ${label} "${part}" is out of range (${min2}-${max2}).`);
|
|
3583
|
+
}
|
|
3584
|
+
for (let v = start; v <= end; v += step) values.add(v);
|
|
3585
|
+
}
|
|
3586
|
+
return values;
|
|
3338
3587
|
}
|
|
3339
|
-
function
|
|
3340
|
-
|
|
3588
|
+
function parseCron(expression) {
|
|
3589
|
+
const fields = expression.trim().split(/\s+/);
|
|
3590
|
+
if (fields.length !== 5) {
|
|
3591
|
+
throw new Error(`Cron expression "${expression}" must have 5 fields (minute hour day-of-month month day-of-week).`);
|
|
3592
|
+
}
|
|
3593
|
+
const [min2, hour, dom, mon, dow] = fields;
|
|
3594
|
+
const daysOfWeek = parseCronField(dow, 0, 7, DAY_NAMES, 0, "day-of-week");
|
|
3595
|
+
if (daysOfWeek.has(7)) daysOfWeek.add(0);
|
|
3596
|
+
daysOfWeek.delete(7);
|
|
3597
|
+
return {
|
|
3598
|
+
minutes: parseCronField(min2, 0, 59, void 0, 0, "minute"),
|
|
3599
|
+
hours: parseCronField(hour, 0, 23, void 0, 0, "hour"),
|
|
3600
|
+
daysOfMonth: parseCronField(dom, 1, 31, void 0, 0, "day-of-month"),
|
|
3601
|
+
months: parseCronField(mon, 1, 12, MONTH_NAMES, 1, "month"),
|
|
3602
|
+
daysOfWeek,
|
|
3603
|
+
dayFieldsBothRestricted: dom !== "*" && dow !== "*"
|
|
3604
|
+
};
|
|
3341
3605
|
}
|
|
3342
|
-
function
|
|
3343
|
-
|
|
3606
|
+
function dayMatches(schedule, date) {
|
|
3607
|
+
const domOk = schedule.daysOfMonth.has(date.getUTCDate());
|
|
3608
|
+
const dowOk = schedule.daysOfWeek.has(date.getUTCDay());
|
|
3609
|
+
return schedule.dayFieldsBothRestricted ? domOk || dowOk : domOk && dowOk;
|
|
3344
3610
|
}
|
|
3345
|
-
function
|
|
3346
|
-
|
|
3611
|
+
function nextCronRun(expression, after) {
|
|
3612
|
+
const schedule = typeof expression === "string" ? parseCron(expression) : expression;
|
|
3613
|
+
const cursor = new Date(after.getTime());
|
|
3614
|
+
cursor.setUTCSeconds(0, 0);
|
|
3615
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
|
|
3616
|
+
const limit = after.getTime() + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
3617
|
+
while (cursor.getTime() <= limit) {
|
|
3618
|
+
if (!schedule.months.has(cursor.getUTCMonth() + 1)) {
|
|
3619
|
+
cursor.setUTCMonth(cursor.getUTCMonth() + 1, 1);
|
|
3620
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
if (!dayMatches(schedule, cursor)) {
|
|
3624
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
3625
|
+
cursor.setUTCHours(0, 0, 0, 0);
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
if (!schedule.hours.has(cursor.getUTCHours())) {
|
|
3629
|
+
cursor.setUTCHours(cursor.getUTCHours() + 1, 0, 0, 0);
|
|
3630
|
+
continue;
|
|
3631
|
+
}
|
|
3632
|
+
if (!schedule.minutes.has(cursor.getUTCMinutes())) {
|
|
3633
|
+
cursor.setUTCMinutes(cursor.getUTCMinutes() + 1, 0, 0);
|
|
3634
|
+
continue;
|
|
3635
|
+
}
|
|
3636
|
+
return cursor;
|
|
3637
|
+
}
|
|
3638
|
+
throw new Error(`Cron expression "${typeof expression === "string" ? expression : "schedule"}" never matches.`);
|
|
3347
3639
|
}
|
|
3348
|
-
function
|
|
3349
|
-
|
|
3640
|
+
function defineTask(task) {
|
|
3641
|
+
if (!task.name || !task.name.trim()) throw new Error("A task needs a non-empty name.");
|
|
3642
|
+
if (task.cron !== void 0) parseCron(task.cron);
|
|
3643
|
+
if (task.lockTimeout !== void 0 && !(task.lockTimeout > 0)) {
|
|
3644
|
+
throw new Error(`Task "${task.name}" lockTimeout must be a positive number of seconds.`);
|
|
3645
|
+
}
|
|
3646
|
+
return task;
|
|
3350
3647
|
}
|
|
3351
|
-
function
|
|
3352
|
-
|
|
3648
|
+
function resolveLogger(config, taskName) {
|
|
3649
|
+
const base = config.logger;
|
|
3650
|
+
if (base && typeof base.child === "function") return base.child({ component: "tasks", task: taskName });
|
|
3651
|
+
const prefix = `[dyrected/tasks] ${taskName}`;
|
|
3652
|
+
return {
|
|
3653
|
+
info: (obj, msg) => console.info(prefix, msg ?? "", obj),
|
|
3654
|
+
warn: (obj, msg) => console.warn(prefix, msg ?? "", obj),
|
|
3655
|
+
error: (obj, msg) => console.error(prefix, msg ?? "", obj)
|
|
3656
|
+
};
|
|
3353
3657
|
}
|
|
3354
|
-
function
|
|
3355
|
-
|
|
3658
|
+
function floorToMinute(date) {
|
|
3659
|
+
const d = new Date(date.getTime());
|
|
3660
|
+
d.setUTCSeconds(0, 0);
|
|
3661
|
+
return d;
|
|
3356
3662
|
}
|
|
3357
|
-
function
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
const
|
|
3361
|
-
|
|
3362
|
-
if (
|
|
3363
|
-
|
|
3364
|
-
|
|
3663
|
+
function createTaskRunner(config) {
|
|
3664
|
+
const db = config.db;
|
|
3665
|
+
if (!db) throw new Error("createTaskRunner requires config.db.");
|
|
3666
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
3667
|
+
for (const task of config.tasks ?? []) {
|
|
3668
|
+
if (tasks.has(task.name)) throw new Error(`Duplicate task name "${task.name}".`);
|
|
3669
|
+
tasks.set(task.name, { ...task, schedule: task.cron ? parseCron(task.cron) : void 0 });
|
|
3670
|
+
}
|
|
3671
|
+
const hasRetention = (config.collections ?? []).some((col) => {
|
|
3672
|
+
const resolved = resolveTrashConfig(col, config);
|
|
3673
|
+
return resolved.enabled && resolved.retentionDays !== null && resolved.retentionDays > 0;
|
|
3674
|
+
});
|
|
3675
|
+
if (hasRetention && !tasks.has("dyrected:trash-purge")) {
|
|
3676
|
+
const purgeTask = createTrashPurgeTask(config);
|
|
3677
|
+
tasks.set(purgeTask.name, { ...purgeTask, schedule: purgeTask.cron ? parseCron(purgeTask.cron) : void 0 });
|
|
3678
|
+
}
|
|
3679
|
+
const instanceId = globalThis.crypto?.randomUUID?.() ?? `inst_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3680
|
+
let timer;
|
|
3681
|
+
let inFlight = Promise.resolve();
|
|
3682
|
+
let ticking = false;
|
|
3683
|
+
let ready;
|
|
3684
|
+
function ensureReady() {
|
|
3685
|
+
ready ??= Promise.resolve(db.sync?.([TASK_LOCKS_COLLECTION_CONFIG], [])).then(() => void 0);
|
|
3686
|
+
ready.catch(() => {
|
|
3687
|
+
ready = void 0;
|
|
3688
|
+
});
|
|
3689
|
+
return ready;
|
|
3690
|
+
}
|
|
3691
|
+
async function ensureLockRow(name, now2) {
|
|
3692
|
+
await ensureReady();
|
|
3693
|
+
const existing = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
3694
|
+
if (existing) return existing;
|
|
3695
|
+
try {
|
|
3696
|
+
return await db.create({
|
|
3697
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3698
|
+
data: {
|
|
3699
|
+
id: name,
|
|
3700
|
+
name,
|
|
3701
|
+
lockedUntil: 0,
|
|
3702
|
+
lockedBy: null,
|
|
3703
|
+
lastScheduledFor: floorToMinute(now2).toISOString(),
|
|
3704
|
+
lastStartedAt: null,
|
|
3705
|
+
lastFinishedAt: null,
|
|
3706
|
+
lastStatus: null,
|
|
3707
|
+
lastError: null
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
} catch (error) {
|
|
3711
|
+
const created = await db.findOne({ collection: TASK_LOCKS_COLLECTION, id: name });
|
|
3712
|
+
if (created) return created;
|
|
3713
|
+
throw error;
|
|
3365
3714
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3715
|
+
}
|
|
3716
|
+
async function execute(task, row, scheduledFor) {
|
|
3717
|
+
const started = Date.now();
|
|
3718
|
+
const lockMs = (task.lockTimeout ?? DEFAULT_LOCK_TIMEOUT_SECONDS) * 1e3;
|
|
3719
|
+
const token = `${instanceId}:${started}`;
|
|
3720
|
+
const where = {
|
|
3721
|
+
id: { equals: task.name },
|
|
3722
|
+
lockedUntil: { less_than: started }
|
|
3723
|
+
};
|
|
3724
|
+
if (scheduledFor) where.lastScheduledFor = { equals: row.lastScheduledFor };
|
|
3725
|
+
const claimed = await db.update({
|
|
3726
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3727
|
+
where,
|
|
3728
|
+
data: {
|
|
3729
|
+
lockedUntil: started + lockMs,
|
|
3730
|
+
lockedBy: token,
|
|
3731
|
+
lastStartedAt: new Date(started).toISOString(),
|
|
3732
|
+
...scheduledFor ? { lastScheduledFor: scheduledFor.toISOString() } : {}
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
if (claimed.affectedRows !== 1) {
|
|
3736
|
+
return { name: task.name, status: "skipped", reason: "locked", durationMs: Date.now() - started };
|
|
3368
3737
|
}
|
|
3369
|
-
|
|
3738
|
+
const logger = resolveLogger(config, task.name);
|
|
3739
|
+
const controller = new AbortController();
|
|
3740
|
+
let timeout;
|
|
3741
|
+
let status = "completed";
|
|
3742
|
+
let errorMessage;
|
|
3743
|
+
try {
|
|
3744
|
+
await Promise.race([
|
|
3745
|
+
Promise.resolve(task.run({ db, logger, signal: controller.signal, scheduledFor })),
|
|
3746
|
+
new Promise((_, reject) => {
|
|
3747
|
+
timeout = setTimeout(() => {
|
|
3748
|
+
reject(new Error(`Task exceeded its lockTimeout of ${lockMs / 1e3}s.`));
|
|
3749
|
+
controller.abort();
|
|
3750
|
+
}, lockMs);
|
|
3751
|
+
})
|
|
3752
|
+
]);
|
|
3753
|
+
} catch (error) {
|
|
3754
|
+
status = "failed";
|
|
3755
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3756
|
+
logger.error({ err: error }, "Task failed");
|
|
3757
|
+
} finally {
|
|
3758
|
+
if (timeout) clearTimeout(timeout);
|
|
3759
|
+
}
|
|
3760
|
+
try {
|
|
3761
|
+
await db.update({
|
|
3762
|
+
collection: TASK_LOCKS_COLLECTION,
|
|
3763
|
+
where: { id: { equals: task.name }, lockedBy: { equals: token } },
|
|
3764
|
+
data: {
|
|
3765
|
+
lockedUntil: 0,
|
|
3766
|
+
lockedBy: null,
|
|
3767
|
+
lastFinishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3768
|
+
lastStatus: status,
|
|
3769
|
+
lastError: errorMessage ?? null
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
} catch (error) {
|
|
3773
|
+
logger.warn({ err: error }, "Could not release task lock; it will expire on its own");
|
|
3774
|
+
}
|
|
3775
|
+
return { name: task.name, status, error: errorMessage, durationMs: Date.now() - started };
|
|
3370
3776
|
}
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
return;
|
|
3777
|
+
function track(work) {
|
|
3778
|
+
inFlight = inFlight.then(() => work).catch(() => void 0);
|
|
3779
|
+
return work;
|
|
3374
3780
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3781
|
+
const runner = {
|
|
3782
|
+
async runTask(name) {
|
|
3783
|
+
const task = tasks.get(name);
|
|
3784
|
+
if (!task) throw new Error(`Unknown task "${name}".`);
|
|
3785
|
+
const row = await ensureLockRow(name, /* @__PURE__ */ new Date());
|
|
3786
|
+
return track(execute(task, row, void 0));
|
|
3787
|
+
},
|
|
3788
|
+
async runDue(now2 = /* @__PURE__ */ new Date()) {
|
|
3789
|
+
const results = [];
|
|
3790
|
+
for (const task of tasks.values()) {
|
|
3791
|
+
if (!task.schedule) continue;
|
|
3792
|
+
const row = await ensureLockRow(task.name, now2);
|
|
3793
|
+
const reference = new Date(String(row.lastScheduledFor));
|
|
3794
|
+
const due = Number.isNaN(reference.getTime()) ? true : nextCronRun(task.schedule, reference).getTime() <= now2.getTime();
|
|
3795
|
+
if (!due) {
|
|
3796
|
+
results.push({ name: task.name, status: "skipped", reason: "not-due", durationMs: 0 });
|
|
3797
|
+
continue;
|
|
3798
|
+
}
|
|
3799
|
+
results.push(await track(execute(task, row, floorToMinute(now2))));
|
|
3800
|
+
}
|
|
3801
|
+
return results;
|
|
3802
|
+
},
|
|
3803
|
+
start(options) {
|
|
3804
|
+
if (timer) return;
|
|
3805
|
+
const intervalMs = options?.intervalMs ?? DEFAULT_TICK_MS;
|
|
3806
|
+
const tick = () => {
|
|
3807
|
+
if (ticking) return;
|
|
3808
|
+
ticking = true;
|
|
3809
|
+
runner.runDue().catch((error) => resolveLogger(config, "runner").error({ err: error }, "Task tick failed")).finally(() => {
|
|
3810
|
+
ticking = false;
|
|
3811
|
+
});
|
|
3812
|
+
};
|
|
3813
|
+
timer = setInterval(tick, intervalMs);
|
|
3814
|
+
timer.unref?.();
|
|
3815
|
+
setTimeout(tick, 0);
|
|
3816
|
+
},
|
|
3817
|
+
async stop() {
|
|
3818
|
+
if (timer) clearInterval(timer);
|
|
3819
|
+
timer = void 0;
|
|
3820
|
+
await inFlight;
|
|
3821
|
+
},
|
|
3822
|
+
getTasks() {
|
|
3823
|
+
return Array.from(tasks.values()).map((t) => ({ name: t.name, cron: t.cron }));
|
|
3382
3824
|
}
|
|
3383
|
-
}
|
|
3384
|
-
|
|
3385
|
-
function getInvalidRoots(expression, allowedRoots) {
|
|
3386
|
-
const { ast } = compileExpression(expression);
|
|
3387
|
-
const roots = /* @__PURE__ */ new Set();
|
|
3388
|
-
collectRootIdentifiers(ast, roots, /* @__PURE__ */ new Set());
|
|
3389
|
-
const allowed = new Set(allowedRoots);
|
|
3390
|
-
return [...roots].filter((root) => !allowed.has(root));
|
|
3825
|
+
};
|
|
3826
|
+
return runner;
|
|
3391
3827
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3828
|
+
|
|
3829
|
+
// src/workflows.ts
|
|
3830
|
+
var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
|
|
3831
|
+
var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
|
|
3832
|
+
var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
|
|
3833
|
+
var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
|
|
3834
|
+
function definePublishingWorkflow(options = {}) {
|
|
3835
|
+
const editors = options.editors ?? ["editor"];
|
|
3836
|
+
const publishers = options.publishers ?? ["publisher", "admin"];
|
|
3837
|
+
return {
|
|
3838
|
+
initialState: "draft",
|
|
3839
|
+
draftState: "draft",
|
|
3840
|
+
states: [
|
|
3841
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
3842
|
+
{ name: "in_review", label: "In review", color: "warning" },
|
|
3843
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
3844
|
+
],
|
|
3845
|
+
transitions: [
|
|
3397
3846
|
{
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
} catch (error) {
|
|
3405
|
-
return [
|
|
3847
|
+
name: "submit",
|
|
3848
|
+
label: "Submit for review",
|
|
3849
|
+
from: "draft",
|
|
3850
|
+
to: "in_review",
|
|
3851
|
+
requiredCapabilities: ["entry.submit"]
|
|
3852
|
+
},
|
|
3406
3853
|
{
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
function validateDeclarativeAccessExpression(expression, surface, path) {
|
|
3416
|
-
try {
|
|
3417
|
-
const invalidRoots = getInvalidRoots(expression, ACCESS_ROOTS);
|
|
3418
|
-
if (invalidRoots.length === 0) return [];
|
|
3419
|
-
return [
|
|
3854
|
+
name: "publish",
|
|
3855
|
+
label: "Publish",
|
|
3856
|
+
from: "in_review",
|
|
3857
|
+
to: "published",
|
|
3858
|
+
requiredCapabilities: ["entry.publish"]
|
|
3859
|
+
},
|
|
3420
3860
|
{
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
return [
|
|
3861
|
+
name: "reject",
|
|
3862
|
+
label: "Request changes",
|
|
3863
|
+
from: "in_review",
|
|
3864
|
+
to: "draft",
|
|
3865
|
+
requiredCapabilities: ["entry.publish"],
|
|
3866
|
+
requireComment: true
|
|
3867
|
+
},
|
|
3429
3868
|
{
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3869
|
+
name: "unpublish",
|
|
3870
|
+
label: "Unpublish",
|
|
3871
|
+
from: "published",
|
|
3872
|
+
to: "draft",
|
|
3873
|
+
requiredCapabilities: ["entry.unpublish"],
|
|
3874
|
+
unpublish: true
|
|
3434
3875
|
}
|
|
3435
|
-
]
|
|
3436
|
-
|
|
3876
|
+
],
|
|
3877
|
+
roles: [
|
|
3878
|
+
...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
|
|
3879
|
+
...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
|
|
3880
|
+
]
|
|
3881
|
+
};
|
|
3437
3882
|
}
|
|
3438
|
-
function
|
|
3439
|
-
|
|
3440
|
-
const invalidRoots = getInvalidRoots(expression, allowedRoots);
|
|
3441
|
-
if (invalidRoots.length === 0) return [];
|
|
3442
|
-
return [
|
|
3443
|
-
{
|
|
3444
|
-
path,
|
|
3445
|
-
surface: "field.admin.condition",
|
|
3446
|
-
expression,
|
|
3447
|
-
message: `uses unsupported context ${invalidRoots.map((root) => `"${root}"`).join(", ")}. Allowed context: ${allowedRoots.map((root) => `"${root}"`).join(", ")}`
|
|
3448
|
-
}
|
|
3449
|
-
];
|
|
3450
|
-
} catch (error) {
|
|
3451
|
-
return [
|
|
3452
|
-
{
|
|
3453
|
-
path,
|
|
3454
|
-
surface: "field.admin.condition",
|
|
3455
|
-
expression,
|
|
3456
|
-
message: `has invalid syntax: ${error.message}`
|
|
3457
|
-
}
|
|
3458
|
-
];
|
|
3459
|
-
}
|
|
3883
|
+
function publishingWorkflow() {
|
|
3884
|
+
return definePublishingWorkflow();
|
|
3460
3885
|
}
|
|
3461
|
-
function
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
{
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
}
|
|
3472
|
-
]
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
surface: "collection.admin.previewUrl",
|
|
3478
|
-
expression,
|
|
3479
|
-
message: `has invalid syntax: ${error.message}`
|
|
3480
|
-
}
|
|
3481
|
-
];
|
|
3482
|
-
}
|
|
3886
|
+
function simplePublishingWorkflow() {
|
|
3887
|
+
return {
|
|
3888
|
+
initialState: "draft",
|
|
3889
|
+
draftState: "draft",
|
|
3890
|
+
states: [
|
|
3891
|
+
{ name: "draft", label: "Draft", color: "neutral" },
|
|
3892
|
+
{ name: "published", label: "Published", color: "success", published: true }
|
|
3893
|
+
],
|
|
3894
|
+
transitions: [
|
|
3895
|
+
{ name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
|
|
3896
|
+
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
3897
|
+
]
|
|
3898
|
+
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
3899
|
+
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
3900
|
+
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
3901
|
+
};
|
|
3483
3902
|
}
|
|
3484
|
-
function
|
|
3485
|
-
const
|
|
3486
|
-
|
|
3487
|
-
throw new Error(`Invalid declarative hook at ${path}: ${issues[0].message}`);
|
|
3903
|
+
function publicMetadata(meta) {
|
|
3904
|
+
const { availableTransitions: _availableTransitions, ...safe } = meta;
|
|
3905
|
+
return safe;
|
|
3488
3906
|
}
|
|
3489
|
-
function
|
|
3490
|
-
|
|
3491
|
-
|
|
3907
|
+
function workflowCapabilities(workflow, user) {
|
|
3908
|
+
const capabilities = new Set(Array.isArray(user?.capabilities) ? user.capabilities : []);
|
|
3909
|
+
const roles = Array.isArray(user?.roles) ? user.roles : typeof user?.role === "string" ? [user.role] : [];
|
|
3910
|
+
for (const mapping of workflow.roles ?? []) {
|
|
3911
|
+
if (roles.includes(mapping.role)) mapping.capabilities.forEach((capability) => capabilities.add(capability));
|
|
3912
|
+
}
|
|
3913
|
+
return capabilities;
|
|
3492
3914
|
}
|
|
3493
|
-
function
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
surface: "unsupported",
|
|
3511
|
-
expression: hook,
|
|
3512
|
-
message: "declarative strings are not supported on field hooks.afterRead"
|
|
3513
|
-
});
|
|
3514
|
-
});
|
|
3515
|
-
if (typeof field.admin?.hooks?.onChange === "string") {
|
|
3516
|
-
issues.push(
|
|
3517
|
-
...validateDeclarativeHookExpression(
|
|
3518
|
-
field.admin.hooks.onChange,
|
|
3519
|
-
"field.admin.onChange",
|
|
3520
|
-
`${fieldPath}.admin.hooks.onChange`
|
|
3521
|
-
)
|
|
3522
|
-
);
|
|
3523
|
-
}
|
|
3524
|
-
if (typeof field.admin?.hooks?.options === "string") {
|
|
3525
|
-
issues.push({
|
|
3526
|
-
path: `${fieldPath}.admin.hooks.options`,
|
|
3527
|
-
surface: "unsupported",
|
|
3528
|
-
expression: field.admin.hooks.options,
|
|
3529
|
-
message: "declarative strings are not supported on field admin.hooks.options"
|
|
3530
|
-
});
|
|
3531
|
-
}
|
|
3532
|
-
if (Array.isArray(field.fields)) {
|
|
3533
|
-
collectFieldIssues(field.fields, fieldPath, issues);
|
|
3534
|
-
}
|
|
3535
|
-
if (Array.isArray(field.blocks)) {
|
|
3536
|
-
field.blocks.forEach((block, blockIndex) => {
|
|
3537
|
-
if (!Array.isArray(block.fields)) return;
|
|
3538
|
-
collectFieldIssues(block.fields, `${fieldPath}.${blockSegment(block, blockIndex)}`, issues);
|
|
3539
|
-
});
|
|
3540
|
-
}
|
|
3915
|
+
function canViewWorkflowDraft(workflow, user) {
|
|
3916
|
+
if (!user) return false;
|
|
3917
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
3918
|
+
if (capabilities.has("entry.edit")) return true;
|
|
3919
|
+
if (workflow.transitions.some(
|
|
3920
|
+
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
3921
|
+
)) {
|
|
3922
|
+
return true;
|
|
3923
|
+
}
|
|
3924
|
+
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
function availableWorkflowTransitions(workflow, state, user) {
|
|
3928
|
+
const capabilities = workflowCapabilities(workflow, user);
|
|
3929
|
+
return workflow.transitions.filter((transition) => {
|
|
3930
|
+
const from = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
3931
|
+
return from.includes(state) && (transition.requiredCapabilities ?? []).every((item) => capabilities.has(item));
|
|
3541
3932
|
});
|
|
3542
3933
|
}
|
|
3543
|
-
function
|
|
3544
|
-
return
|
|
3545
|
-
|
|
3546
|
-
|
|
3934
|
+
function initializeWorkflowDocument(data, workflow) {
|
|
3935
|
+
return {
|
|
3936
|
+
...data,
|
|
3937
|
+
__workflow: { state: workflow.initialState, revision: 1 }
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
function publishedStateName(workflow) {
|
|
3941
|
+
return workflow.states.find((state) => state.published)?.name ?? workflow.states[workflow.states.length - 1]?.name ?? workflow.initialState;
|
|
3547
3942
|
}
|
|
3548
|
-
function
|
|
3549
|
-
const
|
|
3550
|
-
const
|
|
3551
|
-
|
|
3552
|
-
const
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
)
|
|
3560
|
-
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3943
|
+
function materializeWorkflowDocument(doc, workflow, user) {
|
|
3944
|
+
const meta = doc.__workflow;
|
|
3945
|
+
const publishedState = publishedStateName(workflow);
|
|
3946
|
+
if (!meta) {
|
|
3947
|
+
const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
|
|
3948
|
+
const state = publishedStateName(workflow);
|
|
3949
|
+
return {
|
|
3950
|
+
...working2,
|
|
3951
|
+
_workflow: {
|
|
3952
|
+
state,
|
|
3953
|
+
revision: 1,
|
|
3954
|
+
availableTransitions: availableWorkflowTransitions(workflow, state, user).map((item) => item.name)
|
|
3955
|
+
}
|
|
3956
|
+
};
|
|
3957
|
+
}
|
|
3958
|
+
const { __published, __workflow, ...working } = doc;
|
|
3959
|
+
if (!canViewWorkflowDraft(workflow, user)) {
|
|
3960
|
+
if (!__published || typeof __published !== "object") {
|
|
3961
|
+
if (meta.state === publishedState) {
|
|
3962
|
+
return { ...working, _workflow: publicMetadata(meta) };
|
|
3963
|
+
}
|
|
3964
|
+
return null;
|
|
3564
3965
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
issues
|
|
3573
|
-
);
|
|
3574
|
-
});
|
|
3966
|
+
return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
|
|
3967
|
+
}
|
|
3968
|
+
return {
|
|
3969
|
+
...working,
|
|
3970
|
+
_workflow: {
|
|
3971
|
+
...meta,
|
|
3972
|
+
availableTransitions: availableWorkflowTransitions(workflow, meta.state, user).map((item) => item.name)
|
|
3575
3973
|
}
|
|
3576
|
-
}
|
|
3974
|
+
};
|
|
3577
3975
|
}
|
|
3578
|
-
function
|
|
3579
|
-
|
|
3580
|
-
const fieldPath = `${path}.${fieldSegment(field, index)}`;
|
|
3581
|
-
if (typeof field.access?.read === "string") {
|
|
3582
|
-
issues.push(...validateDeclarativeAccessExpression(field.access.read, "field.access", `${fieldPath}.access.read`));
|
|
3583
|
-
}
|
|
3584
|
-
if (typeof field.access?.create === "string") {
|
|
3585
|
-
issues.push(
|
|
3586
|
-
...validateDeclarativeAccessExpression(field.access.create, "field.access", `${fieldPath}.access.create`)
|
|
3587
|
-
);
|
|
3588
|
-
}
|
|
3589
|
-
if (typeof field.access?.update === "string") {
|
|
3590
|
-
issues.push(
|
|
3591
|
-
...validateDeclarativeAccessExpression(field.access.update, "field.access", `${fieldPath}.access.update`)
|
|
3592
|
-
);
|
|
3593
|
-
}
|
|
3594
|
-
if (Array.isArray(field.fields)) {
|
|
3595
|
-
collectFieldAccessIssues(field.fields, fieldPath, issues);
|
|
3596
|
-
}
|
|
3597
|
-
if (Array.isArray(field.blocks)) {
|
|
3598
|
-
field.blocks.forEach((block, blockIndex) => {
|
|
3599
|
-
if (!Array.isArray(block.fields)) return;
|
|
3600
|
-
collectFieldAccessIssues(
|
|
3601
|
-
block.fields,
|
|
3602
|
-
`${fieldPath}.${blockSegment(block, blockIndex)}`,
|
|
3603
|
-
issues
|
|
3604
|
-
);
|
|
3605
|
-
});
|
|
3606
|
-
}
|
|
3607
|
-
});
|
|
3976
|
+
function eventId() {
|
|
3977
|
+
return globalThis.crypto?.randomUUID?.() ?? `evt_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
3608
3978
|
}
|
|
3609
|
-
function
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3979
|
+
function createLifecycleEvent(args) {
|
|
3980
|
+
return {
|
|
3981
|
+
id: eventId(),
|
|
3982
|
+
name: args.name,
|
|
3983
|
+
collection: args.collection,
|
|
3984
|
+
documentId: args.documentId,
|
|
3985
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3986
|
+
actorId: args.actorId,
|
|
3987
|
+
payload: args.payload,
|
|
3988
|
+
attempts: 0,
|
|
3989
|
+
status: "pending"
|
|
3615
3990
|
};
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3991
|
+
}
|
|
3992
|
+
async function persistEvent(db, event) {
|
|
3993
|
+
await db.create({ collection: LIFECYCLE_EVENTS_COLLECTION, data: event });
|
|
3994
|
+
}
|
|
3995
|
+
async function dispatchLifecycleEvent(config, event) {
|
|
3996
|
+
const db = config.db;
|
|
3997
|
+
if (!db || !config.events?.handlers.length) return;
|
|
3998
|
+
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
3999
|
+
const retryDelayMs = config.events.retryDelayMs ?? 1e3;
|
|
4000
|
+
const attempts = event.attempts + 1;
|
|
4001
|
+
try {
|
|
4002
|
+
await db.update({
|
|
4003
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
4004
|
+
id: event.id,
|
|
4005
|
+
data: { status: "processing", attempts }
|
|
4006
|
+
});
|
|
4007
|
+
for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
|
|
4008
|
+
await db.update({
|
|
4009
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
4010
|
+
id: event.id,
|
|
4011
|
+
data: { status: "delivered", attempts, deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null }
|
|
4012
|
+
});
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
const exhausted = attempts >= maxAttempts;
|
|
4015
|
+
await db.update({
|
|
4016
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
4017
|
+
id: event.id,
|
|
4018
|
+
data: {
|
|
4019
|
+
status: "failed",
|
|
4020
|
+
attempts,
|
|
4021
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
4022
|
+
nextAttemptAt: exhausted ? null : new Date(Date.now() + retryDelayMs * 2 ** (attempts - 1)).toISOString()
|
|
3629
4023
|
}
|
|
3630
|
-
issues.push(...validateDeclarativeHookExpression(hook, surface, `${path}.${key}[${index}]`));
|
|
3631
4024
|
});
|
|
3632
4025
|
}
|
|
3633
4026
|
}
|
|
3634
|
-
function
|
|
3635
|
-
|
|
3636
|
-
config.
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
);
|
|
4027
|
+
async function dispatchPendingLifecycleEvents(config, limit = 50) {
|
|
4028
|
+
if (!config.db || !config.events?.handlers.length) return 0;
|
|
4029
|
+
const maxAttempts = config.events.maxAttempts ?? 8;
|
|
4030
|
+
const result = await config.db.find({
|
|
4031
|
+
collection: LIFECYCLE_EVENTS_COLLECTION,
|
|
4032
|
+
where: { status: { in: ["pending", "failed"] } },
|
|
4033
|
+
sort: "occurredAt",
|
|
4034
|
+
limit
|
|
3643
4035
|
});
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
4036
|
+
const now2 = Date.now();
|
|
4037
|
+
const due = result.docs.filter(
|
|
4038
|
+
(doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
|
|
4039
|
+
);
|
|
4040
|
+
for (const event of due) await dispatchLifecycleEvent(config, event);
|
|
4041
|
+
return due.length;
|
|
4042
|
+
}
|
|
4043
|
+
async function saveWorkflowDraft(args) {
|
|
4044
|
+
const { config, collection, id, originalDoc, data, user, beforeCommit } = args;
|
|
4045
|
+
const db = config.db;
|
|
4046
|
+
const workflow = collection.workflow;
|
|
4047
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
4048
|
+
const previous = originalDoc.__workflow;
|
|
4049
|
+
const isLegacyPublishedDoc = !previous;
|
|
4050
|
+
const baselineRevision = previous?.revision ?? 1;
|
|
4051
|
+
const event = createLifecycleEvent({
|
|
4052
|
+
name: "revision.created",
|
|
4053
|
+
collection: collection.slug,
|
|
4054
|
+
documentId: id,
|
|
4055
|
+
actorId: user?.sub,
|
|
4056
|
+
payload: {
|
|
4057
|
+
revision: baselineRevision + 1,
|
|
4058
|
+
previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
|
|
4059
|
+
}
|
|
3653
4060
|
});
|
|
3654
|
-
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
4061
|
+
const doc = await db.transaction(async (tx) => {
|
|
4062
|
+
const nextMeta = {
|
|
4063
|
+
...previous ?? {},
|
|
4064
|
+
state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
|
|
4065
|
+
revision: baselineRevision + 1,
|
|
4066
|
+
...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
|
|
4067
|
+
};
|
|
4068
|
+
const updateData = {
|
|
4069
|
+
...data,
|
|
4070
|
+
__workflow: nextMeta
|
|
4071
|
+
};
|
|
4072
|
+
if (isLegacyPublishedDoc) {
|
|
4073
|
+
const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
|
|
4074
|
+
updateData.__published = publishedSnapshot;
|
|
4075
|
+
}
|
|
4076
|
+
const updated = await tx.update({ collection: collection.slug, id, data: updateData });
|
|
4077
|
+
await persistEvent(tx, event);
|
|
4078
|
+
await beforeCommit?.(tx, updated);
|
|
4079
|
+
return updated;
|
|
4080
|
+
});
|
|
4081
|
+
void dispatchLifecycleEvent(config, event);
|
|
4082
|
+
return { doc, event };
|
|
4083
|
+
}
|
|
4084
|
+
async function createWorkflowDocument(args) {
|
|
4085
|
+
const { config, collection, data, user, beforeCommit } = args;
|
|
4086
|
+
const db = config.db;
|
|
4087
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
4088
|
+
let event;
|
|
4089
|
+
const doc = await db.transaction(async (tx) => {
|
|
4090
|
+
const created = await tx.create({ collection: collection.slug, data });
|
|
4091
|
+
event = createLifecycleEvent({
|
|
4092
|
+
name: "revision.created",
|
|
4093
|
+
collection: collection.slug,
|
|
4094
|
+
documentId: created.id,
|
|
4095
|
+
actorId: user?.sub,
|
|
4096
|
+
payload: { revision: 1, previousRevision: null }
|
|
4097
|
+
});
|
|
4098
|
+
await persistEvent(tx, event);
|
|
4099
|
+
await beforeCommit?.(tx, created);
|
|
4100
|
+
return created;
|
|
3663
4101
|
});
|
|
3664
|
-
|
|
4102
|
+
void dispatchLifecycleEvent(config, event);
|
|
4103
|
+
return { doc, event };
|
|
3665
4104
|
}
|
|
3666
|
-
function
|
|
3667
|
-
const
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
4105
|
+
async function transitionWorkflow(args) {
|
|
4106
|
+
const { config, collection, id, transitionName, expectedRevision, comment, input, user, req } = args;
|
|
4107
|
+
const db = config.db;
|
|
4108
|
+
const workflow = collection.workflow;
|
|
4109
|
+
if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
|
|
4110
|
+
const original = await db.findOne({ collection: collection.slug, id });
|
|
4111
|
+
if (!original) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
4112
|
+
const meta = original.__workflow;
|
|
4113
|
+
if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
|
|
4114
|
+
if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
|
|
4115
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
4116
|
+
statusCode: 409
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
const transition = workflow.transitions.find((item) => item.name === transitionName);
|
|
4120
|
+
const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
|
|
4121
|
+
if (!transition || !fromStates.includes(meta.state)) {
|
|
4122
|
+
throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
|
|
4123
|
+
statusCode: 409
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
|
|
4127
|
+
throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
|
|
4128
|
+
}
|
|
4129
|
+
if (transition.requireComment && !comment?.trim()) {
|
|
4130
|
+
throw Object.assign(new Error(`A comment is required for "${transition.label}".`), { statusCode: 400 });
|
|
4131
|
+
}
|
|
4132
|
+
const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, input, req, db };
|
|
4133
|
+
for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
|
|
4134
|
+
const events = [
|
|
4135
|
+
createLifecycleEvent({
|
|
4136
|
+
name: "workflow.transitioned",
|
|
4137
|
+
collection: collection.slug,
|
|
4138
|
+
documentId: id,
|
|
4139
|
+
actorId: user?.sub,
|
|
4140
|
+
payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
|
|
4141
|
+
})
|
|
4142
|
+
];
|
|
4143
|
+
const targetState = workflow.states.find((state) => state.name === transition.to);
|
|
4144
|
+
if (targetState.published) {
|
|
4145
|
+
events.push(
|
|
4146
|
+
createLifecycleEvent({
|
|
4147
|
+
name: "entry.published",
|
|
4148
|
+
collection: collection.slug,
|
|
4149
|
+
documentId: id,
|
|
4150
|
+
actorId: user?.sub,
|
|
4151
|
+
payload: { revision: meta.revision }
|
|
4152
|
+
})
|
|
3672
4153
|
);
|
|
3673
|
-
})
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
4154
|
+
} else if (transition.unpublish) {
|
|
4155
|
+
events.push(
|
|
4156
|
+
createLifecycleEvent({
|
|
4157
|
+
name: "entry.unpublished",
|
|
4158
|
+
collection: collection.slug,
|
|
4159
|
+
documentId: id,
|
|
4160
|
+
actorId: user?.sub,
|
|
4161
|
+
payload: { revision: meta.publishedRevision }
|
|
4162
|
+
})
|
|
3680
4163
|
);
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
const
|
|
3684
|
-
if (
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
)
|
|
3691
|
-
);
|
|
3692
|
-
}
|
|
3693
|
-
if (typeof collection.access?.create === "string") {
|
|
3694
|
-
issues.push(
|
|
3695
|
-
...validateDeclarativeAccessExpression(
|
|
3696
|
-
collection.access.create,
|
|
3697
|
-
"collection.access",
|
|
3698
|
-
`${collectionPath}.access.create`
|
|
3699
|
-
)
|
|
3700
|
-
);
|
|
3701
|
-
}
|
|
3702
|
-
if (typeof collection.access?.update === "string") {
|
|
3703
|
-
issues.push(
|
|
3704
|
-
...validateDeclarativeAccessExpression(
|
|
3705
|
-
collection.access.update,
|
|
3706
|
-
"collection.access",
|
|
3707
|
-
`${collectionPath}.access.update`
|
|
3708
|
-
)
|
|
3709
|
-
);
|
|
3710
|
-
}
|
|
3711
|
-
if (typeof collection.access?.delete === "string") {
|
|
3712
|
-
issues.push(
|
|
3713
|
-
...validateDeclarativeAccessExpression(
|
|
3714
|
-
collection.access.delete,
|
|
3715
|
-
"collection.access",
|
|
3716
|
-
`${collectionPath}.access.delete`
|
|
3717
|
-
)
|
|
3718
|
-
);
|
|
3719
|
-
}
|
|
3720
|
-
if (typeof collection.access?.readAudit === "string") {
|
|
3721
|
-
issues.push(
|
|
3722
|
-
...validateDeclarativeAccessExpression(
|
|
3723
|
-
collection.access.readAudit,
|
|
3724
|
-
"collection.access",
|
|
3725
|
-
`${collectionPath}.access.readAudit`
|
|
3726
|
-
)
|
|
3727
|
-
);
|
|
4164
|
+
}
|
|
4165
|
+
const updated = await db.transaction(async (tx) => {
|
|
4166
|
+
const locked = await tx.findOne({ collection: collection.slug, id });
|
|
4167
|
+
if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
4168
|
+
const lockedMeta = locked.__workflow;
|
|
4169
|
+
if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
|
|
4170
|
+
throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
|
|
4171
|
+
statusCode: 409
|
|
4172
|
+
});
|
|
3728
4173
|
}
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
4174
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4175
|
+
const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
|
|
4176
|
+
const nextMeta = {
|
|
4177
|
+
...lockedMeta,
|
|
4178
|
+
state: transition.to,
|
|
4179
|
+
...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
|
|
4180
|
+
...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
|
|
4181
|
+
};
|
|
4182
|
+
const data = { __workflow: nextMeta };
|
|
4183
|
+
if (targetState.published) data.__published = working;
|
|
4184
|
+
if (transition.unpublish) data.__published = null;
|
|
4185
|
+
const next = await tx.update({ collection: collection.slug, id, data });
|
|
4186
|
+
if (transition.onTransition) {
|
|
4187
|
+
await transition.onTransition({
|
|
4188
|
+
transition,
|
|
4189
|
+
from: lockedMeta.state,
|
|
4190
|
+
to: transition.to,
|
|
4191
|
+
doc: locked,
|
|
4192
|
+
input,
|
|
4193
|
+
comment,
|
|
4194
|
+
user,
|
|
4195
|
+
req,
|
|
4196
|
+
tx
|
|
4197
|
+
});
|
|
3737
4198
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
4199
|
+
await tx.create({
|
|
4200
|
+
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
4201
|
+
data: {
|
|
4202
|
+
collection: collection.slug,
|
|
4203
|
+
documentId: id,
|
|
4204
|
+
transition: transition.name,
|
|
4205
|
+
from: lockedMeta.state,
|
|
4206
|
+
to: transition.to,
|
|
4207
|
+
revision: lockedMeta.revision,
|
|
4208
|
+
comment: comment ?? null,
|
|
4209
|
+
actorId: user?.sub ?? null,
|
|
4210
|
+
createdAt: now2
|
|
4211
|
+
}
|
|
4212
|
+
});
|
|
4213
|
+
for (const event of events) await persistEvent(tx, event);
|
|
4214
|
+
if (collection.audit) {
|
|
4215
|
+
await tx.create({
|
|
4216
|
+
collection: "__audit",
|
|
4217
|
+
data: {
|
|
4218
|
+
collection: collection.slug,
|
|
4219
|
+
documentId: id,
|
|
4220
|
+
operation: "workflow.transition",
|
|
4221
|
+
user: user?.sub ?? null,
|
|
4222
|
+
timestamp: now2,
|
|
4223
|
+
changes: JSON.stringify({
|
|
4224
|
+
transition: transition.name,
|
|
4225
|
+
from: lockedMeta.state,
|
|
4226
|
+
to: transition.to,
|
|
4227
|
+
revision: lockedMeta.revision
|
|
4228
|
+
})
|
|
4229
|
+
}
|
|
4230
|
+
});
|
|
3746
4231
|
}
|
|
3747
|
-
|
|
3748
|
-
});
|
|
3749
|
-
return issues;
|
|
3750
|
-
}
|
|
3751
|
-
function collectAdminConditionValidationIssues(config) {
|
|
3752
|
-
const issues = [];
|
|
3753
|
-
config.blocks?.forEach((block, blockIndex) => {
|
|
3754
|
-
if (!Array.isArray(block.fields)) return;
|
|
3755
|
-
collectAdminConditionIssues(
|
|
3756
|
-
block.fields,
|
|
3757
|
-
blockSegment(block, blockIndex),
|
|
3758
|
-
collectFieldNames(block.fields),
|
|
3759
|
-
issues
|
|
3760
|
-
);
|
|
3761
|
-
});
|
|
3762
|
-
config.collections.forEach((collection, collectionIndex) => {
|
|
3763
|
-
const collectionPath = collectionSegment(collection, collectionIndex);
|
|
3764
|
-
const rootFieldNames = collectFieldNames(collection.fields);
|
|
3765
|
-
collectAdminConditionIssues(collection.fields, collectionPath, rootFieldNames, issues);
|
|
3766
|
-
});
|
|
3767
|
-
config.globals?.forEach((global, globalIndex) => {
|
|
3768
|
-
const globalPath = globalSegment(global, globalIndex);
|
|
3769
|
-
const rootFieldNames = collectFieldNames(global.fields);
|
|
3770
|
-
collectAdminConditionIssues(global.fields, globalPath, rootFieldNames, issues);
|
|
4232
|
+
return next;
|
|
3771
4233
|
});
|
|
3772
|
-
|
|
4234
|
+
for (const event of events) void dispatchLifecycleEvent(config, event);
|
|
4235
|
+
for (const hook of workflow.hooks?.afterTransition ?? []) {
|
|
4236
|
+
try {
|
|
4237
|
+
await hook({ ...hookContext, doc: updated, event: events[0] });
|
|
4238
|
+
} catch (error) {
|
|
4239
|
+
try {
|
|
4240
|
+
const obs = config?.__observability;
|
|
4241
|
+
obs?.recordWorkflowHookFailure?.({
|
|
4242
|
+
collection: collection.slug,
|
|
4243
|
+
transition: transition.name
|
|
4244
|
+
});
|
|
4245
|
+
const logger = config?.logger;
|
|
4246
|
+
if (logger && typeof logger.child === "function") {
|
|
4247
|
+
logger.child({ component: "workflow" }).error({
|
|
4248
|
+
err: error,
|
|
4249
|
+
msg: "afterTransition hook failed",
|
|
4250
|
+
collection: collection.slug,
|
|
4251
|
+
transition: transition.name,
|
|
4252
|
+
documentId: id
|
|
4253
|
+
});
|
|
4254
|
+
} else if (logger && typeof logger.error === "function") {
|
|
4255
|
+
logger.error({
|
|
4256
|
+
err: error,
|
|
4257
|
+
msg: "afterTransition hook failed",
|
|
4258
|
+
collection: collection.slug,
|
|
4259
|
+
transition: transition.name,
|
|
4260
|
+
documentId: id
|
|
4261
|
+
});
|
|
4262
|
+
} else {
|
|
4263
|
+
console.error("afterTransition hook failed:", error);
|
|
4264
|
+
}
|
|
4265
|
+
} catch {
|
|
4266
|
+
console.error("afterTransition hook failed:", error);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
return updated;
|
|
3773
4271
|
}
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
const allowedRoots = [
|
|
3780
|
-
.../* @__PURE__ */ new Set([...PREVIEW_URL_RESERVED_ROOTS, ...collectFieldNames(collection.fields)])
|
|
3781
|
-
];
|
|
3782
|
-
issues.push(
|
|
3783
|
-
...validatePreviewUrlExpression(
|
|
3784
|
-
collection.admin.previewUrl,
|
|
3785
|
-
`${collectionPath}.admin.previewUrl`,
|
|
3786
|
-
allowedRoots
|
|
3787
|
-
)
|
|
3788
|
-
);
|
|
3789
|
-
});
|
|
3790
|
-
return issues;
|
|
4272
|
+
|
|
4273
|
+
// src/utils/admin-auth.ts
|
|
4274
|
+
function getAdminAuthCollection(config) {
|
|
4275
|
+
const adminCollection = resolveAdminAuthCollection(config.collections, config.adminAuth?.collectionSlug);
|
|
4276
|
+
return adminCollection ?? null;
|
|
3791
4277
|
}
|
|
3792
|
-
function
|
|
3793
|
-
const
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
}));
|
|
3801
|
-
if (diagnostics.length === 0) return;
|
|
3802
|
-
throw new ConfigValidationError(source, diagnostics);
|
|
4278
|
+
function resolveAdminAuthCollection(collections, collectionSlug) {
|
|
4279
|
+
const adminsCollection = collections.find((collection) => collection.slug === "__admins");
|
|
4280
|
+
if (adminsCollection) return adminsCollection;
|
|
4281
|
+
if (collectionSlug) {
|
|
4282
|
+
const requestedCollection = collections.find((collection) => collection.slug === collectionSlug);
|
|
4283
|
+
if (requestedCollection) return requestedCollection;
|
|
4284
|
+
}
|
|
4285
|
+
return collections.find((collection) => collection.auth);
|
|
3803
4286
|
}
|
|
3804
|
-
function
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
message: issue.message,
|
|
3811
|
-
expression: issue.expression
|
|
4287
|
+
function getPublicAdminAuthConfig(adminAuth, collections) {
|
|
4288
|
+
const providers = (adminAuth?.providers ?? []).map((provider) => ({
|
|
4289
|
+
id: provider.id,
|
|
4290
|
+
type: provider.type,
|
|
4291
|
+
displayName: provider.displayName || humanizeProviderName(provider.id, provider.type),
|
|
4292
|
+
autoRedirect: provider.autoRedirect
|
|
3812
4293
|
}));
|
|
3813
|
-
|
|
3814
|
-
|
|
4294
|
+
const resolvedCollectionSlug = collections ? resolveAdminAuthCollection(collections, adminAuth?.collectionSlug)?.slug : adminAuth?.collectionSlug;
|
|
4295
|
+
return {
|
|
4296
|
+
mode: adminAuth?.mode ?? "local",
|
|
4297
|
+
collectionSlug: resolvedCollectionSlug,
|
|
4298
|
+
provisioningMode: adminAuth?.provisioningMode,
|
|
4299
|
+
providers
|
|
4300
|
+
};
|
|
3815
4301
|
}
|
|
3816
|
-
function
|
|
3817
|
-
const
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
source: "adminCondition",
|
|
3836
|
-
path: issue.path,
|
|
3837
|
-
surface: issue.surface,
|
|
3838
|
-
message: issue.message,
|
|
3839
|
-
expression: issue.expression
|
|
3840
|
-
}));
|
|
3841
|
-
const previewUrlIssues = collectPreviewUrlValidationIssues(config).map((issue) => ({
|
|
3842
|
-
severity: "error",
|
|
3843
|
-
source: "previewUrl",
|
|
3844
|
-
path: issue.path,
|
|
3845
|
-
surface: issue.surface,
|
|
3846
|
-
message: issue.message,
|
|
3847
|
-
expression: issue.expression
|
|
3848
|
-
}));
|
|
3849
|
-
return [...hookIssues, ...accessIssues, ...adminConditionIssues, ...previewUrlIssues];
|
|
4302
|
+
function humanizeProviderName(id, type) {
|
|
4303
|
+
const cleaned = id.replace(/[-_]+/g, " ").trim();
|
|
4304
|
+
if (!cleaned) return type.toUpperCase();
|
|
4305
|
+
return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
4306
|
+
}
|
|
4307
|
+
function getAdminRoleForCollection(collection) {
|
|
4308
|
+
if (!collection?.auth) return "admin";
|
|
4309
|
+
if (typeof collection.auth === "object" && collection.auth.adminRole) {
|
|
4310
|
+
return collection.auth.adminRole;
|
|
4311
|
+
}
|
|
4312
|
+
return "admin";
|
|
4313
|
+
}
|
|
4314
|
+
function isUserAdmin(user, collection) {
|
|
4315
|
+
if (!user) return false;
|
|
4316
|
+
const roles = Array.isArray(user.roles) ? user.roles : typeof user.role === "string" ? [user.role] : [];
|
|
4317
|
+
if (roles.length === 0) return false;
|
|
4318
|
+
const configuredRole = getAdminRoleForCollection(collection);
|
|
4319
|
+
if (roles.includes(configuredRole)) return true;
|
|
4320
|
+
return roles.includes("admin") || roles.includes("super_admin");
|
|
3850
4321
|
}
|
|
3851
|
-
function formatConfigDiagnostics(source, diagnostics, options = {}) {
|
|
3852
|
-
const color = options.color ?? false;
|
|
3853
|
-
const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
|
|
3854
|
-
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)}`;
|
|
3855
|
-
const lines = diagnostics.flatMap((issue, index) => [
|
|
3856
|
-
`${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
|
|
3857
|
-
` ${issue.message}`,
|
|
3858
|
-
` ${colorize("surface:", 90, color)} ${issue.surface}`
|
|
3859
|
-
]);
|
|
3860
|
-
return `${header}
|
|
3861
|
-
${lines.join("\n")}
|
|
3862
4322
|
|
|
3863
|
-
|
|
4323
|
+
// src/utils/block-references.ts
|
|
4324
|
+
function mergeUniqueBlocks(blocks) {
|
|
4325
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4326
|
+
for (const block of blocks) {
|
|
4327
|
+
const existing = seen.get(block.slug);
|
|
4328
|
+
if (existing && existing !== block) {
|
|
4329
|
+
throw new Error(
|
|
4330
|
+
`Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
if (!existing) seen.set(block.slug, block);
|
|
4334
|
+
}
|
|
4335
|
+
return Array.from(seen.values());
|
|
3864
4336
|
}
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
4337
|
+
function buildRegistry(blocks) {
|
|
4338
|
+
return new Map(blocks.map((block) => [block.slug, block]));
|
|
4339
|
+
}
|
|
4340
|
+
function resolveField(field, registry, blockCache) {
|
|
4341
|
+
const next = { ...field };
|
|
4342
|
+
if (next.fields) {
|
|
4343
|
+
next.fields = next.fields.map(
|
|
4344
|
+
(child) => resolveField(child, registry, blockCache)
|
|
4345
|
+
);
|
|
3873
4346
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4347
|
+
if (next.type !== "blocks") return next;
|
|
4348
|
+
const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
|
|
4349
|
+
const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
|
|
4350
|
+
if (hasInlineBlocks && hasReferences) {
|
|
4351
|
+
throw new Error(
|
|
4352
|
+
`Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
if (hasReferences) {
|
|
4356
|
+
next.blocks = next.blockReferences.map((slug) => {
|
|
4357
|
+
const block = registry.get(slug);
|
|
4358
|
+
if (!block) {
|
|
4359
|
+
throw new Error(
|
|
4360
|
+
`Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
|
|
4361
|
+
);
|
|
4362
|
+
}
|
|
4363
|
+
return resolveBlock(block, registry, blockCache);
|
|
4364
|
+
});
|
|
4365
|
+
return next;
|
|
4366
|
+
}
|
|
4367
|
+
if (hasInlineBlocks) {
|
|
4368
|
+
next.blocks = next.blocks.map(
|
|
4369
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
return next;
|
|
3877
4373
|
}
|
|
3878
|
-
function
|
|
3879
|
-
const
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4374
|
+
function resolveBlock(block, registry, blockCache) {
|
|
4375
|
+
const cached = blockCache.get(block);
|
|
4376
|
+
if (cached) return cached;
|
|
4377
|
+
const resolved = {
|
|
4378
|
+
...block,
|
|
4379
|
+
fields: []
|
|
4380
|
+
};
|
|
4381
|
+
blockCache.set(block, resolved);
|
|
4382
|
+
resolved.fields = block.fields.map(
|
|
4383
|
+
(field) => resolveField(field, registry, blockCache)
|
|
4384
|
+
);
|
|
4385
|
+
return resolved;
|
|
3889
4386
|
}
|
|
3890
|
-
function
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
4387
|
+
function resolveFields(fields, registry, blockCache) {
|
|
4388
|
+
return fields.map((field) => resolveField(field, registry, blockCache));
|
|
4389
|
+
}
|
|
4390
|
+
function normalizeSchemaFragment(fragment) {
|
|
4391
|
+
const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
|
|
4392
|
+
const registry = buildRegistry(reusableBlocks);
|
|
4393
|
+
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
4394
|
+
const resolvedBlocks = reusableBlocks.map(
|
|
4395
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
4396
|
+
);
|
|
4397
|
+
const resolvedCollections = (fragment.collections ?? []).map(
|
|
4398
|
+
(collection) => ({
|
|
4399
|
+
...collection,
|
|
4400
|
+
fields: resolveFields(collection.fields ?? [], registry, blockCache)
|
|
4401
|
+
})
|
|
4402
|
+
);
|
|
4403
|
+
const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
|
|
4404
|
+
...global,
|
|
4405
|
+
fields: resolveFields(global.fields ?? [], registry, blockCache)
|
|
3898
4406
|
}));
|
|
3899
|
-
|
|
3900
|
-
|
|
4407
|
+
return {
|
|
4408
|
+
...fragment,
|
|
4409
|
+
...fragment.blocks ? { blocks: resolvedBlocks } : {},
|
|
4410
|
+
...fragment.collections ? { collections: resolvedCollections } : {},
|
|
4411
|
+
...fragment.globals ? { globals: resolvedGlobals } : {}
|
|
4412
|
+
};
|
|
3901
4413
|
}
|
|
3902
4414
|
|
|
3903
4415
|
// src/utils/id.ts
|
|
@@ -4081,7 +4593,7 @@ var AUDIT_COLLECTION = {
|
|
|
4081
4593
|
name: "operation",
|
|
4082
4594
|
type: "select",
|
|
4083
4595
|
label: "Operation",
|
|
4084
|
-
options: ["create", "update", "delete"],
|
|
4596
|
+
options: ["create", "update", "delete", "trash", "restore", "purge", "trash-empty"],
|
|
4085
4597
|
required: true
|
|
4086
4598
|
},
|
|
4087
4599
|
{ name: "user", type: "text", label: "User ID" },
|
|
@@ -4175,12 +4687,14 @@ function normalizeConfig(config) {
|
|
|
4175
4687
|
assertValidDeclarativeAccessInConfig(schemaAwareConfig, "config");
|
|
4176
4688
|
assertValidAdminConditionsInConfig(schemaAwareConfig, "config");
|
|
4177
4689
|
assertValidPreviewUrlsInConfig(schemaAwareConfig, "config");
|
|
4690
|
+
assertValidTrashInConfig(schemaAwareConfig, "config");
|
|
4178
4691
|
const collections = schemaAwareConfig?.collections || [];
|
|
4179
4692
|
const globals = schemaAwareConfig?.globals || [];
|
|
4180
4693
|
const needsAudit = collections.some((col) => col.audit);
|
|
4181
4694
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
4182
4695
|
const needsTasks = (config.tasks?.length ?? 0) > 0;
|
|
4183
4696
|
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
4697
|
+
const needsTrash = schemaAwareConfig.trash?.enabled === true || collections.some((col) => resolveTrashConfig(col, schemaAwareConfig).enabled);
|
|
4184
4698
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
4185
4699
|
collections,
|
|
4186
4700
|
adminAuth: schemaAwareConfig.adminAuth
|
|
@@ -4418,6 +4932,9 @@ function normalizeConfig(config) {
|
|
|
4418
4932
|
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
4419
4933
|
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
4420
4934
|
}
|
|
4935
|
+
if (needsTrash && !normalizedCollections.some((col) => col.slug === TRASH_COLLECTION)) {
|
|
4936
|
+
systemCollections.push(TRASH_COLLECTION_CONFIG);
|
|
4937
|
+
}
|
|
4421
4938
|
return {
|
|
4422
4939
|
...schemaAwareConfig,
|
|
4423
4940
|
collections: [...normalizedCollections, ...systemCollections],
|
|
@@ -4780,206 +5297,6 @@ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
|
|
|
4780
5297
|
}).join(", ");
|
|
4781
5298
|
}
|
|
4782
5299
|
|
|
4783
|
-
// src/utils/hooks.ts
|
|
4784
|
-
var import_jexl3 = __toESM(require_Jexl(), 1);
|
|
4785
|
-
async function runCollectionHooks(hooks, args, options = {}) {
|
|
4786
|
-
if (!hooks || hooks.length === 0) {
|
|
4787
|
-
return args.data ?? args.doc ?? args.query ?? void 0;
|
|
4788
|
-
}
|
|
4789
|
-
const payloadKey = args.data !== void 0 ? "data" : args.doc !== void 0 ? "doc" : "query" in args ? "query" : void 0;
|
|
4790
|
-
let currentPayload = payloadKey === void 0 ? void 0 : args[payloadKey];
|
|
4791
|
-
for (const hook of hooks) {
|
|
4792
|
-
try {
|
|
4793
|
-
const hookArgs = {
|
|
4794
|
-
...args,
|
|
4795
|
-
...payloadKey !== void 0 ? { [payloadKey]: currentPayload } : {}
|
|
4796
|
-
};
|
|
4797
|
-
const result = typeof hook === "string" ? await (() => {
|
|
4798
|
-
if (options.surface) {
|
|
4799
|
-
assertValidDeclarativeHookExpression(
|
|
4800
|
-
hook,
|
|
4801
|
-
options.surface,
|
|
4802
|
-
options.path ?? options.surface
|
|
4803
|
-
);
|
|
4804
|
-
}
|
|
4805
|
-
return import_jexl3.default.eval(hook, hookArgs);
|
|
4806
|
-
})() : await hook(hookArgs);
|
|
4807
|
-
if (result !== void 0) {
|
|
4808
|
-
if (typeof hook === "string" && options.surface) {
|
|
4809
|
-
assertValidDeclarativeHookResult(
|
|
4810
|
-
result,
|
|
4811
|
-
options.surface,
|
|
4812
|
-
options.path ?? options.surface
|
|
4813
|
-
);
|
|
4814
|
-
}
|
|
4815
|
-
currentPayload = typeof hook === "string" && currentPayload && typeof currentPayload === "object" && !Array.isArray(currentPayload) && result && typeof result === "object" && !Array.isArray(result) ? {
|
|
4816
|
-
...currentPayload,
|
|
4817
|
-
...result
|
|
4818
|
-
} : result;
|
|
4819
|
-
}
|
|
4820
|
-
} catch (err) {
|
|
4821
|
-
if (options.isolated) {
|
|
4822
|
-
console.error("Side-effect hook failed after a successful write", err);
|
|
4823
|
-
} else {
|
|
4824
|
-
throw err;
|
|
4825
|
-
}
|
|
4826
|
-
}
|
|
4827
|
-
}
|
|
4828
|
-
return currentPayload;
|
|
4829
|
-
}
|
|
4830
|
-
async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pathPrefix = "") {
|
|
4831
|
-
if (!data || typeof data !== "object") return data;
|
|
4832
|
-
const result = { ...data };
|
|
4833
|
-
for (const field of fields) {
|
|
4834
|
-
if (!field.name) continue;
|
|
4835
|
-
const fieldPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
4836
|
-
const value = result[field.name];
|
|
4837
|
-
const origValue = originalDoc?.[field.name];
|
|
4838
|
-
let updatedValue = value;
|
|
4839
|
-
if (field.hooks?.beforeChange) {
|
|
4840
|
-
for (const hook of field.hooks.beforeChange) {
|
|
4841
|
-
const hookArgs = {
|
|
4842
|
-
value: updatedValue,
|
|
4843
|
-
originalDoc: originalDoc ?? void 0,
|
|
4844
|
-
data: result,
|
|
4845
|
-
user,
|
|
4846
|
-
db
|
|
4847
|
-
};
|
|
4848
|
-
updatedValue = typeof hook === "string" ? await (() => {
|
|
4849
|
-
assertValidDeclarativeHookExpression(
|
|
4850
|
-
hook,
|
|
4851
|
-
"field.beforeChange",
|
|
4852
|
-
fieldPath
|
|
4853
|
-
);
|
|
4854
|
-
return import_jexl3.default.eval(hook, hookArgs);
|
|
4855
|
-
})() : await hook(hookArgs);
|
|
4856
|
-
}
|
|
4857
|
-
result[field.name] = updatedValue;
|
|
4858
|
-
}
|
|
4859
|
-
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
4860
|
-
if (field.type === "object" && field.fields) {
|
|
4861
|
-
result[field.name] = await executeFieldBeforeChange(
|
|
4862
|
-
field.fields,
|
|
4863
|
-
updatedValue,
|
|
4864
|
-
origValue,
|
|
4865
|
-
user,
|
|
4866
|
-
db,
|
|
4867
|
-
fieldPath
|
|
4868
|
-
);
|
|
4869
|
-
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
4870
|
-
const arrayResult = [];
|
|
4871
|
-
for (let i = 0; i < updatedValue.length; i++) {
|
|
4872
|
-
const item = updatedValue[i];
|
|
4873
|
-
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
4874
|
-
arrayResult.push(
|
|
4875
|
-
await executeFieldBeforeChange(
|
|
4876
|
-
field.fields,
|
|
4877
|
-
item,
|
|
4878
|
-
origItem,
|
|
4879
|
-
user,
|
|
4880
|
-
db,
|
|
4881
|
-
fieldPath
|
|
4882
|
-
)
|
|
4883
|
-
);
|
|
4884
|
-
}
|
|
4885
|
-
result[field.name] = arrayResult;
|
|
4886
|
-
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
4887
|
-
const blocksResult = [];
|
|
4888
|
-
for (let i = 0; i < updatedValue.length; i++) {
|
|
4889
|
-
const blockData = updatedValue[i];
|
|
4890
|
-
const origBlock = Array.isArray(origValue) ? origValue[i] : null;
|
|
4891
|
-
const blockConfig = field.blocks.find(
|
|
4892
|
-
(b) => b.slug === blockData.blockType
|
|
4893
|
-
);
|
|
4894
|
-
if (blockConfig) {
|
|
4895
|
-
blocksResult.push(
|
|
4896
|
-
await executeFieldBeforeChange(
|
|
4897
|
-
blockConfig.fields,
|
|
4898
|
-
blockData,
|
|
4899
|
-
origBlock,
|
|
4900
|
-
user,
|
|
4901
|
-
db,
|
|
4902
|
-
fieldPath
|
|
4903
|
-
)
|
|
4904
|
-
);
|
|
4905
|
-
} else {
|
|
4906
|
-
blocksResult.push(blockData);
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
4909
|
-
result[field.name] = blocksResult;
|
|
4910
|
-
}
|
|
4911
|
-
}
|
|
4912
|
-
}
|
|
4913
|
-
return result;
|
|
4914
|
-
}
|
|
4915
|
-
async function executeFieldAfterRead(fields, doc, user, db) {
|
|
4916
|
-
if (!doc || typeof doc !== "object") return doc;
|
|
4917
|
-
const result = { ...doc };
|
|
4918
|
-
for (const field of fields) {
|
|
4919
|
-
if (!field.name) continue;
|
|
4920
|
-
const value = result[field.name];
|
|
4921
|
-
let updatedValue = value;
|
|
4922
|
-
if (field.hooks?.afterRead) {
|
|
4923
|
-
for (const hook of field.hooks.afterRead) {
|
|
4924
|
-
const typedHook = hook;
|
|
4925
|
-
const hookArgs = {
|
|
4926
|
-
value: updatedValue,
|
|
4927
|
-
doc: result,
|
|
4928
|
-
user,
|
|
4929
|
-
db
|
|
4930
|
-
};
|
|
4931
|
-
updatedValue = await typedHook(hookArgs);
|
|
4932
|
-
}
|
|
4933
|
-
result[field.name] = updatedValue;
|
|
4934
|
-
}
|
|
4935
|
-
if (updatedValue !== void 0 && updatedValue !== null) {
|
|
4936
|
-
if (field.type === "object" && field.fields) {
|
|
4937
|
-
result[field.name] = await executeFieldAfterRead(
|
|
4938
|
-
field.fields,
|
|
4939
|
-
updatedValue,
|
|
4940
|
-
user,
|
|
4941
|
-
db
|
|
4942
|
-
);
|
|
4943
|
-
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
4944
|
-
const arrayResult = [];
|
|
4945
|
-
for (const item of updatedValue) {
|
|
4946
|
-
arrayResult.push(
|
|
4947
|
-
await executeFieldAfterRead(
|
|
4948
|
-
field.fields,
|
|
4949
|
-
item,
|
|
4950
|
-
user,
|
|
4951
|
-
db
|
|
4952
|
-
)
|
|
4953
|
-
);
|
|
4954
|
-
}
|
|
4955
|
-
result[field.name] = arrayResult;
|
|
4956
|
-
} else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
|
|
4957
|
-
const blocksResult = [];
|
|
4958
|
-
for (const blockData of updatedValue) {
|
|
4959
|
-
const typedBlock = blockData;
|
|
4960
|
-
const blockConfig = field.blocks.find(
|
|
4961
|
-
(b) => b.slug === typedBlock.blockType
|
|
4962
|
-
);
|
|
4963
|
-
if (blockConfig) {
|
|
4964
|
-
blocksResult.push(
|
|
4965
|
-
await executeFieldAfterRead(
|
|
4966
|
-
blockConfig.fields,
|
|
4967
|
-
typedBlock,
|
|
4968
|
-
user,
|
|
4969
|
-
db
|
|
4970
|
-
)
|
|
4971
|
-
);
|
|
4972
|
-
} else {
|
|
4973
|
-
blocksResult.push(blockData);
|
|
4974
|
-
}
|
|
4975
|
-
}
|
|
4976
|
-
result[field.name] = blocksResult;
|
|
4977
|
-
}
|
|
4978
|
-
}
|
|
4979
|
-
}
|
|
4980
|
-
return result;
|
|
4981
|
-
}
|
|
4982
|
-
|
|
4983
5300
|
// src/utils/openapi.ts
|
|
4984
5301
|
function getCollectionLabels(collection) {
|
|
4985
5302
|
return collection.labels || { singular: collection.slug, plural: collection.slug };
|
|
@@ -7403,10 +7720,12 @@ var RESERVED_NAVIGATION_SLUGS = /* @__PURE__ */ new Set([
|
|
|
7403
7720
|
"settings",
|
|
7404
7721
|
"admin",
|
|
7405
7722
|
"preferences",
|
|
7406
|
-
"auth"
|
|
7723
|
+
"auth",
|
|
7724
|
+
"trash"
|
|
7407
7725
|
]);
|
|
7408
7726
|
function assertValidNavigationSlugs(navigation) {
|
|
7409
7727
|
for (const item of navigation) {
|
|
7728
|
+
if (item.dashboard || item.trash) continue;
|
|
7410
7729
|
if (item.slug) {
|
|
7411
7730
|
const normalizedSlug = item.slug.toLowerCase().trim().replace(/^\//, "");
|
|
7412
7731
|
if (RESERVED_NAVIGATION_SLUGS.has(normalizedSlug)) {
|
|
@@ -7486,6 +7805,21 @@ function compileNavigation(config) {
|
|
|
7486
7805
|
});
|
|
7487
7806
|
continue;
|
|
7488
7807
|
}
|
|
7808
|
+
if (item.trash) {
|
|
7809
|
+
registerItem(groupName, {
|
|
7810
|
+
id: "trash",
|
|
7811
|
+
type: "trash",
|
|
7812
|
+
slug: "trash",
|
|
7813
|
+
label: item.label || "Trash",
|
|
7814
|
+
icon: item.icon || "Trash2",
|
|
7815
|
+
group: groupName,
|
|
7816
|
+
order: item.order ?? 950,
|
|
7817
|
+
views: [],
|
|
7818
|
+
badge: item.badge,
|
|
7819
|
+
access: item.access
|
|
7820
|
+
});
|
|
7821
|
+
continue;
|
|
7822
|
+
}
|
|
7489
7823
|
if (item.collection) {
|
|
7490
7824
|
const col = collectionsBySlug.get(item.collection);
|
|
7491
7825
|
const combinedViews = [
|
|
@@ -7595,6 +7929,23 @@ function compileNavigation(config) {
|
|
|
7595
7929
|
views: []
|
|
7596
7930
|
});
|
|
7597
7931
|
}
|
|
7932
|
+
const mentionedTrash = explicitNav.some((i) => i.trash || i.slug === "trash");
|
|
7933
|
+
if (!mentionedTrash) {
|
|
7934
|
+
const hasTrash = collections.some(
|
|
7935
|
+
(col) => !col.slug.startsWith("__") && resolveTrashConfig(col, config).enabled
|
|
7936
|
+
);
|
|
7937
|
+
if (hasTrash) {
|
|
7938
|
+
registerItem(void 0, {
|
|
7939
|
+
id: "trash",
|
|
7940
|
+
type: "trash",
|
|
7941
|
+
slug: "trash",
|
|
7942
|
+
label: "Trash",
|
|
7943
|
+
icon: "Trash2",
|
|
7944
|
+
order: 950,
|
|
7945
|
+
views: []
|
|
7946
|
+
});
|
|
7947
|
+
}
|
|
7948
|
+
}
|
|
7598
7949
|
const sortAndSpliceItems = (items) => {
|
|
7599
7950
|
const sorted = [...items].sort((a, b) => a.order - b.order);
|
|
7600
7951
|
const explicitMap = /* @__PURE__ */ new Map();
|
|
@@ -7762,6 +8113,133 @@ function defineImageField(config) {
|
|
|
7762
8113
|
function defineJoinField(config) {
|
|
7763
8114
|
return _defineJoinFieldBase(config);
|
|
7764
8115
|
}
|
|
8116
|
+
function defineCreatedAtField(config) {
|
|
8117
|
+
const type = config?.type ?? "datetime";
|
|
8118
|
+
const name = config?.name ?? "createdAt";
|
|
8119
|
+
return {
|
|
8120
|
+
name,
|
|
8121
|
+
label: "Created At",
|
|
8122
|
+
...config,
|
|
8123
|
+
type,
|
|
8124
|
+
admin: {
|
|
8125
|
+
readOnly: true,
|
|
8126
|
+
...config?.admin
|
|
8127
|
+
}
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
function defineUpdatedAtField(config) {
|
|
8131
|
+
const type = config?.type ?? "datetime";
|
|
8132
|
+
const name = config?.name ?? "updatedAt";
|
|
8133
|
+
return {
|
|
8134
|
+
name,
|
|
8135
|
+
label: "Updated At",
|
|
8136
|
+
...config,
|
|
8137
|
+
type,
|
|
8138
|
+
admin: {
|
|
8139
|
+
readOnly: true,
|
|
8140
|
+
...config?.admin
|
|
8141
|
+
}
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
function defineCreatedByField(config) {
|
|
8145
|
+
const name = config?.name ?? "createdBy";
|
|
8146
|
+
if (config?.type === "text") {
|
|
8147
|
+
const { type: _2, ...rest2 } = config;
|
|
8148
|
+
return {
|
|
8149
|
+
name,
|
|
8150
|
+
label: "Created By",
|
|
8151
|
+
...rest2,
|
|
8152
|
+
type: "text",
|
|
8153
|
+
admin: {
|
|
8154
|
+
readOnly: true,
|
|
8155
|
+
...rest2?.admin
|
|
8156
|
+
}
|
|
8157
|
+
};
|
|
8158
|
+
}
|
|
8159
|
+
const { type: _, relationTo = "users", ...rest } = config || {};
|
|
8160
|
+
return {
|
|
8161
|
+
name,
|
|
8162
|
+
label: "Created By",
|
|
8163
|
+
...rest,
|
|
8164
|
+
type: "relationship",
|
|
8165
|
+
relationTo,
|
|
8166
|
+
admin: {
|
|
8167
|
+
readOnly: true,
|
|
8168
|
+
...rest?.admin
|
|
8169
|
+
},
|
|
8170
|
+
access: {
|
|
8171
|
+
update: () => false,
|
|
8172
|
+
...rest?.access
|
|
8173
|
+
}
|
|
8174
|
+
};
|
|
8175
|
+
}
|
|
8176
|
+
function defineUpdatedByField(config) {
|
|
8177
|
+
const name = config?.name ?? "updatedBy";
|
|
8178
|
+
if (config?.type === "text") {
|
|
8179
|
+
const { type: _2, ...rest2 } = config;
|
|
8180
|
+
return {
|
|
8181
|
+
name,
|
|
8182
|
+
label: "Updated By",
|
|
8183
|
+
...rest2,
|
|
8184
|
+
type: "text",
|
|
8185
|
+
admin: {
|
|
8186
|
+
readOnly: true,
|
|
8187
|
+
...rest2?.admin
|
|
8188
|
+
}
|
|
8189
|
+
};
|
|
8190
|
+
}
|
|
8191
|
+
const { type: _, relationTo = "users", ...rest } = config || {};
|
|
8192
|
+
return {
|
|
8193
|
+
name,
|
|
8194
|
+
label: "Updated By",
|
|
8195
|
+
...rest,
|
|
8196
|
+
type: "relationship",
|
|
8197
|
+
relationTo,
|
|
8198
|
+
admin: {
|
|
8199
|
+
readOnly: true,
|
|
8200
|
+
...rest?.admin
|
|
8201
|
+
},
|
|
8202
|
+
access: {
|
|
8203
|
+
update: () => false,
|
|
8204
|
+
...rest?.access
|
|
8205
|
+
}
|
|
8206
|
+
};
|
|
8207
|
+
}
|
|
8208
|
+
var DEFAULT_ROLES = [
|
|
8209
|
+
{ value: "admin", label: "Admin" },
|
|
8210
|
+
{ value: "editor", label: "Editor" },
|
|
8211
|
+
{ value: "viewer", label: "Viewer" }
|
|
8212
|
+
];
|
|
8213
|
+
function defineRolesField(config) {
|
|
8214
|
+
const isMultiple = Boolean(config?.multiple || config?.hasMany || config?.type === "multiSelect");
|
|
8215
|
+
const type = isMultiple ? "multiSelect" : "select";
|
|
8216
|
+
const name = config?.name ?? "roles";
|
|
8217
|
+
const label = config?.label ?? (name === "role" ? "Role" : "Roles");
|
|
8218
|
+
const rawRoles = config?.roles ?? config?.options ?? DEFAULT_ROLES;
|
|
8219
|
+
const resolvedOptions = Array.isArray(rawRoles) ? rawRoles.map(
|
|
8220
|
+
(r) => typeof r === "string" ? { value: r, label: r.charAt(0).toUpperCase() + r.slice(1).replace(/[-_]/g, " ") } : r
|
|
8221
|
+
) : rawRoles;
|
|
8222
|
+
const lastOption = Array.isArray(resolvedOptions) && resolvedOptions.length > 0 ? resolvedOptions[resolvedOptions.length - 1] : void 0;
|
|
8223
|
+
const defaultRoleValue = lastOption ? typeof lastOption === "string" ? lastOption : lastOption.value : "viewer";
|
|
8224
|
+
const defaultValue2 = config?.defaultValue !== void 0 ? config.defaultValue : isMultiple ? [defaultRoleValue] : defaultRoleValue;
|
|
8225
|
+
const adminRoles = Array.isArray(config?.adminRole) ? config.adminRole : typeof config?.adminRole === "string" ? [config.adminRole] : ["admin"];
|
|
8226
|
+
const defaultUpdateAccess = adminRoles.map((r) => `user.role == '${r}' || (user.roles != null && '${r}' in user.roles)`).join(" || ");
|
|
8227
|
+
const { multiple: _, roles: __, adminRole: ___, ...rest } = config || {};
|
|
8228
|
+
return {
|
|
8229
|
+
name,
|
|
8230
|
+
label,
|
|
8231
|
+
defaultValue: defaultValue2,
|
|
8232
|
+
options: resolvedOptions,
|
|
8233
|
+
access: {
|
|
8234
|
+
update: defaultUpdateAccess,
|
|
8235
|
+
...rest.access
|
|
8236
|
+
},
|
|
8237
|
+
...rest,
|
|
8238
|
+
type,
|
|
8239
|
+
...isMultiple ? { hasMany: true } : {}
|
|
8240
|
+
};
|
|
8241
|
+
}
|
|
8242
|
+
var defineRoles = defineRolesField;
|
|
7765
8243
|
function defineTab(args) {
|
|
7766
8244
|
return args.fields.map((field) => ({
|
|
7767
8245
|
...field,
|
|
@@ -7797,6 +8275,8 @@ function defineTab(args) {
|
|
|
7797
8275
|
RESERVED_NAVIGATION_SLUGS,
|
|
7798
8276
|
TASK_LOCKS_COLLECTION,
|
|
7799
8277
|
TASK_LOCKS_COLLECTION_CONFIG,
|
|
8278
|
+
TRASH_COLLECTION,
|
|
8279
|
+
TRASH_COLLECTION_CONFIG,
|
|
7800
8280
|
UNASSIGNED_GROUP_KEY,
|
|
7801
8281
|
ValidationError,
|
|
7802
8282
|
WORKFLOW_HISTORY_COLLECTION,
|
|
@@ -7810,6 +8290,7 @@ function defineTab(args) {
|
|
|
7810
8290
|
assertValidDeclarativeHooksInConfig,
|
|
7811
8291
|
assertValidNavigationSlugs,
|
|
7812
8292
|
assertValidPreviewUrlsInConfig,
|
|
8293
|
+
assertValidTrashInConfig,
|
|
7813
8294
|
availableWorkflowTransitions,
|
|
7814
8295
|
buildSqlOrderBy,
|
|
7815
8296
|
canViewWorkflowDraft,
|
|
@@ -7828,6 +8309,7 @@ function defineTab(args) {
|
|
|
7828
8309
|
count,
|
|
7829
8310
|
createLifecycleEvent,
|
|
7830
8311
|
createTaskRunner,
|
|
8312
|
+
createTrashPurgeTask,
|
|
7831
8313
|
createWorkflowDocument,
|
|
7832
8314
|
defaultValue,
|
|
7833
8315
|
defineAction,
|
|
@@ -7839,6 +8321,8 @@ function defineTab(args) {
|
|
|
7839
8321
|
defineCardsView,
|
|
7840
8322
|
defineCollection,
|
|
7841
8323
|
defineConfig,
|
|
8324
|
+
defineCreatedAtField,
|
|
8325
|
+
defineCreatedByField,
|
|
7842
8326
|
defineDateField,
|
|
7843
8327
|
defineDateTimeField,
|
|
7844
8328
|
defineEmailField,
|
|
@@ -7858,6 +8342,8 @@ function defineTab(args) {
|
|
|
7858
8342
|
defineRadioField,
|
|
7859
8343
|
defineRelationshipField,
|
|
7860
8344
|
defineRichTextField,
|
|
8345
|
+
defineRoles,
|
|
8346
|
+
defineRolesField,
|
|
7861
8347
|
defineRowField,
|
|
7862
8348
|
defineSelectField,
|
|
7863
8349
|
defineTab,
|
|
@@ -7866,6 +8352,8 @@ function defineTab(args) {
|
|
|
7866
8352
|
defineTextField,
|
|
7867
8353
|
defineTextareaField,
|
|
7868
8354
|
defineTimeField,
|
|
8355
|
+
defineUpdatedAtField,
|
|
8356
|
+
defineUpdatedByField,
|
|
7869
8357
|
defineUrlField,
|
|
7870
8358
|
defineView,
|
|
7871
8359
|
defineWorkspace,
|
|
@@ -7905,6 +8393,7 @@ function defineTab(args) {
|
|
|
7905
8393
|
getAdminRoleForCollection,
|
|
7906
8394
|
getAllowedSitesForUser,
|
|
7907
8395
|
getPublicAdminAuthConfig,
|
|
8396
|
+
getTrashEntryId,
|
|
7908
8397
|
includes,
|
|
7909
8398
|
initializeWorkflowDocument,
|
|
7910
8399
|
isAICollection,
|
|
@@ -7948,14 +8437,18 @@ function defineTab(args) {
|
|
|
7948
8437
|
resolveAdminAuthCollection,
|
|
7949
8438
|
resolveAllCollectionViews,
|
|
7950
8439
|
resolveAuthorizedSiteId,
|
|
8440
|
+
resolveDocumentTitle,
|
|
7951
8441
|
resolveIndexName,
|
|
7952
8442
|
resolvePrefix,
|
|
8443
|
+
resolveTrashConfig,
|
|
8444
|
+
restoreDocument,
|
|
7953
8445
|
round,
|
|
7954
8446
|
runCollectionHooks,
|
|
7955
8447
|
sanitizeDocForAI,
|
|
7956
8448
|
saveWorkflowDraft,
|
|
7957
8449
|
simplePublishingWorkflow,
|
|
7958
8450
|
slugify,
|
|
8451
|
+
softDeleteDocument,
|
|
7959
8452
|
sqrt,
|
|
7960
8453
|
startsWith,
|
|
7961
8454
|
sum,
|