@byline/host-tanstack-start 4.16.0 → 4.18.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/chrome/admin-menu-drawer.js +14 -14
- package/dist/admin-shell/chrome/dashboard.js +41 -5
- package/dist/admin-shell/chrome/dashboard.test.d.ts +8 -0
- package/dist/admin-shell/chrome/router-pager.d.ts +7 -0
- package/dist/admin-shell/chrome/router-pager.js +13 -0
- package/dist/admin-shell/chrome/scroll-to-top.d.ts +20 -0
- package/dist/admin-shell/chrome/scroll-to-top.js +9 -0
- package/dist/admin-shell/collections/api.d.ts +2 -2
- package/dist/admin-shell/collections/create.d.ts +2 -2
- package/dist/admin-shell/collections/create.js +1 -0
- package/dist/admin-shell/collections/edit.d.ts +2 -2
- package/dist/admin-shell/collections/edit.js +1 -0
- package/dist/admin-shell/collections/history.d.ts +4 -12
- package/dist/admin-shell/collections/history.js +112 -303
- package/dist/admin-shell/collections/reindex-button.d.ts +1 -1
- package/dist/admin-shell/collections/reindex-button.js +5 -1
- package/dist/admin-shell/collections/restore-version-modal.d.ts +4 -4
- package/dist/admin-shell/collections/restore-version-modal.js +8 -24
- package/dist/admin-shell/collections/version-history.d.ts +72 -0
- package/dist/admin-shell/collections/version-history.js +259 -0
- package/dist/admin-shell/singletons/history.d.ts +19 -0
- package/dist/admin-shell/singletons/history.js +170 -0
- package/dist/admin-shell/singletons/history.test.d.ts +8 -0
- package/dist/admin-shell/singletons/preview-link.d.ts +12 -0
- package/dist/admin-shell/singletons/preview-link.js +54 -0
- package/dist/admin-shell/singletons/view-menu.d.ts +13 -0
- package/dist/admin-shell/singletons/view-menu.js +116 -0
- package/dist/admin-shell/singletons/view.d.ts +18 -0
- package/dist/admin-shell/singletons/view.js +259 -0
- package/dist/admin-shell/singletons/view.test.d.ts +8 -0
- package/dist/integrations/api-utils.d.ts +18 -4
- package/dist/integrations/api-utils.js +10 -3
- package/dist/routes/create-admin-dashboard-route.js +2 -2
- package/dist/routes/create-collection-api-route.js +3 -3
- 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 +4 -3
- package/dist/routes/create-collection-list-route.js +4 -3
- package/dist/routes/create-singleton-history-route.d.ts +8 -0
- package/dist/routes/create-singleton-history-route.js +99 -0
- package/dist/routes/create-singleton-route.d.ts +35 -0
- package/dist/routes/create-singleton-route.js +130 -0
- package/dist/routes/get-multi-collection-definition.d.ts +10 -0
- package/dist/routes/get-multi-collection-definition.js +6 -0
- package/dist/routes/get-singleton-definition.d.ts +10 -0
- package/dist/routes/get-singleton-definition.js +6 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/server-fns/actors.d.ts +16 -0
- package/dist/server-fns/actors.js +25 -0
- package/dist/server-fns/admin-document-presentation.d.ts +30 -0
- package/dist/server-fns/admin-document-presentation.js +32 -0
- package/dist/server-fns/collections/actors.d.ts +1 -10
- package/dist/server-fns/collections/actors.js +1 -25
- package/dist/server-fns/collections/get.js +9 -26
- package/dist/server-fns/collections/reorder.js +1 -1
- package/dist/server-fns/collections/scheduled-publication.js +2 -2
- package/dist/server-fns/collections/upload.js +3 -3
- package/dist/server-fns/collections/utils.d.ts +1 -7
- package/dist/server-fns/collections/utils.js +1 -4
- package/dist/server-fns/serialise.d.ts +14 -0
- package/dist/server-fns/serialise.js +4 -0
- package/dist/server-fns/singleton-document-read.d.ts +20 -0
- package/dist/server-fns/singleton-document-read.js +10 -0
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +12 -0
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -0
- package/dist/server-fns/singletons/change-status.d.ts +14 -0
- package/dist/server-fns/singletons/change-status.js +7 -0
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +14 -0
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +9 -0
- package/dist/server-fns/singletons/copy-to-locale.d.ts +18 -0
- package/dist/server-fns/singletons/copy-to-locale.js +11 -0
- package/dist/server-fns/singletons/find-by-version.d.ts +16 -0
- package/dist/server-fns/singletons/find-by-version.js +9 -0
- package/dist/server-fns/singletons/get-scheduled-publish.d.ts +12 -0
- package/dist/server-fns/singletons/get-scheduled-publish.js +7 -0
- package/dist/server-fns/singletons/get.d.ts +24 -0
- package/dist/server-fns/singletons/get.js +6 -0
- package/dist/server-fns/singletons/history.d.ts +31 -0
- package/dist/server-fns/singletons/history.js +22 -0
- package/dist/server-fns/singletons/index.d.ts +19 -0
- package/dist/server-fns/singletons/index.js +12 -0
- package/dist/server-fns/singletons/restore-version.d.ts +14 -0
- package/dist/server-fns/singletons/restore-version.js +7 -0
- package/dist/server-fns/singletons/schedule-publish.d.ts +16 -0
- package/dist/server-fns/singletons/schedule-publish.js +10 -0
- package/dist/server-fns/singletons/unpublish.d.ts +12 -0
- package/dist/server-fns/singletons/unpublish.js +7 -0
- package/dist/server-fns/singletons/update.d.ts +14 -0
- package/dist/server-fns/singletons/update.js +14 -0
- package/dist/vitest-jsdom.test.d.ts +8 -0
- package/package.json +24 -18
- package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -13
- package/src/admin-shell/chrome/dashboard.test.tsx +199 -0
- package/src/admin-shell/chrome/dashboard.tsx +43 -9
- package/src/admin-shell/chrome/router-pager.tsx +22 -0
- package/src/admin-shell/chrome/scroll-to-top.test.ts +56 -0
- package/src/admin-shell/chrome/scroll-to-top.ts +28 -0
- package/src/admin-shell/collections/api.tsx +2 -2
- package/src/admin-shell/collections/create.tsx +6 -2
- package/src/admin-shell/collections/edit.tsx +6 -2
- package/src/admin-shell/collections/history.module.css +1 -1
- package/src/admin-shell/collections/history.tsx +162 -442
- package/src/admin-shell/collections/reindex-button.tsx +4 -2
- package/src/admin-shell/collections/restore-version-modal.tsx +19 -25
- package/src/admin-shell/collections/version-history.tsx +349 -0
- package/src/admin-shell/singletons/history.test.tsx +407 -0
- package/src/admin-shell/singletons/history.tsx +185 -0
- package/src/admin-shell/singletons/preview-link.tsx +75 -0
- package/src/admin-shell/singletons/view-menu.tsx +136 -0
- package/src/admin-shell/singletons/view.test.tsx +400 -0
- package/src/admin-shell/singletons/view.tsx +351 -0
- package/src/integrations/api-utils.ts +34 -9
- package/src/routes/create-admin-dashboard-route.test.node.ts +70 -0
- package/src/routes/create-admin-dashboard-route.tsx +10 -2
- package/src/routes/create-collection-api-route.tsx +4 -4
- package/src/routes/create-collection-create-route.tsx +5 -8
- package/src/routes/create-collection-edit-route.tsx +5 -4
- package/src/routes/create-collection-history-route.tsx +5 -8
- package/src/routes/create-collection-list-route.tsx +5 -8
- package/src/routes/create-singleton-history-route.test.node.ts +148 -0
- package/src/routes/create-singleton-history-route.tsx +116 -0
- package/src/routes/create-singleton-route.test.node.ts +172 -0
- package/src/routes/create-singleton-route.tsx +176 -0
- package/src/routes/get-multi-collection-definition.ts +16 -0
- package/src/routes/get-singleton-definition.ts +16 -0
- package/src/routes/index.ts +2 -0
- package/src/server-fns/actors.ts +43 -0
- package/src/server-fns/admin-document-presentation.ts +78 -0
- package/src/server-fns/collections/actors.ts +1 -52
- package/src/server-fns/collections/get.ts +10 -49
- package/src/server-fns/collections/reorder.ts +1 -1
- package/src/server-fns/collections/scheduled-publication.ts +7 -1
- package/src/server-fns/collections/upload-singleton.test.node.ts +164 -0
- package/src/server-fns/collections/upload.ts +7 -3
- package/src/server-fns/collections/utils.ts +1 -9
- package/src/server-fns/serialise.ts +17 -0
- package/src/server-fns/singleton-document-read.ts +32 -0
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +19 -0
- package/src/server-fns/singletons/change-status.ts +19 -0
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +23 -0
- package/src/server-fns/singletons/copy-to-locale.ts +32 -0
- package/src/server-fns/singletons/find-by-version.ts +23 -0
- package/src/server-fns/singletons/get-scheduled-publish.ts +19 -0
- package/src/server-fns/singletons/get.ts +32 -0
- package/src/server-fns/singletons/history.ts +41 -0
- package/src/server-fns/singletons/index.ts +20 -0
- package/src/server-fns/singletons/restore-version.ts +19 -0
- package/src/server-fns/singletons/schedule-publish.ts +24 -0
- package/src/server-fns/singletons/singletons.test.node.ts +298 -0
- package/src/server-fns/singletons/unpublish.ts +19 -0
- package/src/server-fns/singletons/update.ts +31 -0
- package/src/vitest-jsdom.test.tsx +27 -0
package/src/routes/index.ts
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import type { AdminUserRow } from '@byline/admin/admin-users'
|
|
10
|
+
|
|
11
|
+
import { bylineCore } from '../integrations/byline-core.js'
|
|
12
|
+
|
|
13
|
+
export type ActorLabelMap = Record<string, { label: string }>
|
|
14
|
+
|
|
15
|
+
/** Display label: full name → username → email. */
|
|
16
|
+
function labelFor(row: AdminUserRow): string {
|
|
17
|
+
const name = [row.given_name, row.family_name].filter(Boolean).join(' ')
|
|
18
|
+
return name || row.username || row.email
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Batch-resolve admin-user ids to display labels. The admin server-fn layer
|
|
23
|
+
* owns this presentation concern so both collection and singleton history
|
|
24
|
+
* can share it without teaching document storage about admin-realm tables.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveActorLabels(
|
|
27
|
+
ids: Iterable<string | null | undefined>
|
|
28
|
+
): Promise<ActorLabelMap> {
|
|
29
|
+
const unique = [
|
|
30
|
+
...new Set([...ids].filter((id): id is string => typeof id === 'string' && id.length > 0)),
|
|
31
|
+
]
|
|
32
|
+
if (unique.length === 0) return {}
|
|
33
|
+
|
|
34
|
+
const store = bylineCore().adminStore
|
|
35
|
+
if (store == null) return {}
|
|
36
|
+
|
|
37
|
+
const rows = await store.adminUsers.getByIds(unique)
|
|
38
|
+
const map: ActorLabelMap = {}
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
map[row.id] = { label: labelFor(row) }
|
|
41
|
+
}
|
|
42
|
+
return map
|
|
43
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
import {
|
|
10
|
+
buildRelationSummaryPopulateMap,
|
|
11
|
+
type CollectionDefinition,
|
|
12
|
+
getCollectionAdminConfig,
|
|
13
|
+
getCollectionDefinition,
|
|
14
|
+
type PopulateSpec,
|
|
15
|
+
} from '@byline/core'
|
|
16
|
+
|
|
17
|
+
export interface AdminDocumentReadInput {
|
|
18
|
+
locale?: string
|
|
19
|
+
depth?: number
|
|
20
|
+
populateRelations?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AdminDocumentReadOptions {
|
|
24
|
+
locale: string
|
|
25
|
+
populate?: PopulateSpec
|
|
26
|
+
depth?: number
|
|
27
|
+
status: 'any'
|
|
28
|
+
onMissingLocale: 'empty'
|
|
29
|
+
lenient: true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the shared read controls used by collection and singleton editors.
|
|
34
|
+
* Explicit depth is the API-preview mode; otherwise relation-summary mode
|
|
35
|
+
* projects only each target's picker/title fields for first-paint tiles.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveAdminDocumentRead(
|
|
38
|
+
definition: CollectionDefinition | null,
|
|
39
|
+
input: AdminDocumentReadInput
|
|
40
|
+
): { options: AdminDocumentReadOptions; populatedTree: boolean } {
|
|
41
|
+
const populateRequested = typeof input.depth === 'number' && input.depth > 0
|
|
42
|
+
const autoRelationsActive = !populateRequested && input.populateRelations === true
|
|
43
|
+
|
|
44
|
+
let populate: PopulateSpec | undefined
|
|
45
|
+
let depth: number | undefined
|
|
46
|
+
if (populateRequested) {
|
|
47
|
+
populate = '*'
|
|
48
|
+
depth = input.depth
|
|
49
|
+
} else if (autoRelationsActive && definition != null) {
|
|
50
|
+
const populateMap = buildRelationSummaryPopulateMap(definition.fields, (targetPath) => ({
|
|
51
|
+
def: getCollectionDefinition(targetPath),
|
|
52
|
+
admin: getCollectionAdminConfig(targetPath),
|
|
53
|
+
}))
|
|
54
|
+
if (Object.keys(populateMap).length > 0) {
|
|
55
|
+
populate = populateMap
|
|
56
|
+
depth = 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
options: {
|
|
62
|
+
locale: input.locale ?? 'en',
|
|
63
|
+
populate,
|
|
64
|
+
depth,
|
|
65
|
+
status: 'any',
|
|
66
|
+
// Editors need the raw requested-locale values. Untranslated localized
|
|
67
|
+
// fields stay empty instead of falling back, which is the signal that
|
|
68
|
+
// enables the Copy to Locale workflow. Using `omit` would hide the whole
|
|
69
|
+
// resource and make a missing translation look like a missing slot.
|
|
70
|
+
onMissingLocale: 'empty',
|
|
71
|
+
// Older rows may no longer reconstruct exactly against the current
|
|
72
|
+
// schema. Keep the editor loadable and surface those mismatches through
|
|
73
|
+
// `_restoreWarnings`, which the form renders as an alert.
|
|
74
|
+
lenient: true,
|
|
75
|
+
},
|
|
76
|
+
populatedTree: populateRequested || autoRelationsActive,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -6,55 +6,4 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* Audit actor-label resolution (docs/07-auth-and-security/02-auditability.md — Workstream 1).
|
|
11
|
-
*
|
|
12
|
-
* Shaped documents carry only the raw `createdBy` uuid; turning ids into
|
|
13
|
-
* display labels is an **admin-realm** concern, so it happens here — in
|
|
14
|
-
* the admin server-fn layer, which already holds the admin actor and the
|
|
15
|
-
* `AdminStore` — never as a JOIN inside the document storage adapter
|
|
16
|
-
* (which must stay ignorant of admin-realm tables for the future
|
|
17
|
-
* `UserAuth` writer realm).
|
|
18
|
-
*
|
|
19
|
-
* Responses attach the result as an `actors` map alongside the page of
|
|
20
|
-
* documents; the UI joins by id. Ids with no matching admin-user row
|
|
21
|
-
* (deleted users) are absent from the map — consumers render a tombstone
|
|
22
|
-
* label.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { AdminUserRow } from '@byline/admin/admin-users'
|
|
26
|
-
|
|
27
|
-
import { bylineCore } from '../../integrations/byline-core.js'
|
|
28
|
-
|
|
29
|
-
export type ActorLabelMap = Record<string, { label: string }>
|
|
30
|
-
|
|
31
|
-
/** Display label: full name → username → email. */
|
|
32
|
-
function labelFor(row: AdminUserRow): string {
|
|
33
|
-
const name = [row.given_name, row.family_name].filter(Boolean).join(' ')
|
|
34
|
-
return name || row.username || row.email
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Batch-resolve admin-user ids to display labels. Accepts the raw
|
|
39
|
-
* `createdBy` values straight off a page of shaped documents — nullish
|
|
40
|
-
* and duplicate entries are tolerated and deduplicated. Returns `{}`
|
|
41
|
-
* when there is nothing to resolve or no admin store is configured.
|
|
42
|
-
*/
|
|
43
|
-
export async function resolveActorLabels(
|
|
44
|
-
ids: Iterable<string | null | undefined>
|
|
45
|
-
): Promise<ActorLabelMap> {
|
|
46
|
-
const unique = [
|
|
47
|
-
...new Set([...ids].filter((id): id is string => typeof id === 'string' && id.length > 0)),
|
|
48
|
-
]
|
|
49
|
-
if (unique.length === 0) return {}
|
|
50
|
-
|
|
51
|
-
const store = bylineCore().adminStore
|
|
52
|
-
if (store == null) return {}
|
|
53
|
-
|
|
54
|
-
const rows = await store.adminUsers.getByIds(unique)
|
|
55
|
-
const map: ActorLabelMap = {}
|
|
56
|
-
for (const row of rows) {
|
|
57
|
-
map[row.id] = { label: labelFor(row) }
|
|
58
|
-
}
|
|
59
|
-
return map
|
|
60
|
-
}
|
|
9
|
+
export { type ActorLabelMap, resolveActorLabels } from '../actors.js'
|
|
@@ -11,19 +11,16 @@ import { createServerFn } from '@tanstack/react-start'
|
|
|
11
11
|
import { getAdminBylineClient, getAdminRequestContext } from '@byline/client/server'
|
|
12
12
|
import {
|
|
13
13
|
BylineError,
|
|
14
|
-
|
|
15
|
-
collectionAbilityKey,
|
|
14
|
+
documentAbilityKey,
|
|
16
15
|
ERR_NOT_FOUND,
|
|
17
16
|
ErrorCodes,
|
|
18
|
-
getCollectionAdminConfig,
|
|
19
|
-
getCollectionDefinition,
|
|
20
17
|
getCollectionSchemasForPath,
|
|
21
18
|
getLogger,
|
|
22
19
|
getServerConfig,
|
|
23
|
-
type PopulateSpec,
|
|
24
20
|
} from '@byline/core'
|
|
25
21
|
|
|
26
22
|
import { ensureCollection } from '../../integrations/api-utils.js'
|
|
23
|
+
import { resolveAdminDocumentRead } from '../admin-document-presentation.js'
|
|
27
24
|
import { serialise } from './utils'
|
|
28
25
|
|
|
29
26
|
// ---------------------------------------------------------------------------
|
|
@@ -54,49 +51,14 @@ const getDocumentFn = createServerFn({ method: 'GET' })
|
|
|
54
51
|
const client = getAdminBylineClient()
|
|
55
52
|
const handle = client.collection(path)
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// fields, so each target gets just its picker columns + useAsTitle
|
|
63
|
-
// and relation-summary tiles render on first paint without per-tile
|
|
64
|
-
// fetches.
|
|
65
|
-
const populateRequested = typeof depth === 'number' && depth > 0
|
|
66
|
-
const autoRelationsActive = !populateRequested && populateRelations === true
|
|
67
|
-
|
|
68
|
-
let populate: PopulateSpec | undefined
|
|
69
|
-
let resolvedDepth: number | undefined
|
|
70
|
-
if (populateRequested) {
|
|
71
|
-
populate = '*'
|
|
72
|
-
resolvedDepth = depth
|
|
73
|
-
} else if (autoRelationsActive) {
|
|
74
|
-
const populateMap = buildRelationSummaryPopulateMap(
|
|
75
|
-
config.definition.fields,
|
|
76
|
-
(targetPath) => ({
|
|
77
|
-
def: getCollectionDefinition(targetPath),
|
|
78
|
-
admin: getCollectionAdminConfig(targetPath),
|
|
79
|
-
})
|
|
80
|
-
)
|
|
81
|
-
if (Object.keys(populateMap).length > 0) {
|
|
82
|
-
populate = populateMap
|
|
83
|
-
resolvedDepth = 1
|
|
84
|
-
}
|
|
85
|
-
}
|
|
54
|
+
const { options: readOptions, populatedTree } = resolveAdminDocumentRead(config.definition, {
|
|
55
|
+
locale,
|
|
56
|
+
depth,
|
|
57
|
+
populateRelations,
|
|
58
|
+
})
|
|
86
59
|
|
|
87
60
|
const document = await handle.findById(id, {
|
|
88
|
-
|
|
89
|
-
populate,
|
|
90
|
-
depth: resolvedDepth,
|
|
91
|
-
status: 'any',
|
|
92
|
-
// Admin edit path: show the RAW per-locale values — untranslated localized
|
|
93
|
-
// fields stay empty (the signal to use "Copy to Locale"), never falling
|
|
94
|
-
// back to the default locale. Overrides the client's `'fallback'` default.
|
|
95
|
-
onMissingLocale: 'empty',
|
|
96
|
-
// Admin edit path: tolerate schema-mismatch warnings rather than
|
|
97
|
-
// hard-failing the load. Warnings (if any) come back on the document
|
|
98
|
-
// as `_restoreWarnings` and the edit form surfaces them via an Alert.
|
|
99
|
-
lenient: true,
|
|
61
|
+
...readOptions,
|
|
100
62
|
})
|
|
101
63
|
|
|
102
64
|
if (!document) {
|
|
@@ -114,7 +76,6 @@ const getDocumentFn = createServerFn({ method: 'GET' })
|
|
|
114
76
|
// don't match the raw relation-ref shape the schema expects —
|
|
115
77
|
// applies equally to the depth-based preview path and the
|
|
116
78
|
// admin-edit relation-summary path)
|
|
117
|
-
const populatedTree = populateRequested || autoRelationsActive
|
|
118
79
|
const parsed =
|
|
119
80
|
locale === 'all' || populatedTree
|
|
120
81
|
? (serialised as Record<string, any>)
|
|
@@ -177,8 +138,8 @@ const getDocumentFn = createServerFn({ method: 'GET' })
|
|
|
177
138
|
if (scheduledPublicationEnabled) {
|
|
178
139
|
const actor = (await getAdminRequestContext()).actor
|
|
179
140
|
canSchedulePublication =
|
|
180
|
-
actor?.hasAbility(
|
|
181
|
-
actor.hasAbility(
|
|
141
|
+
actor?.hasAbility(documentAbilityKey(config.definition, 'changeStatus')) === true &&
|
|
142
|
+
actor.hasAbility(documentAbilityKey(config.definition, 'publish'))
|
|
182
143
|
if (canSchedulePublication) {
|
|
183
144
|
scheduledPublish = await handle.getScheduledPublish(id)
|
|
184
145
|
}
|
|
@@ -66,7 +66,7 @@ export const reorderCollectionDocument = createServerFn({ method: 'POST' })
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const requestContext = await getAdminRequestContext()
|
|
69
|
-
assertActorCanPerform(requestContext,
|
|
69
|
+
assertActorCanPerform(requestContext, config.definition, 'update')
|
|
70
70
|
|
|
71
71
|
const serverConfig = getServerConfig()
|
|
72
72
|
const collectionId = config.collection.id
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type DocumentPublishScheduleState,
|
|
16
16
|
ERR_NOT_FOUND,
|
|
17
17
|
ERR_VALIDATION,
|
|
18
|
+
isSingleton,
|
|
18
19
|
type RecurringTaskHealth,
|
|
19
20
|
} from '@byline/core'
|
|
20
21
|
import { listDocumentPublishSchedules } from '@byline/core/services'
|
|
@@ -247,7 +248,12 @@ export const listScheduledPublications = createServerFn({ method: 'GET' })
|
|
|
247
248
|
return {
|
|
248
249
|
...serializeSchedule(schedule),
|
|
249
250
|
collectionPath: definition?.path ?? schedule.collectionId,
|
|
250
|
-
collectionLabel:
|
|
251
|
+
collectionLabel:
|
|
252
|
+
definition == null
|
|
253
|
+
? schedule.collectionId
|
|
254
|
+
: isSingleton(definition)
|
|
255
|
+
? definition.label
|
|
256
|
+
: definition.labels.plural,
|
|
251
257
|
documentPath,
|
|
252
258
|
lastAuthorizedByName:
|
|
253
259
|
schedule.lastAuthorizedBy == null
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
import { AdminAuth, createRequestContext } from '@byline/auth'
|
|
10
|
+
import {
|
|
11
|
+
defineServerConfig,
|
|
12
|
+
defineSingleton,
|
|
13
|
+
type IDbAdapter,
|
|
14
|
+
type IStorageProvider,
|
|
15
|
+
} from '@byline/core'
|
|
16
|
+
import { defineLogger } from '@byline/core/logger'
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
18
|
+
|
|
19
|
+
const mocks = vi.hoisted(() => ({
|
|
20
|
+
getAdminRequestContext: vi.fn(),
|
|
21
|
+
getCollectionRecord: vi.fn(),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('@tanstack/react-start', () => ({
|
|
25
|
+
createServerFn: () => {
|
|
26
|
+
let validate = (input: unknown) => input
|
|
27
|
+
const chain = {
|
|
28
|
+
validator(validator: (input: unknown) => unknown) {
|
|
29
|
+
validate = validator
|
|
30
|
+
return chain
|
|
31
|
+
},
|
|
32
|
+
handler(handler: (options: { data: any }) => Promise<unknown>) {
|
|
33
|
+
return async (options: { data: unknown }) => handler({ data: validate(options.data) })
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
return chain
|
|
37
|
+
},
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
vi.mock('@byline/client/server', () => ({
|
|
41
|
+
getAdminRequestContext: mocks.getAdminRequestContext,
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
vi.mock('@byline/core/image', () => ({
|
|
45
|
+
extractImageMeta: vi.fn(async () => ({ width: null, height: null, format: null })),
|
|
46
|
+
generateImageVariants: vi.fn(async () => []),
|
|
47
|
+
isBypassMimeType: vi.fn(() => false),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
vi.mock('../../integrations/byline-core.js', () => ({
|
|
51
|
+
bylineCore: () => ({ getCollectionRecord: mocks.getCollectionRecord }),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
import { ensureCollection, ensureDocumentResource } from '../../integrations/api-utils.js'
|
|
55
|
+
import { uploadField } from './upload.js'
|
|
56
|
+
|
|
57
|
+
const SERVER_CONFIG = Symbol.for('__byline_server_config__')
|
|
58
|
+
const BYLINE_LOGGER = Symbol.for('__byline_logger__')
|
|
59
|
+
const previousServer = (globalThis as Record<PropertyKey, unknown>)[SERVER_CONFIG]
|
|
60
|
+
const previousLogger = (globalThis as Record<PropertyKey, unknown>)[BYLINE_LOGGER]
|
|
61
|
+
|
|
62
|
+
const singleton = defineSingleton({
|
|
63
|
+
path: 'site-settings',
|
|
64
|
+
label: 'Site settings',
|
|
65
|
+
fields: [
|
|
66
|
+
{
|
|
67
|
+
name: 'hero',
|
|
68
|
+
label: 'Hero image',
|
|
69
|
+
type: 'file',
|
|
70
|
+
upload: { mimeTypes: ['text/plain'], requireSavedDocument: true },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const storageUpload = vi.fn(async () => ({
|
|
76
|
+
storageProvider: 'test',
|
|
77
|
+
storagePath: 'site-settings/hero.txt',
|
|
78
|
+
storageUrl: '/uploads/site-settings/hero.txt',
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
const storage: IStorageProvider = {
|
|
82
|
+
providerName: 'test',
|
|
83
|
+
upload: storageUpload,
|
|
84
|
+
delete: vi.fn(async () => {}),
|
|
85
|
+
getUrl: (path) => `/uploads/${path}`,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeAll(() => {
|
|
89
|
+
const silent = () => {}
|
|
90
|
+
defineLogger({
|
|
91
|
+
log: silent,
|
|
92
|
+
fatal: silent,
|
|
93
|
+
error: silent,
|
|
94
|
+
warn: silent,
|
|
95
|
+
info: silent,
|
|
96
|
+
debug: silent,
|
|
97
|
+
trace: silent,
|
|
98
|
+
silent,
|
|
99
|
+
})
|
|
100
|
+
mocks.getCollectionRecord.mockReturnValue({
|
|
101
|
+
collectionId: 'collection-settings',
|
|
102
|
+
version: 1,
|
|
103
|
+
schemaHash: 'singleton-hash',
|
|
104
|
+
})
|
|
105
|
+
mocks.getAdminRequestContext.mockResolvedValue(
|
|
106
|
+
createRequestContext({
|
|
107
|
+
actor: new AdminAuth({
|
|
108
|
+
id: 'settings-editor',
|
|
109
|
+
abilities: ['singletons.site-settings.update'],
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
defineServerConfig({
|
|
114
|
+
collections: [singleton],
|
|
115
|
+
db: {} as IDbAdapter,
|
|
116
|
+
storage,
|
|
117
|
+
i18n: {
|
|
118
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
119
|
+
content: { defaultLocale: 'en', locales: ['en'] },
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
afterAll(() => {
|
|
125
|
+
const globals = globalThis as Record<PropertyKey, unknown>
|
|
126
|
+
if (previousServer === undefined) delete globals[SERVER_CONFIG]
|
|
127
|
+
else globals[SERVER_CONFIG] = previousServer
|
|
128
|
+
if (previousLogger === undefined) delete globals[BYLINE_LOGGER]
|
|
129
|
+
else globals[BYLINE_LOGGER] = previousLogger
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('singleton field upload host transport', () => {
|
|
133
|
+
it('keeps collection routes closed while resolving the shared upload resource', async () => {
|
|
134
|
+
await expect(ensureCollection('site-settings')).resolves.toBeNull()
|
|
135
|
+
await expect(ensureDocumentResource('site-settings')).resolves.toMatchObject({
|
|
136
|
+
definition: { singleton: true, path: 'site-settings' },
|
|
137
|
+
collection: { id: 'collection-settings', version: 1 },
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('stores a singleton field without taking the collection create branch', async () => {
|
|
142
|
+
const formData = new FormData()
|
|
143
|
+
formData.set('field', 'hero')
|
|
144
|
+
formData.set('file', new File(['hero'], 'Hero.txt', { type: 'text/plain' }))
|
|
145
|
+
|
|
146
|
+
const result = await uploadField('site-settings', formData, false)
|
|
147
|
+
|
|
148
|
+
expect(mocks.getCollectionRecord).toHaveBeenCalledWith('site-settings')
|
|
149
|
+
expect(storageUpload).toHaveBeenCalledWith(
|
|
150
|
+
Buffer.from('hero'),
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
collection: 'site-settings',
|
|
153
|
+
filename: 'hero.txt',
|
|
154
|
+
mimeType: 'text/plain',
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
expect(result.documentId).toBeUndefined()
|
|
158
|
+
expect(result.storedFile).toMatchObject({
|
|
159
|
+
filename: 'hero.txt',
|
|
160
|
+
storageProvider: 'test',
|
|
161
|
+
storagePath: 'site-settings/hero.txt',
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -13,7 +13,7 @@ import { extractImageMeta, generateImageVariants, isBypassMimeType } from '@byli
|
|
|
13
13
|
import { getLogger, withLogContext } from '@byline/core/logger'
|
|
14
14
|
import { uploadField as coreUploadField } from '@byline/core/services'
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { ensureDocumentResource } from '../../integrations/api-utils.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Result of an upload through the host transport. The legacy top-level
|
|
@@ -156,11 +156,15 @@ export const uploadCollectionField = createServerFn({ method: 'POST' })
|
|
|
156
156
|
return withLogContext(
|
|
157
157
|
{ domain: 'api', module: 'upload', function: 'uploadCollectionField' },
|
|
158
158
|
async () => {
|
|
159
|
-
|
|
159
|
+
// Upload is the one field transport shared by both document-resource
|
|
160
|
+
// kinds. The core service selects the kind-aware ability and rejects
|
|
161
|
+
// singleton document creation before hooks or storage; the admin form
|
|
162
|
+
// always requests the field-only branch (`createDocument=false`).
|
|
163
|
+
const config = await ensureDocumentResource(collectionPath)
|
|
160
164
|
if (config == null) {
|
|
161
165
|
throw ERR_NOT_FOUND(
|
|
162
166
|
{
|
|
163
|
-
message: '
|
|
167
|
+
message: 'Document resource not found.',
|
|
164
168
|
details: { collectionPath },
|
|
165
169
|
},
|
|
166
170
|
uploadCollectionField
|
|
@@ -6,12 +6,4 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* Deep-serialise a value through JSON to convert Date objects to ISO strings.
|
|
11
|
-
* TanStack Start server functions pass return values directly during SSR
|
|
12
|
-
* (no automatic JSON round-trip), so Date instances from the DB driver would
|
|
13
|
-
* fail downstream Zod z.string() checks without this explicit step.
|
|
14
|
-
*/
|
|
15
|
-
export function serialise<T>(value: T): T {
|
|
16
|
-
return JSON.parse(JSON.stringify(value))
|
|
17
|
-
}
|
|
9
|
+
export { serialise } from '../serialise.js'
|
|
@@ -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
|
+
/**
|
|
10
|
+
* Deep-serialise a value through JSON to convert Date objects to ISO strings.
|
|
11
|
+
* TanStack Start server functions pass return values directly during SSR
|
|
12
|
+
* (no automatic JSON round-trip), so Date instances from the DB driver would
|
|
13
|
+
* fail downstream string checks without this explicit step.
|
|
14
|
+
*/
|
|
15
|
+
export function serialise<T>(value: T): T {
|
|
16
|
+
return JSON.parse(JSON.stringify(value))
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { getAdminBylineClient } from '@byline/client/server'
|
|
10
|
+
import { getCollectionDefinition } from '@byline/core'
|
|
11
|
+
|
|
12
|
+
import { resolveAdminDocumentRead } from './admin-document-presentation.js'
|
|
13
|
+
import { serialise } from './serialise.js'
|
|
14
|
+
|
|
15
|
+
export interface SingletonDocumentReadInput {
|
|
16
|
+
singleton: string
|
|
17
|
+
locale?: string
|
|
18
|
+
depth?: number
|
|
19
|
+
populateRelations?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the singleton document with the presentation controls shared by the
|
|
24
|
+
* editor route and the public server function. Route-only lifecycle metadata
|
|
25
|
+
* is deliberately composed one layer up, where host database and actor
|
|
26
|
+
* context are available without widening the singleton transport contract.
|
|
27
|
+
*/
|
|
28
|
+
export async function readSingletonDocument(data: SingletonDocumentReadInput) {
|
|
29
|
+
const handle = getAdminBylineClient().singleton(data.singleton)
|
|
30
|
+
const { options } = resolveAdminDocumentRead(getCollectionDefinition(data.singleton), data)
|
|
31
|
+
return serialise(await handle.get(options))
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
|
|
11
|
+
import { getAdminBylineClient } from '@byline/client/server'
|
|
12
|
+
|
|
13
|
+
import { serialise } from '../serialise.js'
|
|
14
|
+
|
|
15
|
+
export const cancelSingletonScheduledPublish = createServerFn({ method: 'POST' })
|
|
16
|
+
.validator((input: { singleton: string }) => input)
|
|
17
|
+
.handler(async ({ data }) =>
|
|
18
|
+
serialise(await getAdminBylineClient().singleton(data.singleton).cancelScheduledPublish())
|
|
19
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
|
|
11
|
+
import { getAdminBylineClient } from '@byline/client/server'
|
|
12
|
+
|
|
13
|
+
import { serialise } from '../serialise.js'
|
|
14
|
+
|
|
15
|
+
export const changeSingletonStatus = createServerFn({ method: 'POST' })
|
|
16
|
+
.validator((input: { singleton: string; status: string }) => input)
|
|
17
|
+
.handler(async ({ data }) =>
|
|
18
|
+
serialise(await getAdminBylineClient().singleton(data.singleton).changeStatus(data.status))
|
|
19
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
|
|
11
|
+
import { getAdminBylineClient } from '@byline/client/server'
|
|
12
|
+
|
|
13
|
+
import { serialise } from '../serialise.js'
|
|
14
|
+
|
|
15
|
+
export const confirmSingletonScheduledPublish = createServerFn({ method: 'POST' })
|
|
16
|
+
.validator((input: { singleton: string; expectedVersionId: string }) => input)
|
|
17
|
+
.handler(async ({ data }) =>
|
|
18
|
+
serialise(
|
|
19
|
+
await getAdminBylineClient()
|
|
20
|
+
.singleton(data.singleton)
|
|
21
|
+
.confirmScheduledPublish({ expectedVersionId: data.expectedVersionId })
|
|
22
|
+
)
|
|
23
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
10
|
+
|
|
11
|
+
import { getAdminBylineClient } from '@byline/client/server'
|
|
12
|
+
|
|
13
|
+
import { serialise } from '../serialise.js'
|
|
14
|
+
|
|
15
|
+
export const copySingletonToLocale = createServerFn({ method: 'POST' })
|
|
16
|
+
.validator(
|
|
17
|
+
(input: {
|
|
18
|
+
singleton: string
|
|
19
|
+
sourceLocale: string
|
|
20
|
+
targetLocale: string
|
|
21
|
+
overwrite?: boolean
|
|
22
|
+
}) => input
|
|
23
|
+
)
|
|
24
|
+
.handler(async ({ data }) =>
|
|
25
|
+
serialise(
|
|
26
|
+
await getAdminBylineClient().singleton(data.singleton).copyToLocale({
|
|
27
|
+
sourceLocale: data.sourceLocale,
|
|
28
|
+
targetLocale: data.targetLocale,
|
|
29
|
+
overwrite: data.overwrite,
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
)
|