@ossy/resources 1.12.2 → 1.12.3
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 +11 -3
- package/src/AudioResource.jsx +1 -1
- package/src/CreateDirectory.jsx +2 -10
- package/src/CreateDocument.jsx +2 -2
- package/src/Definition.js +2 -1
- package/src/DocumentEdit.jsx +10 -38
- package/src/DocumentView.jsx +3 -4
- package/src/GenericResourceCard.jsx +25 -7
- package/src/GenericResourceDetail.jsx +6 -60
- package/src/GenericResourceForm.jsx +66 -31
- package/src/ImageResource.jsx +1 -1
- package/src/PDFResource.jsx +1 -1
- package/src/PlatformFileField.jsx +239 -0
- package/src/ResourceContentPage.jsx +3 -3
- package/src/ResourceDescription.jsx +1 -1
- package/src/ResourceDetails.jsx +3 -4
- package/src/ResourceDialogMove.jsx +2 -2
- 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/SchemaPresenter.jsx +215 -0
- package/src/Upload.jsx +6 -25
- 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 +28 -29
- package/src/delete.action.js +1 -1
- package/src/delete.task.js +10 -13
- package/src/en.translations.json +24 -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 +6 -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 +4 -4
- 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/search.action.js +1 -1
- package/src/search.task.js +1 -1
- package/src/server.js +2 -1
- package/src/sv.translations.json +24 -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/create.task.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { is } from 'ramda'
|
|
2
2
|
import { nanoid } from 'nanoid'
|
|
3
3
|
import { Aggregate } from '@ossy/event-store'
|
|
4
|
-
import { Resource } from './resource.aggregate.js'
|
|
5
|
-
import { ResourcesEvents } from './resources.events.js'
|
|
6
4
|
import { Workspace } from '@ossy/workspaces/server'
|
|
7
|
-
import {
|
|
5
|
+
import { schemaForWorkspace } from '@ossy/platform'
|
|
6
|
+
import { PlatformSchema } from '@ossy/platform'
|
|
7
|
+
import { originalObjectKey } from '@ossy/platform/storage-keys'
|
|
8
|
+
import { commitResource } from './resource-stream.helpers.js'
|
|
9
|
+
import { ResourcesEvents } from './resources.events.js'
|
|
8
10
|
import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
|
|
9
11
|
|
|
10
|
-
export const metadata = { id: 'resources/create' }
|
|
11
|
-
|
|
12
|
-
const NativeResourceTypes = { DIRECTORY: 'directory' }
|
|
12
|
+
export const metadata = { id: '@ossy/resources/tasks/create' }
|
|
13
13
|
|
|
14
|
-
export async function run({ payload, integrations, req }) {
|
|
14
|
+
export async function run ({ payload, integrations, req }) {
|
|
15
15
|
const createdBy = payload?.userId ?? req?.userId
|
|
16
16
|
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
17
17
|
|
|
@@ -30,56 +30,55 @@ export async function run({ payload, integrations, req }) {
|
|
|
30
30
|
location = location.endsWith('/') ? location : location + '/'
|
|
31
31
|
location = location.startsWith('/') ? location : '/' + location
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
...systemTemplates,
|
|
36
|
-
...(workspace.resourceTemplates || []).filter(t => !systemTemplates.find(s => s.id === t.id)),
|
|
37
|
-
]
|
|
38
|
-
const isResourceTemplateType = allTemplates.map(t => t.id).includes(type)
|
|
33
|
+
const engine = schemaForWorkspace(workspace.schemas)
|
|
34
|
+
const isDocumentSchema = engine.has(type)
|
|
39
35
|
|
|
40
|
-
if (type ===
|
|
36
|
+
if (type === 'directory' || type === PlatformSchema.directory) {
|
|
41
37
|
const event = ResourcesEvents.Created({
|
|
42
|
-
|
|
38
|
+
schemaId: PlatformSchema.directory,
|
|
43
39
|
createdBy,
|
|
44
40
|
belongsTo: workspace.id,
|
|
45
41
|
location,
|
|
46
42
|
name,
|
|
47
43
|
access: 'workspace',
|
|
48
44
|
})
|
|
49
|
-
return
|
|
45
|
+
return commitResource(event)
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
if (
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
56
|
-
|
|
48
|
+
if (isDocumentSchema) {
|
|
49
|
+
const schema = engine.resolve(type)
|
|
50
|
+
const result = engine.validate(schema, content)
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
const first = result.errors[0]
|
|
53
|
+
throw Object.assign(new Error(first.message), { status: 400, type: first.code })
|
|
57
54
|
}
|
|
58
55
|
const event = ResourcesEvents.Created({
|
|
59
|
-
type,
|
|
56
|
+
schemaId: type,
|
|
60
57
|
createdBy,
|
|
61
58
|
belongsTo: workspace.id,
|
|
62
59
|
location,
|
|
63
60
|
name,
|
|
64
|
-
content:
|
|
61
|
+
content: result.data,
|
|
62
|
+
access: payload?.access ?? 'workspace',
|
|
65
63
|
})
|
|
66
|
-
return
|
|
64
|
+
return commitResource(event)
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
if (!fileExtension) {
|
|
70
68
|
throw Object.assign(new Error('Missing valid file extension'), { status: 400 })
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
const
|
|
71
|
+
const resourceId = nanoid()
|
|
74
72
|
const event = ResourcesEvents.Created({
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
resourceId,
|
|
74
|
+
schemaId: PlatformSchema.file,
|
|
77
75
|
createdBy,
|
|
78
76
|
belongsTo: workspace.id,
|
|
79
77
|
location,
|
|
80
78
|
name,
|
|
79
|
+
access: payload?.access ?? 'workspace',
|
|
81
80
|
content: {
|
|
82
|
-
Key:
|
|
81
|
+
Key: originalObjectKey(resourceId),
|
|
83
82
|
ContentLength: size,
|
|
84
83
|
ContentType: type,
|
|
85
84
|
},
|
|
@@ -92,7 +91,7 @@ export async function run({ payload, integrations, req }) {
|
|
|
92
91
|
Key: event.payload.content.Key,
|
|
93
92
|
}
|
|
94
93
|
const uploadUrl = storageClient ? await storageClient.createUploadUrl(uploadHeaders) : null
|
|
95
|
-
const resource = await
|
|
94
|
+
const resource = await commitResource(event)
|
|
96
95
|
const withUpload = uploadUrl
|
|
97
96
|
? { ...resource, content: { ...resource.content, uploadUrl } }
|
|
98
97
|
: resource
|
package/src/delete.action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'resources/delete', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/resources/actions/delete', access: 'workspace' }
|
package/src/delete.task.js
CHANGED
|
@@ -1,31 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Resource } from './resource.aggregate.js'
|
|
1
|
+
import { PlatformSchema } from '@ossy/platform'
|
|
3
2
|
import { ResourcesEvents } from './resources.events.js'
|
|
4
3
|
import { ResourcesQueries } from './resources.queries.js'
|
|
4
|
+
import { mutateResource, viewResource } from './resource-stream.helpers.js'
|
|
5
|
+
import { resolveResourceId } from './resource-read.helpers.js'
|
|
5
6
|
|
|
6
|
-
export const metadata = { id: 'resources/delete' }
|
|
7
|
+
export const metadata = { id: '@ossy/resources/tasks/delete' }
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function run({ payload, req }) {
|
|
9
|
+
export async function run ({ payload, req }) {
|
|
11
10
|
const createdBy = payload?.userId ?? req?.userId
|
|
12
|
-
const resourceId = payload
|
|
11
|
+
const resourceId = resolveResourceId({ payload, req })
|
|
13
12
|
|
|
14
13
|
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
15
14
|
|
|
16
|
-
const
|
|
17
|
-
const resource = Aggregate.View()(aggregate)
|
|
15
|
+
const resource = await viewResource(resourceId)
|
|
18
16
|
|
|
19
|
-
if (resource.type ===
|
|
17
|
+
if (resource.type === PlatformSchema.directory || resource.type === 'directory') {
|
|
20
18
|
const nestedResources = await ResourcesQueries.GetNestedResources({
|
|
21
19
|
location: resource.location + resource.name + '/',
|
|
22
20
|
})
|
|
23
21
|
await Promise.all(nestedResources.map(nested =>
|
|
24
|
-
|
|
25
|
-
.then(Aggregate.Add(ResourcesEvents.Deleted({ createdBy })))
|
|
22
|
+
mutateResource(nested.id, ResourcesEvents.Deleted({ createdBy })),
|
|
26
23
|
))
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
await
|
|
26
|
+
await mutateResource(resourceId, ResourcesEvents.Deleted({ createdBy }))
|
|
30
27
|
return null
|
|
31
28
|
}
|
package/src/en.translations.json
CHANGED
|
@@ -7,30 +7,30 @@
|
|
|
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/create.label": "Create resource",
|
|
11
|
-
"resources/create.description": "Create a new resource in the workspace",
|
|
12
|
-
"resources/list.label": "List resources",
|
|
13
|
-
"resources/list.description": "List resources in a workspace location",
|
|
14
|
-
"resources/get.label": "Get resource",
|
|
15
|
-
"resources/get.description": "Fetch a single resource by id",
|
|
16
|
-
"resources/search.label": "Search resources",
|
|
17
|
-
"resources/search.description": "Search resources in a workspace",
|
|
18
|
-
"resources/delete.label": "Delete resource",
|
|
19
|
-
"resources/delete.description": "Delete a resource from the workspace",
|
|
20
|
-
"resources/update-name.label": "Update resource name",
|
|
21
|
-
"resources/update-name.description": "Rename a workspace resource",
|
|
22
|
-
"resources/update-content.label": "Update resource content",
|
|
23
|
-
"resources/update-content.description": "Update the content of a workspace resource",
|
|
24
|
-
"resources/update-location.label": "Update resource location",
|
|
25
|
-
"resources/update-location.description": "Move a resource to a new location",
|
|
26
|
-
"resources/update-access.label": "Update resource access",
|
|
27
|
-
"resources/update-access.description": "Change access settings for a resource",
|
|
28
|
-
"resources/upload-named-version.label": "Upload named version",
|
|
29
|
-
"resources/upload-named-version.description": "Upload a named version of a resource file",
|
|
30
|
-
"resources/create-page-view.label": "Create page view",
|
|
31
|
-
"resources/create-page-view.description": "Record a page view analytics event",
|
|
32
|
-
"resources/get-page-view-stats.label": "Get page view stats",
|
|
33
|
-
"resources/get-page-view-stats.description": "Fetch aggregated page view statistics",
|
|
10
|
+
"@ossy/resources/actions/create.label": "Create resource",
|
|
11
|
+
"@ossy/resources/actions/create.description": "Create a new resource in the workspace",
|
|
12
|
+
"@ossy/resources/actions/list.label": "List resources",
|
|
13
|
+
"@ossy/resources/actions/list.description": "List resources in a workspace location",
|
|
14
|
+
"@ossy/resources/actions/get.label": "Get resource",
|
|
15
|
+
"@ossy/resources/actions/get.description": "Fetch a single resource by id",
|
|
16
|
+
"@ossy/resources/actions/search.label": "Search resources",
|
|
17
|
+
"@ossy/resources/actions/search.description": "Search resources in a workspace",
|
|
18
|
+
"@ossy/resources/actions/delete.label": "Delete resource",
|
|
19
|
+
"@ossy/resources/actions/delete.description": "Delete a resource from the workspace",
|
|
20
|
+
"@ossy/resources/actions/update-name.label": "Update resource name",
|
|
21
|
+
"@ossy/resources/actions/update-name.description": "Rename a workspace resource",
|
|
22
|
+
"@ossy/resources/actions/update-content.label": "Update resource content",
|
|
23
|
+
"@ossy/resources/actions/update-content.description": "Update the content of a workspace resource",
|
|
24
|
+
"@ossy/resources/actions/update-location.label": "Update resource location",
|
|
25
|
+
"@ossy/resources/actions/update-location.description": "Move a resource to a new location",
|
|
26
|
+
"@ossy/resources/actions/update-access.label": "Update resource access",
|
|
27
|
+
"@ossy/resources/actions/update-access.description": "Change access settings for a resource",
|
|
28
|
+
"@ossy/resources/actions/upload-named-version.label": "Upload named version",
|
|
29
|
+
"@ossy/resources/actions/upload-named-version.description": "Upload a named version of a resource file",
|
|
30
|
+
"@ossy/resources/actions/create-page-view.label": "Create page view",
|
|
31
|
+
"@ossy/resources/actions/create-page-view.description": "Record a page view analytics event",
|
|
32
|
+
"@ossy/resources/actions/get-page-view-stats.label": "Get page view stats",
|
|
33
|
+
"@ossy/resources/actions/get-page-view-stats.description": "Fetch aggregated page view statistics",
|
|
34
34
|
"resources/create/generic.documentTitle": "Create resource",
|
|
35
35
|
"create-directory.documentTitle": "Create directory",
|
|
36
36
|
"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,15 @@ 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 './useSchemas.js'
|
|
43
|
+
export * from './useSchemaEngine.js'
|
|
44
|
+
export * from './useDocumentValidator.js'
|
|
39
45
|
export * from './useForm.js'
|
|
40
46
|
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()
|
|
@@ -37,7 +37,7 @@ export default function GenericResourceCreatePage () {
|
|
|
37
37
|
<View layout="off-center-m" style={{ height: '100%' }}>
|
|
38
38
|
<View slot="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}
|
|
@@ -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
|
+
}
|