@byline/client 4.16.0 → 4.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/client.d.ts +10 -7
- package/dist/client.js +34 -5
- package/dist/collection-handle.js +8 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -0
- package/dist/read-internals.d.ts +5 -0
- package/dist/read-internals.js +2 -0
- package/dist/register.d.ts +13 -2
- package/dist/register.test.node.d.ts +8 -0
- package/dist/register.test.node.js +16 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +17 -1
- package/dist/singleton-handle.d.ts +44 -0
- package/dist/singleton-handle.js +155 -0
- package/dist/types.d.ts +15 -0
- package/package.json +7 -8
package/dist/client.d.ts
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
import { type RequestContext } from '@byline/auth';
|
|
9
9
|
import type { BylineLogger, CollectionDefinition, CollectionRegistry, IDbAdapter, IStorageProvider, RichTextPopulateFn, RichTextToTextFn, SearchProvider, SlugifierFn } from '@byline/core';
|
|
10
10
|
import { CollectionHandle } from './collection-handle.js';
|
|
11
|
-
import
|
|
11
|
+
import { SingletonHandle } from './singleton-handle.js';
|
|
12
|
+
import type { RegisteredCollections, RegisteredSingletons } from './register.js';
|
|
12
13
|
import type { BylineClientConfig, ClientSearchResults, ZoneSearchOptions } from './types.js';
|
|
13
14
|
/**
|
|
14
15
|
* The main Byline client instance. Holds the database adapter, collection
|
|
15
16
|
* definitions, and optional storage provider. Use `collection(path)` to get
|
|
16
17
|
* a scoped handle for querying and mutating documents.
|
|
17
18
|
*/
|
|
18
|
-
export declare class BylineClient<
|
|
19
|
+
export declare class BylineClient<TCollections extends CollectionRegistry = RegisteredCollections, TSingletons extends CollectionRegistry = RegisteredSingletons> {
|
|
19
20
|
readonly db: IDbAdapter;
|
|
20
21
|
readonly collections: readonly CollectionDefinition[];
|
|
21
22
|
readonly storage: IStorageProvider | undefined;
|
|
@@ -55,7 +56,9 @@ export declare class BylineClient<TRegistry extends CollectionRegistry = Registe
|
|
|
55
56
|
* Get a handle scoped to a single collection. All subsequent read/write
|
|
56
57
|
* operations on the handle are performed against this collection.
|
|
57
58
|
*/
|
|
58
|
-
collection<TPath extends keyof
|
|
59
|
+
collection<TPath extends keyof TCollections & string>(path: TPath): CollectionHandle<TCollections[TPath]>;
|
|
60
|
+
/** Get the deliberately narrow, document-ID-free handle for a singleton slot. */
|
|
61
|
+
singleton<TPath extends keyof TSingletons & string>(path: TPath): SingletonHandle<TSingletons[TPath]>;
|
|
59
62
|
/**
|
|
60
63
|
* Resolve a collection path to its database row id and current schema
|
|
61
64
|
* version. Cached for the lifetime of this client instance. The version
|
|
@@ -72,8 +75,8 @@ export declare class BylineClient<TRegistry extends CollectionRegistry = Registe
|
|
|
72
75
|
resolveCollectionId(path: string): Promise<string>;
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
|
-
* Create a new Byline client instance.
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
78
|
+
* Create a new Byline client instance. Both registry generics default through
|
|
79
|
+
* `Register`, so an app's generated collection and singleton types make the
|
|
80
|
+
* returned client fully typed with no explicit generic.
|
|
78
81
|
*/
|
|
79
|
-
export declare function createBylineClient<
|
|
82
|
+
export declare function createBylineClient<TCollections extends CollectionRegistry = RegisteredCollections, TSingletons extends CollectionRegistry = RegisteredSingletons>(config: BylineClientConfig): BylineClient<TCollections, TSingletons>;
|
package/dist/client.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { ERR_UNAUTHENTICATED } from '@byline/auth';
|
|
9
|
-
import { ERR_NOT_FOUND, getLogger } from '@byline/core';
|
|
9
|
+
import { ERR_NOT_FOUND, ERR_VALIDATION, getLogger, isSingleton } from '@byline/core';
|
|
10
10
|
import { CollectionHandle } from './collection-handle.js';
|
|
11
11
|
import { zoneSearch } from './search.js';
|
|
12
|
+
import { SingletonHandle } from './singleton-handle.js';
|
|
12
13
|
/**
|
|
13
14
|
* Resolve a logger for the client in priority order:
|
|
14
15
|
* 1. explicit `config.logger`
|
|
@@ -138,12 +139,40 @@ export class BylineClient {
|
|
|
138
139
|
message: `Collection not found: '${path}'`,
|
|
139
140
|
details: {
|
|
140
141
|
collectionPath: path,
|
|
141
|
-
available: this.collections
|
|
142
|
+
available: this.collections
|
|
143
|
+
.filter((definition) => !isSingleton(definition))
|
|
144
|
+
.map((c) => c.path),
|
|
142
145
|
},
|
|
143
146
|
});
|
|
144
147
|
}
|
|
148
|
+
if (isSingleton(definition)) {
|
|
149
|
+
throw ERR_VALIDATION({
|
|
150
|
+
message: `'${path}' is a singleton; use client.singleton('${path}')`,
|
|
151
|
+
details: { path, actualKind: 'singleton', expectedKind: 'collection' },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
145
154
|
return new CollectionHandle(this, definition);
|
|
146
155
|
}
|
|
156
|
+
/** Get the deliberately narrow, document-ID-free handle for a singleton slot. */
|
|
157
|
+
singleton(path) {
|
|
158
|
+
const definition = this.collections.find((c) => c.path === path);
|
|
159
|
+
if (!definition) {
|
|
160
|
+
throw ERR_NOT_FOUND({
|
|
161
|
+
message: `Singleton not found: '${path}'`,
|
|
162
|
+
details: {
|
|
163
|
+
singletonPath: path,
|
|
164
|
+
available: this.collections.filter(isSingleton).map((c) => c.path),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (!isSingleton(definition)) {
|
|
169
|
+
throw ERR_VALIDATION({
|
|
170
|
+
message: `'${path}' is a collection; use client.collection('${path}')`,
|
|
171
|
+
details: { path, actualKind: 'collection', expectedKind: 'singleton' },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return new SingletonHandle(this, definition);
|
|
175
|
+
}
|
|
147
176
|
/**
|
|
148
177
|
* Resolve a collection path to its database row id and current schema
|
|
149
178
|
* version. Cached for the lifetime of this client instance. The version
|
|
@@ -174,9 +203,9 @@ export class BylineClient {
|
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
/**
|
|
177
|
-
* Create a new Byline client instance.
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
206
|
+
* Create a new Byline client instance. Both registry generics default through
|
|
207
|
+
* `Register`, so an app's generated collection and singleton types make the
|
|
208
|
+
* returned client fully typed with no explicit generic.
|
|
180
209
|
*/
|
|
181
210
|
export function createBylineClient(config) {
|
|
182
211
|
return new BylineClient(config);
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { applyAfterRead, assertActorCanPerform, buildSearchDocument, cancelDocumentScheduledPublish, changeDocumentStatus, confirmDocumentScheduledPublish, createDocument, createReadContext, createRichTextDocumentReader, deleteDocument, ERR_VALIDATION, getDocumentScheduledPublish, parseSort, parseWhere, placeTreeNode as placeTreeNodeLifecycle, populateDocuments, populateRichTextFields, removeFromTree as removeFromTreeLifecycle, resolveIdentityField, restoreDocumentVersion, scheduleDocumentPublish, unpublishDocument, updateDocument, } from '@byline/core';
|
|
9
9
|
import { getReadSecurityDomain, resolveReadRequestContext, resolveReadSecurityFilters, } from './read-context.js';
|
|
10
|
+
import { expectedDocumentId } from './read-internals.js';
|
|
10
11
|
import { shapeDocument, shapePopulatedInPlace } from './response.js';
|
|
11
|
-
import { assertSearchQueryLength, finalizeSearchHits } from './search.js';
|
|
12
|
+
import { assertSearchFieldScope, assertSearchQueryLength, finalizeSearchHits } from './search.js';
|
|
12
13
|
function toScheduleInfo(schedule) {
|
|
13
14
|
const { executionToken: _executionToken, executionExpiresAt: _executionExpiresAt, ...info } = schedule;
|
|
14
15
|
return info;
|
|
@@ -147,8 +148,10 @@ export class CollectionHandle {
|
|
|
147
148
|
'see `@byline/search-postgres` → `postgresSearch()` for the built-in driver.',
|
|
148
149
|
});
|
|
149
150
|
}
|
|
151
|
+
assertSearchFieldScope(provider, options.fieldScope);
|
|
150
152
|
const results = await provider.search({
|
|
151
153
|
query: options.query,
|
|
154
|
+
fieldScope: options.fieldScope,
|
|
152
155
|
matching: options.matching,
|
|
153
156
|
collectionPath: this.definition.path,
|
|
154
157
|
locale: options.locale ?? this.client.defaultLocale,
|
|
@@ -240,7 +243,7 @@ export class CollectionHandle {
|
|
|
240
243
|
*/
|
|
241
244
|
async reindex() {
|
|
242
245
|
const requestContext = await this.client.resolveRequestContext();
|
|
243
|
-
assertActorCanPerform(requestContext, this.definition
|
|
246
|
+
assertActorCanPerform(requestContext, this.definition, 'reindex');
|
|
244
247
|
const provider = this.client.searchProvider;
|
|
245
248
|
const result = { collectionPath: this.definition.path, documents: 0, indexed: 0 };
|
|
246
249
|
if (provider == null || this.definition.search == null)
|
|
@@ -612,6 +615,7 @@ export class CollectionHandle {
|
|
|
612
615
|
* `null` without revealing ownership.
|
|
613
616
|
*/
|
|
614
617
|
async findByVersion(versionId, options = {}) {
|
|
618
|
+
const boundOptions = options;
|
|
615
619
|
const readCtx = options._readContext ?? createReadContext();
|
|
616
620
|
const requestContext = await this.resolveAndAssertRead('any', readCtx);
|
|
617
621
|
const filters = await this.resolveBeforeReadFilters(requestContext, readCtx, options._bypassBeforeRead);
|
|
@@ -619,6 +623,7 @@ export class CollectionHandle {
|
|
|
619
623
|
const locale = options.locale ?? this.client.defaultLocale;
|
|
620
624
|
const raw = await this.client.db.queries.documents.getDocumentByVersion({
|
|
621
625
|
document_version_id: versionId,
|
|
626
|
+
document_id: boundOptions[expectedDocumentId],
|
|
622
627
|
locale,
|
|
623
628
|
collection_id: collectionId,
|
|
624
629
|
filters,
|
|
@@ -824,7 +829,7 @@ export class CollectionHandle {
|
|
|
824
829
|
*/
|
|
825
830
|
async resolveAndAssertRead(readMode, readContext) {
|
|
826
831
|
const requestContext = await resolveReadRequestContext(this.client, readContext, readMode);
|
|
827
|
-
assertActorCanPerform(requestContext, this.definition
|
|
832
|
+
assertActorCanPerform(requestContext, this.definition, 'read');
|
|
828
833
|
return requestContext;
|
|
829
834
|
}
|
|
830
835
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
export { type AuditLogEntry, type AuditLogPage, type ChangeStatusResult, type CreateDocumentResult, type CycleRelationValue, createReadContext, type DeleteDocumentResult, ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateSpec, type ReadContext, type ReadMode, type RestoreVersionResult, type UnpublishResult, type UnresolvedRelationValue, type UpdateDocumentResult, } from '@byline/core';
|
|
8
|
+
export { type AuditLogEntry, type AuditLogPage, type ChangeStatusResult, type CreateDocumentResult, type CycleRelationValue, createReadContext, type DeleteDocumentResult, ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateSpec, type ReadContext, type ReadMode, type RestoreVersionResult, type SingletonSaveResult, type UnpublishResult, type UnresolvedRelationValue, type UpdateDocumentResult, } from '@byline/core';
|
|
9
9
|
export { BylineClient, createBylineClient } from './client.js';
|
|
10
10
|
export { CollectionHandle } from './collection-handle.js';
|
|
11
|
-
export
|
|
12
|
-
export type {
|
|
11
|
+
export { SingletonHandle } from './singleton-handle.js';
|
|
12
|
+
export type { Register, RegisteredCollections, RegisteredSingletons } from './register.js';
|
|
13
|
+
export type { AuditLogOptions, BylineClientConfig, ClientDocument, ClientSearchResults, CollectionSearchOptions, ConfirmScheduledPublishOptions, CreateOptions, DocumentPublishScheduleInfo, FilterOperators, FindByIdOptions, FindByPathOptions, FindOneOptions, FindOptions, FindResult, GetAncestorsOptions, GetSingletonOptions, GetSubtreeOptions, GetTreeParentOptions, HydratedSearchHit, PlaceTreeNodeOptions, PopulatedRelation, ReindexResult, RemoveFromTreeOptions, SchedulePublishOptions, SearchFacetBucket, SearchHit, SearchResults, SingletonDocument, SortDirection, SortSpec, TreeNode, TreeParentResult, UpdateOptions, UpdateSingletonOptions, WhereClause, WhereValue, WithPopulated, WithPopulatedMany, ZoneSearchOptions, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@
|
|
|
8
8
|
export { createReadContext, ERR_CONFLICT, ERR_INVALID_TRANSITION, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_VALIDATION, } from '@byline/core';
|
|
9
9
|
export { BylineClient, createBylineClient } from './client.js';
|
|
10
10
|
export { CollectionHandle } from './collection-handle.js';
|
|
11
|
+
export { SingletonHandle } from './singleton-handle.js';
|
package/dist/register.d.ts
CHANGED
|
@@ -15,14 +15,16 @@ import type { CollectionRegistry } from '@byline/core';
|
|
|
15
15
|
* declare module '@byline/client' {
|
|
16
16
|
* interface Register {
|
|
17
17
|
* collections: CollectionFieldsByPath
|
|
18
|
+
* singletons: SingletonFieldsByPath
|
|
18
19
|
* }
|
|
19
20
|
* }
|
|
20
21
|
* ```
|
|
21
22
|
*
|
|
22
23
|
* Once merged, every bare `BylineClient` in the app's program — including
|
|
23
24
|
* the getters on `@byline/client/server` — resolves to
|
|
24
|
-
* `BylineClient<CollectionFieldsByPath>`, so
|
|
25
|
-
*
|
|
25
|
+
* `BylineClient<CollectionFieldsByPath, SingletonFieldsByPath>`, so
|
|
26
|
+
* `client.collection('news')` and `client.singleton('site-settings')`
|
|
27
|
+
* autocomplete their respective paths and return the generated field shapes
|
|
26
28
|
* with no per-call generics and no type assertions.
|
|
27
29
|
*
|
|
28
30
|
* Exactly one application per TypeScript program can augment this
|
|
@@ -42,3 +44,12 @@ export interface Register {
|
|
|
42
44
|
export type RegisteredCollections = Register extends {
|
|
43
45
|
collections: infer TCollections extends CollectionRegistry;
|
|
44
46
|
} ? TCollections : CollectionRegistry;
|
|
47
|
+
/**
|
|
48
|
+
* The app's registered singleton registry, or the same loose
|
|
49
|
+
* `CollectionRegistry` fallback used by collections when no augmentation is
|
|
50
|
+
* present. Keeping this as a conditional lookup preserves the deliberately
|
|
51
|
+
* empty declaration-merge target above.
|
|
52
|
+
*/
|
|
53
|
+
export type RegisteredSingletons = Register extends {
|
|
54
|
+
singletons: infer TSingletons extends CollectionRegistry;
|
|
55
|
+
} ? TSingletons : CollectionRegistry;
|
|
@@ -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,16 @@
|
|
|
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, expectTypeOf, it } from 'vitest';
|
|
9
|
+
describe('unaugmented client registration fallback', () => {
|
|
10
|
+
it('keeps both path registries loose outside an application program', () => {
|
|
11
|
+
expectTypeOf().toEqualTypeOf();
|
|
12
|
+
expectTypeOf().toEqualTypeOf();
|
|
13
|
+
expectTypeOf().toEqualTypeOf();
|
|
14
|
+
expectTypeOf().toEqualTypeOf();
|
|
15
|
+
});
|
|
16
|
+
});
|
package/dist/search.d.ts
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
* into shaped `ClientDocument`s in the same query.
|
|
18
18
|
*/
|
|
19
19
|
import type { RequestContext } from '@byline/auth';
|
|
20
|
-
import type { ReadContext, ReadMode, SearchHit } from '@byline/core';
|
|
20
|
+
import type { ReadContext, ReadMode, SearchHit, SearchProvider } from '@byline/core';
|
|
21
21
|
import type { BylineClient } from './client.js';
|
|
22
22
|
import type { ClientSearchResults, HydratedSearchHit, ZoneSearchOptions } from './types.js';
|
|
23
|
+
/** Validate the optional logical body-field scope before calling a provider. */
|
|
24
|
+
export declare function assertSearchFieldScope(provider: SearchProvider, fieldScope: string[] | undefined): void;
|
|
23
25
|
export interface FinalizeSearchHitsParams {
|
|
24
26
|
client: BylineClient<any>;
|
|
25
27
|
requestContext: RequestContext;
|
package/dist/search.js
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { assertActorCanPerform, createReadContext, ERR_VALIDATION, getCollectionAdminConfig, MAX_SEARCH_QUERY_LENGTH, resolveSearchZones, } from '@byline/core';
|
|
9
9
|
import { resolveReadRequestContext, resolveReadSecurityFilters } from './read-context.js';
|
|
10
|
+
/** Validate the optional logical body-field scope before calling a provider. */
|
|
11
|
+
export function assertSearchFieldScope(provider, fieldScope) {
|
|
12
|
+
if (fieldScope == null)
|
|
13
|
+
return;
|
|
14
|
+
if (fieldScope.length === 0 ||
|
|
15
|
+
fieldScope.some((field) => typeof field !== 'string' || field.trim().length === 0)) {
|
|
16
|
+
throw ERR_VALIDATION({ message: 'Search fieldScope must contain one or more field names.' });
|
|
17
|
+
}
|
|
18
|
+
if (!provider.capabilities.fullText.fieldScope) {
|
|
19
|
+
throw ERR_VALIDATION({
|
|
20
|
+
message: 'The registered search provider does not support field-scoped full-text queries.',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
/**
|
|
11
25
|
* Authorise (and optionally hydrate) provider hits, per collection:
|
|
12
26
|
*
|
|
@@ -152,7 +166,7 @@ export async function zoneSearch(client, options) {
|
|
|
152
166
|
let firstAbilityError;
|
|
153
167
|
for (const member of members) {
|
|
154
168
|
try {
|
|
155
|
-
assertActorCanPerform(requestContext, member
|
|
169
|
+
assertActorCanPerform(requestContext, member, 'read');
|
|
156
170
|
readable.add(member.path);
|
|
157
171
|
}
|
|
158
172
|
catch (err) {
|
|
@@ -163,6 +177,7 @@ export async function zoneSearch(client, options) {
|
|
|
163
177
|
if (readable.size === 0) {
|
|
164
178
|
throw firstAbilityError;
|
|
165
179
|
}
|
|
180
|
+
assertSearchFieldScope(provider, options.fieldScope);
|
|
166
181
|
let aggregateRestricted = readable.size !== members.length;
|
|
167
182
|
if (!options._bypassBeforeRead) {
|
|
168
183
|
for (const member of members) {
|
|
@@ -176,6 +191,7 @@ export async function zoneSearch(client, options) {
|
|
|
176
191
|
}
|
|
177
192
|
const results = await provider.search({
|
|
178
193
|
query: options.query,
|
|
194
|
+
fieldScope: options.fieldScope,
|
|
179
195
|
matching: options.matching,
|
|
180
196
|
zone: options.zone,
|
|
181
197
|
locale: options.locale ?? client.defaultLocale,
|
|
@@ -0,0 +1,44 @@
|
|
|
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 type { ChangeStatusResult, SingletonDefinition, SingletonSaveResult, UnpublishResult } from '@byline/core';
|
|
9
|
+
import type { BylineClient } from './client.js';
|
|
10
|
+
import type { ClientDocument, ConfirmScheduledPublishOptions, DocumentPublishScheduleInfo, FindByVersionOptions, FindResult, GetSingletonOptions, HistoryOptions, SchedulePublishOptions, SingletonDocument, UpdateSingletonOptions } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* A document-ID-free handle for one registered singleton slot.
|
|
13
|
+
*
|
|
14
|
+
* Reads deliberately delegate to the collection document reader only after
|
|
15
|
+
* the singleton read ability and slot mapping have been resolved in that
|
|
16
|
+
* order. This keeps reconstruction, hooks, populate, and response shaping
|
|
17
|
+
* identical without exposing collection-only operations on this handle.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SingletonHandle<TFields extends Record<string, any> = Record<string, any>> {
|
|
20
|
+
private readonly client;
|
|
21
|
+
private readonly definition;
|
|
22
|
+
private readonly reader;
|
|
23
|
+
constructor(client: BylineClient<any, any>, definition: SingletonDefinition);
|
|
24
|
+
get(options?: GetSingletonOptions<TFields>): Promise<SingletonDocument<TFields> | null>;
|
|
25
|
+
update(data: TFields, options?: UpdateSingletonOptions): Promise<SingletonSaveResult>;
|
|
26
|
+
changeStatus(nextStatus: string): Promise<ChangeStatusResult>;
|
|
27
|
+
unpublish(): Promise<UnpublishResult>;
|
|
28
|
+
schedulePublish(options: SchedulePublishOptions): Promise<DocumentPublishScheduleInfo>;
|
|
29
|
+
confirmScheduledPublish(options: ConfirmScheduledPublishOptions): Promise<DocumentPublishScheduleInfo>;
|
|
30
|
+
cancelScheduledPublish(): Promise<DocumentPublishScheduleInfo | null>;
|
|
31
|
+
getScheduledPublish(): Promise<DocumentPublishScheduleInfo | null>;
|
|
32
|
+
history<F = TFields>(options?: HistoryOptions): Promise<FindResult<F>>;
|
|
33
|
+
findByVersion<F = TFields>(versionId: string, options?: FindByVersionOptions<F>): Promise<ClientDocument<F> | null>;
|
|
34
|
+
restoreVersion(sourceVersionId: string): Promise<SingletonSaveResult>;
|
|
35
|
+
copyToLocale(args: {
|
|
36
|
+
sourceLocale: string;
|
|
37
|
+
targetLocale: string;
|
|
38
|
+
overwrite?: boolean;
|
|
39
|
+
}): Promise<SingletonSaveResult>;
|
|
40
|
+
private authorizeRead;
|
|
41
|
+
private buildAuthorizedLifecycleContext;
|
|
42
|
+
private resolveMappedDocumentId;
|
|
43
|
+
private resolveRequiredDocument;
|
|
44
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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 { assertActorCanPerform, cancelDocumentScheduledPublish, changeDocumentStatus, confirmDocumentScheduledPublish, copySingletonToLocale, createReadContext, ERR_NOT_FOUND, getDocumentScheduledPublish, resolveSingletonDocumentId, restoreSingletonVersion, scheduleDocumentPublish, unpublishDocument, updateSingleton, } from '@byline/core';
|
|
9
|
+
import { CollectionHandle } from './collection-handle.js';
|
|
10
|
+
import { resolveReadRequestContext } from './read-context.js';
|
|
11
|
+
import { expectedDocumentId } from './read-internals.js';
|
|
12
|
+
function toScheduleInfo(schedule) {
|
|
13
|
+
const { executionToken: _executionToken, executionExpiresAt: _executionExpiresAt, ...info } = schedule;
|
|
14
|
+
return info;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A document-ID-free handle for one registered singleton slot.
|
|
18
|
+
*
|
|
19
|
+
* Reads deliberately delegate to the collection document reader only after
|
|
20
|
+
* the singleton read ability and slot mapping have been resolved in that
|
|
21
|
+
* order. This keeps reconstruction, hooks, populate, and response shaping
|
|
22
|
+
* identical without exposing collection-only operations on this handle.
|
|
23
|
+
*/
|
|
24
|
+
export class SingletonHandle {
|
|
25
|
+
client;
|
|
26
|
+
definition;
|
|
27
|
+
reader;
|
|
28
|
+
constructor(client, definition) {
|
|
29
|
+
this.client = client;
|
|
30
|
+
this.definition = definition;
|
|
31
|
+
this.reader = new CollectionHandle(client, definition);
|
|
32
|
+
}
|
|
33
|
+
async get(options = {}) {
|
|
34
|
+
const readContext = options._readContext ?? createReadContext();
|
|
35
|
+
await this.authorizeRead(options.status ?? 'published', readContext);
|
|
36
|
+
const documentId = await this.resolveMappedDocumentId();
|
|
37
|
+
if (documentId == null)
|
|
38
|
+
return null;
|
|
39
|
+
const document = await this.reader.findById(documentId, {
|
|
40
|
+
...options,
|
|
41
|
+
_readContext: readContext,
|
|
42
|
+
});
|
|
43
|
+
if (document == null)
|
|
44
|
+
return null;
|
|
45
|
+
const { path: _path, ...singleton } = document;
|
|
46
|
+
return singleton;
|
|
47
|
+
}
|
|
48
|
+
async update(data, options = {}) {
|
|
49
|
+
const ctx = await this.buildAuthorizedLifecycleContext(['update']);
|
|
50
|
+
return updateSingleton(ctx, { data, ...options });
|
|
51
|
+
}
|
|
52
|
+
async changeStatus(nextStatus) {
|
|
53
|
+
const abilities = ['changeStatus'];
|
|
54
|
+
if (nextStatus === 'published')
|
|
55
|
+
abilities.push('publish');
|
|
56
|
+
const { ctx, documentId } = await this.resolveRequiredDocument(abilities);
|
|
57
|
+
return changeDocumentStatus(ctx, { documentId, nextStatus });
|
|
58
|
+
}
|
|
59
|
+
async unpublish() {
|
|
60
|
+
const { ctx, documentId } = await this.resolveRequiredDocument(['changeStatus']);
|
|
61
|
+
return unpublishDocument(ctx, { documentId });
|
|
62
|
+
}
|
|
63
|
+
async schedulePublish(options) {
|
|
64
|
+
const { ctx, documentId } = await this.resolveRequiredDocument(['changeStatus', 'publish']);
|
|
65
|
+
return toScheduleInfo(await scheduleDocumentPublish(ctx, { documentId, ...options }));
|
|
66
|
+
}
|
|
67
|
+
async confirmScheduledPublish(options) {
|
|
68
|
+
const { ctx, documentId } = await this.resolveRequiredDocument(['changeStatus', 'publish']);
|
|
69
|
+
return toScheduleInfo(await confirmDocumentScheduledPublish(ctx, { documentId, ...options }));
|
|
70
|
+
}
|
|
71
|
+
async cancelScheduledPublish() {
|
|
72
|
+
const { ctx, documentId } = await this.resolveRequiredDocument(['changeStatus', 'publish']);
|
|
73
|
+
const schedule = await cancelDocumentScheduledPublish(ctx, { documentId });
|
|
74
|
+
return schedule == null ? null : toScheduleInfo(schedule);
|
|
75
|
+
}
|
|
76
|
+
async getScheduledPublish() {
|
|
77
|
+
const ctx = await this.buildAuthorizedLifecycleContext(['changeStatus', 'publish']);
|
|
78
|
+
const documentId = await resolveSingletonDocumentId(ctx);
|
|
79
|
+
if (documentId == null)
|
|
80
|
+
return null;
|
|
81
|
+
const schedule = await getDocumentScheduledPublish(ctx, { documentId });
|
|
82
|
+
return schedule == null ? null : toScheduleInfo(schedule);
|
|
83
|
+
}
|
|
84
|
+
async history(options = {}) {
|
|
85
|
+
const readContext = options._readContext ?? createReadContext();
|
|
86
|
+
await this.authorizeRead('any', readContext);
|
|
87
|
+
const documentId = await this.resolveMappedDocumentId();
|
|
88
|
+
if (documentId == null) {
|
|
89
|
+
const page = options.page ?? 1;
|
|
90
|
+
const pageSize = options.pageSize ?? 20;
|
|
91
|
+
return { docs: [], meta: { total: 0, page, pageSize, totalPages: 0 } };
|
|
92
|
+
}
|
|
93
|
+
return this.reader.history(documentId, { ...options, _readContext: readContext });
|
|
94
|
+
}
|
|
95
|
+
async findByVersion(versionId, options = {}) {
|
|
96
|
+
const readContext = options._readContext ?? createReadContext();
|
|
97
|
+
await this.authorizeRead('any', readContext);
|
|
98
|
+
const documentId = await this.resolveMappedDocumentId();
|
|
99
|
+
if (documentId == null)
|
|
100
|
+
return null;
|
|
101
|
+
const boundOptions = {
|
|
102
|
+
...options,
|
|
103
|
+
_readContext: readContext,
|
|
104
|
+
[expectedDocumentId]: documentId,
|
|
105
|
+
};
|
|
106
|
+
return this.reader.findByVersion(versionId, boundOptions);
|
|
107
|
+
}
|
|
108
|
+
async restoreVersion(sourceVersionId) {
|
|
109
|
+
const ctx = await this.buildAuthorizedLifecycleContext(['update']);
|
|
110
|
+
return restoreSingletonVersion(ctx, { sourceVersionId });
|
|
111
|
+
}
|
|
112
|
+
async copyToLocale(args) {
|
|
113
|
+
const ctx = await this.buildAuthorizedLifecycleContext(['update']);
|
|
114
|
+
return copySingletonToLocale(ctx, { ...args, overwrite: args.overwrite ?? false });
|
|
115
|
+
}
|
|
116
|
+
async authorizeRead(readMode, readContext) {
|
|
117
|
+
const requestContext = await resolveReadRequestContext(this.client, readContext, readMode);
|
|
118
|
+
assertActorCanPerform(requestContext, this.definition, 'read');
|
|
119
|
+
return requestContext;
|
|
120
|
+
}
|
|
121
|
+
async buildAuthorizedLifecycleContext(abilities) {
|
|
122
|
+
const requestContext = await this.client.resolveRequestContext();
|
|
123
|
+
for (const ability of abilities) {
|
|
124
|
+
assertActorCanPerform(requestContext, this.definition, ability);
|
|
125
|
+
}
|
|
126
|
+
const { id: collectionId, version: collectionVersion } = await this.client.resolveCollectionRecord(this.definition.path);
|
|
127
|
+
return {
|
|
128
|
+
db: this.client.db,
|
|
129
|
+
definition: this.definition,
|
|
130
|
+
collectionId,
|
|
131
|
+
collectionVersion,
|
|
132
|
+
collectionPath: this.definition.path,
|
|
133
|
+
storage: this.client.storage,
|
|
134
|
+
logger: this.client.logger,
|
|
135
|
+
defaultLocale: this.client.defaultLocale,
|
|
136
|
+
slugifier: this.client.slugifier,
|
|
137
|
+
requestContext,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async resolveMappedDocumentId() {
|
|
141
|
+
const collectionId = await this.client.resolveCollectionId(this.definition.path);
|
|
142
|
+
return this.client.db.queries.singletons.getMappedDocumentId(collectionId);
|
|
143
|
+
}
|
|
144
|
+
async resolveRequiredDocument(abilities) {
|
|
145
|
+
const ctx = await this.buildAuthorizedLifecycleContext(abilities);
|
|
146
|
+
const documentId = await resolveSingletonDocumentId(ctx);
|
|
147
|
+
if (documentId == null) {
|
|
148
|
+
throw ERR_NOT_FOUND({
|
|
149
|
+
message: `singleton '${this.definition.path}' has not been saved`,
|
|
150
|
+
details: { singletonPath: this.definition.path },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return { ctx, documentId };
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -183,6 +183,8 @@ interface StatusControls {
|
|
|
183
183
|
export interface CollectionSearchOptions extends BeforeReadControls {
|
|
184
184
|
/** Free-text query string. */
|
|
185
185
|
query: string;
|
|
186
|
+
/** Restrict full-text matching to named fields from the collection's `search.body`. */
|
|
187
|
+
fieldScope?: string[];
|
|
186
188
|
/** Provider-neutral term and phrase matching semantics. */
|
|
187
189
|
matching?: SearchMatching;
|
|
188
190
|
/** Restrict to a single content locale (defaults to the client default). */
|
|
@@ -217,6 +219,8 @@ export interface CollectionSearchOptions extends BeforeReadControls {
|
|
|
217
219
|
export interface ZoneSearchOptions extends BeforeReadControls {
|
|
218
220
|
/** Free-text query string. */
|
|
219
221
|
query: string;
|
|
222
|
+
/** Restrict full-text matching to body-field names shared by participating collections. */
|
|
223
|
+
fieldScope?: string[];
|
|
220
224
|
/** Provider-neutral term and phrase matching semantics. */
|
|
221
225
|
matching?: SearchMatching;
|
|
222
226
|
/** The zone (named cross-collection scope) to search. */
|
|
@@ -335,6 +339,8 @@ export interface FindByIdOptions<F = Record<string, any>> extends PopulateContro
|
|
|
335
339
|
*/
|
|
336
340
|
lenient?: boolean;
|
|
337
341
|
}
|
|
342
|
+
/** Read controls for `SingletonHandle.get()`. */
|
|
343
|
+
export type GetSingletonOptions<F = Record<string, any>> = FindByIdOptions<F>;
|
|
338
344
|
export interface FindByPathOptions<F = Record<string, any>> extends PopulateControls, StatusControls, MissingLocaleControls, BeforeReadControls {
|
|
339
345
|
select?: (keyof F & string)[] | string[];
|
|
340
346
|
locale?: string;
|
|
@@ -417,6 +423,13 @@ export interface UpdateOptions {
|
|
|
417
423
|
*/
|
|
418
424
|
availableLocales?: string[];
|
|
419
425
|
}
|
|
426
|
+
/** Write controls for `SingletonHandle.update()`. */
|
|
427
|
+
export interface UpdateSingletonOptions {
|
|
428
|
+
/** Locale for field value resolution. Defaults to the client's `defaultLocale`. */
|
|
429
|
+
locale?: string;
|
|
430
|
+
/** Reject the save when this is not the slot's current version id. */
|
|
431
|
+
expectedVersionId?: string;
|
|
432
|
+
}
|
|
420
433
|
/** Optimistic, absolute-time input for `CollectionHandle.schedulePublish()`. */
|
|
421
434
|
export interface SchedulePublishOptions {
|
|
422
435
|
/** ISO instant carrying an explicit UTC offset or `Z`. */
|
|
@@ -637,6 +650,8 @@ export interface ClientDocument<F = Record<string, any>> {
|
|
|
637
650
|
*/
|
|
638
651
|
_localeAgnostic?: boolean;
|
|
639
652
|
}
|
|
653
|
+
/** A singleton read envelope. Singleton documents have no public URL path. */
|
|
654
|
+
export type SingletonDocument<F = Record<string, any>> = Omit<ClientDocument<F>, 'path'>;
|
|
640
655
|
export interface FindResult<F = Record<string, any>> {
|
|
641
656
|
docs: ClientDocument<F>[];
|
|
642
657
|
meta: {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/client",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.18.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -44,22 +44,21 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"uuid": "^14.0.2",
|
|
47
|
-
"@byline/auth": "4.
|
|
48
|
-
"@byline/core": "4.
|
|
47
|
+
"@byline/auth": "4.18.0",
|
|
48
|
+
"@byline/core": "4.18.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "2.5.
|
|
52
|
-
"@types/node": "^26.
|
|
51
|
+
"@biomejs/biome": "2.5.10",
|
|
52
|
+
"@types/node": "^26.4.0",
|
|
53
53
|
"chokidar": "^5.0.0",
|
|
54
54
|
"chokidar-cli": "^3.0.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
56
56
|
"npm-run-all": "^4.1.5",
|
|
57
57
|
"tsc-alias": "^1.9.2",
|
|
58
|
-
"tsx": "^4.23.12",
|
|
59
58
|
"typescript": "^7.0.2",
|
|
60
59
|
"vitest": "^4.1.11",
|
|
61
|
-
"@byline/
|
|
62
|
-
"@byline/
|
|
60
|
+
"@byline/search-postgres": "4.18.0",
|
|
61
|
+
"@byline/db-postgres": "4.18.0"
|
|
63
62
|
},
|
|
64
63
|
"publishConfig": {
|
|
65
64
|
"access": "public",
|