@byline/client 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/client.d.ts +10 -7
- package/dist/client.js +34 -5
- package/dist/collection-handle.js +5 -2
- 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.js +1 -1
- package/dist/singleton-handle.d.ts +44 -0
- package/dist/singleton-handle.js +155 -0
- package/dist/types.d.ts +11 -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,6 +7,7 @@
|
|
|
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
12
|
import { assertSearchQueryLength, finalizeSearchHits } from './search.js';
|
|
12
13
|
function toScheduleInfo(schedule) {
|
|
@@ -240,7 +241,7 @@ export class CollectionHandle {
|
|
|
240
241
|
*/
|
|
241
242
|
async reindex() {
|
|
242
243
|
const requestContext = await this.client.resolveRequestContext();
|
|
243
|
-
assertActorCanPerform(requestContext, this.definition
|
|
244
|
+
assertActorCanPerform(requestContext, this.definition, 'reindex');
|
|
244
245
|
const provider = this.client.searchProvider;
|
|
245
246
|
const result = { collectionPath: this.definition.path, documents: 0, indexed: 0 };
|
|
246
247
|
if (provider == null || this.definition.search == null)
|
|
@@ -612,6 +613,7 @@ export class CollectionHandle {
|
|
|
612
613
|
* `null` without revealing ownership.
|
|
613
614
|
*/
|
|
614
615
|
async findByVersion(versionId, options = {}) {
|
|
616
|
+
const boundOptions = options;
|
|
615
617
|
const readCtx = options._readContext ?? createReadContext();
|
|
616
618
|
const requestContext = await this.resolveAndAssertRead('any', readCtx);
|
|
617
619
|
const filters = await this.resolveBeforeReadFilters(requestContext, readCtx, options._bypassBeforeRead);
|
|
@@ -619,6 +621,7 @@ export class CollectionHandle {
|
|
|
619
621
|
const locale = options.locale ?? this.client.defaultLocale;
|
|
620
622
|
const raw = await this.client.db.queries.documents.getDocumentByVersion({
|
|
621
623
|
document_version_id: versionId,
|
|
624
|
+
document_id: boundOptions[expectedDocumentId],
|
|
622
625
|
locale,
|
|
623
626
|
collection_id: collectionId,
|
|
624
627
|
filters,
|
|
@@ -824,7 +827,7 @@ export class CollectionHandle {
|
|
|
824
827
|
*/
|
|
825
828
|
async resolveAndAssertRead(readMode, readContext) {
|
|
826
829
|
const requestContext = await resolveReadRequestContext(this.client, readContext, readMode);
|
|
827
|
-
assertActorCanPerform(requestContext, this.definition
|
|
830
|
+
assertActorCanPerform(requestContext, this.definition, 'read');
|
|
828
831
|
return requestContext;
|
|
829
832
|
}
|
|
830
833
|
/**
|
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.js
CHANGED
|
@@ -152,7 +152,7 @@ export async function zoneSearch(client, options) {
|
|
|
152
152
|
let firstAbilityError;
|
|
153
153
|
for (const member of members) {
|
|
154
154
|
try {
|
|
155
|
-
assertActorCanPerform(requestContext, member
|
|
155
|
+
assertActorCanPerform(requestContext, member, 'read');
|
|
156
156
|
readable.add(member.path);
|
|
157
157
|
}
|
|
158
158
|
catch (err) {
|
|
@@ -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
|
@@ -335,6 +335,8 @@ export interface FindByIdOptions<F = Record<string, any>> extends PopulateContro
|
|
|
335
335
|
*/
|
|
336
336
|
lenient?: boolean;
|
|
337
337
|
}
|
|
338
|
+
/** Read controls for `SingletonHandle.get()`. */
|
|
339
|
+
export type GetSingletonOptions<F = Record<string, any>> = FindByIdOptions<F>;
|
|
338
340
|
export interface FindByPathOptions<F = Record<string, any>> extends PopulateControls, StatusControls, MissingLocaleControls, BeforeReadControls {
|
|
339
341
|
select?: (keyof F & string)[] | string[];
|
|
340
342
|
locale?: string;
|
|
@@ -417,6 +419,13 @@ export interface UpdateOptions {
|
|
|
417
419
|
*/
|
|
418
420
|
availableLocales?: string[];
|
|
419
421
|
}
|
|
422
|
+
/** Write controls for `SingletonHandle.update()`. */
|
|
423
|
+
export interface UpdateSingletonOptions {
|
|
424
|
+
/** Locale for field value resolution. Defaults to the client's `defaultLocale`. */
|
|
425
|
+
locale?: string;
|
|
426
|
+
/** Reject the save when this is not the slot's current version id. */
|
|
427
|
+
expectedVersionId?: string;
|
|
428
|
+
}
|
|
420
429
|
/** Optimistic, absolute-time input for `CollectionHandle.schedulePublish()`. */
|
|
421
430
|
export interface SchedulePublishOptions {
|
|
422
431
|
/** ISO instant carrying an explicit UTC offset or `Z`. */
|
|
@@ -637,6 +646,8 @@ export interface ClientDocument<F = Record<string, any>> {
|
|
|
637
646
|
*/
|
|
638
647
|
_localeAgnostic?: boolean;
|
|
639
648
|
}
|
|
649
|
+
/** A singleton read envelope. Singleton documents have no public URL path. */
|
|
650
|
+
export type SingletonDocument<F = Record<string, any>> = Omit<ClientDocument<F>, 'path'>;
|
|
640
651
|
export interface FindResult<F = Record<string, any>> {
|
|
641
652
|
docs: ClientDocument<F>[];
|
|
642
653
|
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.17.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.17.0",
|
|
48
|
+
"@byline/core": "4.17.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/search-postgres": "4.
|
|
62
|
-
"@byline/db-postgres": "4.
|
|
60
|
+
"@byline/search-postgres": "4.17.0",
|
|
61
|
+
"@byline/db-postgres": "4.17.0"
|
|
63
62
|
},
|
|
64
63
|
"publishConfig": {
|
|
65
64
|
"access": "public",
|