@morscherlab/mint-sdk 1.0.62 → 1.0.63

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 (29) hide show
  1. package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
  2. package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/{components-CpnOaPbP.js → components-DdzHh3a_.js} +2 -2
  5. package/dist/{components-CpnOaPbP.js.map → components-DdzHh3a_.js.map} +1 -1
  6. package/dist/composables/analysisArtifactTarget.d.ts +5 -0
  7. package/dist/composables/index.d.ts +2 -0
  8. package/dist/composables/index.js +3 -3
  9. package/dist/composables/platformContextHelpers.d.ts +2 -0
  10. package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
  11. package/dist/{composables-GpJD6R1R.js → composables-BUUphBqg.js} +38 -3
  12. package/dist/composables-BUUphBqg.js.map +1 -0
  13. package/dist/index.js +4 -4
  14. package/dist/install.js +1 -1
  15. package/dist/types/analysisArtifactTypes.d.ts +8 -0
  16. package/dist/types/index.d.ts +1 -1
  17. package/dist/{useAnalysisArtifacts-Ob8UtVwl.js → useAnalysisArtifacts-C3l0a0vu.js} +30 -4
  18. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +1 -0
  19. package/package.json +1 -1
  20. package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
  21. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
  22. package/src/composables/analysisArtifactTarget.ts +53 -0
  23. package/src/composables/index.ts +5 -0
  24. package/src/composables/platformContextHelpers.ts +10 -3
  25. package/src/composables/useAnalysisArtifacts.ts +48 -0
  26. package/src/types/analysisArtifactTypes.ts +9 -0
  27. package/src/types/index.ts +1 -0
  28. package/dist/composables-GpJD6R1R.js.map +0 -1
  29. package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.0.62",
3
+ "version": "1.0.63",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,77 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ buildAnalysisArtifactTargetQuery,
5
+ parseAnalysisArtifactTarget,
6
+ } from '../../composables/analysisArtifactTarget'
7
+ import {
8
+ currentExperimentIdFromUrl,
9
+ resolveCurrentPluginId,
10
+ } from '../../composables/platformContextHelpers'
11
+
12
+ describe('analysis artifact target', () => {
13
+ afterEach(() => {
14
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
15
+ vi.unstubAllGlobals()
16
+ })
17
+
18
+ it('should resolve the canonical injected plugin id', () => {
19
+ ;(window as unknown as { __MINT_PLATFORM__: unknown }).__MINT_PLATFORM__ = {
20
+ isIntegrated: true,
21
+ plugin: { id: 'LEAF', name: 'Leaf UI' },
22
+ theme: 'system',
23
+ }
24
+
25
+ expect(resolveCurrentPluginId()).toBe('LEAF')
26
+ })
27
+
28
+ it('should tolerate a host that provides a partial location object', () => {
29
+ vi.stubGlobal('window', {
30
+ location: { search: '' },
31
+ })
32
+
33
+ expect(currentExperimentIdFromUrl()).toBeUndefined()
34
+ })
35
+
36
+ it('should build the canonical readable query when an artifact id hint is present', () => {
37
+ const query = buildAnalysisArtifactTargetQuery({
38
+ experimentId: 66,
39
+ artifactPluginId: 'LEAF',
40
+ artifactKey: 'exp316_260703_ys_rna_ide_scr',
41
+ artifactId: 206,
42
+ })
43
+
44
+ expect(query.toString()).toBe(
45
+ 'experiment_id=66&artifact_plugin_id=LEAF'
46
+ + '&artifact_key=exp316_260703_ys_rna_ide_scr&artifact_id=206',
47
+ )
48
+ })
49
+
50
+ it('should parse the canonical identity without requiring an artifact id hint', () => {
51
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
52
+ 'experiment_id=66&artifact_plugin_id=LEAF&artifact_key=result',
53
+ ))
54
+
55
+ expect(target).toEqual({
56
+ experimentId: 66,
57
+ artifactPluginId: 'LEAF',
58
+ artifactKey: 'result',
59
+ })
60
+ })
61
+
62
+ it('should reject an incomplete target instead of guessing another artifact', () => {
63
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
64
+ 'experiment_id=66&artifact_key=result',
65
+ ))
66
+
67
+ expect(target).toBeNull()
68
+ })
69
+
70
+ it('should reject non-positive numeric identifiers', () => {
71
+ const target = parseAnalysisArtifactTarget(new URLSearchParams(
72
+ 'experiment_id=0&artifact_plugin_id=LEAF&artifact_key=result&artifact_id=-1',
73
+ ))
74
+
75
+ expect(target).toBeNull()
76
+ })
77
+ })
@@ -19,7 +19,10 @@ vi.mock('../../composables/useApi', () => ({
19
19
  }))
