@byline/core 3.17.0 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@types/collection-types.d.ts +3 -3
- package/dist/@types/site-config.d.ts +23 -0
- package/dist/auth/register-collection-abilities.d.ts +1 -1
- package/dist/config/config.js +4 -0
- package/dist/config/validate-collections.d.ts +2 -1
- package/dist/config/validate-collections.js +10 -1
- package/dist/config/validate-collections.test.node.js +22 -0
- package/dist/lib/errors.d.ts +11 -11
- package/dist/patches/apply-patches.js +1 -1
- package/dist/schemas/zod/builder.d.ts +20 -264
- package/dist/schemas/zod/cache.d.ts +2 -2
- package/dist/services/document-lifecycle/audit.d.ts +4 -4
- package/dist/services/document-lifecycle/delete.js +16 -10
- package/dist/services/document-lifecycle.test.node.js +52 -0
- package/dist/utils/storage-utils.d.ts +11 -10
- package/dist/utils/storage-utils.js +30 -11
- package/dist/utils/storage-utils.test.node.d.ts +8 -0
- package/dist/utils/storage-utils.test.node.js +90 -0
- package/dist/validation/shared.d.ts +3 -3
- package/package.json +9 -9
|
@@ -141,9 +141,9 @@ export interface UploadConfig {
|
|
|
141
141
|
*
|
|
142
142
|
* `[draft, ...customStatuses, published, archived]`
|
|
143
143
|
*/
|
|
144
|
-
export declare const WORKFLOW_STATUS_DRAFT:
|
|
145
|
-
export declare const WORKFLOW_STATUS_PUBLISHED:
|
|
146
|
-
export declare const WORKFLOW_STATUS_ARCHIVED:
|
|
144
|
+
export declare const WORKFLOW_STATUS_DRAFT: 'draft';
|
|
145
|
+
export declare const WORKFLOW_STATUS_PUBLISHED: 'published';
|
|
146
|
+
export declare const WORKFLOW_STATUS_ARCHIVED: 'archived';
|
|
147
147
|
export declare const REQUIRED_WORKFLOW_STATUSES: readonly ["draft", "published", "archived"];
|
|
148
148
|
export type RequiredWorkflowStatusName = (typeof REQUIRED_WORKFLOW_STATUSES)[number];
|
|
149
149
|
/**
|
|
@@ -144,6 +144,24 @@ export type TranslationBundleShape = Readonly<{
|
|
|
144
144
|
export interface ClientConfig extends BaseConfig {
|
|
145
145
|
/** Admin UI configuration for collections (client-side only). */
|
|
146
146
|
admin?: CollectionAdminConfig[];
|
|
147
|
+
/**
|
|
148
|
+
* Installation-wide slugifier — the **client-side** copy, used by the
|
|
149
|
+
* admin path-widget to render the live `path` preview (create-mode
|
|
150
|
+
* placeholder and the "Regenerate" target) as the editor types.
|
|
151
|
+
*
|
|
152
|
+
* Must be the *same* pure, synchronous function registered on
|
|
153
|
+
* `ServerConfig.slugifier`: the server derivation is authoritative, and a
|
|
154
|
+
* divergent client copy would show a preview that disagrees with what is
|
|
155
|
+
* persisted (and could let "Regenerate" overwrite a correct path). Falls
|
|
156
|
+
* back to the default `slugify` from `@byline/core` when not set — so
|
|
157
|
+
* installations that keep the default slugifier need not set this at all.
|
|
158
|
+
*
|
|
159
|
+
* Lives on `ClientConfig` rather than `BaseConfig` because it is a function
|
|
160
|
+
* (not serialisable), and `BaseConfig` is contractually serialisable.
|
|
161
|
+
*
|
|
162
|
+
* @see ServerConfig.slugifier
|
|
163
|
+
*/
|
|
164
|
+
slugifier?: SlugifierFn;
|
|
147
165
|
/**
|
|
148
166
|
* Site-wide field-level UI defaults. Currently surfaces the richtext
|
|
149
167
|
* editor adapter slot — additional field-level defaults (custom
|
|
@@ -214,6 +232,11 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
214
232
|
* Falls back to the default `slugify` from `@byline/core` when not set.
|
|
215
233
|
* Must be pure and synchronous — it runs server-side at write time and
|
|
216
234
|
* client-side for live form preview, and the two must agree on output.
|
|
235
|
+
*
|
|
236
|
+
* This is the **server-side** copy (the authoritative one — its output is
|
|
237
|
+
* what gets persisted). For the admin path-widget's live preview to match,
|
|
238
|
+
* register the *same* function on `ClientConfig.slugifier` via
|
|
239
|
+
* `defineClientConfig`. See {@link ClientConfig.slugifier}.
|
|
217
240
|
*/
|
|
218
241
|
slugifier?: SlugifierFn;
|
|
219
242
|
/**
|
|
@@ -36,7 +36,7 @@ import type { CollectionDefinition } from '../@types/index.js';
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function registerCollectionAbilities(registry: AbilityRegistry, definition: CollectionDefinition): void;
|
|
38
38
|
/** The ability suffixes that every collection contributes. Exposed for contract tests. */
|
|
39
|
-
export declare const COLLECTION_ABILITY_VERBS: readonly [
|
|
39
|
+
export declare const COLLECTION_ABILITY_VERBS: readonly ['read', 'create', 'update', 'delete', 'publish', 'changeStatus', 'reindex'];
|
|
40
40
|
export type CollectionAbilityVerb = (typeof COLLECTION_ABILITY_VERBS)[number];
|
|
41
41
|
/** Compute the full ability key for a collection path and verb. */
|
|
42
42
|
export declare function collectionAbilityKey(path: string, verb: CollectionAbilityVerb): string;
|
package/dist/config/config.js
CHANGED
|
@@ -90,6 +90,10 @@ export function getClientConfig() {
|
|
|
90
90
|
routes: serverConfig.routes,
|
|
91
91
|
collections: serverConfig.collections,
|
|
92
92
|
admin: [],
|
|
93
|
+
// Carry the slugifier through the SSR fallback so a form rendered
|
|
94
|
+
// server-side derives the same path preview as the hydrated client
|
|
95
|
+
// (both configs are meant to register the same function).
|
|
96
|
+
slugifier: serverConfig.slugifier,
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
throw new Error('Byline has not been configured yet. Please call defineClientConfig in byline.config.ts first.');
|
|
@@ -23,7 +23,8 @@ export declare const RESERVED_FIELD_NAMES: ReadonlySet<string>;
|
|
|
23
23
|
* into derived paths via `useAsPath`.
|
|
24
24
|
* - When `useAsPath` is set, the referenced field must exist at the
|
|
25
25
|
* top level of the collection and be of a type the slugifier can
|
|
26
|
-
* sensibly consume (text-like
|
|
26
|
+
* sensibly consume (text-like, date-like, or a numeric identity
|
|
27
|
+
* field — `integer` / `counter`).
|
|
27
28
|
* - No field may be named `availableLocales`; collections opt into the
|
|
28
29
|
* editorial available-locales control via `advertiseLocales: true`.
|
|
29
30
|
* - When `advertiseLocales` is `true`, the collection must have at least
|
|
@@ -28,6 +28,14 @@ const USE_AS_PATH_SOURCE_TYPES = new Set([
|
|
|
28
28
|
'date',
|
|
29
29
|
'datetime',
|
|
30
30
|
'time',
|
|
31
|
+
// Numeric identity fields. `derivePath` stringifies the value before
|
|
32
|
+
// slugifying, so an integer or an allocator-assigned `counter` becomes a
|
|
33
|
+
// clean numeric slug (e.g. `1`, `42`). A replacement slugifier can branch
|
|
34
|
+
// on `collectionPath` to reshape it further — e.g. zero-padding a serial
|
|
35
|
+
// number to a fixed width. `float` / `decimal` are deliberately excluded:
|
|
36
|
+
// their string form carries a `.` which does not belong in a path segment.
|
|
37
|
+
'integer',
|
|
38
|
+
'counter',
|
|
31
39
|
]);
|
|
32
40
|
/**
|
|
33
41
|
* True when any field in the tree (at any nesting depth) is `localized`.
|
|
@@ -65,7 +73,8 @@ function walkFields(fields, visit) {
|
|
|
65
73
|
* into derived paths via `useAsPath`.
|
|
66
74
|
* - When `useAsPath` is set, the referenced field must exist at the
|
|
67
75
|
* top level of the collection and be of a type the slugifier can
|
|
68
|
-
* sensibly consume (text-like
|
|
76
|
+
* sensibly consume (text-like, date-like, or a numeric identity
|
|
77
|
+
* field — `integer` / `counter`).
|
|
69
78
|
* - No field may be named `availableLocales`; collections opt into the
|
|
70
79
|
* editorial available-locales control via `advertiseLocales: true`.
|
|
71
80
|
* - When `advertiseLocales` is `true`, the collection must have at least
|
|
@@ -87,6 +87,28 @@ describe('validateCollections', () => {
|
|
|
87
87
|
};
|
|
88
88
|
expect(() => validateCollections([collection])).not.toThrow();
|
|
89
89
|
});
|
|
90
|
+
it('accepts useAsPath pointing at a counter field', () => {
|
|
91
|
+
const collection = {
|
|
92
|
+
...baseCollection,
|
|
93
|
+
fields: [
|
|
94
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
95
|
+
{ name: 'serialNumber', label: 'Serial Number', type: 'counter', group: 'serials' },
|
|
96
|
+
],
|
|
97
|
+
useAsPath: 'serialNumber',
|
|
98
|
+
};
|
|
99
|
+
expect(() => validateCollections([collection])).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
it('accepts useAsPath pointing at an integer field', () => {
|
|
102
|
+
const collection = {
|
|
103
|
+
...baseCollection,
|
|
104
|
+
fields: [
|
|
105
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
106
|
+
{ name: 'issue', label: 'Issue', type: 'integer' },
|
|
107
|
+
],
|
|
108
|
+
useAsPath: 'issue',
|
|
109
|
+
};
|
|
110
|
+
expect(() => validateCollections([collection])).not.toThrow();
|
|
111
|
+
});
|
|
90
112
|
// useAsPath deliberately resolves against top-level fields only. A
|
|
91
113
|
// nested source (inside a group, array, or block) isn't addressable
|
|
92
114
|
// in the derivation cascade — path is a singular identity anchor, not
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -76,17 +76,17 @@ export declare class BylineError extends Error {
|
|
|
76
76
|
*/
|
|
77
77
|
export declare const createErrorType: (code: string, logLevel?: LogLevel) => (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
78
78
|
export declare const ErrorCodes: {
|
|
79
|
-
readonly UNHANDLED:
|
|
80
|
-
readonly NOT_FOUND:
|
|
81
|
-
readonly CONFLICT:
|
|
82
|
-
readonly VALIDATION:
|
|
83
|
-
readonly INVALID_TRANSITION:
|
|
84
|
-
readonly PATCH_FAILED:
|
|
85
|
-
readonly DATABASE:
|
|
86
|
-
readonly STORAGE:
|
|
87
|
-
readonly READ_BUDGET_EXCEEDED:
|
|
88
|
-
readonly PATH_CONFLICT:
|
|
89
|
-
readonly AUDIT_UNSUPPORTED:
|
|
79
|
+
readonly UNHANDLED: 'ERR_UNHANDLED';
|
|
80
|
+
readonly NOT_FOUND: 'ERR_NOT_FOUND';
|
|
81
|
+
readonly CONFLICT: 'ERR_CONFLICT';
|
|
82
|
+
readonly VALIDATION: 'ERR_VALIDATION';
|
|
83
|
+
readonly INVALID_TRANSITION: 'ERR_INVALID_TRANSITION';
|
|
84
|
+
readonly PATCH_FAILED: 'ERR_PATCH_FAILED';
|
|
85
|
+
readonly DATABASE: 'ERR_DATABASE';
|
|
86
|
+
readonly STORAGE: 'ERR_STORAGE';
|
|
87
|
+
readonly READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED';
|
|
88
|
+
readonly PATH_CONFLICT: 'ERR_PATH_CONFLICT';
|
|
89
|
+
readonly AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED';
|
|
90
90
|
};
|
|
91
91
|
export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
92
92
|
export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
@@ -46,7 +46,7 @@ export function resolveFieldForPath(definition, path) {
|
|
|
46
46
|
if (segments.length === 0)
|
|
47
47
|
return null;
|
|
48
48
|
const [first, ...rest] = segments;
|
|
49
|
-
if (
|
|
49
|
+
if (first?.kind !== 'field')
|
|
50
50
|
return null;
|
|
51
51
|
let current = definition.fields.find((f) => f.name === first.key);
|
|
52
52
|
if (!current)
|
|
@@ -53,9 +53,6 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
53
53
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
54
54
|
}, z.core.$strip>;
|
|
55
55
|
full: z.ZodObject<{
|
|
56
|
-
fields: z.ZodObject<{
|
|
57
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
58
|
-
}, z.core.$strip>;
|
|
59
56
|
id: z.ZodUUID;
|
|
60
57
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
61
58
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -68,12 +65,12 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
68
65
|
updatedAt: z.ZodISODateTime;
|
|
69
66
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
70
67
|
eventType: z.ZodOptional<z.ZodString>;
|
|
68
|
+
fields: z.ZodObject<{
|
|
69
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
71
|
}, z.core.$strip>;
|
|
72
72
|
list: z.ZodObject<{
|
|
73
73
|
docs: z.ZodArray<z.ZodObject<{
|
|
74
|
-
fields: z.ZodObject<{
|
|
75
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
76
|
-
}, z.core.$strip>;
|
|
77
74
|
id: z.ZodUUID;
|
|
78
75
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
79
76
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -86,6 +83,9 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
86
83
|
updatedAt: z.ZodISODateTime;
|
|
87
84
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
88
85
|
eventType: z.ZodOptional<z.ZodString>;
|
|
86
|
+
fields: z.ZodObject<{
|
|
87
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
88
|
+
}, z.core.$strip>;
|
|
89
89
|
}, z.core.$strip>>;
|
|
90
90
|
meta: z.ZodObject<{
|
|
91
91
|
page: z.ZodNumber;
|
|
@@ -108,9 +108,6 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
108
108
|
}, z.core.$strip>;
|
|
109
109
|
history: z.ZodObject<{
|
|
110
110
|
docs: z.ZodArray<z.ZodObject<{
|
|
111
|
-
fields: z.ZodObject<{
|
|
112
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
113
|
-
}, z.core.$strip>;
|
|
114
111
|
id: z.ZodUUID;
|
|
115
112
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
116
113
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -123,6 +120,9 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
123
120
|
updatedAt: z.ZodISODateTime;
|
|
124
121
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
125
122
|
eventType: z.ZodOptional<z.ZodString>;
|
|
123
|
+
fields: z.ZodObject<{
|
|
124
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
125
|
+
}, z.core.$strip>;
|
|
126
126
|
}, z.core.$strip>>;
|
|
127
127
|
meta: z.ZodObject<{
|
|
128
128
|
page: z.ZodNumber;
|
|
@@ -137,9 +137,6 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
137
137
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
138
138
|
}, z.core.$strip>;
|
|
139
139
|
get: z.ZodObject<{
|
|
140
|
-
fields: z.ZodObject<{
|
|
141
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
142
|
-
}, z.core.$strip>;
|
|
143
140
|
id: z.ZodUUID;
|
|
144
141
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
145
142
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -152,6 +149,9 @@ export declare const createCollectionSchemasForPath: (path: string) => {
|
|
|
152
149
|
updatedAt: z.ZodISODateTime;
|
|
153
150
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
154
151
|
eventType: z.ZodOptional<z.ZodString>;
|
|
152
|
+
fields: z.ZodObject<{
|
|
153
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
154
|
+
}, z.core.$strip>;
|
|
155
155
|
}, z.core.$strip>;
|
|
156
156
|
update: z.ZodObject<{
|
|
157
157
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
@@ -176,93 +176,6 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
176
176
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
177
177
|
}, z.core.$strip>;
|
|
178
178
|
full: z.ZodObject<{
|
|
179
|
-
fields: z.ZodObject<{
|
|
180
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
181
|
-
}, z.core.$strip>;
|
|
182
|
-
id: z.ZodUUID;
|
|
183
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
184
|
-
path: z.ZodOptional<z.ZodString>;
|
|
185
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
186
|
-
status: z.ZodEnum<{
|
|
187
|
-
[x: string]: string;
|
|
188
|
-
}>;
|
|
189
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
190
|
-
createdAt: z.ZodISODateTime;
|
|
191
|
-
updatedAt: z.ZodISODateTime;
|
|
192
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
193
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
194
|
-
}, z.core.$strip>;
|
|
195
|
-
list: z.ZodObject<{
|
|
196
|
-
docs: z.ZodArray<z.ZodObject<{
|
|
197
|
-
fields: z.ZodObject<{
|
|
198
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
199
|
-
}, z.core.$strip>;
|
|
200
|
-
id: z.ZodUUID;
|
|
201
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
202
|
-
path: z.ZodOptional<z.ZodString>;
|
|
203
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
204
|
-
status: z.ZodEnum<{
|
|
205
|
-
[x: string]: string;
|
|
206
|
-
}>;
|
|
207
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
208
|
-
createdAt: z.ZodISODateTime;
|
|
209
|
-
updatedAt: z.ZodISODateTime;
|
|
210
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
211
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
212
|
-
}, z.core.$strip>>;
|
|
213
|
-
meta: z.ZodObject<{
|
|
214
|
-
page: z.ZodNumber;
|
|
215
|
-
pageSize: z.ZodNumber;
|
|
216
|
-
total: z.ZodNumber;
|
|
217
|
-
totalPages: z.ZodNumber;
|
|
218
|
-
order: z.ZodOptional<z.ZodString>;
|
|
219
|
-
desc: z.ZodOptional<z.ZodBoolean>;
|
|
220
|
-
}, z.core.$strip>;
|
|
221
|
-
included: z.ZodObject<{
|
|
222
|
-
collection: z.ZodObject<{
|
|
223
|
-
id: z.ZodString;
|
|
224
|
-
labels: z.ZodObject<{
|
|
225
|
-
singular: z.ZodString;
|
|
226
|
-
plural: z.ZodString;
|
|
227
|
-
}, z.core.$strip>;
|
|
228
|
-
path: z.ZodLiteral<string>;
|
|
229
|
-
}, z.core.$strip>;
|
|
230
|
-
}, z.core.$strip>;
|
|
231
|
-
}, z.core.$strip>;
|
|
232
|
-
history: z.ZodObject<{
|
|
233
|
-
docs: z.ZodArray<z.ZodObject<{
|
|
234
|
-
fields: z.ZodObject<{
|
|
235
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
236
|
-
}, z.core.$strip>;
|
|
237
|
-
id: z.ZodUUID;
|
|
238
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
239
|
-
path: z.ZodOptional<z.ZodString>;
|
|
240
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
241
|
-
status: z.ZodEnum<{
|
|
242
|
-
[x: string]: string;
|
|
243
|
-
}>;
|
|
244
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
245
|
-
createdAt: z.ZodISODateTime;
|
|
246
|
-
updatedAt: z.ZodISODateTime;
|
|
247
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
248
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
249
|
-
}, z.core.$strip>>;
|
|
250
|
-
meta: z.ZodObject<{
|
|
251
|
-
page: z.ZodNumber;
|
|
252
|
-
pageSize: z.ZodNumber;
|
|
253
|
-
total: z.ZodNumber;
|
|
254
|
-
totalPages: z.ZodNumber;
|
|
255
|
-
order: z.ZodOptional<z.ZodString>;
|
|
256
|
-
desc: z.ZodOptional<z.ZodBoolean>;
|
|
257
|
-
}, z.core.$strip>;
|
|
258
|
-
}, z.core.$strip>;
|
|
259
|
-
create: z.ZodObject<{
|
|
260
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
261
|
-
}, z.core.$strip>;
|
|
262
|
-
get: z.ZodObject<{
|
|
263
|
-
fields: z.ZodObject<{
|
|
264
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
265
|
-
}, z.core.$strip>;
|
|
266
179
|
id: z.ZodUUID;
|
|
267
180
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
268
181
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -275,88 +188,12 @@ export declare const createCollectionSchemas: (collection: CollectionDefinition)
|
|
|
275
188
|
updatedAt: z.ZodISODateTime;
|
|
276
189
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
277
190
|
eventType: z.ZodOptional<z.ZodString>;
|
|
278
|
-
}, z.core.$strip>;
|
|
279
|
-
update: z.ZodObject<{
|
|
280
|
-
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
281
|
-
}, z.core.$strip>;
|
|
282
|
-
};
|
|
283
|
-
export declare const createTypedCollectionSchemas: (collection: CollectionDefinition) => {
|
|
284
|
-
base: z.ZodObject<{
|
|
285
|
-
id: z.ZodUUID;
|
|
286
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
287
|
-
path: z.ZodOptional<z.ZodString>;
|
|
288
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
289
|
-
status: z.ZodEnum<{
|
|
290
|
-
[x: string]: string;
|
|
291
|
-
}>;
|
|
292
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
293
|
-
createdAt: z.ZodISODateTime;
|
|
294
|
-
updatedAt: z.ZodISODateTime;
|
|
295
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
296
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
297
|
-
}, z.core.$strip>;
|
|
298
|
-
fields: z.ZodObject<{
|
|
299
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
300
|
-
}, z.core.$strip>;
|
|
301
|
-
full: z.ZodObject<{
|
|
302
191
|
fields: z.ZodObject<{
|
|
303
192
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
304
193
|
}, z.core.$strip>;
|
|
305
|
-
id: z.ZodUUID;
|
|
306
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
307
|
-
path: z.ZodOptional<z.ZodString>;
|
|
308
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
309
|
-
status: z.ZodEnum<{
|
|
310
|
-
[x: string]: string;
|
|
311
|
-
}>;
|
|
312
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
313
|
-
createdAt: z.ZodISODateTime;
|
|
314
|
-
updatedAt: z.ZodISODateTime;
|
|
315
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
316
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
317
194
|
}, z.core.$strip>;
|
|
318
195
|
list: z.ZodObject<{
|
|
319
196
|
docs: z.ZodArray<z.ZodObject<{
|
|
320
|
-
fields: z.ZodObject<{
|
|
321
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
322
|
-
}, z.core.$strip>;
|
|
323
|
-
id: z.ZodUUID;
|
|
324
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
325
|
-
path: z.ZodOptional<z.ZodString>;
|
|
326
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
327
|
-
status: z.ZodEnum<{
|
|
328
|
-
[x: string]: string;
|
|
329
|
-
}>;
|
|
330
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
331
|
-
createdAt: z.ZodISODateTime;
|
|
332
|
-
updatedAt: z.ZodISODateTime;
|
|
333
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
334
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
335
|
-
}, z.core.$strip>>;
|
|
336
|
-
meta: z.ZodObject<{
|
|
337
|
-
page: z.ZodNumber;
|
|
338
|
-
pageSize: z.ZodNumber;
|
|
339
|
-
total: z.ZodNumber;
|
|
340
|
-
totalPages: z.ZodNumber;
|
|
341
|
-
order: z.ZodOptional<z.ZodString>;
|
|
342
|
-
desc: z.ZodOptional<z.ZodBoolean>;
|
|
343
|
-
}, z.core.$strip>;
|
|
344
|
-
included: z.ZodObject<{
|
|
345
|
-
collection: z.ZodObject<{
|
|
346
|
-
id: z.ZodString;
|
|
347
|
-
labels: z.ZodObject<{
|
|
348
|
-
singular: z.ZodString;
|
|
349
|
-
plural: z.ZodString;
|
|
350
|
-
}, z.core.$strip>;
|
|
351
|
-
path: z.ZodLiteral<string>;
|
|
352
|
-
}, z.core.$strip>;
|
|
353
|
-
}, z.core.$strip>;
|
|
354
|
-
}, z.core.$strip>;
|
|
355
|
-
history: z.ZodObject<{
|
|
356
|
-
docs: z.ZodArray<z.ZodObject<{
|
|
357
|
-
fields: z.ZodObject<{
|
|
358
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
359
|
-
}, z.core.$strip>;
|
|
360
197
|
id: z.ZodUUID;
|
|
361
198
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
362
199
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -369,92 +206,9 @@ export declare const createTypedCollectionSchemas: (collection: CollectionDefini
|
|
|
369
206
|
updatedAt: z.ZodISODateTime;
|
|
370
207
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
371
208
|
eventType: z.ZodOptional<z.ZodString>;
|
|
372
|
-
}, z.core.$strip>>;
|
|
373
|
-
meta: z.ZodObject<{
|
|
374
|
-
page: z.ZodNumber;
|
|
375
|
-
pageSize: z.ZodNumber;
|
|
376
|
-
total: z.ZodNumber;
|
|
377
|
-
totalPages: z.ZodNumber;
|
|
378
|
-
order: z.ZodOptional<z.ZodString>;
|
|
379
|
-
desc: z.ZodOptional<z.ZodBoolean>;
|
|
380
|
-
}, z.core.$strip>;
|
|
381
|
-
}, z.core.$strip>;
|
|
382
|
-
create: z.ZodObject<{
|
|
383
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
384
|
-
}, z.core.$strip>;
|
|
385
|
-
get: z.ZodObject<{
|
|
386
|
-
fields: z.ZodObject<{
|
|
387
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
388
|
-
}, z.core.$strip>;
|
|
389
|
-
id: z.ZodUUID;
|
|
390
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
391
|
-
path: z.ZodOptional<z.ZodString>;
|
|
392
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
393
|
-
status: z.ZodEnum<{
|
|
394
|
-
[x: string]: string;
|
|
395
|
-
}>;
|
|
396
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
397
|
-
createdAt: z.ZodISODateTime;
|
|
398
|
-
updatedAt: z.ZodISODateTime;
|
|
399
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
400
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
401
|
-
}, z.core.$strip>;
|
|
402
|
-
update: z.ZodObject<{
|
|
403
|
-
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
404
|
-
}, z.core.$strip>;
|
|
405
|
-
};
|
|
406
|
-
export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
407
|
-
base: z.ZodObject<{
|
|
408
|
-
id: z.ZodUUID;
|
|
409
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
410
|
-
path: z.ZodOptional<z.ZodString>;
|
|
411
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
412
|
-
status: z.ZodEnum<{
|
|
413
|
-
[x: string]: string;
|
|
414
|
-
}>;
|
|
415
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
416
|
-
createdAt: z.ZodISODateTime;
|
|
417
|
-
updatedAt: z.ZodISODateTime;
|
|
418
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
419
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
420
|
-
}, z.core.$strip>;
|
|
421
|
-
fields: z.ZodObject<{
|
|
422
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
423
|
-
}, z.core.$strip>;
|
|
424
|
-
full: z.ZodObject<{
|
|
425
|
-
fields: z.ZodObject<{
|
|
426
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
427
|
-
}, z.core.$strip>;
|
|
428
|
-
id: z.ZodUUID;
|
|
429
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
430
|
-
path: z.ZodOptional<z.ZodString>;
|
|
431
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
432
|
-
status: z.ZodEnum<{
|
|
433
|
-
[x: string]: string;
|
|
434
|
-
}>;
|
|
435
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
436
|
-
createdAt: z.ZodISODateTime;
|
|
437
|
-
updatedAt: z.ZodISODateTime;
|
|
438
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
439
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
440
|
-
}, z.core.$strip>;
|
|
441
|
-
list: z.ZodObject<{
|
|
442
|
-
docs: z.ZodArray<z.ZodObject<{
|
|
443
209
|
fields: z.ZodObject<{
|
|
444
210
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
445
211
|
}, z.core.$strip>;
|
|
446
|
-
id: z.ZodUUID;
|
|
447
|
-
versionId: z.ZodOptional<z.ZodUUID>;
|
|
448
|
-
path: z.ZodOptional<z.ZodString>;
|
|
449
|
-
sourceLocale: z.ZodOptional<z.ZodString>;
|
|
450
|
-
status: z.ZodEnum<{
|
|
451
|
-
[x: string]: string;
|
|
452
|
-
}>;
|
|
453
|
-
hasPublishedVersion: z.ZodOptional<z.ZodBoolean>;
|
|
454
|
-
createdAt: z.ZodISODateTime;
|
|
455
|
-
updatedAt: z.ZodISODateTime;
|
|
456
|
-
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
457
|
-
eventType: z.ZodOptional<z.ZodString>;
|
|
458
212
|
}, z.core.$strip>>;
|
|
459
213
|
meta: z.ZodObject<{
|
|
460
214
|
page: z.ZodNumber;
|
|
@@ -477,9 +231,6 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
477
231
|
}, z.core.$strip>;
|
|
478
232
|
history: z.ZodObject<{
|
|
479
233
|
docs: z.ZodArray<z.ZodObject<{
|
|
480
|
-
fields: z.ZodObject<{
|
|
481
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
482
|
-
}, z.core.$strip>;
|
|
483
234
|
id: z.ZodUUID;
|
|
484
235
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
485
236
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -492,6 +243,9 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
492
243
|
updatedAt: z.ZodISODateTime;
|
|
493
244
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
494
245
|
eventType: z.ZodOptional<z.ZodString>;
|
|
246
|
+
fields: z.ZodObject<{
|
|
247
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
248
|
+
}, z.core.$strip>;
|
|
495
249
|
}, z.core.$strip>>;
|
|
496
250
|
meta: z.ZodObject<{
|
|
497
251
|
page: z.ZodNumber;
|
|
@@ -506,9 +260,6 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
506
260
|
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
507
261
|
}, z.core.$strip>;
|
|
508
262
|
get: z.ZodObject<{
|
|
509
|
-
fields: z.ZodObject<{
|
|
510
|
-
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
511
|
-
}, z.core.$strip>;
|
|
512
263
|
id: z.ZodUUID;
|
|
513
264
|
versionId: z.ZodOptional<z.ZodUUID>;
|
|
514
265
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -521,8 +272,13 @@ export declare const createTypedCollectionSchemasForPath: (path: string) => {
|
|
|
521
272
|
updatedAt: z.ZodISODateTime;
|
|
522
273
|
createdBy: z.ZodOptional<z.ZodUUID>;
|
|
523
274
|
eventType: z.ZodOptional<z.ZodString>;
|
|
275
|
+
fields: z.ZodObject<{
|
|
276
|
+
[x: string]: z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>;
|
|
277
|
+
}, z.core.$strip>;
|
|
524
278
|
}, z.core.$strip>;
|
|
525
279
|
update: z.ZodObject<{
|
|
526
280
|
[x: string]: z.ZodOptional<z.ZodType<any, unknown, z.core.$ZodTypeInternals<any, unknown>>>;
|
|
527
281
|
}, z.core.$strip>;
|
|
528
282
|
};
|
|
283
|
+
export declare const createTypedCollectionSchemas: typeof createCollectionSchemas;
|
|
284
|
+
export declare const createTypedCollectionSchemasForPath: typeof createCollectionSchemasForPath;
|
|
@@ -3,8 +3,8 @@ import type { CollectionDefinition } from '../../@types/index.js';
|
|
|
3
3
|
type TypedSchemaSet = ReturnType<typeof createTypedCollectionSchemas>;
|
|
4
4
|
export declare const getCollectionSchemasForPath: (path: string) => TypedSchemaSet;
|
|
5
5
|
export declare const getCollectionSchemas: (collection: CollectionDefinition) => TypedSchemaSet;
|
|
6
|
-
export declare const getTypedCollectionSchemasForPath:
|
|
7
|
-
export declare const getTypedCollectionSchemas:
|
|
6
|
+
export declare const getTypedCollectionSchemasForPath: typeof getCollectionSchemasForPath;
|
|
7
|
+
export declare const getTypedCollectionSchemas: typeof getCollectionSchemas;
|
|
8
8
|
export declare const clearSchemaCache: (collectionPath?: string) => void;
|
|
9
9
|
export declare const getCacheStats: () => {
|
|
10
10
|
size: number;
|
|
@@ -9,10 +9,10 @@ import type { AuditActorRealm, AuditLogAppendInput, IDbAdapter } from '../../@ty
|
|
|
9
9
|
import type { DocumentLifecycleContext } from './context.js';
|
|
10
10
|
/** Namespaced audit actions for document-grain changes. */
|
|
11
11
|
export declare const AUDIT_ACTIONS: {
|
|
12
|
-
readonly pathChanged:
|
|
13
|
-
readonly localesChanged:
|
|
14
|
-
readonly statusChanged:
|
|
15
|
-
readonly deleted:
|
|
12
|
+
readonly pathChanged: 'document.path.changed';
|
|
13
|
+
readonly localesChanged: 'document.locales.changed';
|
|
14
|
+
readonly statusChanged: 'document.status.changed';
|
|
15
|
+
readonly deleted: 'document.deleted';
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* The actor id + realm for an audit-log row. Mirrors `actorId()`: a real
|
|
@@ -9,7 +9,8 @@ import { resolveHooks } from '../../@types/index.js';
|
|
|
9
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
10
|
import { ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { hasUploadField, isUploadField } from '../../utils/storage-utils.js';
|
|
13
|
+
import { walkFieldTree } from '../walk-field-tree.js';
|
|
13
14
|
import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
|
|
14
15
|
import { invokeHook } from './internals.js';
|
|
15
16
|
import { promoteChildrenAndRemove } from './tree.js';
|
|
@@ -46,8 +47,7 @@ export async function deleteDocument(ctx, params) {
|
|
|
46
47
|
// AND a storage provider, fetch with reconstruct: true so we
|
|
47
48
|
// can read the stored file paths (and persisted variant paths)
|
|
48
49
|
// from the field values before the DB rows are deleted.
|
|
49
|
-
const
|
|
50
|
-
const isUploadCollection = uploadFieldNames.length > 0 && ctx.storage != null;
|
|
50
|
+
const isUploadCollection = hasUploadField(definition) && ctx.storage != null;
|
|
51
51
|
const latest = await db.queries.documents.getDocumentById({
|
|
52
52
|
collection_id: ctx.collectionId,
|
|
53
53
|
document_id: params.documentId,
|
|
@@ -60,21 +60,27 @@ export async function deleteDocument(ctx, params) {
|
|
|
60
60
|
}).log(ctx.logger);
|
|
61
61
|
}
|
|
62
62
|
// Collect storage paths for every upload-capable field on the doc:
|
|
63
|
-
// the original file plus every persisted variant.
|
|
63
|
+
// the original file plus every persisted variant. The schema/data
|
|
64
|
+
// walk descends into `group` / `array` / `blocks`, so upload fields
|
|
65
|
+
// nested in repeating structures are cleaned up too. Reading the
|
|
64
66
|
// variants from the field value (rather than re-deriving from
|
|
65
67
|
// `upload.sizes`) keeps cleanup correct even when the size set
|
|
66
68
|
// changes between upload and delete.
|
|
67
69
|
const storagePathsToDelete = [];
|
|
68
70
|
if (isUploadCollection) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
const data = latest?.fields;
|
|
72
|
+
for (const leaf of walkFieldTree(definition.fields, data)) {
|
|
73
|
+
if (!isUploadField(leaf.field))
|
|
74
|
+
continue;
|
|
75
|
+
const fieldValue = leaf.value;
|
|
71
76
|
if (!fieldValue || typeof fieldValue !== 'object')
|
|
72
77
|
continue;
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const stored = fieldValue;
|
|
79
|
+
if (typeof stored.storagePath === 'string') {
|
|
80
|
+
storagePathsToDelete.push(stored.storagePath);
|
|
75
81
|
}
|
|
76
|
-
if (Array.isArray(
|
|
77
|
-
for (const variant of
|
|
82
|
+
if (Array.isArray(stored.variants)) {
|
|
83
|
+
for (const variant of stored.variants) {
|
|
78
84
|
if (variant && typeof variant.storagePath === 'string') {
|
|
79
85
|
storagePathsToDelete.push(variant.storagePath);
|
|
80
86
|
}
|
|
@@ -926,6 +926,58 @@ describe('Document lifecycle service', () => {
|
|
|
926
926
|
action: 'document.deleted',
|
|
927
927
|
}));
|
|
928
928
|
});
|
|
929
|
+
it('cleans up stored files for upload fields at any nesting depth', async () => {
|
|
930
|
+
const { db, getDocumentById } = createMockDb();
|
|
931
|
+
const upload = { mimeTypes: ['application/pdf'], maxFileSize: 1024 };
|
|
932
|
+
const definition = {
|
|
933
|
+
...minimalCollection,
|
|
934
|
+
fields: [
|
|
935
|
+
{ name: 'cover', label: 'Cover', type: 'image', upload },
|
|
936
|
+
{
|
|
937
|
+
name: 'files',
|
|
938
|
+
label: 'Files',
|
|
939
|
+
type: 'array',
|
|
940
|
+
fields: [
|
|
941
|
+
{
|
|
942
|
+
name: 'filesGroup',
|
|
943
|
+
type: 'group',
|
|
944
|
+
fields: [{ name: 'publicationFile', label: 'File', type: 'file', upload }],
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
};
|
|
950
|
+
getDocumentById.mockResolvedValue({
|
|
951
|
+
document_version_id: 'ver-1',
|
|
952
|
+
document_id: 'doc-1',
|
|
953
|
+
path: 'doc-to-delete',
|
|
954
|
+
fields: {
|
|
955
|
+
cover: {
|
|
956
|
+
storagePath: 'covers/original.jpg',
|
|
957
|
+
variants: [{ storagePath: 'covers/thumb.avif' }],
|
|
958
|
+
},
|
|
959
|
+
files: [
|
|
960
|
+
{ filesGroup: { publicationFile: { storagePath: 'files/a.pdf' } } },
|
|
961
|
+
{ filesGroup: { publicationFile: null } },
|
|
962
|
+
{ filesGroup: { publicationFile: { storagePath: 'files/b.pdf' } } },
|
|
963
|
+
],
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
const storageDelete = vi.fn().mockResolvedValue(undefined);
|
|
967
|
+
const ctx = {
|
|
968
|
+
...buildCtx(db, definition),
|
|
969
|
+
storage: { delete: storageDelete },
|
|
970
|
+
};
|
|
971
|
+
await deleteDocument(ctx, { documentId: 'doc-1' });
|
|
972
|
+
// reconstruct: true because the collection is upload-capable
|
|
973
|
+
expect(getDocumentById).toHaveBeenCalledWith(expect.objectContaining({ reconstruct: true }));
|
|
974
|
+
expect(storageDelete.mock.calls.map((c) => c[0])).toEqual([
|
|
975
|
+
'covers/original.jpg',
|
|
976
|
+
'covers/thumb.avif',
|
|
977
|
+
'files/a.pdf',
|
|
978
|
+
'files/b.pdf',
|
|
979
|
+
]);
|
|
980
|
+
});
|
|
929
981
|
});
|
|
930
982
|
// -----------------------------------------------------------------------
|
|
931
983
|
// updateDocumentSystemFields (audited, non-versioned)
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { type Field, type FileField, type ImageField } from '../@types/field-types.js';
|
|
8
9
|
import type { CollectionDefinition } from '../@types/collection-types.js';
|
|
9
|
-
import type { Field, FileField, ImageField } from '../@types/field-types.js';
|
|
10
10
|
/**
|
|
11
11
|
* Predicate: does this field carry an `upload` config? True for any
|
|
12
12
|
* `image` / `file` field with an `upload` block declared.
|
|
@@ -15,20 +15,21 @@ export declare function isUploadField(field: Field): field is (ImageField | File
|
|
|
15
15
|
upload: NonNullable<(ImageField | FileField)['upload']>;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
* Walk the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Walk the field set and return every upload-capable image/file field
|
|
19
|
+
* on the collection, recursing into `group` / `array` / `blocks`
|
|
20
|
+
* structure fields. Used by the delete path, the upload-route resolver,
|
|
21
|
+
* and any UI that needs to reason about "is this collection
|
|
22
|
+
* upload-capable, and which fields take uploads?"
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Field names are the upload transport's selector, so a schema should
|
|
25
|
+
* not declare two upload fields with the same name in different nesting
|
|
26
|
+
* scopes — resolvers match by name and take the first hit in
|
|
27
|
+
* declaration order.
|
|
27
28
|
*/
|
|
28
29
|
export declare function getUploadFields(definition: Pick<CollectionDefinition, 'fields'>): (ImageField | FileField)[];
|
|
29
30
|
/**
|
|
30
31
|
* Convenience: does this collection have at least one upload-capable
|
|
31
|
-
* image/file field at
|
|
32
|
+
* image/file field at any nesting depth? Replaces the old
|
|
32
33
|
* `definition.upload != null` discriminator that "this collection is
|
|
33
34
|
* an upload collection / media library."
|
|
34
35
|
*/
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
import { isArrayField, isBlocksField, isGroupField, } from '../@types/field-types.js';
|
|
8
9
|
/**
|
|
9
10
|
* Predicate: does this field carry an `upload` config? True for any
|
|
10
11
|
* `image` / `file` field with an `upload` block declared.
|
|
@@ -13,25 +14,43 @@ export function isUploadField(field) {
|
|
|
13
14
|
return (field.type === 'image' || field.type === 'file') && field.upload != null;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
|
-
* Walk the
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Walk the field set and return every upload-capable image/file field
|
|
18
|
+
* on the collection, recursing into `group` / `array` / `blocks`
|
|
19
|
+
* structure fields. Used by the delete path, the upload-route resolver,
|
|
20
|
+
* and any UI that needs to reason about "is this collection
|
|
21
|
+
* upload-capable, and which fields take uploads?"
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Field names are the upload transport's selector, so a schema should
|
|
24
|
+
* not declare two upload fields with the same name in different nesting
|
|
25
|
+
* scopes — resolvers match by name and take the first hit in
|
|
26
|
+
* declaration order.
|
|
25
27
|
*/
|
|
26
28
|
export function getUploadFields(definition) {
|
|
27
|
-
|
|
29
|
+
const found = [];
|
|
30
|
+
collectUploadFields(definition.fields, found);
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
function collectUploadFields(fields, found) {
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
if (isUploadField(field)) {
|
|
36
|
+
found.push(field);
|
|
37
|
+
}
|
|
38
|
+
else if (isGroupField(field) || isArrayField(field)) {
|
|
39
|
+
collectUploadFields(field.fields, found);
|
|
40
|
+
}
|
|
41
|
+
else if (isBlocksField(field)) {
|
|
42
|
+
for (const block of field.blocks) {
|
|
43
|
+
collectUploadFields(block.fields, found);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
28
47
|
}
|
|
29
48
|
/**
|
|
30
49
|
* Convenience: does this collection have at least one upload-capable
|
|
31
|
-
* image/file field at
|
|
50
|
+
* image/file field at any nesting depth? Replaces the old
|
|
32
51
|
* `definition.upload != null` discriminator that "this collection is
|
|
33
52
|
* an upload collection / media library."
|
|
34
53
|
*/
|
|
35
54
|
export function hasUploadField(definition) {
|
|
36
|
-
return definition.
|
|
55
|
+
return getUploadFields(definition).length > 0;
|
|
37
56
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { getUploadFields, hasUploadField, isUploadField } from './storage-utils.js';
|
|
10
|
+
const upload = { mimeTypes: ['application/pdf'], maxFileSize: 1024 };
|
|
11
|
+
const coverField = { name: 'cover', label: 'Cover', type: 'image', upload };
|
|
12
|
+
const nestedFileField = { name: 'publicationFile', label: 'File', type: 'file', upload };
|
|
13
|
+
describe('isUploadField', () => {
|
|
14
|
+
it('is true for image/file fields with an upload block', () => {
|
|
15
|
+
expect(isUploadField(coverField)).toBe(true);
|
|
16
|
+
expect(isUploadField(nestedFileField)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('is false for image/file fields without an upload block', () => {
|
|
19
|
+
expect(isUploadField({ name: 'pic', label: 'Pic', type: 'image' })).toBe(false);
|
|
20
|
+
expect(isUploadField({ name: 'doc', label: 'Doc', type: 'file' })).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('is false for non-media fields', () => {
|
|
23
|
+
expect(isUploadField({ name: 'title', label: 'Title', type: 'text' })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('getUploadFields', () => {
|
|
27
|
+
it('returns top-level upload fields', () => {
|
|
28
|
+
const fields = [{ name: 'title', label: 'Title', type: 'text' }, coverField];
|
|
29
|
+
expect(getUploadFields({ fields }).map((f) => f.name)).toEqual(['cover']);
|
|
30
|
+
});
|
|
31
|
+
it('recurses into group / array / blocks structure fields', () => {
|
|
32
|
+
const fields = [
|
|
33
|
+
coverField,
|
|
34
|
+
{
|
|
35
|
+
name: 'files',
|
|
36
|
+
label: 'Files',
|
|
37
|
+
type: 'array',
|
|
38
|
+
fields: [
|
|
39
|
+
{
|
|
40
|
+
name: 'filesGroup',
|
|
41
|
+
type: 'group',
|
|
42
|
+
fields: [nestedFileField, { name: 'label', label: 'Label', type: 'text' }],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'content',
|
|
48
|
+
label: 'Content',
|
|
49
|
+
type: 'blocks',
|
|
50
|
+
blocks: [
|
|
51
|
+
{
|
|
52
|
+
blockType: 'photo',
|
|
53
|
+
label: 'Photo',
|
|
54
|
+
fields: [{ name: 'photo', label: 'Photo', type: 'image', upload }],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
expect(getUploadFields({ fields }).map((f) => f.name)).toEqual([
|
|
60
|
+
'cover',
|
|
61
|
+
'publicationFile',
|
|
62
|
+
'photo',
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
it('skips nested image/file fields without an upload block', () => {
|
|
66
|
+
const fields = [
|
|
67
|
+
{
|
|
68
|
+
name: 'gallery',
|
|
69
|
+
label: 'Gallery',
|
|
70
|
+
type: 'group',
|
|
71
|
+
fields: [{ name: 'pic', label: 'Pic', type: 'image' }],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
expect(getUploadFields({ fields })).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('hasUploadField', () => {
|
|
78
|
+
it('detects upload fields at any nesting depth', () => {
|
|
79
|
+
const fields = [
|
|
80
|
+
{
|
|
81
|
+
name: 'files',
|
|
82
|
+
label: 'Files',
|
|
83
|
+
type: 'array',
|
|
84
|
+
fields: [{ name: 'filesGroup', type: 'group', fields: [nestedFileField] }],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
expect(hasUploadField({ fields })).toBe(true);
|
|
88
|
+
expect(hasUploadField({ fields: [{ name: 'title', label: 'Title', type: 'text' }] })).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -33,9 +33,9 @@ import { z } from 'zod';
|
|
|
33
33
|
* this file — emit codes here, translate in `@byline/admin`.
|
|
34
34
|
*/
|
|
35
35
|
export declare const PASSWORD_ERROR_CODES: {
|
|
36
|
-
readonly TOO_SHORT:
|
|
37
|
-
readonly TOO_LONG:
|
|
38
|
-
readonly COMPLEXITY:
|
|
36
|
+
readonly TOO_SHORT: 'password.tooShort';
|
|
37
|
+
readonly TOO_LONG: 'password.tooLong';
|
|
38
|
+
readonly COMPLEXITY: 'password.complexity';
|
|
39
39
|
};
|
|
40
40
|
/**
|
|
41
41
|
* Standard password policy — 8 to 128 characters, must contain at least
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.18.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -74,20 +74,20 @@
|
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"dotenv": "^17.4.2",
|
|
76
76
|
"pino": "^10.3.1",
|
|
77
|
-
"sharp": "^0.
|
|
77
|
+
"sharp": "^0.35.3",
|
|
78
78
|
"zod": "^4.4.3",
|
|
79
|
-
"@byline/auth": "3.
|
|
79
|
+
"@byline/auth": "3.18.0"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@biomejs/biome": "2.
|
|
83
|
-
"@types/node": "^
|
|
82
|
+
"@biomejs/biome": "2.5.2",
|
|
83
|
+
"@types/node": "^26.1.0",
|
|
84
84
|
"chokidar": "^5.0.0",
|
|
85
85
|
"chokidar-cli": "^3.0.0",
|
|
86
86
|
"npm-run-all": "^4.1.5",
|
|
87
|
-
"tsc-alias": "^1.
|
|
88
|
-
"tsx": "^4.
|
|
89
|
-
"typescript": "
|
|
90
|
-
"vitest": "^4.1.
|
|
87
|
+
"tsc-alias": "^1.9.0",
|
|
88
|
+
"tsx": "^4.23.0",
|
|
89
|
+
"typescript": "^7.0.2",
|
|
90
|
+
"vitest": "^4.1.10"
|
|
91
91
|
},
|
|
92
92
|
"publishConfig": {
|
|
93
93
|
"access": "public",
|