@nonext/husk 0.2.3
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/CHANGELOG.md +148 -0
- package/LICENSE +166 -0
- package/README.md +140 -0
- package/api/public-api.md +1070 -0
- package/dist/adapter-C_0mhENy.d.ts +320 -0
- package/dist/admin/admin.css +5 -0
- package/dist/admin/index.d.ts +265 -0
- package/dist/admin/index.js +393 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/tokens.css +597 -0
- package/dist/admin/ui.d.ts +3013 -0
- package/dist/admin/ui.js +9783 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-B2gBtHl5.d.ts +28 -0
- package/dist/can-Chy7zmx7.d.ts +69 -0
- package/dist/chunk-3I6337WJ.js +83 -0
- package/dist/chunk-3I6337WJ.js.map +1 -0
- package/dist/chunk-4Z6P3XCE.js +62 -0
- package/dist/chunk-4Z6P3XCE.js.map +1 -0
- package/dist/chunk-6E3ZS5Z5.js +1007 -0
- package/dist/chunk-6E3ZS5Z5.js.map +1 -0
- package/dist/chunk-77JDRXXU.js +3 -0
- package/dist/chunk-77JDRXXU.js.map +1 -0
- package/dist/chunk-FHYWJCG6.js +171 -0
- package/dist/chunk-FHYWJCG6.js.map +1 -0
- package/dist/chunk-JAE7HHMM.js +506 -0
- package/dist/chunk-JAE7HHMM.js.map +1 -0
- package/dist/chunk-NPDBQBLW.js +68 -0
- package/dist/chunk-NPDBQBLW.js.map +1 -0
- package/dist/chunk-ORMYARXD.js +6 -0
- package/dist/chunk-ORMYARXD.js.map +1 -0
- package/dist/chunk-R4O7APIX.js +51 -0
- package/dist/chunk-R4O7APIX.js.map +1 -0
- package/dist/chunk-ROLKRJWI.js +187 -0
- package/dist/chunk-ROLKRJWI.js.map +1 -0
- package/dist/chunk-SOTQ3WP5.js +31 -0
- package/dist/chunk-SOTQ3WP5.js.map +1 -0
- package/dist/chunk-TCFFHTY3.js +97 -0
- package/dist/chunk-TCFFHTY3.js.map +1 -0
- package/dist/chunk-TS7Y7GEZ.js +7 -0
- package/dist/chunk-TS7Y7GEZ.js.map +1 -0
- package/dist/chunk-VLGR5PZX.js +3 -0
- package/dist/chunk-VLGR5PZX.js.map +1 -0
- package/dist/chunk-ZW2PUSVS.js +40 -0
- package/dist/chunk-ZW2PUSVS.js.map +1 -0
- package/dist/chunk-ZYNBMYJ6.js +392 -0
- package/dist/chunk-ZYNBMYJ6.js.map +1 -0
- package/dist/cli/nonext-husk.js +3360 -0
- package/dist/cli/nonext-husk.js.map +1 -0
- package/dist/cli/templates/firestore.rules +393 -0
- package/dist/cli/templates/storage.rules +165 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cms-9fSA3uQV.d.ts +158 -0
- package/dist/core/index.d.ts +1267 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/entry-D3kiE8T1.d.ts +163 -0
- package/dist/field-Diyf5siF.d.ts +158 -0
- package/dist/field-type-D25Vojb0.d.ts +157 -0
- package/dist/firebase/index.d.ts +251 -0
- package/dist/firebase/index.js +991 -0
- package/dist/firebase/index.js.map +1 -0
- package/dist/index-5ylaMBUH.d.ts +155 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/index.d.ts +101 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/registry-CUC25qay.d.ts +59 -0
- package/dist/richtext/index.d.ts +85 -0
- package/dist/richtext/index.js +127 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/schema/index.d.ts +50 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/write-limits-Djmm6L3R.d.ts +215 -0
- package/package.json +136 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
import { fieldKeyState, slugFieldKey, normalizeLimit, buildRegistry, schemaVersionOf } from './chunk-JAE7HHMM.js';
|
|
2
|
+
import { assertValidContentType } from './chunk-ROLKRJWI.js';
|
|
3
|
+
import { RESERVED_FIELD_KEYS } from './chunk-SOTQ3WP5.js';
|
|
4
|
+
import { can } from './chunk-NPDBQBLW.js';
|
|
5
|
+
import { SchemaError, ValidationError, PermissionError, WriteLimitError } from './chunk-4Z6P3XCE.js';
|
|
6
|
+
|
|
7
|
+
// src/core/cache-tags.ts
|
|
8
|
+
var HUSK_TAG_PREFIX = "husk";
|
|
9
|
+
var MAX_TAG_LENGTH = 256;
|
|
10
|
+
function part(kind, value) {
|
|
11
|
+
if (value === "") {
|
|
12
|
+
throw new Error(`A cache tag needs ${kind}. An empty one would match everything.`);
|
|
13
|
+
}
|
|
14
|
+
if (value.includes(":")) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`The ${kind} in a cache tag may not contain ":", because that is the separator. Received "${value}".`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function tag(segments) {
|
|
22
|
+
const built = [HUSK_TAG_PREFIX, ...segments].join(":");
|
|
23
|
+
if (built.length > MAX_TAG_LENGTH) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`The cache tag "${built}" is ${String(built.length)} characters, over the ${String(MAX_TAG_LENGTH)} character limit. A tag that is rejected invalidates nothing, silently.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return built;
|
|
29
|
+
}
|
|
30
|
+
function contentTypeTag(slug) {
|
|
31
|
+
return tag(["type", part("a content type slug", slug)]);
|
|
32
|
+
}
|
|
33
|
+
function entryTag(slug, entryId) {
|
|
34
|
+
return tag(["entry", part("a content type slug", slug), part("an entry id", entryId)]);
|
|
35
|
+
}
|
|
36
|
+
function entrySlugTag(slug, value) {
|
|
37
|
+
return tag(["slug", part("a content type slug", slug), part("a slug value", value)]);
|
|
38
|
+
}
|
|
39
|
+
function entryTags(slug, entryId) {
|
|
40
|
+
return [contentTypeTag(slug), entryTag(slug, entryId)];
|
|
41
|
+
}
|
|
42
|
+
function mediaTag(mediaId) {
|
|
43
|
+
return tag(["media", part("a media id", mediaId)]);
|
|
44
|
+
}
|
|
45
|
+
function settingsTag(key) {
|
|
46
|
+
return tag(["settings", part("a settings key", key)]);
|
|
47
|
+
}
|
|
48
|
+
function isHuskTag(value) {
|
|
49
|
+
return value.startsWith(`${HUSK_TAG_PREFIX}:`) && value.length <= MAX_TAG_LENGTH;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/core/entry-title.ts
|
|
53
|
+
var UNTITLED_ENTRY = "Untitled";
|
|
54
|
+
function entryTitle(type, entry) {
|
|
55
|
+
const key = type.titleField;
|
|
56
|
+
if (key === void 0) return UNTITLED_ENTRY;
|
|
57
|
+
const value = entry[key];
|
|
58
|
+
if (typeof value !== "string") return UNTITLED_ENTRY;
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
return trimmed === "" ? UNTITLED_ENTRY : trimmed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/core/delete-confirmation.ts
|
|
64
|
+
function deleteConfirmationName(type, entry) {
|
|
65
|
+
const title = entryTitle(type, entry);
|
|
66
|
+
return title === UNTITLED_ENTRY ? entry.id : title;
|
|
67
|
+
}
|
|
68
|
+
function deleteConfirmationPhrase(type, count) {
|
|
69
|
+
const noun = count === 1 ? type.label : type.labelPlural ?? `${type.label}s`;
|
|
70
|
+
return `${String(count)} ${noun.toLowerCase()}`;
|
|
71
|
+
}
|
|
72
|
+
function normalizeConfirmation(value) {
|
|
73
|
+
return value.normalize("NFC").replace(/\s+/gu, " ").trim();
|
|
74
|
+
}
|
|
75
|
+
function confirmationMatches(expected, typed) {
|
|
76
|
+
const target = normalizeConfirmation(expected);
|
|
77
|
+
if (target === "") return false;
|
|
78
|
+
return normalizeConfirmation(typed) === target;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/core/reorder.ts
|
|
82
|
+
function moveItem(items, from, to) {
|
|
83
|
+
const next = [...items];
|
|
84
|
+
if (from === to) return next;
|
|
85
|
+
if (from < 0 || from >= next.length || to < 0 || to >= next.length) return next;
|
|
86
|
+
const [moved] = next.splice(from, 1);
|
|
87
|
+
if (moved === void 0) return [...items];
|
|
88
|
+
next.splice(to, 0, moved);
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/core/schema-editor.ts
|
|
93
|
+
function orderedFieldKeys(type) {
|
|
94
|
+
const declared = Array.isArray(type.fieldOrder) ? type.fieldOrder : [];
|
|
95
|
+
const ordered = [];
|
|
96
|
+
for (const key of declared) {
|
|
97
|
+
if (typeof key === "string" && key in type.fields && !ordered.includes(key)) {
|
|
98
|
+
ordered.push(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const key of Object.keys(type.fields)) {
|
|
102
|
+
if (!ordered.includes(key)) ordered.push(key);
|
|
103
|
+
}
|
|
104
|
+
return ordered;
|
|
105
|
+
}
|
|
106
|
+
function orderedFields(type) {
|
|
107
|
+
const rows = [];
|
|
108
|
+
for (const key of orderedFieldKeys(type)) {
|
|
109
|
+
const field = type.fields[key];
|
|
110
|
+
if (field !== void 0) rows.push({ key, field });
|
|
111
|
+
}
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
function moveFieldRow(rows, from, to) {
|
|
115
|
+
return moveItem(rows, from, to);
|
|
116
|
+
}
|
|
117
|
+
function withFieldRows(type, rows) {
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
const fields = {};
|
|
120
|
+
for (const row of rows) {
|
|
121
|
+
if (seen.has(row.key)) {
|
|
122
|
+
throw new SchemaError(
|
|
123
|
+
`Content type "${type.slug}" would have two fields called "${row.key}".`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
seen.add(row.key);
|
|
127
|
+
fields[row.key] = row.field;
|
|
128
|
+
}
|
|
129
|
+
const next = {
|
|
130
|
+
...type,
|
|
131
|
+
fields,
|
|
132
|
+
fieldOrder: rows.map((row) => row.key)
|
|
133
|
+
};
|
|
134
|
+
assertValidContentType(next);
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
function sameDefinition(left, right) {
|
|
138
|
+
return canonical(left) === canonical(right);
|
|
139
|
+
}
|
|
140
|
+
function canonical(value) {
|
|
141
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
142
|
+
if (typeof value === "object" && value !== null) {
|
|
143
|
+
return `{${Object.entries(value).filter(([, member]) => member !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, member]) => `${JSON.stringify(key)}:${canonical(member)}`).join(",")}}`;
|
|
144
|
+
}
|
|
145
|
+
return JSON.stringify(value) ?? "null";
|
|
146
|
+
}
|
|
147
|
+
var FIELD_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
148
|
+
var KEY_AVAILABLE = { available: true };
|
|
149
|
+
function checkFieldKey(type, key) {
|
|
150
|
+
if (!FIELD_KEY_PATTERN.test(key)) {
|
|
151
|
+
return {
|
|
152
|
+
available: false,
|
|
153
|
+
reason: "invalid",
|
|
154
|
+
message: `"${key}" is not a valid field key. Use letters, digits and underscores, starting with a letter, for example "jobTitle".`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (RESERVED_FIELD_KEYS.includes(key)) {
|
|
158
|
+
return {
|
|
159
|
+
available: false,
|
|
160
|
+
reason: "reserved",
|
|
161
|
+
message: `"${key}" is reserved for entry metadata. Reserved keys: ${RESERVED_FIELD_KEYS.join(", ")}.`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const state = fieldKeyState(type, key);
|
|
165
|
+
if (state === "live") {
|
|
166
|
+
return {
|
|
167
|
+
available: false,
|
|
168
|
+
reason: "live",
|
|
169
|
+
message: `"${key}" is already a field of this content type.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (state === "removed") {
|
|
173
|
+
return {
|
|
174
|
+
available: false,
|
|
175
|
+
reason: "removed",
|
|
176
|
+
message: `"${key}" was removed and its values are still stored. Restore it to get those values back, or purge them first.`
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return KEY_AVAILABLE;
|
|
180
|
+
}
|
|
181
|
+
function checkNestedFieldKey(fields, key) {
|
|
182
|
+
if (!FIELD_KEY_PATTERN.test(key)) {
|
|
183
|
+
return {
|
|
184
|
+
available: false,
|
|
185
|
+
reason: "invalid",
|
|
186
|
+
message: `"${key}" is not a valid field key. Use letters, digits and underscores, starting with a letter.`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return key in fields ? { available: false, reason: "live", message: `"${key}" is already a field here.` } : KEY_AVAILABLE;
|
|
190
|
+
}
|
|
191
|
+
function suggestFieldKey(label) {
|
|
192
|
+
const words = label.normalize("NFKD").replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).filter((word) => word.length > 0);
|
|
193
|
+
const [first, ...rest] = words;
|
|
194
|
+
if (first === void 0) return "";
|
|
195
|
+
const key = first.toLowerCase() + rest.map((word) => `${(word[0] ?? "").toUpperCase()}${word.slice(1).toLowerCase()}`).join("");
|
|
196
|
+
return /^[0-9]/.test(key) ? `f${key}` : key;
|
|
197
|
+
}
|
|
198
|
+
function newFieldDefinition(registry, type, label) {
|
|
199
|
+
const spec = registry.require(type);
|
|
200
|
+
return { ...spec.editor.defaults, type, label };
|
|
201
|
+
}
|
|
202
|
+
function isBlank(value) {
|
|
203
|
+
if (value === void 0 || value === null) return true;
|
|
204
|
+
if (typeof value === "string") return value.trim() === "";
|
|
205
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
206
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
function fieldOptionIssues(registry, field) {
|
|
210
|
+
const spec = registry.get(field.type);
|
|
211
|
+
if (spec === void 0) return [];
|
|
212
|
+
const values = field;
|
|
213
|
+
return spec.editor.options.filter((option) => option.required === true && isBlank(values[option.key])).map((option) => ({
|
|
214
|
+
option: option.key,
|
|
215
|
+
message: `${option.label} is required for a ${field.type} field.`
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
function draftIssues(registry, rows) {
|
|
219
|
+
return rows.flatMap(
|
|
220
|
+
(row) => fieldOptionIssues(registry, row.field).map((issue) => ({ key: row.key, ...issue }))
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
var VALIDATION_RULE_CONTROLS = {
|
|
224
|
+
required: {
|
|
225
|
+
label: "Required",
|
|
226
|
+
control: "boolean",
|
|
227
|
+
help: "An entry cannot be saved while this field is empty."
|
|
228
|
+
},
|
|
229
|
+
minLength: {
|
|
230
|
+
label: "Minimum length",
|
|
231
|
+
control: "number",
|
|
232
|
+
help: "Fewest characters allowed."
|
|
233
|
+
},
|
|
234
|
+
maxLength: {
|
|
235
|
+
label: "Maximum length",
|
|
236
|
+
control: "number",
|
|
237
|
+
help: "Most characters allowed."
|
|
238
|
+
},
|
|
239
|
+
min: { label: "Minimum", control: "number", help: "Smallest value allowed." },
|
|
240
|
+
max: { label: "Maximum", control: "number", help: "Largest value allowed." },
|
|
241
|
+
pattern: {
|
|
242
|
+
label: "Pattern",
|
|
243
|
+
control: "text",
|
|
244
|
+
help: "A regular expression the value must match."
|
|
245
|
+
},
|
|
246
|
+
unique: {
|
|
247
|
+
label: "Unique",
|
|
248
|
+
control: "boolean",
|
|
249
|
+
help: "No other entry of this content type may hold the same value."
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
function withValidationRule(field, rule, value) {
|
|
253
|
+
const { validation, ...rest } = field;
|
|
254
|
+
const next = { ...validation };
|
|
255
|
+
if (value === void 0 || value === "" || value === false) {
|
|
256
|
+
delete next[rule];
|
|
257
|
+
} else {
|
|
258
|
+
next[rule] = value;
|
|
259
|
+
}
|
|
260
|
+
return Object.keys(next).length === 0 ? rest : { ...rest, validation: next };
|
|
261
|
+
}
|
|
262
|
+
function withFieldOption(field, option, value) {
|
|
263
|
+
const next = { ...field };
|
|
264
|
+
if (value === void 0 || value === "") {
|
|
265
|
+
delete next[option];
|
|
266
|
+
} else {
|
|
267
|
+
next[option] = value;
|
|
268
|
+
}
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/core/entry-form.ts
|
|
273
|
+
function formFields(type) {
|
|
274
|
+
return orderedFields(type).filter(({ field }) => field.hidden !== true);
|
|
275
|
+
}
|
|
276
|
+
function initialFormValues(type, registry) {
|
|
277
|
+
const values = {};
|
|
278
|
+
for (const [key, field] of Object.entries(type.fields)) {
|
|
279
|
+
const spec = registry.get(field.type);
|
|
280
|
+
values[key] = spec === void 0 ? null : spec.defaultValue(field);
|
|
281
|
+
}
|
|
282
|
+
return values;
|
|
283
|
+
}
|
|
284
|
+
function formValuesFromEntry(type, entry, registry) {
|
|
285
|
+
const stored = entry;
|
|
286
|
+
const values = {};
|
|
287
|
+
for (const [key, field] of Object.entries(type.fields)) {
|
|
288
|
+
if (key in stored && stored[key] !== void 0) {
|
|
289
|
+
values[key] = stored[key];
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const spec = registry.get(field.type);
|
|
293
|
+
values[key] = spec === void 0 ? null : spec.defaultValue(field);
|
|
294
|
+
}
|
|
295
|
+
return values;
|
|
296
|
+
}
|
|
297
|
+
function errorsUnder(errors, path) {
|
|
298
|
+
return errors.filter(
|
|
299
|
+
(error) => error.path === path || error.path.startsWith(`${path}.`)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
function mergeIssues(client, server) {
|
|
303
|
+
const claimed = new Set(client.map((error) => error.path));
|
|
304
|
+
return [
|
|
305
|
+
...client.map((error) => ({ ...error, origin: "client" })),
|
|
306
|
+
...server.filter((error) => !claimed.has(error.path)).map((error) => ({ ...error, origin: "server" }))
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
function unroutableIssues(issues, type) {
|
|
310
|
+
const rendered = new Set(formFields(type).map((entry) => entry.key));
|
|
311
|
+
return issues.filter((issue) => {
|
|
312
|
+
const root = issue.path.split(".")[0];
|
|
313
|
+
return root === void 0 || root === "" || !rendered.has(root);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function deepEqual(a, b) {
|
|
317
|
+
if (a === b) return true;
|
|
318
|
+
if (a === null || b === null) return false;
|
|
319
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
320
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
321
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
322
|
+
return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
|
|
323
|
+
}
|
|
324
|
+
const left = a;
|
|
325
|
+
const right = b;
|
|
326
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
327
|
+
for (const key of keys) {
|
|
328
|
+
if (!deepEqual(left[key], right[key])) return false;
|
|
329
|
+
}
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
function valuesChanged(baseline, current) {
|
|
333
|
+
return !deepEqual(baseline, current);
|
|
334
|
+
}
|
|
335
|
+
function entryChangedSince(baseline, current) {
|
|
336
|
+
return baseline.updatedAt !== current.updatedAt;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/core/entry-status.ts
|
|
340
|
+
var TRANSITIONS = {
|
|
341
|
+
draft: [
|
|
342
|
+
{
|
|
343
|
+
to: "published",
|
|
344
|
+
label: "Publish",
|
|
345
|
+
effect: "Makes this entry readable on the public site."
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
to: "archived",
|
|
349
|
+
label: "Archive",
|
|
350
|
+
effect: "Keeps the entry but takes it out of the public site."
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
published: [
|
|
354
|
+
{
|
|
355
|
+
to: "draft",
|
|
356
|
+
label: "Unpublish",
|
|
357
|
+
effect: "Takes the entry off the public site and clears its publication date."
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
to: "archived",
|
|
361
|
+
label: "Archive",
|
|
362
|
+
effect: "Keeps the entry but takes it out of the public site."
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
archived: [
|
|
366
|
+
{
|
|
367
|
+
to: "draft",
|
|
368
|
+
label: "Restore as draft",
|
|
369
|
+
effect: "Brings the entry back into editing. It stays off the public site."
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
to: "published",
|
|
373
|
+
label: "Publish",
|
|
374
|
+
effect: "Makes this entry readable on the public site again."
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
var STATUS_EFFECT = {
|
|
379
|
+
draft: "Not visible on the public site.",
|
|
380
|
+
published: "Visible on the public site.",
|
|
381
|
+
archived: "Kept out of the public site and out of the way."
|
|
382
|
+
};
|
|
383
|
+
function statusTransitions(user, type, entry) {
|
|
384
|
+
const from = entry?.status ?? "draft";
|
|
385
|
+
const shapes = entry === null ? TRANSITIONS.draft.filter((shape) => shape.to === "published") : TRANSITIONS[from];
|
|
386
|
+
const allowed = entry === null ? can(user, "create", { type }) : can(user, "update", { type, entry: { createdBy: entry.createdBy } });
|
|
387
|
+
const reason = allowed ? null : denial(user, entry);
|
|
388
|
+
return shapes.map((shape) => ({ ...shape, allowed, reason }));
|
|
389
|
+
}
|
|
390
|
+
function denial(user, entry) {
|
|
391
|
+
if (user.role === "author" && entry !== null && entry.createdBy !== user.uid) {
|
|
392
|
+
return "An author can only change the status of their own entries.";
|
|
393
|
+
}
|
|
394
|
+
return "Your role cannot change the status of this entry.";
|
|
395
|
+
}
|
|
396
|
+
function canSaveEntry(user, type, entry) {
|
|
397
|
+
return entry === null ? can(user, "create", { type }) : can(user, "update", { type, entry: { createdBy: entry.createdBy } });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/core/entry-unique.ts
|
|
401
|
+
function entryUniqueCheck(options) {
|
|
402
|
+
const { adapter, type, entryId } = options;
|
|
403
|
+
const key = slugFieldKey(type);
|
|
404
|
+
return async (path, value) => {
|
|
405
|
+
if (key === null || path !== key) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Uniqueness of "${path}" cannot be checked from the form. The adapter checks it before the write.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const found = await adapter.getBySlug(type.slug, value);
|
|
411
|
+
return found === null || found.id === entryId;
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/core/error-taxonomy.ts
|
|
416
|
+
var ERROR_CATEGORIES = [
|
|
417
|
+
"validation",
|
|
418
|
+
"permission",
|
|
419
|
+
"not-found",
|
|
420
|
+
"conflict",
|
|
421
|
+
"transport",
|
|
422
|
+
"unknown"
|
|
423
|
+
];
|
|
424
|
+
var TREATMENTS = {
|
|
425
|
+
validation: "field",
|
|
426
|
+
permission: "banner",
|
|
427
|
+
"not-found": "state",
|
|
428
|
+
conflict: "resolve",
|
|
429
|
+
transport: "retry",
|
|
430
|
+
unknown: "banner"
|
|
431
|
+
};
|
|
432
|
+
function treatmentFor(category) {
|
|
433
|
+
return TREATMENTS[category];
|
|
434
|
+
}
|
|
435
|
+
var TRANSPORT_CODES = [
|
|
436
|
+
"unavailable",
|
|
437
|
+
"deadline-exceeded",
|
|
438
|
+
"cancelled",
|
|
439
|
+
"internal",
|
|
440
|
+
"resource-exhausted"
|
|
441
|
+
];
|
|
442
|
+
var PERMISSION_CODES = ["permission-denied", "unauthenticated"];
|
|
443
|
+
var NOT_FOUND_CODES = ["not-found"];
|
|
444
|
+
var CONFLICT_CODES = [
|
|
445
|
+
"aborted",
|
|
446
|
+
"already-exists",
|
|
447
|
+
"failed-precondition"
|
|
448
|
+
];
|
|
449
|
+
function codeOf(thrown) {
|
|
450
|
+
if (typeof thrown !== "object" || thrown === null) return void 0;
|
|
451
|
+
const code = thrown.code;
|
|
452
|
+
if (typeof code !== "string") return void 0;
|
|
453
|
+
const slash = code.lastIndexOf("/");
|
|
454
|
+
return slash === -1 ? code : code.slice(slash + 1);
|
|
455
|
+
}
|
|
456
|
+
function looksLikeNetworkFailure(thrown) {
|
|
457
|
+
if (!(thrown instanceof Error)) return false;
|
|
458
|
+
return /network|fetch failed|failed to fetch|offline|connection/i.test(thrown.message);
|
|
459
|
+
}
|
|
460
|
+
function withRole(sentence, role) {
|
|
461
|
+
return role === void 0 ? sentence : `${sentence} You are signed in as ${role}.`;
|
|
462
|
+
}
|
|
463
|
+
function classifyError(thrown, context = {}) {
|
|
464
|
+
const target = context.subject === void 0 ? "" : ` "${context.subject}"`;
|
|
465
|
+
if (thrown instanceof ValidationError) {
|
|
466
|
+
return {
|
|
467
|
+
category: "validation",
|
|
468
|
+
treatment: TREATMENTS.validation,
|
|
469
|
+
title: thrown.errors.length === 1 ? "One field needs attention" : `${String(thrown.errors.length)} fields need attention`,
|
|
470
|
+
message: "The values were refused before anything was saved. Each problem is shown on the field it belongs to.",
|
|
471
|
+
fieldErrors: thrown.errors,
|
|
472
|
+
retryable: false,
|
|
473
|
+
cause: thrown
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (thrown instanceof PermissionError) {
|
|
477
|
+
return {
|
|
478
|
+
category: "permission",
|
|
479
|
+
treatment: TREATMENTS.permission,
|
|
480
|
+
title: "You are not allowed to do that",
|
|
481
|
+
// The brief for this category: say what is denied and by whom. PermissionError
|
|
482
|
+
// already carries the action and the content type; the role comes from the caller.
|
|
483
|
+
message: withRole(thrown.message, context.role),
|
|
484
|
+
fieldErrors: [],
|
|
485
|
+
retryable: false,
|
|
486
|
+
cause: thrown
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (thrown instanceof WriteLimitError) {
|
|
490
|
+
if (thrown.code === "too-fast") {
|
|
491
|
+
return {
|
|
492
|
+
category: "transport",
|
|
493
|
+
treatment: TREATMENTS.transport,
|
|
494
|
+
title: "Too many writes at once",
|
|
495
|
+
message: `${thrown.message} Wait a moment and try again.`,
|
|
496
|
+
fieldErrors: [],
|
|
497
|
+
retryable: true,
|
|
498
|
+
cause: thrown
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
category: "validation",
|
|
503
|
+
treatment: TREATMENTS.validation,
|
|
504
|
+
title: "This is too large to save",
|
|
505
|
+
message: thrown.message,
|
|
506
|
+
fieldErrors: [{ path: thrown.path, rule: thrown.code, message: thrown.message }],
|
|
507
|
+
retryable: false,
|
|
508
|
+
cause: thrown
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
if (thrown instanceof SchemaError) {
|
|
512
|
+
return {
|
|
513
|
+
category: "validation",
|
|
514
|
+
treatment: TREATMENTS.validation,
|
|
515
|
+
title: "This definition was refused",
|
|
516
|
+
message: thrown.message,
|
|
517
|
+
fieldErrors: [],
|
|
518
|
+
retryable: false,
|
|
519
|
+
cause: thrown
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const code = codeOf(thrown);
|
|
523
|
+
if (code !== void 0 && PERMISSION_CODES.includes(code)) {
|
|
524
|
+
return {
|
|
525
|
+
category: "permission",
|
|
526
|
+
treatment: TREATMENTS.permission,
|
|
527
|
+
title: "You are not allowed to do that",
|
|
528
|
+
message: withRole(
|
|
529
|
+
code === "unauthenticated" ? "Your session is no longer valid, so the database refused the request. Sign in again." : `The database refused this request${target}. Your account does not have permission for it.`,
|
|
530
|
+
context.role
|
|
531
|
+
),
|
|
532
|
+
fieldErrors: [],
|
|
533
|
+
retryable: false,
|
|
534
|
+
cause: thrown
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (code !== void 0 && NOT_FOUND_CODES.includes(code)) {
|
|
538
|
+
return {
|
|
539
|
+
category: "not-found",
|
|
540
|
+
treatment: TREATMENTS["not-found"],
|
|
541
|
+
title: "Not found",
|
|
542
|
+
message: `What this page refers to${target} does not exist. It may have been deleted, or the address may be wrong.`,
|
|
543
|
+
fieldErrors: [],
|
|
544
|
+
retryable: false,
|
|
545
|
+
cause: thrown
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (code !== void 0 && CONFLICT_CODES.includes(code)) {
|
|
549
|
+
return {
|
|
550
|
+
category: "conflict",
|
|
551
|
+
treatment: TREATMENTS.conflict,
|
|
552
|
+
title: "This changed somewhere else",
|
|
553
|
+
message: "Someone else wrote to this while you were working on it, so nothing was saved. Reload to see their version, or save again to overwrite it.",
|
|
554
|
+
fieldErrors: [],
|
|
555
|
+
retryable: false,
|
|
556
|
+
cause: thrown
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (code !== void 0 && TRANSPORT_CODES.includes(code) || looksLikeNetworkFailure(thrown)) {
|
|
560
|
+
return {
|
|
561
|
+
category: "transport",
|
|
562
|
+
treatment: TREATMENTS.transport,
|
|
563
|
+
title: "Could not reach the database",
|
|
564
|
+
message: `The request did not get through${context.action === void 0 ? "" : `, so nothing was changed while trying to ${context.action}`}. Check your connection and try again.`,
|
|
565
|
+
fieldErrors: [],
|
|
566
|
+
retryable: true,
|
|
567
|
+
cause: thrown
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
category: "unknown",
|
|
572
|
+
treatment: TREATMENTS.unknown,
|
|
573
|
+
title: context.action === void 0 ? "Something went wrong" : `Could not ${context.action}`,
|
|
574
|
+
message: thrown instanceof Error && thrown.message !== "" ? thrown.message : "No further detail was reported. Nothing is known to have changed.",
|
|
575
|
+
fieldErrors: [],
|
|
576
|
+
retryable: false,
|
|
577
|
+
cause: thrown
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function staleWriteError(subject) {
|
|
581
|
+
return {
|
|
582
|
+
category: "conflict",
|
|
583
|
+
treatment: TREATMENTS.conflict,
|
|
584
|
+
title: "Someone else saved this first",
|
|
585
|
+
message: `${subject} was changed after you opened it. Reload to see their version, or save again to overwrite it with yours.`,
|
|
586
|
+
fieldErrors: [],
|
|
587
|
+
retryable: false,
|
|
588
|
+
cause: null
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function missingError(subject) {
|
|
592
|
+
return {
|
|
593
|
+
category: "not-found",
|
|
594
|
+
treatment: TREATMENTS["not-found"],
|
|
595
|
+
title: `This ${subject} no longer exists`,
|
|
596
|
+
message: "It was deleted, or the address points at something this project does not hold. Nothing was changed.",
|
|
597
|
+
fieldErrors: [],
|
|
598
|
+
retryable: false,
|
|
599
|
+
cause: null
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function deniedError(what, role) {
|
|
603
|
+
return {
|
|
604
|
+
category: "permission",
|
|
605
|
+
treatment: TREATMENTS.permission,
|
|
606
|
+
title: "You are not allowed to do that",
|
|
607
|
+
message: withRole(`Your account may not ${what}.`, role),
|
|
608
|
+
fieldErrors: [],
|
|
609
|
+
retryable: false,
|
|
610
|
+
cause: null
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/core/list-view.ts
|
|
615
|
+
var COLUMN_ALIGN = {
|
|
616
|
+
text: "text",
|
|
617
|
+
textarea: "none",
|
|
618
|
+
number: "numeric",
|
|
619
|
+
boolean: "text",
|
|
620
|
+
select: "text",
|
|
621
|
+
date: "date",
|
|
622
|
+
slug: "text",
|
|
623
|
+
richtext: "none",
|
|
624
|
+
image: "none",
|
|
625
|
+
file: "none",
|
|
626
|
+
relation: "none",
|
|
627
|
+
repeater: "none"
|
|
628
|
+
};
|
|
629
|
+
var FILTERABLE = ["select", "boolean"];
|
|
630
|
+
var FALLBACK_SORT = { field: "updatedAt", direction: "desc" };
|
|
631
|
+
function defaultListSort(type) {
|
|
632
|
+
return type.defaultSort === void 0 ? FALLBACK_SORT : { field: type.defaultSort.field, direction: type.defaultSort.direction };
|
|
633
|
+
}
|
|
634
|
+
function initialListState(type) {
|
|
635
|
+
return {
|
|
636
|
+
search: "",
|
|
637
|
+
status: "any",
|
|
638
|
+
filters: {},
|
|
639
|
+
sort: defaultListSort(type),
|
|
640
|
+
cursor: null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function canSearch(type) {
|
|
644
|
+
return type.titleField !== void 0;
|
|
645
|
+
}
|
|
646
|
+
function isSearching(state) {
|
|
647
|
+
return state.search.trim() !== "";
|
|
648
|
+
}
|
|
649
|
+
function sortableFieldKeys(type) {
|
|
650
|
+
const keys = /* @__PURE__ */ new Set();
|
|
651
|
+
if (type.titleField !== void 0) keys.add(type.titleField);
|
|
652
|
+
if (type.defaultSort !== void 0) keys.add(type.defaultSort.field);
|
|
653
|
+
keys.add("updatedAt");
|
|
654
|
+
return [...keys];
|
|
655
|
+
}
|
|
656
|
+
var MAX_FIELD_COLUMNS = 2;
|
|
657
|
+
function listColumns(type, { maxFieldColumns = MAX_FIELD_COLUMNS } = {}) {
|
|
658
|
+
const sortable = new Set(sortableFieldKeys(type));
|
|
659
|
+
const titleKey = type.titleField;
|
|
660
|
+
const titleField = titleKey === void 0 ? void 0 : type.fields[titleKey];
|
|
661
|
+
const columns = [
|
|
662
|
+
{
|
|
663
|
+
key: titleKey ?? "title",
|
|
664
|
+
label: titleField?.label ?? "Title",
|
|
665
|
+
kind: "title",
|
|
666
|
+
align: "text",
|
|
667
|
+
sortable: titleKey !== void 0 && sortable.has(titleKey)
|
|
668
|
+
}
|
|
669
|
+
];
|
|
670
|
+
for (const { key, field } of orderedFields(type)) {
|
|
671
|
+
if (columns.length > maxFieldColumns) break;
|
|
672
|
+
if (key === titleKey || field.hidden === true) continue;
|
|
673
|
+
const align = COLUMN_ALIGN[field.type];
|
|
674
|
+
if (align === "none") continue;
|
|
675
|
+
columns.push({
|
|
676
|
+
key,
|
|
677
|
+
label: field.label,
|
|
678
|
+
kind: "field",
|
|
679
|
+
align,
|
|
680
|
+
sortable: sortable.has(key)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
columns.push(
|
|
684
|
+
{ key: "status", label: "Status", kind: "status", align: "text", sortable: false },
|
|
685
|
+
{
|
|
686
|
+
key: "updatedAt",
|
|
687
|
+
label: "Updated",
|
|
688
|
+
kind: "updated",
|
|
689
|
+
align: "date",
|
|
690
|
+
sortable: true
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
return columns;
|
|
694
|
+
}
|
|
695
|
+
function isFilterableField(field) {
|
|
696
|
+
return FILTERABLE.includes(field.type);
|
|
697
|
+
}
|
|
698
|
+
function listFilters(type) {
|
|
699
|
+
const descriptors = [];
|
|
700
|
+
for (const { key, field } of orderedFields(type)) {
|
|
701
|
+
if (field.hidden === true || !isFilterableField(field)) continue;
|
|
702
|
+
if (field.type === "select") {
|
|
703
|
+
descriptors.push({
|
|
704
|
+
key,
|
|
705
|
+
label: field.label,
|
|
706
|
+
options: field.options.map((option) => ({
|
|
707
|
+
value: option.value,
|
|
708
|
+
label: option.label
|
|
709
|
+
})),
|
|
710
|
+
// A multi-select stores an array, so equality would only match an entry whose
|
|
711
|
+
// whole array is that one value.
|
|
712
|
+
op: field.multiple === true ? "array-contains" : "==",
|
|
713
|
+
valueType: "string"
|
|
714
|
+
});
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
descriptors.push({
|
|
718
|
+
key,
|
|
719
|
+
label: field.label,
|
|
720
|
+
options: [
|
|
721
|
+
{ value: "true", label: "Yes" },
|
|
722
|
+
{ value: "false", label: "No" }
|
|
723
|
+
],
|
|
724
|
+
op: "==",
|
|
725
|
+
valueType: "boolean"
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return descriptors;
|
|
729
|
+
}
|
|
730
|
+
function listFilterQuery(type, filters) {
|
|
731
|
+
const active = [];
|
|
732
|
+
for (const descriptor of listFilters(type)) {
|
|
733
|
+
const chosen = filters[descriptor.key];
|
|
734
|
+
if (chosen === void 0 || chosen === "") continue;
|
|
735
|
+
if (!descriptor.options.some((option) => option.value === chosen)) continue;
|
|
736
|
+
active.push({
|
|
737
|
+
field: descriptor.key,
|
|
738
|
+
op: descriptor.op,
|
|
739
|
+
value: descriptor.valueType === "boolean" ? chosen === "true" : chosen
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return active;
|
|
743
|
+
}
|
|
744
|
+
function buildListQuery(type, state, { pageSize } = {}) {
|
|
745
|
+
const term = state.search.trim();
|
|
746
|
+
const searching = term !== "" && canSearch(type);
|
|
747
|
+
return {
|
|
748
|
+
filters: listFilterQuery(type, state.filters),
|
|
749
|
+
...searching ? { search: term } : { sort: state.sort },
|
|
750
|
+
limit: normalizeLimit(pageSize),
|
|
751
|
+
cursor: state.cursor,
|
|
752
|
+
status: state.status
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function buildCountQuery(type, state) {
|
|
756
|
+
const term = state.search.trim();
|
|
757
|
+
return {
|
|
758
|
+
filters: listFilterQuery(type, state.filters),
|
|
759
|
+
status: state.status,
|
|
760
|
+
...term !== "" && canSearch(type) ? { search: term } : {}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function isNarrowed(state) {
|
|
764
|
+
return isSearching(state) || state.status !== "any" || Object.values(state.filters).some((value) => value !== "");
|
|
765
|
+
}
|
|
766
|
+
function nextSort(current, column, key = column.key) {
|
|
767
|
+
if (current.field === key) {
|
|
768
|
+
return { field: key, direction: current.direction === "asc" ? "desc" : "asc" };
|
|
769
|
+
}
|
|
770
|
+
return { field: key, direction: column.align === "date" ? "desc" : "asc" };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/core/indexes.ts
|
|
774
|
+
var ENTRY_COLLECTION_GROUP = "entries";
|
|
775
|
+
var MAX_ENTRY_INDEXES = 200;
|
|
776
|
+
function order(direction) {
|
|
777
|
+
return direction === "asc" ? "ASCENDING" : "DESCENDING";
|
|
778
|
+
}
|
|
779
|
+
function subsets(keys) {
|
|
780
|
+
return keys.reduce(
|
|
781
|
+
(found, key) => [...found, ...found.map((subset) => [...subset, key])],
|
|
782
|
+
[[]]
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
function filterableKeys(type) {
|
|
786
|
+
return Object.entries(type.fields).filter(([, field]) => field.hidden !== true && isFilterableField(field)).map(([key]) => key);
|
|
787
|
+
}
|
|
788
|
+
function keyOf(index) {
|
|
789
|
+
return index.fields.map((field) => `${field.fieldPath}:${field.order}`).join("|");
|
|
790
|
+
}
|
|
791
|
+
function requiredEntryIndexes(types) {
|
|
792
|
+
const found = /* @__PURE__ */ new Map();
|
|
793
|
+
for (const type of types) {
|
|
794
|
+
if (type.kind !== "collection") continue;
|
|
795
|
+
for (const filters of subsets(filterableKeys(type))) {
|
|
796
|
+
for (const sortField of sortableFieldKeys(type)) {
|
|
797
|
+
for (const direction of ["asc", "desc"]) {
|
|
798
|
+
for (const withStatus of [true, false]) {
|
|
799
|
+
if (!withStatus && filters.length === 0) continue;
|
|
800
|
+
if (filters.includes(sortField)) continue;
|
|
801
|
+
const index = {
|
|
802
|
+
collectionGroup: ENTRY_COLLECTION_GROUP,
|
|
803
|
+
queryScope: "COLLECTION",
|
|
804
|
+
fields: [
|
|
805
|
+
...withStatus ? [{ fieldPath: "status", order: "ASCENDING" }] : [],
|
|
806
|
+
...filters.map((key) => ({
|
|
807
|
+
fieldPath: key,
|
|
808
|
+
order: "ASCENDING"
|
|
809
|
+
})),
|
|
810
|
+
{ fieldPath: sortField, order: order(direction) },
|
|
811
|
+
{ fieldPath: "__name__", order: order(direction) }
|
|
812
|
+
]
|
|
813
|
+
};
|
|
814
|
+
found.set(keyOf(index), index);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const indexes = [...found.values()].sort(
|
|
821
|
+
(left, right) => keyOf(left).localeCompare(keyOf(right))
|
|
822
|
+
);
|
|
823
|
+
if (indexes.length > MAX_ENTRY_INDEXES) {
|
|
824
|
+
throw new SchemaError(
|
|
825
|
+
`These content types need ${indexes.length} composite indexes on "entries", and Firestore allows ${MAX_ENTRY_INDEXES} per database. Reduce the number of filterable fields or sort fields.`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return indexes;
|
|
829
|
+
}
|
|
830
|
+
function indexCovered(file, required) {
|
|
831
|
+
return (file.indexes ?? []).some(
|
|
832
|
+
(candidate) => candidate.collectionGroup === required.collectionGroup && keyOf(candidate) === keyOf(required)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
function missingEntryIndexes(file, types) {
|
|
836
|
+
return requiredEntryIndexes(types).filter((required) => !indexCovered(file, required));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/core/registry-store.ts
|
|
840
|
+
function fingerprint(types) {
|
|
841
|
+
return JSON.stringify(
|
|
842
|
+
[...types].sort((left, right) => left.slug.localeCompare(right.slug))
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
function createRegistryStore(code, initialDatabase = []) {
|
|
846
|
+
let database = [...initialDatabase];
|
|
847
|
+
let signature = fingerprint(database);
|
|
848
|
+
let registry = buildRegistry(code, database);
|
|
849
|
+
let revision = 0;
|
|
850
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
851
|
+
return {
|
|
852
|
+
get registry() {
|
|
853
|
+
return registry;
|
|
854
|
+
},
|
|
855
|
+
get revision() {
|
|
856
|
+
return revision;
|
|
857
|
+
},
|
|
858
|
+
setDatabaseTypes(types) {
|
|
859
|
+
const next = fingerprint(types);
|
|
860
|
+
if (next === signature) return false;
|
|
861
|
+
database = [...types];
|
|
862
|
+
signature = next;
|
|
863
|
+
registry = buildRegistry(code, database);
|
|
864
|
+
revision += 1;
|
|
865
|
+
for (const listener of listeners) listener(registry);
|
|
866
|
+
return true;
|
|
867
|
+
},
|
|
868
|
+
subscribe(listener) {
|
|
869
|
+
listeners.add(listener);
|
|
870
|
+
return () => {
|
|
871
|
+
listeners.delete(listener);
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/core/migrations.ts
|
|
878
|
+
function createMigrationRegistry(migrations) {
|
|
879
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
880
|
+
for (const migration of migrations) {
|
|
881
|
+
if (migration.to !== migration.from + 1) {
|
|
882
|
+
throw new SchemaError(
|
|
883
|
+
`Migration "${migration.slug}" ${String(migration.from)} to ${String(migration.to)} is not a single step. A migration moves an entry one version forward.`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
if (migration.from < 1) {
|
|
887
|
+
throw new SchemaError(
|
|
888
|
+
`Migration "${migration.slug}" starts at version ${String(migration.from)}. The first stored version is 1.`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const found = bySlug.get(migration.slug) ?? [];
|
|
892
|
+
if (found.some((candidate) => candidate.from === migration.from)) {
|
|
893
|
+
throw new SchemaError(
|
|
894
|
+
`Two migrations of "${migration.slug}" both start at version ${String(migration.from)}.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
found.push(migration);
|
|
898
|
+
bySlug.set(migration.slug, found);
|
|
899
|
+
}
|
|
900
|
+
for (const found of bySlug.values()) found.sort((left, right) => left.from - right.from);
|
|
901
|
+
return {
|
|
902
|
+
forType(slug) {
|
|
903
|
+
return bySlug.get(slug) ?? [];
|
|
904
|
+
},
|
|
905
|
+
plan(type, fromVersion) {
|
|
906
|
+
const target = schemaVersionOf(type);
|
|
907
|
+
if (fromVersion >= target) return [];
|
|
908
|
+
const steps = [];
|
|
909
|
+
const available = bySlug.get(type.slug) ?? [];
|
|
910
|
+
for (let version = fromVersion; version < target; version += 1) {
|
|
911
|
+
const step = available.find((candidate) => candidate.from === version);
|
|
912
|
+
if (step !== void 0) steps.push(step);
|
|
913
|
+
}
|
|
914
|
+
return steps;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function applyPlan(plan, values, context) {
|
|
919
|
+
return plan.reduce(
|
|
920
|
+
(current, step) => step.migrate(current, { ...context, fromVersion: step.from }),
|
|
921
|
+
values
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
var MIGRATION_BATCH_SIZE = 50;
|
|
925
|
+
var MIGRATION_MAX_BATCHES = 20;
|
|
926
|
+
async function runMigration(deps, options = {}) {
|
|
927
|
+
const { migrations, registry, type } = deps;
|
|
928
|
+
const target = schemaVersionOf(type);
|
|
929
|
+
const batchSize = Math.min(
|
|
930
|
+
Math.max(Math.trunc(options.batchSize ?? MIGRATION_BATCH_SIZE), 1),
|
|
931
|
+
MIGRATION_BATCH_SIZE
|
|
932
|
+
);
|
|
933
|
+
const maxBatches = Math.max(Math.trunc(options.maxBatches ?? MIGRATION_MAX_BATCHES), 1);
|
|
934
|
+
const failed = [];
|
|
935
|
+
let scanned = 0;
|
|
936
|
+
let migrated = 0;
|
|
937
|
+
let skipped = 0;
|
|
938
|
+
let remaining = false;
|
|
939
|
+
for (let batch = 0; batch < maxBatches; batch += 1) {
|
|
940
|
+
const page = await migrations.listBehind(type.slug, target, { limit: batchSize });
|
|
941
|
+
if (page.items.length === 0) break;
|
|
942
|
+
let progressed = false;
|
|
943
|
+
for (const record of page.items) {
|
|
944
|
+
scanned += 1;
|
|
945
|
+
const outcome = await migrateOne(migrations, registry, type, target, record);
|
|
946
|
+
if (outcome === "migrated") {
|
|
947
|
+
migrated += 1;
|
|
948
|
+
progressed = true;
|
|
949
|
+
} else if (outcome === "skipped") {
|
|
950
|
+
skipped += 1;
|
|
951
|
+
progressed = true;
|
|
952
|
+
} else {
|
|
953
|
+
failed.push(outcome);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
options.onProgress?.({ scanned, migrated });
|
|
957
|
+
if (!progressed) {
|
|
958
|
+
remaining = true;
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
if (failed.length > 0) break;
|
|
962
|
+
if (page.hasMore && batch === maxBatches - 1) remaining = true;
|
|
963
|
+
}
|
|
964
|
+
if (failed.length > 0) remaining = true;
|
|
965
|
+
return {
|
|
966
|
+
slug: type.slug,
|
|
967
|
+
targetVersion: target,
|
|
968
|
+
scanned,
|
|
969
|
+
migrated,
|
|
970
|
+
skipped,
|
|
971
|
+
failed,
|
|
972
|
+
remaining
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
async function migrateOne(migrations, registry, type, target, record) {
|
|
976
|
+
try {
|
|
977
|
+
const plan = registry.plan(type, record.schemaVersion);
|
|
978
|
+
if (plan.length === 0) {
|
|
979
|
+
const stamped = await migrations.applyMigration(
|
|
980
|
+
type.slug,
|
|
981
|
+
record.id,
|
|
982
|
+
record.values,
|
|
983
|
+
{
|
|
984
|
+
expectedVersion: record.schemaVersion,
|
|
985
|
+
nextVersion: target
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
return stamped ? "migrated" : "skipped";
|
|
989
|
+
}
|
|
990
|
+
const values = applyPlan(plan, record.values, { entryId: record.id, slug: type.slug });
|
|
991
|
+
const written = await migrations.applyMigration(type.slug, record.id, values, {
|
|
992
|
+
expectedVersion: record.schemaVersion,
|
|
993
|
+
nextVersion: target
|
|
994
|
+
});
|
|
995
|
+
return written ? "migrated" : "skipped";
|
|
996
|
+
} catch (thrown) {
|
|
997
|
+
return {
|
|
998
|
+
entryId: record.id,
|
|
999
|
+
fromVersion: record.schemaVersion,
|
|
1000
|
+
reason: thrown instanceof Error ? thrown.message : String(thrown)
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export { ENTRY_COLLECTION_GROUP, ERROR_CATEGORIES, FALLBACK_SORT, HUSK_TAG_PREFIX, MAX_ENTRY_INDEXES, MAX_FIELD_COLUMNS, MIGRATION_BATCH_SIZE, MIGRATION_MAX_BATCHES, STATUS_EFFECT, UNTITLED_ENTRY, VALIDATION_RULE_CONTROLS, applyPlan, buildCountQuery, buildListQuery, canSaveEntry, canSearch, checkFieldKey, checkNestedFieldKey, classifyError, confirmationMatches, contentTypeTag, createMigrationRegistry, createRegistryStore, defaultListSort, deleteConfirmationName, deleteConfirmationPhrase, deniedError, draftIssues, entryChangedSince, entrySlugTag, entryTag, entryTags, entryTitle, entryUniqueCheck, errorsUnder, fieldOptionIssues, formFields, formValuesFromEntry, indexCovered, initialFormValues, initialListState, isFilterableField, isHuskTag, isNarrowed, isSearching, listColumns, listFilterQuery, listFilters, mediaTag, mergeIssues, missingEntryIndexes, missingError, moveFieldRow, moveItem, newFieldDefinition, nextSort, normalizeConfirmation, orderedFieldKeys, orderedFields, requiredEntryIndexes, runMigration, sameDefinition, settingsTag, sortableFieldKeys, staleWriteError, statusTransitions, suggestFieldKey, treatmentFor, unroutableIssues, valuesChanged, withFieldOption, withFieldRows, withValidationRule };
|
|
1006
|
+
//# sourceMappingURL=chunk-6E3ZS5Z5.js.map
|
|
1007
|
+
//# sourceMappingURL=chunk-6E3ZS5Z5.js.map
|