@byline/core 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/@types/collection-types.d.ts +66 -31
- package/dist/@types/db-types.d.ts +93 -47
- package/dist/@types/field-types.d.ts +25 -1
- package/dist/@types/query-predicate.d.ts +3 -3
- package/dist/@types/search-types.d.ts +3 -4
- package/dist/@types/site-config.d.ts +51 -12
- package/dist/auth/apply-before-read.d.ts +23 -11
- package/dist/auth/apply-before-read.js +139 -33
- package/dist/auth/apply-before-read.test.node.js +241 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/read-context-scope.d.ts +20 -0
- package/dist/auth/read-context-scope.js +48 -0
- package/dist/codegen/index.js +34 -3
- package/dist/codegen/index.test.node.js +20 -2
- package/dist/config/attach-hooks.d.ts +25 -0
- package/dist/config/attach-hooks.js +130 -0
- package/dist/config/attach-hooks.test.node.d.ts +1 -0
- package/dist/config/attach-hooks.test.node.js +173 -0
- package/dist/config/config-hooks.test.node.d.ts +1 -0
- package/dist/config/config-hooks.test.node.js +56 -0
- package/dist/config/config.d.ts +9 -5
- package/dist/config/config.js +20 -2
- package/dist/config/routes.d.ts +5 -5
- package/dist/config/routes.js +42 -9
- package/dist/config/routes.test.node.d.ts +1 -0
- package/dist/config/routes.test.node.js +152 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +20 -14
- package/dist/core.test.node.d.ts +1 -0
- package/dist/core.test.node.js +28 -0
- package/dist/host/host-request-bridge.d.ts +62 -0
- package/dist/host/host-request-bridge.js +38 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +14 -0
- package/dist/query/parse-where.d.ts +9 -0
- package/dist/query/parse-where.js +146 -2
- package/dist/query/parse-where.test.node.js +60 -1
- package/dist/services/collection-bootstrap.test.node.js +30 -59
- package/dist/services/discover-counter-groups.test.node.js +23 -0
- package/dist/services/document-lifecycle/audit.d.ts +22 -1
- package/dist/services/document-lifecycle/audit.js +32 -1
- package/dist/services/document-lifecycle/create.js +13 -6
- package/dist/services/document-lifecycle/delete.d.ts +17 -1
- package/dist/services/document-lifecycle/delete.js +91 -26
- package/dist/services/document-lifecycle/index.d.ts +1 -1
- package/dist/services/document-lifecycle/internals.d.ts +0 -20
- package/dist/services/document-lifecycle/internals.js +22 -46
- package/dist/services/document-lifecycle/status.d.ts +1 -1
- package/dist/services/document-lifecycle/status.js +20 -3
- package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
- package/dist/services/document-lifecycle/system-fields.js +112 -74
- package/dist/services/document-lifecycle/tree.d.ts +22 -24
- package/dist/services/document-lifecycle/tree.js +244 -123
- package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/tree.test.node.js +663 -0
- package/dist/services/document-lifecycle/update.js +2 -1
- package/dist/services/document-lifecycle.test.node.js +360 -16
- package/dist/services/document-read.d.ts +14 -6
- package/dist/services/document-read.js +47 -9
- package/dist/services/field-upload.test.node.js +70 -0
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +2 -2
- package/dist/services/populate.d.ts +7 -3
- package/dist/services/populate.js +180 -28
- package/dist/services/populate.test.node.js +59 -0
- package/dist/services/richtext-embed.d.ts +39 -2
- package/dist/services/richtext-embed.js +4 -34
- package/dist/services/richtext-embed.test.node.js +15 -0
- package/dist/services/richtext-populate.d.ts +20 -2
- package/dist/services/richtext-populate.js +160 -19
- package/dist/services/richtext-populate.test.node.js +523 -2
- package/dist/utils/root-relative-redirect.d.ts +6 -0
- package/dist/utils/root-relative-redirect.js +37 -0
- package/package.json +2 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeRootRelativeRedirect } from '../utils/root-relative-redirect.js';
|
|
3
|
+
import { defineClientConfig, getClientConfig } from './config.js';
|
|
4
|
+
import { resolveRoutes } from './routes.js';
|
|
5
|
+
describe('resolveRoutes', () => {
|
|
6
|
+
it('returns canonical defaults', () => {
|
|
7
|
+
const routes = resolveRoutes();
|
|
8
|
+
expect(routes).toEqual({ admin: '/admin', api: '/api', signIn: '/sign-in' });
|
|
9
|
+
expect(Object.isFrozen(routes)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it.each([
|
|
12
|
+
['cms', '/cms'],
|
|
13
|
+
['/cms', '/cms'],
|
|
14
|
+
['/cms/', '/cms'],
|
|
15
|
+
['///cms///', '/cms'],
|
|
16
|
+
['/internal/cms/', '/internal/cms'],
|
|
17
|
+
])('canonicalizes an admin route of %j', (admin, expected) => {
|
|
18
|
+
expect(resolveRoutes({ admin }).admin).toBe(expected);
|
|
19
|
+
});
|
|
20
|
+
it('falls back for empty route values', () => {
|
|
21
|
+
expect(resolveRoutes({ admin: '', api: ' ', signIn: '' })).toEqual({
|
|
22
|
+
admin: '/admin',
|
|
23
|
+
api: '/api',
|
|
24
|
+
signIn: '/sign-in',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it.each([
|
|
28
|
+
'/../cms',
|
|
29
|
+
'/cms?next=x',
|
|
30
|
+
'/cms#section',
|
|
31
|
+
'/cms\\users',
|
|
32
|
+
'/%63ms',
|
|
33
|
+
])('rejects a non-segment admin route of %j', (admin) => {
|
|
34
|
+
expect(() => resolveRoutes({ admin })).toThrow(/routes\.admin/);
|
|
35
|
+
});
|
|
36
|
+
it('normalizes and validates the API route too', () => {
|
|
37
|
+
expect(resolveRoutes({ api: 'content-api/' }).api).toBe('/content-api');
|
|
38
|
+
expect(resolveRoutes({ api: '/internal/api' }).api).toBe('/internal/api');
|
|
39
|
+
});
|
|
40
|
+
it.each([
|
|
41
|
+
['staff/login', '/staff/login'],
|
|
42
|
+
['/staff/login/', '/staff/login'],
|
|
43
|
+
['/staff//login/', '/staff/login'],
|
|
44
|
+
])('canonicalizes a sign-in route of %j', (signIn, expected) => {
|
|
45
|
+
expect(resolveRoutes({ signIn }).signIn).toBe(expected);
|
|
46
|
+
});
|
|
47
|
+
it.each([
|
|
48
|
+
'https://evil.test/login',
|
|
49
|
+
'//evil.test/login',
|
|
50
|
+
'/staff/login?next=/cms',
|
|
51
|
+
'/staff/login#form',
|
|
52
|
+
'/staff\\login',
|
|
53
|
+
'/staff/%6cogin',
|
|
54
|
+
'/staff/../login',
|
|
55
|
+
])('rejects an unsafe sign-in route of %j', (signIn) => {
|
|
56
|
+
expect(() => resolveRoutes({ signIn })).toThrow(/routes\.signIn/);
|
|
57
|
+
});
|
|
58
|
+
it('rejects conflicting admin and API segments', () => {
|
|
59
|
+
expect(() => resolveRoutes({ admin: '/internal', api: 'internal/' })).toThrow(/routes\.admin and routes\.api/);
|
|
60
|
+
});
|
|
61
|
+
it.each([
|
|
62
|
+
['/internal', '/internal/api'],
|
|
63
|
+
['/internal/admin', '/internal'],
|
|
64
|
+
])('rejects overlapping admin and API trees %j and %j', (admin, api) => {
|
|
65
|
+
expect(() => resolveRoutes({ admin, api })).toThrow(/routes\.admin and routes\.api/);
|
|
66
|
+
});
|
|
67
|
+
it.each([
|
|
68
|
+
['/cms', '/cms'],
|
|
69
|
+
['/cms', '/cms/login'],
|
|
70
|
+
['/cms', '/api/login'],
|
|
71
|
+
['/internal/cms', '/internal'],
|
|
72
|
+
])('rejects conflicting admin %j and sign-in %j paths', (admin, signIn) => {
|
|
73
|
+
expect(() => resolveRoutes({ admin, signIn })).toThrow(/routes\.signIn/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('normalizeRootRelativeRedirect', () => {
|
|
77
|
+
it.each([
|
|
78
|
+
['/cms', '/cms'],
|
|
79
|
+
['/cms/account?tab=profile#name', '/cms/account?tab=profile#name'],
|
|
80
|
+
['/cms/account?query=hello world', '/cms/account?query=hello%20world'],
|
|
81
|
+
])('accepts and canonicalizes %j', (value, expected) => {
|
|
82
|
+
expect(normalizeRootRelativeRedirect(value)).toBe(expected);
|
|
83
|
+
});
|
|
84
|
+
it.each([
|
|
85
|
+
'',
|
|
86
|
+
' /cms',
|
|
87
|
+
'https://evil.test',
|
|
88
|
+
'//evil.test',
|
|
89
|
+
'/\\evil.test',
|
|
90
|
+
'/cms\\account',
|
|
91
|
+
'/cms\naccount',
|
|
92
|
+
'/cms\u0085account',
|
|
93
|
+
'/cms/../account',
|
|
94
|
+
'/cms/./account',
|
|
95
|
+
'/%2F%2Fevil.test',
|
|
96
|
+
'/cms/%2e%2e/account',
|
|
97
|
+
])('rejects %j', (value) => {
|
|
98
|
+
expect(normalizeRootRelativeRedirect(value)).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('route configuration boundary', () => {
|
|
102
|
+
it('stores and exposes resolved routes independently of partial input', () => {
|
|
103
|
+
const input = { admin: 'internal/cms/' };
|
|
104
|
+
const registered = defineClientConfig({
|
|
105
|
+
serverURL: 'https://example.test',
|
|
106
|
+
routes: input,
|
|
107
|
+
collections: [],
|
|
108
|
+
admin: [],
|
|
109
|
+
i18n: {
|
|
110
|
+
interface: { defaultLocale: 'en', locales: [] },
|
|
111
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
input.admin = '/changed';
|
|
115
|
+
expect(registered.routes).toEqual({
|
|
116
|
+
admin: '/internal/cms',
|
|
117
|
+
api: '/api',
|
|
118
|
+
signIn: '/sign-in',
|
|
119
|
+
});
|
|
120
|
+
expect(getClientConfig().routes).toBe(registered.routes);
|
|
121
|
+
expect(Object.isFrozen(registered.routes)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it('prevents post-registration route mutation', () => {
|
|
124
|
+
const { routes } = defineClientConfig({
|
|
125
|
+
serverURL: 'https://example.test',
|
|
126
|
+
routes: { admin: '/internal/cms' },
|
|
127
|
+
collections: [],
|
|
128
|
+
admin: [],
|
|
129
|
+
i18n: {
|
|
130
|
+
interface: { defaultLocale: 'en', locales: [] },
|
|
131
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
expect(() => {
|
|
135
|
+
;
|
|
136
|
+
routes.admin = '/changed';
|
|
137
|
+
}).toThrow(TypeError);
|
|
138
|
+
expect(getClientConfig().routes.admin).toBe('/internal/cms');
|
|
139
|
+
});
|
|
140
|
+
it('rejects unsafe routes during registration', () => {
|
|
141
|
+
expect(() => defineClientConfig({
|
|
142
|
+
serverURL: 'https://example.test',
|
|
143
|
+
routes: { admin: '/cms?next=/account' },
|
|
144
|
+
collections: [],
|
|
145
|
+
admin: [],
|
|
146
|
+
i18n: {
|
|
147
|
+
interface: { defaultLocale: 'en', locales: [] },
|
|
148
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
149
|
+
},
|
|
150
|
+
})).toThrow(/routes\.admin/);
|
|
151
|
+
});
|
|
152
|
+
});
|
package/dist/core.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ import { type AbilityDescriptor, AbilityRegistry, type SessionProvider } from '@
|
|
|
9
9
|
import type { Logger as PinoLogger } from 'pino';
|
|
10
10
|
import { type BylineLogger } from './lib/logger.js';
|
|
11
11
|
import { type CollectionRecord } from './services/collection-bootstrap.js';
|
|
12
|
-
import type { CollectionDefinition, IDbAdapter, IStorageProvider, ServerConfig } from './@types/index.js';
|
|
12
|
+
import type { CollectionDefinition, IDbAdapter, IStorageProvider, ResolvedServerConfig, ServerConfig } from './@types/index.js';
|
|
13
13
|
export interface BylineCore<TAdminStore = unknown> {
|
|
14
|
-
config:
|
|
14
|
+
config: ResolvedServerConfig<TAdminStore>;
|
|
15
15
|
collections: readonly CollectionDefinition[];
|
|
16
16
|
db: IDbAdapter;
|
|
17
17
|
storage: IStorageProvider | undefined;
|
package/dist/core.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { AbilityRegistry } from '@byline/auth';
|
|
9
9
|
import { registerCollectionAbilities } from './auth/register-collection-abilities.js';
|
|
10
|
-
import { defineBylineCore,
|
|
10
|
+
import { defineBylineCore, getBylineCoreUnsafe, registerServerConfig, resolveServerConfig, } from './config/config.js';
|
|
11
11
|
import { createBylineLogger, defineLogger } from './lib/logger.js';
|
|
12
12
|
import { Registry } from './lib/registry.js';
|
|
13
13
|
import { ensureCollections } from './services/collection-bootstrap.js';
|
|
14
14
|
import { discoverCounterGroups } from './services/discover-counter-groups.js';
|
|
15
|
+
import { validateTreeAuditCapability } from './services/document-lifecycle/audit.js';
|
|
15
16
|
import { validateTranslations } from './services/i18n-validator.js';
|
|
16
17
|
import { validateRichTextFieldFlags } from './services/richtext-populate.js';
|
|
17
18
|
import { validateSearchConfig } from './services/validate-search-config.js';
|
|
@@ -32,11 +33,12 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
32
33
|
const { pino } = await import('pino');
|
|
33
34
|
pinoLogger = pino({ level: 'info' });
|
|
34
35
|
}
|
|
36
|
+
const resolvedConfig = resolveServerConfig(config);
|
|
35
37
|
const registry = new Registry()
|
|
36
|
-
.addValue('config',
|
|
37
|
-
.addValue('collections',
|
|
38
|
-
.addValue('db',
|
|
39
|
-
.addValue('storage',
|
|
38
|
+
.addValue('config', resolvedConfig)
|
|
39
|
+
.addValue('collections', resolvedConfig.collections)
|
|
40
|
+
.addValue('db', resolvedConfig.db)
|
|
41
|
+
.addValue('storage', resolvedConfig.storage)
|
|
40
42
|
.addFactory('logger', createBylineLogger);
|
|
41
43
|
const composed = registry.compose({ pinoLogger });
|
|
42
44
|
// Validate richText field flags against the registered server adapter
|
|
@@ -44,15 +46,18 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
44
46
|
// (both flags off) and missing-adapter cases at boot rather than at
|
|
45
47
|
// request time.
|
|
46
48
|
validateRichTextFieldFlags(composed.collections, {
|
|
47
|
-
populate:
|
|
48
|
-
embed:
|
|
49
|
+
populate: resolvedConfig.fields?.richText?.populate != null,
|
|
50
|
+
embed: resolvedConfig.fields?.richText?.embed != null,
|
|
49
51
|
});
|
|
50
52
|
// Validate search configuration: a collection that opts into search must
|
|
51
53
|
// have a SearchProvider registered, otherwise indexing / client.search()
|
|
52
54
|
// would silently no-op. Fail-fast at boot, same posture as richText above.
|
|
53
55
|
validateSearchConfig(composed.collections, {
|
|
54
|
-
provider:
|
|
56
|
+
provider: resolvedConfig.search != null,
|
|
55
57
|
});
|
|
58
|
+
// Tree edges are unversioned metadata and may only run on adapters that can
|
|
59
|
+
// lock, mutate, and append audit rows in one transaction.
|
|
60
|
+
validateTreeAuditCapability(composed.collections, composed.db);
|
|
56
61
|
// Validate the admin i18n translation registry against the configured
|
|
57
62
|
// interface locale set. Throws on structural errors (missing bundle for
|
|
58
63
|
// a declared locale, defaultLocale outside the permitted set, …); soft
|
|
@@ -62,9 +67,9 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
62
67
|
// validator takes the assembled shape so locale config and bundle
|
|
63
68
|
// travel together.
|
|
64
69
|
const i18nValidation = validateTranslations({
|
|
65
|
-
defaultLocale:
|
|
66
|
-
locales:
|
|
67
|
-
translations:
|
|
70
|
+
defaultLocale: resolvedConfig.i18n.interface.defaultLocale,
|
|
71
|
+
locales: resolvedConfig.i18n.interface.locales,
|
|
72
|
+
translations: resolvedConfig.i18n.translations,
|
|
68
73
|
});
|
|
69
74
|
for (const warning of i18nValidation.warnings) {
|
|
70
75
|
composed.logger.warn({
|
|
@@ -73,9 +78,6 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
73
78
|
missingKeys: warning.missingKeys,
|
|
74
79
|
}, `[i18n] '${warning.locale}.${warning.namespace}' is missing ${warning.missingKeys.length} key(s) relative to the other locales`);
|
|
75
80
|
}
|
|
76
|
-
// Backward compat: populate globalThis singletons
|
|
77
|
-
defineServerConfig(config);
|
|
78
|
-
defineLogger(composed.logger);
|
|
79
81
|
// Reconcile collection definitions with the database: insert new rows,
|
|
80
82
|
// bump schema versions when the fingerprint has drifted, and build the
|
|
81
83
|
// in-memory record cache used by the lifecycle/upload/client paths.
|
|
@@ -140,6 +142,10 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
140
142
|
sessionProvider: composed.config.sessionProvider,
|
|
141
143
|
adminStore: composed.config.adminStore,
|
|
142
144
|
};
|
|
145
|
+
// Commit globals only after the replacement core has fully initialized. A
|
|
146
|
+
// failed reinitialization leaves the prior config, logger, and core intact.
|
|
147
|
+
registerServerConfig(resolvedConfig);
|
|
148
|
+
defineLogger(composed.logger);
|
|
143
149
|
// Register on the global singleton so server-side packages
|
|
144
150
|
// (`@byline/host-tanstack-start/server-fns/*`, future hosts) can read
|
|
145
151
|
// post-init state via `getBylineCore()` instead of importing the
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defineServerConfig, getServerConfig } from './config/config.js';
|
|
3
|
+
import { initBylineCore } from './core.js';
|
|
4
|
+
function serverConfig(admin) {
|
|
5
|
+
return {
|
|
6
|
+
serverURL: 'https://example.test',
|
|
7
|
+
routes: { admin },
|
|
8
|
+
collections: [],
|
|
9
|
+
db: {},
|
|
10
|
+
i18n: {
|
|
11
|
+
interface: { defaultLocale: 'en', locales: [] },
|
|
12
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('initBylineCore configuration registration', () => {
|
|
17
|
+
it('does not overwrite a valid singleton when synchronous validation fails', async () => {
|
|
18
|
+
const valid = defineServerConfig(serverConfig('/stable/admin'));
|
|
19
|
+
const invalid = serverConfig('/replacement/admin');
|
|
20
|
+
invalid.i18n = {
|
|
21
|
+
interface: { defaultLocale: 'en', locales: ['en'] },
|
|
22
|
+
content: { defaultLocale: 'en', locales: [] },
|
|
23
|
+
};
|
|
24
|
+
await expect(initBylineCore(invalid, {})).rejects.toThrow(/translations bundle/i);
|
|
25
|
+
expect(getServerConfig()).toBe(valid);
|
|
26
|
+
expect(getServerConfig().routes.admin).toBe('/stable/admin');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
* The host-framework request bridge — the seam that makes the server-side
|
|
10
|
+
* client stack (`@byline/client/server`) host-agnostic.
|
|
11
|
+
*
|
|
12
|
+
* Everything request-scoped in that stack bottoms out in three
|
|
13
|
+
* primitives: "which request am I in" (identity for per-request
|
|
14
|
+
* memoization), "read a cookie", and "write a cookie" (admin session
|
|
15
|
+
* refresh rotates tokens). A host adapter implements those three over its
|
|
16
|
+
* framework's runtime and registers the bridge once at server boot;
|
|
17
|
+
* `@byline/client/server` is written against the interface and never
|
|
18
|
+
* imports a framework.
|
|
19
|
+
*
|
|
20
|
+
* Registration follows the same pattern as the config singletons in
|
|
21
|
+
* `config/config.ts`: a `Symbol.for` slot on `globalThis`, so every copy
|
|
22
|
+
* of this module (Vite SSR can resolve workspace-linked packages through
|
|
23
|
+
* different module graphs) shares the same state. Server-only by nature —
|
|
24
|
+
* hosts register from their boot path (side-effect imports guarantee
|
|
25
|
+
* registration before any request is handled), and nothing in a browser
|
|
26
|
+
* graph should ever reach for it.
|
|
27
|
+
*/
|
|
28
|
+
export interface HostCookieSetOptions {
|
|
29
|
+
httpOnly?: boolean;
|
|
30
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
31
|
+
secure?: boolean;
|
|
32
|
+
path?: string;
|
|
33
|
+
maxAge?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface HostRequestBridge {
|
|
36
|
+
/**
|
|
37
|
+
* A stable identity object for the current HTTP request, or `undefined`
|
|
38
|
+
* when running outside a request (seed scripts, background jobs, unit
|
|
39
|
+
* tests). Used purely as a WeakMap key for per-request memoization —
|
|
40
|
+
* never inspected.
|
|
41
|
+
*/
|
|
42
|
+
getRequest(): object | undefined;
|
|
43
|
+
/** Read a request cookie. Returns `undefined` when not present. */
|
|
44
|
+
getCookie(name: string): string | undefined;
|
|
45
|
+
/** Write a response cookie (admin session refresh, preview toggles). */
|
|
46
|
+
setCookie(name: string, value: string, options?: HostCookieSetOptions): void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register the host adapter's bridge. Idempotent and last-write-wins —
|
|
50
|
+
* host adapters register from side-effect imports, which may evaluate
|
|
51
|
+
* more than once across module graphs.
|
|
52
|
+
*/
|
|
53
|
+
export declare function registerHostRequestBridge(bridge: HostRequestBridge): void;
|
|
54
|
+
/** The registered bridge, or `undefined` when no host has registered one. */
|
|
55
|
+
export declare function tryGetHostRequestBridge(): HostRequestBridge | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The registered bridge, throwing with setup guidance when absent. Cookie
|
|
58
|
+
* reads/writes require a host; scripts and tests that have no request
|
|
59
|
+
* should pass an explicit `requestContext` to `createBylineClient`
|
|
60
|
+
* instead of using the request-bound getters.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getHostRequestBridge(): HostRequestBridge;
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
const BYLINE_HOST_REQUEST_BRIDGE = Symbol.for('__byline_host_request_bridge__');
|
|
9
|
+
/**
|
|
10
|
+
* Register the host adapter's bridge. Idempotent and last-write-wins —
|
|
11
|
+
* host adapters register from side-effect imports, which may evaluate
|
|
12
|
+
* more than once across module graphs.
|
|
13
|
+
*/
|
|
14
|
+
export function registerHostRequestBridge(bridge) {
|
|
15
|
+
;
|
|
16
|
+
globalThis[BYLINE_HOST_REQUEST_BRIDGE] = bridge;
|
|
17
|
+
}
|
|
18
|
+
/** The registered bridge, or `undefined` when no host has registered one. */
|
|
19
|
+
export function tryGetHostRequestBridge() {
|
|
20
|
+
return globalThis[BYLINE_HOST_REQUEST_BRIDGE] ?? undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The registered bridge, throwing with setup guidance when absent. Cookie
|
|
24
|
+
* reads/writes require a host; scripts and tests that have no request
|
|
25
|
+
* should pass an explicit `requestContext` to `createBylineClient`
|
|
26
|
+
* instead of using the request-bound getters.
|
|
27
|
+
*/
|
|
28
|
+
export function getHostRequestBridge() {
|
|
29
|
+
const bridge = tryGetHostRequestBridge();
|
|
30
|
+
if (!bridge) {
|
|
31
|
+
throw new Error('No HostRequestBridge registered. A host adapter (e.g. ' +
|
|
32
|
+
'@byline/host-tanstack-start) must call registerHostRequestBridge() ' +
|
|
33
|
+
'at server boot before request-bound client getters can resolve ' +
|
|
34
|
+
'cookies. Scripts and tests should pass an explicit requestContext ' +
|
|
35
|
+
'to createBylineClient instead.');
|
|
36
|
+
}
|
|
37
|
+
return bridge;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
export * from './@types/index.js';
|
|
2
|
-
export { applyBeforeRead, assertActorCanPerform, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
|
|
2
|
+
export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
|
|
3
3
|
export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
|
|
4
4
|
export { resolveRoutes } from './config/routes.js';
|
|
5
5
|
export { validateAdminConfigs } from './config/validate-admin-configs.js';
|
|
6
6
|
export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
7
7
|
export { type BylineCore, getBylineCore, initBylineCore } from './core.js';
|
|
8
8
|
export * from './defaults/default-values.js';
|
|
9
|
-
export {
|
|
9
|
+
export { getHostRequestBridge, type HostCookieSetOptions, type HostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
10
|
+
export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
10
11
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
11
12
|
export { type BylineLogger, getLogger } from './lib/logger.js';
|
|
12
13
|
export { AsyncRegistry, type RegisteredServices, Registry } from './lib/registry.js';
|
|
13
14
|
export * from './patches/index.js';
|
|
14
|
-
export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere, parseSort, parseWhere, } from './query/parse-where.js';
|
|
15
|
+
export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
|
|
15
16
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
16
17
|
export * from './services/index.js';
|
|
17
18
|
export * from './storage/index.js';
|
|
19
|
+
export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
|
|
18
20
|
export { formatTextValue, looksLikeISODate, type SlugifierFn, type SlugifyContext, slugify, } from './utils/slugify.js';
|
|
19
21
|
export { getUploadFields, hasUploadField, isUploadField } from './utils/storage-utils.js';
|
|
20
22
|
export * from './workflow/index.js';
|
package/dist/index.js
CHANGED
|
@@ -15,22 +15,24 @@
|
|
|
15
15
|
// through this main entry or `@byline/client`.
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
export * from './@types/index.js';
|
|
18
|
-
export { applyBeforeRead, assertActorCanPerform, COLLECTION_ABILITY_VERBS, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
|
|
18
|
+
export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
|
|
19
19
|
export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
|
|
20
20
|
export { resolveRoutes } from './config/routes.js';
|
|
21
21
|
export { validateAdminConfigs } from './config/validate-admin-configs.js';
|
|
22
22
|
export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
23
23
|
export { getBylineCore, initBylineCore } from './core.js';
|
|
24
24
|
export * from './defaults/default-values.js';
|
|
25
|
-
export {
|
|
25
|
+
export { getHostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
|
|
26
|
+
export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
|
|
26
27
|
export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
|
|
27
28
|
export { getLogger } from './lib/logger.js';
|
|
28
29
|
export { AsyncRegistry, Registry } from './lib/registry.js';
|
|
29
30
|
export * from './patches/index.js';
|
|
30
|
-
export { mergePredicates, parseSort, parseWhere, } from './query/parse-where.js';
|
|
31
|
+
export { mergePredicates, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
|
|
31
32
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
32
33
|
export * from './services/index.js';
|
|
33
34
|
export * from './storage/index.js';
|
|
35
|
+
export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
|
|
34
36
|
export { formatTextValue, looksLikeISODate, slugify, } from './utils/slugify.js';
|
|
35
37
|
export { getUploadFields, hasUploadField, isUploadField } from './utils/storage-utils.js';
|
|
36
38
|
export * from './workflow/index.js';
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -85,9 +85,15 @@ export declare const ErrorCodes: {
|
|
|
85
85
|
readonly DATABASE: 'ERR_DATABASE';
|
|
86
86
|
readonly STORAGE: 'ERR_STORAGE';
|
|
87
87
|
readonly READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED';
|
|
88
|
+
readonly READ_RECURSION: 'ERR_READ_RECURSION';
|
|
88
89
|
readonly PATH_CONFLICT: 'ERR_PATH_CONFLICT';
|
|
89
90
|
readonly AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED';
|
|
91
|
+
readonly TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED';
|
|
90
92
|
};
|
|
93
|
+
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
94
|
+
export declare const TREE_PLACEMENT_STALE_MARKER = "[ERR_CONFLICT:TREE_PLACEMENT_STALE]";
|
|
95
|
+
/** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
|
|
96
|
+
export declare const TREE_HOOK_COMMITTED_MARKER = "[ERR_TREE_HOOK_COMMITTED]";
|
|
91
97
|
export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
92
98
|
export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
93
99
|
export declare const ERR_CONFLICT: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
@@ -97,6 +103,7 @@ export declare const ERR_PATCH_FAILED: (opts: BylineErrorOptions, errorConstruct
|
|
|
97
103
|
export declare const ERR_DATABASE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
98
104
|
export declare const ERR_STORAGE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
99
105
|
export declare const ERR_READ_BUDGET_EXCEEDED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
106
|
+
export declare const ERR_READ_RECURSION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
100
107
|
/**
|
|
101
108
|
* Thrown when a write attempts to set `path` to a value already used by
|
|
102
109
|
* another document in the same `(collection, locale)` scope. Surfaces the
|
|
@@ -114,3 +121,9 @@ export declare const ERR_PATH_CONFLICT: (opts: BylineErrorOptions, errorConstruc
|
|
|
114
121
|
* docs/03-architecture/03-transactions.md and docs/06-auth-and-security/02-auditability.md.
|
|
115
122
|
*/
|
|
116
123
|
export declare const ERR_AUDIT_UNSUPPORTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
124
|
+
/**
|
|
125
|
+
* A tree mutation and its audit row committed, but its post-commit
|
|
126
|
+
* `afterTreeChange` work failed. Callers still receive a rejection so they can
|
|
127
|
+
* reconcile, while transports can distinguish it from a rolled-back mutation.
|
|
128
|
+
*/
|
|
129
|
+
export declare const ERR_TREE_HOOK_COMMITTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
package/dist/lib/errors.js
CHANGED
|
@@ -53,6 +53,7 @@ export class BylineError extends Error {
|
|
|
53
53
|
super(message, { cause });
|
|
54
54
|
}
|
|
55
55
|
this.code = code;
|
|
56
|
+
this.name = 'BylineError';
|
|
56
57
|
this.details = details;
|
|
57
58
|
this.logExtra = logExtra;
|
|
58
59
|
this.logLevel = logLevel;
|
|
@@ -118,9 +119,15 @@ export const ErrorCodes = {
|
|
|
118
119
|
DATABASE: 'ERR_DATABASE',
|
|
119
120
|
STORAGE: 'ERR_STORAGE',
|
|
120
121
|
READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED',
|
|
122
|
+
READ_RECURSION: 'ERR_READ_RECURSION',
|
|
121
123
|
PATH_CONFLICT: 'ERR_PATH_CONFLICT',
|
|
122
124
|
AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED',
|
|
125
|
+
TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED',
|
|
123
126
|
};
|
|
127
|
+
/** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
|
|
128
|
+
export const TREE_PLACEMENT_STALE_MARKER = '[ERR_CONFLICT:TREE_PLACEMENT_STALE]';
|
|
129
|
+
/** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
|
|
130
|
+
export const TREE_HOOK_COMMITTED_MARKER = '[ERR_TREE_HOOK_COMMITTED]';
|
|
124
131
|
// ---------------------------------------------------------------------------
|
|
125
132
|
// Pre-instantiated factories
|
|
126
133
|
// ---------------------------------------------------------------------------
|
|
@@ -133,6 +140,7 @@ export const ERR_PATCH_FAILED = createErrorType(ErrorCodes.PATCH_FAILED);
|
|
|
133
140
|
export const ERR_DATABASE = createErrorType(ErrorCodes.DATABASE);
|
|
134
141
|
export const ERR_STORAGE = createErrorType(ErrorCodes.STORAGE);
|
|
135
142
|
export const ERR_READ_BUDGET_EXCEEDED = createErrorType(ErrorCodes.READ_BUDGET_EXCEEDED);
|
|
143
|
+
export const ERR_READ_RECURSION = createErrorType(ErrorCodes.READ_RECURSION);
|
|
136
144
|
/**
|
|
137
145
|
* Thrown when a write attempts to set `path` to a value already used by
|
|
138
146
|
* another document in the same `(collection, locale)` scope. Surfaces the
|
|
@@ -150,3 +158,9 @@ export const ERR_PATH_CONFLICT = createErrorType(ErrorCodes.PATH_CONFLICT, 'warn
|
|
|
150
158
|
* docs/03-architecture/03-transactions.md and docs/06-auth-and-security/02-auditability.md.
|
|
151
159
|
*/
|
|
152
160
|
export const ERR_AUDIT_UNSUPPORTED = createErrorType(ErrorCodes.AUDIT_UNSUPPORTED);
|
|
161
|
+
/**
|
|
162
|
+
* A tree mutation and its audit row committed, but its post-commit
|
|
163
|
+
* `afterTreeChange` work failed. Callers still receive a rejection so they can
|
|
164
|
+
* reconcile, while transports can distinguish it from a rolled-back mutation.
|
|
165
|
+
*/
|
|
166
|
+
export const ERR_TREE_HOOK_COMMITTED = createErrorType(ErrorCodes.TREE_HOOK_COMMITTED, 'warn');
|
|
@@ -87,6 +87,15 @@ declare const DOCUMENT_SORT_COLUMNS: Record<string, string>;
|
|
|
87
87
|
* sensible composition through a relation hop.
|
|
88
88
|
*/
|
|
89
89
|
export declare function parseWhere(where: WhereClause | undefined, definition: CollectionDefinition, ctx?: ParseContext): Promise<ParsedWhere>;
|
|
90
|
+
/**
|
|
91
|
+
* Compile a predicate wholly to adapter `DocumentFilter`s. Unlike
|
|
92
|
+
* `parseWhere`, top-level `status` and `path` are emitted as document-column
|
|
93
|
+
* filters rather than scalar list-query options. This is the form used by
|
|
94
|
+
* `beforeRead` on detail, populate, count, and tree reads.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parsePredicateFilters(where: QueryPredicate | undefined, definition: CollectionDefinition, ctx?: ParseContext, options?: {
|
|
97
|
+
strict?: boolean;
|
|
98
|
+
}): Promise<DocumentFilter[]>;
|
|
90
99
|
/**
|
|
91
100
|
* Combine a `beforeRead` hook predicate with a caller-supplied where
|
|
92
101
|
* clause using implicit AND. Returns whichever side is non-empty, or
|