@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.
Files changed (79) hide show
  1. package/package.json +11 -3
  2. package/src/AudioResource.jsx +1 -1
  3. package/src/CreateDirectory.jsx +2 -10
  4. package/src/CreateDocument.jsx +2 -2
  5. package/src/Definition.js +2 -1
  6. package/src/DocumentEdit.jsx +10 -38
  7. package/src/DocumentView.jsx +3 -4
  8. package/src/GenericResourceCard.jsx +25 -7
  9. package/src/GenericResourceDetail.jsx +6 -60
  10. package/src/GenericResourceForm.jsx +66 -31
  11. package/src/ImageResource.jsx +1 -1
  12. package/src/PDFResource.jsx +1 -1
  13. package/src/PlatformFileField.jsx +239 -0
  14. package/src/ResourceContentPage.jsx +3 -3
  15. package/src/ResourceDescription.jsx +1 -1
  16. package/src/ResourceDetails.jsx +3 -4
  17. package/src/ResourceDialogMove.jsx +2 -2
  18. package/src/ResourceFactory.jsx +11 -64
  19. package/src/ResourceGenericView.jsx +1 -1
  20. package/src/ResourceList.jsx +89 -106
  21. package/src/ResourcePanel.jsx +40 -57
  22. package/src/ResourceTags.jsx +1 -1
  23. package/src/ResourcesPage.jsx +26 -35
  24. package/src/SchemaPresenter.jsx +215 -0
  25. package/src/Upload.jsx +6 -25
  26. package/src/UploadResources.jsx +2 -2
  27. package/src/VideoResource.jsx +1 -1
  28. package/src/access-filter.spec.js +1 -1
  29. package/src/create-page-view.action.js +1 -1
  30. package/src/create-page-view.task.js +10 -9
  31. package/src/create.action.js +1 -1
  32. package/src/create.task.js +28 -29
  33. package/src/delete.action.js +1 -1
  34. package/src/delete.task.js +10 -13
  35. package/src/en.translations.json +24 -24
  36. package/src/get-page-view-stats.action.js +1 -1
  37. package/src/get-page-view-stats.task.js +3 -4
  38. package/src/get-resource-alias.api.js +8 -0
  39. package/src/get-resource-info-alias.api.js +8 -0
  40. package/src/get-resource-info.api.js +36 -0
  41. package/src/get-resource-variant.api.js +8 -0
  42. package/src/get-resource.api.js +51 -0
  43. package/src/get.action.js +1 -1
  44. package/src/get.task.js +6 -6
  45. package/src/index.js +6 -0
  46. package/src/list.action.js +1 -1
  47. package/src/list.task.js +1 -1
  48. package/src/platform-file-field.component.jsx +5 -0
  49. package/src/platform-image-field.component.jsx +5 -0
  50. package/src/resource-create.page.jsx +4 -4
  51. package/src/resource-read.helpers.js +151 -0
  52. package/src/resource-stream.helpers.js +20 -0
  53. package/src/resource-stream.js +108 -0
  54. package/src/resource.helpers.js +9 -9
  55. package/src/resources.attach-media-urls.js +9 -3
  56. package/src/resources.events.js +42 -81
  57. package/src/{resources.spec.js → resources.integration.spec.js} +19 -19
  58. package/src/resources.queries.js +5 -2
  59. package/src/search.action.js +1 -1
  60. package/src/search.task.js +1 -1
  61. package/src/server.js +2 -1
  62. package/src/sv.translations.json +24 -24
  63. package/src/update-access.action.js +1 -1
  64. package/src/update-access.task.js +9 -8
  65. package/src/update-content.action.js +1 -1
  66. package/src/update-content.task.js +25 -27
  67. package/src/update-location.action.js +1 -1
  68. package/src/update-location.task.js +9 -8
  69. package/src/update-name.action.js +1 -1
  70. package/src/update-name.task.js +9 -8
  71. package/src/upload-named-version.action.js +1 -1
  72. package/src/upload-named-version.task.js +16 -22
  73. package/src/useActivePath.jsx +3 -8
  74. package/src/useDocumentValidator.js +16 -0
  75. package/src/useSchemaEngine.js +9 -0
  76. package/src/useSchemas.js +29 -0
  77. package/src/useSelectedResourceId.jsx +36 -0
  78. package/src/ResourceContentPage.stories.jsx +0 -19
  79. package/src/resource.aggregate.js +0 -86
