@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.
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/{components-CpnOaPbP.js → components-DdzHh3a_.js} +2 -2
- package/dist/{components-CpnOaPbP.js.map → components-DdzHh3a_.js.map} +1 -1
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +3 -3
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/{composables-GpJD6R1R.js → composables-BUUphBqg.js} +38 -3
- package/dist/composables-BUUphBqg.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useAnalysisArtifacts-Ob8UtVwl.js → useAnalysisArtifacts-C3l0a0vu.js} +30 -4
- package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/index.ts +1 -0
- package/dist/composables-GpJD6R1R.js.map +0 -1
- 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.
|
|
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 {
|
|
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
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -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
|
|
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(
|
|
44
|
-
?? experimentIdFromPath(
|
|
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
|