@ossy/platform 3.7.0 → 3.8.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/platform",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Ossy application server runtime",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,16 +45,16 @@
45
45
  "@aws-sdk/util-format-url": "^3.972.17",
46
46
  "@modelcontextprotocol/sdk": "^1.12.1",
47
47
  "@ossy/config": "^3.0.9",
48
- "@ossy/event-store": "^3.7.0",
48
+ "@ossy/event-store": "^3.8.0",
49
49
  "@ossy/locale": "^3.4.0",
50
- "@ossy/manifest": "^3.6.1",
50
+ "@ossy/manifest": "^3.8.0",
51
51
  "@ossy/observability": "^3.0.9",
52
52
  "@ossy/policies": "^3.0.9",
53
- "@ossy/schema": "^3.6.1",
53
+ "@ossy/schema": "^3.8.0",
54
54
  "@ossy/sdk": "^3.5.0",
55
55
  "@ossy/tokens": "^3.5.0",
56
- "@ossy/users": "^3.6.1",
57
- "@ossy/workspaces": "^3.6.1",
56
+ "@ossy/users": "^3.8.0",
57
+ "@ossy/workspaces": "^3.8.0",
58
58
  "cookie-parser": "^1.4.7",
59
59
  "dotenv": ">=16.0.0 <18.0.0",
60
60
  "express": ">=5.0.0 <6.0.0",
@@ -75,5 +75,5 @@
75
75
  "Dockerfile",
76
76
  "docker-healthcheck.js"
77
77
  ],
78
- "gitHead": "622d97535abad08415963405ace2f5aaaecd7ca4"
78
+ "gitHead": "14548dfc2019e7258e8c75db527acbf66fe829bd"
79
79
  }
@@ -15,6 +15,15 @@ import { rejectOnAbort } from '../request-diagnostics.js'
15
15
 
16
16
  const log = createLogger('platform/task-run')
17
17
 
