@ossy/resources 1.12.2 → 3.0.1
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/package.json +16 -4
- package/src/AudioResource.jsx +2 -2
- package/src/CreateDirectory.jsx +2 -10
- package/src/CreateDirectory.page.jsx +1 -1
- package/src/CreateDocument.jsx +2 -2
- package/src/CreateDocument.page.jsx +1 -1
- package/src/Definition.js +3 -2
- package/src/DocumentEdit.jsx +10 -38
- package/src/DocumentView.jsx +11 -22
- package/src/GenericResourceCard.jsx +25 -7
- package/src/GenericResourceDetail.jsx +6 -60
- package/src/GenericResourceForm.jsx +171 -37
- package/src/ImageResource.jsx +1 -1
- package/src/PDFResource.jsx +1 -1
- package/src/PlatformFileField.jsx +239 -0
- package/src/ResourceContentPage.jsx +5 -5
- package/src/ResourceDescription.jsx +1 -1
- package/src/ResourceDetails.jsx +3 -4
- package/src/ResourceDialogMove.jsx +3 -3
- package/src/ResourceFactory.jsx +11 -64
- package/src/ResourceGenericView.jsx +1 -1
- package/src/ResourceList.jsx +89 -106
- package/src/ResourcePanel.jsx +40 -57
- package/src/ResourceTags.jsx +1 -1
- package/src/ResourcesPage.jsx +26 -35
- package/src/SchemaDetailView.jsx +97 -0
- package/src/SchemaFormDefault.jsx +86 -0
- package/src/SchemaPresenter.jsx +207 -0
- package/src/Upload.jsx +6 -25
- package/src/Upload.page.jsx +1 -1
- package/src/UploadResources.jsx +2 -2
- package/src/VideoResource.jsx +1 -1
- package/src/access-filter.spec.js +1 -1
- package/src/create-page-view.action.js +1 -1
- package/src/create-page-view.task.js +10 -9
- package/src/create.action.js +1 -1
- package/src/create.task.js +27 -29
- package/src/delete.action.js +1 -1
- package/src/delete.task.js +9 -13
- package/src/en.translations.json +25 -24
- package/src/get-page-view-stats.action.js +1 -1
- package/src/get-page-view-stats.task.js +3 -4
- package/src/get-resource-alias.api.js +8 -0
- package/src/get-resource-info-alias.api.js +8 -0
- package/src/get-resource-info.api.js +36 -0
- package/src/get-resource-variant.api.js +8 -0
- package/src/get-resource.api.js +51 -0
- package/src/get.action.js +1 -1
- package/src/get.task.js +6 -6
- package/src/index.js +10 -0
- package/src/list.action.js +1 -1
- package/src/list.task.js +1 -1
- package/src/platform-file-field.component.jsx +5 -0
- package/src/platform-image-field.component.jsx +5 -0
- package/src/resource-create.page.jsx +5 -5
- package/src/resource-detail.page.jsx +1 -1
- package/src/resource-read.helpers.js +151 -0
- package/src/resource-stream.helpers.js +20 -0
- package/src/resource-stream.js +108 -0
- package/src/resource.helpers.js +9 -9
- package/src/resources.attach-media-urls.js +9 -3
- package/src/resources.events.js +42 -81
- package/src/{resources.spec.js → resources.integration.spec.js} +19 -19
- package/src/resources.queries.js +5 -2
- package/src/schema-detail.component.jsx +24 -0
- package/src/schema-form-default.component.jsx +14 -0
- package/src/schemaFormSlots.js +6 -0
- package/src/schemaViewSlots.js +6 -0
- package/src/search.action.js +1 -1
- package/src/search.task.js +1 -1
- package/src/server.js +2 -1
- package/src/sv.translations.json +25 -24
- package/src/update-access.action.js +1 -1
- package/src/update-access.task.js +9 -8
- package/src/update-content.action.js +1 -1
- package/src/update-content.task.js +25 -27
- package/src/update-location.action.js +1 -1
- package/src/update-location.task.js +9 -8
- package/src/update-name.action.js +1 -1
- package/src/update-name.task.js +9 -8
- package/src/upload-named-version.action.js +1 -1
- package/src/upload-named-version.task.js +16 -22
- package/src/useActivePath.jsx +3 -8
- package/src/useDocumentValidator.js +16 -0
- package/src/useSchemaEngine.js +9 -0
- package/src/useSchemas.js +29 -0
- package/src/useSelectedResourceId.jsx +36 -0
- package/src/ResourceContentPage.stories.jsx +0 -19
- package/src/resource.aggregate.js +0 -86
package/src/en.translations.json
CHANGED
|
@@ -7,30 +7,31 @@
|
|
|
7
7
|
"resources.form.selectType": "Select a resource type to continue.",
|
|
8
8
|
"resources.form.createFailed": "Create failed",
|
|
9
9
|
"resources.form.createResource": "Create resource",
|
|
10
|
-
"resources
|
|
11
|
-
"resources/create.
|
|
12
|
-
"resources/
|
|
13
|
-
"resources/list.
|
|
14
|
-
"resources/
|
|
15
|
-
"resources/get.
|
|
16
|
-
"resources/
|
|
17
|
-
"resources/search.
|
|
18
|
-
"resources/
|
|
19
|
-
"resources/delete.
|
|
20
|
-
"resources/
|
|
21
|
-
"resources/update-name.
|
|
22
|
-
"resources/update-
|
|
23
|
-
"resources/update-content.
|
|
24
|
-
"resources/update-
|
|
25
|
-
"resources/update-location.
|
|
26
|
-
"resources/update-
|
|
27
|
-
"resources/update-access.
|
|
28
|
-
"resources/
|
|
29
|
-
"resources/upload-named-version.
|
|
30
|
-
"resources/
|
|
31
|
-
"resources/create-page-view.
|
|
32
|
-
"resources/
|
|
33
|
-
"resources/get-page-view-stats.
|
|
10
|
+
"resources.form.typeRemoved": "This document type has been removed and can't be edited.",
|
|
11
|
+
"@ossy/resources/actions/create.label": "Create resource",
|
|
12
|
+
"@ossy/resources/actions/create.description": "Create a new resource in the workspace",
|
|
13
|
+
"@ossy/resources/actions/list.label": "List resources",
|
|
14
|
+
"@ossy/resources/actions/list.description": "List resources in a workspace location",
|
|
15
|
+
"@ossy/resources/actions/get.label": "Get resource",
|
|
16
|
+
"@ossy/resources/actions/get.description": "Fetch a single resource by id",
|
|
17
|
+
"@ossy/resources/actions/search.label": "Search resources",
|
|
18
|
+
"@ossy/resources/actions/search.description": "Search resources in a workspace",
|
|
19
|
+
"@ossy/resources/actions/delete.label": "Delete resource",
|
|
20
|
+
"@ossy/resources/actions/delete.description": "Delete a resource from the workspace",
|
|
21
|
+
"@ossy/resources/actions/update-name.label": "Update resource name",
|
|
22
|
+
"@ossy/resources/actions/update-name.description": "Rename a workspace resource",
|
|
23
|
+
"@ossy/resources/actions/update-content.label": "Update resource content",
|
|
24
|
+
"@ossy/resources/actions/update-content.description": "Update the content of a workspace resource",
|
|
25
|
+
"@ossy/resources/actions/update-location.label": "Update resource location",
|
|
26
|
+
"@ossy/resources/actions/update-location.description": "Move a resource to a new location",
|
|
27
|
+
"@ossy/resources/actions/update-access.label": "Update resource access",
|
|
28
|
+
"@ossy/resources/actions/update-access.description": "Change access settings for a resource",
|
|
29
|
+
"@ossy/resources/actions/upload-named-version.label": "Upload named version",
|
|
30
|
+
"@ossy/resources/actions/upload-named-version.description": "Upload a named version of a resource file",
|
|
31
|
+
"@ossy/resources/actions/create-page-view.label": "Create page view",
|
|
32
|
+
"@ossy/resources/actions/create-page-view.description": "Record a page view analytics event",
|
|
33
|
+
"@ossy/resources/actions/get-page-view-stats.label": "Get page view stats",
|
|
34
|
+
"@ossy/resources/actions/get-page-view-stats.description": "Fetch aggregated page view statistics",
|
|
34
35
|
"resources/create/generic.documentTitle": "Create resource",
|
|
35
36
|
"create-directory.documentTitle": "Create directory",
|
|
36
37
|
"create-document.documentTitle": "Create document",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'resources/get-page-view-stats', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/resources/actions/get-page-view-stats', access: 'workspace' }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Aggregate } from '@ossy/event-store'
|
|
2
2
|
import { Workspace } from '@ossy/workspaces/server'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'resources/get-page-view-stats' }
|
|
4
|
+
export const metadata = { id: '@ossy/resources/tasks/get-page-view-stats' }
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const PAGE_VIEW_SCHEMA_ID = '@ossy/web/schema/page-view'
|
|
7
7
|
const PAGE_VIEW_LOCATION = '/@ossy/analytics/page-views/'
|
|
8
8
|
|
|
9
9
|
function parseMillis(input, fallback) {
|
|
@@ -26,10 +26,9 @@ export async function run({ payload, req }) {
|
|
|
26
26
|
const topLimit = Math.min(Math.max(Number(payload?.limit ?? req?.query?.limit ?? 10), 1), 50)
|
|
27
27
|
|
|
28
28
|
const baseMatch = {
|
|
29
|
-
type:
|
|
29
|
+
type: PAGE_VIEW_SCHEMA_ID,
|
|
30
30
|
'state.status': { $ne: 'removed' },
|
|
31
31
|
'state.belongsTo': workspace.id,
|
|
32
|
-
'state.type': PAGE_VIEW_TEMPLATE_ID,
|
|
33
32
|
'state.location': PAGE_VIEW_LOCATION,
|
|
34
33
|
'state.content.eventAt': { $gte: from, $lte: to },
|
|
35
34
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadResourceForRead,
|
|
3
|
+
parseResourceIdFromPath,
|
|
4
|
+
resourceInfoPayload,
|
|
5
|
+
} from './resource-read.helpers.js'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: 'resources/read-info',
|
|
9
|
+
path: '/r/:resourceId/info',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function handleInfo(req, res) {
|
|
13
|
+
const pathname = req.path || req.url?.split('?')[0] || '/'
|
|
14
|
+
const resourceId = parseResourceIdFromPath(pathname, 'info')
|
|
15
|
+
if (!resourceId) {
|
|
16
|
+
res.status(400).json({ error: 'Invalid resource path' })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const resource = await loadResourceForRead(resourceId, req)
|
|
22
|
+
res.status(200).json(resourceInfoPayload(resource))
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(err?.status ?? 500).json({ error: err?.message ?? 'Internal error' })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default async function handle(req, res) {
|
|
29
|
+
if (req.method !== 'GET') {
|
|
30
|
+
res.setHeader('Allow', 'GET')
|
|
31
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await handleInfo(req, res)
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadResourceForRead,
|
|
3
|
+
parseResourceReadTarget,
|
|
4
|
+
readResourceFileBytes,
|
|
5
|
+
} from './resource-read.helpers.js'
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: 'resources/read',
|
|
9
|
+
path: '/r/:resourceId',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function handleRead(req, res) {
|
|
13
|
+
const pathname = req.path || req.url?.split('?')[0] || '/'
|
|
14
|
+
const target = parseResourceReadTarget(pathname)
|
|
15
|
+
if (!target?.resourceId) {
|
|
16
|
+
res.status(400).json({ error: 'Invalid resource path' })
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const resource = await loadResourceForRead(target.resourceId, req)
|
|
22
|
+
const filePayload = await readResourceFileBytes(resource, { variant: target.variant })
|
|
23
|
+
|
|
24
|
+
if (filePayload?.buffer) {
|
|
25
|
+
if (filePayload.contentType) {
|
|
26
|
+
res.setHeader('Content-Type', filePayload.contentType)
|
|
27
|
+
}
|
|
28
|
+
res.status(200).send(filePayload.buffer)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (resource.content && typeof resource.content === 'object' && !resource.content.Key) {
|
|
33
|
+
res.status(200).json(resource.content)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.status(404).json({ error: 'Resource body not found' })
|
|
38
|
+
} catch (err) {
|
|
39
|
+
res.status(err?.status ?? 500).json({ error: err?.message ?? 'Internal error' })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default async function handle(req, res) {
|
|
44
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
45
|
+
res.setHeader('Allow', 'GET, HEAD')
|
|
46
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await handleRead(req, res)
|
|
51
|
+
}
|
package/src/get.action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'resources/get', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/resources/actions/get', access: 'workspace' }
|
package/src/get.task.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Resource } from './resource.aggregate.js'
|
|
1
|
+
import { viewResource } from './resource-stream.helpers.js'
|
|
3
2
|
import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
|
|
3
|
+
import { resolveResourceId } from './resource-read.helpers.js'
|
|
4
4
|
|
|
5
|
-
export const metadata = { id: 'resources/get' }
|
|
5
|
+
export const metadata = { id: '@ossy/resources/tasks/get' }
|
|
6
6
|
|
|
7
|
-
export async function run({ payload, req }) {
|
|
8
|
-
const resourceId = payload
|
|
7
|
+
export async function run ({ payload, req }) {
|
|
8
|
+
const resourceId = resolveResourceId({ payload, req })
|
|
9
9
|
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
10
10
|
|
|
11
|
-
const resource = await
|
|
11
|
+
const resource = await viewResource(resourceId)
|
|
12
12
|
if (!resource?.id) throw Object.assign(new Error('Resource not found'), { status: 404 })
|
|
13
13
|
return withResourceMedia(resource, mediaContextFromRun({ payload, req }))
|
|
14
14
|
}
|
package/src/index.js
CHANGED
|
@@ -32,9 +32,19 @@ export * from './ResourcePage.jsx'
|
|
|
32
32
|
export * from './ResourcePanel.jsx'
|
|
33
33
|
export * from './ResourceTags.jsx'
|
|
34
34
|
export * from './ResourcesPage.jsx'
|
|
35
|
+
export * from './PlatformFileField.jsx'
|
|
35
36
|
export * from './Upload.jsx'
|
|
36
37
|
export * from './UploadResources.jsx'
|
|
37
38
|
export * from './VideoResource.jsx'
|
|
38
39
|
export * from './useActivePath.jsx'
|
|
40
|
+
export * from './useSelectedResourceId.jsx'
|
|
41
|
+
export * from './SchemaPresenter.jsx'
|
|
42
|
+
export * from './SchemaDetailView.jsx'
|
|
43
|
+
export * from './SchemaFormDefault.jsx'
|
|
44
|
+
export { DEFAULT_SCHEMA_VIEW_SLOTS } from './schemaViewSlots.js'
|
|
45
|
+
export { DEFAULT_SCHEMA_FORM_SLOTS } from './schemaFormSlots.js'
|
|
46
|
+
export * from './useSchemas.js'
|
|
47
|
+
export * from './useSchemaEngine.js'
|
|
48
|
+
export * from './useDocumentValidator.js'
|
|
39
49
|
export * from './useForm.js'
|
|
40
50
|
export * from './resource.helpers.js'
|
package/src/list.action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'resources/list', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/resources/actions/list', access: 'workspace' }
|
package/src/list.task.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ResourcesQueries } from './resources.queries.js'
|
|
2
2
|
import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
|
|
3
3
|
|
|
4
|
-
export const metadata = { id: 'resources/list' }
|
|
4
|
+
export const metadata = { id: '@ossy/resources/tasks/list' }
|
|
5
5
|
|
|
6
6
|
export async function run({ payload, req }) {
|
|
7
7
|
const query = { ...(payload?.query ?? {}) }
|
|
@@ -8,8 +8,8 @@ import { GenericResourceForm } from './GenericResourceForm.jsx'
|
|
|
8
8
|
export const metadata = {
|
|
9
9
|
id: 'resources/create/generic',
|
|
10
10
|
path: {
|
|
11
|
-
en: '/resources/create/:
|
|
12
|
-
sv: '/resources/skapa/:
|
|
11
|
+
en: '/resources/create/:schemaId',
|
|
12
|
+
sv: '/resources/skapa/:schemaId',
|
|
13
13
|
},
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ export const metadata = {
|
|
|
17
17
|
export default function GenericResourceCreatePage () {
|
|
18
18
|
const router = useRouter()
|
|
19
19
|
const { t } = useLocale()
|
|
20
|
-
const
|
|
20
|
+
const schemaId = decodeURIComponent(router.params.schemaId || '')
|
|
21
21
|
const location = router.searchParams.location
|
|
22
22
|
const sdk = useSdk()
|
|
23
23
|
const [error, setError] = useState()
|
|
@@ -35,9 +35,9 @@ export default function GenericResourceCreatePage () {
|
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
38
|
-
<View
|
|
38
|
+
<View data-region="content" surface="primary" roundness="m" inset="m">
|
|
39
39
|
<GenericResourceForm
|
|
40
|
-
|
|
40
|
+
schemaId={schemaId}
|
|
41
41
|
mode="create"
|
|
42
42
|
onSubmit={onSubmit}
|
|
43
43
|
onCancel={onCancel}
|
|
@@ -17,7 +17,7 @@ export default function ResourceDetailPage () {
|
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
20
|
-
<View
|
|
20
|
+
<View data-region="content" surface="primary" roundness="m" inset="m">
|
|
21
21
|
<GenericResourceDetail resourceId={resourceId} />
|
|
22
22
|
</View>
|
|
23
23
|
</View>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { StorageClient } from '@ossy/platform'
|
|
3
|
+
import { derivativeObjectKey } from '@ossy/platform/storage-keys'
|
|
4
|
+
import { Workspace } from '@ossy/workspaces/server'
|
|
5
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
6
|
+
import { viewResource } from './resource-stream.helpers.js'
|
|
7
|
+
|
|
8
|
+
export function resolveResourceId({ payload, req } = {}) {
|
|
9
|
+
return payload?.resourceId ?? payload?.id ?? req?.params?.resourceId ?? req?.params?.id
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse resource read target from `/r/{id}` or `/r/{id}/{variant}`.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} pathname
|
|
16
|
+
* @returns {{ resourceId: string, variant: string | null } | null}
|
|
17
|
+
*/
|
|
18
|
+
export function parseResourceReadTarget(pathname) {
|
|
19
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
20
|
+
if (parts.length < 2) return null
|
|
21
|
+
if (parts[0] !== 'r' && parts[0] !== 'resources') return null
|
|
22
|
+
if (parts.length === 2) return { resourceId: parts[1], variant: null }
|
|
23
|
+
if (parts.length === 3 && parts[2] !== 'info') {
|
|
24
|
+
return { resourceId: parts[1], variant: parts[2] }
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse `resourceId` from a resource read URL path.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} pathname
|
|
33
|
+
* @param {'content' | 'info'} mode
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
export function parseResourceIdFromPath(pathname, mode = 'content') {
|
|
37
|
+
if (mode === 'info') {
|
|
38
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
39
|
+
if (parts.length === 3 && (parts[0] === 'r' || parts[0] === 'resources') && parts[2] === 'info') {
|
|
40
|
+
return parts[1]
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parseResourceReadTarget(pathname)?.resourceId ?? null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function assertResourceAccess(resource, req) {
|
|
49
|
+
const access = resource.access || 'restricted'
|
|
50
|
+
if (access === 'public') return
|
|
51
|
+
|
|
52
|
+
const userId = req?.userId
|
|
53
|
+
const workspaceId = req?.workspaceId
|
|
54
|
+
|
|
55
|
+
if (!workspaceId) {
|
|
56
|
+
throw Object.assign(new Error('Forbidden'), { status: 403 })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (resource.belongsTo !== workspaceId) {
|
|
60
|
+
throw Object.assign(new Error('Forbidden'), { status: 403 })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!userId || userId === 'anonymous') {
|
|
64
|
+
throw Object.assign(new Error('Unauthorized'), { status: 401 })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
68
|
+
if (!workspace?.users?.includes(userId)) {
|
|
69
|
+
throw Object.assign(new Error('Forbidden'), { status: 403 })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load resource view with media URLs (same auth as get action).
|
|
75
|
+
*
|
|
76
|
+
* @param {string} resourceId
|
|
77
|
+
* @param {import('express').Request} req
|
|
78
|
+
*/
|
|
79
|
+
export async function loadResourceForRead(resourceId, req) {
|
|
80
|
+
if (!resourceId) {
|
|
81
|
+
throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resource = await viewResource(resourceId)
|
|
85
|
+
if (!resource?.id) {
|
|
86
|
+
throw Object.assign(new Error('Resource not found'), { status: 404 })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await assertResourceAccess(resource, req)
|
|
90
|
+
|
|
91
|
+
return attachResourceMediaUrls(resource, {
|
|
92
|
+
userId: req?.userId,
|
|
93
|
+
workspaceId: req?.workspaceId,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {object} resource
|
|
99
|
+
* @returns {Promise<{ buffer: Buffer, contentType?: string } | null>}
|
|
100
|
+
*/
|
|
101
|
+
export async function readResourceFileBytes(resource, { variant } = {}) {
|
|
102
|
+
let key = resource?.content?.Key
|
|
103
|
+
if (variant) {
|
|
104
|
+
key = derivativeObjectKey(resource.id, variant)
|
|
105
|
+
}
|
|
106
|
+
if (!key) return null
|
|
107
|
+
|
|
108
|
+
if (typeof StorageClient.load === 'function') {
|
|
109
|
+
const { buffer, exists } = await StorageClient.load(key)
|
|
110
|
+
if (!exists || !buffer) return null
|
|
111
|
+
return {
|
|
112
|
+
buffer,
|
|
113
|
+
contentType: resource.content?.ContentType,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const url = resource.content?.src
|
|
118
|
+
if (!url) return null
|
|
119
|
+
const response = await fetch(url)
|
|
120
|
+
if (!response.ok) return null
|
|
121
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
122
|
+
return {
|
|
123
|
+
buffer: Buffer.from(arrayBuffer),
|
|
124
|
+
contentType: resource.content?.ContentType || response.headers.get('content-type') || undefined,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Metadata-only shape for `/info` responses (no streamed file body).
|
|
130
|
+
*
|
|
131
|
+
* @param {object} resource
|
|
132
|
+
*/
|
|
133
|
+
export function resourceInfoPayload(resource) {
|
|
134
|
+
const { content, ...rest } = resource
|
|
135
|
+
const infoContent = content ? { ...content } : {}
|
|
136
|
+
delete infoContent.Key
|
|
137
|
+
delete infoContent.src
|
|
138
|
+
if (infoContent.sizes) {
|
|
139
|
+
infoContent.sizes = Object.fromEntries(
|
|
140
|
+
Object.keys(infoContent.sizes).map(name => [name, { configured: true }]),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
id: rest.id,
|
|
145
|
+
name: rest.name,
|
|
146
|
+
type: rest.type,
|
|
147
|
+
access: rest.access,
|
|
148
|
+
location: rest.location,
|
|
149
|
+
content: infoContent,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ResourceStream } from './resource-stream.js'
|
|
2
|
+
|
|
3
|
+
export async function viewResource (resourceId) {
|
|
4
|
+
const stream = await ResourceStream.Of(resourceId)
|
|
5
|
+
return ResourceStream.View()(stream)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function commitResource (event) {
|
|
9
|
+
const stream = await ResourceStream.Of(event)
|
|
10
|
+
await ResourceStream.Add(event)(stream)
|
|
11
|
+
await ResourceStream.Save()(stream)
|
|
12
|
+
return ResourceStream.View()(stream)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function mutateResource (resourceId, event) {
|
|
16
|
+
const stream = await ResourceStream.Of(resourceId)
|
|
17
|
+
await ResourceStream.Add(event)(stream)
|
|
18
|
+
await ResourceStream.Save()(stream)
|
|
19
|
+
return ResourceStream.View()(stream)
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { EventStore, Mongo, withMongoReconnect } from '@ossy/event-store'
|
|
3
|
+
import { createDefaultReducer } from '@ossy/fold'
|
|
4
|
+
import { createLogger } from '@ossy/observability'
|
|
5
|
+
|
|
6
|
+
const log = createLogger('@ossy/resources')
|
|
7
|
+
|
|
8
|
+
const reduce = createDefaultReducer()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ADR 0008 resource stream — keyed by `resourceId`, events use `{ type, event, version, payload }`.
|
|
12
|
+
*/
|
|
13
|
+
export class ResourceStream {
|
|
14
|
+
|
|
15
|
+
static get Collection () {
|
|
16
|
+
return Mongo.db.collection('aggregates')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string | object} identifier resourceId or a Created event partial
|
|
21
|
+
*/
|
|
22
|
+
static Of (identifier) {
|
|
23
|
+
if (typeof identifier === 'object' && identifier !== null) {
|
|
24
|
+
const resourceId = identifier.resourceId ?? nanoid()
|
|
25
|
+
return Promise.resolve(new ResourceStream(resourceId, [], null))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ResourceStream.Find(identifier)
|
|
29
|
+
.then(snapshot =>
|
|
30
|
+
EventStore.GetResourceStream({ resourceId: identifier, fromVersion: snapshot?.version ?? 0 })
|
|
31
|
+
.then(events => new ResourceStream(identifier, events, snapshot)),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static Find (resourceId) {
|
|
36
|
+
return withMongoReconnect(() => ResourceStream.Collection.findOne({ id: resourceId }))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} event partial resource event (without id, created, version)
|
|
41
|
+
*/
|
|
42
|
+
static Add (event) {
|
|
43
|
+
return stream => {
|
|
44
|
+
const version = stream.version + 1
|
|
45
|
+
const saved = {
|
|
46
|
+
...event,
|
|
47
|
+
type: event.type ?? stream.events[0]?.type ?? stream.state?.type,
|
|
48
|
+
id: nanoid(),
|
|
49
|
+
created: Date.now(),
|
|
50
|
+
resourceId: stream.id,
|
|
51
|
+
version,
|
|
52
|
+
}
|
|
53
|
+
return EventStore.AppendResourceEvent(saved)
|
|
54
|
+
.then(() => {
|
|
55
|
+
stream.version = version
|
|
56
|
+
stream.events = [...stream.events, saved]
|
|
57
|
+
return stream
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static Save () {
|
|
63
|
+
return stream => {
|
|
64
|
+
if (!stream.events?.length) return Promise.resolve()
|
|
65
|
+
|
|
66
|
+
const latestVersion = stream.events[stream.events.length - 1]?.version ?? stream.version
|
|
67
|
+
const state = reduce(stream.events, stream.state)
|
|
68
|
+
const schemaId = state.type ?? stream.events[0]?.type
|
|
69
|
+
|
|
70
|
+
return withMongoReconnect(() =>
|
|
71
|
+
ResourceStream.Collection.updateOne(
|
|
72
|
+
{ id: stream.id },
|
|
73
|
+
{
|
|
74
|
+
$set: {
|
|
75
|
+
id: stream.id,
|
|
76
|
+
version: latestVersion,
|
|
77
|
+
type: schemaId,
|
|
78
|
+
state,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{ upsert: true },
|
|
82
|
+
),
|
|
83
|
+
).catch(err => {
|
|
84
|
+
log.error(`[ResourceStream] save failed for ${stream.id}`, undefined, err)
|
|
85
|
+
throw err
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static View () {
|
|
91
|
+
return stream => reduce(stream.events, stream.state)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} id resourceId
|
|
96
|
+
* @param {object[]} events
|
|
97
|
+
* @param {object} [snapshot]
|
|
98
|
+
*/
|
|
99
|
+
constructor (id, events, snapshot) {
|
|
100
|
+
this.id = id
|
|
101
|
+
this.version = snapshot?.version ?? 0
|
|
102
|
+
this.state = snapshot?.state
|
|
103
|
+
this.events = events ?? []
|
|
104
|
+
this.View = reduce
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { ResourceStream as Aggregate }
|
package/src/resource.helpers.js
CHANGED
|
@@ -48,36 +48,36 @@ export async function uploadFile(sdk, uploadLocation, file) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function removeResource(sdk, id, location) {
|
|
51
|
-
await sdk.invoke(DeleteResource, { id })
|
|
51
|
+
await sdk.invoke(DeleteResource, { resourceId: id })
|
|
52
52
|
if (location) invalidateLocation(sdk, location)
|
|
53
|
-
sdk.invalidate(cacheKey({ id: 'resources/get' }, { id }))
|
|
53
|
+
sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: id }))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export async function updateResourceContent(sdk, id, content) {
|
|
57
|
-
const resource = await sdk.invoke(UpdateResourceContent, { id, content })
|
|
57
|
+
const resource = await sdk.invoke(UpdateResourceContent, { resourceId: id, content })
|
|
58
58
|
invalidateLocation(sdk, resource.location)
|
|
59
|
-
sdk.invalidate(cacheKey({ id: 'resources/get' }, { id }))
|
|
59
|
+
sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: id }))
|
|
60
60
|
return resource
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export async function moveResource(sdk, id, target, sourceLocation) {
|
|
64
|
-
const resource = await sdk.invoke(UpdateResourceLocation, { id, target })
|
|
64
|
+
const resource = await sdk.invoke(UpdateResourceLocation, { resourceId: id, target })
|
|
65
65
|
invalidateLocation(sdk, resource.location)
|
|
66
66
|
if (sourceLocation) invalidateLocation(sdk, sourceLocation)
|
|
67
|
-
sdk.invalidate(cacheKey({ id: 'resources/get' }, { id }))
|
|
67
|
+
sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: id }))
|
|
68
68
|
return resource
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export async function renameResource(sdk, id, name) {
|
|
72
|
-
const resource = await sdk.invoke(UpdateResourceName, { id, name })
|
|
72
|
+
const resource = await sdk.invoke(UpdateResourceName, { resourceId: id, name })
|
|
73
73
|
invalidateLocation(sdk, resource.location)
|
|
74
|
-
sdk.invalidate(cacheKey({ id: 'resources/get' }, { id }))
|
|
74
|
+
sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: id }))
|
|
75
75
|
return resource
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export async function updateResourceAccess(sdk, params) {
|
|
79
79
|
const resource = await sdk.invoke(UpdateResourceAccess, params)
|
|
80
80
|
invalidateLocation(sdk, resource.location)
|
|
81
|
-
sdk.invalidate(cacheKey({ id: 'resources/get' }, {
|
|
81
|
+
sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: params.resourceId ?? params.id }))
|
|
82
82
|
return resource
|
|
83
83
|
}
|