20
20
 
21
21
  import { useAnalysisArtifacts } from '../../composables/useAnalysisArtifacts'
22
- import type { AnalysisArtifactSummary } from '../../types/analysisArtifactTypes'
22
+ import type {
23
+ AnalysisArtifactDetail,
24
+ AnalysisArtifactSummary,
25
+ } from '../../types/analysisArtifactTypes'
23
26
 
24
27
  // ---------------------------------------------------------------------------
25
28
  // Fixtures
@@ -177,6 +180,76 @@ describe('useAnalysisArtifacts', () => {
177
180
  })
178
181
  })
179
182
 
183
+ describe('resolveTarget', () => {
184
+ it('should resolve the exact stable identity when the numeric hint is stale', async () => {
185
+ const detail = {
186
+ ...makeArtifact({
187
+ id: 2,
188
+ experiment_id: 42,
189
+ plugin_id: 'plugin-b',
190
+ artifact_key: 'peaks',
191
+ }),
192
+ result: { schema_version: 'mint.analysis_file.v1' },
193
+ tree: [],
194
+ summary: null,
195
+ } satisfies AnalysisArtifactDetail
196
+ mockGet
197
+ .mockResolvedValueOnce({ artifacts: ARTIFACTS })
198
+ .mockResolvedValueOnce(detail)
199
+ const { resolveTarget } = setup()
200
+
201
+ const resolved = await resolveTarget({
202
+ experimentId: 42,
203
+ artifactPluginId: 'plugin-b',
204
+ artifactKey: 'peaks',
205
+ artifactId: 999,
206
+ })
207
+
208
+ expect({
209
+ resolvedId: resolved?.id,
210
+ detailPath: mockGet.mock.calls[1]?.[0],
211
+ }).toEqual({
212
+ resolvedId: 2,
213
+ detailPath: '/experiments/42/analysis-artifacts/2',
214
+ })
215
+ })
216
+
217
+ it('should fail explicitly when the stable identity is not available', async () => {
218
+ const { resolveTarget, error } = setup()
219
+
220
+ const resolved = await resolveTarget({
221
+ experimentId: 42,
222
+ artifactPluginId: 'LEAF',
223
+ artifactKey: 'missing',
224
+ })
225
+
226
+ expect({ resolved, error: error.value }).toEqual({
227
+ resolved: null,
228
+ error: 'Analysis artifact LEAF/missing is not available.',
229
+ })
230
+ })
231
+
232
+ it('should reject a target from another experiment before making a request', async () => {
233
+ const { resolveTarget, error } = setup()
234
+
235
+ const resolved = await resolveTarget({
236
+ experimentId: 66,
237
+ artifactPluginId: 'LEAF',
238
+ artifactKey: 'result',
239
+ })
240
+
241
+ expect({
242
+ resolved,
243
+ error: error.value,
244
+ requests: mockGet.mock.calls,
245
+ }).toEqual({
246
+ resolved: null,
247
+ error: 'Analysis artifact target belongs to experiment 66, not 42.',
248
+ requests: [],
249
+ })
250
+ })
251
+ })
252
+
180
253
  // -------------------------------------------------------------------------
181
254
  // experiment id resolution
182
255
  // -------------------------------------------------------------------------
