@ossy/resources 1.6.0 → 1.7.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/package.json +2 -2
- package/src/create-page-view.action.js +38 -0
- package/src/create.action.js +98 -0
- package/src/delete.action.js +31 -0
- package/src/get-page-view-stats.action.js +65 -0
- package/src/get.action.js +14 -0
- package/src/item-access.api.js +34 -0
- package/src/item-content.api.js +35 -0
- package/src/item-location.api.js +34 -0
- package/src/item-name.api.js +34 -0
- package/src/item-version.api.js +48 -0
- package/src/item.api.js +58 -0
- package/src/list.action.js +19 -0
- package/src/list.api.js +43 -0
- package/src/page-views-stats.api.js +23 -0
- package/src/page-views.api.js +23 -0
- package/src/resources.attach-media-urls.js +60 -0
- package/src/resources.queries.js +95 -0
- package/src/search.action.js +19 -0
- package/src/search.api.js +27 -0
- package/src/update-access.action.js +20 -0
- package/src/update-content.action.js +39 -0
- package/src/update-location.action.js +18 -0
- package/src/update-name.action.js +18 -0
- package/src/upload-named-version.action.js +42 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/resources",
|
|
3
3
|
"description": "Resource domain — aggregate and events for the Ossy resource model",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"/src",
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "c6697078268867d88553ca0bac08faad5cea1546"
|
|
26
26
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
import { Workspace } from '@ossy/workspaces'
|
|
4
|
+
import { normalizeAndValidateDocumentContent } from '@ossy/platform'
|
|
5
|
+
|
|
6
|
+
export const id = 'resources/create-page-view'
|
|
7
|
+
export const access = 'workspace'
|
|
8
|
+
|
|
9
|
+
const PAGE_VIEW_TEMPLATE_ID = '@ossy/web/page-view'
|
|
10
|
+
const PAGE_VIEW_LOCATION = '/@ossy/analytics/page-views/'
|
|
11
|
+
|
|
12
|
+
export async function run({ payload, req }) {
|
|
13
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
14
|
+
if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
15
|
+
|
|
16
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
17
|
+
|
|
18
|
+
// Page-view template is a built-in; instantiate directly without schema validation
|
|
19
|
+
const contentInput = {
|
|
20
|
+
...(payload ?? {}),
|
|
21
|
+
userAgent: payload?.userAgent ?? req?.headers?.['user-agent'],
|
|
22
|
+
eventAt: payload?.eventAt ?? Date.now(),
|
|
23
|
+
path: payload?.path ?? '/',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const event = ResourcesEvents.Created({
|
|
27
|
+
type: PAGE_VIEW_TEMPLATE_ID,
|
|
28
|
+
createdBy: payload?.userId ?? req?.userId ?? 'public',
|
|
29
|
+
belongsTo: workspace.id,
|
|
30
|
+
location: PAGE_VIEW_LOCATION,
|
|
31
|
+
name: `${Date.now()}`,
|
|
32
|
+
content: contentInput,
|
|
33
|
+
access: 'restricted',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const resource = await Aggregate.Of(Resource, event).then(Aggregate.View())
|
|
37
|
+
return { id: resource.id }
|
|
38
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { is } from 'ramda'
|
|
2
|
+
import { nanoid } from 'nanoid'
|
|
3
|
+
import { Aggregate } from '@ossy/event-store'
|
|
4
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
5
|
+
import { Workspace } from '@ossy/workspaces'
|
|
6
|
+
import { getSystemResourceTemplates, normalizeAndValidateDocumentContent } from '@ossy/platform'
|
|
7
|
+
|
|
8
|
+
export const id = 'resources/create'
|
|
9
|
+
export const access = 'workspace'
|
|
10
|
+
|
|
11
|
+
const NativeResourceTypes = { DIRECTORY: 'directory' }
|
|
12
|
+
|
|
13
|
+
export async function run({ payload, integrations, req }) {
|
|
14
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
15
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
16
|
+
|
|
17
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
18
|
+
let location = payload?.location
|
|
19
|
+
const type = payload?.type
|
|
20
|
+
const name = payload?.name
|
|
21
|
+
const fileExtension = name?.split?.('.')?.pop?.()
|
|
22
|
+
const content = payload?.content
|
|
23
|
+
const size = payload?.size
|
|
24
|
+
|
|
25
|
+
if (!is(String, location)) {
|
|
26
|
+
throw Object.assign(new Error(`Not a valid location path: ${location}`), { status: 400, type: 'INVALID_PATH' })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
location = location.endsWith('/') ? location : location + '/'
|
|
30
|
+
location = location.startsWith('/') ? location : '/' + location
|
|
31
|
+
|
|
32
|
+
const systemTemplates = getSystemResourceTemplates()
|
|
33
|
+
const allTemplates = [
|
|
34
|
+
...systemTemplates,
|
|
35
|
+
...(workspace.resourceTemplates || []).filter(t => !systemTemplates.find(s => s.id === t.id)),
|
|
36
|
+
]
|
|
37
|
+
const isResourceTemplateType = allTemplates.map(t => t.id).includes(type)
|
|
38
|
+
|
|
39
|
+
if (type === NativeResourceTypes.DIRECTORY) {
|
|
40
|
+
const event = ResourcesEvents.Created({
|
|
41
|
+
type: NativeResourceTypes.DIRECTORY,
|
|
42
|
+
createdBy,
|
|
43
|
+
belongsTo: workspace.id,
|
|
44
|
+
location,
|
|
45
|
+
name,
|
|
46
|
+
access: 'workspace',
|
|
47
|
+
})
|
|
48
|
+
return Aggregate.Of(Resource, event).then(Aggregate.View())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isResourceTemplateType) {
|
|
52
|
+
const template = allTemplates.find(t => t.id === type)
|
|
53
|
+
const normalized = normalizeAndValidateDocumentContent(content, template)
|
|
54
|
+
if (!normalized.ok) {
|
|
55
|
+
throw Object.assign(new Error(normalized.message), { status: 400, type: normalized.code })
|
|
56
|
+
}
|
|
57
|
+
const event = ResourcesEvents.Created({
|
|
58
|
+
type,
|
|
59
|
+
createdBy,
|
|
60
|
+
belongsTo: workspace.id,
|
|
61
|
+
location,
|
|
62
|
+
name,
|
|
63
|
+
content: normalized.content,
|
|
64
|
+
})
|
|
65
|
+
return Aggregate.Of(Resource, event).then(Aggregate.View())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!fileExtension) {
|
|
69
|
+
throw Object.assign(new Error('Missing valid file extension'), { status: 400 })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const aggregateId = nanoid()
|
|
73
|
+
const event = ResourcesEvents.Created({
|
|
74
|
+
aggregateId,
|
|
75
|
+
type,
|
|
76
|
+
createdBy,
|
|
77
|
+
belongsTo: workspace.id,
|
|
78
|
+
location,
|
|
79
|
+
name,
|
|
80
|
+
content: {
|
|
81
|
+
Key: `media/${workspace.id}/${aggregateId}/original.${fileExtension}`,
|
|
82
|
+
ContentLength: size,
|
|
83
|
+
ContentType: type,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const storageClient = integrations?.get?.('storage')
|
|
88
|
+
const uploadHeaders = {
|
|
89
|
+
ContentLength: event.payload.content.ContentLength,
|
|
90
|
+
ContentType: event.payload.content.ContentType,
|
|
91
|
+
Key: event.payload.content.Key,
|
|
92
|
+
}
|
|
93
|
+
const uploadUrl = storageClient ? await storageClient.createUploadUrl(uploadHeaders) : null
|
|
94
|
+
const resource = await Aggregate.Of(Resource, event).then(Aggregate.View())
|
|
95
|
+
return uploadUrl
|
|
96
|
+
? { ...resource, content: { ...resource.content, uploadUrl } }
|
|
97
|
+
: resource
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
import { ResourcesQueries } from './resources.queries.js'
|
|
4
|
+
|
|
5
|
+
export const id = 'resources/delete'
|
|
6
|
+
export const access = 'workspace'
|
|
7
|
+
|
|
8
|
+
const DIRECTORY_TYPE = 'directory'
|
|
9
|
+
|
|
10
|
+
export async function run({ payload, req }) {
|
|
11
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
12
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
13
|
+
|
|
14
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
15
|
+
|
|
16
|
+
const aggregate = await Aggregate.Of(Resource, resourceId)
|
|
17
|
+
const resource = Aggregate.View()(aggregate)
|
|
18
|
+
|
|
19
|
+
if (resource.type === DIRECTORY_TYPE) {
|
|
20
|
+
const nestedResources = await ResourcesQueries.GetNestedResources({
|
|
21
|
+
location: resource.location + resource.name + '/',
|
|
22
|
+
})
|
|
23
|
+
await Promise.all(nestedResources.map(nested =>
|
|
24
|
+
Aggregate.Of(Resource, nested.id)
|
|
25
|
+
.then(Aggregate.Add(ResourcesEvents.Deleted({ createdBy })))
|
|
26
|
+
))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await Aggregate.Add(ResourcesEvents.Deleted({ createdBy }))(aggregate)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Workspace } from '@ossy/workspaces'
|
|
3
|
+
|
|
4
|
+
export const id = 'resources/get-page-view-stats'
|
|
5
|
+
export const access = 'workspace'
|
|
6
|
+
|
|
7
|
+
const PAGE_VIEW_TEMPLATE_ID = '@ossy/web/page-view'
|
|
8
|
+
const PAGE_VIEW_LOCATION = '/@ossy/analytics/page-views/'
|
|
9
|
+
|
|
10
|
+
function parseMillis(input, fallback) {
|
|
11
|
+
if (input === undefined || input === null || input === '') return fallback
|
|
12
|
+
const n = Number(input)
|
|
13
|
+
if (!Number.isFinite(n) || n <= 0) return fallback
|
|
14
|
+
return n
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function run({ payload, req }) {
|
|
18
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
19
|
+
if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
|
|
20
|
+
|
|
21
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
22
|
+
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const defaultFrom = now - (30 * 24 * 60 * 60 * 1000)
|
|
25
|
+
const from = parseMillis(payload?.from ?? req?.query?.from, defaultFrom)
|
|
26
|
+
const to = parseMillis(payload?.to ?? req?.query?.to, now)
|
|
27
|
+
const topLimit = Math.min(Math.max(Number(payload?.limit ?? req?.query?.limit ?? 10), 1), 50)
|
|
28
|
+
|
|
29
|
+
const baseMatch = {
|
|
30
|
+
type: 'Resource',
|
|
31
|
+
'state.status': { $ne: 'removed' },
|
|
32
|
+
'state.belongsTo': workspace.id,
|
|
33
|
+
'state.type': PAGE_VIEW_TEMPLATE_ID,
|
|
34
|
+
'state.location': PAGE_VIEW_LOCATION,
|
|
35
|
+
'state.content.eventAt': { $gte: from, $lte: to },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [dailyViews, topPaths, totals] = await Promise.all([
|
|
39
|
+
Aggregate.Collection.aggregate([
|
|
40
|
+
{ $match: baseMatch },
|
|
41
|
+
{ $project: { date: { $dateToString: { format: '%Y-%m-%d', date: { $toDate: '$state.content.eventAt' } } } } },
|
|
42
|
+
{ $group: { _id: '$date', views: { $sum: 1 } } },
|
|
43
|
+
{ $sort: { _id: 1 } },
|
|
44
|
+
]).toArray(),
|
|
45
|
+
Aggregate.Collection.aggregate([
|
|
46
|
+
{ $match: baseMatch },
|
|
47
|
+
{ $group: { _id: '$state.content.path', views: { $sum: 1 } } },
|
|
48
|
+
{ $sort: { views: -1 } },
|
|
49
|
+
{ $limit: topLimit },
|
|
50
|
+
{ $project: { _id: 0, path: '$_id', views: 1 } },
|
|
51
|
+
]).toArray(),
|
|
52
|
+
Aggregate.Collection.aggregate([
|
|
53
|
+
{ $match: baseMatch },
|
|
54
|
+
{ $count: 'views' },
|
|
55
|
+
]).toArray(),
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
from,
|
|
60
|
+
to,
|
|
61
|
+
totalViews: totals?.[0]?.views || 0,
|
|
62
|
+
dailyViews: dailyViews.map(x => ({ date: x._id, views: x.views })),
|
|
63
|
+
topPaths,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource } from '@ossy/resources'
|
|
3
|
+
|
|
4
|
+
export const id = 'resources/get'
|
|
5
|
+
export const access = 'workspace'
|
|
6
|
+
|
|
7
|
+
export async function run({ payload, req }) {
|
|
8
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
9
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
10
|
+
|
|
11
|
+
const resource = await Aggregate.Of(Resource, resourceId).then(Aggregate.View())
|
|
12
|
+
if (!resource?.id) throw Object.assign(new Error('Resource not found'), { status: 404 })
|
|
13
|
+
return resource
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)\/access$/
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: 'resources.item.access',
|
|
8
|
+
path: '/api/v0/resources/:resourceId/access',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function handle(req, res) {
|
|
12
|
+
if (req.method !== 'PUT') {
|
|
13
|
+
res.setHeader('Allow', 'PUT')
|
|
14
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
18
|
+
const m = pathname.match(pathRe)
|
|
19
|
+
if (!m) {
|
|
20
|
+
res.status(404).json('')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const resourceId = decodeURIComponent(m[1])
|
|
24
|
+
try {
|
|
25
|
+
const resource = await ActionService.invoke('resources/update-access', {
|
|
26
|
+
payload: { resourceId, access: req.body?.access, userId: req.userId },
|
|
27
|
+
req,
|
|
28
|
+
})
|
|
29
|
+
const withUrls = await attachResourceMediaUrls(resource, { userId: req.userId, workspaceId: req.workspaceId })
|
|
30
|
+
res.json(withUrls)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)\/content$/
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: 'resources.item.content',
|
|
8
|
+
path: '/api/v0/resources/:resourceId/content',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function handle(req, res) {
|
|
12
|
+
if (req.method !== 'PUT') {
|
|
13
|
+
res.setHeader('Allow', 'PUT')
|
|
14
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
18
|
+
const m = pathname.match(pathRe)
|
|
19
|
+
if (!m) {
|
|
20
|
+
res.status(404).json('')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const resourceId = decodeURIComponent(m[1])
|
|
24
|
+
try {
|
|
25
|
+
const resource = await ActionService.invoke('resources/update-content', {
|
|
26
|
+
payload: { resourceId, content: req.body?.content, userId: req.userId, workspaceId: req.workspaceId },
|
|
27
|
+
req,
|
|
28
|
+
})
|
|
29
|
+
if (resource === null || resource === undefined) return
|
|
30
|
+
const withUrls = await attachResourceMediaUrls(resource, { userId: req.userId, workspaceId: req.workspaceId })
|
|
31
|
+
res.json(withUrls)
|
|
32
|
+
} catch (err) {
|
|
33
|
+
res.status(err?.status ?? 400).json({ type: err?.type, message: err?.message })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)\/location$/
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: 'resources.item.location',
|
|
8
|
+
path: '/api/v0/resources/:resourceId/location',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function handle(req, res) {
|
|
12
|
+
if (req.method !== 'PUT') {
|
|
13
|
+
res.setHeader('Allow', 'PUT')
|
|
14
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
18
|
+
const m = pathname.match(pathRe)
|
|
19
|
+
if (!m) {
|
|
20
|
+
res.status(404).json('')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const resourceId = decodeURIComponent(m[1])
|
|
24
|
+
try {
|
|
25
|
+
const resource = await ActionService.invoke('resources/update-location', {
|
|
26
|
+
payload: { resourceId, target: req.body?.target, userId: req.userId },
|
|
27
|
+
req,
|
|
28
|
+
})
|
|
29
|
+
const withUrls = await attachResourceMediaUrls(resource, { userId: req.userId, workspaceId: req.workspaceId })
|
|
30
|
+
res.json(withUrls)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)\/name$/
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: 'resources.item.name',
|
|
8
|
+
path: '/api/v0/resources/:resourceId/name',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function handle(req, res) {
|
|
12
|
+
if (req.method !== 'PUT') {
|
|
13
|
+
res.setHeader('Allow', 'PUT')
|
|
14
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
18
|
+
const m = pathname.match(pathRe)
|
|
19
|
+
if (!m) {
|
|
20
|
+
res.status(404).json('')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const resourceId = decodeURIComponent(m[1])
|
|
24
|
+
try {
|
|
25
|
+
const resource = await ActionService.invoke('resources/update-name', {
|
|
26
|
+
payload: { resourceId, name: req.body?.name, userId: req.userId },
|
|
27
|
+
req,
|
|
28
|
+
})
|
|
29
|
+
const withUrls = await attachResourceMediaUrls(resource, { userId: req.userId, workspaceId: req.workspaceId })
|
|
30
|
+
res.json(withUrls)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)\/([^/]+)$/
|
|
5
|
+
const reservedSecond = new Set(['name', 'location', 'content', 'access'])
|
|
6
|
+
|
|
7
|
+
export const metadata = {
|
|
8
|
+
id: 'resources.item.version',
|
|
9
|
+
path: '/api/v0/resources/:resourceId/:namedVersion',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default async function handle(req, res) {
|
|
13
|
+
if (req.method !== 'PUT') {
|
|
14
|
+
res.setHeader('Allow', 'PUT')
|
|
15
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
19
|
+
const m = pathname.match(pathRe)
|
|
20
|
+
if (!m) {
|
|
21
|
+
res.status(404).json('')
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
const namedVersion = decodeURIComponent(m[2])
|
|
25
|
+
if (reservedSecond.has(namedVersion)) {
|
|
26
|
+
res.status(404).json('')
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
const resourceId = decodeURIComponent(m[1])
|
|
30
|
+
try {
|
|
31
|
+
const resource = await ActionService.invoke('resources/upload-named-version', {
|
|
32
|
+
payload: {
|
|
33
|
+
resourceId,
|
|
34
|
+
namedVersion,
|
|
35
|
+
type: req.body?.type,
|
|
36
|
+
size: req.body?.size,
|
|
37
|
+
userId: req.userId,
|
|
38
|
+
workspaceId: req.workspaceId,
|
|
39
|
+
},
|
|
40
|
+
integrations: req.integrations,
|
|
41
|
+
req,
|
|
42
|
+
})
|
|
43
|
+
const withUrls = await attachResourceMediaUrls(resource, { userId: req.userId, workspaceId: req.workspaceId })
|
|
44
|
+
res.json(withUrls)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.status(err?.status ?? 400).json({ error: err?.message })
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/item.api.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
const pathRe = /^\/api\/v0\/resources\/([^/]+)$/
|
|
5
|
+
|
|
6
|
+
export const metadata = {
|
|
7
|
+
id: 'resources.item',
|
|
8
|
+
path: '/api/v0/resources/:resourceId',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function mediaContext(req) {
|
|
12
|
+
return { userId: req.userId, workspaceId: req.workspaceId }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default async function handle(req, res) {
|
|
16
|
+
const pathname = (req.path || new URL(req.originalUrl, 'http://localhost').pathname).replace(/\/+$/, '')
|
|
17
|
+
const m = pathname.match(pathRe)
|
|
18
|
+
if (!m) {
|
|
19
|
+
res.status(404).json('')
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const segment = decodeURIComponent(m[1])
|
|
23
|
+
const reserved = new Set(['search', 'page-views'])
|
|
24
|
+
if (reserved.has(segment)) {
|
|
25
|
+
res.status(404).json('')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
req.params = { ...req.params, resourceId: segment }
|
|
30
|
+
|
|
31
|
+
if (req.method === 'GET') {
|
|
32
|
+
try {
|
|
33
|
+
const resource = await ActionService.invoke('resources/get', {
|
|
34
|
+
payload: { resourceId: segment },
|
|
35
|
+
req,
|
|
36
|
+
})
|
|
37
|
+
const withUrls = await attachResourceMediaUrls(resource, mediaContext(req))
|
|
38
|
+
res.json(withUrls)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
41
|
+
}
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if (req.method === 'DELETE') {
|
|
45
|
+
try {
|
|
46
|
+
await ActionService.invoke('resources/delete', {
|
|
47
|
+
payload: { resourceId: segment, userId: req.userId },
|
|
48
|
+
req,
|
|
49
|
+
})
|
|
50
|
+
res.status(204).end()
|
|
51
|
+
} catch (err) {
|
|
52
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
53
|
+
}
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
res.setHeader('Allow', 'GET, DELETE')
|
|
57
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ResourcesQueries } from './resources.queries.js'
|
|
2
|
+
|
|
3
|
+
export const id = 'resources/list'
|
|
4
|
+
export const access = 'workspace'
|
|
5
|
+
|
|
6
|
+
export async function run({ payload, req }) {
|
|
7
|
+
const query = { ...(payload?.query ?? {}) }
|
|
8
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
9
|
+
const user = payload?.user ?? req?.user
|
|
10
|
+
let location = payload?.location ?? query.location
|
|
11
|
+
|
|
12
|
+
if (location && !location.endsWith('/')) location += '/'
|
|
13
|
+
if (location && !location.startsWith('/')) location = '/' + location
|
|
14
|
+
if (location) query.location = location
|
|
15
|
+
|
|
16
|
+
if (workspaceId) query.belongsTo = workspaceId
|
|
17
|
+
|
|
18
|
+
return ResourcesQueries.GetResources(query, user)
|
|
19
|
+
}
|
package/src/list.api.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
id: 'resources.list',
|
|
6
|
+
path: '/api/v0/resources',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mediaContext(req) {
|
|
10
|
+
return { userId: req.userId, workspaceId: req.workspaceId }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default async function handle(req, res) {
|
|
14
|
+
if (req.method === 'POST') {
|
|
15
|
+
try {
|
|
16
|
+
const resource = await ActionService.invoke('resources/create', {
|
|
17
|
+
payload: { ...req.body, userId: req.userId, workspaceId: req.workspaceId },
|
|
18
|
+
integrations: req.integrations,
|
|
19
|
+
req,
|
|
20
|
+
})
|
|
21
|
+
const withUrls = await attachResourceMediaUrls(resource, mediaContext(req))
|
|
22
|
+
res.json(withUrls)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
25
|
+
}
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
if (req.method === 'GET') {
|
|
29
|
+
try {
|
|
30
|
+
const resources = await ActionService.invoke('resources/list', {
|
|
31
|
+
payload: { query: req.query, workspaceId: req.workspaceId, userId: req.userId, user: req.user },
|
|
32
|
+
req,
|
|
33
|
+
})
|
|
34
|
+
const withUrls = await Promise.all(resources.map(r => attachResourceMediaUrls(r, mediaContext(req))))
|
|
35
|
+
res.json(withUrls)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
38
|
+
}
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
res.setHeader('Allow', 'GET, POST')
|
|
42
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
id: 'resources.pageViews.stats',
|
|
5
|
+
path: '/api/v0/resources/page-views/stats',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default async function handle(req, res) {
|
|
9
|
+
if (req.method !== 'GET') {
|
|
10
|
+
res.setHeader('Allow', 'GET')
|
|
11
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const result = await ActionService.invoke('resources/get-page-view-stats', {
|
|
16
|
+
payload: { workspaceId: req.workspaceId, from: req.query?.from, to: req.query?.to, limit: req.query?.limit },
|
|
17
|
+
req,
|
|
18
|
+
})
|
|
19
|
+
res.json(result)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
id: 'resources.pageViews',
|
|
5
|
+
path: '/api/v0/resources/page-views',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default async function handle(req, res) {
|
|
9
|
+
if (req.method !== 'POST') {
|
|
10
|
+
res.setHeader('Allow', 'POST')
|
|
11
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const result = await ActionService.invoke('resources/create-page-view', {
|
|
16
|
+
payload: { ...req.body, workspaceId: req.workspaceId, userId: req.userId },
|
|
17
|
+
req,
|
|
18
|
+
})
|
|
19
|
+
res.status(201).json(result)
|
|
20
|
+
} catch (err) {
|
|
21
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { StorageClient } from '@ossy/platform'
|
|
2
|
+
import { Aggregate } from '@ossy/event-store'
|
|
3
|
+
import { Workspace } from '@ossy/workspaces'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds `content.src` (and `content.sizes[*]` URLs) for file resources, respecting
|
|
7
|
+
* `access` and workspace membership. Restricted resources get short-lived S3 presigned GETs
|
|
8
|
+
* for members only; public resources get CDN URLs for any caller.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} resource - Resource view
|
|
11
|
+
* @param {{ userId?: string, workspaceId?: string }} context
|
|
12
|
+
* @returns {Promise<object>}
|
|
13
|
+
*/
|
|
14
|
+
export async function attachResourceMediaUrls(resource, { userId, workspaceId } = {}) {
|
|
15
|
+
if (!resource?.content) return resource
|
|
16
|
+
const workspace = workspaceId
|
|
17
|
+
? await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
18
|
+
: null
|
|
19
|
+
const { Key, sizes } = resource.content
|
|
20
|
+
const hasSizes = sizes && typeof sizes === 'object' && Object.keys(sizes).length > 0
|
|
21
|
+
if (!Key && !hasSizes) return resource
|
|
22
|
+
|
|
23
|
+
const access = resource.access || 'restricted'
|
|
24
|
+
const isMember = Boolean(userId && workspace?.users?.includes(userId))
|
|
25
|
+
const allowMedia = access === 'public' || (access === 'restricted' && isMember)
|
|
26
|
+
|
|
27
|
+
const nextContent = { ...resource.content }
|
|
28
|
+
|
|
29
|
+
if (!allowMedia) {
|
|
30
|
+
delete nextContent.Key
|
|
31
|
+
delete nextContent.src
|
|
32
|
+
if (hasSizes) nextContent.sizes = {}
|
|
33
|
+
return { ...resource, content: nextContent }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Key) {
|
|
37
|
+
nextContent.src =
|
|
38
|
+
access === 'public'
|
|
39
|
+
? StorageClient.createDownloadUrl(Key)
|
|
40
|
+
: await StorageClient.createPresignedDownloadUrl(Key)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (hasSizes) {
|
|
44
|
+
const nextSizes = {}
|
|
45
|
+
for (const [name, value] of Object.entries(sizes)) {
|
|
46
|
+
if (value == null || typeof value !== 'string') continue
|
|
47
|
+
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
48
|
+
nextSizes[name] = value
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
nextSizes[name] =
|
|
52
|
+
access === 'public'
|
|
53
|
+
? StorageClient.createDownloadUrl(value)
|
|
54
|
+
: await StorageClient.createPresignedDownloadUrl(value)
|
|
55
|
+
}
|
|
56
|
+
nextContent.sizes = nextSizes
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { ...resource, content: nextContent }
|
|
60
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { createLogger } from '@ossy/observability'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('resources')
|
|
5
|
+
|
|
6
|
+
function globToRegex(pattern) {
|
|
7
|
+
const escaped = pattern
|
|
8
|
+
.replace(/\./g, '\\.')
|
|
9
|
+
.replace(/\*\*/g, '§§')
|
|
10
|
+
.replace(/\*/g, '[^/]*')
|
|
11
|
+
.replace(/§§/g, '.*')
|
|
12
|
+
return new RegExp('^' + escaped + '$')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function policyToQueryClause(policy) {
|
|
16
|
+
const { where, actions, effect } = policy.state
|
|
17
|
+
if (effect !== 'allow' || !actions.includes('resource:read')) return null
|
|
18
|
+
|
|
19
|
+
const clause = { 'state.access': 'restricted' }
|
|
20
|
+
|
|
21
|
+
if (where.workspace && where.workspace !== '*')
|
|
22
|
+
clause['state.belongsTo'] = where.workspace
|
|
23
|
+
|
|
24
|
+
if (where.location && where.location !== '*')
|
|
25
|
+
clause['state.location'] = { $regex: globToRegex(where.location).source }
|
|
26
|
+
|
|
27
|
+
if (where.type && where.type !== '*')
|
|
28
|
+
clause['state.type'] = { $regex: globToRegex(where.type).source }
|
|
29
|
+
|
|
30
|
+
return clause
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function accessFilter(user) {
|
|
34
|
+
const conditions = [{ 'state.access': 'public' }]
|
|
35
|
+
|
|
36
|
+
if (!user?.anonymous) {
|
|
37
|
+
if (user?.workspaces?.length) {
|
|
38
|
+
conditions.push({
|
|
39
|
+
'state.access': { $in: ['public', 'workspace'] },
|
|
40
|
+
'state.belongsTo': { $in: user.workspaces },
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
for (const policy of (user?.policies ?? [])) {
|
|
44
|
+
const clause = policyToQueryClause(policy)
|
|
45
|
+
if (clause) conditions.push(clause)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { $or: conditions }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ResourcesQueries {
|
|
53
|
+
static #prefixKey(key) {
|
|
54
|
+
if (key.startsWith('$')) return key
|
|
55
|
+
return `state.${key}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static #prefixQuery(query) {
|
|
59
|
+
if (Array.isArray(query)) return query.map(item => ResourcesQueries.#prefixQuery(item))
|
|
60
|
+
if (query !== null && typeof query === 'object') {
|
|
61
|
+
return Object.entries(query).reduce((acc, [key, value]) => {
|
|
62
|
+
const prefixedKey = ResourcesQueries.#prefixKey(key)
|
|
63
|
+
const prefixedValue = key.startsWith('$') ? ResourcesQueries.#prefixQuery(value) : value
|
|
64
|
+
return { ...acc, [prefixedKey]: prefixedValue }
|
|
65
|
+
}, {})
|
|
66
|
+
}
|
|
67
|
+
return query
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static GetResources(_query = {}, user = null) {
|
|
71
|
+
log.info('[ResourcesQueries][GetResources] Building query')
|
|
72
|
+
const query = ResourcesQueries.#prefixQuery({ ..._query, status: { $ne: 'removed' } })
|
|
73
|
+
const workspaceScope = query['state.belongsTo'] ?? '(no workspace scope)'
|
|
74
|
+
log.info(`[ResourcesQueries][GetResources] Fetching resources for ${workspaceScope}`)
|
|
75
|
+
|
|
76
|
+
const baseQuery = { ...query, type: 'Resource' }
|
|
77
|
+
const mongoQuery = user ? { $and: [baseQuery, accessFilter(user)] } : baseQuery
|
|
78
|
+
|
|
79
|
+
return Aggregate.Collection.find(mongoQuery, { state: true }).toArray()
|
|
80
|
+
.then(resources => {
|
|
81
|
+
log.info(`[ResourcesQueries][GetResources] Found ${resources.length} resources`)
|
|
82
|
+
return resources.map(resource => resource.state)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static GetNestedResources({ location }) {
|
|
87
|
+
log.info(`[ResourcesQueries][GetNestedResources] Fetching nested resources for ${location}`)
|
|
88
|
+
const regex = new RegExp(`^${location}`)
|
|
89
|
+
return Aggregate.Collection.find({
|
|
90
|
+
'state.location': { $regex: regex },
|
|
91
|
+
'state.status': { $ne: 'removed' },
|
|
92
|
+
}, { state: true }).toArray()
|
|
93
|
+
.then(resources => resources.map(resource => resource.state))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ResourcesQueries } from './resources.queries.js'
|
|
2
|
+
|
|
3
|
+
export const id = 'resources/search'
|
|
4
|
+
export const access = 'workspace'
|
|
5
|
+
|
|
6
|
+
export async function run({ payload, req }) {
|
|
7
|
+
const query = { ...(payload ?? {}) }
|
|
8
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
9
|
+
const user = payload?.user ?? req?.user
|
|
10
|
+
let location = query.location
|
|
11
|
+
|
|
12
|
+
if (location && !location.endsWith('/')) location += '/'
|
|
13
|
+
if (location && !location.startsWith('/')) location = '/' + location
|
|
14
|
+
if (location) query.location = location
|
|
15
|
+
|
|
16
|
+
if (workspaceId) query.belongsTo = workspaceId
|
|
17
|
+
|
|
18
|
+
return ResourcesQueries.GetResources(query, user)
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ActionService } from '@ossy/platform'
|
|
2
|
+
import { attachResourceMediaUrls } from './resources.attach-media-urls.js'
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
id: 'resources.search',
|
|
6
|
+
path: '/api/v0/resources/search',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default async function handle(req, res) {
|
|
10
|
+
if (req.method !== 'POST') {
|
|
11
|
+
res.setHeader('Allow', 'POST')
|
|
12
|
+
res.status(405).json({ error: 'Method Not Allowed' })
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const resources = await ActionService.invoke('resources/search', {
|
|
17
|
+
payload: { ...req.body, workspaceId: req.workspaceId, user: req.user },
|
|
18
|
+
req,
|
|
19
|
+
})
|
|
20
|
+
const withUrls = await Promise.all(
|
|
21
|
+
resources.map(r => attachResourceMediaUrls(r, { userId: req.userId, workspaceId: req.workspaceId }))
|
|
22
|
+
)
|
|
23
|
+
res.json(withUrls)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
res.status(err?.status ?? 500).json({ error: err?.message })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
|
|
4
|
+
export const id = 'resources/update-access'
|
|
5
|
+
export const access = 'workspace'
|
|
6
|
+
|
|
7
|
+
export async function run({ payload, req }) {
|
|
8
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
9
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
10
|
+
const resourceAccess = payload?.access
|
|
11
|
+
|
|
12
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
13
|
+
if (!['public', 'restricted'].includes(resourceAccess)) {
|
|
14
|
+
throw Object.assign(new Error(`Invalid access value: ${resourceAccess}`), { status: 400 })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return Aggregate.Of(Resource, resourceId)
|
|
18
|
+
.then(Aggregate.Add(ResourcesEvents.AccessUpdated({ createdBy, access: resourceAccess })))
|
|
19
|
+
.then(Aggregate.View())
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
import { Workspace } from '@ossy/workspaces'
|
|
4
|
+
import { getSystemResourceTemplates, normalizeAndValidateDocumentContent } from '@ossy/platform'
|
|
5
|
+
|
|
6
|
+
export const id = 'resources/update-content'
|
|
7
|
+
export const access = 'workspace'
|
|
8
|
+
|
|
9
|
+
export async function run({ payload, req }) {
|
|
10
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
11
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
12
|
+
const content = payload?.content
|
|
13
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
14
|
+
|
|
15
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
16
|
+
|
|
17
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
18
|
+
|
|
19
|
+
return Aggregate.Of(Resource, resourceId)
|
|
20
|
+
.then(aggregate => {
|
|
21
|
+
const resource = Aggregate.View()(aggregate)
|
|
22
|
+
const systemTemplates = getSystemResourceTemplates()
|
|
23
|
+
const allTemplates = [
|
|
24
|
+
...systemTemplates,
|
|
25
|
+
...(workspace.resourceTemplates || []).filter(t => !systemTemplates.find(s => s.id === t.id)),
|
|
26
|
+
]
|
|
27
|
+
const template = allTemplates.find(t => t.id === resource.type)
|
|
28
|
+
let finalContent = content
|
|
29
|
+
if (template) {
|
|
30
|
+
const normalized = normalizeAndValidateDocumentContent(content, template)
|
|
31
|
+
if (!normalized.ok) {
|
|
32
|
+
throw Object.assign(new Error(normalized.message), { status: 400, type: normalized.code })
|
|
33
|
+
}
|
|
34
|
+
finalContent = normalized.content
|
|
35
|
+
}
|
|
36
|
+
return Aggregate.Add(ResourcesEvents.ContentUpdated({ createdBy, content: finalContent }))(aggregate)
|
|
37
|
+
})
|
|
38
|
+
.then(Aggregate.View())
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
|
|
4
|
+
export const id = 'resources/update-location'
|
|
5
|
+
export const access = 'workspace'
|
|
6
|
+
|
|
7
|
+
export async function run({ payload, req }) {
|
|
8
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
9
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
10
|
+
const location = payload?.target ?? payload?.location
|
|
11
|
+
|
|
12
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
13
|
+
if (!location) throw Object.assign(new Error('target location is required'), { status: 400 })
|
|
14
|
+
|
|
15
|
+
return Aggregate.Of(Resource, resourceId)
|
|
16
|
+
.then(Aggregate.Add(ResourcesEvents.LocationUpdated({ createdBy, location })))
|
|
17
|
+
.then(Aggregate.View())
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
|
|
4
|
+
export const id = 'resources/update-name'
|
|
5
|
+
export const access = 'workspace'
|
|
6
|
+
|
|
7
|
+
export async function run({ payload, req }) {
|
|
8
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
9
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
10
|
+
const name = payload?.name
|
|
11
|
+
|
|
12
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
13
|
+
if (!name) throw Object.assign(new Error('name is required'), { status: 400 })
|
|
14
|
+
|
|
15
|
+
return Aggregate.Of(Resource, resourceId)
|
|
16
|
+
.then(Aggregate.Add(ResourcesEvents.NameUpdated({ createdBy, name })))
|
|
17
|
+
.then(Aggregate.View())
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Aggregate } from '@ossy/event-store'
|
|
2
|
+
import { Resource, ResourcesEvents } from '@ossy/resources'
|
|
3
|
+
import { Workspace } from '@ossy/workspaces'
|
|
4
|
+
|
|
5
|
+
export const id = 'resources/upload-named-version'
|
|
6
|
+
export const access = 'workspace'
|
|
7
|
+
|
|
8
|
+
export async function run({ payload, integrations, req }) {
|
|
9
|
+
const createdBy = payload?.userId ?? req?.userId
|
|
10
|
+
const workspaceId = payload?.workspaceId ?? req?.workspaceId
|
|
11
|
+
const resourceId = payload?.resourceId ?? req?.params?.resourceId
|
|
12
|
+
const namedVersion = payload?.namedVersion ?? req?.params?.namedVersion
|
|
13
|
+
const type = payload?.type
|
|
14
|
+
const size = payload?.size
|
|
15
|
+
|
|
16
|
+
if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
|
|
17
|
+
if (!namedVersion) throw Object.assign(new Error('namedVersion is required'), { status: 400 })
|
|
18
|
+
|
|
19
|
+
const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
|
|
20
|
+
|
|
21
|
+
const event = ResourcesEvents.NamedVersionUploaded({
|
|
22
|
+
createdBy,
|
|
23
|
+
namedVersion,
|
|
24
|
+
Key: `media/${workspace.id}/${resourceId}/${namedVersion}.${type.replace('image/', '')}`,
|
|
25
|
+
ContentLength: size,
|
|
26
|
+
ContentType: type,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const storageClient = integrations?.get?.('storage')
|
|
30
|
+
const uploadHeaders = {
|
|
31
|
+
ContentLength: event.payload.ContentLength,
|
|
32
|
+
ContentType: event.payload.ContentType,
|
|
33
|
+
Key: event.payload.Key,
|
|
34
|
+
}
|
|
35
|
+
const uploadUrl = storageClient ? await storageClient.createUploadUrl(uploadHeaders) : null
|
|
36
|
+
const resource = await Aggregate.Of(Resource, resourceId)
|
|
37
|
+
.then(Aggregate.Add(event))
|
|
38
|
+
.then(Aggregate.View())
|
|
39
|
+
return uploadUrl
|
|
40
|
+
? { ...resource, content: { ...resource.content, uploadUrl } }
|
|
41
|
+
: resource
|
|
42
|
+
}
|