@@ -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 }
@@ -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' }, { id: params.id }))
81
+ sdk.invalidate(cacheKey({ id: '@ossy/resources/actions/get' }, { resourceId: params.resourceId ?? params.id }))
82
82
  return resource
83
83
  }
@@ -4,8 +4,8 @@ import { Workspace } from '@ossy/workspaces/server'
4
4
 
5
5
  /**
6
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.
7
+ * `access` and workspace membership. All backends expose the same read URL shape:
8
+ * `/r/{resourceId}` (derivatives: `/r/{resourceId}/{variant}`).
9
9
  *
10
10
  * @param {object} resource - Resource view
11
11
  * @param {{ userId?: string, workspaceId?: string }} context
@@ -22,7 +22,13 @@ export async function attachResourceMediaUrls(resource, { userId, workspaceId }
22
22
 
23
23
  const access = resource.access || 'restricted'
24
24
  const isMember = Boolean(userId && workspace?.users?.includes(userId))
25
- const allowMedia = access === 'public' || (access === 'restricted' && isMember)
25
+ const inWorkspaceContext = Boolean(
26
+ userId && workspaceId && resource.belongsTo === workspaceId,
27
+ )
28
+ const allowMedia =
29
+ access === 'public' ||
30
+ (access === 'workspace' && inWorkspaceContext) ||
31
+ (access === 'restricted' && isMember)
26
32
 
27
33
  const nextContent = { ...resource.content }
28
34
 
@@ -1,98 +1,59 @@
1
1
  /**
2
- * Database queries related to workspace events
3
- * @class
4
- */
2
+ * ADR 0008 resource lifecycle events.
3
+ */
5
4
  export class ResourcesEvents {
6
5
 
7
- static Created({
8
- aggregateId, //optional
6
+ static Created ({
7
+ resourceId,
8
+ schemaId,
9
9
  createdBy,
10
10
  belongsTo,
11
11
  location,
12
- type,
13
12
  name,
14
- ...specificTypeProps
13
+ access,
14
+ content,
15
+ ...rest
15
16
  }) {
16
- return ({
17
- aggregateId: aggregateId,
18
- type: 'Created',
19
- aggregateType: 'Resource',
20
- createdBy: createdBy,
17
+ return {
18
+ type: schemaId,
19
+ resourceId,
20
+ event: 'Created',
21
+ createdBy,
21
22
  payload: {
22
- ...specificTypeProps,
23
- belongsTo: belongsTo,
24
- location: location,
25
- type: type,
26
- name: name
27
- }
28
- })
23
+ belongsTo,
24
+ location,
25
+ name,
26
+ access,
27
+ content: content ?? {},
28
+ ...rest,
29
+ },
30
+ }
29
31
  }
30
32
 
31
- static ContentUpdated({ createdBy, content }) {
32
- return ({
33
- type: 'ContentUpdated',
34
- createdBy: createdBy,
35
- payload: {
36
- content: content
37
- }
38
- })
39
- }
40
-
41
- static NameUpdated({ createdBy, name }) {
42
- return ({
43
- type: 'NameUpdated',
44
- createdBy: createdBy,
45
- payload: {
46
- name: name
47
- }
48
- })
49
- }
50
-
51
- static AccessUpdated({ createdBy, access }) {
52
- return ({
53
- type: 'AccessUpdated',
54
- createdBy: createdBy,
55
- payload: {
56
- access: access
57
- }
58
- })
59
- }
60
-
61
- static LocationUpdated({ createdBy, location }) {
62
- return ({
63
- type: 'LocationUpdated',
64
- createdBy: createdBy,
65
- payload: {
66
- location: location
67
- }
68
- })
69
- }
70
-
71
- static Deleted({ createdBy }) {
72
- return ({
73
- type: 'Deleted',
74
- createdBy: createdBy
75
- })
33
+ static Patched ({ createdBy, ...partial }) {
34
+ return {
35
+ event: 'Patched',
36
+ createdBy,
37
+ payload: partial,
38
+ }
76
39
  }
77
40
 
78
- // TODO: remove named version concept
79
- static NamedVersionUploaded({
80
- createdBy,
81
- namedVersion,
82
- ContentLength,
83
- ContentType,
84
- Key
85
- }) {
86
- return ({
87
- type: 'NamedVersionUploaded',
88
- createdBy: createdBy,
41
+ static Updated ({ createdBy, content, name }) {
42
+ return {
43
+ event: 'Updated',
44
+ createdBy,
89
45
  payload: {
90
- namedVersion,
91
- Key,
92
- ContentLength,
93
- ContentType,
94
- }
95
- })
46
+ ...(content !== undefined ? { content } : {}),
47
+ ...(name !== undefined ? { name } : {}),
48
+ },
49
+ }
96
50
  }
97
51
 
52
+ static Deleted ({ createdBy }) {
53
+ return {
54
+ event: 'Deleted',
55
+ createdBy,
56
+ payload: {},
57
+ }
58
+ }
98
59
  }
@@ -9,58 +9,58 @@ function workspaceHeaders(userToken, workspaceId) {
9
9
  }
10
10
  }
11
11
 
12
- describe('[resources/create]', () => {
12
+ describe('[resources/actions/create]', () => {
13
13
 
14
14
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
15
- actionId: 'resources/create',
15
+ actionId: '@ossy/resources/actions/create',
16
16
  payload: { type: 'directory', location: '/', name: 'test' },
17
17
  headers: { workspaceId: 'ws-test' },
18
18
  })
19
19
 
20
20
  })
21
21
 
22
- describe('[resources/list]', () => {
22
+ describe('[resources/actions/list]', () => {
23
23
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
24
- actionId: 'resources/list',
24
+ actionId: '@ossy/resources/actions/list',
25
25
  headers: { workspaceId: 'ws-test' },
26
26
  })
27
27
  })
28
28
 
29
- describe('[resources/get]', () => {
29
+ describe('[resources/actions/get]', () => {
30
30
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
31
- actionId: 'resources/get',
31
+ actionId: '@ossy/resources/actions/get',
32
32
  payload: { resourceId: 'r1' },
33
33
  headers: { workspaceId: 'ws-test' },
34
34
  })
35
35
  })
36
36
 
37
- describe('[resources/update-name]', () => {
37
+ describe('[resources/actions/update-name]', () => {
38
38
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
39
- actionId: 'resources/update-name',
39
+ actionId: '@ossy/resources/actions/update-name',
40
40
  payload: { resourceId: 'r1', name: 'x' },
41
41
  headers: { workspaceId: 'ws-test' },
42
42
  })
43
43
  })
44
44
 
45
- describe('[resources/update-location]', () => {
45
+ describe('[resources/actions/update-location]', () => {
46
46
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
47
- actionId: 'resources/update-location',
47
+ actionId: '@ossy/resources/actions/update-location',
48
48
  payload: { resourceId: 'r1', target: '/other/' },
49
49
  headers: { workspaceId: 'ws-test' },
50
50
  })
51
51
  })
52
52
 
53
- describe('[resources/update-content]', () => {
53
+ describe('[resources/actions/update-content]', () => {
54
54
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
55
- actionId: 'resources/update-content',
55
+ actionId: '@ossy/resources/actions/update-content',
56
56
  payload: { resourceId: 'r1', content: {} },
57
57
  headers: { workspaceId: 'ws-test' },
58
58
  })
59
59
  })
60
60
 
61
- describe('[resources/delete]', () => {
61
+ describe('[resources/actions/delete]', () => {
62
62
  TestUtil.AssertWorkspaceActionAuthenticationNeeded({
63
- actionId: 'resources/delete',
63
+ actionId: '@ossy/resources/actions/delete',
64
64
  payload: { resourceId: 'r1' },
65
65
  headers: { workspaceId: 'ws-test' },
66
66
  })
@@ -70,7 +70,7 @@ describe('E2E: template-backed resource (workspaceId header)', () => {
70
70
  it('create → update content → delete', async () => {
71
71
  const user = await TestUtil.GetAuthenticatedTestUser()
72
72
  const workspace = await TestUtil.InvokeAction({
73
- actionId: 'workspaces/create',
73
+ actionId: '@ossy/workspaces/actions/create',
74
74
  headers: { 'Content-Type': 'application/json', Authorization: user.token },
75
75
  payload: { name: casual.word },
76
76
  }).then(r => r.json())
@@ -88,7 +88,7 @@ describe('E2E: template-backed resource (workspaceId header)', () => {
88
88
  }
89
89
 
90
90
  const createRes = await TestUtil.InvokeAction({
91
- actionId: 'resources/create',
91
+ actionId: '@ossy/resources/actions/create',
92
92
  headers: workspaceHeaders(user.token, workspace.id),
93
93
  payload: createBody,
94
94
  })
@@ -103,7 +103,7 @@ describe('E2E: template-backed resource (workspaceId header)', () => {
103
103
  })
104
104
 
105
105
  const updateRes = await TestUtil.InvokeAction({
106
- actionId: 'resources/update-content',
106
+ actionId: '@ossy/resources/actions/update-content',
107
107
  headers: workspaceHeaders(user.token, workspace.id),
108
108
  payload: {
109
109
  resourceId: created.id,
@@ -124,7 +124,7 @@ describe('E2E: template-backed resource (workspaceId header)', () => {
124
124
  })
125
125
 
126
126
  const deleteRes = await TestUtil.InvokeAction({
127
- actionId: 'resources/delete',
127
+ actionId: '@ossy/resources/actions/delete',
128
128
  headers: workspaceHeaders(user.token, workspace.id),
129
129
  payload: { resourceId: created.id },
130
130
  })
@@ -134,7 +134,7 @@ describe('E2E: template-backed resource (workspaceId header)', () => {
134
134
  expect(deleteBody).toEqual({ ok: true })
135
135
 
136
136
  const listRes = await TestUtil.InvokeAction({
137
- actionId: 'resources/list',
137
+ actionId: '@ossy/resources/actions/list',
138
138
  headers: workspaceHeaders(user.token, workspace.id),
139
139
  payload: { query: { location: '/' } },
140
140
  })
@@ -36,7 +36,7 @@ export function accessFilter(user) {
36
36
  if (!user?.anonymous) {
37
37
  if (user?.workspaces?.length) {
38
38
  conditions.push({
39
- 'state.access': { $in: ['public', 'workspace'] },
39
+ 'state.access': { $in: ['public', 'workspace', 'restricted'] },
40
40
  'state.belongsTo': { $in: user.workspaces },
41
41
  })
42
42
  }
@@ -73,7 +73,10 @@ export class ResourcesQueries {
73
73
  const workspaceScope = query['state.belongsTo'] ?? '(no workspace scope)'
74
74
  log.info(`[ResourcesQueries][GetResources] Fetching resources for ${workspaceScope}`)
75
75
 
76
- const baseQuery = { ...query, type: 'Resource' }
76
+ const baseQuery = {
77
+ ...query,
78
+ type: { $regex: /^@[^/]+\/[^/]+\/schema\// },
79
+ }
77
80
  const mongoQuery = user ? { $and: [baseQuery, accessFilter(user)] } : baseQuery
78
81
 
79
82
  return Aggregate.Collection.find(mongoQuery, { state: true }).toArray()
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/search', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/search', access: 'workspace' }
@@ -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/search' }
4
+ export const metadata = { id: '@ossy/resources/tasks/search' }
5
5
 
6
6
  export async function run({ payload, req }) {
7
7
  const query = { ...(payload ?? {}) }
package/src/server.js CHANGED
@@ -1,4 +1,5 @@
1
- export * from './resource.aggregate.js'
1
+ export * from './resource-stream.js'
2
+ export * from './resource-stream.helpers.js'
2
3
  export * from './resources.events.js'
3
4
  export * from './resources.queries.js'
4
5
  export { attachResourceMediaUrls } from './resources.attach-media-urls.js'
@@ -7,30 +7,30 @@
7
7
  "resources.form.selectType": "Välj en resurstyp för att fortsätta.",
8
8
  "resources.form.createFailed": "Skapandet misslyckades",
9
9
  "resources.form.createResource": "Skapa resurs",
10
- "resources/create.label": "Skapa resurs",
11
- "resources/create.description": "Skapa en ny resurs i workspace",
12
- "resources/list.label": "Lista resurser",
13
- "resources/list.description": "Lista resurser på en plats i workspace",
14
- "resources/get.label": "Hämta resurs",
15
- "resources/get.description": "Hämta en enskild resurs via id",
16
- "resources/search.label": "Sök resurser",
17
- "resources/search.description": "Sök resurser i en workspace",
18
- "resources/delete.label": "Ta bort resurs",
19
- "resources/delete.description": "Ta bort en resurs från workspace",
20
- "resources/update-name.label": "Uppdatera resursnamn",
21
- "resources/update-name.description": "Byt namn på en resurs i workspace",
22
- "resources/update-content.label": "Uppdatera resursinnehåll",
23
- "resources/update-content.description": "Uppdatera innehållet i en resurs",
24
- "resources/update-location.label": "Uppdatera resursplats",
25
- "resources/update-location.description": "Flytta en resurs till en ny plats",
26
- "resources/update-access.label": "Uppdatera resursåtkomst",
27
- "resources/update-access.description": "Ändra åtkomstinställningar för en resurs",
28
- "resources/upload-named-version.label": "Ladda upp namngiven version",
29
- "resources/upload-named-version.description": "Ladda upp en namngiven version av en resursfil",
30
- "resources/create-page-view.label": "Skapa sidvisning",
31
- "resources/create-page-view.description": "Registrera en sidvisningshändelse för analys",
32
- "resources/get-page-view-stats.label": "Hämta sidvisningsstatistik",
33
- "resources/get-page-view-stats.description": "Hämta aggregerad statistik för sidvisningar",
10
+ "@ossy/resources/actions/create.label": "Skapa resurs",
11
+ "@ossy/resources/actions/create.description": "Skapa en ny resurs i workspace",
12
+ "@ossy/resources/actions/list.label": "Lista resurser",
13
+ "@ossy/resources/actions/list.description": "Lista resurser på en plats i workspace",
14
+ "@ossy/resources/actions/get.label": "Hämta resurs",
15
+ "@ossy/resources/actions/get.description": "Hämta en enskild resurs via id",
16
+ "@ossy/resources/actions/search.label": "Sök resurser",
17
+ "@ossy/resources/actions/search.description": "Sök resurser i en workspace",
18
+ "@ossy/resources/actions/delete.label": "Ta bort resurs",
19
+ "@ossy/resources/actions/delete.description": "Ta bort en resurs från workspace",
20
+ "@ossy/resources/actions/update-name.label": "Uppdatera resursnamn",
21
+ "@ossy/resources/actions/update-name.description": "Byt namn på en resurs i workspace",
22
+ "@ossy/resources/actions/update-content.label": "Uppdatera resursinnehåll",
23
+ "@ossy/resources/actions/update-content.description": "Uppdatera innehållet i en resurs",
24
+ "@ossy/resources/actions/update-location.label": "Uppdatera resursplats",
25
+ "@ossy/resources/actions/update-location.description": "Flytta en resurs till en ny plats",
26
+ "@ossy/resources/actions/update-access.label": "Uppdatera resursåtkomst",
27
+ "@ossy/resources/actions/update-access.description": "Ändra åtkomstinställningar för en resurs",
28
+ "@ossy/resources/actions/upload-named-version.label": "Ladda upp namngiven version",
29
+ "@ossy/resources/actions/upload-named-version.description": "Ladda upp en namngiven version av en resursfil",
30
+ "@ossy/resources/actions/create-page-view.label": "Skapa sidvisning",
31
+ "@ossy/resources/actions/create-page-view.description": "Registrera en sidvisningshändelse för analys",
32
+ "@ossy/resources/actions/get-page-view-stats.label": "Hämta sidvisningsstatistik",
33
+ "@ossy/resources/actions/get-page-view-stats.description": "Hämta aggregerad statistik för sidvisningar",
34
34
  "resources/create/generic.documentTitle": "Skapa resurs",
35
35
  "create-directory.documentTitle": "Skapa mapp",
36
36
  "create-document.documentTitle": "Skapa dokument",
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/update-access', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/update-access', access: 'workspace' }
@@ -1,13 +1,13 @@
1
- import { Aggregate } from '@ossy/event-store'
2
- import { Resource } from './resource.aggregate.js'
3
1
  import { ResourcesEvents } from './resources.events.js'
2
+ import { mutateResource } from './resource-stream.helpers.js'
3
+ import { resolveResourceId } from './resource-read.helpers.js'
4
4
  import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
5
5
 
6
- export const metadata = { id: 'resources/update-access' }
6
+ export const metadata = { id: '@ossy/resources/tasks/update-access' }
7
7
 
8
- export async function run({ payload, req }) {
8
+ export async function run ({ payload, req }) {
9
9
  const createdBy = payload?.userId ?? req?.userId
10
- const resourceId = payload?.resourceId ?? req?.params?.resourceId
10
+ const resourceId = resolveResourceId({ payload, req })
11
11
  const resourceAccess = payload?.access
12
12
 
13
13
  if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
@@ -15,8 +15,9 @@ export async function run({ payload, req }) {
15
15
  throw Object.assign(new Error(`Invalid access value: ${resourceAccess}`), { status: 400 })
16
16
  }
17
17
 
18
- const resource = await Aggregate.Of(Resource, resourceId)
19
- .then(Aggregate.Add(ResourcesEvents.AccessUpdated({ createdBy, access: resourceAccess })))
20
- .then(Aggregate.View())
18
+ const resource = await mutateResource(
19
+ resourceId,
20
+ ResourcesEvents.Patched({ createdBy, access: resourceAccess }),
21
+ )
21
22
  return withResourceMedia(resource, mediaContextFromRun({ payload, req }))
22
23
  }
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/update-content', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/update-content', access: 'workspace' }
@@ -1,41 +1,39 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
- import { Resource } from './resource.aggregate.js'
3
- import { ResourcesEvents } from './resources.events.js'
4
2
  import { Workspace } from '@ossy/workspaces/server'
5
- import { getSystemResourceTemplates, normalizeAndValidateDocumentContent } from '@ossy/platform'
3
+ import { schemaForWorkspace } from '@ossy/platform'
4
+ import { ResourcesEvents } from './resources.events.js'
5
+ import { mutateResource, viewResource } from './resource-stream.helpers.js'
6
6
  import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
7
+ import { resolveResourceId } from './resource-read.helpers.js'
7
8
 
8
- export const metadata = { id: 'resources/update-content' }
9
+ export const metadata = { id: '@ossy/resources/tasks/update-content' }
9
10
 
10
- export async function run({ payload, req }) {
11
+ export async function run ({ payload, req }) {
11
12
  const createdBy = payload?.userId ?? req?.userId
12
- const resourceId = payload?.resourceId ?? req?.params?.resourceId
13
+ const resourceId = resolveResourceId({ payload, req })
13
14
  const content = payload?.content
14
15
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
15
16
 
16
17
  if (!resourceId) throw Object.assign(new Error('resourceId is required'), { status: 400 })
17
18
 
18
19
  const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
20
+ const current = await viewResource(resourceId)
21
+
22
+ const engine = schemaForWorkspace(workspace.schemas)
23
+ const schema = engine.has(current.type) ? engine.resolve(current.type) : null
24
+ let finalContent = content
25
+ if (schema) {
26
+ const result = engine.validate(schema, content)
27
+ if (!result.ok) {
28
+ const first = result.errors[0]
29
+ throw Object.assign(new Error(first.message), { status: 400, type: first.code })
30
+ }
31
+ finalContent = result.data
32
+ }
19
33
 
20
- return Aggregate.Of(Resource, resourceId)
21
- .then(aggregate => {
22
- const resource = Aggregate.View()(aggregate)
23
- const systemTemplates = getSystemResourceTemplates()
24
- const allTemplates = [
25
- ...systemTemplates,
26
- ...(workspace.resourceTemplates || []).filter(t => !systemTemplates.find(s => s.id === t.id)),
27
- ]
28
- const template = allTemplates.find(t => t.id === resource.type)
29
- let finalContent = content
30
- if (template) {
31
- const normalized = normalizeAndValidateDocumentContent(content, template)
32
- if (!normalized.ok) {
33
- throw Object.assign(new Error(normalized.message), { status: 400, type: normalized.code })
34
- }
35
- finalContent = normalized.content
36
- }
37
- return Aggregate.Add(ResourcesEvents.ContentUpdated({ createdBy, content: finalContent }))(aggregate)
38
- })
39
- .then(Aggregate.View())
40
- .then(resource => withResourceMedia(resource, mediaContextFromRun({ payload, req })))
34
+ const updated = await mutateResource(
35
+ resourceId,
36
+ ResourcesEvents.Updated({ createdBy, content: finalContent }),
37
+ )
38
+ return withResourceMedia(updated, mediaContextFromRun({ payload, req }))
41
39
  }
@@ -1 +1 @@
1
- export const metadata = { id: 'resources/update-location', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/resources/actions/update-location', access: 'workspace' }