@byline/host-tanstack-start 3.21.0 → 4.0.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/admin-shell/admin-activity/list.js +15 -5
- package/dist/admin-shell/admin-roles/delete.js +2 -1
- package/dist/admin-shell/admin-roles/list.js +2 -1
- package/dist/admin-shell/admin-users/delete.js +2 -1
- package/dist/admin-shell/admin-users/list.js +4 -7
- package/dist/admin-shell/chrome/admin-app-bar.js +2 -1
- package/dist/admin-shell/chrome/admin-menu-drawer.js +9 -8
- package/dist/admin-shell/chrome/admin-menu-provider.js +2 -1
- package/dist/admin-shell/chrome/dashboard.js +4 -3
- package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -2
- package/dist/admin-shell/chrome/sign-in-page.js +2 -2
- package/dist/admin-shell/collections/create.js +4 -3
- package/dist/admin-shell/collections/document-history.js +6 -1
- package/dist/admin-shell/collections/edit.js +18 -14
- package/dist/admin-shell/collections/history.js +5 -4
- package/dist/admin-shell/collections/list.js +8 -7
- package/dist/admin-shell/collections/restore-version-modal.js +2 -1
- package/dist/admin-shell/collections/tree-list.js +53 -17
- package/dist/admin-shell/collections/view-menu.js +9 -8
- package/dist/auth/auth-context.d.ts +13 -0
- package/dist/auth/auth-context.js +4 -0
- package/dist/auth/request-scope.d.ts +16 -0
- package/dist/auth/request-scope.js +24 -0
- package/dist/integrations/abilities.js +8 -7
- package/dist/integrations/byline-public-client.d.ts +38 -0
- package/dist/integrations/byline-public-client.js +8 -4
- package/dist/integrations/byline-viewer-client.d.ts +6 -0
- package/dist/integrations/byline-viewer-client.js +22 -17
- package/dist/integrations/start-errors.js +2 -1
- package/dist/routes/admin-path.d.ts +22 -0
- package/dist/routes/admin-path.js +35 -0
- package/dist/routes/create-admin-account-route.js +3 -2
- package/dist/routes/create-admin-activity-route.js +3 -2
- package/dist/routes/create-admin-dashboard-route.js +2 -1
- package/dist/routes/create-admin-layout-route.d.ts +1 -1
- package/dist/routes/create-admin-layout-route.js +3 -1
- package/dist/routes/create-admin-permissions-route.js +3 -2
- package/dist/routes/create-admin-role-edit-route.js +4 -3
- package/dist/routes/create-admin-roles-list-route.js +3 -2
- package/dist/routes/create-admin-user-edit-route.js +4 -3
- package/dist/routes/create-admin-users-list-route.js +3 -2
- package/dist/routes/create-collection-api-route.js +5 -4
- package/dist/routes/create-collection-create-route.js +4 -3
- package/dist/routes/create-collection-edit-route.js +4 -3
- package/dist/routes/create-collection-history-route.js +5 -4
- package/dist/routes/create-collection-list-route.js +3 -2
- package/dist/routes/create-sign-in-route.js +3 -2
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/sign-in-path.d.ts +13 -0
- package/dist/routes/sign-in-path.js +19 -0
- package/dist/server-fns/collections/delete-outcome.d.ts +30 -0
- package/dist/server-fns/collections/delete-outcome.js +34 -0
- package/dist/server-fns/collections/delete.d.ts +1 -4
- package/dist/server-fns/collections/delete.js +2 -4
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- package/dist/server-fns/collections/tree-mutation.d.ts +46 -0
- package/dist/server-fns/collections/tree-mutation.js +109 -0
- package/dist/server-fns/collections/tree-read.d.ts +6 -0
- package/dist/server-fns/collections/tree-read.js +15 -0
- package/dist/server-fns/collections/tree.d.ts +5 -4
- package/dist/server-fns/collections/tree.js +15 -12
- package/dist/server-fns/collections/update.js +2 -1
- package/dist/vite/client-hook-boundary.d.ts +19 -0
- package/dist/vite/client-hook-boundary.js +20 -0
- package/dist/vite/index.d.ts +8 -0
- package/dist/vite/index.js +1 -0
- package/package.json +15 -9
- package/src/admin-shell/admin-activity/list.tsx +13 -3
- package/src/admin-shell/admin-roles/delete.tsx +2 -1
- package/src/admin-shell/admin-roles/list.tsx +2 -1
- package/src/admin-shell/admin-users/delete.tsx +3 -2
- package/src/admin-shell/admin-users/list.tsx +9 -8
- package/src/admin-shell/chrome/admin-app-bar.tsx +2 -1
- package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -8
- package/src/admin-shell/chrome/admin-menu-provider.tsx +2 -1
- package/src/admin-shell/chrome/dashboard.tsx +4 -3
- package/src/admin-shell/chrome/sign-in-page.tsx +3 -3
- package/src/admin-shell/collections/create.tsx +4 -3
- package/src/admin-shell/collections/document-history.tsx +5 -0
- package/src/admin-shell/collections/edit.tsx +20 -13
- package/src/admin-shell/collections/history.tsx +6 -5
- package/src/admin-shell/collections/list.tsx +8 -7
- package/src/admin-shell/collections/restore-version-modal.tsx +2 -1
- package/src/admin-shell/collections/tree-list.tsx +78 -22
- package/src/admin-shell/collections/view-menu.tsx +9 -8
- package/src/auth/auth-context.test.node.ts +87 -0
- package/src/auth/auth-context.ts +18 -0
- package/src/auth/request-scope.test.node.ts +93 -0
- package/src/auth/request-scope.ts +62 -0
- package/src/integrations/abilities.tsx +14 -4
- package/src/integrations/byline-client.ts +6 -4
- package/src/integrations/byline-public-client.ts +18 -4
- package/src/integrations/byline-viewer-client.ts +30 -18
- package/src/integrations/request-context-contract.test.node.ts +131 -0
- package/src/integrations/request-context-factories.test.node.ts +140 -0
- package/src/integrations/start-errors.test.node.ts +22 -0
- package/src/integrations/start-errors.ts +1 -0
- package/src/routes/admin-path.test.node.ts +129 -0
- package/src/routes/admin-path.ts +66 -0
- package/src/routes/auth-routes.test.node.ts +39 -0
- package/src/routes/create-admin-account-route.tsx +3 -2
- package/src/routes/create-admin-activity-route.tsx +3 -2
- package/src/routes/create-admin-dashboard-route.tsx +2 -1
- package/src/routes/create-admin-layout-route.tsx +4 -4
- package/src/routes/create-admin-permissions-route.tsx +3 -2
- package/src/routes/create-admin-role-edit-route.tsx +7 -3
- package/src/routes/create-admin-roles-list-route.tsx +3 -2
- package/src/routes/create-admin-user-edit-route.tsx +7 -3
- package/src/routes/create-admin-users-list-route.tsx +3 -2
- package/src/routes/create-collection-api-route.tsx +8 -4
- package/src/routes/create-collection-create-route.tsx +7 -3
- package/src/routes/create-collection-edit-route.tsx +7 -3
- package/src/routes/create-collection-history-route.tsx +8 -4
- package/src/routes/create-collection-list-route.tsx +3 -2
- package/src/routes/create-sign-in-route.tsx +9 -3
- package/src/routes/index.ts +11 -0
- package/src/routes/sign-in-path.test.node.ts +79 -0
- package/src/routes/sign-in-path.ts +36 -0
- package/src/server-fns/collections/delete-outcome.test.node.ts +72 -0
- package/src/server-fns/collections/delete-outcome.ts +80 -0
- package/src/server-fns/collections/delete.ts +2 -1
- package/src/server-fns/collections/get.ts +2 -0
- package/src/server-fns/collections/index.ts +1 -0
- package/src/server-fns/collections/tree-mutation.test.node.ts +225 -0
- package/src/server-fns/collections/tree-mutation.ts +165 -0
- package/src/server-fns/collections/tree-read.test.node.ts +46 -0
- package/src/server-fns/collections/tree-read.ts +26 -0
- package/src/server-fns/collections/tree.ts +26 -10
- package/src/server-fns/collections/update.ts +3 -0
- package/src/vite/client-hook-boundary.test.node.ts +50 -0
- package/src/vite/client-hook-boundary.ts +45 -0
- package/src/vite/index.ts +9 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { CollectionHandle, PlaceTreeNodeOptions, RemoveFromTreeOptions } from '@byline/client'
|
|
2
|
+
import { ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER } from '@byline/core'
|
|
3
|
+
|
|
4
|
+
function hasErrorCode(error: unknown, code: string): boolean {
|
|
5
|
+
return (
|
|
6
|
+
typeof error === 'object' &&
|
|
7
|
+
error != null &&
|
|
8
|
+
'code' in error &&
|
|
9
|
+
(error as { code?: unknown }).code === code
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasMessageMarker(error: unknown, marker: string): boolean {
|
|
14
|
+
return error instanceof Error && error.message.startsWith(marker)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Prefer the code; the marker survives transports that reconstruct a code-less Error. */
|
|
18
|
+
export function isTreeMutationConflict(error: unknown): boolean {
|
|
19
|
+
if (hasErrorCode(error, ErrorCodes.CONFLICT)) return true
|
|
20
|
+
return hasMessageMarker(error, TREE_PLACEMENT_STALE_MARKER)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Detect when tree/audit committed even if transport reconstruction dropped the code. */
|
|
24
|
+
export function isCommittedTreeHookFailure(error: unknown): boolean {
|
|
25
|
+
if (hasErrorCode(error, ErrorCodes.TREE_HOOK_COMMITTED)) return true
|
|
26
|
+
return hasMessageMarker(error, TREE_HOOK_COMMITTED_MARKER)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type AdminTreeMoveOutcome =
|
|
30
|
+
| { status: 'ok' }
|
|
31
|
+
| { status: 'suppressed' }
|
|
32
|
+
| { status: 'mutation-failed'; error: unknown; refreshError?: unknown }
|
|
33
|
+
| { status: 'committed-hook-failed'; error: unknown; refreshError?: unknown }
|
|
34
|
+
| { status: 'refresh-failed'; error: unknown }
|
|
35
|
+
|
|
36
|
+
export interface AdminTreeMoveUiEffects {
|
|
37
|
+
rollback: boolean
|
|
38
|
+
preserveOptimistic: boolean
|
|
39
|
+
structuralFailure: 'conflict' | 'generic' | null
|
|
40
|
+
committedHookWarning: boolean
|
|
41
|
+
refreshWarning: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pure UI policy for every mutation/refresh outcome. */
|
|
45
|
+
export function getAdminTreeMoveUiEffects(outcome: AdminTreeMoveOutcome): AdminTreeMoveUiEffects {
|
|
46
|
+
if (outcome.status === 'mutation-failed') {
|
|
47
|
+
return {
|
|
48
|
+
rollback: true,
|
|
49
|
+
preserveOptimistic: false,
|
|
50
|
+
structuralFailure: isTreeMutationConflict(outcome.error) ? 'conflict' : 'generic',
|
|
51
|
+
committedHookWarning: false,
|
|
52
|
+
refreshWarning: outcome.refreshError != null,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (outcome.status === 'committed-hook-failed') {
|
|
56
|
+
return {
|
|
57
|
+
rollback: false,
|
|
58
|
+
preserveOptimistic: outcome.refreshError != null,
|
|
59
|
+
structuralFailure: null,
|
|
60
|
+
committedHookWarning: true,
|
|
61
|
+
refreshWarning: outcome.refreshError != null,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (outcome.status === 'refresh-failed') {
|
|
65
|
+
return {
|
|
66
|
+
rollback: false,
|
|
67
|
+
preserveOptimistic: true,
|
|
68
|
+
structuralFailure: null,
|
|
69
|
+
committedHookWarning: false,
|
|
70
|
+
refreshWarning: true,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
rollback: outcome.status === 'suppressed',
|
|
75
|
+
preserveOptimistic: false,
|
|
76
|
+
structuralFailure: null,
|
|
77
|
+
committedHookWarning: false,
|
|
78
|
+
refreshWarning: false,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Preserve a committed optimistic move until a later loader returns new row data. */
|
|
83
|
+
export function shouldSyncAdminTreeRows(
|
|
84
|
+
rows: unknown,
|
|
85
|
+
preservedRows: unknown,
|
|
86
|
+
isMoving: boolean
|
|
87
|
+
): boolean {
|
|
88
|
+
return !isMoving && rows !== preservedRows
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Keep mutation and canonical-refresh failures distinct for optimistic tree UIs. */
|
|
92
|
+
export async function executeAdminTreeMove(
|
|
93
|
+
mutate: () => Promise<unknown>,
|
|
94
|
+
refresh: () => Promise<unknown>
|
|
95
|
+
): Promise<AdminTreeMoveOutcome> {
|
|
96
|
+
try {
|
|
97
|
+
await mutate()
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const status = isCommittedTreeHookFailure(error)
|
|
100
|
+
? ('committed-hook-failed' as const)
|
|
101
|
+
: ('mutation-failed' as const)
|
|
102
|
+
try {
|
|
103
|
+
await refresh()
|
|
104
|
+
return { status, error }
|
|
105
|
+
} catch (refreshError) {
|
|
106
|
+
return { status, error, refreshError }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await refresh()
|
|
112
|
+
return { status: 'ok' }
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { status: 'refresh-failed', error }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AdminTreeMoveController {
|
|
119
|
+
isMoving(): boolean
|
|
120
|
+
execute(
|
|
121
|
+
mutate: () => Promise<unknown>,
|
|
122
|
+
refresh: () => Promise<unknown>
|
|
123
|
+
): Promise<AdminTreeMoveOutcome>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Single-flight controller shared by pointer and keyboard tree moves. */
|
|
127
|
+
export function createAdminTreeMoveController(): AdminTreeMoveController {
|
|
128
|
+
let moving = false
|
|
129
|
+
return {
|
|
130
|
+
isMoving: () => moving,
|
|
131
|
+
execute: async (mutate, refresh) => {
|
|
132
|
+
if (moving) return { status: 'suppressed' }
|
|
133
|
+
moving = true
|
|
134
|
+
try {
|
|
135
|
+
return await executeAdminTreeMove(mutate, refresh)
|
|
136
|
+
} finally {
|
|
137
|
+
moving = false
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
|
|
144
|
+
export function placeAdminTreeNode(
|
|
145
|
+
handle: Pick<CollectionHandle, 'placeTreeNode'>,
|
|
146
|
+
documentId: string,
|
|
147
|
+
options: PlaceTreeNodeOptions
|
|
148
|
+
): Promise<{ orderKey: string }> {
|
|
149
|
+
return handle.placeTreeNode(documentId, {
|
|
150
|
+
...options,
|
|
151
|
+
reconcile: options.reconcile ?? true,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Admin retries reconcile post-commit tree hooks unless explicitly disabled. */
|
|
156
|
+
export function removeAdminTreeNode(
|
|
157
|
+
handle: Pick<CollectionHandle, 'removeFromTree'>,
|
|
158
|
+
documentId: string,
|
|
159
|
+
options: RemoveFromTreeOptions = {}
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
return handle.removeFromTree(documentId, {
|
|
162
|
+
...options,
|
|
163
|
+
reconcile: options.reconcile ?? true,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createReadContext } from '@byline/core'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { getAdminTreeParent, getAdminUnplacedTreeDocuments } from './tree-read.js'
|
|
5
|
+
|
|
6
|
+
describe('getAdminTreeParent', () => {
|
|
7
|
+
it('explicitly selects status:any', async () => {
|
|
8
|
+
const getTreeParent = vi.fn().mockResolvedValue({
|
|
9
|
+
placed: false,
|
|
10
|
+
parentDocumentId: null,
|
|
11
|
+
parentVisibility: 'none',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
await getAdminTreeParent({ getTreeParent } as any, 'doc-1')
|
|
15
|
+
|
|
16
|
+
expect(getTreeParent).toHaveBeenCalledWith('doc-1', { status: 'any' })
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('getAdminUnplacedTreeDocuments', () => {
|
|
21
|
+
it('uses scoped any-mode reads so hidden unplaced nodes cannot reappear', async () => {
|
|
22
|
+
const visible = { id: 'visible-unplaced' }
|
|
23
|
+
const hidden = { id: 'hidden-unplaced' }
|
|
24
|
+
const readContext = createReadContext()
|
|
25
|
+
const find = vi.fn(async (options: Record<string, unknown>) => ({
|
|
26
|
+
docs: options._bypassBeforeRead === true ? [visible, hidden] : [visible],
|
|
27
|
+
meta: { total: 1, page: 1, pageSize: 1000, totalPages: 1 },
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
const result = await getAdminUnplacedTreeDocuments(
|
|
31
|
+
{ find } as any,
|
|
32
|
+
new Set(['visible-placed']),
|
|
33
|
+
readContext,
|
|
34
|
+
'en'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect(find).toHaveBeenCalledWith({
|
|
38
|
+
status: 'any',
|
|
39
|
+
locale: 'en',
|
|
40
|
+
pageSize: 1000,
|
|
41
|
+
_readContext: readContext,
|
|
42
|
+
})
|
|
43
|
+
expect(result.map((doc) => doc.id)).toEqual(['visible-unplaced'])
|
|
44
|
+
expect(result).not.toContain(hidden)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClientDocument, CollectionHandle, TreeParentResult } from '@byline/client'
|
|
2
|
+
import type { ReadContext } from '@byline/core'
|
|
3
|
+
|
|
4
|
+
/** Admin tree placement reads must include drafts. */
|
|
5
|
+
export function getAdminTreeParent(
|
|
6
|
+
handle: Pick<CollectionHandle, 'getTreeParent'>,
|
|
7
|
+
documentId: string
|
|
8
|
+
): Promise<TreeParentResult> {
|
|
9
|
+
return handle.getTreeParent(documentId, { status: 'any' })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Find unplaced nodes through the same row scope as the structural tree read. */
|
|
13
|
+
export async function getAdminUnplacedTreeDocuments(
|
|
14
|
+
handle: Pick<CollectionHandle, 'find'>,
|
|
15
|
+
placed: ReadonlySet<string>,
|
|
16
|
+
readContext: ReadContext,
|
|
17
|
+
locale?: string
|
|
18
|
+
): Promise<ClientDocument[]> {
|
|
19
|
+
const all = await handle.find({
|
|
20
|
+
status: 'any',
|
|
21
|
+
locale,
|
|
22
|
+
pageSize: 1000,
|
|
23
|
+
_readContext: readContext,
|
|
24
|
+
})
|
|
25
|
+
return all.docs.filter((doc) => !placed.has(doc.id))
|
|
26
|
+
}
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import { createServerFn } from '@tanstack/react-start'
|
|
18
18
|
|
|
19
|
-
import { ERR_NOT_FOUND, getLogger } from '@byline/core'
|
|
19
|
+
import { createReadContext, ERR_NOT_FOUND, getLogger } from '@byline/core'
|
|
20
20
|
|
|
21
21
|
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
22
22
|
import { getAdminBylineClient } from '../../integrations/byline-client.js'
|
|
23
|
+
import { placeAdminTreeNode, removeAdminTreeNode } from './tree-mutation.js'
|
|
24
|
+
import { getAdminTreeParent, getAdminUnplacedTreeDocuments } from './tree-read.js'
|
|
23
25
|
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
// Place / move a node within the tree (place / reorder / re-parent)
|
|
@@ -33,6 +35,7 @@ export const placeTreeNode = createServerFn({ method: 'POST' })
|
|
|
33
35
|
parentDocumentId: string | null
|
|
34
36
|
beforeDocumentId?: string | null
|
|
35
37
|
afterDocumentId?: string | null
|
|
38
|
+
reconcile?: boolean
|
|
36
39
|
}) => input
|
|
37
40
|
)
|
|
38
41
|
.handler(async ({ data }) => {
|
|
@@ -45,10 +48,11 @@ export const placeTreeNode = createServerFn({ method: 'POST' })
|
|
|
45
48
|
}).log(getLogger())
|
|
46
49
|
}
|
|
47
50
|
const handle = getAdminBylineClient().collection(path)
|
|
48
|
-
const result = await handle
|
|
51
|
+
const result = await placeAdminTreeNode(handle, documentId, {
|
|
49
52
|
parentDocumentId: data.parentDocumentId,
|
|
50
53
|
beforeDocumentId: data.beforeDocumentId ?? null,
|
|
51
54
|
afterDocumentId: data.afterDocumentId ?? null,
|
|
55
|
+
reconcile: data.reconcile,
|
|
52
56
|
})
|
|
53
57
|
return { status: 'ok' as const, orderKey: result.orderKey }
|
|
54
58
|
})
|
|
@@ -58,7 +62,7 @@ export const placeTreeNode = createServerFn({ method: 'POST' })
|
|
|
58
62
|
// ---------------------------------------------------------------------------
|
|
59
63
|
|
|
60
64
|
export const removeFromTree = createServerFn({ method: 'POST' })
|
|
61
|
-
.validator((input: { collection: string; documentId: string }) => input)
|
|
65
|
+
.validator((input: { collection: string; documentId: string; reconcile?: boolean }) => input)
|
|
62
66
|
.handler(async ({ data }) => {
|
|
63
67
|
const { collection: path, documentId } = data
|
|
64
68
|
const config = await ensureCollection(path)
|
|
@@ -68,7 +72,9 @@ export const removeFromTree = createServerFn({ method: 'POST' })
|
|
|
68
72
|
details: { collectionPath: path },
|
|
69
73
|
}).log(getLogger())
|
|
70
74
|
}
|
|
71
|
-
await getAdminBylineClient().collection(path)
|
|
75
|
+
await removeAdminTreeNode(getAdminBylineClient().collection(path), documentId, {
|
|
76
|
+
reconcile: data.reconcile,
|
|
77
|
+
})
|
|
72
78
|
return { status: 'ok' as const }
|
|
73
79
|
})
|
|
74
80
|
|
|
@@ -118,7 +124,7 @@ export const getTreeParent = createServerFn({ method: 'GET' })
|
|
|
118
124
|
details: { collectionPath: path },
|
|
119
125
|
}).log(getLogger())
|
|
120
126
|
}
|
|
121
|
-
return getAdminBylineClient().collection(path)
|
|
127
|
+
return getAdminTreeParent(getAdminBylineClient().collection(path), documentId)
|
|
122
128
|
})
|
|
123
129
|
|
|
124
130
|
// ---------------------------------------------------------------------------
|
|
@@ -154,8 +160,19 @@ export const getCollectionTree = createServerFn({ method: 'GET' })
|
|
|
154
160
|
}).log(getLogger())
|
|
155
161
|
}
|
|
156
162
|
const handle = getAdminBylineClient().collection(path)
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
// Shared deliberately across the structural and unplaced reads: the
|
|
164
|
+
// beforeRead predicate cache is per-ReadContext, so sharing gives both
|
|
165
|
+
// halves one predicate snapshot — a time-dependent predicate (embargo
|
|
166
|
+
// cutoff) evaluated twice could otherwise disagree between the placed
|
|
167
|
+
// forest and the unplaced list. The request-memoized context factory
|
|
168
|
+
// keeps this sharing safe (one request authority per request).
|
|
169
|
+
const readContext = createReadContext()
|
|
170
|
+
|
|
171
|
+
const forest = await handle.getSubtree({
|
|
172
|
+
status: 'any',
|
|
173
|
+
locale: data.locale,
|
|
174
|
+
_readContext: readContext,
|
|
175
|
+
})
|
|
159
176
|
const rows: CollectionTreeRow[] = []
|
|
160
177
|
const placed = new Set<string>()
|
|
161
178
|
const walk = (nodes: typeof forest, depth: number, parentId: string | null): void => {
|
|
@@ -180,9 +197,8 @@ export const getCollectionTree = createServerFn({ method: 'GET' })
|
|
|
180
197
|
|
|
181
198
|
// Surface documents not yet in the tree (e.g. freshly created) so they
|
|
182
199
|
// remain reachable. Trees are small by design, so a single wide read is fine.
|
|
183
|
-
const
|
|
184
|
-
for (const doc of
|
|
185
|
-
if (placed.has(doc.id)) continue
|
|
200
|
+
const unplaced = await getAdminUnplacedTreeDocuments(handle, placed, readContext, data.locale)
|
|
201
|
+
for (const doc of unplaced) {
|
|
186
202
|
rows.push({
|
|
187
203
|
id: doc.id,
|
|
188
204
|
parentId: null,
|
|
@@ -118,6 +118,9 @@ export const updateCollectionDocumentSystemFields = createServerFn({ method: 'PO
|
|
|
118
118
|
locale: locale ?? serverConfig.i18n.content.defaultLocale,
|
|
119
119
|
path: explicitPath,
|
|
120
120
|
availableLocales,
|
|
121
|
+
// A failed after-hook happens after commit. Retrying this same admin save
|
|
122
|
+
// must re-run cache/search reconciliation when DB values are now current.
|
|
123
|
+
reconcile: true,
|
|
121
124
|
})
|
|
122
125
|
|
|
123
126
|
return { status: 'ok' as const }
|
|
@@ -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
|
+
/**
|
|
10
|
+
* Unit coverage for the client-bundle guard's module classifier. Production
|
|
11
|
+
* application builds exercise the Vite plugin itself; these focused cases pin
|
|
12
|
+
* path normalization and the supported server-only filename conventions.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from 'vitest'
|
|
15
|
+
|
|
16
|
+
import { findServerHookModules } from './client-hook-boundary.js'
|
|
17
|
+
|
|
18
|
+
describe('client hook boundary', () => {
|
|
19
|
+
it('finds collection hook implementations and their shared server graph', () => {
|
|
20
|
+
expect(
|
|
21
|
+
findServerHookModules([
|
|
22
|
+
'/project/byline/collections/docs/hooks.ts',
|
|
23
|
+
'/project/byline/collections/content-types/publications/file-hooks.ts',
|
|
24
|
+
'/project/byline/collections/content-types/publications/cover-hooks.tsx',
|
|
25
|
+
'/project/byline/collections/server-hooks.ts?import',
|
|
26
|
+
String.raw`C:\project\byline\collections\create-revalidation-lifecycle-hooks.ts`,
|
|
27
|
+
'/project/byline/collections/events/hooks.mts',
|
|
28
|
+
'/project/byline/collections/run-webhook-side-effects.ts?worker',
|
|
29
|
+
'/project/byline/collections/docs/schema.ts',
|
|
30
|
+
])
|
|
31
|
+
).toEqual([
|
|
32
|
+
'/project/byline/collections/docs/hooks.ts',
|
|
33
|
+
'/project/byline/collections/content-types/publications/file-hooks.ts',
|
|
34
|
+
'/project/byline/collections/content-types/publications/cover-hooks.tsx',
|
|
35
|
+
'/project/byline/collections/server-hooks.ts?import',
|
|
36
|
+
String.raw`C:\project\byline\collections\create-revalidation-lifecycle-hooks.ts`,
|
|
37
|
+
'/project/byline/collections/events/hooks.mts',
|
|
38
|
+
'/project/byline/collections/run-webhook-side-effects.ts?worker',
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('allows schemas and unrelated client modules', () => {
|
|
43
|
+
expect(
|
|
44
|
+
findServerHookModules([
|
|
45
|
+
'/project/byline/collections/docs/schema.ts',
|
|
46
|
+
'/project/src/ui/byline/render-blocks.tsx',
|
|
47
|
+
])
|
|
48
|
+
).toEqual([])
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Production client-bundle guard for server-only Byline lifecycle modules.
|
|
11
|
+
*
|
|
12
|
+
* Collection schemas are isomorphic, but registered lifecycle hooks may
|
|
13
|
+
* import Node APIs, caches, storage SDKs, and other server-only dependencies.
|
|
14
|
+
* This Vite plugin inspects emitted client chunks and fails the build if a
|
|
15
|
+
* hook implementation, server hook registry, or shared lifecycle helper
|
|
16
|
+
* becomes reachable from the browser bundle.
|
|
17
|
+
*/
|
|
18
|
+
import type { Plugin } from 'vite'
|
|
19
|
+
|
|
20
|
+
const SERVER_HOOK_MODULE =
|
|
21
|
+
/\/byline\/collections\/(?:.+\/)?(?:hooks|[^/]+-hooks|[^/]+-lifecycle-hooks|[^/]+-side-effects)\.[cm]?[jt]sx?$/
|
|
22
|
+
|
|
23
|
+
export function findServerHookModules(moduleIds: Iterable<string>): string[] {
|
|
24
|
+
return [...moduleIds].filter((moduleId) => {
|
|
25
|
+
const normalized = moduleId.replaceAll('\\', '/').split('?', 1)[0]
|
|
26
|
+
return SERVER_HOOK_MODULE.test(normalized)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function bylineClientHookBoundary(): Plugin {
|
|
31
|
+
return {
|
|
32
|
+
name: 'byline:client-hook-boundary',
|
|
33
|
+
generateBundle(_options, bundle) {
|
|
34
|
+
if (this.environment?.name !== 'client') return
|
|
35
|
+
const leaked = findServerHookModules(
|
|
36
|
+
Object.values(bundle).flatMap((output) =>
|
|
37
|
+
output.type === 'chunk' ? Object.keys(output.modules) : []
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
if (leaked.length > 0) {
|
|
41
|
+
this.error(`server lifecycle modules entered the client bundle:\n${leaked.join('\n')}`)
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export { bylineClientHookBoundary } from './client-hook-boundary.js'
|