@peanut-admin/admin 0.1.0-alpha.11

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 (73) hide show
  1. package/LICENSE +202 -0
  2. package/admin-core/src/access/access.ts +27 -0
  3. package/admin-core/src/access/permission-policy.ts +37 -0
  4. package/admin-core/src/api/client.ts +200 -0
  5. package/admin-core/src/api/problem.ts +67 -0
  6. package/admin-core/src/api/refresh.ts +122 -0
  7. package/admin-core/src/auth/stores.ts +121 -0
  8. package/admin-core/src/auth/tenant-session.ts +32 -0
  9. package/admin-core/src/generated/api.d.ts +22106 -0
  10. package/admin-core/src/governance/audit.ts +56 -0
  11. package/admin-core/src/governance/catalog.ts +86 -0
  12. package/admin-core/src/governance/index.ts +26 -0
  13. package/admin-core/src/governance/menu.ts +63 -0
  14. package/admin-core/src/governance/roles.ts +144 -0
  15. package/admin-core/src/governance/types.ts +64 -0
  16. package/admin-core/src/index.ts +122 -0
  17. package/admin-core/src/lifecycle/tenant.ts +59 -0
  18. package/admin-core/src/module/contribution.ts +124 -0
  19. package/admin-core/src/module/plugin-contribution-policy.ts +51 -0
  20. package/admin-core/src/module/tenant-modules.ts +20 -0
  21. package/admin-core/src/runtime/config.ts +55 -0
  22. package/admin-core/src/runtime/errors.ts +81 -0
  23. package/admin-core/src/runtime/guard.ts +40 -0
  24. package/admin-core/src/runtime/navigation.ts +105 -0
  25. package/admin-core/src/runtime/overrides.ts +214 -0
  26. package/admin-core/src/targets/store.ts +153 -0
  27. package/admin-shell/src/config.ts +84 -0
  28. package/admin-shell/src/deployment-mode.ts +36 -0
  29. package/admin-shell/src/index.ts +44 -0
  30. package/admin-shell/src/layout.ts +332 -0
  31. package/admin-shell/src/overrides.ts +53 -0
  32. package/admin-shell/src/states.ts +93 -0
  33. package/admin-shell/src/tabs.ts +31 -0
  34. package/admin-shell/src/targets.ts +128 -0
  35. package/admin-shell/src/theme.ts +15 -0
  36. package/client-core/src/index.ts +415 -0
  37. package/client-nuxt/src/index.ts +48 -0
  38. package/client-uniapp/src/index.ts +50 -0
  39. package/file-media/src/FileAssetSelector.vue +117 -0
  40. package/file-media/src/FileMediaPage.vue +158 -0
  41. package/file-media/src/contracts.ts +220 -0
  42. package/file-media/src/index.ts +19 -0
  43. package/file-media/src/runtime.ts +210 -0
  44. package/import-export/src/ImportExportPage.vue +155 -0
  45. package/import-export/src/contracts.ts +96 -0
  46. package/import-export/src/index.ts +3 -0
  47. package/import-export/src/runtime.ts +128 -0
  48. package/integration-security/src/IntegrationSecurityPage.vue +402 -0
  49. package/integration-security/src/contracts.ts +171 -0
  50. package/integration-security/src/index.ts +3 -0
  51. package/integration-security/src/runtime.ts +180 -0
  52. package/notification-sms/src/NotificationInboxPage.vue +266 -0
  53. package/notification-sms/src/contracts.ts +195 -0
  54. package/notification-sms/src/index.ts +4 -0
  55. package/notification-sms/src/runtime.ts +143 -0
  56. package/ops-console/src/OpsConsolePage.vue +337 -0
  57. package/ops-console/src/contracts.ts +169 -0
  58. package/ops-console/src/index.ts +3 -0
  59. package/ops-console/src/runtime.ts +199 -0
  60. package/package.json +139 -0
  61. package/reference-codes/src/ReferenceCodesPage.vue +942 -0
  62. package/reference-codes/src/contracts.ts +484 -0
  63. package/reference-codes/src/index.ts +53 -0
  64. package/reference-codes/src/runtime.ts +855 -0
  65. package/settings/src/SettingsPage.vue +536 -0
  66. package/settings/src/contracts.ts +331 -0
  67. package/settings/src/index.ts +45 -0
  68. package/settings/src/runtime.ts +545 -0
  69. package/task-job/src/TaskJobPage.vue +120 -0
  70. package/task-job/src/contracts.ts +117 -0
  71. package/task-job/src/index.ts +2 -0
  72. package/task-job/src/runtime.ts +105 -0
  73. 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
+ })