@morscherlab/mint-sdk 1.0.12 → 1.0.14

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 (63) hide show
  1. package/dist/{ExperimentPopover-CCYB1oWp.js → ExperimentPopover-B29fIHQz.js} +6 -6
  2. package/dist/ExperimentPopover-B29fIHQz.js.map +1 -0
  3. package/dist/{ExperimentPopover-D0bg_fqM.js → ExperimentPopover-gdSA9ZCF.js} +1 -1
  4. package/dist/{ExperimentSelectorModal-DeBE0YKT.js → ExperimentSelectorModal-CHsU-LIh.js} +3 -3
  5. package/dist/{ExperimentSelectorModal-DeBE0YKT.js.map → ExperimentSelectorModal-CHsU-LIh.js.map} +1 -1
  6. package/dist/{ExperimentSelectorModal-BXf-XnQw.js → ExperimentSelectorModal-DIFyL5ta.js} +1 -1
  7. package/dist/__tests__/composables/useCommandHistory.test.d.ts +1 -0
  8. package/dist/__tests__/composables/useFileImport.test.d.ts +1 -0
  9. package/dist/__tests__/composables/useOptimisticMutation.test.d.ts +1 -0
  10. package/dist/__tests__/composables/useResourceCrud.test.d.ts +1 -0
  11. package/dist/__tests__/composables/useWellPainting.test.d.ts +1 -0
  12. package/dist/__tests__/composables/useWellPlateAdapter.test.d.ts +1 -0
  13. package/dist/__tests__/composables/useWellPlateValidation.test.d.ts +1 -0
  14. package/dist/components/index.js +3 -3
  15. package/dist/{components-CqdBz8DI.js → components-Dq02EVZH.js} +10 -10
  16. package/dist/components-Dq02EVZH.js.map +1 -0
  17. package/dist/composables/index.d.ts +7 -0
  18. package/dist/composables/index.js +5 -5
  19. package/dist/composables/useCommandHistory.d.ts +29 -0
  20. package/dist/composables/useFileImport.d.ts +35 -0
  21. package/dist/composables/useOptimisticMutation.d.ts +28 -0
  22. package/dist/composables/useResourceCrud.d.ts +40 -0
  23. package/dist/composables/useWellPainting.d.ts +35 -0
  24. package/dist/composables/useWellPlateAdapter.d.ts +36 -0
  25. package/dist/composables/useWellPlateValidation.d.ts +27 -0
  26. package/dist/{composables-BWh0MpcK.js → composables-D9mexHSW.js} +668 -5
  27. package/dist/composables-D9mexHSW.js.map +1 -0
  28. package/dist/{experiment-utils-hGXMHlAc.js → experiment-utils-D11yT3AR.js} +1 -2
  29. package/dist/{experiment-utils-hGXMHlAc.js.map → experiment-utils-D11yT3AR.js.map} +1 -1
  30. package/dist/index.js +8 -8
  31. package/dist/install.js +3 -3
  32. package/dist/styles.css +4 -8
  33. package/dist/{useExperimentSelector-B3hAGvL4.js → useExperimentSelector-BBaz0w51.js} +2 -2
  34. package/dist/{useExperimentSelector-B3hAGvL4.js.map → useExperimentSelector-BBaz0w51.js.map} +1 -1
  35. package/dist/{useProtocolTemplates-BJxS5F0_.js → useProtocolTemplates-DODHlhxr.js} +3 -3
  36. package/dist/{useProtocolTemplates-BJxS5F0_.js.map → useProtocolTemplates-DODHlhxr.js.map} +1 -1
  37. package/package.json +1 -1
  38. package/src/__tests__/components/ExperimentPopover.test.ts +23 -0
  39. package/src/__tests__/composables/experiment-utils.test.ts +2 -2
  40. package/src/__tests__/composables/useAppExperiment.test.ts +2 -1
  41. package/src/__tests__/composables/useCommandHistory.test.ts +43 -0
  42. package/src/__tests__/composables/useFileImport.test.ts +44 -0
  43. package/src/__tests__/composables/useOptimisticMutation.test.ts +40 -0
  44. package/src/__tests__/composables/useResourceCrud.test.ts +56 -0
  45. package/src/__tests__/composables/useWellPainting.test.ts +52 -0
  46. package/src/__tests__/composables/useWellPlateAdapter.test.ts +50 -0
  47. package/src/__tests__/composables/useWellPlateValidation.test.ts +42 -0
  48. package/src/components/ExperimentPopover.story.vue +3 -4
  49. package/src/components/ExperimentPopover.vue +6 -6
  50. package/src/components/PluginWorkspaceView.vue +3 -3
  51. package/src/composables/experiment-utils.ts +1 -1
  52. package/src/composables/index.ts +67 -0
  53. package/src/composables/useCommandHistory.ts +113 -0
  54. package/src/composables/useFileImport.ts +231 -0
  55. package/src/composables/useOptimisticMutation.ts +107 -0
  56. package/src/composables/useResourceCrud.ts +245 -0
  57. package/src/composables/useWellPainting.ts +187 -0
  58. package/src/composables/useWellPlateAdapter.ts +147 -0
  59. package/src/composables/useWellPlateValidation.ts +85 -0
  60. package/src/styles/components/experiment-popover.css +2 -4
  61. package/dist/ExperimentPopover-CCYB1oWp.js.map +0 -1
  62. package/dist/components-CqdBz8DI.js.map +0 -1
  63. package/dist/composables-BWh0MpcK.js.map +0 -1
