@ossy/resources 1.11.7 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +10 -4
  2. package/src/AudioResource.jsx +4 -2
  3. package/src/CreateDirectory.jsx +14 -13
  4. package/src/CreateDocument.jsx +7 -5
  5. package/src/Definition.js +2 -8
  6. package/src/DocumentEdit.jsx +9 -6
  7. package/src/DocumentView.jsx +8 -4
  8. package/src/GenericResourceCard.jsx +4 -2
  9. package/src/GenericResourceDetail.jsx +9 -4
  10. package/src/GenericResourceForm.jsx +14 -7
  11. package/src/ImageResource.jsx +4 -2
  12. package/src/PDFResource.jsx +4 -2
  13. package/src/ResourceContentPage.jsx +4 -2
  14. package/src/ResourceDescription.jsx +4 -3
  15. package/src/ResourceDetails.jsx +9 -4
  16. package/src/ResourceDialogMove.jsx +4 -3
  17. package/src/ResourceFactory.jsx +6 -3
  18. package/src/ResourceGenericView.jsx +4 -2
  19. package/src/ResourceList.jsx +4 -2
  20. package/src/ResourcePanel.jsx +9 -12
  21. package/src/ResourceTags.jsx +4 -3
  22. package/src/ResourcesPage.jsx +8 -6
  23. package/src/UploadResources.jsx +9 -11
  24. package/src/VideoResource.jsx +4 -2
  25. package/src/create-page-view.action.js +1 -38
  26. package/src/create-page-view.task.js +38 -0
  27. package/src/create.action.js +1 -98
  28. package/src/create.task.js +100 -0
  29. package/src/delete.action.js +1 -31
  30. package/src/delete.task.js +31 -0
  31. package/src/en.translations.json +48 -0
  32. package/src/get-page-view-stats.action.js +1 -65
  33. package/src/get-page-view-stats.task.js +64 -0
  34. package/src/get.action.js +1 -14
  35. package/src/get.task.js +14 -0
  36. package/src/index.js +13 -4
  37. package/src/list.action.js +1 -19
  38. package/src/list.task.js +20 -0
  39. package/src/resource-create.page.jsx +7 -5
  40. package/src/resource-detail.page.jsx +0 -2
  41. package/src/resource.helpers.js +83 -0
  42. package/src/resources.action-media.js +18 -0
  43. package/src/resources.attach-media-urls.js +1 -1
  44. package/src/resources.events.js +1 -0
  45. package/src/resources.spec.js +54 -55
  46. package/src/search.action.js +1 -19
  47. package/src/search.task.js +20 -0
  48. package/src/server.js +4 -0
  49. package/src/sv.translations.json +48 -0
  50. package/src/update-access.action.js +1 -20
  51. package/src/update-access.task.js +22 -0
  52. package/src/update-content.action.js +1 -39
  53. package/src/update-content.task.js +41 -0
  54. package/src/update-location.action.js +1 -18
  55. package/src/update-location.task.js +20 -0
  56. package/src/update-name.action.js +1 -18
  57. package/src/update-name.task.js +20 -0
  58. package/src/upload-named-version.action.js +1 -42
  59. package/src/upload-named-version.task.js +46 -0
  60. package/src/item-access.api.js +0 -34
  61. package/src/item-content.api.js +0 -35
  62. package/src/item-location.api.js +0 -34
  63. package/src/item-name.api.js +0 -34
  64. package/src/item-version.api.js +0 -48
  65. package/src/item.api.js +0 -58
  66. package/src/list.api.js +0 -43
  67. package/src/page-views-stats.api.js +0 -23
  68. package/src/page-views.api.js +0 -23
  69. package/src/search.api.js +0 -27
@@ -1,9 +1,9 @@
1
1
  import React from 'react'
2
2
  import { SDK } from '@ossy/sdk'
3
- import { useResources, AsyncStatus, WorkspaceProvider } from '@ossy/sdk-react'
4
- import { Title, Text, Switch, View, ImageCard, Tags } from '@ossy/design-system'
3
+ import { metadata as ListResources } from './list.action.js'
4
+ import { AsyncStatus, WorkspaceProvider, useSdk } from '@ossy/sdk-react'
5
+ import { Title, Text, Switch, View, ImageCard, Tags, useLocale } from '@ossy/design-system'
5
6
  import { useRouter } from '@ossy/router-react'
6
- import { Definition } from './Definition.js'
7
7
 
