@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,117 @@
|
|
|
1
|
+
export type TaskStatus = 'queued' | 'running' | 'succeeded' | 'dead' | 'cancelled'
|
|
2
|
+
|
|
3
|
+
export interface TaskJob {
|
|
4
|
+
readonly jobKey: string
|
|
5
|
+
readonly taskType: string
|
|
6
|
+
readonly status: TaskStatus
|
|
7
|
+
readonly attemptCount: number
|
|
8
|
+
readonly maxAttempts: number
|
|
9
|
+
readonly revision: number
|
|
10
|
+
readonly lastErrorCode: string | null
|
|
11
|
+
readonly availableAt: string
|
|
12
|
+
readonly createdAt: string
|
|
13
|
+
readonly updatedAt: string
|
|
14
|
+
readonly completedAt: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TaskJobList {
|
|
18
|
+
readonly items: readonly TaskJob[]
|
|
19
|
+
readonly page: number
|
|
20
|
+
readonly pageSize: number
|
|
21
|
+
readonly total: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TaskTransportResult {
|
|
25
|
+
readonly body: unknown
|
|
26
|
+
readonly headers: Headers
|
|
27
|
+
readonly status: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TaskJobTransport {
|
|
31
|
+
list: (status: TaskStatus, page: number, pageSize: number, signal: AbortSignal) => Promise<TaskTransportResult>
|
|
32
|
+
cancel: (jobKey: string, revision: number, signal: AbortSignal) => Promise<TaskTransportResult>
|
|
33
|
+
retry: (jobKey: string, revision: number, signal: AbortSignal) => Promise<TaskTransportResult>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const jobKeyPattern = /^job_[0-9a-f]{32}$/
|
|
37
|
+
const taskTypePattern = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/
|
|
38
|
+
const errorCodePattern = /^[A-Z][A-Z0-9_]{2,63}$/
|
|
39
|
+
const statuses: readonly TaskStatus[] = ['queued', 'running', 'succeeded', 'dead', 'cancelled']
|
|
40
|
+
|
|
41
|
+
const record = (value: unknown): Record<string, unknown> => {
|
|
42
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('TASK_RESPONSE_INVALID')
|
|
43
|
+
return value as Record<string, unknown>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const exactKeys = (value: Record<string, unknown>, keys: readonly string[]): void => {
|
|
47
|
+
const actual = Object.keys(value).sort()
|
|
48
|
+
const expected = [...keys].sort()
|
|
49
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
50
|
+
throw new Error('TASK_RESPONSE_INVALID')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timestamp = (value: unknown): value is string => typeof value === 'string'
|
|
55
|
+
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
|
|
56
|
+
&& Number.isFinite(Date.parse(value))
|
|
57
|
+
|
|
58
|
+
export const parseTaskJob = (value: unknown): TaskJob => {
|
|
59
|
+
const item = record(value)
|
|
60
|
+
exactKeys(item, [
|
|
61
|
+
'job_key', 'task_type', 'status', 'attempt_count', 'max_attempts', 'revision',
|
|
62
|
+
'last_error_code', 'available_at', 'created_at', 'updated_at', 'completed_at',
|
|
63
|
+
])
|
|
64
|
+
if (
|
|
65
|
+
typeof item.job_key !== 'string' || !jobKeyPattern.test(item.job_key)
|
|
66
|
+
|| typeof item.task_type !== 'string' || !taskTypePattern.test(item.task_type)
|
|
67
|
+
|| typeof item.status !== 'string' || !statuses.includes(item.status as TaskStatus)
|
|
68
|
+
|| typeof item.attempt_count !== 'number' || !Number.isSafeInteger(item.attempt_count) || item.attempt_count < 0
|
|
69
|
+
|| typeof item.max_attempts !== 'number' || !Number.isSafeInteger(item.max_attempts) || item.max_attempts < 1 || item.max_attempts > 10
|
|
70
|
+
|| item.attempt_count > item.max_attempts
|
|
71
|
+
|| typeof item.revision !== 'number' || !Number.isSafeInteger(item.revision) || item.revision < 1
|
|
72
|
+
|| (item.last_error_code !== null && (typeof item.last_error_code !== 'string' || !errorCodePattern.test(item.last_error_code)))
|
|
73
|
+
|| !timestamp(item.available_at) || !timestamp(item.created_at) || !timestamp(item.updated_at)
|
|
74
|
+
|| (item.completed_at !== null && !timestamp(item.completed_at))
|
|
75
|
+
|| (['succeeded', 'dead', 'cancelled'].includes(item.status) !== (item.completed_at !== null))
|
|
76
|
+
) throw new Error('TASK_RESPONSE_INVALID')
|
|
77
|
+
return {
|
|
78
|
+
jobKey: item.job_key,
|
|
79
|
+
taskType: item.task_type,
|
|
80
|
+
status: item.status as TaskStatus,
|
|
81
|
+
attemptCount: item.attempt_count,
|
|
82
|
+
maxAttempts: item.max_attempts,
|
|
83
|
+
revision: item.revision,
|
|
84
|
+
lastErrorCode: item.last_error_code,
|
|
85
|
+
availableAt: item.available_at,
|
|
86
|
+
createdAt: item.created_at,
|
|
87
|
+
updatedAt: item.updated_at,
|
|
88
|
+
completedAt: item.completed_at,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const parseTaskResponse = (value: unknown): TaskJob => {
|
|
93
|
+
const body = record(value)
|
|
94
|
+
exactKeys(body, ['data', 'meta'])
|
|
95
|
+
record(body.meta)
|
|
96
|
+
return parseTaskJob(body.data)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const parseTaskList = (value: unknown): TaskJobList => {
|
|
100
|
+
const body = record(value)
|
|
101
|
+
exactKeys(body, ['data', 'meta'])
|
|
102
|
+
const data = record(body.data)
|
|
103
|
+
const meta = record(body.meta)
|
|
104
|
+
exactKeys(data, ['items'])
|
|
105
|
+
if (!Array.isArray(data.items)) throw new Error('TASK_RESPONSE_INVALID')
|
|
106
|
+
for (const key of ['page', 'page_size', 'total'] as const) {
|
|
107
|
+
if (typeof meta[key] !== 'number' || !Number.isSafeInteger(meta[key]) || meta[key] < (key === 'total' ? 0 : 1)) {
|
|
108
|
+
throw new Error('TASK_RESPONSE_INVALID')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
items: data.items.map(parseTaskJob),
|
|
113
|
+
page: meta.page as number,
|
|
114
|
+
pageSize: meta.page_size as number,
|
|
115
|
+
total: meta.total as number,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { parseTaskList, parseTaskResponse } from './contracts'
|
|
7
|
+
import type { TaskJob, TaskJobTransport, TaskStatus, TaskTransportResult } from './contracts'
|
|
8
|
+
|
|
9
|
+
export const TASK_JOB_MODULE_KEY = 'peanut.task-job' as const
|
|
10
|
+
export const TASK_JOB_ROUTE_NAME = 'peanut.task-job.list' as const
|
|
11
|
+
export const TASK_JOB_ROUTE_PATH = '/app/tasks' as const
|
|
12
|
+
export const TASK_JOB_READ_PERMISSION = 'peanut.task-job.read' as const
|
|
13
|
+
export const TASK_JOB_MANAGE_PERMISSION = 'peanut.task-job.manage' as const
|
|
14
|
+
export const TASK_JOB_STORE_KEY = 'peanut.task-job.runtime' as const
|
|
15
|
+
|
|
16
|
+
export interface TaskJobError { readonly message: string; readonly requestId: string | null; readonly status: number | null }
|
|
17
|
+
export interface TaskJobState {
|
|
18
|
+
items: TaskJob[]; status: TaskStatus; page: number; pageSize: number; total: number
|
|
19
|
+
loading: boolean; mutating: boolean; error: TaskJobError | null
|
|
20
|
+
}
|
|
21
|
+
export interface TaskJobRuntime {
|
|
22
|
+
readonly state: TaskJobState
|
|
23
|
+
readonly canManage: () => boolean
|
|
24
|
+
load: () => Promise<void>
|
|
25
|
+
setStatus: (status: TaskStatus) => Promise<void>
|
|
26
|
+
cancel: (job: TaskJob) => Promise<void>
|
|
27
|
+
retry: (job: TaskJob) => Promise<void>
|
|
28
|
+
dispose: () => void
|
|
29
|
+
}
|
|
30
|
+
export interface TaskJobRuntimeOptions {
|
|
31
|
+
readonly transport: TaskJobTransport
|
|
32
|
+
readonly canRead: () => boolean
|
|
33
|
+
readonly canManage: () => boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const failure = (result: TaskTransportResult): TaskJobError => {
|
|
37
|
+
const body = typeof result.body === 'object' && result.body !== null && !Array.isArray(result.body)
|
|
38
|
+
? result.body as Record<string, unknown> : {}
|
|
39
|
+
const id = body.request_id ?? result.headers.get('X-Request-Id')
|
|
40
|
+
return {
|
|
41
|
+
message: typeof body.detail === 'string' && body.detail !== '' ? body.detail : `Task request failed (${result.status}).`,
|
|
42
|
+
requestId: typeof id === 'string' && id !== '' ? id : null,
|
|
43
|
+
status: result.status,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const createTaskJobRuntime = (options: TaskJobRuntimeOptions): TaskJobRuntime => {
|
|
48
|
+
const state = reactive<TaskJobState>({
|
|
49
|
+
items: [], status: 'queued', page: 1, pageSize: 20, total: 0,
|
|
50
|
+
loading: false, mutating: false, error: null,
|
|
51
|
+
})
|
|
52
|
+
const controllers = new Set<AbortController>()
|
|
53
|
+
let generation = 0
|
|
54
|
+
const run = async <T>(operation: (signal: AbortSignal) => Promise<T>): Promise<T> => {
|
|
55
|
+
const controller = new AbortController(); controllers.add(controller)
|
|
56
|
+
try { return await operation(controller.signal) } finally { controllers.delete(controller) }
|
|
57
|
+
}
|
|
58
|
+
const load = async (): Promise<void> => {
|
|
59
|
+
const current = ++generation; state.loading = true; state.error = null
|
|
60
|
+
try {
|
|
61
|
+
if (!options.canRead()) throw new Error('TASK_PERMISSION_DENIED')
|
|
62
|
+
const result = await run(signal => options.transport.list(state.status, state.page, state.pageSize, signal))
|
|
63
|
+
if (current !== generation) return
|
|
64
|
+
if (result.status !== 200) { state.error = failure(result); return }
|
|
65
|
+
const list = parseTaskList(result.body)
|
|
66
|
+
state.items = [...list.items]; state.page = list.page; state.pageSize = list.pageSize; state.total = list.total
|
|
67
|
+
} catch {
|
|
68
|
+
if (current === generation) state.error = { message: 'The task service could not be reached.', requestId: null, status: null }
|
|
69
|
+
} finally { if (current === generation) state.loading = false }
|
|
70
|
+
}
|
|
71
|
+
const mutate = async (job: TaskJob, operation: 'cancel' | 'retry'): Promise<void> => {
|
|
72
|
+
if (!options.canManage() || state.mutating) return
|
|
73
|
+
state.mutating = true; state.error = null
|
|
74
|
+
try {
|
|
75
|
+
const result = await run(signal => options.transport[operation](job.jobKey, job.revision, signal))
|
|
76
|
+
if (result.status !== 200) { state.error = failure(result); return }
|
|
77
|
+
parseTaskResponse(result.body); await load()
|
|
78
|
+
} catch { state.error = { message: 'The task action could not be completed.', requestId: null, status: null } }
|
|
79
|
+
finally { state.mutating = false }
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
state, canManage: options.canManage, load,
|
|
83
|
+
async setStatus(status) { state.status = status; state.page = 1; await load() },
|
|
84
|
+
cancel: job => mutate(job, 'cancel'),
|
|
85
|
+
retry: job => mutate(job, 'retry'),
|
|
86
|
+
dispose() { generation += 1; for (const controller of controllers) controller.abort(); controllers.clear() },
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const taskJobRuntimeKey: InjectionKey<TaskJobRuntime> = Symbol(TASK_JOB_STORE_KEY)
|
|
91
|
+
export const useTaskJobRuntime = (): TaskJobRuntime => {
|
|
92
|
+
const runtime = inject(taskJobRuntimeKey)
|
|
93
|
+
if (runtime === undefined) throw new Error('TASK_JOB_RUNTIME_MISSING')
|
|
94
|
+
return runtime
|
|
95
|
+
}
|
|
96
|
+
export const createTaskJobModuleContribution = (runtime: TaskJobRuntime): AdminModuleContribution => defineAdminModule({
|
|
97
|
+
key: TASK_JOB_MODULE_KEY,
|
|
98
|
+
routes: [{
|
|
99
|
+
name: TASK_JOB_ROUTE_NAME, path: TASK_JOB_ROUTE_PATH,
|
|
100
|
+
component: async () => ({ default: (await import('./TaskJobPage.vue')).default }),
|
|
101
|
+
access: { moduleKey: TASK_JOB_MODULE_KEY, permissionKeys: [TASK_JOB_READ_PERMISSION] },
|
|
102
|
+
}],
|
|
103
|
+
disposeOnTenantChange: true,
|
|
104
|
+
stores: [{ key: TASK_JOB_STORE_KEY, dispose: runtime.dispose }],
|
|
105
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlatformContextData,
|
|
3
|
+
ProblemDetails,
|
|
4
|
+
TenantContextData,
|
|
5
|
+
} from '@peanut-admin/admin/core'
|
|
6
|
+
|
|
7
|
+
export const WEB_TESTING_PACKAGE = '@peanut-admin/admin/testing' as const
|
|
8
|
+
export const WEB_TESTING_VERSION = '0.1.0' as const
|
|
9
|
+
|
|
10
|
+
export const mockTenantContext = (
|
|
11
|
+
overrides: Partial<TenantContextData> = {},
|
|
12
|
+
): TenantContextData => ({
|
|
13
|
+
audience: 'tenant',
|
|
14
|
+
accountId: '1',
|
|
15
|
+
tenantId: '10',
|
|
16
|
+
memberId: '20',
|
|
17
|
+
moduleKeys: ['core'],
|
|
18
|
+
permissionKeys: ['core.member.read'],
|
|
19
|
+
authorizationRevision: '1',
|
|
20
|
+
...overrides,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const mockPlatformContext = (
|
|
24
|
+
overrides: Partial<PlatformContextData> = {},
|
|
25
|
+
): PlatformContextData => ({
|
|
26
|
+
audience: 'platform',
|
|
27
|
+
accountId: '1',
|
|
28
|
+
operatorId: '30',
|
|
29
|
+
permissionKeys: ['platform.tenant.read'],
|
|
30
|
+
authorizationRevision: '1',
|
|
31
|
+
...overrides,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export interface MockProblemOptions extends Partial<ProblemDetails> {
|
|
35
|
+
code: string
|
|
36
|
+
status: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const mockProblemDetails = (options: MockProblemOptions): ProblemDetails => {
|
|
40
|
+
const { code, status, ...overrides } = options
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: `/docs/problems/${code.toLowerCase().replaceAll('_', '-')}`,
|
|
44
|
+
title: 'Request rejected',
|
|
45
|
+
status,
|
|
46
|
+
detail: 'The request was rejected by the test fixture.',
|
|
47
|
+
code,
|
|
48
|
+
request_id: 'req_web_testing_fixture',
|
|
49
|
+
...overrides,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MockAccessState {
|
|
54
|
+
permissionKeys: ReadonlySet<string>
|
|
55
|
+
moduleKeys: ReadonlySet<string>
|
|
56
|
+
hasPermission: (permission: string) => boolean
|
|
57
|
+
hasModule: (moduleKey: string) => boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const mockAccessState = (options: {
|
|
61
|
+
permissionKeys?: readonly string[]
|
|
62
|
+
moduleKeys?: readonly string[]
|
|
63
|
+
} = {}): MockAccessState => {
|
|
64
|
+
const permissionKeys = new Set(options.permissionKeys ?? [])
|
|
65
|
+
const moduleKeys = new Set(options.moduleKeys ?? [])
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
permissionKeys,
|
|
69
|
+
moduleKeys,
|
|
70
|
+
hasPermission: permission => permission !== '*' && permissionKeys.has(permission),
|
|
71
|
+
hasModule: moduleKey => moduleKeys.has(moduleKey),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type GuardResult = boolean | Promise<boolean>
|
|
76
|
+
export type AudienceGuard = (path: string) => GuardResult
|
|
77
|
+
|
|
78
|
+
export interface RouteGuardHarness {
|
|
79
|
+
navigate: (path: string) => Promise<'allowed' | 'denied'>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const createRouteGuardHarness = (guards: {
|
|
83
|
+
tenant: AudienceGuard
|
|
84
|
+
platform: AudienceGuard
|
|
85
|
+
}): RouteGuardHarness => ({
|
|
86
|
+
async navigate(path) {
|
|
87
|
+
const pathname = new URL(path, 'https://peanut-admin.test').pathname
|
|
88
|
+
if (pathname === '/app' || pathname.startsWith('/app/')) {
|
|
89
|
+
return await guards.tenant(pathname) ? 'allowed' : 'denied'
|
|
90
|
+
}
|
|
91
|
+
if (pathname === '/platform' || pathname.startsWith('/platform/')) {
|
|
92
|
+
return await guards.platform(pathname) ? 'allowed' : 'denied'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return 'allowed'
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const containsTenantState = (value: unknown): boolean => {
|
|
100
|
+
if (value === null || value === undefined || value === false || value === '' || value === 0) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return value.length > 0
|
|
105
|
+
}
|
|
106
|
+
if (value instanceof Map || value instanceof Set) {
|
|
107
|
+
return value.size > 0
|
|
108
|
+
}
|
|
109
|
+
if (typeof value === 'object') {
|
|
110
|
+
return Object.values(value).some(containsTenantState)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const assertTenantStateDisposed = async (
|
|
117
|
+
inspect: () => unknown,
|
|
118
|
+
switchTenant: () => void | Promise<void>,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
await switchTenant()
|
|
121
|
+
if (containsTenantState(inspect())) {
|
|
122
|
+
throw new Error('TENANT_STATE_LEAK')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Deferred<T> {
|
|
127
|
+
promise: Promise<T>
|
|
128
|
+
resolve: (value: T | PromiseLike<T>) => void
|
|
129
|
+
reject: (reason?: unknown) => void
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const createDeferred = <T>(): Deferred<T> => {
|
|
133
|
+
let resolve!: Deferred<T>['resolve']
|
|
134
|
+
let reject!: Deferred<T>['reject']
|
|
135
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
136
|
+
resolve = resolvePromise
|
|
137
|
+
reject = rejectPromise
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return { promise, resolve, reject }
|
|
141
|
+
}
|