@open-mercato/core 0.6.2-develop.3461.1.605f31c2c9 → 0.6.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/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/queue-runner.js +15 -3
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/helpers/integration/queue.js +1 -1
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +24 -1
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +37 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +36 -2
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +36 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +101 -13
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
- package/dist/modules/auth/backend/roles/[id]/edit/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 +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/backend/users/roleOptions.js +1 -1
- package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +8 -0
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/commands/users.js +3 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +53 -26
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/frontend/login.js +35 -0
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +97 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +3 -3
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/customers/ai-agents.js +10 -1
- package/dist/modules/customers/ai-agents.js.map +2 -2
- package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
- package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
- package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +2 -0
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +77 -58
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/data/validators.js +9 -6
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +42 -2
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +70 -89
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +8 -6
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +133 -0
- package/dist/modules/messages/components/inboxFilters.js.map +7 -0
- package/dist/modules/messages/components/messageListLabels.js +14 -0
- package/dist/modules/messages/components/messageListLabels.js.map +7 -0
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js +47 -23
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
- package/dist/modules/messages/data/validators.js +8 -6
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +26 -16
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/query_index/di.js +8 -6
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +12 -10
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
- package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
- package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
- package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +104 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +20 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +126 -116
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -4
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/jest.config.cjs +4 -1
- package/package.json +15 -16
- package/src/helpers/integration/queue-runner.ts +21 -2
- package/src/helpers/integration/queue.ts +1 -1
- package/src/helpers/integration/salesUi.ts +26 -1
- package/src/modules/auth/api/roles/acl/route.ts +39 -1
- package/src/modules/auth/api/roles/route.ts +38 -2
- package/src/modules/auth/api/users/acl/route.ts +41 -1
- package/src/modules/auth/api/users/route.ts +105 -12
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/auth/backend/users/roleOptions.ts +2 -1
- package/src/modules/auth/commands/roles.ts +8 -0
- package/src/modules/auth/commands/users.ts +3 -0
- package/src/modules/auth/components/AclEditor.tsx +50 -24
- package/src/modules/auth/frontend/login.tsx +40 -0
- package/src/modules/auth/lib/grantChecks.ts +116 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
- package/src/modules/business_rules/lib/rule-evaluator.ts +3 -3
- package/src/modules/customers/ai-agents.ts +10 -1
- package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
- package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
- package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
- package/src/modules/data_sync/api/options.ts +2 -0
- package/src/modules/data_sync/api/run.ts +80 -61
- package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
- package/src/modules/data_sync/i18n/de.json +3 -0
- package/src/modules/data_sync/i18n/en.json +3 -0
- package/src/modules/data_sync/i18n/es.json +3 -0
- package/src/modules/data_sync/i18n/pl.json +3 -0
- package/src/modules/data_sync/lib/adapter.ts +9 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
- package/src/modules/integrations/data/validators.ts +5 -4
- package/src/modules/messages/api/route.ts +2 -2
- package/src/modules/messages/commands/messages.ts +54 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +80 -92
- package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +9 -5
- package/src/modules/messages/components/inboxFilters.ts +162 -0
- package/src/modules/messages/components/messageListLabels.ts +29 -0
- package/src/modules/messages/components/useMessagesInboxBulkActions.ts +58 -26
- package/src/modules/messages/data/validators.ts +7 -5
- package/src/modules/messages/i18n/de.json +7 -4
- package/src/modules/messages/i18n/en.json +7 -4
- package/src/modules/messages/i18n/es.json +7 -4
- package/src/modules/messages/i18n/pl.json +7 -4
- package/src/modules/progress/api/jobs/route.ts +30 -20
- package/src/modules/query_index/di.ts +9 -7
- package/src/modules/query_index/lib/indexer.ts +13 -11
- package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
- package/src/modules/sales/api/order-adjustments/route.ts +2 -1
- package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +116 -0
- package/src/modules/sales/data/validators.ts +38 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/sync_excel/api/import/route.ts +141 -131
- package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
- package/src/modules/translations/components/TranslationManager.tsx +9 -4
- package/src/modules/workflows/lib/activity-executor.ts +4 -3
- package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
- package/tsconfig.json +1 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { matchEventPattern } from "@open-mercato/shared/lib/events/patterns";
|
|
2
|
+
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
|
+
import { testLinearRegex } from "@open-mercato/shared/lib/regex/linear";
|
|
2
4
|
import {
|
|
3
5
|
WorkflowEventTrigger,
|
|
4
6
|
WorkflowDefinition,
|
|
@@ -140,11 +142,12 @@ function evaluateCondition(condition, payload) {
|
|
|
140
142
|
if (typeof value !== "string" || typeof expected !== "string") return false;
|
|
141
143
|
if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false;
|
|
142
144
|
if (!isSafeWorkflowRegexPattern(expected)) return false;
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
{
|
|
146
|
+
const result = testLinearRegex(expected, value, {
|
|
147
|
+
maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,
|
|
148
|
+
maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH
|
|
149
|
+
});
|
|
150
|
+
return result.ok ? result.matched : false;
|
|
148
151
|
}
|
|
149
152
|
default:
|
|
150
153
|
return false;
|
|
@@ -168,7 +171,9 @@ function getCacheKey(tenantId, organizationId) {
|
|
|
168
171
|
return `${tenantId}:${organizationId}`;
|
|
169
172
|
}
|
|
170
173
|
async function loadLegacyTriggers(em, tenantId, organizationId) {
|
|
171
|
-
const
|
|
174
|
+
const postgresEm = em;
|
|
175
|
+
const legacyTriggers = await findWithDecryption(
|
|
176
|
+
postgresEm,
|
|
172
177
|
WorkflowEventTrigger,
|
|
173
178
|
{
|
|
174
179
|
tenantId,
|
|
@@ -178,14 +183,17 @@ async function loadLegacyTriggers(em, tenantId, organizationId) {
|
|
|
178
183
|
},
|
|
179
184
|
{
|
|
180
185
|
orderBy: { priority: "DESC", createdAt: "ASC" }
|
|
181
|
-
}
|
|
186
|
+
},
|
|
187
|
+
{ tenantId, organizationId }
|
|
182
188
|
);
|
|
183
189
|
const definitionIds = [...new Set(legacyTriggers.map((t) => t.workflowDefinitionId))];
|
|
184
|
-
const definitions = definitionIds.length > 0 ? await
|
|
190
|
+
const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {
|
|
185
191
|
id: { $in: definitionIds },
|
|
192
|
+
tenantId,
|
|
193
|
+
organizationId,
|
|
186
194
|
enabled: true,
|
|
187
195
|
deletedAt: null
|
|
188
|
-
}) : [];
|
|
196
|
+
}, {}, { tenantId, organizationId }) : [];
|
|
189
197
|
const definitionMap = new Map(definitions.map((d) => [d.id, d]));
|
|
190
198
|
return legacyTriggers.filter((t) => definitionMap.has(t.workflowDefinitionId)).map((t) => {
|
|
191
199
|
const def = definitionMap.get(t.workflowDefinitionId);
|
|
@@ -208,14 +216,18 @@ async function loadLegacyTriggers(em, tenantId, organizationId) {
|
|
|
208
216
|
});
|
|
209
217
|
}
|
|
210
218
|
async function loadEmbeddedTriggers(em, tenantId, organizationId) {
|
|
211
|
-
const
|
|
219
|
+
const postgresEm = em;
|
|
220
|
+
const definitions = await findWithDecryption(
|
|
221
|
+
postgresEm,
|
|
212
222
|
WorkflowDefinition,
|
|
213
223
|
{
|
|
214
224
|
tenantId,
|
|
215
225
|
organizationId,
|
|
216
226
|
enabled: true,
|
|
217
227
|
deletedAt: null
|
|
218
|
-
}
|
|
228
|
+
},
|
|
229
|
+
{},
|
|
230
|
+
{ tenantId, organizationId }
|
|
219
231
|
);
|
|
220
232
|
const triggers = [];
|
|
221
233
|
for (const def of definitions) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/event-trigger-service.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n try {\n const regex = new RegExp(expected)\n return regex.test(value)\n } catch {\n return false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org)\nconst triggerCache = new Map<string, CachedTriggers>()\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const legacyTriggers = await em.find(\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n }\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await em.find(WorkflowDefinition, {\n id: { $in: definitionIds },\n enabled: true,\n deletedAt: null,\n }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n // Load all enabled definitions that may have triggers\n const definitions = await em.find(\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n\n // Check in-memory cache\n const cached = triggerCache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n triggerCache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n triggerCache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of triggerCache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n triggerCache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n {\n const result = testLinearRegex(expected, value, {\n maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,\n maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org)\nconst triggerCache = new Map<string, CachedTriggers>()\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n const legacyTriggers = await findWithDecryption(\n postgresEm,\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n },\n { tenantId, organizationId },\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {\n id: { $in: definitionIds },\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }, {}, { tenantId, organizationId }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n // Load all enabled definitions that may have triggers\n const definitions = await findWithDecryption(\n postgresEm,\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {},\n { tenantId, organizationId },\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n\n // Check in-memory cache\n const cached = triggerCache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n triggerCache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n triggerCache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of triggerCache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n triggerCache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/jest.config.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @type {import('jest').Config} */
|
|
2
2
|
module.exports = {
|
|
3
3
|
testEnvironment: 'node',
|
|
4
|
+
testTimeout: 30000,
|
|
4
5
|
watchman: false,
|
|
5
6
|
rootDir: '.',
|
|
6
7
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
@@ -21,13 +22,15 @@ module.exports = {
|
|
|
21
22
|
{
|
|
22
23
|
tsconfig: {
|
|
23
24
|
jsx: 'react-jsx',
|
|
25
|
+
rootDir: '.',
|
|
26
|
+
ignoreDeprecations: '6.0',
|
|
24
27
|
},
|
|
25
28
|
},
|
|
26
29
|
],
|
|
27
30
|
},
|
|
28
31
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
|
29
32
|
transformIgnorePatterns: [
|
|
30
|
-
'node_modules/(?!(@mikro-orm)/)',
|
|
33
|
+
'node_modules/(?!(@mikro-orm|kysely)/)',
|
|
31
34
|
],
|
|
32
35
|
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
33
36
|
passWithNoTests: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.2
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -227,31 +227,31 @@
|
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
229
|
"dependencies": {
|
|
230
|
-
"@mikro-orm/core": "^7.0.
|
|
231
|
-
"@mikro-orm/decorators": "^7.0.
|
|
232
|
-
"@mikro-orm/postgresql": "^7.0.
|
|
230
|
+
"@mikro-orm/core": "^7.0.17",
|
|
231
|
+
"@mikro-orm/decorators": "^7.0.17",
|
|
232
|
+
"@mikro-orm/postgresql": "^7.0.17",
|
|
233
233
|
"@xyflow/react": "^12.10.2",
|
|
234
|
-
"ai": "^6.0.
|
|
235
|
-
"date-fns": "4.1
|
|
234
|
+
"ai": "^6.0.185",
|
|
235
|
+
"date-fns": "4.2.1",
|
|
236
236
|
"date-fns-tz": "^3.2.0",
|
|
237
|
-
"html-to-text": "^
|
|
237
|
+
"html-to-text": "^10.0.0",
|
|
238
238
|
"mammoth": "^1.9.0",
|
|
239
239
|
"pdfjs-dist": "^5.7.284",
|
|
240
240
|
"semver": "^7.8.0",
|
|
241
241
|
"ts-pattern": "^5.0.0",
|
|
242
|
-
"zod": "^4.3
|
|
242
|
+
"zod": "^4.4.3"
|
|
243
243
|
},
|
|
244
244
|
"peerDependencies": {
|
|
245
|
-
"@open-mercato/ai-assistant": "0.6.2
|
|
246
|
-
"@open-mercato/shared": "0.6.2
|
|
247
|
-
"@open-mercato/ui": "0.6.2
|
|
245
|
+
"@open-mercato/ai-assistant": "0.6.2",
|
|
246
|
+
"@open-mercato/shared": "0.6.2",
|
|
247
|
+
"@open-mercato/ui": "0.6.2",
|
|
248
248
|
"react": "^19.0.0",
|
|
249
249
|
"react-dom": "^19.0.0"
|
|
250
250
|
},
|
|
251
251
|
"devDependencies": {
|
|
252
|
-
"@open-mercato/ai-assistant": "0.6.2
|
|
253
|
-
"@open-mercato/shared": "0.6.2
|
|
254
|
-
"@open-mercato/ui": "0.6.2
|
|
252
|
+
"@open-mercato/ai-assistant": "0.6.2",
|
|
253
|
+
"@open-mercato/shared": "0.6.2",
|
|
254
|
+
"@open-mercato/ui": "0.6.2",
|
|
255
255
|
"@testing-library/dom": "^10.4.1",
|
|
256
256
|
"@testing-library/jest-dom": "^6.9.1",
|
|
257
257
|
"@testing-library/react": "^16.3.1",
|
|
@@ -275,6 +275,5 @@
|
|
|
275
275
|
"type": "git",
|
|
276
276
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
277
277
|
"directory": "packages/core"
|
|
278
|
-
}
|
|
279
|
-
"stableVersion": "0.6.1"
|
|
278
|
+
}
|
|
280
279
|
}
|
|
@@ -7,13 +7,27 @@ import { createQueue } from '@open-mercato/queue'
|
|
|
7
7
|
export type QueueDrainRunnerOptions = {
|
|
8
8
|
appRoot: string
|
|
9
9
|
jobLimit: number
|
|
10
|
+
queueBaseDir?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type BootstrapData = Awaited<ReturnType<typeof bootstrapFromAppRoot>>
|
|
14
|
+
|
|
15
|
+
const bootstrapCache = new Map<string, Promise<BootstrapData>>()
|
|
16
|
+
|
|
17
|
+
function getBootstrapData(appRoot: string): Promise<BootstrapData> {
|
|
18
|
+
const resolvedAppRoot = path.resolve(appRoot)
|
|
19
|
+
const cached = bootstrapCache.get(resolvedAppRoot)
|
|
20
|
+
if (cached) return cached
|
|
21
|
+
const promise = bootstrapFromAppRoot(resolvedAppRoot)
|
|
22
|
+
bootstrapCache.set(resolvedAppRoot, promise)
|
|
23
|
+
return promise
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
export async function drainQueueFromAppRoot(
|
|
13
27
|
queueName: string,
|
|
14
28
|
options: QueueDrainRunnerOptions,
|
|
15
29
|
): Promise<number> {
|
|
16
|
-
const data = await
|
|
30
|
+
const data = await getBootstrapData(options.appRoot)
|
|
17
31
|
const worker = data.modules
|
|
18
32
|
.flatMap((module) => module.workers ?? [])
|
|
19
33
|
.find((entry) => entry.queue === queueName)
|
|
@@ -21,7 +35,11 @@ export async function drainQueueFromAppRoot(
|
|
|
21
35
|
|
|
22
36
|
const container = await createRequestContainer()
|
|
23
37
|
const queue = createQueue(queueName, 'local', {
|
|
24
|
-
baseDir: path.resolve(
|
|
38
|
+
baseDir: path.resolve(
|
|
39
|
+
options.queueBaseDir?.trim()
|
|
40
|
+
|| process.env.QUEUE_BASE_DIR?.trim()
|
|
41
|
+
|| path.resolve(options.appRoot, '.mercato/queue'),
|
|
42
|
+
),
|
|
25
43
|
concurrency: 1,
|
|
26
44
|
})
|
|
27
45
|
const resolve = <T = unknown>(name: string): T => container.resolve(name) as T
|
|
@@ -53,6 +71,7 @@ export async function runQueueDrainFromEnv(): Promise<void> {
|
|
|
53
71
|
const processed = await drainQueueFromAppRoot(queueName, {
|
|
54
72
|
appRoot: path.resolve(appRoot),
|
|
55
73
|
jobLimit,
|
|
74
|
+
queueBaseDir: process.env.QUEUE_BASE_DIR,
|
|
56
75
|
})
|
|
57
76
|
console.log(JSON.stringify({ processed }))
|
|
58
77
|
}
|
|
@@ -32,7 +32,7 @@ function drainQueueInAppProcess(queueName: string, options: Required<DrainQueueO
|
|
|
32
32
|
OM_INTEGRATION_APP_ROOT: options.appRoot,
|
|
33
33
|
OM_INTEGRATION_QUEUE_NAME: queueName,
|
|
34
34
|
OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),
|
|
35
|
-
QUEUE_BASE_DIR: path.resolve(options.appRoot, '.mercato/queue'),
|
|
35
|
+
QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),
|
|
36
36
|
},
|
|
37
37
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
38
|
})
|
|
@@ -872,7 +872,32 @@ export async function createSalesDocument(page: Page, options: CreateDocumentOpt
|
|
|
872
872
|
return id;
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
|
|
875
|
+
// The Create button can detach during the click as the React form re-renders
|
|
876
|
+
// after lookups settle. Retry briefly with a short stability wait, then fall
|
|
877
|
+
// back to the API fixture path if the click never lands.
|
|
878
|
+
let clicked = false;
|
|
879
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
880
|
+
try {
|
|
881
|
+
await page.waitForLoadState('networkidle', { timeout: 2_000 }).catch(() => {});
|
|
882
|
+
const liveButton = page.getByRole('button', { name: /^Create$/i }).first();
|
|
883
|
+
if (!(await liveButton.isVisible().catch(() => false))) break;
|
|
884
|
+
if (!(await liveButton.isEnabled().catch(() => false))) {
|
|
885
|
+
await page.waitForTimeout(300);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
await liveButton.click({ timeout: 5_000 });
|
|
889
|
+
clicked = true;
|
|
890
|
+
break;
|
|
891
|
+
} catch {
|
|
892
|
+
await page.waitForTimeout(300);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (!clicked) {
|
|
896
|
+
const token = await resolveFallbackToken();
|
|
897
|
+
const id = await createSalesDocumentFixture(page, token, options.kind, customerQuery, channelQuery, fixtureContext);
|
|
898
|
+
await openSalesDocumentPage(page, id, options.kind);
|
|
899
|
+
return id;
|
|
900
|
+
}
|
|
876
901
|
const navigated = await page.waitForURL(
|
|
877
902
|
new RegExp(
|
|
878
903
|
`/backend/sales/(?:documents/[0-9a-f-]{36}\\?kind=${options.kind}|${options.kind === 'order' ? 'orders' : 'quotes'}/[0-9a-f-]{36})$`,
|
|
@@ -9,7 +9,11 @@ import { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
|
9
9
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
10
|
import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
11
11
|
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
assertActorCanGrantAcl,
|
|
14
|
+
assertActorCanModifySuperAdminRoleTarget,
|
|
15
|
+
normalizeGrantFeatureList,
|
|
16
|
+
} from '@open-mercato/core/modules/auth/lib/grantChecks'
|
|
13
17
|
|
|
14
18
|
type TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }
|
|
15
19
|
|
|
@@ -71,6 +75,23 @@ export async function GET(req: Request) {
|
|
|
71
75
|
}
|
|
72
76
|
if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null
|
|
73
77
|
|
|
78
|
+
if (!isSuperAdmin && auth.sub) {
|
|
79
|
+
try {
|
|
80
|
+
await assertActorCanModifySuperAdminRoleTarget({
|
|
81
|
+
em,
|
|
82
|
+
rbacService: container.resolve('rbacService') as RbacService,
|
|
83
|
+
actorUserId: auth.sub,
|
|
84
|
+
tenantId: tenantScope,
|
|
85
|
+
organizationId: auth.orgId ?? null,
|
|
86
|
+
targetRoleId: parsed.data.roleId,
|
|
87
|
+
actorIsSuperAdmin: false,
|
|
88
|
+
})
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
91
|
+
throw err
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
const acl = tenantScope
|
|
75
96
|
? await em.findOne(RoleAcl, { role, tenantId: tenantScope })
|
|
76
97
|
: null
|
|
@@ -133,6 +154,23 @@ export async function PUT(req: Request) {
|
|
|
133
154
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
if (!isSuperAdmin && auth.sub) {
|
|
158
|
+
try {
|
|
159
|
+
await assertActorCanModifySuperAdminRoleTarget({
|
|
160
|
+
em,
|
|
161
|
+
rbacService,
|
|
162
|
+
actorUserId: auth.sub,
|
|
163
|
+
tenantId: targetTenantId,
|
|
164
|
+
organizationId: auth.orgId ?? null,
|
|
165
|
+
targetRoleId: parsed.data.roleId,
|
|
166
|
+
actorIsSuperAdmin: false,
|
|
167
|
+
})
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
170
|
+
throw err
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
136
174
|
let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })
|
|
137
175
|
if (!acl) {
|
|
138
176
|
acl = em.create(RoleAcl, {
|
|
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
5
5
|
import { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
6
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
7
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
8
9
|
import { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'
|
|
@@ -13,6 +14,8 @@ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
13
14
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
14
15
|
import { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'
|
|
15
16
|
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
17
|
+
import { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
|
|
18
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
16
19
|
|
|
17
20
|
const querySchema = z.object({
|
|
18
21
|
id: z.string().uuid().optional(),
|
|
@@ -87,7 +90,12 @@ const crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({
|
|
|
87
90
|
update: {
|
|
88
91
|
commandId: 'auth.roles.update',
|
|
89
92
|
schema: rawBodySchema,
|
|
90
|
-
mapInput: ({ parsed }) =>
|
|
93
|
+
mapInput: async ({ parsed, ctx }) => {
|
|
94
|
+
if (ctx.request && typeof parsed.id === 'string' && parsed.id.length) {
|
|
95
|
+
await assertCanModifySuperAdminRole(ctx.request, parsed.id)
|
|
96
|
+
}
|
|
97
|
+
return parsed
|
|
98
|
+
},
|
|
91
99
|
response: () => ({ ok: true }),
|
|
92
100
|
},
|
|
93
101
|
delete: {
|
|
@@ -236,7 +244,35 @@ export async function GET(req: Request) {
|
|
|
236
244
|
|
|
237
245
|
export const POST = crud.POST
|
|
238
246
|
export const PUT = crud.PUT
|
|
239
|
-
export const DELETE =
|
|
247
|
+
export const DELETE = async (req: Request) => {
|
|
248
|
+
const targetId = new URL(req.url).searchParams.get('id')
|
|
249
|
+
if (targetId) {
|
|
250
|
+
try {
|
|
251
|
+
await assertCanModifySuperAdminRole(req, targetId)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof CrudHttpError) {
|
|
254
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
255
|
+
}
|
|
256
|
+
throw err
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return crud.DELETE(req)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function assertCanModifySuperAdminRole(req: Request, targetRoleId: string) {
|
|
263
|
+
const auth = await getAuthFromRequest(req)
|
|
264
|
+
if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })
|
|
265
|
+
const container = await createRequestContainer()
|
|
266
|
+
const em = container.resolve('em') as EntityManager
|
|
267
|
+
await assertActorCanModifySuperAdminRoleTarget({
|
|
268
|
+
em,
|
|
269
|
+
rbacService: container.resolve('rbacService') as RbacService,
|
|
270
|
+
actorUserId: auth.sub,
|
|
271
|
+
tenantId: auth.tenantId ?? null,
|
|
272
|
+
organizationId: auth.orgId ?? null,
|
|
273
|
+
targetRoleId,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
240
276
|
|
|
241
277
|
export const openApi: OpenApiRouteDoc = {
|
|
242
278
|
tag: 'Authentication & Accounts',
|
|
@@ -4,8 +4,11 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
|
4
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
6
6
|
import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
|
|
7
|
-
import { forbidden } from '@open-mercato/shared/lib/crud/errors'
|
|
7
|
+
import { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
8
|
import { UserAcl } from '@open-mercato/core/modules/auth/data/entities'
|
|
9
|
+
import { assertActorCanModifySuperAdminUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
|
|
10
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
11
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
9
12
|
|
|
10
13
|
const getSchema = z.object({ userId: z.string().uuid() })
|
|
11
14
|
const putSchema = z.object({
|
|
@@ -42,6 +45,26 @@ export async function GET(req: Request) {
|
|
|
42
45
|
if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
|
|
43
46
|
const container = await createRequestContainer()
|
|
44
47
|
const em = container.resolve('em') as any
|
|
48
|
+
const rbacService = container.resolve('rbacService') as any
|
|
49
|
+
const actorAcl = auth.sub
|
|
50
|
+
? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
|
|
51
|
+
: null
|
|
52
|
+
if (!actorAcl?.isSuperAdmin && auth.sub) {
|
|
53
|
+
try {
|
|
54
|
+
await assertActorCanModifySuperAdminUserTarget({
|
|
55
|
+
em: em as EntityManager,
|
|
56
|
+
rbacService: rbacService as RbacService,
|
|
57
|
+
actorUserId: auth.sub,
|
|
58
|
+
tenantId: auth.tenantId ?? null,
|
|
59
|
+
organizationId: auth.orgId ?? null,
|
|
60
|
+
targetUserId: parsed.data.userId,
|
|
61
|
+
actorIsSuperAdmin: false,
|
|
62
|
+
})
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
65
|
+
throw err
|
|
66
|
+
}
|
|
67
|
+
}
|
|
45
68
|
const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })
|
|
46
69
|
const response = acl
|
|
47
70
|
? {
|
|
@@ -83,6 +106,23 @@ export async function PUT(req: Request) {
|
|
|
83
106
|
: null
|
|
84
107
|
const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin
|
|
85
108
|
|
|
109
|
+
if (!actorIsSuperAdmin && auth.sub) {
|
|
110
|
+
try {
|
|
111
|
+
await assertActorCanModifySuperAdminUserTarget({
|
|
112
|
+
em: em as EntityManager,
|
|
113
|
+
rbacService: rbacService as RbacService,
|
|
114
|
+
actorUserId: auth.sub,
|
|
115
|
+
tenantId: auth.tenantId ?? null,
|
|
116
|
+
organizationId: auth.orgId ?? null,
|
|
117
|
+
targetUserId: parsed.data.userId,
|
|
118
|
+
actorIsSuperAdmin: false,
|
|
119
|
+
})
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
122
|
+
throw err
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
86
126
|
const requestedFeatures = normalizeFeatureList(parsed.data.features)
|
|
87
127
|
const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null
|
|
88
128
|
|