18
+ /** Workspace scope must be on every TaskRun event so list projections can Apply it. */
19
+ function taskRunScopePayload ({ taskId, workspaceId }) {
20
+ return {
21
+ taskId,
22
+ moduleId: moduleIdFromTaskId(taskId),
23
+ belongsTo: workspaceId,
24
+ }
25
+ }
26
+
18
27
  function assertNotAborted (context) {
19
28
  if (context.signal?.aborted) {
20
29
  throw context.signal.reason instanceof Error
@@ -91,6 +100,8 @@ export const TaskRunService = {
91
100
  const durationMs = Date.now() - startMs
92
101
  await TaskRunService._appendCompleted({
93
102
  runId,
103
+ taskId,
104
+ workspaceId,
94
105
  actorId,
95
106
  durationMs,
96
107
  resultSummary: summarizeResult(result),
@@ -114,6 +125,8 @@ export const TaskRunService = {
114
125
  const durationMs = Date.now() - startMs
115
126
  await TaskRunService._appendFailed({
116
127
  runId,
128
+ taskId,
129
+ workspaceId,
117
130
  actorId,
118
131
  durationMs,
119
132
  error: summarizeError(error),
@@ -180,26 +193,34 @@ export const TaskRunService = {
180
193
  }
181
194
  },
182
195
 
183
- async _appendCompleted ({ runId, actorId, durationMs, resultSummary }) {
196
+ async _appendCompleted ({ runId, taskId, workspaceId, actorId, durationMs, resultSummary }) {
184
197
  try {
185
198
  await Aggregate.Of(TaskRun, runId)
186
199
  .then(Aggregate.Add({
187
200
  event: 'Completed',
188
201
  createdBy: actorId ?? undefined,
189
- payload: { durationMs, resultSummary },
202
+ payload: {
203
+ durationMs,
204
+ resultSummary,
205
+ ...taskRunScopePayload({ taskId, workspaceId }),
206
+ },
190
207
  }))
191
208
  } catch (err) {
192
209
  log.warn('[TaskRunService] Failed to record Completed', { runId }, err)
193
210
  }
194
211
  },
195
212
 
196
- async _appendFailed ({ runId, actorId, durationMs, error }) {
213
+ async _appendFailed ({ runId, taskId, workspaceId, actorId, durationMs, error }) {
197
214
  try {
198
215
  await Aggregate.Of(TaskRun, runId)
199
216
  .then(Aggregate.Add({
200
217
  event: 'Failed',
201
218
  createdBy: actorId ?? undefined,
202
- payload: { durationMs, error },
219
+ payload: {
220
+ durationMs,
221
+ error,
222
+ ...taskRunScopePayload({ taskId, workspaceId }),
223
+ },
203
224
  }))
204
225
  } catch (err) {
205
226
  log.warn('[TaskRunService] Failed to record Failed', { runId }, err)
package/src/server.js CHANGED
@@ -495,6 +495,8 @@ export async function startServer (options = {}) {
495
495
  } = resolvePageLayoutRender(layoutsById, pageEntry)
496
496
  const { language, messages, fallbackMessages } = resolveRequestLocale(pageRouter, requestUrl, buildDir, config)
497
497
  let schemasForPage = cloneSerializable(manifest.schemas || [])
498
+ let ssrWorkspaceName
499
+ let ssrWorkspaceServices
498
500
  if (req.workspaceId && loadWorkspaceForEntitlements) {
499
501
  const workspaceTimer = createTimedOperation(`SSR loadWorkspace ${req.workspaceId}`, { log })
500
502
  try {
@@ -506,6 +508,8 @@ export async function startServer (options = {}) {
506
508
  cloneSerializable(workspace.schemas),
507
509
  )
508
510
  }
511
+ ssrWorkspaceName = workspace?.name
512
+ ssrWorkspaceServices = workspace?.services
509
513
  } catch (err) {
510
514
  workspaceTimer.finish({
511
515
  pageId: pageRoute.id,
@@ -527,6 +531,9 @@ export async function startServer (options = {}) {
527
531
  const props = {
528
532
  ...config,
529
533
  ...(req.userAppSettings || {}),
534
+ ...(req.workspaceId ? { workspaceId: req.workspaceId } : {}),
535
+ ...(ssrWorkspaceName != null ? { workspaceName: ssrWorkspaceName } : {}),
536
+ ...(ssrWorkspaceServices != null ? { workspaceServices: cloneSerializable(ssrWorkspaceServices) } : {}),
530
537
  theme: req.userAppSettings?.theme || cloneSerializable(config?.theme),
531
538
  themes: cloneSerializable(config?.themes),
532
539
  schemas: schemasForPage,
@@ -6,6 +6,24 @@
6
6
  */
7
7
 
8
8
  const KEY_PATTERN = /^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)?$/
9
+ const RESOURCE_ID_PATTERN = /^[A-Za-z0-9_-]+$/
10
+
11
+ export function isStorageKey (key) {
12
+ return typeof key === 'string' && KEY_PATTERN.test(key)
13
+ }
14
+
15
+ /**
16
+ * Map a stored `content.Key` to a logical key for `/r/{id}` URLs.
17
+ * Legacy S3 paths (`media/{workspaceId}/{resourceId}/original.ext`) fall back
18
+ * to `resourceId` so folder list does not 500 after leftover File docs appear.
19
+ */
20
+ export function coerceStorageKey (key, fallbackResourceId) {
21
+ if (isStorageKey(key)) return key
22
+ if (typeof fallbackResourceId === 'string' && RESOURCE_ID_PATTERN.test(fallbackResourceId)) {
23
+ return fallbackResourceId
24
+ }
25
+ return null
26
+ }
9
27
 
10
28
  export function originalObjectKey (resourceId) {
11
29
  assertResourceId(resourceId)
@@ -31,7 +49,7 @@ export function assertStorageKey (key) {
31
49
  }
32
50
 
33
51
  function assertResourceId (resourceId) {
34
- if (!resourceId || typeof resourceId !== 'string' || !/^[A-Za-z0-9_-]+$/.test(resourceId)) {
52
+ if (!resourceId || typeof resourceId !== 'string' || !RESOURCE_ID_PATTERN.test(resourceId)) {
35
53
  throw new Error(`[storage-keys] invalid resourceId: ${resourceId}`)
36
54
  }
37
55
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from '@jest/globals'
2
- import { originalObjectKey, derivativeObjectKey, assertStorageKey } from './storage-keys.js'
2
+ import { originalObjectKey, derivativeObjectKey, assertStorageKey, coerceStorageKey } from './storage-keys.js'
3
3
 
4
4
  describe('storage-keys', () => {
5
5
  it('originalObjectKey returns flat resourceId', () => {
@@ -13,4 +13,10 @@ describe('storage-keys', () => {
13
13
  it('rejects path-like keys', () => {
14
14
  expect(() => assertStorageKey('media/ws/file')).toThrow()
15
15
  })
16
+
17
+ it('coerces legacy media paths to the resource id', () => {
18
+ expect(coerceStorageKey('media/ws/abc123/original.pdf', 'abc123')).toBe('abc123')
19
+ expect(coerceStorageKey('abc123:galleryMedium', 'abc123')).toBe('abc123:galleryMedium')
20
+ expect(coerceStorageKey('media/ws/file', 'not valid')).toBe(null)
21
+ })
16
22
  })
@@ -132,12 +132,28 @@ describe('TaskRunList projection', () => {
132
132
  resourceId: 'run1',
133
133
  event: 'Completed',
134
134
  created: 2000,
135
- payload: { durationMs: 1000, resultSummary: null },
135
+ payload: {
136
+ durationMs: 1000,
137
+ resultSummary: null,
138
+ belongsTo: 'ws1',
139
+ taskId: '@ossy/media-tasks/tasks/resize',
140
+ moduleId: 'media-tasks',
141
+ },
136
142
  }, state)
137
143
 
138
144
  expect(state.runs[0].status).toBe('success')
139
145
  expect(state.runs[0].durationMs).toBe(1000)
140
146
  })
147
+
148
+ it('scopes Completed/Failed from payload.belongsTo (changestream dispatch)', () => {
149
+ expect(TaskRunListProjection.scopeFromEvent({
150
+ payload: { durationMs: 10 },
151
+ })).toBeNull()
152
+
153
+ expect(TaskRunListProjection.scopeFromEvent({
154
+ payload: { durationMs: 10, belongsTo: 'ws1' },
155
+ })).toBe('ws1')
156
+ })
141
157
  })
142
158
 
143
159
  describe('ActionInvocation entity fold', () => {