@byline/core 4.15.0 → 4.17.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/admin-types.d.ts +86 -31
- package/dist/@types/admin-types.js +12 -0
- package/dist/@types/collection-types.d.ts +211 -29
- package/dist/@types/collection-types.js +68 -8
- package/dist/@types/db-types.d.ts +50 -0
- package/dist/@types/form-admin-config.test.node.d.ts +6 -0
- package/dist/@types/form-admin-config.test.node.js +26 -0
- package/dist/@types/singleton-admin-config.test.node.js +66 -0
- package/dist/@types/singleton-definition.test.node.d.ts +8 -0
- package/dist/@types/singleton-definition.test.node.js +75 -0
- package/dist/@types/site-config.d.ts +9 -8
- package/dist/auth/apply-before-read.d.ts +2 -1
- package/dist/auth/apply-before-read.js +13 -3
- package/dist/auth/apply-before-read.test.node.js +44 -0
- package/dist/auth/assert-actor-can-perform.d.ts +4 -3
- package/dist/auth/assert-actor-can-perform.js +9 -7
- package/dist/auth/assert-actor-can-perform.test.node.js +32 -11
- package/dist/auth/filter-readable-collections.d.ts +6 -6
- package/dist/auth/filter-readable-collections.js +8 -8
- package/dist/auth/filter-readable-collections.test.node.js +26 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/register-collection-abilities.d.ts +34 -30
- package/dist/auth/register-collection-abilities.js +81 -77
- package/dist/auth/register-collection-abilities.test.node.js +86 -8
- package/dist/codegen/fixtures/all-fields.expected.d.ts +3 -0
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +32 -16
- package/dist/codegen/index.test.node.js +35 -2
- package/dist/config/attach-hooks.d.ts +3 -3
- package/dist/config/attach-hooks.js +8 -0
- package/dist/config/attach-hooks.test.node.js +59 -0
- package/dist/config/config-hooks.test.node.js +34 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.js +9 -1
- package/dist/config/group-collections.d.ts +10 -9
- package/dist/config/group-collections.js +4 -4
- package/dist/config/group-collections.test.node.js +14 -0
- package/dist/config/validate-admin-configs.d.ts +4 -4
- package/dist/config/validate-admin-configs.js +40 -7
- package/dist/config/validate-admin-configs.test.node.js +44 -0
- package/dist/config/validate-collections.js +43 -0
- package/dist/config/validate-collections.test.node.js +75 -0
- package/dist/core.d.ts +2 -3
- package/dist/core.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/errors.d.ts +5 -3
- package/dist/lib/errors.js +1 -0
- package/dist/services/collection-bootstrap.js +12 -0
- package/dist/services/collection-bootstrap.test.node.js +63 -6
- package/dist/services/discover-counter-groups.test.node.js +6 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +9 -17
- package/dist/services/document-lifecycle/create.js +6 -11
- package/dist/services/document-lifecycle/delete-locale.js +1 -1
- package/dist/services/document-lifecycle/delete.js +1 -1
- package/dist/services/document-lifecycle/duplicate.js +7 -18
- package/dist/services/document-lifecycle/persistence.d.ts +29 -0
- package/dist/services/document-lifecycle/persistence.js +42 -0
- package/dist/services/document-lifecycle/restore.js +9 -17
- package/dist/services/document-lifecycle/scheduled-publish.js +5 -5
- package/dist/services/document-lifecycle/status.js +3 -3
- package/dist/services/document-lifecycle/system-fields.js +1 -1
- package/dist/services/document-lifecycle/tree.js +2 -2
- package/dist/services/document-lifecycle/tree.test.node.js +6 -1
- package/dist/services/document-lifecycle/update.js +20 -36
- package/dist/services/document-lifecycle.test.node.js +35 -0
- package/dist/services/field-upload.d.ts +1 -1
- package/dist/services/field-upload.js +20 -6
- package/dist/services/field-upload.test.node.js +125 -9
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/populate.js +1 -1
- package/dist/services/populate.test.node.js +9 -0
- package/dist/services/richtext-populate.js +1 -1
- package/dist/services/singleton-lifecycle/copy-to-locale.d.ts +16 -0
- package/dist/services/singleton-lifecycle/copy-to-locale.js +100 -0
- package/dist/services/singleton-lifecycle/index.d.ts +14 -0
- package/dist/services/singleton-lifecycle/index.js +11 -0
- package/dist/services/singleton-lifecycle/internals.d.ts +49 -0
- package/dist/services/singleton-lifecycle/internals.js +118 -0
- package/dist/services/singleton-lifecycle/restore.d.ts +13 -0
- package/dist/services/singleton-lifecycle/restore.js +93 -0
- package/dist/services/singleton-lifecycle/update.d.ts +16 -0
- package/dist/services/singleton-lifecycle/update.js +107 -0
- package/dist/services/singleton-lifecycle.test.node.d.ts +8 -0
- package/dist/services/singleton-lifecycle.test.node.js +396 -0
- package/dist/storage/collection-fingerprint.js +18 -6
- package/dist/storage/collection-fingerprint.test.node.js +15 -0
- package/package.json +5 -5
- package/dist/config/resolve-item-view-columns.test.node.js +0 -30
- /package/dist/{config/resolve-item-view-columns.test.node.d.ts → @types/singleton-admin-config.test.node.d.ts} +0 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { defineSingleton } from '../@types/index.js';
|
|
9
10
|
import { filterReadableCollections } from './filter-readable-collections.js';
|
|
10
11
|
const define = (path) => ({
|
|
11
12
|
path,
|
|
@@ -15,10 +16,16 @@ const define = (path) => ({
|
|
|
15
16
|
const news = define('news');
|
|
16
17
|
const pages = define('pages');
|
|
17
18
|
const media = define('media');
|
|
19
|
+
const settings = defineSingleton({
|
|
20
|
+
path: 'site-settings',
|
|
21
|
+
label: 'Site settings',
|
|
22
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
23
|
+
});
|
|
18
24
|
const all = [news, pages, media];
|
|
19
25
|
describe('filterReadableCollections', () => {
|
|
20
26
|
it('returns every collection for a super admin, regardless of abilities', () => {
|
|
21
|
-
|
|
27
|
+
const resources = [...all, settings];
|
|
28
|
+
expect(filterReadableCollections(resources, { isSuperAdmin: true, abilities: [] })).toEqual(resources);
|
|
22
29
|
});
|
|
23
30
|
it('returns nothing when the actor holds no abilities', () => {
|
|
24
31
|
expect(filterReadableCollections(all, { isSuperAdmin: false, abilities: [] })).toEqual([]);
|
|
@@ -51,6 +58,24 @@ describe('filterReadableCollections', () => {
|
|
|
51
58
|
});
|
|
52
59
|
expect(result).toEqual([]);
|
|
53
60
|
});
|
|
61
|
+
it('uses the singleton read namespace for singleton resources', () => {
|
|
62
|
+
expect(filterReadableCollections([settings], {
|
|
63
|
+
isSuperAdmin: false,
|
|
64
|
+
abilities: ['singletons.site-settings.read'],
|
|
65
|
+
})).toEqual([settings]);
|
|
66
|
+
});
|
|
67
|
+
it('does not let the collection namespace bleed into a singleton path', () => {
|
|
68
|
+
expect(filterReadableCollections([settings], {
|
|
69
|
+
isSuperAdmin: false,
|
|
70
|
+
abilities: ['collections.site-settings.read'],
|
|
71
|
+
})).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
it('does not advertise an update-only singleton without read access', () => {
|
|
74
|
+
expect(filterReadableCollections([settings], {
|
|
75
|
+
isSuperAdmin: false,
|
|
76
|
+
abilities: ['singletons.site-settings.update'],
|
|
77
|
+
})).toEqual([]);
|
|
78
|
+
});
|
|
54
79
|
it('does not match on a prefix of a collection path', () => {
|
|
55
80
|
const result = filterReadableCollections([define('news-categories')], {
|
|
56
81
|
isSuperAdmin: false,
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
export { applyBeforeRead, bindReadContextAuthority, compileBeforeReadFilters, } from './apply-before-read.js';
|
|
9
9
|
export { assertActorCanPerform } from './assert-actor-can-perform.js';
|
|
10
10
|
export { type ActorAbilitySnapshot, filterReadableCollections, } from './filter-readable-collections.js';
|
|
11
|
-
export { COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, registerCollectionAbilities, } from './register-collection-abilities.js';
|
|
11
|
+
export { COLLECTION_ABILITY_VERBS, type CollectionAbilityResource, type CollectionAbilityResourceDescriptor, type CollectionAbilityVerb, collectionAbilityKey, type DocumentAbilityResource, type DocumentAbilityVerbFor, documentAbilityKey, registerCollectionAbilities, registerDocumentAbilities, registerSingletonAbilities, SINGLETON_ABILITY_VERBS, type SingletonAbilityResource, type SingletonAbilityResourceDescriptor, type SingletonAbilityVerb, } from './register-collection-abilities.js';
|
package/dist/auth/index.js
CHANGED
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
export { applyBeforeRead, bindReadContextAuthority, compileBeforeReadFilters, } from './apply-before-read.js';
|
|
9
9
|
export { assertActorCanPerform } from './assert-actor-can-perform.js';
|
|
10
10
|
export { filterReadableCollections, } from './filter-readable-collections.js';
|
|
11
|
-
export { COLLECTION_ABILITY_VERBS, collectionAbilityKey, registerCollectionAbilities, } from './register-collection-abilities.js';
|
|
11
|
+
export { COLLECTION_ABILITY_VERBS, collectionAbilityKey, documentAbilityKey, registerCollectionAbilities, registerDocumentAbilities, registerSingletonAbilities, SINGLETON_ABILITY_VERBS, } from './register-collection-abilities.js';
|
|
@@ -6,37 +6,41 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { AbilityRegistry } from '@byline/auth';
|
|
9
|
-
import type
|
|
10
|
-
/**
|
|
11
|
-
* Auto-register the CRUD + workflow abilities contributed by a collection.
|
|
12
|
-
*
|
|
13
|
-
* Every registered collection contributes exactly seven abilities, all in
|
|
14
|
-
* the `collections.<path>` group:
|
|
15
|
-
*
|
|
16
|
-
* - `collections.<path>.read` — enumerate / fetch documents
|
|
17
|
-
* - `collections.<path>.create` — create new documents
|
|
18
|
-
* - `collections.<path>.update` — modify existing documents
|
|
19
|
-
* - `collections.<path>.delete` — delete documents (soft or hard)
|
|
20
|
-
* - `collections.<path>.publish` — transition a document into the
|
|
21
|
-
* `published` status
|
|
22
|
-
* - `collections.<path>.changeStatus` — any other workflow transition
|
|
23
|
-
* (draft → custom state, etc.)
|
|
24
|
-
* - `collections.<path>.reindex` — rebuild the collection's search
|
|
25
|
-
* index (admin maintenance task)
|
|
26
|
-
*
|
|
27
|
-
* Registration is unconditional: every collection in Byline has a workflow
|
|
28
|
-
* (the default `draft → published → archived` one when not explicitly
|
|
29
|
-
* configured), so `publish` and `changeStatus` always apply; `reindex` is
|
|
30
|
-
* likewise uniform (a no-op for collections without a `search` config).
|
|
31
|
-
* Keeping the seven-ability contract uniform makes the role editor UI
|
|
32
|
-
* predictable and avoids hidden conditional logic downstream.
|
|
33
|
-
*
|
|
34
|
-
* Called from `initBylineCore()` for each declared collection. See
|
|
35
|
-
* docs/07-auth-and-security/01-authn-authz.md.
|
|
36
|
-
*/
|
|
37
|
-
export declare function registerCollectionAbilities(registry: AbilityRegistry, definition: CollectionDefinition): void;
|
|
9
|
+
import { type CollectionDefinition, type MultiCollectionDefinition, type SingletonDefinition } from '../@types/index.js';
|
|
38
10
|
/** The ability suffixes that every collection contributes. Exposed for contract tests. */
|
|
39
11
|
export declare const COLLECTION_ABILITY_VERBS: readonly ['read', 'create', 'update', 'delete', 'publish', 'changeStatus', 'reindex'];
|
|
12
|
+
/** The smaller ability family contributed by every singleton. */
|
|
13
|
+
export declare const SINGLETON_ABILITY_VERBS: readonly ['read', 'update', 'publish', 'changeStatus'];
|
|
40
14
|
export type CollectionAbilityVerb = (typeof COLLECTION_ABILITY_VERBS)[number];
|
|
41
|
-
|
|
15
|
+
export type SingletonAbilityVerb = (typeof SINGLETON_ABILITY_VERBS)[number];
|
|
16
|
+
/** Minimal explicit descriptor for collection ability checks. */
|
|
17
|
+
export interface CollectionAbilityResourceDescriptor {
|
|
18
|
+
kind: 'collection';
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
21
|
+
/** Minimal explicit descriptor for singleton ability checks. */
|
|
22
|
+
export interface SingletonAbilityResourceDescriptor {
|
|
23
|
+
kind: 'singleton';
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
export type CollectionAbilityResource = MultiCollectionDefinition | CollectionAbilityResourceDescriptor;
|
|
27
|
+
export type SingletonAbilityResource = SingletonDefinition | SingletonAbilityResourceDescriptor;
|
|
28
|
+
export type DocumentAbilityResource = CollectionAbilityResource | SingletonAbilityResource;
|
|
29
|
+
/** Correlate the permitted verb family with the resource kind. */
|
|
30
|
+
export type DocumentAbilityVerbFor<Resource extends DocumentAbilityResource> = Resource extends SingletonAbilityResource ? SingletonAbilityVerb : CollectionAbilityVerb;
|
|
31
|
+
/**
|
|
32
|
+
* Compute a document-resource ability key from a kind-bearing descriptor.
|
|
33
|
+
*
|
|
34
|
+
* TypeScript callers cannot pair a known singleton with collection-only verbs.
|
|
35
|
+
* Runtime validation preserves the same boundary for untyped JavaScript callers
|
|
36
|
+
* and for union-typed definitions whose concrete kind is known only at runtime.
|
|
37
|
+
*/
|
|
38
|
+
export declare function documentAbilityKey<Resource extends DocumentAbilityResource>(resource: Resource, verb: DocumentAbilityVerbFor<Resource>): string;
|
|
39
|
+
/** @deprecated Use {@link documentAbilityKey} with a kind-bearing resource descriptor. */
|
|
42
40
|
export declare function collectionAbilityKey(path: string, verb: CollectionAbilityVerb): string;
|
|
41
|
+
/** Auto-register the seven abilities contributed by a multi-document collection. */
|
|
42
|
+
export declare function registerCollectionAbilities(registry: AbilityRegistry, definition: MultiCollectionDefinition): void;
|
|
43
|
+
/** Auto-register the four read, update, and workflow abilities for a singleton. */
|
|
44
|
+
export declare function registerSingletonAbilities(registry: AbilityRegistry, definition: SingletonDefinition): void;
|
|
45
|
+
/** Register the correct ability family for either document-resource kind. */
|
|
46
|
+
export declare function registerDocumentAbilities(registry: AbilityRegistry, definition: CollectionDefinition): void;
|
|
@@ -5,81 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
* Auto-register the CRUD + workflow abilities contributed by a collection.
|
|
10
|
-
*
|
|
11
|
-
* Every registered collection contributes exactly seven abilities, all in
|
|
12
|
-
* the `collections.<path>` group:
|
|
13
|
-
*
|
|
14
|
-
* - `collections.<path>.read` — enumerate / fetch documents
|
|
15
|
-
* - `collections.<path>.create` — create new documents
|
|
16
|
-
* - `collections.<path>.update` — modify existing documents
|
|
17
|
-
* - `collections.<path>.delete` — delete documents (soft or hard)
|
|
18
|
-
* - `collections.<path>.publish` — transition a document into the
|
|
19
|
-
* `published` status
|
|
20
|
-
* - `collections.<path>.changeStatus` — any other workflow transition
|
|
21
|
-
* (draft → custom state, etc.)
|
|
22
|
-
* - `collections.<path>.reindex` — rebuild the collection's search
|
|
23
|
-
* index (admin maintenance task)
|
|
24
|
-
*
|
|
25
|
-
* Registration is unconditional: every collection in Byline has a workflow
|
|
26
|
-
* (the default `draft → published → archived` one when not explicitly
|
|
27
|
-
* configured), so `publish` and `changeStatus` always apply; `reindex` is
|
|
28
|
-
* likewise uniform (a no-op for collections without a `search` config).
|
|
29
|
-
* Keeping the seven-ability contract uniform makes the role editor UI
|
|
30
|
-
* predictable and avoids hidden conditional logic downstream.
|
|
31
|
-
*
|
|
32
|
-
* Called from `initBylineCore()` for each declared collection. See
|
|
33
|
-
* docs/07-auth-and-security/01-authn-authz.md.
|
|
34
|
-
*/
|
|
35
|
-
export function registerCollectionAbilities(registry, definition) {
|
|
36
|
-
const path = definition.path;
|
|
37
|
-
const group = `collections.${path}`;
|
|
38
|
-
const base = `collections.${path}`;
|
|
39
|
-
const { singular, plural } = definition.labels;
|
|
40
|
-
registry.register({
|
|
41
|
-
key: `${base}.read`,
|
|
42
|
-
label: `Read ${plural}`,
|
|
43
|
-
group,
|
|
44
|
-
source: 'collection',
|
|
45
|
-
});
|
|
46
|
-
registry.register({
|
|
47
|
-
key: `${base}.create`,
|
|
48
|
-
label: `Create ${singular}`,
|
|
49
|
-
group,
|
|
50
|
-
source: 'collection',
|
|
51
|
-
});
|
|
52
|
-
registry.register({
|
|
53
|
-
key: `${base}.update`,
|
|
54
|
-
label: `Update ${singular}`,
|
|
55
|
-
group,
|
|
56
|
-
source: 'collection',
|
|
57
|
-
});
|
|
58
|
-
registry.register({
|
|
59
|
-
key: `${base}.delete`,
|
|
60
|
-
label: `Delete ${singular}`,
|
|
61
|
-
group,
|
|
62
|
-
source: 'collection',
|
|
63
|
-
});
|
|
64
|
-
registry.register({
|
|
65
|
-
key: `${base}.publish`,
|
|
66
|
-
label: `Publish ${plural}`,
|
|
67
|
-
group,
|
|
68
|
-
source: 'collection',
|
|
69
|
-
});
|
|
70
|
-
registry.register({
|
|
71
|
-
key: `${base}.changeStatus`,
|
|
72
|
-
label: `Change status of ${plural}`,
|
|
73
|
-
group,
|
|
74
|
-
source: 'collection',
|
|
75
|
-
});
|
|
76
|
-
registry.register({
|
|
77
|
-
key: `${base}.reindex`,
|
|
78
|
-
label: `Reindex ${plural} search`,
|
|
79
|
-
group,
|
|
80
|
-
source: 'collection',
|
|
81
|
-
});
|
|
82
|
-
}
|
|
8
|
+
import { isSingleton, } from '../@types/index.js';
|
|
83
9
|
/** The ability suffixes that every collection contributes. Exposed for contract tests. */
|
|
84
10
|
export const COLLECTION_ABILITY_VERBS = [
|
|
85
11
|
'read',
|
|
@@ -90,7 +16,85 @@ export const COLLECTION_ABILITY_VERBS = [
|
|
|
90
16
|
'changeStatus',
|
|
91
17
|
'reindex',
|
|
92
18
|
];
|
|
93
|
-
/**
|
|
19
|
+
/** The smaller ability family contributed by every singleton. */
|
|
20
|
+
export const SINGLETON_ABILITY_VERBS = ['read', 'update', 'publish', 'changeStatus'];
|
|
21
|
+
/**
|
|
22
|
+
* Compute a document-resource ability key from a kind-bearing descriptor.
|
|
23
|
+
*
|
|
24
|
+
* TypeScript callers cannot pair a known singleton with collection-only verbs.
|
|
25
|
+
* Runtime validation preserves the same boundary for untyped JavaScript callers
|
|
26
|
+
* and for union-typed definitions whose concrete kind is known only at runtime.
|
|
27
|
+
*/
|
|
28
|
+
export function documentAbilityKey(resource, verb) {
|
|
29
|
+
const kind = resourceKind(resource);
|
|
30
|
+
const candidate = verb;
|
|
31
|
+
const allowed = kind === 'singleton' ? SINGLETON_ABILITY_VERBS : COLLECTION_ABILITY_VERBS;
|
|
32
|
+
if (!allowed.includes(candidate)) {
|
|
33
|
+
throw new TypeError(`ability verb '${candidate}' is not valid for resource kind '${kind}'`);
|
|
34
|
+
}
|
|
35
|
+
return `${resourceNamespace(kind)}.${resource.path}.${candidate}`;
|
|
36
|
+
}
|
|
37
|
+
/** @deprecated Use {@link documentAbilityKey} with a kind-bearing resource descriptor. */
|
|
94
38
|
export function collectionAbilityKey(path, verb) {
|
|
95
|
-
return
|
|
39
|
+
return documentAbilityKey({ kind: 'collection', path }, verb);
|
|
40
|
+
}
|
|
41
|
+
/** Auto-register the seven abilities contributed by a multi-document collection. */
|
|
42
|
+
export function registerCollectionAbilities(registry, definition) {
|
|
43
|
+
const group = documentAbilityGroup(definition);
|
|
44
|
+
const { singular, plural } = definition.labels;
|
|
45
|
+
const labels = {
|
|
46
|
+
read: `Read ${plural}`,
|
|
47
|
+
create: `Create ${singular}`,
|
|
48
|
+
update: `Update ${singular}`,
|
|
49
|
+
delete: `Delete ${singular}`,
|
|
50
|
+
publish: `Publish ${plural}`,
|
|
51
|
+
changeStatus: `Change status of ${plural}`,
|
|
52
|
+
reindex: `Reindex ${plural} search`,
|
|
53
|
+
};
|
|
54
|
+
for (const verb of COLLECTION_ABILITY_VERBS) {
|
|
55
|
+
registry.register({
|
|
56
|
+
key: documentAbilityKey(definition, verb),
|
|
57
|
+
label: labels[verb],
|
|
58
|
+
group,
|
|
59
|
+
source: 'collection',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Auto-register the four read, update, and workflow abilities for a singleton. */
|
|
64
|
+
export function registerSingletonAbilities(registry, definition) {
|
|
65
|
+
const group = documentAbilityGroup(definition);
|
|
66
|
+
const label = definition.label;
|
|
67
|
+
const labels = {
|
|
68
|
+
read: `Read ${label}`,
|
|
69
|
+
update: `Update ${label}`,
|
|
70
|
+
publish: `Publish ${label}`,
|
|
71
|
+
changeStatus: `Change status of ${label}`,
|
|
72
|
+
};
|
|
73
|
+
for (const verb of SINGLETON_ABILITY_VERBS) {
|
|
74
|
+
registry.register({
|
|
75
|
+
key: documentAbilityKey(definition, verb),
|
|
76
|
+
label: labels[verb],
|
|
77
|
+
group,
|
|
78
|
+
source: 'collection',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Register the correct ability family for either document-resource kind. */
|
|
83
|
+
export function registerDocumentAbilities(registry, definition) {
|
|
84
|
+
if (isSingleton(definition)) {
|
|
85
|
+
registerSingletonAbilities(registry, definition);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
registerCollectionAbilities(registry, definition);
|
|
89
|
+
}
|
|
90
|
+
function documentAbilityGroup(resource) {
|
|
91
|
+
return `${resourceNamespace(resourceKind(resource))}.${resource.path}`;
|
|
92
|
+
}
|
|
93
|
+
function resourceKind(resource) {
|
|
94
|
+
if ('kind' in resource)
|
|
95
|
+
return resource.kind;
|
|
96
|
+
return resource.singleton === true ? 'singleton' : 'collection';
|
|
97
|
+
}
|
|
98
|
+
function resourceNamespace(kind) {
|
|
99
|
+
return kind === 'singleton' ? 'singletons' : 'collections';
|
|
96
100
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { AbilityRegistry } from '@byline/auth';
|
|
9
9
|
import { describe, expect, it } from 'vitest';
|
|
10
|
-
import { COLLECTION_ABILITY_VERBS, collectionAbilityKey, registerCollectionAbilities, } from './register-collection-abilities.js';
|
|
10
|
+
import { COLLECTION_ABILITY_VERBS, collectionAbilityKey, documentAbilityKey, registerCollectionAbilities, registerDocumentAbilities, registerSingletonAbilities, SINGLETON_ABILITY_VERBS, } from './register-collection-abilities.js';
|
|
11
11
|
function pageCollection() {
|
|
12
12
|
return {
|
|
13
13
|
path: 'pages',
|
|
@@ -30,8 +30,16 @@ function newsCollection() {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
function siteSettings() {
|
|
34
|
+
return {
|
|
35
|
+
singleton: true,
|
|
36
|
+
path: 'site-settings',
|
|
37
|
+
label: 'Site settings',
|
|
38
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
33
41
|
describe('registerCollectionAbilities', () => {
|
|
34
|
-
it('registers exactly the
|
|
42
|
+
it('registers exactly the seven collection abilities', () => {
|
|
35
43
|
const registry = new AbilityRegistry();
|
|
36
44
|
registerCollectionAbilities(registry, pageCollection());
|
|
37
45
|
const keys = registry.list().map((d) => d.key);
|
|
@@ -101,7 +109,47 @@ describe('registerCollectionAbilities', () => {
|
|
|
101
109
|
expect(registry.size).toBe(14);
|
|
102
110
|
});
|
|
103
111
|
});
|
|
104
|
-
describe('
|
|
112
|
+
describe('registerSingletonAbilities', () => {
|
|
113
|
+
it('registers exactly the four singleton abilities', () => {
|
|
114
|
+
const registry = new AbilityRegistry();
|
|
115
|
+
registerSingletonAbilities(registry, siteSettings());
|
|
116
|
+
expect(registry.list().map((descriptor) => descriptor.key)).toEqual([
|
|
117
|
+
'singletons.site-settings.read',
|
|
118
|
+
'singletons.site-settings.update',
|
|
119
|
+
'singletons.site-settings.publish',
|
|
120
|
+
'singletons.site-settings.changeStatus',
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
it('uses the singleton label and one singleton group', () => {
|
|
124
|
+
const registry = new AbilityRegistry();
|
|
125
|
+
registerSingletonAbilities(registry, siteSettings());
|
|
126
|
+
expect(registry.list().map((descriptor) => descriptor.label)).toEqual([
|
|
127
|
+
'Read Site settings',
|
|
128
|
+
'Update Site settings',
|
|
129
|
+
'Publish Site settings',
|
|
130
|
+
'Change status of Site settings',
|
|
131
|
+
]);
|
|
132
|
+
expect(registry.byGroup().get('singletons.site-settings')).toHaveLength(4);
|
|
133
|
+
});
|
|
134
|
+
it('does not register collection-only verbs', () => {
|
|
135
|
+
const registry = new AbilityRegistry();
|
|
136
|
+
registerSingletonAbilities(registry, siteSettings());
|
|
137
|
+
expect(registry.has('singletons.site-settings.create')).toBe(false);
|
|
138
|
+
expect(registry.has('singletons.site-settings.delete')).toBe(false);
|
|
139
|
+
expect(registry.has('singletons.site-settings.reindex')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('registerDocumentAbilities', () => {
|
|
143
|
+
it('dispatches both definition kinds into isolated ability families', () => {
|
|
144
|
+
const registry = new AbilityRegistry();
|
|
145
|
+
registerDocumentAbilities(registry, pageCollection());
|
|
146
|
+
registerDocumentAbilities(registry, siteSettings());
|
|
147
|
+
expect(registry.size).toBe(11);
|
|
148
|
+
expect(registry.byGroup().get('collections.pages')).toHaveLength(7);
|
|
149
|
+
expect(registry.byGroup().get('singletons.site-settings')).toHaveLength(4);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('ability verb contracts / documentAbilityKey', () => {
|
|
105
153
|
it('COLLECTION_ABILITY_VERBS exposes the canonical verb list in registration order', () => {
|
|
106
154
|
expect(COLLECTION_ABILITY_VERBS).toEqual([
|
|
107
155
|
'read',
|
|
@@ -113,15 +161,45 @@ describe('COLLECTION_ABILITY_VERBS / collectionAbilityKey', () => {
|
|
|
113
161
|
'reindex',
|
|
114
162
|
]);
|
|
115
163
|
});
|
|
116
|
-
it('
|
|
117
|
-
expect(
|
|
118
|
-
|
|
164
|
+
it('SINGLETON_ABILITY_VERBS exposes the canonical verb list in registration order', () => {
|
|
165
|
+
expect(SINGLETON_ABILITY_VERBS).toEqual(['read', 'update', 'publish', 'changeStatus']);
|
|
166
|
+
});
|
|
167
|
+
it('composes a kind-aware flat dotted key from definitions and explicit descriptors', () => {
|
|
168
|
+
expect(documentAbilityKey(pageCollection(), 'publish')).toBe('collections.pages.publish');
|
|
169
|
+
expect(documentAbilityKey(siteSettings(), 'changeStatus')).toBe('singletons.site-settings.changeStatus');
|
|
170
|
+
expect(documentAbilityKey({ kind: 'collection', path: 'news' }, 'read')).toBe('collections.news.read');
|
|
171
|
+
expect(documentAbilityKey({ kind: 'singleton', path: 'navigation' }, 'update')).toBe('singletons.navigation.update');
|
|
172
|
+
});
|
|
173
|
+
it('preserves the deprecated collectionAbilityKey string-path contract', () => {
|
|
174
|
+
expect(collectionAbilityKey('news', 'read')).toBe('collections.news.read');
|
|
119
175
|
});
|
|
120
176
|
it('matches the keys produced by registerCollectionAbilities', () => {
|
|
121
177
|
const registry = new AbilityRegistry();
|
|
122
|
-
|
|
178
|
+
const definition = pageCollection();
|
|
179
|
+
registerCollectionAbilities(registry, definition);
|
|
123
180
|
for (const verb of COLLECTION_ABILITY_VERBS) {
|
|
124
|
-
expect(registry.has(
|
|
181
|
+
expect(registry.has(documentAbilityKey(definition, verb))).toBe(true);
|
|
125
182
|
}
|
|
126
183
|
});
|
|
184
|
+
it('matches the keys produced by registerSingletonAbilities', () => {
|
|
185
|
+
const registry = new AbilityRegistry();
|
|
186
|
+
const definition = siteSettings();
|
|
187
|
+
registerSingletonAbilities(registry, definition);
|
|
188
|
+
for (const verb of SINGLETON_ABILITY_VERBS) {
|
|
189
|
+
expect(registry.has(documentAbilityKey(definition, verb))).toBe(true);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
it('rejects collection-only verbs for singleton descriptors at compile time and runtime', () => {
|
|
193
|
+
const singleton = siteSettings();
|
|
194
|
+
const compileOnlyInvalidCall = () => {
|
|
195
|
+
documentAbilityKey(singleton,
|
|
196
|
+
// @ts-expect-error create is not a singleton ability verb
|
|
197
|
+
'create');
|
|
198
|
+
};
|
|
199
|
+
expect(compileOnlyInvalidCall).toBeTypeOf('function');
|
|
200
|
+
const untypedKey = documentAbilityKey;
|
|
201
|
+
expect(() => untypedKey(singleton, 'create')).toThrow(/verb 'create'.*singleton/);
|
|
202
|
+
expect(() => untypedKey(singleton, 'delete')).toThrow(/verb 'delete'.*singleton/);
|
|
203
|
+
expect(() => untypedKey(singleton, 'reindex')).toThrow(/verb 'reindex'.*singleton/);
|
|
204
|
+
});
|
|
127
205
|
});
|
|
@@ -122,3 +122,6 @@ export type CollectionFieldsAllLocalesByPath = {
|
|
|
122
122
|
minimal: MinimalFieldsAllLocales;
|
|
123
123
|
};
|
|
124
124
|
export type CollectionPath = keyof CollectionFieldsByPath;
|
|
125
|
+
export type SingletonFieldsByPath = Record<never, never>;
|
|
126
|
+
export type SingletonFieldsAllLocalesByPath = Record<never, never>;
|
|
127
|
+
export type SingletonPath = keyof SingletonFieldsByPath;
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type CollectionDefinition } from '../@types/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* Emit deterministic application collection types from evaluated runtime definitions.
|
|
11
11
|
* The returned hash is SHA-256 over the versioned format domain and generated module body.
|
package/dist/codegen/index.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
-
|
|
10
|
-
const
|
|
9
|
+
import { isSingleton, } from '../@types/index.js';
|
|
10
|
+
const FORMAT_VERSION = 3;
|
|
11
|
+
const HASH_DOMAIN = '@byline/core/codegen:collection-types:v3\n';
|
|
11
12
|
const IMPORT_ORDER = ['JsonObject', 'JsonValue', 'RelatedDocumentValue', 'StoredFileValue'];
|
|
12
13
|
function compareStrings(a, b) {
|
|
13
14
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -285,6 +286,8 @@ function emitBody(analysis) {
|
|
|
285
286
|
alias: collectionNames.get(definition) ?? 'UnnamedFields',
|
|
286
287
|
definition,
|
|
287
288
|
}));
|
|
289
|
+
const multiCollectionContracts = collectionContracts.filter((collection) => !isSingleton(collection.definition));
|
|
290
|
+
const singletonContracts = collectionContracts.filter((collection) => isSingleton(collection.definition));
|
|
288
291
|
const blockAliasByKey = new Map(blocks.map((block) => [block.key, block.alias]));
|
|
289
292
|
function fieldSetType(fields, mode) {
|
|
290
293
|
return objectType(fields.map((field) => ({
|
|
@@ -400,15 +403,25 @@ function emitBody(analysis) {
|
|
|
400
403
|
for (const collection of [...collectionContracts].sort((a, b) => compareStrings(a.alias, b.alias))) {
|
|
401
404
|
declarations.push(`export type ${collection.alias} = ${fieldSetType(collection.definition.fields, 'single').join('\n')}`, '', `export type ${collection.alias}AllLocales = ${fieldSetType(collection.definition.fields, 'all').join('\n')}`, '');
|
|
402
405
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
function emitRegistry(name, contracts, suffix) {
|
|
407
|
+
if (contracts.length === 0) {
|
|
408
|
+
declarations.push(`export type ${name} = {}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
declarations.push(`export type ${name} = {`);
|
|
412
|
+
for (const contract of contracts) {
|
|
413
|
+
declarations.push(` ${propertyKey(contract.definition.path)}: ${contract.alias}${suffix}`);
|
|
414
|
+
}
|
|
415
|
+
declarations.push('}');
|
|
410
416
|
}
|
|
411
|
-
|
|
417
|
+
emitRegistry('CollectionFieldsByPath', multiCollectionContracts, '');
|
|
418
|
+
declarations.push('');
|
|
419
|
+
emitRegistry('CollectionFieldsAllLocalesByPath', multiCollectionContracts, 'AllLocales');
|
|
420
|
+
declarations.push('', 'export type CollectionPath = keyof CollectionFieldsByPath', '');
|
|
421
|
+
emitRegistry('SingletonFieldsByPath', singletonContracts, '');
|
|
422
|
+
declarations.push('');
|
|
423
|
+
emitRegistry('SingletonFieldsAllLocalesByPath', singletonContracts, 'AllLocales');
|
|
424
|
+
declarations.push('', 'export type SingletonPath = keyof SingletonFieldsByPath');
|
|
412
425
|
const usedImports = IMPORT_ORDER.filter((name) => imports.has(name));
|
|
413
426
|
const importLines = usedImports.length === 0
|
|
414
427
|
? []
|
|
@@ -424,16 +437,19 @@ function emitBody(analysis) {
|
|
|
424
437
|
.replace(/\n+$/, '')
|
|
425
438
|
.split('\n')
|
|
426
439
|
.map((line) => (line.length === 0 ? line : ` ${line}`));
|
|
427
|
-
// Register the app's collection
|
|
428
|
-
// declaration merge
|
|
429
|
-
// including the `@byline/client/server` getters — then resolves
|
|
430
|
-
// `
|
|
431
|
-
//
|
|
432
|
-
//
|
|
440
|
+
// Register the app's collection and singleton registries with @byline/client
|
|
441
|
+
// through a second declaration merge. Every bare `BylineClient` in the app's
|
|
442
|
+
// program — including the `@byline/client/server` getters — then resolves its
|
|
443
|
+
// collection generic to `CollectionFieldsByPath`. The singleton member is
|
|
444
|
+
// emitted now so Plan 3's parallel `RegisteredSingletons` fallback can resolve
|
|
445
|
+
// `SingletonFieldsByPath` through the same merge without runtime definitions.
|
|
446
|
+
// Both `@byline/generated-types` and `@byline/client` must be resolvable in
|
|
447
|
+
// the consuming program; apps that generate types depend on both.
|
|
433
448
|
const registerLines = [
|
|
434
449
|
"declare module '@byline/client' {",
|
|
435
450
|
' interface Register {',
|
|
436
451
|
" collections: import('@byline/generated-types').CollectionFieldsByPath",
|
|
452
|
+
" singletons: import('@byline/generated-types').SingletonFieldsByPath",
|
|
437
453
|
' }',
|
|
438
454
|
'}',
|
|
439
455
|
];
|
|
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
6
|
+
import { defineCollection, defineSingleton, } from '../@types/collection-types.js';
|
|
6
7
|
import { AllFieldsCollection, AllFieldsCollections, } from './fixtures/all-fields.js';
|
|
7
8
|
import { emitCollectionTypes } from './index.js';
|
|
8
9
|
const fixtureDirectory = resolve(dirname(fileURLToPath(import.meta.url)), 'fixtures');
|
|
@@ -24,16 +25,19 @@ describe('emitCollectionTypes', () => {
|
|
|
24
25
|
expectTypeOf().toEqualTypeOf();
|
|
25
26
|
expectTypeOf().toEqualTypeOf();
|
|
26
27
|
expectTypeOf().toEqualTypeOf();
|
|
28
|
+
expectTypeOf().toEqualTypeOf();
|
|
27
29
|
});
|
|
28
30
|
it('emits a versioned header, verifiable body hash, and canonical formatting', () => {
|
|
29
31
|
const { source, hash } = emitCollectionTypes([collection('plain', [])]);
|
|
30
32
|
const body = source.split('\n').slice(4).join('\n');
|
|
31
|
-
expect(source).toMatch(/^\/\/ Generated by @byline\/core\/codegen\n\/\/ Format version:
|
|
32
|
-
expect(hash).toBe(createHash('sha256').update(`@byline/core/codegen:collection-types:
|
|
33
|
+
expect(source).toMatch(/^\/\/ Generated by @byline\/core\/codegen\n\/\/ Format version: 3\n\/\/ Hash: [a-f0-9]{64}\n\n/);
|
|
34
|
+
expect(hash).toBe(createHash('sha256').update(`@byline/core/codegen:collection-types:v3\n${body}`).digest('hex'));
|
|
33
35
|
expect(source).toContain(`// Hash: ${hash}`);
|
|
34
36
|
expect(source.endsWith('\n')).toBe(true);
|
|
35
37
|
expect(source).not.toContain('\r');
|
|
36
38
|
expect(source).not.toContain(';');
|
|
39
|
+
expect(source).toContain(' export type SingletonFieldsByPath = {}');
|
|
40
|
+
expect(source).toContain(' export type SingletonFieldsAllLocalesByPath = {}');
|
|
37
41
|
});
|
|
38
42
|
it('declares every type inside the @byline/generated-types augmentation block', () => {
|
|
39
43
|
const { source } = emitCollectionTypes([collection('plain', [{ name: 'title', type: 'text' }])]);
|
|
@@ -49,10 +53,39 @@ describe('emitCollectionTypes', () => {
|
|
|
49
53
|
"declare module '@byline/client' {",
|
|
50
54
|
' interface Register {',
|
|
51
55
|
" collections: import('@byline/generated-types').CollectionFieldsByPath",
|
|
56
|
+
" singletons: import('@byline/generated-types').SingletonFieldsByPath",
|
|
52
57
|
' }',
|
|
53
58
|
'}',
|
|
54
59
|
].join('\n'));
|
|
55
60
|
});
|
|
61
|
+
it('partitions collection and singleton registries and registers both with the client', () => {
|
|
62
|
+
const definitions = [
|
|
63
|
+
defineCollection({
|
|
64
|
+
path: 'articles',
|
|
65
|
+
labels: { singular: 'Article', plural: 'Articles' },
|
|
66
|
+
fields: [{ name: 'title', type: 'text' }],
|
|
67
|
+
}),
|
|
68
|
+
defineSingleton({
|
|
69
|
+
path: 'site-settings',
|
|
70
|
+
label: 'Site settings',
|
|
71
|
+
fields: [{ name: 'siteName', type: 'text', localized: true }],
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
const { source } = emitCollectionTypes(definitions);
|
|
75
|
+
expect(source).toContain('export type SingletonPath = keyof SingletonFieldsByPath');
|
|
76
|
+
expect(source).toContain('export type CollectionPath = keyof CollectionFieldsByPath');
|
|
77
|
+
expect(source).toMatch(/export type SingletonFieldsByPath = \{[^}]*'site-settings'/);
|
|
78
|
+
expect(source).toMatch(/export type SingletonFieldsAllLocalesByPath = \{[^}]*'site-settings'/);
|
|
79
|
+
expect(source).not.toMatch(/export type CollectionFieldsByPath = \{[^}]*'site-settings'/);
|
|
80
|
+
expect(source).not.toMatch(/export type CollectionFieldsAllLocalesByPath = \{[^}]*'site-settings'/);
|
|
81
|
+
expect(source).toMatch(/export type CollectionFieldsByPath = \{[^}]*articles/);
|
|
82
|
+
expect(source).not.toMatch(/export type SingletonFieldsByPath = \{[^}]*articles/);
|
|
83
|
+
expect(source).toContain("singletons: import('@byline/generated-types').SingletonFieldsByPath");
|
|
84
|
+
expectTypeOf().toEqualTypeOf();
|
|
85
|
+
expectTypeOf().toEqualTypeOf();
|
|
86
|
+
expectTypeOf().toEqualTypeOf();
|
|
87
|
+
expectTypeOf().toEqualTypeOf();
|
|
88
|
+
});
|
|
56
89
|
it('sorts collections and declarations without changing field, select, or block union order', () => {
|
|
57
90
|
const alpha = collection('alpha', [
|
|
58
91
|
{
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { CollectionDefinition, CollectionHooks, CollectionHooksLoader, FileField, ImageField, ServerHooksConfig, UploadHooks, UploadHooksLoader } from '../@types/index.js';
|
|
2
|
-
type
|
|
1
|
+
import type { CollectionDefinition, CollectionHooks, CollectionHooksLoader, FileField, ImageField, ServerHooksConfig, SingletonHooks, SingletonHooksLoader, UploadHooks, UploadHooksLoader } from '../@types/index.js';
|
|
2
|
+
type DocumentHookValue = CollectionHooks | CollectionHooksLoader | SingletonHooks | SingletonHooksLoader;
|
|
3
3
|
type UploadHookValue = UploadHooks | UploadHooksLoader;
|
|
4
4
|
type UploadField = ImageField | FileField;
|
|
5
5
|
export type HookAttachmentOperation = {
|
|
6
6
|
kind: 'collection';
|
|
7
7
|
target: CollectionDefinition;
|
|
8
|
-
hooks:
|
|
8
|
+
hooks: DocumentHookValue | undefined;
|
|
9
9
|
} | {
|
|
10
10
|
kind: 'upload';
|
|
11
11
|
target: UploadField;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertHooksMatchDefinition } from '../@types/collection-types.js';
|
|
1
2
|
import { formatDeclarationPath, walkFieldDeclarations } from '../paths/index.js';
|
|
2
3
|
const registryCollectionHooks = new WeakMap();
|
|
3
4
|
const registryUploadHooks = new WeakMap();
|
|
@@ -61,6 +62,8 @@ export function commitHookAttachment(operations) {
|
|
|
61
62
|
}
|
|
62
63
|
function planCollectionAttachment(collection, requested, operations) {
|
|
63
64
|
const owned = registryCollectionHooks.get(collection);
|
|
65
|
+
assertInlineHookFamily(collection, collection.hooks);
|
|
66
|
+
assertInlineHookFamily(collection, requested);
|
|
64
67
|
assertOwnershipIntact(`collection "${collection.path}"`, collection.hooks, owned);
|
|
65
68
|
if (requested === undefined) {
|
|
66
69
|
if (owned !== undefined) {
|
|
@@ -75,6 +78,11 @@ function planCollectionAttachment(collection, requested, operations) {
|
|
|
75
78
|
operations.push({ kind: 'collection', target: collection, hooks: requested });
|
|
76
79
|
}
|
|
77
80
|
}
|
|
81
|
+
function assertInlineHookFamily(definition, hooks) {
|
|
82
|
+
if (hooks !== undefined && typeof hooks !== 'function') {
|
|
83
|
+
assertHooksMatchDefinition(definition, hooks);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
78
86
|
function planUploadAttachment(path, field, requested, operations) {
|
|
79
87
|
const owned = registryUploadHooks.get(field);
|
|
80
88
|
assertOwnershipIntact(`upload field "${path}"`, field.upload?.hooks, owned);
|