@@ -0,0 +1,53 @@
1
+ import type { AnalysisArtifactTarget } from '../types/analysisArtifactTypes'
2
+
3
+ const EXPERIMENT_ID = 'experiment_id'
4
+ const ARTIFACT_PLUGIN_ID = 'artifact_plugin_id'
5
+ const ARTIFACT_KEY = 'artifact_key'
6
+ const ARTIFACT_ID = 'artifact_id'
7
+
8
+ function parsePositiveInteger(value: string | null): number | undefined {
9
+ if (value === null || !/^\d+$/.test(value)) return undefined
10
+ const parsed = Number(value)
11
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined
12
+ }
13
+
14
+ /** Build the canonical query used to open one analysis artifact in a plugin. */
15
+ export function buildAnalysisArtifactTargetQuery(
16
+ target: AnalysisArtifactTarget,
17
+ ): URLSearchParams {
18
+ const query = new URLSearchParams()
19
+ query.set(EXPERIMENT_ID, String(target.experimentId))
20
+ query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId)
21
+ query.set(ARTIFACT_KEY, target.artifactKey)
22
+ if (target.artifactId !== undefined) {
23
+ query.set(ARTIFACT_ID, String(target.artifactId))
24
+ }
25
+ return query
26
+ }
27
+
28
+ /** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
29
+ export function parseAnalysisArtifactTarget(
30
+ query: URLSearchParams,
31
+ ): AnalysisArtifactTarget | null {
32
+ const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID))
33
+ const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim()
34
+ const artifactKey = query.get(ARTIFACT_KEY)?.trim()
35
+ const rawArtifactId = query.get(ARTIFACT_ID)
36
+ const artifactId = parsePositiveInteger(rawArtifactId)
37
+
38
+ if (
39
+ experimentId === undefined
40
+ || !artifactPluginId
41
+ || !artifactKey
42
+ || (rawArtifactId !== null && artifactId === undefined)
43
+ ) {
44
+ return null
45
+ }
46
+
47
+ return {
48
+ experimentId,
49
+ artifactPluginId,
50
+ artifactKey,
51
+ ...(artifactId === undefined ? {} : { artifactId }),
52
+ }
53
+ }
@@ -147,6 +147,11 @@ export {
147
147
  type UseAnalysisArtifactsOptions,
148
148
  type UseAnalysisArtifactsReturn,
149
149
  } from './useAnalysisArtifacts'
150
+ export {
151
+ buildAnalysisArtifactTargetQuery,
152
+ parseAnalysisArtifactTarget,
153
+ } from './analysisArtifactTarget'
154
+ export { resolveCurrentPluginId } from './platformContextHelpers'
150
155
  export {
151
156
  formatExperimentDate,
152
157
  formatExperimentStatus,
@@ -12,6 +12,12 @@ export function getInjectedPlatformContext(): PlatformContext | undefined {
12
12
  return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
13
13
  }
14
14
 
15
+ /** Return the platform's canonical plugin identity for the mounted frontend. */
16
+ export function resolveCurrentPluginId(): string | undefined {
17
+ const pluginId = getInjectedPlatformContext()?.plugin?.id
18
+ return typeof pluginId === 'string' && pluginId ? pluginId : undefined
19
+ }
20
+
15
21
  export function getInjectedExperimentContext(): ExperimentAwarePlatformContext | undefined {
16
22
  return getInjectedPlatformContext() as ExperimentAwarePlatformContext | undefined
17
23
  }
