@peanut-admin/admin 0.1.0-alpha.2
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/LICENSE +202 -0
- package/admin-core/src/access/access.ts +27 -0
- package/admin-core/src/api/client.ts +200 -0
- package/admin-core/src/api/problem.ts +67 -0
- package/admin-core/src/api/refresh.ts +122 -0
- package/admin-core/src/auth/stores.ts +121 -0
- package/admin-core/src/generated/api.d.ts +22106 -0
- package/admin-core/src/governance/audit.ts +56 -0
- package/admin-core/src/governance/catalog.ts +86 -0
- package/admin-core/src/governance/index.ts +26 -0
- package/admin-core/src/governance/menu.ts +63 -0
- package/admin-core/src/governance/roles.ts +144 -0
- package/admin-core/src/governance/types.ts +64 -0
- package/admin-core/src/index.ts +105 -0
- package/admin-core/src/lifecycle/tenant.ts +59 -0
- package/admin-core/src/module/contribution.ts +124 -0
- package/admin-core/src/runtime/config.ts +55 -0
- package/admin-core/src/runtime/errors.ts +81 -0
- package/admin-core/src/runtime/guard.ts +40 -0
- package/admin-core/src/runtime/navigation.ts +105 -0
- package/admin-core/src/runtime/overrides.ts +214 -0
- package/admin-core/src/targets/store.ts +153 -0
- package/admin-shell/src/config.ts +84 -0
- package/admin-shell/src/index.ts +40 -0
- package/admin-shell/src/layout.ts +332 -0
- package/admin-shell/src/overrides.ts +53 -0
- package/admin-shell/src/states.ts +93 -0
- package/admin-shell/src/targets.ts +128 -0
- package/admin-shell/src/theme.ts +15 -0
- package/client-core/src/index.ts +325 -0
- package/client-nuxt/src/index.ts +40 -0
- package/client-uniapp/src/index.ts +50 -0
- package/file-media/src/FileAssetSelector.vue +117 -0
- package/file-media/src/FileMediaPage.vue +158 -0
- package/file-media/src/contracts.ts +220 -0
- package/file-media/src/index.ts +19 -0
- package/file-media/src/runtime.ts +210 -0
- package/import-export/src/ImportExportPage.vue +155 -0
- package/import-export/src/contracts.ts +96 -0
- package/import-export/src/index.ts +3 -0
- package/import-export/src/runtime.ts +128 -0
- package/integration-security/src/IntegrationSecurityPage.vue +402 -0
- package/integration-security/src/contracts.ts +171 -0
- package/integration-security/src/index.ts +3 -0
- package/integration-security/src/runtime.ts +180 -0
- package/notification-sms/src/NotificationInboxPage.vue +266 -0
- package/notification-sms/src/contracts.ts +195 -0
- package/notification-sms/src/index.ts +4 -0
- package/notification-sms/src/runtime.ts +143 -0
- package/ops-console/src/OpsConsolePage.vue +337 -0
- package/ops-console/src/contracts.ts +169 -0
- package/ops-console/src/index.ts +3 -0
- package/ops-console/src/runtime.ts +199 -0
- package/package.json +108 -0
- package/reference-codes/src/ReferenceCodesPage.vue +942 -0
- package/reference-codes/src/contracts.ts +484 -0
- package/reference-codes/src/index.ts +53 -0
- package/reference-codes/src/runtime.ts +855 -0
- package/settings/src/SettingsPage.vue +536 -0
- package/settings/src/contracts.ts +331 -0
- package/settings/src/index.ts +45 -0
- package/settings/src/runtime.ts +545 -0
- package/task-job/src/TaskJobPage.vue +120 -0
- package/task-job/src/contracts.ts +117 -0
- package/task-job/src/index.ts +2 -0
- package/task-job/src/runtime.ts +105 -0
- package/testing/src/index.ts +141 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
import { defineAdminModule } from '@peanut-admin/admin/core'
|
|
2
|
+
import { inject, reactive } from 'vue'
|
|
3
|
+
import type { AdminModuleContribution } from '@peanut-admin/admin/core'
|
|
4
|
+
import type { InjectionKey } from 'vue'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
normalizeReferenceCodeInstant,
|
|
8
|
+
parseReferenceCode,
|
|
9
|
+
parseReferenceCodeList,
|
|
10
|
+
parseReferenceCodeMetadataText,
|
|
11
|
+
parseReferenceCodeSets,
|
|
12
|
+
} from './contracts'
|
|
13
|
+
import type {
|
|
14
|
+
ReferenceCodeCreateInput,
|
|
15
|
+
ReferenceCodeEntry,
|
|
16
|
+
ReferenceCodeListQuery,
|
|
17
|
+
ReferenceCodeMetadata,
|
|
18
|
+
ReferenceCodeSetSummary,
|
|
19
|
+
ReferenceCodesTransport,
|
|
20
|
+
ReferenceCodesTransportResult,
|
|
21
|
+
ReferenceCodeStatus,
|
|
22
|
+
ReferenceCodeVersionInput,
|
|
23
|
+
} from './contracts'
|
|
24
|
+
|
|
25
|
+
export const REFERENCE_CODES_MODULE_KEY = 'peanut.reference-codes' as const
|
|
26
|
+
export const REFERENCE_CODES_ROUTE_NAME = 'peanut.reference-codes.list' as const
|
|
27
|
+
export const REFERENCE_CODES_ROUTE_PATH = '/app/reference-codes' as const
|
|
28
|
+
export const REFERENCE_CODES_READ_PERMISSION = 'peanut.reference-codes.read' as const
|
|
29
|
+
export const REFERENCE_CODES_MANAGE_PERMISSION = 'peanut.reference-codes.manage' as const
|
|
30
|
+
export const REFERENCE_CODES_STORE_KEY = 'peanut.reference-codes.runtime' as const
|
|
31
|
+
|
|
32
|
+
export interface ReferenceCodeRequestError {
|
|
33
|
+
readonly kind: 'http' | 'protocol' | 'transport' | 'validation'
|
|
34
|
+
readonly message: string
|
|
35
|
+
readonly requestId: string | null
|
|
36
|
+
readonly status: number | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ReferenceCodeStaleState {
|
|
40
|
+
readonly message: string
|
|
41
|
+
readonly requestId: string | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ReferenceCodeDraftFields {
|
|
45
|
+
label: string
|
|
46
|
+
metadataText: string
|
|
47
|
+
status: ReferenceCodeStatus
|
|
48
|
+
sortOrder: number
|
|
49
|
+
effectiveAt: string
|
|
50
|
+
expiresAt: string | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReferenceCodeCreateDraft extends ReferenceCodeDraftFields {
|
|
54
|
+
code: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ReferenceCodeAppendDraft extends ReferenceCodeDraftFields {
|
|
58
|
+
readonly code: string
|
|
59
|
+
etag: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ReferenceCodesRuntimeState {
|
|
63
|
+
sets: ReferenceCodeSetSummary[]
|
|
64
|
+
selectedSet: ReferenceCodeSetSummary | null
|
|
65
|
+
asOf: string
|
|
66
|
+
effectiveStatus: ReferenceCodeListQuery['effectiveStatus']
|
|
67
|
+
includeRetired: boolean
|
|
68
|
+
entries: ReferenceCodeEntry[]
|
|
69
|
+
page: number
|
|
70
|
+
pageSize: number
|
|
71
|
+
total: number
|
|
72
|
+
createDraft: ReferenceCodeCreateDraft | null
|
|
73
|
+
appendDraft: ReferenceCodeAppendDraft | null
|
|
74
|
+
retireCode: string | null
|
|
75
|
+
errors: Record<string, ReferenceCodeRequestError>
|
|
76
|
+
stale: Record<string, ReferenceCodeStaleState>
|
|
77
|
+
pendingResources: Set<string>
|
|
78
|
+
requests: Set<string>
|
|
79
|
+
loading: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ReferenceCodesRuntime {
|
|
83
|
+
readonly state: ReferenceCodesRuntimeState
|
|
84
|
+
canManage: () => boolean
|
|
85
|
+
loadSets: () => Promise<void>
|
|
86
|
+
selectSet: (moduleKey: string, setKey: string) => Promise<void>
|
|
87
|
+
setAsOf: (value: string) => Promise<void>
|
|
88
|
+
setFilters: (effectiveStatus: ReferenceCodeListQuery['effectiveStatus'], includeRetired: boolean) => Promise<void>
|
|
89
|
+
setPage: (page: number) => Promise<void>
|
|
90
|
+
loadEntries: () => Promise<void>
|
|
91
|
+
beginCreate: () => void
|
|
92
|
+
updateCreateDraft: (changes: Partial<ReferenceCodeCreateDraft>) => void
|
|
93
|
+
cancelCreate: () => void
|
|
94
|
+
create: () => Promise<void>
|
|
95
|
+
beginAppend: (entry: ReferenceCodeEntry) => void
|
|
96
|
+
updateAppendDraft: (changes: Partial<ReferenceCodeDraftFields>) => void
|
|
97
|
+
cancelAppend: () => void
|
|
98
|
+
appendVersion: () => Promise<void>
|
|
99
|
+
reloadStale: (code: string) => Promise<void>
|
|
100
|
+
beginRetire: (entry: ReferenceCodeEntry) => void
|
|
101
|
+
cancelRetire: () => void
|
|
102
|
+
retire: () => Promise<void>
|
|
103
|
+
isPending: (code: string) => boolean
|
|
104
|
+
dispose: () => void
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ReferenceCodesRuntimeOptions {
|
|
108
|
+
readonly transport: ReferenceCodesTransport
|
|
109
|
+
readonly canRead: () => boolean
|
|
110
|
+
readonly canManage: () => boolean
|
|
111
|
+
readonly createIdempotencyKey?: () => string
|
|
112
|
+
readonly now?: () => string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ActiveRequest {
|
|
116
|
+
readonly controller: AbortController
|
|
117
|
+
readonly generation: number
|
|
118
|
+
readonly id: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const idempotencyKey = (): string => {
|
|
122
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
123
|
+
return `idem_${globalThis.crypto.randomUUID().replaceAll('-', '')}`
|
|
124
|
+
}
|
|
125
|
+
return `idem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const isAbortError = (error: unknown): boolean => (
|
|
129
|
+
typeof error === 'object' && error !== null && 'name' in error && error.name === 'AbortError'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const requestId = (result: ReferenceCodesTransportResult): string | null => {
|
|
133
|
+
if (typeof result.body === 'object' && result.body !== null && !Array.isArray(result.body)) {
|
|
134
|
+
const value = (result.body as Record<string, unknown>).request_id
|
|
135
|
+
if (typeof value === 'string' && value !== '') return value
|
|
136
|
+
}
|
|
137
|
+
const value = result.headers.get('X-Request-Id')
|
|
138
|
+
return value === null || value === '' ? null : value
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const httpError = (result: ReferenceCodesTransportResult): ReferenceCodeRequestError => {
|
|
142
|
+
const body = typeof result.body === 'object' && result.body !== null && !Array.isArray(result.body)
|
|
143
|
+
? result.body as Record<string, unknown>
|
|
144
|
+
: {}
|
|
145
|
+
return {
|
|
146
|
+
kind: 'http',
|
|
147
|
+
message: typeof body.detail === 'string' && body.detail !== ''
|
|
148
|
+
? body.detail
|
|
149
|
+
: `Reference-code request failed (${result.status}).`,
|
|
150
|
+
requestId: requestId(result),
|
|
151
|
+
status: result.status,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const protocolError = (result: ReferenceCodesTransportResult): ReferenceCodeRequestError => ({
|
|
156
|
+
kind: 'protocol',
|
|
157
|
+
message: 'The reference-code response could not be validated.',
|
|
158
|
+
requestId: requestId(result),
|
|
159
|
+
status: result.status,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const statusError = (result: ReferenceCodesTransportResult): ReferenceCodeRequestError => (
|
|
163
|
+
result.status >= 200 && result.status < 300 ? protocolError(result) : httpError(result)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const transportError = (): ReferenceCodeRequestError => ({
|
|
167
|
+
kind: 'transport',
|
|
168
|
+
message: 'The reference-code service could not be reached.',
|
|
169
|
+
requestId: null,
|
|
170
|
+
status: null,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const validationError = (message: string): ReferenceCodeRequestError => ({
|
|
174
|
+
kind: 'validation',
|
|
175
|
+
message,
|
|
176
|
+
requestId: null,
|
|
177
|
+
status: null,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const metadataText = (value: ReferenceCodeMetadata): string => JSON.stringify(value)
|
|
181
|
+
|
|
182
|
+
const emptyDraft = (now: string): ReferenceCodeCreateDraft => ({
|
|
183
|
+
code: '',
|
|
184
|
+
label: '',
|
|
185
|
+
metadataText: '{}',
|
|
186
|
+
status: 'active',
|
|
187
|
+
sortOrder: 0,
|
|
188
|
+
effectiveAt: now,
|
|
189
|
+
expiresAt: null,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const appendDraft = (entry: ReferenceCodeEntry): ReferenceCodeAppendDraft => {
|
|
193
|
+
if (entry.lifecycle === 'retired') throw new Error('REFERENCE_CODE_RETIRED')
|
|
194
|
+
if (entry.effective === null) throw new Error('REFERENCE_CODE_EFFECTIVE_VERSION_MISSING')
|
|
195
|
+
return {
|
|
196
|
+
code: entry.code,
|
|
197
|
+
etag: entry.etag,
|
|
198
|
+
label: entry.effective.label,
|
|
199
|
+
metadataText: metadataText(entry.effective.metadata),
|
|
200
|
+
status: entry.effective.status,
|
|
201
|
+
sortOrder: entry.effective.sortOrder,
|
|
202
|
+
effectiveAt: entry.effective.effectiveAt,
|
|
203
|
+
expiresAt: entry.effective.expiresAt,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const validateDraft = (draft: ReferenceCodeDraftFields): ReferenceCodeVersionInput => {
|
|
208
|
+
const label = draft.label.trim()
|
|
209
|
+
if (label === '' || [...label].length > 160) throw new Error('Enter a label from 1 to 160 characters.')
|
|
210
|
+
if (draft.status !== 'active' && draft.status !== 'inactive') throw new Error('Select a valid status.')
|
|
211
|
+
if (!Number.isInteger(draft.sortOrder) || draft.sortOrder < -1_000_000 || draft.sortOrder > 1_000_000) {
|
|
212
|
+
throw new Error('Sort order must be an integer from -1000000 to 1000000.')
|
|
213
|
+
}
|
|
214
|
+
const effectiveAt = normalizeReferenceCodeInstant(draft.effectiveAt)
|
|
215
|
+
const expiresAt = draft.expiresAt === null || draft.expiresAt === ''
|
|
216
|
+
? null
|
|
217
|
+
: normalizeReferenceCodeInstant(draft.expiresAt)
|
|
218
|
+
if (expiresAt !== null && Date.parse(expiresAt) <= Date.parse(effectiveAt)) {
|
|
219
|
+
throw new Error('Expiration must be later than the effective instant.')
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
label,
|
|
223
|
+
metadata: parseReferenceCodeMetadataText(draft.metadataText),
|
|
224
|
+
status: draft.status,
|
|
225
|
+
sortOrder: draft.sortOrder,
|
|
226
|
+
effectiveAt,
|
|
227
|
+
expiresAt,
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const validateCreateDraft = (draft: ReferenceCodeCreateDraft): ReferenceCodeCreateInput => {
|
|
232
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(draft.code) || draft.code.length > 64) {
|
|
233
|
+
throw new Error('Code must use lower-case slug syntax and be at most 64 characters.')
|
|
234
|
+
}
|
|
235
|
+
return { code: draft.code, ...validateDraft(draft) }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const versionFingerprint = (input: ReferenceCodeVersionInput): string => JSON.stringify({
|
|
239
|
+
label: input.label,
|
|
240
|
+
metadata: Object.fromEntries(Object.entries(input.metadata).sort(([left], [right]) => (
|
|
241
|
+
left < right ? -1 : left > right ? 1 : 0
|
|
242
|
+
))),
|
|
243
|
+
status: input.status,
|
|
244
|
+
sort_order: input.sortOrder,
|
|
245
|
+
effective_at: input.effectiveAt,
|
|
246
|
+
expires_at: input.expiresAt,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const createFingerprint = (input: ReferenceCodeCreateInput): string => JSON.stringify({
|
|
250
|
+
code: input.code,
|
|
251
|
+
version: versionFingerprint(input),
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const replaceFingerprint = (input: ReferenceCodeVersionInput, etag: string): string => JSON.stringify({
|
|
255
|
+
etag,
|
|
256
|
+
version: versionFingerprint(input),
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
export const createReferenceCodesRuntime = (options: ReferenceCodesRuntimeOptions): ReferenceCodesRuntime => {
|
|
260
|
+
const state = reactive<ReferenceCodesRuntimeState>({
|
|
261
|
+
sets: [],
|
|
262
|
+
selectedSet: null,
|
|
263
|
+
asOf: '',
|
|
264
|
+
effectiveStatus: 'all',
|
|
265
|
+
includeRetired: false,
|
|
266
|
+
entries: [],
|
|
267
|
+
page: 1,
|
|
268
|
+
pageSize: 50,
|
|
269
|
+
total: 0,
|
|
270
|
+
createDraft: null,
|
|
271
|
+
appendDraft: null,
|
|
272
|
+
retireCode: null,
|
|
273
|
+
errors: {},
|
|
274
|
+
stale: {},
|
|
275
|
+
pendingResources: new Set<string>(),
|
|
276
|
+
requests: new Set<string>(),
|
|
277
|
+
loading: false,
|
|
278
|
+
})
|
|
279
|
+
const controllers = new Map<string, AbortController>()
|
|
280
|
+
const resourceRequests = new Map<string, ActiveRequest>()
|
|
281
|
+
const createKey = options.createIdempotencyKey ?? idempotencyKey
|
|
282
|
+
const mutationKeys = new Map<string, { fingerprint: string; key: string }>()
|
|
283
|
+
const now = options.now ?? (() => new Date().toISOString())
|
|
284
|
+
let collectionRequest: ActiveRequest | null = null
|
|
285
|
+
let generation = 0
|
|
286
|
+
let sequence = 0
|
|
287
|
+
|
|
288
|
+
const beginRequest = (kind: string): ActiveRequest => {
|
|
289
|
+
sequence += 1
|
|
290
|
+
const id = `${kind}:${sequence}`
|
|
291
|
+
const controller = new AbortController()
|
|
292
|
+
controllers.set(id, controller)
|
|
293
|
+
state.requests.add(id)
|
|
294
|
+
return { controller, generation, id }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const finishRequest = (request: ActiveRequest): void => {
|
|
298
|
+
controllers.delete(request.id)
|
|
299
|
+
state.requests.delete(request.id)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const cancelCollection = (): void => {
|
|
303
|
+
if (collectionRequest === null) return
|
|
304
|
+
const request = collectionRequest
|
|
305
|
+
collectionRequest = null
|
|
306
|
+
request.controller.abort()
|
|
307
|
+
finishRequest(request)
|
|
308
|
+
state.loading = false
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const beginCollection = (kind: string): ActiveRequest => {
|
|
312
|
+
if (resourceRequests.size > 0) throw new Error('REFERENCE_CODES_MUTATION_PENDING')
|
|
313
|
+
cancelCollection()
|
|
314
|
+
const request = beginRequest(kind)
|
|
315
|
+
collectionRequest = request
|
|
316
|
+
state.loading = true
|
|
317
|
+
return request
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const currentCollection = (request: ActiveRequest): boolean => (
|
|
321
|
+
request.generation === generation && collectionRequest?.id === request.id && !request.controller.signal.aborted
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const finishCollection = (request: ActiveRequest): void => {
|
|
325
|
+
if (collectionRequest?.id === request.id) {
|
|
326
|
+
collectionRequest = null
|
|
327
|
+
state.loading = false
|
|
328
|
+
}
|
|
329
|
+
finishRequest(request)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const beginResource = (kind: string, key: string): ActiveRequest => {
|
|
333
|
+
if (resourceRequests.has(key)) throw new Error('REFERENCE_CODE_REQUEST_PENDING')
|
|
334
|
+
cancelCollection()
|
|
335
|
+
const request = beginRequest(`${kind}:${key}`)
|
|
336
|
+
resourceRequests.set(key, request)
|
|
337
|
+
state.pendingResources.add(key)
|
|
338
|
+
return request
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const currentResource = (key: string, request: ActiveRequest): boolean => (
|
|
342
|
+
request.generation === generation
|
|
343
|
+
&& resourceRequests.get(key)?.id === request.id
|
|
344
|
+
&& !request.controller.signal.aborted
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
const finishResource = (key: string, request: ActiveRequest): void => {
|
|
348
|
+
if (resourceRequests.get(key)?.id === request.id) {
|
|
349
|
+
resourceRequests.delete(key)
|
|
350
|
+
state.pendingResources.delete(key)
|
|
351
|
+
}
|
|
352
|
+
finishRequest(request)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const selectedSet = (): ReferenceCodeSetSummary => {
|
|
356
|
+
if (state.selectedSet === null) throw new Error('REFERENCE_CODE_SET_NOT_SELECTED')
|
|
357
|
+
return state.selectedSet
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const currentEntry = (code: string): ReferenceCodeEntry => {
|
|
361
|
+
const entry = state.entries.find(candidate => candidate.code === code)
|
|
362
|
+
if (entry === undefined) throw new Error('REFERENCE_CODE_NOT_FOUND')
|
|
363
|
+
return entry
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const assertMutationEntry = (entry: ReferenceCodeEntry, code: string): void => {
|
|
367
|
+
const set = selectedSet()
|
|
368
|
+
if (entry.moduleKey !== set.moduleKey || entry.setKey !== set.setKey || entry.code !== code) {
|
|
369
|
+
throw new Error('REFERENCE_CODES_RESPONSE_SCOPE_MISMATCH')
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const mutationKey = (scope: string, fingerprint: string): string => {
|
|
374
|
+
const current = mutationKeys.get(scope)
|
|
375
|
+
if (current !== undefined && current.fingerprint === fingerprint) return current.key
|
|
376
|
+
const key = createKey()
|
|
377
|
+
mutationKeys.set(scope, { fingerprint, key })
|
|
378
|
+
return key
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const clearMutationKey = (scope: string): void => {
|
|
382
|
+
mutationKeys.delete(scope)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const applyPageError = (error: ReferenceCodeRequestError): void => {
|
|
386
|
+
state.entries = []
|
|
387
|
+
state.total = 0
|
|
388
|
+
state.errors = { page: error }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const setResourceError = (key: string, error: ReferenceCodeRequestError): void => {
|
|
392
|
+
state.errors[key] = error
|
|
393
|
+
delete state.stale[key]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const parsedMutation = (result: ReferenceCodesTransportResult): ReferenceCodeEntry => {
|
|
397
|
+
const etag = result.headers.get('ETag')
|
|
398
|
+
if (etag === null) throw new Error('REFERENCE_CODES_RESPONSE_ETAG_INVALID')
|
|
399
|
+
return parseReferenceCode(result.body, etag)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const loadSets = async (): Promise<void> => {
|
|
403
|
+
if (!options.canRead()) throw new Error('REFERENCE_CODES_READ_FORBIDDEN')
|
|
404
|
+
const request = beginCollection('sets')
|
|
405
|
+
try {
|
|
406
|
+
let result: ReferenceCodesTransportResult
|
|
407
|
+
try {
|
|
408
|
+
result = await options.transport.listSets(request.controller.signal)
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (!currentCollection(request) || isAbortError(error)) return
|
|
411
|
+
state.sets = []
|
|
412
|
+
applyPageError(transportError())
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
if (!currentCollection(request)) return
|
|
416
|
+
if (result.status !== 200) {
|
|
417
|
+
state.sets = []
|
|
418
|
+
applyPageError(statusError(result))
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
state.sets = parseReferenceCodeSets(result.body)
|
|
423
|
+
state.asOf = normalizeReferenceCodeInstant(now())
|
|
424
|
+
state.errors = {}
|
|
425
|
+
} catch {
|
|
426
|
+
state.sets = []
|
|
427
|
+
applyPageError(protocolError(result))
|
|
428
|
+
}
|
|
429
|
+
} finally {
|
|
430
|
+
finishCollection(request)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const loadEntries = async (): Promise<void> => {
|
|
435
|
+
if (!options.canRead()) throw new Error('REFERENCE_CODES_READ_FORBIDDEN')
|
|
436
|
+
const set = selectedSet()
|
|
437
|
+
const staleCreateCode = state.createDraft?.code
|
|
438
|
+
const reloadsStaleCreate = staleCreateCode !== undefined && state.stale[staleCreateCode] !== undefined
|
|
439
|
+
if (state.asOf === '') state.asOf = normalizeReferenceCodeInstant(now())
|
|
440
|
+
const request = beginCollection('entries')
|
|
441
|
+
try {
|
|
442
|
+
let result: ReferenceCodesTransportResult
|
|
443
|
+
try {
|
|
444
|
+
result = await options.transport.listCodes(set.moduleKey, set.setKey, {
|
|
445
|
+
asOf: state.asOf,
|
|
446
|
+
effectiveStatus: state.effectiveStatus,
|
|
447
|
+
includeRetired: state.includeRetired,
|
|
448
|
+
page: state.page,
|
|
449
|
+
pageSize: state.pageSize,
|
|
450
|
+
}, request.controller.signal)
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (!currentCollection(request) || isAbortError(error)) return
|
|
453
|
+
applyPageError(transportError())
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
if (!currentCollection(request)) return
|
|
457
|
+
if (result.status !== 200) {
|
|
458
|
+
applyPageError(statusError(result))
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const list = parseReferenceCodeList(result.body)
|
|
463
|
+
if (list.asOf !== state.asOf || list.page !== state.page || list.pageSize !== state.pageSize) {
|
|
464
|
+
throw new Error('REFERENCE_CODES_RESPONSE_QUERY_MISMATCH')
|
|
465
|
+
}
|
|
466
|
+
if (list.items.some(entry => entry.moduleKey !== set.moduleKey || entry.setKey !== set.setKey)) {
|
|
467
|
+
throw new Error('REFERENCE_CODES_RESPONSE_SCOPE_MISMATCH')
|
|
468
|
+
}
|
|
469
|
+
state.entries = [...list.items]
|
|
470
|
+
state.total = list.total
|
|
471
|
+
state.errors = {}
|
|
472
|
+
state.stale = {}
|
|
473
|
+
if (reloadsStaleCreate) clearMutationKey('create')
|
|
474
|
+
} catch {
|
|
475
|
+
applyPageError(protocolError(result))
|
|
476
|
+
}
|
|
477
|
+
} finally {
|
|
478
|
+
finishCollection(request)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const createEntry = async (): Promise<void> => {
|
|
483
|
+
const draft = state.createDraft
|
|
484
|
+
if (draft === null) throw new Error('REFERENCE_CODE_CREATE_DRAFT_MISSING')
|
|
485
|
+
if (!options.canManage()) {
|
|
486
|
+
setResourceError('create', validationError('You do not have permission to manage reference codes.'))
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
let input: ReferenceCodeCreateInput
|
|
490
|
+
try {
|
|
491
|
+
input = validateCreateDraft(draft)
|
|
492
|
+
} catch (error) {
|
|
493
|
+
setResourceError('create', validationError(error instanceof Error ? error.message : 'The create form is invalid.'))
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
const set = selectedSet()
|
|
497
|
+
const scope = 'create'
|
|
498
|
+
const fingerprint = createFingerprint(input)
|
|
499
|
+
const request = beginResource('create', input.code)
|
|
500
|
+
let reload = false
|
|
501
|
+
delete state.errors.create
|
|
502
|
+
try {
|
|
503
|
+
let result: ReferenceCodesTransportResult
|
|
504
|
+
try {
|
|
505
|
+
result = await options.transport.create(set.moduleKey, set.setKey, {
|
|
506
|
+
input,
|
|
507
|
+
idempotencyKey: mutationKey(scope, fingerprint),
|
|
508
|
+
signal: request.controller.signal,
|
|
509
|
+
})
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (!currentResource(input.code, request) || isAbortError(error)) return
|
|
512
|
+
setResourceError('create', transportError())
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
if (!currentResource(input.code, request)) return
|
|
516
|
+
if (result.status === 412) {
|
|
517
|
+
const conflict = httpError(result)
|
|
518
|
+
state.stale[input.code] = { message: conflict.message, requestId: conflict.requestId }
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
if (result.status !== 201) {
|
|
522
|
+
setResourceError('create', statusError(result))
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const parsed = parsedMutation(result)
|
|
527
|
+
assertMutationEntry(parsed, input.code)
|
|
528
|
+
if (Date.parse(state.asOf) < Date.parse(parsed.createdAt)) state.asOf = parsed.createdAt
|
|
529
|
+
state.createDraft = null
|
|
530
|
+
delete state.stale[input.code]
|
|
531
|
+
clearMutationKey(scope)
|
|
532
|
+
reload = true
|
|
533
|
+
} catch {
|
|
534
|
+
setResourceError('create', protocolError(result))
|
|
535
|
+
}
|
|
536
|
+
} finally {
|
|
537
|
+
finishResource(input.code, request)
|
|
538
|
+
}
|
|
539
|
+
if (reload) await loadEntries()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const replaceEntry = async (): Promise<void> => {
|
|
543
|
+
const draft = state.appendDraft
|
|
544
|
+
if (draft === null) throw new Error('REFERENCE_CODE_APPEND_DRAFT_MISSING')
|
|
545
|
+
if (!options.canManage()) {
|
|
546
|
+
setResourceError(draft.code, validationError('You do not have permission to manage reference codes.'))
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
let input: ReferenceCodeVersionInput
|
|
550
|
+
try {
|
|
551
|
+
input = validateDraft(draft)
|
|
552
|
+
} catch (error) {
|
|
553
|
+
setResourceError(draft.code, validationError(error instanceof Error ? error.message : 'The version form is invalid.'))
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
const set = selectedSet()
|
|
557
|
+
const scope = `replace:${draft.code}`
|
|
558
|
+
const fingerprint = replaceFingerprint(input, draft.etag)
|
|
559
|
+
const request = beginResource('replace', draft.code)
|
|
560
|
+
let reload = false
|
|
561
|
+
delete state.errors[draft.code]
|
|
562
|
+
delete state.stale[draft.code]
|
|
563
|
+
try {
|
|
564
|
+
let result: ReferenceCodesTransportResult
|
|
565
|
+
try {
|
|
566
|
+
result = await options.transport.replace(set.moduleKey, set.setKey, draft.code, {
|
|
567
|
+
input,
|
|
568
|
+
etag: draft.etag,
|
|
569
|
+
idempotencyKey: mutationKey(scope, fingerprint),
|
|
570
|
+
signal: request.controller.signal,
|
|
571
|
+
})
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (!currentResource(draft.code, request) || isAbortError(error)) return
|
|
574
|
+
setResourceError(draft.code, transportError())
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
if (!currentResource(draft.code, request)) return
|
|
578
|
+
if (result.status === 412) {
|
|
579
|
+
const conflict = httpError(result)
|
|
580
|
+
state.stale[draft.code] = { message: conflict.message, requestId: conflict.requestId }
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
if (result.status !== 200) {
|
|
584
|
+
setResourceError(draft.code, statusError(result))
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
assertMutationEntry(parsedMutation(result), draft.code)
|
|
589
|
+
state.appendDraft = null
|
|
590
|
+
clearMutationKey(scope)
|
|
591
|
+
reload = true
|
|
592
|
+
} catch {
|
|
593
|
+
setResourceError(draft.code, protocolError(result))
|
|
594
|
+
}
|
|
595
|
+
} finally {
|
|
596
|
+
finishResource(draft.code, request)
|
|
597
|
+
}
|
|
598
|
+
if (reload) await loadEntries()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const reloadStale = async (code: string): Promise<void> => {
|
|
602
|
+
currentEntry(code)
|
|
603
|
+
const set = selectedSet()
|
|
604
|
+
const request = beginResource('reload', code)
|
|
605
|
+
let reload = false
|
|
606
|
+
try {
|
|
607
|
+
let result: ReferenceCodesTransportResult
|
|
608
|
+
try {
|
|
609
|
+
result = await options.transport.getCode(
|
|
610
|
+
set.moduleKey,
|
|
611
|
+
set.setKey,
|
|
612
|
+
code,
|
|
613
|
+
state.asOf,
|
|
614
|
+
request.controller.signal,
|
|
615
|
+
)
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (!currentResource(code, request) || isAbortError(error)) return
|
|
618
|
+
setResourceError(code, transportError())
|
|
619
|
+
return
|
|
620
|
+
}
|
|
621
|
+
if (!currentResource(code, request)) return
|
|
622
|
+
if (result.status !== 200) {
|
|
623
|
+
setResourceError(code, statusError(result))
|
|
624
|
+
return
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const parsed = parsedMutation(result)
|
|
628
|
+
assertMutationEntry(parsed, code)
|
|
629
|
+
if (state.appendDraft?.code === code) state.appendDraft.etag = parsed.etag
|
|
630
|
+
delete state.stale[code]
|
|
631
|
+
delete state.errors[code]
|
|
632
|
+
clearMutationKey(`replace:${code}`)
|
|
633
|
+
clearMutationKey(`retire:${code}`)
|
|
634
|
+
reload = true
|
|
635
|
+
} catch {
|
|
636
|
+
setResourceError(code, protocolError(result))
|
|
637
|
+
}
|
|
638
|
+
} finally {
|
|
639
|
+
finishResource(code, request)
|
|
640
|
+
}
|
|
641
|
+
if (reload) await loadEntries()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const retireEntry = async (): Promise<void> => {
|
|
645
|
+
const code = state.retireCode
|
|
646
|
+
if (code === null) throw new Error('REFERENCE_CODE_RETIRE_TARGET_MISSING')
|
|
647
|
+
const entry = currentEntry(code)
|
|
648
|
+
if (entry.lifecycle === 'retired') {
|
|
649
|
+
setResourceError(code, validationError('This reference-code identity is already retired.'))
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
if (!options.canManage()) {
|
|
653
|
+
setResourceError(code, validationError('You do not have permission to manage reference codes.'))
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
const set = selectedSet()
|
|
657
|
+
const scope = `retire:${code}`
|
|
658
|
+
const fingerprint = entry.etag
|
|
659
|
+
const request = beginResource('retire', code)
|
|
660
|
+
let reload = false
|
|
661
|
+
delete state.errors[code]
|
|
662
|
+
delete state.stale[code]
|
|
663
|
+
try {
|
|
664
|
+
let result: ReferenceCodesTransportResult
|
|
665
|
+
try {
|
|
666
|
+
result = await options.transport.retire(set.moduleKey, set.setKey, code, {
|
|
667
|
+
etag: entry.etag,
|
|
668
|
+
idempotencyKey: mutationKey(scope, fingerprint),
|
|
669
|
+
signal: request.controller.signal,
|
|
670
|
+
})
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (!currentResource(code, request) || isAbortError(error)) return
|
|
673
|
+
setResourceError(code, transportError())
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
if (!currentResource(code, request)) return
|
|
677
|
+
if (result.status === 412) {
|
|
678
|
+
const conflict = httpError(result)
|
|
679
|
+
state.stale[code] = { message: conflict.message, requestId: conflict.requestId }
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
if (result.status !== 200) {
|
|
683
|
+
setResourceError(code, statusError(result))
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
assertMutationEntry(parsedMutation(result), code)
|
|
688
|
+
state.retireCode = null
|
|
689
|
+
clearMutationKey(scope)
|
|
690
|
+
reload = true
|
|
691
|
+
} catch {
|
|
692
|
+
setResourceError(code, protocolError(result))
|
|
693
|
+
}
|
|
694
|
+
} finally {
|
|
695
|
+
finishResource(code, request)
|
|
696
|
+
}
|
|
697
|
+
if (reload) await loadEntries()
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const runtime: ReferenceCodesRuntime = {
|
|
701
|
+
state,
|
|
702
|
+
canManage: options.canManage,
|
|
703
|
+
loadSets,
|
|
704
|
+
async selectSet(moduleKey, setKey) {
|
|
705
|
+
const selected = state.sets.find(set => set.moduleKey === moduleKey && set.setKey === setKey)
|
|
706
|
+
if (selected === undefined) throw new Error('REFERENCE_CODE_SET_UNKNOWN')
|
|
707
|
+
state.selectedSet = selected
|
|
708
|
+
state.page = 1
|
|
709
|
+
state.entries = []
|
|
710
|
+
state.total = 0
|
|
711
|
+
state.createDraft = null
|
|
712
|
+
state.appendDraft = null
|
|
713
|
+
state.retireCode = null
|
|
714
|
+
state.errors = {}
|
|
715
|
+
state.stale = {}
|
|
716
|
+
mutationKeys.clear()
|
|
717
|
+
await loadEntries()
|
|
718
|
+
},
|
|
719
|
+
async setAsOf(value) {
|
|
720
|
+
state.asOf = normalizeReferenceCodeInstant(value)
|
|
721
|
+
state.page = 1
|
|
722
|
+
if (state.selectedSet !== null) await loadEntries()
|
|
723
|
+
},
|
|
724
|
+
async setFilters(effectiveStatus, includeRetired) {
|
|
725
|
+
state.effectiveStatus = effectiveStatus
|
|
726
|
+
state.includeRetired = includeRetired
|
|
727
|
+
state.page = 1
|
|
728
|
+
if (state.selectedSet !== null) await loadEntries()
|
|
729
|
+
},
|
|
730
|
+
async setPage(page) {
|
|
731
|
+
if (!Number.isInteger(page) || page < 1 || page > 10_000) throw new Error('REFERENCE_CODES_PAGE_INVALID')
|
|
732
|
+
state.page = page
|
|
733
|
+
await loadEntries()
|
|
734
|
+
},
|
|
735
|
+
loadEntries,
|
|
736
|
+
beginCreate() {
|
|
737
|
+
selectedSet()
|
|
738
|
+
if (state.appendDraft !== null) clearMutationKey(`replace:${state.appendDraft.code}`)
|
|
739
|
+
if (state.retireCode !== null) clearMutationKey(`retire:${state.retireCode}`)
|
|
740
|
+
clearMutationKey('create')
|
|
741
|
+
state.appendDraft = null
|
|
742
|
+
state.retireCode = null
|
|
743
|
+
state.createDraft = emptyDraft(state.asOf === '' ? normalizeReferenceCodeInstant(now()) : state.asOf)
|
|
744
|
+
delete state.errors.create
|
|
745
|
+
},
|
|
746
|
+
updateCreateDraft(changes) {
|
|
747
|
+
if (state.createDraft === null) throw new Error('REFERENCE_CODE_CREATE_DRAFT_MISSING')
|
|
748
|
+
clearMutationKey('create')
|
|
749
|
+
Object.assign(state.createDraft, changes)
|
|
750
|
+
delete state.errors.create
|
|
751
|
+
},
|
|
752
|
+
cancelCreate() {
|
|
753
|
+
clearMutationKey('create')
|
|
754
|
+
state.createDraft = null
|
|
755
|
+
delete state.errors.create
|
|
756
|
+
},
|
|
757
|
+
create: createEntry,
|
|
758
|
+
beginAppend(entry) {
|
|
759
|
+
currentEntry(entry.code)
|
|
760
|
+
clearMutationKey('create')
|
|
761
|
+
if (state.appendDraft !== null) clearMutationKey(`replace:${state.appendDraft.code}`)
|
|
762
|
+
if (state.retireCode !== null) clearMutationKey(`retire:${state.retireCode}`)
|
|
763
|
+
clearMutationKey(`replace:${entry.code}`)
|
|
764
|
+
state.createDraft = null
|
|
765
|
+
state.retireCode = null
|
|
766
|
+
state.appendDraft = appendDraft(entry)
|
|
767
|
+
delete state.errors[entry.code]
|
|
768
|
+
},
|
|
769
|
+
updateAppendDraft(changes) {
|
|
770
|
+
if (state.appendDraft === null) throw new Error('REFERENCE_CODE_APPEND_DRAFT_MISSING')
|
|
771
|
+
clearMutationKey(`replace:${state.appendDraft.code}`)
|
|
772
|
+
Object.assign(state.appendDraft, changes)
|
|
773
|
+
delete state.errors[state.appendDraft.code]
|
|
774
|
+
},
|
|
775
|
+
cancelAppend() {
|
|
776
|
+
if (state.appendDraft !== null) clearMutationKey(`replace:${state.appendDraft.code}`)
|
|
777
|
+
state.appendDraft = null
|
|
778
|
+
},
|
|
779
|
+
appendVersion: replaceEntry,
|
|
780
|
+
reloadStale,
|
|
781
|
+
beginRetire(entry) {
|
|
782
|
+
const current = currentEntry(entry.code)
|
|
783
|
+
if (current.lifecycle === 'retired') throw new Error('REFERENCE_CODE_RETIRED')
|
|
784
|
+
clearMutationKey('create')
|
|
785
|
+
if (state.appendDraft !== null) clearMutationKey(`replace:${state.appendDraft.code}`)
|
|
786
|
+
if (state.retireCode !== null) clearMutationKey(`retire:${state.retireCode}`)
|
|
787
|
+
clearMutationKey(`retire:${current.code}`)
|
|
788
|
+
state.createDraft = null
|
|
789
|
+
state.appendDraft = null
|
|
790
|
+
state.retireCode = current.code
|
|
791
|
+
delete state.errors[current.code]
|
|
792
|
+
},
|
|
793
|
+
cancelRetire() {
|
|
794
|
+
if (state.retireCode !== null) clearMutationKey(`retire:${state.retireCode}`)
|
|
795
|
+
state.retireCode = null
|
|
796
|
+
},
|
|
797
|
+
retire: retireEntry,
|
|
798
|
+
isPending: code => state.pendingResources.has(code),
|
|
799
|
+
dispose() {
|
|
800
|
+
generation += 1
|
|
801
|
+
collectionRequest = null
|
|
802
|
+
resourceRequests.clear()
|
|
803
|
+
for (const controller of controllers.values()) controller.abort()
|
|
804
|
+
controllers.clear()
|
|
805
|
+
mutationKeys.clear()
|
|
806
|
+
state.sets = []
|
|
807
|
+
state.selectedSet = null
|
|
808
|
+
state.asOf = ''
|
|
809
|
+
state.effectiveStatus = 'all'
|
|
810
|
+
state.includeRetired = false
|
|
811
|
+
state.entries = []
|
|
812
|
+
state.page = 1
|
|
813
|
+
state.pageSize = 50
|
|
814
|
+
state.total = 0
|
|
815
|
+
state.createDraft = null
|
|
816
|
+
state.appendDraft = null
|
|
817
|
+
state.retireCode = null
|
|
818
|
+
state.errors = {}
|
|
819
|
+
state.stale = {}
|
|
820
|
+
state.pendingResources.clear()
|
|
821
|
+
state.requests.clear()
|
|
822
|
+
state.loading = false
|
|
823
|
+
},
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return runtime
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export const referenceCodesRuntimeKey: InjectionKey<ReferenceCodesRuntime> = Symbol(REFERENCE_CODES_STORE_KEY)
|
|
830
|
+
|
|
831
|
+
export const useReferenceCodesRuntime = (): ReferenceCodesRuntime => {
|
|
832
|
+
const runtime = inject(referenceCodesRuntimeKey, null)
|
|
833
|
+
if (runtime === null) throw new Error('REFERENCE_CODES_RUNTIME_NOT_INSTALLED')
|
|
834
|
+
return runtime
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export const createReferenceCodesModuleContribution = (
|
|
838
|
+
runtime: ReferenceCodesRuntime,
|
|
839
|
+
): AdminModuleContribution => defineAdminModule({
|
|
840
|
+
key: REFERENCE_CODES_MODULE_KEY,
|
|
841
|
+
routes: [{
|
|
842
|
+
name: REFERENCE_CODES_ROUTE_NAME,
|
|
843
|
+
path: REFERENCE_CODES_ROUTE_PATH,
|
|
844
|
+
component: () => import('./ReferenceCodesPage.vue'),
|
|
845
|
+
access: {
|
|
846
|
+
moduleKey: REFERENCE_CODES_MODULE_KEY,
|
|
847
|
+
permissionKeys: [REFERENCE_CODES_READ_PERMISSION],
|
|
848
|
+
},
|
|
849
|
+
}],
|
|
850
|
+
disposeOnTenantChange: true,
|
|
851
|
+
stores: [{
|
|
852
|
+
key: REFERENCE_CODES_STORE_KEY,
|
|
853
|
+
dispose: () => runtime.dispose(),
|
|
854
|
+
}],
|
|
855
|
+
})
|