8
8
  const sdk = SDK.of({
9
9
  /** Ossy.se workspaceID - used to fetch free resources */
@@ -18,7 +18,9 @@ const freeResourcesGridStyles = {
18
18
 
19
19
  export const ResourcesPage= () => {
20
20
  const router = useRouter()
21
- const { status, resources } = useResources('/publications/images/')
21
+ const { t } = useLocale()
22
+ const sdk = useSdk()
23
+ const { status, data: resources = [] } = sdk.read(ListResources, { location: '/publications/images/' })
22
24
  const tags = resources.flatMap(resource => resource?.content?.tags || [])
23
25
  const tagsCount = tags.reduce((acc, t) => ({ ...acc, [t]: (acc?.[t] ?? 0) + 1 }), {})
24
26
 
@@ -34,8 +36,8 @@ export const ResourcesPage= () => {
34
36
  <View gap="l" style={{ margin: '0 auto' }}>
35
37
 
36
38
  <View gap="m">
37
- <Title>{Definition.title}</Title>
38
- <Text style={{ maxWidth: '900px'}}>{Definition.description}</Text>
39
+ <Title>{t('resources.home.title')}</Title>
40
+ <Text style={{ maxWidth: '900px'}}>{t('resources.home.description')}</Text>
39
41
  </View>
40
42
 
41
43
  <WorkspaceProvider sdk={sdk}>
@@ -1,31 +1,29 @@
1
1
  import React from 'react'
2
- import { useResources } from '@ossy/sdk-react'
3
- import { Title, View, Text, Upload as _Upload } from '@ossy/design-system'
2
+ import { uploadFile } from './resource.helpers.js'
3
+ import { useSdk } from '@ossy/sdk-react'
4
+ import { Title, View, Text, useLocale } from '@ossy/design-system'
4
5
  import { useRouter } from '@ossy/router-react'
5
6
  import { Upload } from './Upload.jsx'
6
7
 
7
8
  export const UploadResources = () => {
9
+ const { t } = useLocale()
8
10
  const router = useRouter()
11
+ const sdk = useSdk()
9
12
  const location = router.searchParams.location
10
- const { uploadFile } = useResources()
11
13
 
12
14
  return (
13
15
  <View gap="s" style={{ height: '100%' }}>
14
-
15
16
  <View gap="s">
16
- <Title variant="tertiary" style={{ marginBottom: '0' }}>
17
- Upload
18
- </Title>
17
+ <Title variant="tertiary" style={{ marginBottom: '0' }}>{t('resources.upload.title')}</Title>
19
18
  <View layout="row" gap="s">
20
- <Text variant="small" >Location: {location}</Text>
19
+ <Text variant="small">{t('resources.upload.location', { location })}</Text>
21
20
  </View>
22
21
  </View>
23
-
24
22
  <Upload
25
- onUpload={(file) => uploadFile(location, file)}
23
+ onUpload={(file) => uploadFile(sdk, location, file)}
26
24
  onCancel={() => router.navigate(`@storage/home?location=${location}`)}
27
25
  onDone={() => router.navigate(`@storage/home?location=${location}`)}
28
26
  />
29
27
  </View>
30
28
  )
31
- }
29
+ }
@@ -1,11 +1,13 @@
1
1
  import React from 'react'
2
- import { useResource } from '@ossy/sdk-react'
2
+ import { metadata as GetResource } from './get.action.js'
3
+ import { useSdk } from '@ossy/sdk-react'
3
4
  import { View } from '@ossy/design-system'
4
5
 
5
6
  export const VideoResource = ({
6
7
  resourceId
7
8
  }) => {
8
- const { resource } = useResource(resourceId)
9
+ const sdk = useSdk()
10
+ const { data: resource } = sdk.read(GetResource, { id: resourceId }, { enabled: !!resourceId })
9
11
 
10
12
  return (
11
13
  <View stack bordered>
@@ -1,38 +1 @@
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
- }
1
+ export const metadata = { id: 'resources/create-page-view', access: 'workspace' }
@@ -0,0 +1,38 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Resource } from './resource.aggregate.js'
3
+ import { ResourcesEvents } from './resources.events.js'
4
+ import { Workspace } from '@ossy/workspaces/server'
5
+ import { normalizeAndValidateDocumentContent } from '@ossy/platform'
6
+
7
+ export const metadata = { id: 'resources/create-page-view' }
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
+ }
@@ -1,98 +1 @@
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
- }
1
+ export const metadata = { id: 'resources/create', access: 'workspace' }
@@ -0,0 +1,100 @@
1
+ import { is } from 'ramda'
2
+ import { nanoid } from 'nanoid'
3
+ import { Aggregate } from '@ossy/event-store'
4
+ import { Resource } from './resource.aggregate.js'
5
+ import { ResourcesEvents } from './resources.events.js'
6
+ import { Workspace } from '@ossy/workspaces/server'
7
+ import { getSystemResourceTemplates, normalizeAndValidateDocumentContent } from '@ossy/platform'
8
+ import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
9
+
10
+ export const metadata = { id: 'resources/create' }
11
+
12
+ const NativeResourceTypes = { DIRECTORY: 'directory' }
13
+
14
+ export async function run({ payload, integrations, req }) {
15
+ const createdBy = payload?.userId ?? req?.userId
16
+ const workspaceId = payload?.workspaceId ?? req?.workspaceId
17
+
18
+ const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
19
+ let location = payload?.location
20
+ const type = payload?.type
21
+ const name = payload?.name
22
+ const fileExtension = name?.split?.('.')?.pop?.()
23
+ const content = payload?.content
24
+ const size = payload?.size
25
+
26
+ if (!is(String, location)) {
27
+ throw Object.assign(new Error(`Not a valid location path: ${location}`), { status: 400, type: 'INVALID_PATH' })
28
+ }
29
+
30
+ location = location.endsWith('/') ? location : location + '/'
31
+ location = location.startsWith('/') ? location : '/' + location
32
+
33
+ const systemTemplates = getSystemResourceTemplates()
34
+ const allTemplates = [
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)
39
+
40
+ if (type === NativeResourceTypes.DIRECTORY) {
41
+ const event = ResourcesEvents.Created({
42
+ type: NativeResourceTypes.DIRECTORY,
43
+ createdBy,
44
+ belongsTo: workspace.id,
45
+ location,
46
+ name,
47
+ access: 'workspace',
48
+ })
49
+ return Aggregate.Of(Resource, event).then(Aggregate.View())
50
+ }
51
+
52
+ if (isResourceTemplateType) {
53
+ const template = allTemplates.find(t => t.id === type)
54
+ const normalized = normalizeAndValidateDocumentContent(content, template)
55
+ if (!normalized.ok) {
56
+ throw Object.assign(new Error(normalized.message), { status: 400, type: normalized.code })
57
+ }
58
+ const event = ResourcesEvents.Created({
59
+ type,
60
+ createdBy,
61
+ belongsTo: workspace.id,
62
+ location,
63
+ name,
64
+ content: normalized.content,
65
+ })
66
+ return Aggregate.Of(Resource, event).then(Aggregate.View())
67
+ }
68
+
69
+ if (!fileExtension) {
70
+ throw Object.assign(new Error('Missing valid file extension'), { status: 400 })
71
+ }
72
+
73
+ const aggregateId = nanoid()
74
+ const event = ResourcesEvents.Created({
75
+ aggregateId,
76
+ type,
77
+ createdBy,
78
+ belongsTo: workspace.id,
79
+ location,
80
+ name,
81
+ content: {
82
+ Key: `media/${workspace.id}/${aggregateId}/original.${fileExtension}`,
83
+ ContentLength: size,
84
+ ContentType: type,
85
+ },
86
+ })
87
+
88
+ const storageClient = integrations?.get?.('storage')
89
+ const uploadHeaders = {
90
+ ContentLength: event.payload.content.ContentLength,
91
+ ContentType: event.payload.content.ContentType,
92
+ Key: event.payload.content.Key,
93
+ }
94
+ const uploadUrl = storageClient ? await storageClient.createUploadUrl(uploadHeaders) : null
95
+ const resource = await Aggregate.Of(Resource, event).then(Aggregate.View())
96
+ const withUpload = uploadUrl
97
+ ? { ...resource, content: { ...resource.content, uploadUrl } }
98
+ : resource
99
+ return withResourceMedia(withUpload, mediaContextFromRun({ payload, req }))
100
+ }
@@ -1,31 +1 @@
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
- }
1
+ export const metadata = { id: 'resources/delete', access: 'workspace' }
@@ -0,0 +1,31 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Resource } from './resource.aggregate.js'
3
+ import { ResourcesEvents } from './resources.events.js'
4
+ import { ResourcesQueries } from './resources.queries.js'
5
+
6
+ export const metadata = { id: 'resources/delete' }
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,48 @@
1
+ {
2
+ "resources/home.documentTitle": "Free Resources",
3
+ "resources.home.title": "Free Resources",
4
+ "resources.home.description": "We believe in providing value at every step of your journey with us. That's why we've curated a collection of free resources. Whether you're looking to enhance your creative projects or simply seeking inspiration, explore the gallery below to discover valuable assets that align with your vision. We hope you find them useful, and we're here if you have any questions!",
5
+ "resources.form.nameRequired": "Name is required",
6
+ "resources.form.untitled": "Untitled",
7
+ "resources.form.selectType": "Select a resource type to continue.",
8
+ "resources.form.createFailed": "Create failed",
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",
34
+ "resources/create/generic.documentTitle": "Create resource",
35
+ "create-directory.documentTitle": "Create directory",
36
+ "create-document.documentTitle": "Create document",
37
+ "upload-media.documentTitle": "Upload",
38
+ "@resource.documentTitle": "Resource",
39
+ "resources.upload.title": "Upload",
40
+ "resources.upload.location": "Location: {location}",
41
+ "resources.createDirectory.title": "Create directory",
42
+ "resources.createDocument.title": "Create document",
43
+ "resources.createDirectory.description": "Create a new directory to organize your resources. The new directory will be created in the location: {location}",
44
+ "resources.createDirectory.namePlaceholder": "Directory name",
45
+ "resources.createDirectory.submit": "Create directory",
46
+ "resources.createDirectory.errorEmpty": "Directory name cannot be empty",
47
+ "resources.createDirectory.errorOssyPrefix": "Directory name cannot start with @ossy"
48
+ }
@@ -1,65 +1 @@
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
- }
1
+ export const metadata = { id: 'resources/get-page-view-stats', access: 'workspace' }
@@ -0,0 +1,64 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Workspace } from '@ossy/workspaces/server'
3
+
4
+ export const metadata = { id: 'resources/get-page-view-stats' }
5
+
6
+ const PAGE_VIEW_TEMPLATE_ID = '@ossy/web/page-view'
7
+ const PAGE_VIEW_LOCATION = '/@ossy/analytics/page-views/'
8
+
9
+ function parseMillis(input, fallback) {
10
+ if (input === undefined || input === null || input === '') return fallback
11
+ const n = Number(input)
12
+ if (!Number.isFinite(n) || n <= 0) return fallback
13
+ return n
14
+ }
15
+
16
+ export async function run({ payload, req }) {
17
+ const workspaceId = payload?.workspaceId ?? req?.workspaceId
18
+ if (!workspaceId) throw Object.assign(new Error('workspaceId is required'), { status: 400 })
19
+
20
+ const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
21
+
22
+ const now = Date.now()
23
+ const defaultFrom = now - (30 * 24 * 60 * 60 * 1000)
24
+ const from = parseMillis(payload?.from ?? req?.query?.from, defaultFrom)
25
+ const to = parseMillis(payload?.to ?? req?.query?.to, now)
26
+ const topLimit = Math.min(Math.max(Number(payload?.limit ?? req?.query?.limit ?? 10), 1), 50)
27
+
28
+ const baseMatch = {
29
+ type: 'Resource',
30
+ 'state.status': { $ne: 'removed' },
31
+ 'state.belongsTo': workspace.id,
32
+ 'state.type': PAGE_VIEW_TEMPLATE_ID,
33
+ 'state.location': PAGE_VIEW_LOCATION,
34
+ 'state.content.eventAt': { $gte: from, $lte: to },
35
+ }
36
+
37
+ const [dailyViews, topPaths, totals] = await Promise.all([
38
+ Aggregate.Collection.aggregate([
39
+ { $match: baseMatch },
40
+ { $project: { date: { $dateToString: { format: '%Y-%m-%d', date: { $toDate: '$state.content.eventAt' } } } } },
41
+ { $group: { _id: '$date', views: { $sum: 1 } } },
42
+ { $sort: { _id: 1 } },
43
+ ]).toArray(),
44
+ Aggregate.Collection.aggregate([
45
+ { $match: baseMatch },
46
+ { $group: { _id: '$state.content.path', views: { $sum: 1 } } },
47
+ { $sort: { views: -1 } },
48
+ { $limit: topLimit },
49
+ { $project: { _id: 0, path: '$_id', views: 1 } },
50
+ ]).toArray(),
51
+ Aggregate.Collection.aggregate([
52
+ { $match: baseMatch },
53
+ { $count: 'views' },
54
+ ]).toArray(),
55
+ ])
56
+
57
+ return {
58
+ from,
59
+ to,
60
+ totalViews: totals?.[0]?.views || 0,
61
+ dailyViews: dailyViews.map(x => ({ date: x._id, views: x.views })),
62
+ topPaths,
63
+ }
64
+ }
package/src/get.action.js CHANGED
@@ -1,14 +1 @@
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
- }
1
+ export const metadata = { id: 'resources/get', access: 'workspace' }
@@ -0,0 +1,14 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Resource } from './resource.aggregate.js'
3
+ import { mediaContextFromRun, withResourceMedia } from './resources.action-media.js'
4
+
5
+ export const metadata = { id: 'resources/get' }
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 withResourceMedia(resource, mediaContextFromRun({ payload, req }))
14
+ }