@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/api/file/[id]/route.js +7 -2
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +127 -8
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js +1 -1
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js +1 -1
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +6 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/di.js +17 -3
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
- package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +8 -1
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/configs/cli.js +27 -14
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/currencies/api/currencies/route.js +3 -4
- package/dist/modules/currencies/api/currencies/route.js.map +2 -2
- package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
- package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +26 -24
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
- package/dist/modules/directory/utils/organizationScope.js +85 -0
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
- package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
- package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/StepsEditor.js +31 -0
- package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/index.js +3 -1
- package/dist/modules/workflows/components/nodes/index.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +117 -0
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/di.js +5 -1
- package/dist/modules/workflows/di.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +42 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
- package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
- package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +32 -0
- package/dist/modules/workflows/lib/duration.js.map +7 -0
- package/dist/modules/workflows/lib/event-logger.js +1 -0
- package/dist/modules/workflows/lib/event-logger.js.map +2 -2
- package/dist/modules/workflows/lib/format-validation-error.js +12 -0
- package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +6 -3
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/node-type-icons.js +9 -5
- package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +55 -23
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +79 -29
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +159 -0
- package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-executor.js +1 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
- package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/file/[id]/route.ts +7 -2
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
- package/src/modules/audit_logs/services/accessLogService.ts +179 -15
- package/src/modules/auth/backend/auth/profile/page.tsx +1 -1
- package/src/modules/auth/backend/profile/change-password/page.tsx +1 -1
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +6 -1
- package/src/modules/auth/di.ts +26 -3
- package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +8 -1
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +3 -2
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +3 -2
- package/src/modules/configs/cli.ts +34 -13
- package/src/modules/currencies/api/currencies/route.ts +3 -4
- package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
- package/src/modules/customers/api/people/route.ts +27 -25
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
- package/src/modules/directory/utils/organizationScope.ts +121 -0
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
- package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
- package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
- package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
- package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
- package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
- package/src/modules/workflows/components/StepsEditor.tsx +36 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
- package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
- package/src/modules/workflows/components/nodes/index.ts +3 -0
- package/src/modules/workflows/data/validators.ts +121 -0
- package/src/modules/workflows/di.ts +4 -0
- package/src/modules/workflows/i18n/de.json +10 -1
- package/src/modules/workflows/i18n/en.json +10 -1
- package/src/modules/workflows/i18n/es.json +10 -1
- package/src/modules/workflows/i18n/pl.json +10 -1
- package/src/modules/workflows/lib/activity-executor.ts +86 -2
- package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
- package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
- package/src/modules/workflows/lib/duration.ts +51 -0
- package/src/modules/workflows/lib/event-logger.ts +1 -0
- package/src/modules/workflows/lib/format-validation-error.ts +30 -0
- package/src/modules/workflows/lib/graph-utils.ts +3 -0
- package/src/modules/workflows/lib/node-type-icons.ts +6 -2
- package/src/modules/workflows/lib/signal-handler.ts +62 -24
- package/src/modules/workflows/lib/step-handler.ts +107 -50
- package/src/modules/workflows/lib/timer-handler.ts +213 -0
- package/src/modules/workflows/lib/workflow-executor.ts +1 -1
- package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { CacheStrategy, CacheValue, CacheSetOptions, CacheGetOptions } from '@open-mercato/cache'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Process-scoped fallback CacheStrategy for RbacService.
|
|
5
|
+
*
|
|
6
|
+
* Used only when no shared `cache` service is registered in DI
|
|
7
|
+
* (CLI scripts, lean test bootstraps, isolated unit harnesses). Production
|
|
8
|
+
* deployments wire `@open-mercato/cache` via bootstrap.ts, which preempts
|
|
9
|
+
* this fallback.
|
|
10
|
+
*
|
|
11
|
+
* Goals:
|
|
12
|
+
* - Match the CacheStrategy contract that RbacService consumes (`get`,
|
|
13
|
+
* `set`, `has`, `delete`, `deleteByTags`, `clear`).
|
|
14
|
+
* - Bound memory: LRU eviction at MAX_ENTRIES.
|
|
15
|
+
* - Honor `OM_RBAC_DEFAULT_CACHE=off` so callers can disable it explicitly.
|
|
16
|
+
* - Stay process-scoped via `globalThis` so HMR / module duplication does
|
|
17
|
+
* not produce divergent caches (same pattern as registerDiRegistrars).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type FallbackEntry = {
|
|
21
|
+
value: CacheValue
|
|
22
|
+
tags: string[]
|
|
23
|
+
expiresAt: number | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type FallbackCache = CacheStrategy & {
|
|
27
|
+
__reset: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const GLOBAL_KEY = '__openMercatoRbacFallbackCache__'
|
|
31
|
+
const MAX_ENTRIES = 5000
|
|
32
|
+
|
|
33
|
+
export function isRbacDefaultCacheEnabled(): boolean {
|
|
34
|
+
// Default OFF — same gating posture as Phases 2/4/5 in this PR. The
|
|
35
|
+
// integration runtime stays on the bare `asClass(RbacService).scoped()`
|
|
36
|
+
// path (matching develop) unless an operator opts in explicitly.
|
|
37
|
+
// Set `OM_RBAC_DEFAULT_CACHE=on` (or `1`/`true`/`yes`) to enable the
|
|
38
|
+
// in-process LRU fallback.
|
|
39
|
+
const raw = process.env.OM_RBAC_DEFAULT_CACHE
|
|
40
|
+
if (raw === undefined) return false
|
|
41
|
+
const normalized = raw.trim().toLowerCase()
|
|
42
|
+
if (!normalized.length) return false
|
|
43
|
+
return normalized === 'on' || normalized === '1' || normalized === 'true' || normalized === 'yes'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function nowMs(): number {
|
|
47
|
+
return Date.now()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createCache(): FallbackCache {
|
|
51
|
+
const store = new Map<string, FallbackEntry>()
|
|
52
|
+
const touch = (key: string) => {
|
|
53
|
+
const entry = store.get(key)
|
|
54
|
+
if (!entry) return undefined
|
|
55
|
+
if (entry.expiresAt !== null && entry.expiresAt < nowMs()) {
|
|
56
|
+
store.delete(key)
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
// Move to most-recently-used position (Map preserves insertion order).
|
|
60
|
+
store.delete(key)
|
|
61
|
+
store.set(key, entry)
|
|
62
|
+
return entry
|
|
63
|
+
}
|
|
64
|
+
const evictIfNeeded = () => {
|
|
65
|
+
while (store.size > MAX_ENTRIES) {
|
|
66
|
+
const oldest = store.keys().next().value
|
|
67
|
+
if (typeof oldest !== 'string') break
|
|
68
|
+
store.delete(oldest)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const cache: FallbackCache = {
|
|
72
|
+
async get(key: string, _options?: CacheGetOptions): Promise<CacheValue | null> {
|
|
73
|
+
const entry = touch(key)
|
|
74
|
+
return entry ? entry.value : null
|
|
75
|
+
},
|
|
76
|
+
async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {
|
|
77
|
+
const ttl = options?.ttl ?? null
|
|
78
|
+
store.delete(key)
|
|
79
|
+
store.set(key, {
|
|
80
|
+
value,
|
|
81
|
+
tags: options?.tags ?? [],
|
|
82
|
+
expiresAt: typeof ttl === 'number' && ttl > 0 ? nowMs() + ttl : null,
|
|
83
|
+
})
|
|
84
|
+
evictIfNeeded()
|
|
85
|
+
},
|
|
86
|
+
async has(key: string): Promise<boolean> {
|
|
87
|
+
return touch(key) !== undefined
|
|
88
|
+
},
|
|
89
|
+
async delete(key: string): Promise<boolean> {
|
|
90
|
+
return store.delete(key)
|
|
91
|
+
},
|
|
92
|
+
async deleteByTags(tags: string[]): Promise<number> {
|
|
93
|
+
if (!tags.length) return 0
|
|
94
|
+
const tagSet = new Set(tags)
|
|
95
|
+
let removed = 0
|
|
96
|
+
for (const [key, entry] of store.entries()) {
|
|
97
|
+
if (entry.tags.some((tag) => tagSet.has(tag))) {
|
|
98
|
+
store.delete(key)
|
|
99
|
+
removed += 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return removed
|
|
103
|
+
},
|
|
104
|
+
async clear(): Promise<number> {
|
|
105
|
+
const count = store.size
|
|
106
|
+
store.clear()
|
|
107
|
+
return count
|
|
108
|
+
},
|
|
109
|
+
async keys(pattern?: string): Promise<string[]> {
|
|
110
|
+
if (!pattern) return Array.from(store.keys())
|
|
111
|
+
const matcher = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.')
|
|
112
|
+
const regex = new RegExp(`^${matcher}$`)
|
|
113
|
+
return Array.from(store.keys()).filter((key) => regex.test(key))
|
|
114
|
+
},
|
|
115
|
+
async size(): Promise<number> {
|
|
116
|
+
return store.size
|
|
117
|
+
},
|
|
118
|
+
async stats(): Promise<{ size: number; expired: number }> {
|
|
119
|
+
const now = nowMs()
|
|
120
|
+
let expired = 0
|
|
121
|
+
for (const entry of store.values()) {
|
|
122
|
+
if (entry.expiresAt !== null && entry.expiresAt < now) expired += 1
|
|
123
|
+
}
|
|
124
|
+
return { size: store.size, expired }
|
|
125
|
+
},
|
|
126
|
+
__reset() {
|
|
127
|
+
store.clear()
|
|
128
|
+
},
|
|
129
|
+
} as FallbackCache
|
|
130
|
+
return cache
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createRbacFallbackCache(): CacheStrategy {
|
|
134
|
+
const existing = (globalThis as any)[GLOBAL_KEY] as FallbackCache | undefined
|
|
135
|
+
if (existing) return existing
|
|
136
|
+
const cache = createCache()
|
|
137
|
+
;(globalThis as any)[GLOBAL_KEY] = cache
|
|
138
|
+
return cache
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Test-only helper. Clears the process-scoped fallback cache. */
|
|
142
|
+
export function resetRbacFallbackCache(): void {
|
|
143
|
+
const existing = (globalThis as any)[GLOBAL_KEY] as FallbackCache | undefined
|
|
144
|
+
existing?.__reset()
|
|
145
|
+
}
|
|
@@ -556,7 +556,14 @@ export default function EditCatalogProductPage({
|
|
|
556
556
|
const productRes = await apiCall<ProductResponse>(
|
|
557
557
|
`/api/catalog/products?id=${encodeURIComponent(productId!)}&page=1&pageSize=1&withDeleted=false`,
|
|
558
558
|
);
|
|
559
|
-
if (!productRes.ok)
|
|
559
|
+
if (!productRes.ok) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
t(
|
|
562
|
+
"catalog.products.edit.errors.load",
|
|
563
|
+
"Failed to load product details.",
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
560
567
|
const record = Array.isArray(productRes.result?.items)
|
|
561
568
|
? productRes.result?.items?.[0]
|
|
562
569
|
: undefined;
|
package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors
|
|
|
9
9
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
10
10
|
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
11
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
12
13
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
14
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
14
15
|
import { E } from '#generated/entities.ids.generated'
|
|
@@ -165,7 +166,7 @@ export default function EditVariantPage({ params }: { params?: { productId?: str
|
|
|
165
166
|
const variantRes = await apiCall<VariantResponse>(
|
|
166
167
|
`/api/catalog/variants?id=${encodeURIComponent(variantId!)}&page=1&pageSize=1`,
|
|
167
168
|
)
|
|
168
|
-
if (!variantRes.ok) throw new Error('
|
|
169
|
+
if (!variantRes.ok) throw new Error(t('catalog.variants.form.errors.load', 'Failed to load variant.'))
|
|
169
170
|
const record = Array.isArray(variantRes.result?.items) ? variantRes.result?.items?.[0] : undefined
|
|
170
171
|
if (!record) throw new Error(t('catalog.variants.form.errors.notFound', 'Variant not found.'))
|
|
171
172
|
const resolvedProductId =
|
|
@@ -421,7 +422,7 @@ export default function EditVariantPage({ params }: { params?: { productId?: str
|
|
|
421
422
|
<Page>
|
|
422
423
|
<PageBody>
|
|
423
424
|
{error ? (
|
|
424
|
-
<
|
|
425
|
+
<ErrorMessage label={error} className="mb-4" />
|
|
425
426
|
) : null}
|
|
426
427
|
<CrudForm<VariantFormValues>
|
|
427
428
|
title={formTitle}
|
|
@@ -9,6 +9,7 @@ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors
|
|
|
9
9
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
10
10
|
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
11
11
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
|
+
import { ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
12
13
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
14
|
import { E } from '#generated/entities.ids.generated'
|
|
14
15
|
import {
|
|
@@ -136,7 +137,7 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
136
137
|
const res = await apiCall<ProductResponse>(
|
|
137
138
|
`/api/catalog/products?id=${encodeURIComponent(productId!)}&page=1&pageSize=1`,
|
|
138
139
|
)
|
|
139
|
-
if (!res.ok) throw new Error('
|
|
140
|
+
if (!res.ok) throw new Error(t('catalog.variants.form.errors.load', 'Failed to load product context.'))
|
|
140
141
|
const record = Array.isArray(res.result?.items) ? res.result?.items?.[0] : undefined
|
|
141
142
|
if (!record) throw new Error(t('catalog.products.edit.errors.notFound', 'Product not found.'))
|
|
142
143
|
const metadata = (record.metadata ?? {}) as Record<string, unknown>
|
|
@@ -298,7 +299,7 @@ export default function CreateVariantPage({ params }: { params?: { productId?: s
|
|
|
298
299
|
<Page>
|
|
299
300
|
<PageBody>
|
|
300
301
|
{error ? (
|
|
301
|
-
<
|
|
302
|
+
<ErrorMessage label={error} className="mb-4" />
|
|
302
303
|
) : null}
|
|
303
304
|
<CrudForm<VariantFormValues>
|
|
304
305
|
title={formTitle}
|
|
@@ -16,6 +16,12 @@ import { touchGeneratedBarrels } from './lib/touchGeneratedBarrels'
|
|
|
16
16
|
|
|
17
17
|
type ParsedArgs = Record<string, string | boolean>
|
|
18
18
|
|
|
19
|
+
export const STRUCTURAL_CACHE_REQUESTS: CachePurgeRequest[] = [
|
|
20
|
+
{ kind: 'pattern', pattern: 'nav:*' },
|
|
21
|
+
{ kind: 'segment', segment: 'admin-nav' },
|
|
22
|
+
{ kind: 'segment', segment: 'portal-nav' },
|
|
23
|
+
]
|
|
24
|
+
|
|
19
25
|
type CacheScope = {
|
|
20
26
|
label: string
|
|
21
27
|
tenantId: string | null
|
|
@@ -157,7 +163,7 @@ function printCacheHelp() {
|
|
|
157
163
|
console.log('ℹ️ Notes:')
|
|
158
164
|
console.log(' `stats` mirrors the cache admin page segment overview for CRUD/widget caches.')
|
|
159
165
|
console.log(' `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).')
|
|
160
|
-
console.log(' `structural` targets navigation caches
|
|
166
|
+
console.log(' `structural` targets navigation/sidebar caches and is the recommended post-step after module/sidebar structure changes.')
|
|
161
167
|
console.log(' When no scope flag is supplied, this command uses the global cache scope only.')
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -201,11 +207,14 @@ async function runCacheStats(args: ParsedArgs) {
|
|
|
201
207
|
}
|
|
202
208
|
}
|
|
203
209
|
|
|
204
|
-
async function
|
|
210
|
+
async function runCachePurgeRequest(
|
|
211
|
+
args: ParsedArgs,
|
|
212
|
+
request: CachePurgeRequest,
|
|
213
|
+
emitOutput = true,
|
|
214
|
+
) {
|
|
205
215
|
const json = flagEnabled(args, 'json')
|
|
206
216
|
const quiet = flagEnabled(args, 'quiet')
|
|
207
217
|
const dryRun = flagEnabled(args, 'dry-run', 'dryRun')
|
|
208
|
-
const request = resolveCachePurgeRequest(args)
|
|
209
218
|
const container = await createRequestContainer()
|
|
210
219
|
try {
|
|
211
220
|
const em = container.resolve('em') as EntityManager
|
|
@@ -228,13 +237,13 @@ async function runCachePurge(args: ParsedArgs) {
|
|
|
228
237
|
})
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
if (json) {
|
|
240
|
+
if (json && emitOutput) {
|
|
232
241
|
console.log(JSON.stringify(results, null, 2))
|
|
233
|
-
return
|
|
242
|
+
return results
|
|
234
243
|
}
|
|
235
244
|
|
|
236
|
-
if (quiet) {
|
|
237
|
-
return
|
|
245
|
+
if (quiet || !emitOutput) {
|
|
246
|
+
return results
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
for (const result of results) {
|
|
@@ -246,22 +255,34 @@ async function runCachePurge(args: ParsedArgs) {
|
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
257
|
}
|
|
258
|
+
return results
|
|
249
259
|
} finally {
|
|
250
260
|
await disposeContainer(container)
|
|
251
261
|
}
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
async function runCachePurge(args: ParsedArgs) {
|
|
265
|
+
await runCachePurgeRequest(args, resolveCachePurgeRequest(args))
|
|
266
|
+
}
|
|
267
|
+
|
|
254
268
|
async function runStructuralCachePurge(args: ParsedArgs) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
const json = flagEnabled(args, 'json')
|
|
270
|
+
const structuralResults: Array<{
|
|
271
|
+
request: CachePurgeRequest
|
|
272
|
+
results: Awaited<ReturnType<typeof runCachePurgeRequest>>
|
|
273
|
+
}> = []
|
|
274
|
+
for (const request of STRUCTURAL_CACHE_REQUESTS) {
|
|
275
|
+
const results = await runCachePurgeRequest(args, request, !json)
|
|
276
|
+
structuralResults.push({ request, results })
|
|
277
|
+
}
|
|
278
|
+
if (json) {
|
|
279
|
+
console.log(JSON.stringify(structuralResults, null, 2))
|
|
258
280
|
}
|
|
259
|
-
await runCachePurge(nextArgs)
|
|
260
281
|
const quiet = flagEnabled(args, 'quiet')
|
|
261
282
|
try {
|
|
262
|
-
touchGeneratedBarrels({ quiet })
|
|
283
|
+
touchGeneratedBarrels({ quiet: quiet || json })
|
|
263
284
|
} catch (err) {
|
|
264
|
-
if (!quiet) {
|
|
285
|
+
if (!quiet && !json) {
|
|
265
286
|
console.warn(
|
|
266
287
|
`[structural] failed to touch generated barrels: ${(err as Error).message ?? err}`,
|
|
267
288
|
)
|
|
@@ -172,10 +172,9 @@ export async function GET(req: Request) {
|
|
|
172
172
|
orderBy.code = 'ASC'
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const items = paged.map(toRow)
|
|
175
|
+
const offset = (page - 1) * pageSize
|
|
176
|
+
const [rows, total] = await em.findAndCount(Currency, filter, { orderBy, limit: pageSize, offset })
|
|
177
|
+
const items = rows.map(toRow)
|
|
179
178
|
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
180
179
|
|
|
181
180
|
return NextResponse.json({ items, total, page, pageSize, totalPages })
|
|
@@ -169,10 +169,9 @@ export async function GET(req: Request) {
|
|
|
169
169
|
orderBy.date = 'DESC'
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const items = paged.map(toRow)
|
|
172
|
+
const offset = (page - 1) * pageSize
|
|
173
|
+
const [rows, total] = await em.findAndCount(ExchangeRate, where, { orderBy, limit: pageSize, offset })
|
|
174
|
+
const items = rows.map(toRow)
|
|
176
175
|
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
177
176
|
|
|
178
177
|
return NextResponse.json({ items, total, page, pageSize, totalPages })
|
|
@@ -427,37 +427,39 @@ const crud = makeCrudRoute({
|
|
|
427
427
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
428
428
|
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
429
429
|
}
|
|
430
|
-
const
|
|
431
|
-
em,
|
|
432
|
-
CustomerEntity,
|
|
433
|
-
{
|
|
434
|
-
id: { $in: ids },
|
|
435
|
-
deletedAt: null,
|
|
436
|
-
kind: 'person',
|
|
437
|
-
} as FilterQuery<CustomerEntity>,
|
|
438
|
-
undefined,
|
|
439
|
-
decryptionScope,
|
|
440
|
-
)
|
|
441
|
-
const entitiesById = new Map<string, CustomerEntity>()
|
|
442
|
-
for (const entity of entities) {
|
|
443
|
-
entitiesById.set(entity.id, entity)
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const where: Record<string, unknown> = {
|
|
430
|
+
const profileWhere: Record<string, unknown> = {
|
|
447
431
|
entity: { $in: ids },
|
|
448
432
|
tenantId: ctx.auth?.tenantId ?? null,
|
|
449
433
|
}
|
|
450
434
|
if (ctx.selectedOrganizationId) {
|
|
451
|
-
|
|
435
|
+
profileWhere.organizationId = ctx.selectedOrganizationId
|
|
452
436
|
}
|
|
453
437
|
|
|
454
|
-
const profiles = await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
438
|
+
const [entities, profiles] = await Promise.all([
|
|
439
|
+
findWithDecryption(
|
|
440
|
+
em,
|
|
441
|
+
CustomerEntity,
|
|
442
|
+
{
|
|
443
|
+
id: { $in: ids },
|
|
444
|
+
deletedAt: null,
|
|
445
|
+
kind: 'person',
|
|
446
|
+
} as FilterQuery<CustomerEntity>,
|
|
447
|
+
undefined,
|
|
448
|
+
decryptionScope,
|
|
449
|
+
),
|
|
450
|
+
findWithDecryption(
|
|
451
|
+
em,
|
|
452
|
+
CustomerPersonProfile,
|
|
453
|
+
profileWhere as FilterQuery<CustomerPersonProfile>,
|
|
454
|
+
{ populate: ['entity', 'company'] },
|
|
455
|
+
decryptionScope,
|
|
456
|
+
),
|
|
457
|
+
])
|
|
458
|
+
|
|
459
|
+
const entitiesById = new Map<string, CustomerEntity>()
|
|
460
|
+
for (const entity of entities) {
|
|
461
|
+
entitiesById.set(entity.id, entity)
|
|
462
|
+
}
|
|
461
463
|
|
|
462
464
|
const profilesByEntityId = new Map<string, CustomerPersonProfile>()
|
|
463
465
|
for (const profile of profiles) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Invalidate the OrganizationScope cache when an organization mutates.
|
|
2
|
+
//
|
|
3
|
+
// resolveOrganizationScopeForRequest caches its result with a short TTL
|
|
4
|
+
// (default 60s, OM_ORG_SCOPE_CACHE_TTL_MS). When an organization is
|
|
5
|
+
// created/updated/deleted, the cached scope for users of the affected
|
|
6
|
+
// tenant may be stale (visibility set or descendant tree changed). We
|
|
7
|
+
// drop every cache entry tagged for that tenant; the TTL is the backstop
|
|
8
|
+
// for races where the event fires after a request reads the cache.
|
|
9
|
+
|
|
10
|
+
type CacheService = {
|
|
11
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const metadata = {
|
|
15
|
+
event: 'directory.organization.*',
|
|
16
|
+
persistent: false,
|
|
17
|
+
id: 'directory:invalidate-org-scope-cache',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function handle(
|
|
21
|
+
payload: unknown,
|
|
22
|
+
ctx: { resolve: <T = unknown>(name: string) => T },
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const data = (payload ?? {}) as Record<string, unknown>
|
|
25
|
+
const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null
|
|
26
|
+
if (!tenantId) return
|
|
27
|
+
let cache: CacheService | null = null
|
|
28
|
+
try {
|
|
29
|
+
cache = ctx.resolve<CacheService>('cache')
|
|
30
|
+
} catch {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (!cache) return
|
|
34
|
+
try {
|
|
35
|
+
await cache.deleteByTags([`org-scope:tenant:${tenantId}`])
|
|
36
|
+
} catch {
|
|
37
|
+
// best-effort; TTL is the backstop.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
|
|
|
5
5
|
import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
|
|
6
6
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
8
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
8
9
|
import { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'
|
|
9
10
|
|
|
10
11
|
export { parseSelectedOrganizationCookie, parseSelectedTenantCookie }
|
|
@@ -16,6 +17,90 @@ export type OrganizationScope = {
|
|
|
16
17
|
tenantId: string | null
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Phase 4 — short-TTL cache for resolveOrganizationScopeForRequest.
|
|
21
|
+
// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,
|
|
22
|
+
// requestedTenant) between membership changes; caching it bypasses 1
|
|
23
|
+
// SELECT on `organizations` per CRUD request. TTL is short (60s default)
|
|
24
|
+
// to keep staleness bounded for membership/visibility changes. Tag-based
|
|
25
|
+
// invalidation kicks the cache when user_organizations or organizations
|
|
26
|
+
// mutate (wired via invalidateOrganizationScopeCacheFor).
|
|
27
|
+
const ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope'
|
|
28
|
+
// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)
|
|
29
|
+
// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`
|
|
30
|
+
// (or any positive integer) to opt in once cross-request safety is re-verified.
|
|
31
|
+
const ORG_SCOPE_DEFAULT_TTL_MS = 0
|
|
32
|
+
|
|
33
|
+
function resolveOrgScopeTtlMs(): number {
|
|
34
|
+
const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS
|
|
35
|
+
if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS
|
|
36
|
+
const parsed = Number(raw)
|
|
37
|
+
if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS
|
|
38
|
+
return parsed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildOrgScopeCacheKey(parts: {
|
|
42
|
+
userId: string
|
|
43
|
+
effectiveTenantId: string
|
|
44
|
+
selectedOrgId: string | null
|
|
45
|
+
requestedTenantId: string | null
|
|
46
|
+
}): string {
|
|
47
|
+
const selected = parts.selectedOrgId ?? 'none'
|
|
48
|
+
const requested = parts.requestedTenantId ?? 'none'
|
|
49
|
+
return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {
|
|
53
|
+
return [
|
|
54
|
+
`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${parts.userId}`,
|
|
55
|
+
`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${parts.effectiveTenantId}`,
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidCachedScope(value: unknown): value is OrganizationScope {
|
|
60
|
+
if (typeof value !== 'object' || value === null) return false
|
|
61
|
+
const record = value as Partial<OrganizationScope>
|
|
62
|
+
const idOk = (v: unknown) => v === null || typeof v === 'string'
|
|
63
|
+
const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))
|
|
64
|
+
return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {
|
|
68
|
+
if (!container) return null
|
|
69
|
+
try {
|
|
70
|
+
const c = container.resolve('cache') as CacheStrategy | undefined
|
|
71
|
+
if (c && typeof c.get === 'function' && typeof c.set === 'function') return c
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function invalidateOrganizationScopeCacheForUser(
|
|
79
|
+
container: AwilixContainer,
|
|
80
|
+
userId: string,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const cache = resolveCacheFromContainer(container)
|
|
83
|
+
if (!cache?.deleteByTags) return
|
|
84
|
+
try {
|
|
85
|
+
await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`])
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn('[org-scope:cache] invalidate user failed', err)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function invalidateOrganizationScopeCacheForTenant(
|
|
92
|
+
container: AwilixContainer,
|
|
93
|
+
tenantId: string,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const cache = resolveCacheFromContainer(container)
|
|
96
|
+
if (!cache?.deleteByTags) return
|
|
97
|
+
try {
|
|
98
|
+
await cache.deleteByTags([`${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`])
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[org-scope:cache] invalidate tenant failed', err)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
19
104
|
function normalizeOrganizationId(value: unknown): string | null {
|
|
20
105
|
if (typeof value !== 'string') return null
|
|
21
106
|
const trimmed = value.trim()
|
|
@@ -270,6 +355,31 @@ export async function resolveOrganizationScopeForRequest({
|
|
|
270
355
|
}
|
|
271
356
|
|
|
272
357
|
const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)
|
|
358
|
+
const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0
|
|
359
|
+
? rawSelected.trim()
|
|
360
|
+
: null
|
|
361
|
+
|
|
362
|
+
const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null
|
|
363
|
+
const ttlMs = resolveOrgScopeTtlMs()
|
|
364
|
+
const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null
|
|
365
|
+
const cacheKey = userId
|
|
366
|
+
? buildOrgScopeCacheKey({
|
|
367
|
+
userId,
|
|
368
|
+
effectiveTenantId,
|
|
369
|
+
selectedOrgId: normalizedSelectedId,
|
|
370
|
+
requestedTenantId: requestedTenantId ?? null,
|
|
371
|
+
})
|
|
372
|
+
: null
|
|
373
|
+
|
|
374
|
+
if (cache && cacheKey && typeof cache.get === 'function') {
|
|
375
|
+
try {
|
|
376
|
+
const cached = await cache.get(cacheKey)
|
|
377
|
+
if (isValidCachedScope(cached)) return cached
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.warn('[org-scope:cache] read failed', err)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
273
383
|
const baseScope = await resolveOrganizationScope({
|
|
274
384
|
em,
|
|
275
385
|
rbac,
|
|
@@ -278,6 +388,17 @@ export async function resolveOrganizationScopeForRequest({
|
|
|
278
388
|
tenantId: effectiveTenantId,
|
|
279
389
|
})
|
|
280
390
|
|
|
391
|
+
if (cache && cacheKey && userId && typeof cache.set === 'function') {
|
|
392
|
+
try {
|
|
393
|
+
await cache.set(cacheKey, baseScope, {
|
|
394
|
+
ttl: ttlMs,
|
|
395
|
+
tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),
|
|
396
|
+
})
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.warn('[org-scope:cache] write failed', err)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
281
402
|
return baseScope
|
|
282
403
|
}
|
|
283
404
|
|
|
@@ -37,7 +37,7 @@ export default function ResourcesResourceTypeEditPage({ params }: { params?: { i
|
|
|
37
37
|
{ errorMessage: t('resources.resourceTypes.errors.load', 'Failed to load resource types.') },
|
|
38
38
|
)
|
|
39
39
|
const item = Array.isArray(payload.items) ? payload.items[0] : null
|
|
40
|
-
if (!item) throw new Error('
|
|
40
|
+
if (!item) throw new Error(t('resources.resourceTypes.errors.notFound', 'Resource type not found.'))
|
|
41
41
|
if (!cancelled) {
|
|
42
42
|
const customValues = extractCustomFieldValues(item)
|
|
43
43
|
setInitialValues({
|
|
@@ -53,7 +53,7 @@ export default function EditChannelPage({ params }: { params?: { channelId?: str
|
|
|
53
53
|
)
|
|
54
54
|
const item = Array.isArray(payload.items) ? payload.items[0] : null
|
|
55
55
|
if (!item) {
|
|
56
|
-
throw new Error('
|
|
56
|
+
throw new Error(t('sales.channels.form.errors.notFound', 'Channel not found.'))
|
|
57
57
|
}
|
|
58
58
|
if (!cancelled) {
|
|
59
59
|
setInitialValues(mapChannelToFormValues(item))
|
|
@@ -291,7 +291,7 @@ export function ChannelOfferForm({ channelId: lockedChannelId, offerId, mode }:
|
|
|
291
291
|
{ errorMessage: t('sales.channels.offers.errors.loadOffer', 'Failed to load offer.') },
|
|
292
292
|
)
|
|
293
293
|
const offer = Array.isArray(payload.items) ? payload.items[0] : null
|
|
294
|
-
if (!offer) throw new Error('
|
|
294
|
+
if (!offer) throw new Error(t('sales.channels.offers.errors.notFound', 'Offer not found.'))
|
|
295
295
|
const values = mapOfferToFormValues(offer, lockedChannelId)
|
|
296
296
|
const pricePayload = await readApiResultOrThrow<PriceResponse>(
|
|
297
297
|
`/api/catalog/prices?offerId=${encodeURIComponent(offer.id as string)}&pageSize=${MAX_LIST_PAGE_SIZE}`,
|
|
@@ -10,6 +10,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
10
10
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
11
11
|
import { apiFetch } from '@open-mercato/ui/backend/utils/api'
|
|
12
12
|
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
13
|
+
import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
|
|
13
14
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
14
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
16
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
@@ -94,8 +95,8 @@ export default function EditWorkflowDefinitionPage() {
|
|
|
94
95
|
body: JSON.stringify(payload),
|
|
95
96
|
})
|
|
96
97
|
if (!response.ok) {
|
|
97
|
-
const errorBody = await readJsonSafe<{ error?: string }>(response, null)
|
|
98
|
-
throw new Error(errorBody
|
|
98
|
+
const errorBody = await readJsonSafe<{ error?: string; details?: Array<{ path?: Array<string | number>; message?: string }> }>(response, null)
|
|
99
|
+
throw new Error(formatWorkflowValidationError(errorBody, t('workflows.errors.updateFailed')))
|
|
99
100
|
}
|
|
100
101
|
return response
|
|
101
102
|
},
|