@@ -38,10 +44,11 @@ export function currentExperimentIdFromContext(
38
44
 
39
45
  export function currentExperimentIdFromUrl(): number | undefined {
40
46
  if (typeof window === 'undefined') return undefined
41
- const params = new URLSearchParams(window.location.search)
47
+ const location = window.location
48
+ const params = new URLSearchParams(location?.search ?? '')
42
49
  return parseExperimentId(params.get('experimentId') ?? params.get('experiment_id'))
43
- ?? experimentIdFromPath(window.location.pathname)
44
- ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, '/'))
50
+ ?? experimentIdFromPath(location?.pathname ?? '')
51
+ ?? experimentIdFromPath((location?.hash ?? '').replace(/^#\/?/, '/'))
45
52
  }
46
53
 
47
54
  export function resolveCurrentExperimentId(
@@ -19,6 +19,7 @@ import type {
19
19
  AnalysisArtifactMetadataUpdate,
20
20
  AnalysisArtifactStatusFilter,
21
21
  AnalysisArtifactSummary,
22
+ AnalysisArtifactTarget,
22
23
  } from '../types/analysisArtifactTypes'
23
24
 
24
25
  export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
@@ -67,6 +68,8 @@ export interface UseAnalysisArtifactsReturn {
67
68
  refresh: () => Promise<AnalysisArtifactSummary[]>
68
69
  /** Fetch one artifact with its result payload; null when it fails or is superseded. */
69
70
  getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
71
+ /** Resolve and fetch the exact stable artifact identity. */
72
+ resolveTarget: (target: AnalysisArtifactTarget) => Promise<AnalysisArtifactDetail | null>
70
73
  /** Update display_name and/or note; resolves to the updated summary or null. */
71
74
  updateMetadata: (
72
75
  artifactId: number,
@@ -206,6 +209,50 @@ export function useAnalysisArtifacts(
206
209
  }
207
210
  }
208
211
 
212
+ async function resolveTarget(
213
+ target: AnalysisArtifactTarget,
214
+ ): Promise<AnalysisArtifactDetail | null> {
215
+ const experimentId = currentExperimentId.value
216
+ if (experimentId !== target.experimentId) {
217
+ request.setError(
218
+ `Analysis artifact target belongs to experiment ${target.experimentId}, not `
219
+ + `${experimentId ?? 'the current selection'}.`,
220
+ )
221
+ return null
222
+ }
223
+
224
+ const available = await list()
225
+ const summary = available.find((artifact) => (
226
+ artifact.plugin_id === target.artifactPluginId
227
+ && artifact.artifact_key === target.artifactKey
228
+ && artifact.status === 'active'
229
+ ))
230
+ if (!summary) {
231
+ request.setError(
232
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`,
233
+ )
234
+ return null
235
+ }
236
+
237
+ const detail = await getDetail(summary.id)
238
+ if (
239
+ detail
240
+ && (
241
+ detail.experiment_id !== target.experimentId
242
+ || detail.plugin_id !== target.artifactPluginId
243
+ || detail.artifact_key !== target.artifactKey
244
+ )
245
+ ) {
246
+ selectedDetail.value = null
247
+ request.setError(
248
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not `
249
+ + 'match the requested identity.',
250
+ )
251
+ return null
252
+ }
253
+ return detail
254
+ }
255
+
209
256
  async function mutate(
210
257
  action: string,
211
258
  operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
@@ -296,6 +343,7 @@ export function useAnalysisArtifacts(
296
343
  list,
297
344
  refresh: list,
298
345
  getDetail,
346
+ resolveTarget,
299
347
  updateMetadata,
300
348
  archive,
301
349
  restore,
@@ -38,6 +38,15 @@ export interface AnalysisArtifactListResponse {
38
38
  artifacts: AnalysisArtifactSummary[]
39
39
  }
40
40
 
41
+ /** Stable identity used when one plugin opens another plugin's artifact. */
42
+ export interface AnalysisArtifactTarget {
43
+ experimentId: number
44
+ artifactPluginId: string
45
+ artifactKey: string
46
+ /** Optional lookup hint. The stable identity remains the three fields above. */
47
+ artifactId?: number
48
+ }
49
+
41
50
  /** Body of PATCH /experiments/{id}/analysis-artifacts/{artifactId}. */
42
51
  export interface AnalysisArtifactMetadataUpdate {
43
52
  display_name?: string
@@ -182,6 +182,7 @@ export type {
182
182
  AnalysisArtifactSummary,
183
183
  AnalysisArtifactDetail,
184
184
  AnalysisArtifactListResponse,
185
+ AnalysisArtifactTarget,
185
186
  AnalysisArtifactMetadataUpdate,
186
187
  ArtifactSaveMode,
187
188
  ArtifactFileSource,