@byline/client 3.21.0 → 4.1.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 +6 -3
- package/dist/client.js +3 -1
- package/dist/collection-handle.d.ts +33 -38
- package/dist/collection-handle.js +175 -137
- package/dist/index.d.ts +2 -1
- package/dist/read-context.d.ts +11 -0
- package/dist/read-context.js +36 -0
- package/dist/register.d.ts +44 -0
- package/dist/register.js +8 -0
- package/dist/search.js +23 -6
- package/dist/server/admin-context.d.ts +50 -0
- package/dist/server/admin-context.js +125 -0
- package/dist/server/admin-context.test.node.d.ts +8 -0
- package/dist/server/admin-context.test.node.js +256 -0
- package/dist/server/browser.d.ts +8 -0
- package/dist/server/browser.js +18 -0
- package/dist/server/clients.d.ts +92 -0
- package/dist/server/clients.js +170 -0
- package/dist/server/index.d.ts +26 -0
- package/dist/server/index.js +26 -0
- package/dist/server/preview-cookies.d.ts +19 -0
- package/dist/server/preview-cookies.js +69 -0
- package/dist/server/request-context-contract.test.node.d.ts +8 -0
- package/dist/server/request-context-contract.test.node.js +114 -0
- package/dist/server/request-context-factories.test.node.d.ts +8 -0
- package/dist/server/request-context-factories.test.node.js +116 -0
- package/dist/server/request-scope.d.ts +17 -0
- package/dist/server/request-scope.js +50 -0
- package/dist/server/request-scope.test.node.d.ts +8 -0
- package/dist/server/request-scope.test.node.js +89 -0
- package/dist/server/session-cookies.d.ts +34 -0
- package/dist/server/session-cookies.js +95 -0
- package/dist/types.d.ts +60 -23
- package/package.json +12 -5
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Resolve `factory` at most once per HTTP request for a given `key`. The
|
|
10
|
+
* returned promise is memoized on the current request — rejections
|
|
11
|
+
* included, since a request's cookies cannot change mid-flight and retrying
|
|
12
|
+
* a failed refresh would burn a second rotation on an already-rotated
|
|
13
|
+
* token. Outside a request (seed scripts, unit tests, background jobs) —
|
|
14
|
+
* or before a host bridge is registered — there is no request to key on
|
|
15
|
+
* and the factory runs unmemoized.
|
|
16
|
+
*/
|
|
17
|
+
export declare function oncePerRequest<T>(key: string, factory: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Per-request memoization for `RequestContext` factories.
|
|
10
|
+
*
|
|
11
|
+
* `RequestContext.requestId` is documented as "per logical request", and
|
|
12
|
+
* the read-authorization layer binds every `ReadContext` to one immutable
|
|
13
|
+
* request authority whose token includes that id. A factory that mints a
|
|
14
|
+
* fresh context per call breaks both contracts as soon as two reads share
|
|
15
|
+
* a `ReadContext` (the admin tree view, hook-threaded nested reads):
|
|
16
|
+
* the second bind sees a different authority token and throws
|
|
17
|
+
* 'ReadContext cannot be reused across request authorities'.
|
|
18
|
+
*
|
|
19
|
+
* Memoizing on the current request (via the registered
|
|
20
|
+
* `HostRequestBridge`) makes every context factory request-stable. It
|
|
21
|
+
* also means at most one session verification — and at most one
|
|
22
|
+
* refresh-token rotation — per request, instead of one per call.
|
|
23
|
+
*/
|
|
24
|
+
import { tryGetHostRequestBridge } from '@byline/core';
|
|
25
|
+
const requestScopes = new WeakMap();
|
|
26
|
+
/**
|
|
27
|
+
* Resolve `factory` at most once per HTTP request for a given `key`. The
|
|
28
|
+
* returned promise is memoized on the current request — rejections
|
|
29
|
+
* included, since a request's cookies cannot change mid-flight and retrying
|
|
30
|
+
* a failed refresh would burn a second rotation on an already-rotated
|
|
31
|
+
* token. Outside a request (seed scripts, unit tests, background jobs) —
|
|
32
|
+
* or before a host bridge is registered — there is no request to key on
|
|
33
|
+
* and the factory runs unmemoized.
|
|
34
|
+
*/
|
|
35
|
+
export function oncePerRequest(key, factory) {
|
|
36
|
+
const request = tryGetHostRequestBridge()?.getRequest();
|
|
37
|
+
if (!request)
|
|
38
|
+
return factory();
|
|
39
|
+
let scope = requestScopes.get(request);
|
|
40
|
+
if (!scope) {
|
|
41
|
+
scope = new Map();
|
|
42
|
+
requestScopes.set(request, scope);
|
|
43
|
+
}
|
|
44
|
+
const existing = scope.get(key);
|
|
45
|
+
if (existing)
|
|
46
|
+
return existing;
|
|
47
|
+
const pending = factory();
|
|
48
|
+
scope.set(key, pending);
|
|
49
|
+
return pending;
|
|
50
|
+
}
|
|
@@ -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,89 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Unit coverage for per-request memoization. Registers a fake
|
|
10
|
+
* `HostRequestBridge` so "which request am I in" can be simulated: a
|
|
11
|
+
* stable object per request, or `undefined` when running outside a
|
|
12
|
+
* request.
|
|
13
|
+
*/
|
|
14
|
+
import { registerHostRequestBridge } from '@byline/core';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { oncePerRequest } from './request-scope.js';
|
|
17
|
+
const BRIDGE_SLOT = Symbol.for('__byline_host_request_bridge__');
|
|
18
|
+
const previousBridge = globalThis[BRIDGE_SLOT];
|
|
19
|
+
const bridge = {
|
|
20
|
+
getRequest: vi.fn(),
|
|
21
|
+
getCookie: vi.fn(),
|
|
22
|
+
setCookie: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
describe('oncePerRequest', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
registerHostRequestBridge(bridge);
|
|
28
|
+
// Default: no request in scope, so every call resolves fresh.
|
|
29
|
+
bridge.getRequest.mockReturnValue(undefined);
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
;
|
|
33
|
+
globalThis[BRIDGE_SLOT] = previousBridge;
|
|
34
|
+
});
|
|
35
|
+
it('resolves the factory once per request per key', async () => {
|
|
36
|
+
bridge.getRequest.mockReturnValue({ id: 'request-a' });
|
|
37
|
+
const factory = vi.fn(async () => ({ token: Math.random() }));
|
|
38
|
+
const first = await oncePerRequest('ctx', factory);
|
|
39
|
+
const second = await oncePerRequest('ctx', factory);
|
|
40
|
+
expect(second).toBe(first);
|
|
41
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
it('keys memoization by request identity', async () => {
|
|
44
|
+
const requestA = { id: 'a' };
|
|
45
|
+
const requestB = { id: 'b' };
|
|
46
|
+
const factory = vi.fn(async () => ({ token: Math.random() }));
|
|
47
|
+
bridge.getRequest.mockReturnValue(requestA);
|
|
48
|
+
const first = await oncePerRequest('ctx', factory);
|
|
49
|
+
bridge.getRequest.mockReturnValue(requestB);
|
|
50
|
+
const second = await oncePerRequest('ctx', factory);
|
|
51
|
+
expect(second).not.toBe(first);
|
|
52
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
|
53
|
+
});
|
|
54
|
+
it('isolates entries by key within one request', async () => {
|
|
55
|
+
bridge.getRequest.mockReturnValue({ id: 'request-a' });
|
|
56
|
+
const admin = vi.fn(async () => 'admin-context');
|
|
57
|
+
const publicCtx = vi.fn(async () => 'public-context');
|
|
58
|
+
await expect(oncePerRequest('admin', admin)).resolves.toBe('admin-context');
|
|
59
|
+
await expect(oncePerRequest('public', publicCtx)).resolves.toBe('public-context');
|
|
60
|
+
await expect(oncePerRequest('admin', admin)).resolves.toBe('admin-context');
|
|
61
|
+
expect(admin).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(publicCtx).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
it('memoizes rejections for the remainder of the request', async () => {
|
|
65
|
+
bridge.getRequest.mockReturnValue({ id: 'request-a' });
|
|
66
|
+
const factory = vi.fn(async () => {
|
|
67
|
+
throw new Error('no admin session');
|
|
68
|
+
});
|
|
69
|
+
await expect(oncePerRequest('ctx', factory)).rejects.toThrow('no admin session');
|
|
70
|
+
await expect(oncePerRequest('ctx', factory)).rejects.toThrow('no admin session');
|
|
71
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
it('runs unmemoized outside a request', async () => {
|
|
74
|
+
const factory = vi.fn(async () => ({ token: Math.random() }));
|
|
75
|
+
const first = await oncePerRequest('ctx', factory);
|
|
76
|
+
const second = await oncePerRequest('ctx', factory);
|
|
77
|
+
expect(second).not.toBe(first);
|
|
78
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
|
79
|
+
});
|
|
80
|
+
it('runs unmemoized when no host bridge is registered', async () => {
|
|
81
|
+
;
|
|
82
|
+
globalThis[BRIDGE_SLOT] = undefined;
|
|
83
|
+
const factory = vi.fn(async () => ({ token: Math.random() }));
|
|
84
|
+
const first = await oncePerRequest('ctx', factory);
|
|
85
|
+
const second = await oncePerRequest('ctx', factory);
|
|
86
|
+
expect(second).not.toBe(first);
|
|
87
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 declare const ACCESS_TOKEN_COOKIE = "byline_access_token";
|
|
9
|
+
export declare const REFRESH_TOKEN_COOKIE = "byline_refresh_token";
|
|
10
|
+
/** Read the access-token cookie. Returns undefined when not present. */
|
|
11
|
+
export declare function readAccessTokenCookie(): string | undefined;
|
|
12
|
+
/** Read the refresh-token cookie. Returns undefined when not present. */
|
|
13
|
+
export declare function readRefreshTokenCookie(): string | undefined;
|
|
14
|
+
export interface SessionCookieTokens {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
accessTokenExpiresAt: Date;
|
|
18
|
+
refreshTokenExpiresAt: Date;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Write both access and refresh cookies. Called after sign-in and after
|
|
22
|
+
* every transparent refresh in `getAdminRequestContext()`.
|
|
23
|
+
*
|
|
24
|
+
* `maxAge` is derived from each token's own expiry claim so the browser
|
|
25
|
+
* drops the cookies at the same moment the server would reject them —
|
|
26
|
+
* saves round trips when the refresh token has fully expired.
|
|
27
|
+
*/
|
|
28
|
+
export declare function setSessionCookies(tokens: SessionCookieTokens): void;
|
|
29
|
+
/**
|
|
30
|
+
* Clear both session cookies. Called on sign-out and on any auth failure
|
|
31
|
+
* during `getAdminRequestContext()` (ensures the browser does not keep
|
|
32
|
+
* trying a token that the server has already rejected).
|
|
33
|
+
*/
|
|
34
|
+
export declare function clearSessionCookies(): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Cookie helpers for admin session management.
|
|
10
|
+
*
|
|
11
|
+
* Two separate httpOnly cookies, one for each token half:
|
|
12
|
+
*
|
|
13
|
+
* - `byline_access_token` — short-lived JWT; sent on every request.
|
|
14
|
+
* - `byline_refresh_token` — long-lived opaque string; sent back only
|
|
15
|
+
* when the access cookie is missing or
|
|
16
|
+
* expired, to mint a new access token.
|
|
17
|
+
*
|
|
18
|
+
* Presenting "one session" to the user is a function of the middleware —
|
|
19
|
+
* `getAdminRequestContext()` transparently refreshes the access cookie
|
|
20
|
+
* using the refresh cookie, so the UI layer doesn't have to care.
|
|
21
|
+
*
|
|
22
|
+
* All cookies set here use:
|
|
23
|
+
* - `httpOnly: true` — inaccessible to JavaScript (XSS-hardened).
|
|
24
|
+
* - `sameSite: 'lax'` — sent on top-level navigations but not
|
|
25
|
+
* cross-origin subrequests (reasonable default
|
|
26
|
+
* for an admin SSR app).
|
|
27
|
+
* - `secure: true` in production — https-only; dev keeps it off so
|
|
28
|
+
* cookies work on http://localhost.
|
|
29
|
+
* - `path: '/'` — available everywhere in the app.
|
|
30
|
+
*
|
|
31
|
+
* Cookie transport goes through the registered `HostRequestBridge`, so
|
|
32
|
+
* this module is host-framework agnostic.
|
|
33
|
+
*/
|
|
34
|
+
import { getHostRequestBridge } from '@byline/core';
|
|
35
|
+
export const ACCESS_TOKEN_COOKIE = 'byline_access_token';
|
|
36
|
+
export const REFRESH_TOKEN_COOKIE = 'byline_refresh_token';
|
|
37
|
+
const IS_PROD = process.env.NODE_ENV === 'production';
|
|
38
|
+
/** Read the access-token cookie. Returns undefined when not present. */
|
|
39
|
+
export function readAccessTokenCookie() {
|
|
40
|
+
return getHostRequestBridge().getCookie(ACCESS_TOKEN_COOKIE);
|
|
41
|
+
}
|
|
42
|
+
/** Read the refresh-token cookie. Returns undefined when not present. */
|
|
43
|
+
export function readRefreshTokenCookie() {
|
|
44
|
+
return getHostRequestBridge().getCookie(REFRESH_TOKEN_COOKIE);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Write both access and refresh cookies. Called after sign-in and after
|
|
48
|
+
* every transparent refresh in `getAdminRequestContext()`.
|
|
49
|
+
*
|
|
50
|
+
* `maxAge` is derived from each token's own expiry claim so the browser
|
|
51
|
+
* drops the cookies at the same moment the server would reject them —
|
|
52
|
+
* saves round trips when the refresh token has fully expired.
|
|
53
|
+
*/
|
|
54
|
+
export function setSessionCookies(tokens) {
|
|
55
|
+
const bridge = getHostRequestBridge();
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const accessMaxAgeSeconds = Math.max(0, Math.floor((tokens.accessTokenExpiresAt.getTime() - now) / 1000));
|
|
58
|
+
const refreshMaxAgeSeconds = Math.max(0, Math.floor((tokens.refreshTokenExpiresAt.getTime() - now) / 1000));
|
|
59
|
+
bridge.setCookie(ACCESS_TOKEN_COOKIE, tokens.accessToken, {
|
|
60
|
+
httpOnly: true,
|
|
61
|
+
sameSite: 'lax',
|
|
62
|
+
secure: IS_PROD,
|
|
63
|
+
path: '/',
|
|
64
|
+
maxAge: accessMaxAgeSeconds,
|
|
65
|
+
});
|
|
66
|
+
bridge.setCookie(REFRESH_TOKEN_COOKIE, tokens.refreshToken, {
|
|
67
|
+
httpOnly: true,
|
|
68
|
+
sameSite: 'lax',
|
|
69
|
+
secure: IS_PROD,
|
|
70
|
+
path: '/',
|
|
71
|
+
maxAge: refreshMaxAgeSeconds,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Clear both session cookies. Called on sign-out and on any auth failure
|
|
76
|
+
* during `getAdminRequestContext()` (ensures the browser does not keep
|
|
77
|
+
* trying a token that the server has already rejected).
|
|
78
|
+
*/
|
|
79
|
+
export function clearSessionCookies() {
|
|
80
|
+
const bridge = getHostRequestBridge();
|
|
81
|
+
bridge.setCookie(ACCESS_TOKEN_COOKIE, '', {
|
|
82
|
+
httpOnly: true,
|
|
83
|
+
sameSite: 'lax',
|
|
84
|
+
secure: IS_PROD,
|
|
85
|
+
path: '/',
|
|
86
|
+
maxAge: 0,
|
|
87
|
+
});
|
|
88
|
+
bridge.setCookie(REFRESH_TOKEN_COOKIE, '', {
|
|
89
|
+
httpOnly: true,
|
|
90
|
+
sameSite: 'lax',
|
|
91
|
+
secure: IS_PROD,
|
|
92
|
+
path: '/',
|
|
93
|
+
maxAge: 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -97,9 +97,18 @@ export interface BylineClientConfig {
|
|
|
97
97
|
* - a static `RequestContext` — convenient for long-lived processes
|
|
98
98
|
* that authenticate once (seeds, migrations, CLI tooling); or
|
|
99
99
|
* - a factory `() => RequestContext | Promise<RequestContext>` —
|
|
100
|
-
*
|
|
101
|
-
* authenticated request (the pattern
|
|
102
|
-
*
|
|
100
|
+
* invoked per operation so each one picks up the current
|
|
101
|
+
* authenticated request (the pattern the admin webapp uses to
|
|
102
|
+
* thread middleware-derived actors).
|
|
103
|
+
*
|
|
104
|
+
* **Factory contract — request-stable resolution.** Within one logical
|
|
105
|
+
* request the factory must return the same context instance (same
|
|
106
|
+
* `requestId`) on every call. Reads that share a `ReadContext` bind an
|
|
107
|
+
* immutable request authority whose token includes `requestId`, so a
|
|
108
|
+
* factory that mints a fresh context per call makes the second such
|
|
109
|
+
* read throw `ERR_VALIDATION` ('ReadContext cannot be reused across
|
|
110
|
+
* request authorities'). Host adapters should memoize resolution per
|
|
111
|
+
* request — see `oncePerRequest` in `@byline/host-tanstack-start`.
|
|
103
112
|
*
|
|
104
113
|
* When omitted, calls fail closed with `ERR_UNAUTHENTICATED`. Scripts
|
|
105
114
|
* and tests pass `createSuperAdminContext()` from `@byline/auth`.
|
|
@@ -112,12 +121,10 @@ export interface BylineClientConfig {
|
|
|
112
121
|
* documents; `depth` caps the traversal (default 1 when populate is set,
|
|
113
122
|
* 0 otherwise). Clamped to the request's internal `ReadContext.maxDepth`.
|
|
114
123
|
*/
|
|
115
|
-
interface
|
|
116
|
-
populate?: PopulateSpec;
|
|
117
|
-
depth?: number;
|
|
124
|
+
interface ReadContextControls {
|
|
118
125
|
/**
|
|
119
126
|
* Escape hatch for read-side hook re-entry. Carries the visited set,
|
|
120
|
-
* depth clamp, and `
|
|
127
|
+
* depth clamp, and `afterRead` recursion/object state across nested reads so the
|
|
121
128
|
* A→B→A recursion guard stays intact.
|
|
122
129
|
*
|
|
123
130
|
* Threading rules:
|
|
@@ -138,6 +145,10 @@ interface PopulateControls {
|
|
|
138
145
|
*/
|
|
139
146
|
_readContext?: ReadContext;
|
|
140
147
|
}
|
|
148
|
+
interface PopulateControls extends ReadContextControls {
|
|
149
|
+
populate?: PopulateSpec;
|
|
150
|
+
depth?: number;
|
|
151
|
+
}
|
|
141
152
|
/**
|
|
142
153
|
* Read-mode selector shared by every read method.
|
|
143
154
|
*
|
|
@@ -227,9 +238,9 @@ export interface HydratedSearchHit extends SearchHit {
|
|
|
227
238
|
}
|
|
228
239
|
/**
|
|
229
240
|
* The client-side search result envelope — `SearchResults` with the hits
|
|
230
|
-
* widened to carry an optional hydrated document.
|
|
231
|
-
*
|
|
232
|
-
*
|
|
241
|
+
* widened to carry an optional hydrated document. When authorization removes
|
|
242
|
+
* collections or applies row predicates, `total` is conservatively reduced to
|
|
243
|
+
* the authorized hits in this page and facets are omitted.
|
|
233
244
|
*/
|
|
234
245
|
export interface ClientSearchResults {
|
|
235
246
|
hits: HydratedSearchHit[];
|
|
@@ -324,8 +335,8 @@ export interface FindByPathOptions<F = Record<string, any>> extends PopulateCont
|
|
|
324
335
|
/**
|
|
325
336
|
* Options for `CollectionHandle.history(documentId, options)`. The history
|
|
326
337
|
* endpoint is paginated; `order` / `desc` mirror the storage adapter's
|
|
327
|
-
* version-row sort axes. `_bypassBeforeRead` skips
|
|
328
|
-
*
|
|
338
|
+
* version-row sort axes. `_bypassBeforeRead` skips per-version row scoping
|
|
339
|
+
* for admin tooling.
|
|
329
340
|
*/
|
|
330
341
|
export interface HistoryOptions extends BeforeReadControls {
|
|
331
342
|
locale?: string;
|
|
@@ -353,12 +364,11 @@ export interface AuditLogOptions extends BeforeReadControls {
|
|
|
353
364
|
_readContext?: ReadContext;
|
|
354
365
|
}
|
|
355
366
|
/**
|
|
356
|
-
* Options for `CollectionHandle.findByVersion(versionId, options)`.
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* responsibility (typically by gating with a prior `findById` call).
|
|
367
|
+
* Options for `CollectionHandle.findByVersion(versionId, options)`. Historical
|
|
368
|
+
* reads are collection-bound and apply the same `beforeRead` predicate as
|
|
369
|
+
* current reads.
|
|
360
370
|
*/
|
|
361
|
-
export interface FindByVersionOptions<F = Record<string, any>> {
|
|
371
|
+
export interface FindByVersionOptions<F = Record<string, any>> extends BeforeReadControls, ReadContextControls {
|
|
362
372
|
select?: (keyof F & string)[] | string[];
|
|
363
373
|
locale?: string;
|
|
364
374
|
}
|
|
@@ -418,6 +428,13 @@ export interface PlaceTreeNodeOptions {
|
|
|
418
428
|
beforeDocumentId?: string | null;
|
|
419
429
|
/** Right neighbour — the node lands immediately before it. */
|
|
420
430
|
afterDocumentId?: string | null;
|
|
431
|
+
/** Re-run `afterTreeChange` when this exact placement is already current. */
|
|
432
|
+
reconcile?: boolean;
|
|
433
|
+
}
|
|
434
|
+
/** Options for an idempotent tree removal or post-commit hook retry. */
|
|
435
|
+
export interface RemoveFromTreeOptions {
|
|
436
|
+
/** Re-run `afterTreeChange` when the document is already unplaced. */
|
|
437
|
+
reconcile?: boolean;
|
|
421
438
|
}
|
|
422
439
|
/**
|
|
423
440
|
* Options for `CollectionHandle.getSubtree(options)`. Reads a node's subtree as
|
|
@@ -426,8 +443,10 @@ export interface PlaceTreeNodeOptions {
|
|
|
426
443
|
* at (and including) that node. `depth` bounds the descent (0-based; the root is
|
|
427
444
|
* depth 0). `status` follows the usual published/any selector — in `'published'`
|
|
428
445
|
* mode an unpublished node hides its entire subtree (the spine breaks).
|
|
446
|
+
* `beforeRead`-hidden nodes use the same edge semantics. `_bypassBeforeRead`
|
|
447
|
+
* is reserved for internal tooling.
|
|
429
448
|
*/
|
|
430
|
-
export interface GetSubtreeOptions<F = Record<string, any>> extends StatusControls {
|
|
449
|
+
export interface GetSubtreeOptions<F = Record<string, any>> extends StatusControls, BeforeReadControls, ReadContextControls {
|
|
431
450
|
rootDocumentId?: string | null;
|
|
432
451
|
depth?: number;
|
|
433
452
|
/** Locale for field value resolution. Defaults to the client's `defaultLocale`. */
|
|
@@ -439,14 +458,30 @@ export interface GetSubtreeOptions<F = Record<string, any>> extends StatusContro
|
|
|
439
458
|
* Options for `CollectionHandle.getAncestors(documentId, options)`. Walks the
|
|
440
459
|
* node's ancestor chain upward, returning the ancestors **root-first** (the
|
|
441
460
|
* breadcrumb trail, excluding the node itself). In `'published'` mode only
|
|
442
|
-
* ancestors with a published version are returned.
|
|
461
|
+
* ancestors with a published version are returned. The walk also stops at the
|
|
462
|
+
* first ancestor excluded by `beforeRead`.
|
|
443
463
|
*/
|
|
444
|
-
export interface GetAncestorsOptions<F = Record<string, any>> extends StatusControls {
|
|
464
|
+
export interface GetAncestorsOptions<F = Record<string, any>> extends StatusControls, BeforeReadControls, ReadContextControls {
|
|
445
465
|
/** Locale for field value resolution. Defaults to the client's `defaultLocale`. */
|
|
446
466
|
locale?: string;
|
|
447
467
|
/** Return only these fields on each ancestor. Omit for all fields. */
|
|
448
468
|
select?: (keyof F & string)[] | string[];
|
|
449
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Options for `CollectionHandle.getTreeParent(documentId, options)`. A hidden
|
|
472
|
+
* queried node is indistinguishable from an unplaced node. When only the parent
|
|
473
|
+
* is hidden, placement remains true but its id is redacted to `null`.
|
|
474
|
+
*/
|
|
475
|
+
export interface GetTreeParentOptions extends StatusControls, BeforeReadControls, ReadContextControls {
|
|
476
|
+
/** Locale used to evaluate localized `beforeRead` field predicates. */
|
|
477
|
+
locale?: string;
|
|
478
|
+
}
|
|
479
|
+
export interface TreeParentResult {
|
|
480
|
+
placed: boolean;
|
|
481
|
+
parentDocumentId: string | null;
|
|
482
|
+
/** Distinguishes a true root/unplaced node from a hidden parent id. */
|
|
483
|
+
parentVisibility: 'none' | 'visible' | 'redacted';
|
|
484
|
+
}
|
|
450
485
|
/**
|
|
451
486
|
* A node in a hydrated document tree. `document` is the fully-shaped
|
|
452
487
|
* `ClientDocument`; `depth` is 0-based from the subtree root; `children` are the
|
|
@@ -598,16 +633,18 @@ export interface PopulatedRelation<T> {
|
|
|
598
633
|
document?: ClientDocument<T>;
|
|
599
634
|
}
|
|
600
635
|
/**
|
|
601
|
-
* Re-type one key of a
|
|
636
|
+
* Re-type one key of a generated fields type as a populated relation.
|
|
602
637
|
* Use to overlay populate's per-call enrichment on top of the unpopulated
|
|
603
|
-
* shape
|
|
638
|
+
* shape emitted for the configured collection.
|
|
604
639
|
*
|
|
605
640
|
* Homomorphic mapped form preserves the optionality of the underlying key —
|
|
606
641
|
* an optional relation field stays optional after populate is layered on.
|
|
607
642
|
*
|
|
608
643
|
* ```ts
|
|
644
|
+
* import type { MediaFields, NewsCategoriesFields, NewsFields } from './byline/generated/collection-types.js'
|
|
645
|
+
*
|
|
609
646
|
* type NewsPopulated = WithPopulated<
|
|
610
|
-
* WithPopulated<NewsFields, 'category',
|
|
647
|
+
* WithPopulated<NewsFields, 'category', NewsCategoriesFields>,
|
|
611
648
|
* 'featureImage', MediaFields
|
|
612
649
|
* >
|
|
613
650
|
* client.collection('news').find<NewsPopulated>({ populate: { category: '*', featureImage: '*' } })
|
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": "
|
|
5
|
+
"version": "4.1.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -31,14 +31,21 @@
|
|
|
31
31
|
"import": "./dist/index.js",
|
|
32
32
|
"require": "./dist/index.js"
|
|
33
33
|
},
|
|
34
|
+
"./server": {
|
|
35
|
+
"types": "./dist/server/index.d.ts",
|
|
36
|
+
"browser": "./dist/server/browser.js",
|
|
37
|
+
"import": "./dist/server/index.js",
|
|
38
|
+
"require": "./dist/server/index.js"
|
|
39
|
+
},
|
|
34
40
|
"./package.json": "./package.json"
|
|
35
41
|
},
|
|
36
42
|
"files": [
|
|
37
43
|
"dist"
|
|
38
44
|
],
|
|
39
45
|
"dependencies": {
|
|
40
|
-
"
|
|
41
|
-
"@byline/
|
|
46
|
+
"uuid": "^14.0.1",
|
|
47
|
+
"@byline/auth": "4.1.0",
|
|
48
|
+
"@byline/core": "4.1.0"
|
|
42
49
|
},
|
|
43
50
|
"devDependencies": {
|
|
44
51
|
"@biomejs/biome": "2.5.2",
|
|
@@ -51,8 +58,8 @@
|
|
|
51
58
|
"tsx": "^4.23.0",
|
|
52
59
|
"typescript": "^7.0.2",
|
|
53
60
|
"vitest": "^4.1.10",
|
|
54
|
-
"@byline/
|
|
55
|
-
"@byline/
|
|
61
|
+
"@byline/search-postgres": "4.1.0",
|
|
62
|
+
"@byline/db-postgres": "4.1.0"
|
|
56
63
|
},
|
|
57
64
|
"publishConfig": {
|
|
58
65
|
"access": "public",
|