@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.
Files changed (34) hide show
  1. package/dist/client.d.ts +6 -3
  2. package/dist/client.js +3 -1
  3. package/dist/collection-handle.d.ts +33 -38
  4. package/dist/collection-handle.js +175 -137
  5. package/dist/index.d.ts +2 -1
  6. package/dist/read-context.d.ts +11 -0
  7. package/dist/read-context.js +36 -0
  8. package/dist/register.d.ts +44 -0
  9. package/dist/register.js +8 -0
  10. package/dist/search.js +23 -6
  11. package/dist/server/admin-context.d.ts +50 -0
  12. package/dist/server/admin-context.js +125 -0
  13. package/dist/server/admin-context.test.node.d.ts +8 -0
  14. package/dist/server/admin-context.test.node.js +256 -0
  15. package/dist/server/browser.d.ts +8 -0
  16. package/dist/server/browser.js +18 -0
  17. package/dist/server/clients.d.ts +92 -0
  18. package/dist/server/clients.js +170 -0
  19. package/dist/server/index.d.ts +26 -0
  20. package/dist/server/index.js +26 -0
  21. package/dist/server/preview-cookies.d.ts +19 -0
  22. package/dist/server/preview-cookies.js +69 -0
  23. package/dist/server/request-context-contract.test.node.d.ts +8 -0
  24. package/dist/server/request-context-contract.test.node.js +114 -0
  25. package/dist/server/request-context-factories.test.node.d.ts +8 -0
  26. package/dist/server/request-context-factories.test.node.js +116 -0
  27. package/dist/server/request-scope.d.ts +17 -0
  28. package/dist/server/request-scope.js +50 -0
  29. package/dist/server/request-scope.test.node.d.ts +8 -0
  30. package/dist/server/request-scope.test.node.js +89 -0
  31. package/dist/server/session-cookies.d.ts +34 -0
  32. package/dist/server/session-cookies.js +95 -0
  33. package/dist/types.d.ts +60 -23
  34. 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
- * resolved per-call so each operation picks up the current
101
- * authenticated request (the pattern Phase 5's admin webapp will
102
- * use to thread middleware-derived actors).
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 PopulateControls {
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 `afterReadFired` set across nested reads so the
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. `total` (and facet
231
- * counts) are the provider's pre-authorization numbers: approximate when
232
- * row scoping or hydration drops hits, exact otherwise.
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 the `findById` access
328
- * gate for admin tooling.
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)`. No
357
- * `BeforeReadControls` — `findByVersion` is a low-level pass-through
358
- * intended for admin diff views; row-level scoping is the caller's
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 schema-inferred fields type as a populated relation.
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 that `CollectionFieldData<typeof X>` derives from the schema.
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', NewsCategoryFields>,
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": "3.21.0",
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
- "@byline/auth": "3.21.0",
41
- "@byline/core": "3.21.0"
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/db-postgres": "3.21.0",
55
- "@byline/search-postgres": "3.21.0"
61
+ "@byline/search-postgres": "4.1.0",
62
+ "@byline/db-postgres": "4.1.0"
56
63
  },
57
64
  "publishConfig": {
58
65
  "access": "public",