@@ -0,0 +1,231 @@
1
+ import { ref, shallowRef, type Ref, type ShallowRef } from 'vue'
2
+
3
+ export interface DelimitedTable {
4
+ columns: string[]
5
+ rows: Record<string, string>[]
6
+ rawRows: string[][]
7
+ delimiter: string
8
+ }
9
+
10
+ export interface ParseDelimitedTextOptions {
11
+ delimiter?: string
12
+ headers?: readonly string[]
13
+ hasHeaderRow?: boolean
14
+ trim?: boolean
15
+ }
16
+
17
+ export interface FileImportValidationOptions {
18
+ accept?: string | readonly string[]
19
+ maxSizeBytes?: number
20
+ }
21
+
22
+ export interface FileImportOptions<TResult = string> extends FileImportValidationOptions {
23
+ encoding?: string
24
+ parse?: (text: string, file: File) => TResult | Promise<TResult>
25
+ }
26
+
27
+ export interface UseFileImportReturn<TResult> {
28
+ loading: ShallowRef<boolean>
29
+ error: Ref<string | null>
30
+ fileName: Ref<string>
31
+ result: Ref<TResult | null>
32
+ importFile: (file: File) => Promise<TResult>
33
+ clear: () => void
34
+ }
35
+
36
+ /** Stateful file reader for plugin import flows with shared validation and error handling. */
37
+ export function useFileImport<TResult = string>(
38
+ options: FileImportOptions<TResult> = {},
39
+ ): UseFileImportReturn<TResult> {
40
+ const loading = shallowRef(false)
41
+ const error = ref<string | null>(null)
42
+ const fileName = ref('')
43
+ const result = ref<TResult | null>(null) as Ref<TResult | null>
44
+
45
+ function clear(): void {
46
+ loading.value = false
47
+ error.value = null
48
+ fileName.value = ''
49
+ result.value = null
50
+ }
51
+
52
+ async function importFile(file: File): Promise<TResult> {
53
+ loading.value = true
54
+ error.value = null
55
+ fileName.value = file.name
56
+
57
+ try {
58
+ validateImportFile(file, options)
59
+ const text = await readFileAsText(file, options.encoding)
60
+ const parsed = options.parse
61
+ ? await options.parse(text, file)
62
+ : text as TResult
63
+ result.value = parsed
64
+ return parsed
65
+ } catch (err) {
66
+ const message = readErrorMessage(err)
67
+ error.value = message
68
+ throw err
69
+ } finally {
70
+ loading.value = false
71
+ }
72
+ }
73
+
74
+ return {
75
+ loading,
76
+ error,
77
+ fileName,
78
+ result,
79
+ importFile,
80
+ clear,
81
+ }
82
+ }
83
+
84
+ export function validateImportFile(file: File, options: FileImportValidationOptions = {}): void {
85
+ if (options.maxSizeBytes !== undefined && file.size > options.maxSizeBytes) {
86
+ throw new Error(`File is too large. Maximum size is ${formatBytes(options.maxSizeBytes)}.`)
87
+ }
88
+
89
+ const accept = normalizeAccept(options.accept)
90
+ if (accept.length === 0) return
91
+
92
+ const name = file.name.toLowerCase()
93
+ const type = file.type.toLowerCase()
94
+ const matches = accept.some((entry) => {
95
+ if (entry.startsWith('.')) return name.endsWith(entry)
96
+ if (entry.endsWith('/*')) return type.startsWith(entry.slice(0, -1))
97
+ return type === entry
98
+ })
99
+
100
+ if (!matches) {
101
+ throw new Error(`Unsupported file type. Expected ${accept.join(', ')}.`)
102
+ }
103
+ }
104
+
105
+ export function readFileAsText(file: File, encoding?: string): Promise<string> {
106
+ if (typeof FileReader === 'undefined' && typeof file.text === 'function') {
107
+ return file.text()
108
+ }
109
+
110
+ return new Promise((resolve, reject) => {
111
+ const reader = new FileReader()
112
+ reader.onload = () => resolve(String(reader.result ?? ''))
113
+ reader.onerror = () => reject(reader.error ?? new Error('Failed to read file.'))
114
+ reader.readAsText(file, encoding)
115
+ })
116
+ }
117
+
118
+ export function parseDelimitedText(
119
+ text: string,
120
+ options: ParseDelimitedTextOptions = {},
121
+ ): DelimitedTable {
122
+ const trim = options.trim ?? true
123
+ const delimiter = options.delimiter ?? detectDelimiter(text)
124
+ const rawRows = parseDelimitedRows(text, delimiter)
125
+ .filter(row => row.some(cell => cell.trim().length > 0))
126
+ .map(row => trim ? row.map(cell => cell.trim()) : row)
127
+
128
+ const hasHeaderRow = options.hasHeaderRow ?? !options.headers
129
+ const columns = options.headers
130
+ ? [...options.headers]
131
+ : hasHeaderRow
132
+ ? rawRows[0] ?? []
133
+ : createColumnNames(maxRowLength(rawRows))
134
+
135
+ const dataRows = hasHeaderRow && !options.headers ? rawRows.slice(1) : rawRows
136
+ const rows = dataRows.map(row => Object.fromEntries(
137
+ columns.map((column, index) => [column, row[index] ?? '']),
138
+ ))
139
+
140
+ return {
141
+ columns,
142
+ rows,
143
+ rawRows,
144
+ delimiter,
145
+ }
146
+ }
147
+
148
+ export function detectDelimiter(text: string): string {
149
+ const firstLine = text.split(/\r?\n/, 1)[0] ?? ''
150
+ const commaCount = countChar(firstLine, ',')
151
+ const tabCount = countChar(firstLine, '\t')
152
+ const semicolonCount = countChar(firstLine, ';')
153
+ if (tabCount > commaCount && tabCount >= semicolonCount) return '\t'
154
+ if (semicolonCount > commaCount) return ';'
155
+ return ','
156
+ }
157
+
158
+ function parseDelimitedRows(text: string, delimiter: string): string[][] {
159
+ const rows: string[][] = []
160
+ let row: string[] = []
161
+ let field = ''
162
+ let quoted = false
163
+
164
+ for (let index = 0; index < text.length; index++) {
165
+ const char = text[index]
166
+ const next = text[index + 1]
167
+
168
+ if (char === '"') {
169
+ if (quoted && next === '"') {
170
+ field += '"'
171
+ index++
172
+ } else {
173
+ quoted = !quoted
174
+ }
175
+ continue
176
+ }
177
+
178
+ if (!quoted && char === delimiter) {
179
+ row.push(field)
180
+ field = ''
181
+ continue
182
+ }
183
+
184
+ if (!quoted && (char === '\n' || char === '\r')) {
185
+ if (char === '\r' && next === '\n') index++
186
+ row.push(field)
187
+ rows.push(row)
188
+ row = []
189
+ field = ''
190
+ continue
191
+ }
192
+
193
+ field += char
194
+ }
195
+
196
+ row.push(field)
197
+ rows.push(row)
198
+ return rows
199
+ }
200
+
201
+ function normalizeAccept(accept: string | readonly string[] | undefined): string[] {
202
+ if (!accept) return []
203
+ const entries = typeof accept === 'string' ? accept.split(',') : [...accept]
204
+ return entries
205
+ .map((entry: string) => entry.trim().toLowerCase())
206
+ .filter(Boolean)
207
+ }
208
+
209
+ function createColumnNames(count: number): string[] {
210
+ return Array.from({ length: count }, (_, index) => `column_${index + 1}`)
211
+ }
212
+
213
+ function maxRowLength(rows: readonly string[][]): number {
214
+ return rows.reduce((max, row) => Math.max(max, row.length), 0)
215
+ }
216
+
217
+ function countChar(value: string, char: string): number {
218
+ return [...value].filter(candidate => candidate === char).length
219
+ }
220
+
221
+ function formatBytes(bytes: number): string {
222
+ if (bytes < 1024) return `${bytes} B`
223
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
224
+ return `${Math.round(bytes / (1024 * 1024))} MB`
225
+ }
226
+
227
+ function readErrorMessage(value: unknown): string {
228
+ if (value instanceof Error && value.message) return value.message
229
+ if (typeof value === 'string' && value.trim()) return value
230
+ return 'Failed to import file.'
231
+ }
@@ -0,0 +1,107 @@
1
+ import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+
3
+ export interface OptimisticMutationContext<TVariables, TSnapshot> {
4
+ variables: TVariables
5
+ snapshot: TSnapshot | undefined
6
+ }
7
+
8
+ export interface UseOptimisticMutationOptions<TVariables, TData, TSnapshot = unknown> {
9
+ mutation: (variables: TVariables) => Promise<TData>
10
+ snapshot?: (variables: TVariables) => TSnapshot
11
+ apply?: (variables: TVariables, snapshot: TSnapshot | undefined) => void
12
+ commit?: (data: TData, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
13
+ rollback?: (
14
+ snapshot: TSnapshot | undefined,
15
+ variables: TVariables,
16
+ error: unknown,
17
+ ) => void
18
+ onSuccess?: (data: TData, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
19
+ onError?: (error: unknown, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
20
+ onSettled?: (context: OptimisticMutationContext<TVariables, TSnapshot>) => void
21
+ readErrorMessage?: (error: unknown) => string
22
+ }
23
+
24
+ export interface UseOptimisticMutationReturn<TVariables, TData, TSnapshot = unknown> {
25
+ loading: ShallowRef<boolean>
26
+ error: Ref<string | null>
27
+ data: Ref<TData | null>
28
+ lastSnapshot: Ref<TSnapshot | undefined>
29
+ isIdle: ComputedRef<boolean>
30
+ run: (variables: TVariables) => Promise<TData>
31
+ clearError: () => void
32
+ reset: () => void
33
+ }
34
+
35
+ /** Run async mutations with optional optimistic local updates and rollback. */
36
+ export function useOptimisticMutation<TVariables, TData, TSnapshot = unknown>(
37
+ options: UseOptimisticMutationOptions<TVariables, TData, TSnapshot>,
38
+ ): UseOptimisticMutationReturn<TVariables, TData, TSnapshot> {
39
+ const loading = shallowRef(false)
40
+ const error = ref<string | null>(null)
41
+ const data = ref<TData | null>(null) as Ref<TData | null>
42
+ const lastSnapshot = ref<TSnapshot | undefined>(undefined) as Ref<TSnapshot | undefined>
43
+ const isIdle = computed(() => !loading.value && data.value === null && error.value === null)
44
+
45
+ function clearError(): void {
46
+ error.value = null
47
+ }
48
+
49
+ function reset(): void {
50
+ loading.value = false
51
+ error.value = null
52
+ data.value = null
53
+ lastSnapshot.value = undefined
54
+ }
55
+
56
+ async function run(variables: TVariables): Promise<TData> {
57
+ const snapshot = options.snapshot?.(variables)
58
+ const context: OptimisticMutationContext<TVariables, TSnapshot> = { variables, snapshot }
59
+
60
+ loading.value = true
61
+ error.value = null
62
+ lastSnapshot.value = snapshot
63
+ options.apply?.(variables, snapshot)
64
+
65
+ try {
66
+ const result = await options.mutation(variables)
67
+ data.value = result
68
+ options.commit?.(result, context)
69
+ options.onSuccess?.(result, context)
70
+ return result
71
+ } catch (err) {
72
+ options.rollback?.(snapshot, variables, err)
73
+ error.value = options.readErrorMessage?.(err) ?? readErrorMessage(err)
74
+ options.onError?.(err, context)
75
+ throw err
76
+ } finally {
77
+ loading.value = false
78
+ options.onSettled?.(context)
79
+ }
80
+ }
81
+
82
+ return {
83
+ loading,
84
+ error,
85
+ data,
86
+ lastSnapshot,
87
+ isIdle,
88
+ run,
89
+ clearError,
90
+ reset,
91
+ }
92
+ }
93
+
94
+ function readErrorMessage(value: unknown): string {
95
+ if (value instanceof Error && value.message) return value.message
96
+ if (typeof value === 'string' && value.trim()) return value
97
+ if (
98
+ typeof value === 'object'
99
+ && value !== null
100
+ && 'message' in value
101
+ && typeof (value as { message?: unknown }).message === 'string'
102
+ && (value as { message: string }).message.trim()
103
+ ) {
104
+ return (value as { message: string }).message
105
+ }
106
+ return 'Request failed.'
107
+ }
@@ -0,0 +1,245 @@
1
+ import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+
3
+ export type ResourceKey = string | number
4
+
5
+ export interface ResourceCrudAdapter<TItem, TCreate = Partial<TItem>, TUpdate = Partial<TItem>, TKey extends ResourceKey = ResourceKey> {
6
+ list: () => Promise<TItem[]>
7
+ create?: (payload: TCreate) => Promise<TItem>
8
+ update?: (key: TKey, payload: TUpdate) => Promise<TItem>
9
+ remove?: (key: TKey) => Promise<void>
10
+ }
11
+
12
+ export interface UseResourceCrudOptions<
13
+ TItem,
14
+ TCreate = Partial<TItem>,
15
+ TUpdate = Partial<TItem>,
16
+ TKey extends ResourceKey = ResourceKey,
17
+ > {
18
+ adapter: ResourceCrudAdapter<TItem, TCreate, TUpdate, TKey>
19
+ initialItems?: readonly TItem[]
20
+ getKey?: (item: TItem) => TKey
21
+ readErrorMessage?: (error: unknown) => string
22
+ }
23
+
24
+ export interface UseResourceCrudReturn<TItem, TCreate, TUpdate, TKey extends ResourceKey> {
25
+ items: Ref<TItem[]>
26
+ loading: ShallowRef<boolean>
27
+ saving: ShallowRef<boolean>
28
+ deleting: ShallowRef<boolean>
29
+ error: Ref<string | null>
30
+ isBusy: ComputedRef<boolean>
31
+ load: () => Promise<TItem[]>
32
+ createItem: (payload: TCreate) => Promise<TItem>
33
+ updateItem: (key: TKey, payload: TUpdate) => Promise<TItem>
34
+ deleteItem: (keyOrItem: TKey | TItem) => Promise<void>
35
+ setItems: (items: readonly TItem[]) => void
36
+ upsertLocal: (item: TItem) => void
37
+ removeLocal: (keyOrItem: TKey | TItem) => void
38
+ clearError: () => void
39
+ }
40
+
41
+ /** Common list/create/update/delete state wrapper for generated plugin clients. */
42
+ export function useResourceCrud<
43
+ TItem,
44
+ TCreate = Partial<TItem>,
45
+ TUpdate = Partial<TItem>,
46
+ TKey extends ResourceKey = ResourceKey,
47
+ >(
48
+ options: UseResourceCrudOptions<TItem, TCreate, TUpdate, TKey>,
49
+ ): UseResourceCrudReturn<TItem, TCreate, TUpdate, TKey> {
50
+ const items = ref<TItem[]>([...(options.initialItems ?? [])]) as Ref<TItem[]>
51
+ const loading = shallowRef(false)
52
+ const saving = shallowRef(false)
53
+ const deleting = shallowRef(false)
54
+ const error = ref<string | null>(null)
55
+ const isBusy = computed(() => loading.value || saving.value || deleting.value)
56
+
57
+ const readMessage = options.readErrorMessage ?? readErrorMessage
58
+ const getKey = options.getKey ?? defaultGetKey<TKey, TItem>
59
+
60
+ function clearError(): void {
61
+ error.value = null
62
+ }
63
+
64
+ function setError(err: unknown): void {
65
+ error.value = readMessage(err)
66
+ }
67
+
68
+ function setItems(nextItems: readonly TItem[]): void {
69
+ items.value = [...nextItems]
70
+ }
71
+
72
+ function upsertLocal(item: TItem): void {
73
+ const key = getKey(item)
74
+ const index = items.value.findIndex(existing => getKey(existing) === key)
75
+ if (index === -1) {
76
+ items.value = [...items.value, item]
77
+ return
78
+ }
79
+ items.value = [
80
+ ...items.value.slice(0, index),
81
+ item,
82
+ ...items.value.slice(index + 1),
83
+ ]
84
+ }
85
+
86
+ function removeLocal(keyOrItem: TKey | TItem): void {
87
+ const key = resolveKey(keyOrItem, getKey)
88
+ items.value = items.value.filter(item => getKey(item) !== key)
89
+ }
90
+
91
+ async function load(): Promise<TItem[]> {
92
+ loading.value = true
93
+ clearError()
94
+ try {
95
+ const result = await options.adapter.list()
96
+ setItems(result)
97
+ return result
98
+ } catch (err) {
99
+ setError(err)
100
+ throw err
101
+ } finally {
102
+ loading.value = false
103
+ }
104
+ }
105
+
106
+ async function createItem(payload: TCreate): Promise<TItem> {
107
+ if (!options.adapter.create) {
108
+ throw new Error('[MINT SDK] Resource adapter does not implement create().')
109
+ }
110
+ saving.value = true
111
+ clearError()
112
+ try {
113
+ const result = await options.adapter.create(payload)
114
+ upsertLocal(result)
115
+ return result
116
+ } catch (err) {
117
+ setError(err)
118
+ throw err
119
+ } finally {
120
+ saving.value = false
121
+ }
122
+ }
123
+
124
+ async function updateItem(key: TKey, payload: TUpdate): Promise<TItem> {
125
+ if (!options.adapter.update) {
126
+ throw new Error('[MINT SDK] Resource adapter does not implement update().')
127
+ }
128
+ saving.value = true
129
+ clearError()
130
+ try {
131
+ const result = await options.adapter.update(key, payload)
132
+ upsertLocal(result)
133
+ return result
134
+ } catch (err) {
135
+ setError(err)
136
+ throw err
137
+ } finally {
138
+ saving.value = false
139
+ }
140
+ }
141
+
142
+ async function deleteItem(keyOrItem: TKey | TItem): Promise<void> {
143
+ if (!options.adapter.remove) {
144
+ throw new Error('[MINT SDK] Resource adapter does not implement remove().')
145
+ }
146
+ const key = resolveKey(keyOrItem, getKey)
147
+ deleting.value = true
148
+ clearError()
149
+ try {
150
+ await options.adapter.remove(key)
151
+ removeLocal(key)
152
+ } catch (err) {
153
+ setError(err)
154
+ throw err
155
+ } finally {
156
+ deleting.value = false
157
+ }
158
+ }
159
+
160
+ return {
161
+ items,
162
+ loading,
163
+ saving,
164
+ deleting,
165
+ error,
166
+ isBusy,
167
+ load,
168
+ createItem,
169
+ updateItem,
170
+ deleteItem,
171
+ setItems,
172
+ upsertLocal,
173
+ removeLocal,
174
+ clearError,
175
+ }
176
+ }
177
+
178
+ export interface PluginResourceClientOptions<
179
+ TClient,
180
+ TItem,
181
+ TCreate = Partial<TItem>,
182
+ TUpdate = Partial<TItem>,
183
+ TKey extends ResourceKey = ResourceKey,
184
+ > {
185
+ client: TClient
186
+ list: (client: TClient) => Promise<TItem[]>
187
+ create?: (client: TClient, payload: TCreate) => Promise<TItem>
188
+ update?: (client: TClient, key: TKey, payload: TUpdate) => Promise<TItem>
189
+ remove?: (client: TClient, key: TKey) => Promise<void>
190
+ }
191
+
192
+ export function createPluginResourceClient<
193
+ TClient,
194
+ TItem,
195
+ TCreate = Partial<TItem>,
196
+ TUpdate = Partial<TItem>,
197
+ TKey extends ResourceKey = ResourceKey,
198
+ >(
199
+ options: PluginResourceClientOptions<TClient, TItem, TCreate, TUpdate, TKey>,
200
+ ): ResourceCrudAdapter<TItem, TCreate, TUpdate, TKey> {
201
+ return {
202
+ list: () => options.list(options.client),
203
+ create: options.create
204
+ ? payload => options.create!(options.client, payload)
205
+ : undefined,
206
+ update: options.update
207
+ ? (key, payload) => options.update!(options.client, key, payload)
208
+ : undefined,
209
+ remove: options.remove
210
+ ? key => options.remove!(options.client, key)
211
+ : undefined,
212
+ }
213
+ }
214
+
215
+ function defaultGetKey<TKey extends ResourceKey, TItem>(item: TItem): TKey {
216
+ if (item && typeof item === 'object' && 'id' in item) {
217
+ return (item as { id: ResourceKey }).id as TKey
218
+ }
219
+ throw new Error('[MINT SDK] Resource items need an id field or a getKey option.')
220
+ }
221
+
222
+ function resolveKey<TItem, TKey extends ResourceKey>(
223
+ keyOrItem: TKey | TItem,
224
+ getKey: (item: TItem) => TKey,
225
+ ): TKey {
226
+ if (typeof keyOrItem === 'string' || typeof keyOrItem === 'number') {
227
+ return keyOrItem as TKey
228
+ }
229
+ return getKey(keyOrItem as TItem)
230
+ }
231
+
232
+ function readErrorMessage(value: unknown): string {
233
+ if (value instanceof Error && value.message) return value.message
234
+ if (typeof value === 'string' && value.trim()) return value
235
+ if (
236
+ typeof value === 'object'
237
+ && value !== null
238
+ && 'message' in value
239
+ && typeof (value as { message?: unknown }).message === 'string'
240
+ && (value as { message: string }).message.trim()
241
+ ) {
242
+ return (value as { message: string }).message
243
+ }
244
+ return 'Request failed.'
245
+ }