@open-mercato/core 0.6.4-develop.4236.1.9fa6806b34 → 0.6.4-develop.4254.1.7a123d970c
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/authFixtures.js +70 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/dbFixtures.js +98 -0
- package/dist/helpers/integration/dbFixtures.js.map +7 -0
- package/dist/modules/business_rules/api/execute/route.js +2 -1
- package/dist/modules/business_rules/api/execute/route.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +10 -0
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/backend/logs/[id]/page.js +24 -5
- package/dist/modules/business_rules/backend/logs/[id]/page.js.map +2 -2
- package/dist/modules/business_rules/cli.js +6 -0
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +116 -9
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js +3 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js.map +2 -2
- package/dist/modules/catalog/api/offers/route.js +15 -5
- package/dist/modules/catalog/api/offers/route.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +21 -4
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/lib/pricing.js +6 -0
- package/dist/modules/catalog/lib/pricing.js.map +2 -2
- package/dist/modules/catalog/services/catalogPricingService.js +5 -1
- package/dist/modules/catalog/services/catalogPricingService.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +19 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +27 -7
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +27 -7
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +15 -2
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/comments/route.js +15 -3
- package/dist/modules/customers/api/comments/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +2 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +2 -4
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +2 -9
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +2 -8
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +2 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/[id]/page.js +29 -8
- package/dist/modules/customers/backend/customers/people/[id]/page.js.map +2 -2
- package/dist/modules/directory/utils/organizationScopeGuard.js +22 -0
- package/dist/modules/directory/utils/organizationScopeGuard.js.map +7 -0
- package/dist/modules/progress/acl.js +8 -4
- package/dist/modules/progress/acl.js.map +2 -2
- package/dist/modules/workflows/backend/events/[id]/page.js +24 -6
- package/dist/modules/workflows/backend/events/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.js +27 -5
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +25 -6
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +8 -0
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +8 -4
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/setup.js +3 -1
- package/dist/modules/workflows/setup.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/authFixtures.ts +98 -0
- package/src/helpers/integration/dbFixtures.ts +144 -0
- package/src/modules/business_rules/api/execute/route.ts +2 -1
- package/src/modules/business_rules/api/rules/route.ts +10 -0
- package/src/modules/business_rules/backend/logs/[id]/page.tsx +32 -7
- package/src/modules/business_rules/cli.ts +6 -0
- package/src/modules/business_rules/lib/rule-engine.ts +163 -9
- package/src/modules/business_rules/subscribers/crud-rule-trigger.ts +3 -2
- package/src/modules/catalog/api/offers/route.ts +20 -5
- package/src/modules/catalog/api/products/route.ts +23 -4
- package/src/modules/catalog/lib/pricing.ts +9 -0
- package/src/modules/catalog/services/catalogPricingService.ts +6 -0
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +21 -2
- package/src/modules/currencies/i18n/de.json +1 -0
- package/src/modules/currencies/i18n/en.json +1 -0
- package/src/modules/currencies/i18n/es.json +1 -0
- package/src/modules/currencies/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +34 -11
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +34 -11
- package/src/modules/customers/api/activities/route.ts +16 -5
- package/src/modules/customers/api/comments/route.ts +15 -5
- package/src/modules/customers/api/companies/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/companies/[id]/route.ts +2 -5
- package/src/modules/customers/api/deals/[id]/companies/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/people/route.ts +2 -4
- package/src/modules/customers/api/deals/[id]/route.ts +2 -9
- package/src/modules/customers/api/deals/[id]/stats/route.ts +2 -9
- package/src/modules/customers/api/entity-roles-factory.ts +2 -12
- package/src/modules/customers/api/people/[id]/companies/context.ts +2 -5
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +2 -5
- package/src/modules/customers/api/people/[id]/route.ts +2 -5
- package/src/modules/customers/backend/customers/people/[id]/page.tsx +35 -11
- package/src/modules/directory/utils/organizationScopeGuard.ts +39 -0
- package/src/modules/progress/acl.ts +4 -0
- package/src/modules/workflows/backend/events/[id]/page.tsx +32 -10
- package/src/modules/workflows/backend/instances/[id]/page.tsx +33 -9
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +33 -10
- package/src/modules/workflows/cli.ts +8 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/seeds.ts +13 -3
- package/src/modules/workflows/setup.ts +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/workflows/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrl, getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { WorkflowDefinition } from './data/entities'\nimport { BusinessRule, type RuleType } from '@open-mercato/core/modules/business_rules/data/entities'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\n/**\n * Parse CLI arguments\n */\nfunction parseArgs(args: string[]) {\n const result: Record<string, string> = {}\n for (let i = 0; i < args.length; i += 2) {\n const key = args[i]?.replace(/^-+/, '') // Remove one or more dashes\n const value = args[i + 1]\n if (key && value) {\n result[key] = value\n }\n }\n return result\n}\n\n/**\n * Seed demo checkout workflow\n *\n * The 'workflows.checkout-demo' workflow is now code-defined\n * (see packages/core/src/modules/workflows/workflows.ts) and available\n * to every tenant without DB seeding. This command remains as a thin\n * wrapper that informs operators of the new location.\n */\nconst seedDemo: ModuleCli = {\n command: 'seed-demo',\n async run() {\n console.log('\u2139\uFE0F The \"workflows.checkout-demo\" workflow is now code-defined.')\n console.log(' No seeding required \u2014 it is auto-registered by the workflows module')\n console.log(' (see packages/core/src/modules/workflows/workflows.ts).')\n console.log('')\n console.log(' Visit /backend/definitions to view it as a Code-defined workflow.')\n },\n}\n\n/**\n * Seed demo checkout workflow guard rules\n *\n * The 'workflows.checkout-demo' workflow is code-defined; only the\n * guard rules still need DB seeding.\n */\nconst seedDemoWithRules: ModuleCli = {\n command: 'seed-demo-with-rules',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-demo-with-rules --tenant <tenantId> --org <organizationId>')\n console.error(' or: mercato workflows seed-demo-with-rules -t <tenantId> -o <organizationId>')\n return\n }\n\n console.log('\uD83E\uDDE9 Seeding checkout-demo guard rules (workflow itself is code-defined)...\\n')\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n\n // Import BusinessRule entity\n const { BusinessRule } = await import('../business_rules/data/entities')\n\n // Read guard rules\n const rulesPath = path.join(__dirname, 'examples', 'guard-rules-example.json')\n const rulesData = JSON.parse(fs.readFileSync(rulesPath, 'utf8'))\n\n let seededCount = 0\n let skippedCount = 0\n\n for (const ruleData of rulesData) {\n const existing = await em.findOne(BusinessRule, {\n ruleId: ruleData.ruleId,\n tenantId,\n organizationId,\n })\n\n if (existing) {\n console.log(` \u2298 Guard rule '${ruleData.ruleId}' already exists`)\n skippedCount++\n continue\n }\n\n const rule = em.create(BusinessRule, {\n ...ruleData,\n tenantId,\n organizationId,\n })\n\n await em.persist(rule).flush()\n console.log(` \u2713 Seeded guard rule: ${rule.ruleName}`)\n seededCount++\n }\n\n console.log(`\\n\u2705 Checkout-demo guard rules seeded successfully!`)\n console.log(` - Workflow: workflows.checkout-demo (code-defined)`)\n console.log(` - Guard rules seeded: ${seededCount}`)\n console.log(` - Guard rules skipped: ${skippedCount}`)\n } catch (error) {\n console.error('Error seeding demo guard rules:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed sales pipeline example\n */\nconst seedSalesPipeline: ModuleCli = {\n command: 'seed-sales-pipeline',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-sales-pipeline --tenant <tenantId> --org <organizationId>')\n return\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n\n // Read the sales pipeline workflow definition\n const pipelinePath = path.join(__dirname, 'examples', 'sales-pipeline-definition.json')\n const pipelineData = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'))\n\n // Check if it already exists\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId: pipelineData.workflowId,\n tenantId,\n organizationId,\n })\n\n if (existing) {\n console.log(`\u2139\uFE0F Sales pipeline workflow '${pipelineData.workflowId}' already exists (ID: ${existing.id})`)\n return\n }\n\n // Create the workflow definition\n const workflow = em.create(WorkflowDefinition, {\n ...pipelineData,\n tenantId,\n organizationId,\n })\n\n await em.persist(workflow).flush()\n\n console.log(`\u2705 Seeded sales pipeline workflow: ${workflow.workflowName}`)\n console.log(` - ID: ${workflow.id}`)\n console.log(` - Workflow ID: ${workflow.workflowId}`)\n console.log(` - Version: ${workflow.version}`)\n console.log(` - Steps: ${workflow.definition.steps.length}`)\n console.log(` - Transitions: ${workflow.definition.transitions.length}`)\n console.log(` - Activities: ${workflow.definition.transitions.reduce((sum, t) => sum + (t.activities?.length || 0), 0)}`)\n console.log('')\n console.log('Sales pipeline workflow is ready!')\n } catch (error) {\n console.error('Error seeding sales pipeline workflow:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed simple approval example\n *\n * The 'workflows.simple-approval' workflow is now code-defined\n * (see packages/core/src/modules/workflows/workflows.ts). This command\n * remains as a thin wrapper that informs operators of the new location.\n */\nconst seedSimpleApproval: ModuleCli = {\n command: 'seed-simple-approval',\n async run() {\n console.log('\u2139\uFE0F The \"workflows.simple-approval\" workflow is now code-defined.')\n console.log(' No seeding required \u2014 it is auto-registered by the workflows module')\n console.log(' (see packages/core/src/modules/workflows/workflows.ts).')\n console.log('')\n console.log(' Visit /backend/definitions to view it as a Code-defined workflow.')\n },\n}\n\n/**\n * Seed order approval example\n */\nconst seedOrderApproval: ModuleCli = {\n command: 'seed-order-approval',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-order-approval --tenant <tenantId> --org <organizationId>')\n return\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n\n // 1. Seed order approval guard rules first\n const guardRulesPath = path.join(__dirname, 'examples', 'order-approval-guard-rules.json')\n const guardRulesData = JSON.parse(fs.readFileSync(guardRulesPath, 'utf8')) as Array<{\n ruleId: string\n ruleName: string\n ruleType: RuleType\n entityType: string\n description?: string\n eventType?: string\n conditionExpression?: Record<string, unknown>\n enabled?: boolean\n priority?: number\n }>\n\n let rulesSeeded = 0\n let rulesSkipped = 0\n for (const rule of guardRulesData) {\n const existingRule = await em.findOne(BusinessRule, {\n ruleId: rule.ruleId,\n tenantId,\n organizationId,\n })\n\n if (existingRule) {\n rulesSkipped++\n continue\n }\n\n const newRule = em.create(BusinessRule, {\n ...rule,\n tenantId,\n organizationId,\n })\n em.persist(newRule)\n console.log(` \u2713 Seeded guard rule: ${rule.ruleName}`)\n rulesSeeded++\n }\n\n if (rulesSeeded > 0) {\n await em.flush()\n }\n\n console.log(`\u2705 Seeded order approval guard rules`)\n console.log(` - Guard rules seeded: ${rulesSeeded}`)\n console.log(` - Guard rules skipped: ${rulesSkipped}`)\n console.log('')\n console.log('Note: The \"sales.order-approval\" workflow definition is now code-defined')\n console.log('(see packages/core/src/modules/sales/workflows.ts) and no longer needs DB seeding.')\n } catch (error) {\n console.error('Error seeding order approval workflow:', error)\n throw error\n }\n },\n}\n\n/**\n * Start workflow activity worker\n */\nconst startWorker: ModuleCli = {\n command: 'start-worker',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const concurrency = parseInt(args.concurrency ?? args.c ?? '5')\n\n const strategy = process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n\n console.log('[Workflow Worker] Starting activity worker...')\n console.log(`[Workflow Worker] Strategy: ${strategy}`)\n\n if (strategy === 'local') {\n const pollMs = process.env.LOCAL_QUEUE_POLL_MS || '5000'\n console.log(`[Workflow Worker] Polling interval: ${pollMs}ms`)\n console.log('[Workflow Worker] NOTE: Local strategy is for development only.')\n console.log('[Workflow Worker] Use QUEUE_STRATEGY=async with Redis for production.')\n } else {\n console.log(`[Workflow Worker] Concurrency: ${concurrency}`)\n console.log(`[Workflow Worker] Redis: ${getRedisUrl('QUEUE') ?? '(not configured)'}`)\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Import queue and handler\n const { runWorker } = await import('@open-mercato/queue/worker')\n const { createActivityWorkerHandler } = await import('./lib/activity-worker-handler')\n const { WORKFLOW_ACTIVITIES_QUEUE_NAME } = await import('./lib/activity-queue-types')\n\n // Create handler\n const handler = createActivityWorkerHandler(em, container)\n\n // Run worker\n await runWorker({\n queueName: WORKFLOW_ACTIVITIES_QUEUE_NAME,\n handler,\n connection: strategy === 'async' ? {\n url: getRedisUrlOrThrow('QUEUE'),\n } : undefined,\n concurrency,\n gracefulShutdown: true,\n })\n } catch (error) {\n console.error('[Workflow Worker] Failed to start worker:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed all example workflows\n */\nconst seedAll: ModuleCli = {\n command: 'seed-all',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-all --tenant <tenantId> --org <organizationId>')\n return\n }\n\n console.log('\uD83E\uDDE9 Seeding all example workflows...\\n')\n\n try {\n // Seed demo checkout with rules\n await seedDemoWithRules.run(rest)\n console.log('')\n\n // Seed sales pipeline\n await seedSalesPipeline.run(rest)\n console.log('')\n\n // Seed simple approval\n await seedSimpleApproval.run(rest)\n console.log('')\n\n // Seed order approval\n await seedOrderApproval.run(rest)\n console.log('')\n\n console.log('\u2705 All example workflows seeded successfully!')\n } catch (error) {\n console.error('Error seeding workflows:', error)\n throw error\n }\n },\n}\n\n/**\n * Manually process pending workflow activities\n */\nconst processActivities: ModuleCli = {\n command: 'process-activities',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const limit = parseInt(args.limit ?? args.l ?? '0')\n\n console.log('[Workflow Activities] Processing pending activities...')\n if (limit > 0) {\n console.log(`[Workflow Activities] Limit: ${limit} jobs`)\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Import queue and handler\n const { createQueue } = await import('@open-mercato/queue')\n const { createActivityWorkerHandler } = await import('./lib/activity-worker-handler')\n const { WORKFLOW_ACTIVITIES_QUEUE_NAME } = await import('./lib/activity-queue-types')\n\n // Create queue instance\n const queue = createQueue(WORKFLOW_ACTIVITIES_QUEUE_NAME, 'local')\n\n // Create handler\n const handler = createActivityWorkerHandler(em, container)\n\n // Get initial counts\n const initialCounts = await queue.getJobCounts()\n console.log(`[Workflow Activities] Pending jobs: ${initialCounts.waiting}`)\n\n if (initialCounts.waiting === 0) {\n console.log('[Workflow Activities] No jobs to process')\n await queue.close()\n return\n }\n\n // Process jobs\n const result = await queue.process(handler as any, limit > 0 ? { limit } : undefined)\n\n console.log(`\\n[Workflow Activities] \u2713 Processed ${result.processed} activities`)\n if (result.failed > 0) {\n console.log(`[Workflow Activities] \u2717 Failed: ${result.failed} activities`)\n }\n\n // Show remaining\n const finalCounts = await queue.getJobCounts()\n if (finalCounts.waiting > 0) {\n console.log(`[Workflow Activities] Remaining: ${finalCounts.waiting} jobs`)\n }\n\n await queue.close()\n } catch (error) {\n console.error('[Workflow Activities] Error processing activities:', error)\n throw error\n }\n },\n}\n\nconst workflowsCliCommands = [\n startWorker,\n processActivities,\n seedDemo,\n seedDemoWithRules,\n seedSalesPipeline,\n seedSimpleApproval,\n seedOrderApproval,\n seedAll,\n]\n\nexport default workflowsCliCommands\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,aAAa,0BAA0B;AAEhD,SAAS,0BAA0B;AACnC,SAAS,oBAAmC;AAC5C,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,qBAAqB;AAE9B,MAAM,aAAa,cAAc,YAAY,GAAG;AAChD,MAAM,YAAY,KAAK,QAAQ,UAAU;AAKzC,SAAS,UAAU,MAAgB;AACjC,QAAM,SAAiC,CAAC;AACxC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,GAAG;AACvC,UAAM,MAAM,KAAK,CAAC,GAAG,QAAQ,OAAO,EAAE;AACtC,UAAM,QAAQ,KAAK,IAAI,CAAC;AACxB,QAAI,OAAO,OAAO;AAChB,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAUA,MAAM,WAAsB;AAAA,EAC1B,SAAS;AAAA,EACT,MAAM,MAAM;AACV,YAAQ,IAAI,2EAAiE;AAC7E,YAAQ,IAAI,6EAAwE;AACpF,YAAQ,IAAI,4DAA4D;AACxE,YAAQ,IAAI,EAAE;AACd,YAAQ,IAAI,sEAAsE;AAAA,EACpF;AACF;AAQA,MAAM,oBAA+B;AAAA,EACnC,SAAS;AAAA,EACT,MAAM,IAAI,MAAgB;AACxB,UAAM,OAAO,UAAU,IAAI;AAC3B,UAAM,WAAW,OAAO,KAAK,YAAY,KAAK,UAAU,KAAK,KAAK,EAAE;AACpE,UAAM,iBAAiB,OAAO,KAAK,kBAAkB,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,EAAE;AAE3F,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,cAAQ,MAAM,0FAA0F;AACxG,cAAQ,MAAM,iFAAiF;AAC/F;AAAA,IACF;AAEA,YAAQ,IAAI,oFAA6E;AAEzF,QAAI;AACF,YAAM,EAAE,QAAQ,IAAI,MAAM,uBAAuB;AACjD,YAAM,KAAK,QAAuB,IAAI;
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrl, getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { WorkflowDefinition } from './data/entities'\nimport { BusinessRule, type RuleType } from '@open-mercato/core/modules/business_rules/data/entities'\nimport {\n invalidateBusinessRuleDiscoveryCache,\n resolveBusinessRuleDiscoveryCache,\n} from '@open-mercato/core/modules/business_rules/lib/rule-engine'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\n/**\n * Parse CLI arguments\n */\nfunction parseArgs(args: string[]) {\n const result: Record<string, string> = {}\n for (let i = 0; i < args.length; i += 2) {\n const key = args[i]?.replace(/^-+/, '') // Remove one or more dashes\n const value = args[i + 1]\n if (key && value) {\n result[key] = value\n }\n }\n return result\n}\n\n/**\n * Seed demo checkout workflow\n *\n * The 'workflows.checkout-demo' workflow is now code-defined\n * (see packages/core/src/modules/workflows/workflows.ts) and available\n * to every tenant without DB seeding. This command remains as a thin\n * wrapper that informs operators of the new location.\n */\nconst seedDemo: ModuleCli = {\n command: 'seed-demo',\n async run() {\n console.log('\u2139\uFE0F The \"workflows.checkout-demo\" workflow is now code-defined.')\n console.log(' No seeding required \u2014 it is auto-registered by the workflows module')\n console.log(' (see packages/core/src/modules/workflows/workflows.ts).')\n console.log('')\n console.log(' Visit /backend/definitions to view it as a Code-defined workflow.')\n },\n}\n\n/**\n * Seed demo checkout workflow guard rules\n *\n * The 'workflows.checkout-demo' workflow is code-defined; only the\n * guard rules still need DB seeding.\n */\nconst seedDemoWithRules: ModuleCli = {\n command: 'seed-demo-with-rules',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-demo-with-rules --tenant <tenantId> --org <organizationId>')\n console.error(' or: mercato workflows seed-demo-with-rules -t <tenantId> -o <organizationId>')\n return\n }\n\n console.log('\uD83E\uDDE9 Seeding checkout-demo guard rules (workflow itself is code-defined)...\\n')\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n const cache = resolveBusinessRuleDiscoveryCache(resolve)\n\n // Import BusinessRule entity\n const { BusinessRule } = await import('../business_rules/data/entities')\n\n // Read guard rules\n const rulesPath = path.join(__dirname, 'examples', 'guard-rules-example.json')\n const rulesData = JSON.parse(fs.readFileSync(rulesPath, 'utf8'))\n\n let seededCount = 0\n let skippedCount = 0\n\n for (const ruleData of rulesData) {\n const existing = await em.findOne(BusinessRule, {\n ruleId: ruleData.ruleId,\n tenantId,\n organizationId,\n })\n\n if (existing) {\n console.log(` \u2298 Guard rule '${ruleData.ruleId}' already exists`)\n skippedCount++\n continue\n }\n\n const rule = em.create(BusinessRule, {\n ...ruleData,\n tenantId,\n organizationId,\n })\n\n await em.persist(rule).flush()\n await invalidateBusinessRuleDiscoveryCache(cache, tenantId, organizationId)\n console.log(` \u2713 Seeded guard rule: ${rule.ruleName}`)\n seededCount++\n }\n\n console.log(`\\n\u2705 Checkout-demo guard rules seeded successfully!`)\n console.log(` - Workflow: workflows.checkout-demo (code-defined)`)\n console.log(` - Guard rules seeded: ${seededCount}`)\n console.log(` - Guard rules skipped: ${skippedCount}`)\n } catch (error) {\n console.error('Error seeding demo guard rules:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed sales pipeline example\n */\nconst seedSalesPipeline: ModuleCli = {\n command: 'seed-sales-pipeline',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-sales-pipeline --tenant <tenantId> --org <organizationId>')\n return\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n\n // Read the sales pipeline workflow definition\n const pipelinePath = path.join(__dirname, 'examples', 'sales-pipeline-definition.json')\n const pipelineData = JSON.parse(fs.readFileSync(pipelinePath, 'utf8'))\n\n // Check if it already exists\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId: pipelineData.workflowId,\n tenantId,\n organizationId,\n })\n\n if (existing) {\n console.log(`\u2139\uFE0F Sales pipeline workflow '${pipelineData.workflowId}' already exists (ID: ${existing.id})`)\n return\n }\n\n // Create the workflow definition\n const workflow = em.create(WorkflowDefinition, {\n ...pipelineData,\n tenantId,\n organizationId,\n })\n\n await em.persist(workflow).flush()\n\n console.log(`\u2705 Seeded sales pipeline workflow: ${workflow.workflowName}`)\n console.log(` - ID: ${workflow.id}`)\n console.log(` - Workflow ID: ${workflow.workflowId}`)\n console.log(` - Version: ${workflow.version}`)\n console.log(` - Steps: ${workflow.definition.steps.length}`)\n console.log(` - Transitions: ${workflow.definition.transitions.length}`)\n console.log(` - Activities: ${workflow.definition.transitions.reduce((sum, t) => sum + (t.activities?.length || 0), 0)}`)\n console.log('')\n console.log('Sales pipeline workflow is ready!')\n } catch (error) {\n console.error('Error seeding sales pipeline workflow:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed simple approval example\n *\n * The 'workflows.simple-approval' workflow is now code-defined\n * (see packages/core/src/modules/workflows/workflows.ts). This command\n * remains as a thin wrapper that informs operators of the new location.\n */\nconst seedSimpleApproval: ModuleCli = {\n command: 'seed-simple-approval',\n async run() {\n console.log('\u2139\uFE0F The \"workflows.simple-approval\" workflow is now code-defined.')\n console.log(' No seeding required \u2014 it is auto-registered by the workflows module')\n console.log(' (see packages/core/src/modules/workflows/workflows.ts).')\n console.log('')\n console.log(' Visit /backend/definitions to view it as a Code-defined workflow.')\n },\n}\n\n/**\n * Seed order approval example\n */\nconst seedOrderApproval: ModuleCli = {\n command: 'seed-order-approval',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-order-approval --tenant <tenantId> --org <organizationId>')\n return\n }\n\n try {\n const { resolve } = await createRequestContainer()\n const em = resolve<EntityManager>('em')\n const cache = resolveBusinessRuleDiscoveryCache(resolve)\n\n // 1. Seed order approval guard rules first\n const guardRulesPath = path.join(__dirname, 'examples', 'order-approval-guard-rules.json')\n const guardRulesData = JSON.parse(fs.readFileSync(guardRulesPath, 'utf8')) as Array<{\n ruleId: string\n ruleName: string\n ruleType: RuleType\n entityType: string\n description?: string\n eventType?: string\n conditionExpression?: Record<string, unknown>\n enabled?: boolean\n priority?: number\n }>\n\n let rulesSeeded = 0\n let rulesSkipped = 0\n for (const rule of guardRulesData) {\n const existingRule = await em.findOne(BusinessRule, {\n ruleId: rule.ruleId,\n tenantId,\n organizationId,\n })\n\n if (existingRule) {\n rulesSkipped++\n continue\n }\n\n const newRule = em.create(BusinessRule, {\n ...rule,\n tenantId,\n organizationId,\n })\n em.persist(newRule)\n console.log(` \u2713 Seeded guard rule: ${rule.ruleName}`)\n rulesSeeded++\n }\n\n if (rulesSeeded > 0) {\n await em.flush()\n await invalidateBusinessRuleDiscoveryCache(cache, tenantId, organizationId)\n }\n\n console.log(`\u2705 Seeded order approval guard rules`)\n console.log(` - Guard rules seeded: ${rulesSeeded}`)\n console.log(` - Guard rules skipped: ${rulesSkipped}`)\n console.log('')\n console.log('Note: The \"sales.order-approval\" workflow definition is now code-defined')\n console.log('(see packages/core/src/modules/sales/workflows.ts) and no longer needs DB seeding.')\n } catch (error) {\n console.error('Error seeding order approval workflow:', error)\n throw error\n }\n },\n}\n\n/**\n * Start workflow activity worker\n */\nconst startWorker: ModuleCli = {\n command: 'start-worker',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const concurrency = parseInt(args.concurrency ?? args.c ?? '5')\n\n const strategy = process.env.QUEUE_STRATEGY === 'async' ? 'async' : 'local'\n\n console.log('[Workflow Worker] Starting activity worker...')\n console.log(`[Workflow Worker] Strategy: ${strategy}`)\n\n if (strategy === 'local') {\n const pollMs = process.env.LOCAL_QUEUE_POLL_MS || '5000'\n console.log(`[Workflow Worker] Polling interval: ${pollMs}ms`)\n console.log('[Workflow Worker] NOTE: Local strategy is for development only.')\n console.log('[Workflow Worker] Use QUEUE_STRATEGY=async with Redis for production.')\n } else {\n console.log(`[Workflow Worker] Concurrency: ${concurrency}`)\n console.log(`[Workflow Worker] Redis: ${getRedisUrl('QUEUE') ?? '(not configured)'}`)\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Import queue and handler\n const { runWorker } = await import('@open-mercato/queue/worker')\n const { createActivityWorkerHandler } = await import('./lib/activity-worker-handler')\n const { WORKFLOW_ACTIVITIES_QUEUE_NAME } = await import('./lib/activity-queue-types')\n\n // Create handler\n const handler = createActivityWorkerHandler(em, container)\n\n // Run worker\n await runWorker({\n queueName: WORKFLOW_ACTIVITIES_QUEUE_NAME,\n handler,\n connection: strategy === 'async' ? {\n url: getRedisUrlOrThrow('QUEUE'),\n } : undefined,\n concurrency,\n gracefulShutdown: true,\n })\n } catch (error) {\n console.error('[Workflow Worker] Failed to start worker:', error)\n throw error\n }\n },\n}\n\n/**\n * Seed all example workflows\n */\nconst seedAll: ModuleCli = {\n command: 'seed-all',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const tenantId = String(args.tenantId ?? args.tenant ?? args.t ?? '')\n const organizationId = String(args.organizationId ?? args.orgId ?? args.org ?? args.o ?? '')\n\n if (!tenantId || !organizationId) {\n console.error('Usage: mercato workflows seed-all --tenant <tenantId> --org <organizationId>')\n return\n }\n\n console.log('\uD83E\uDDE9 Seeding all example workflows...\\n')\n\n try {\n // Seed demo checkout with rules\n await seedDemoWithRules.run(rest)\n console.log('')\n\n // Seed sales pipeline\n await seedSalesPipeline.run(rest)\n console.log('')\n\n // Seed simple approval\n await seedSimpleApproval.run(rest)\n console.log('')\n\n // Seed order approval\n await seedOrderApproval.run(rest)\n console.log('')\n\n console.log('\u2705 All example workflows seeded successfully!')\n } catch (error) {\n console.error('Error seeding workflows:', error)\n throw error\n }\n },\n}\n\n/**\n * Manually process pending workflow activities\n */\nconst processActivities: ModuleCli = {\n command: 'process-activities',\n async run(rest: string[]) {\n const args = parseArgs(rest)\n const limit = parseInt(args.limit ?? args.l ?? '0')\n\n console.log('[Workflow Activities] Processing pending activities...')\n if (limit > 0) {\n console.log(`[Workflow Activities] Limit: ${limit} jobs`)\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n\n // Import queue and handler\n const { createQueue } = await import('@open-mercato/queue')\n const { createActivityWorkerHandler } = await import('./lib/activity-worker-handler')\n const { WORKFLOW_ACTIVITIES_QUEUE_NAME } = await import('./lib/activity-queue-types')\n\n // Create queue instance\n const queue = createQueue(WORKFLOW_ACTIVITIES_QUEUE_NAME, 'local')\n\n // Create handler\n const handler = createActivityWorkerHandler(em, container)\n\n // Get initial counts\n const initialCounts = await queue.getJobCounts()\n console.log(`[Workflow Activities] Pending jobs: ${initialCounts.waiting}`)\n\n if (initialCounts.waiting === 0) {\n console.log('[Workflow Activities] No jobs to process')\n await queue.close()\n return\n }\n\n // Process jobs\n const result = await queue.process(handler as any, limit > 0 ? { limit } : undefined)\n\n console.log(`\\n[Workflow Activities] \u2713 Processed ${result.processed} activities`)\n if (result.failed > 0) {\n console.log(`[Workflow Activities] \u2717 Failed: ${result.failed} activities`)\n }\n\n // Show remaining\n const finalCounts = await queue.getJobCounts()\n if (finalCounts.waiting > 0) {\n console.log(`[Workflow Activities] Remaining: ${finalCounts.waiting} jobs`)\n }\n\n await queue.close()\n } catch (error) {\n console.error('[Workflow Activities] Error processing activities:', error)\n throw error\n }\n },\n}\n\nconst workflowsCliCommands = [\n startWorker,\n processActivities,\n seedDemo,\n seedDemoWithRules,\n seedSalesPipeline,\n seedSimpleApproval,\n seedOrderApproval,\n seedAll,\n]\n\nexport default workflowsCliCommands\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["BusinessRule"]
|
|
7
7
|
}
|
|
@@ -3,6 +3,9 @@ import * as path from "path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { WorkflowDefinition } from "../data/entities.js";
|
|
5
5
|
import { BusinessRule } from "@open-mercato/core/modules/business_rules/data/entities";
|
|
6
|
+
import {
|
|
7
|
+
invalidateBusinessRuleDiscoveryCache
|
|
8
|
+
} from "@open-mercato/core/modules/business_rules/lib/rule-engine";
|
|
6
9
|
const __esmDirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
10
|
function readExampleJson(fileName) {
|
|
8
11
|
const candidates = [
|
|
@@ -62,7 +65,7 @@ async function seedWorkflowDefinition(em, scope, fileName) {
|
|
|
62
65
|
await em.flush();
|
|
63
66
|
return true;
|
|
64
67
|
}
|
|
65
|
-
async function seedGuardRules(em, scope, fileName) {
|
|
68
|
+
async function seedGuardRules(em, scope, fileName, cache) {
|
|
66
69
|
const seeds = readExampleJson(fileName);
|
|
67
70
|
if (!Array.isArray(seeds)) {
|
|
68
71
|
throw new Error("Invalid guard rules seed data.");
|
|
@@ -115,13 +118,14 @@ async function seedGuardRules(em, scope, fileName) {
|
|
|
115
118
|
}
|
|
116
119
|
if (seeded > 0 || updated > 0) {
|
|
117
120
|
await em.flush();
|
|
121
|
+
await invalidateBusinessRuleDiscoveryCache(cache, scope.tenantId, scope.organizationId);
|
|
118
122
|
}
|
|
119
123
|
return { seeded, skipped, updated };
|
|
120
124
|
}
|
|
121
|
-
async function seedExampleWorkflows(em, scope) {
|
|
122
|
-
await seedGuardRules(em, scope, "guard-rules-example.json");
|
|
125
|
+
async function seedExampleWorkflows(em, scope, options = {}) {
|
|
126
|
+
await seedGuardRules(em, scope, "guard-rules-example.json", options.cache);
|
|
123
127
|
await seedWorkflowDefinition(em, scope, "sales-pipeline-definition.json");
|
|
124
|
-
await seedGuardRules(em, scope, "order-approval-guard-rules.json");
|
|
128
|
+
await seedGuardRules(em, scope, "order-approval-guard-rules.json", options.cache);
|
|
125
129
|
}
|
|
126
130
|
export {
|
|
127
131
|
seedExampleWorkflows
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/seeds.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { WorkflowDefinition, type WorkflowDefinitionData } from '../data/entities'\nimport { BusinessRule, type RuleType } from '@open-mercato/core/modules/business_rules/data/entities'\n\nconst __esmDirname = path.dirname(fileURLToPath(import.meta.url))\n\nexport type WorkflowSeedScope = { tenantId: string; organizationId: string }\n\ntype WorkflowSeedDefinition = {\n workflowId: string\n workflowName: string\n description?: string | null\n version?: number\n definition: WorkflowDefinitionData\n metadata?: Record<string, unknown> | null\n enabled?: boolean\n effectiveFrom?: string | null\n effectiveTo?: string | null\n createdBy?: string | null\n updatedBy?: string | null\n}\n\ntype GuardRuleSeed = {\n ruleId: string\n ruleName: string\n ruleType: RuleType\n entityType: string\n conditionExpression: unknown\n eventType?: string | null\n ruleCategory?: string | null\n description?: string | null\n successActions?: unknown\n failureActions?: unknown\n enabled?: boolean\n priority?: number\n version?: number\n effectiveFrom?: string | null\n effectiveTo?: string | null\n createdBy?: string | null\n updatedBy?: string | null\n tagsJson?: string[]\n labelsJson?: Record<string, string>\n}\n\nfunction readExampleJson<T>(fileName: string): T {\n const candidates = [\n path.join(__esmDirname, '..', 'examples', fileName),\n path.join(process.cwd(), 'packages', 'core', 'src', 'modules', 'workflows', 'examples', fileName),\n path.join(process.cwd(), 'src', 'modules', 'workflows', 'examples', fileName),\n ]\n const filePath = candidates.find((candidate) => fs.existsSync(candidate))\n if (!filePath) {\n throw new Error(`Missing workflow seed file: ${fileName}`)\n }\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T\n}\n\nfunction requireString(value: unknown, label: string): string {\n if (typeof value === 'string' && value.trim().length > 0) return value\n throw new Error(`Invalid ${label} in workflow seed data.`)\n}\n\nasync function seedWorkflowDefinition(\n em: EntityManager,\n scope: WorkflowSeedScope,\n fileName: string,\n): Promise<boolean> {\n const seed = readExampleJson<WorkflowSeedDefinition>(fileName)\n const workflowId = requireString(seed.workflowId, 'workflowId')\n\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (existing) {\n // Check if the definition needs to be updated by comparing steps and transitions\n const seedStepCount = seed.definition.steps.length\n const existingStepCount = existing.definition.steps.length\n const seedTransitionCount = seed.definition.transitions.length\n const existingTransitionCount = existing.definition.transitions.length\n\n // Check for preConditions on transitions\n const seedHasTransitionPreConditions = seed.definition.transitions.some(\n (t: any) => t.preConditions && t.preConditions.length > 0\n )\n const existingHasTransitionPreConditions = existing.definition.transitions.some(\n (t: any) => t.preConditions && t.preConditions.length > 0\n )\n\n // Check for preConditions on START step\n const seedStartStep = seed.definition.steps.find((s: any) => s.stepType === 'START')\n const existingStartStep = existing.definition.steps.find((s: any) => s.stepType === 'START')\n const seedHasStartPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0\n const existingHasStartPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0\n\n // Update if structure has changed\n const needsUpdate =\n seedStepCount !== existingStepCount ||\n seedTransitionCount !== existingTransitionCount ||\n (seedHasStartPreConditions && !existingHasStartPreConditions) ||\n (seedHasTransitionPreConditions && !existingHasTransitionPreConditions)\n\n if (needsUpdate) {\n console.log(`[seed] Updating workflow ${workflowId} (steps: ${existingStepCount}\u2192${seedStepCount}, transitions: ${existingTransitionCount}\u2192${seedTransitionCount})`)\n existing.definition = seed.definition\n await em.flush()\n return true\n }\n\n return false\n }\n\n const workflow = em.create(WorkflowDefinition, {\n ...seed,\n workflowId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n em.persist(workflow)\n await em.flush()\n return true\n}\n\nasync function seedGuardRules(\n em: EntityManager,\n scope: WorkflowSeedScope,\n fileName: string,\n): Promise<{ seeded: number; skipped: number; updated: number }> {\n const seeds = readExampleJson<GuardRuleSeed[]>(fileName)\n if (!Array.isArray(seeds)) {\n throw new Error('Invalid guard rules seed data.')\n }\n\n let seeded = 0\n let skipped = 0\n let updated = 0\n for (const rule of seeds) {\n const ruleId = requireString(rule.ruleId, 'ruleId')\n const existing = await em.findOne(BusinessRule, {\n ruleId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n if (existing) {\n // Check if entityType or eventType needs updating\n const needsUpdate = existing.entityType !== rule.entityType || existing.eventType !== rule.eventType\n if (needsUpdate) {\n console.log(`[seed] Updating business rule ${ruleId}: entityType=${rule.entityType}, eventType=${rule.eventType}`)\n existing.entityType = rule.entityType\n existing.eventType = rule.eventType ?? null\n updated += 1\n } else {\n skipped += 1\n }\n continue\n }\n const entry = em.create(BusinessRule, {\n ruleId,\n ruleName: rule.ruleName,\n ruleType: rule.ruleType,\n entityType: rule.entityType,\n conditionExpression: rule.conditionExpression,\n eventType: rule.eventType,\n ruleCategory: rule.ruleCategory,\n description: rule.description,\n successActions: rule.successActions,\n failureActions: rule.failureActions,\n enabled: rule.enabled,\n priority: rule.priority,\n version: rule.version,\n effectiveFrom: rule.effectiveFrom,\n effectiveTo: rule.effectiveTo,\n createdBy: rule.createdBy,\n updatedBy: rule.updatedBy,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n em.persist(entry)\n seeded += 1\n }\n if (seeded > 0 || updated > 0) {\n await em.flush()\n }\n return { seeded, skipped, updated }\n}\n\nexport async function seedExampleWorkflows(em: EntityManager
|
|
5
|
-
"mappings": "AACA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,qBAAqB;AAC9B,SAAS,0BAAuD;AAChE,SAAS,oBAAmC;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { fileURLToPath } from 'node:url'\nimport { WorkflowDefinition, type WorkflowDefinitionData } from '../data/entities'\nimport { BusinessRule, type RuleType } from '@open-mercato/core/modules/business_rules/data/entities'\nimport {\n invalidateBusinessRuleDiscoveryCache,\n type RuleDiscoveryCache,\n} from '@open-mercato/core/modules/business_rules/lib/rule-engine'\n\nconst __esmDirname = path.dirname(fileURLToPath(import.meta.url))\n\nexport type WorkflowSeedScope = { tenantId: string; organizationId: string }\n\ntype WorkflowSeedDefinition = {\n workflowId: string\n workflowName: string\n description?: string | null\n version?: number\n definition: WorkflowDefinitionData\n metadata?: Record<string, unknown> | null\n enabled?: boolean\n effectiveFrom?: string | null\n effectiveTo?: string | null\n createdBy?: string | null\n updatedBy?: string | null\n}\n\ntype GuardRuleSeed = {\n ruleId: string\n ruleName: string\n ruleType: RuleType\n entityType: string\n conditionExpression: unknown\n eventType?: string | null\n ruleCategory?: string | null\n description?: string | null\n successActions?: unknown\n failureActions?: unknown\n enabled?: boolean\n priority?: number\n version?: number\n effectiveFrom?: string | null\n effectiveTo?: string | null\n createdBy?: string | null\n updatedBy?: string | null\n tagsJson?: string[]\n labelsJson?: Record<string, string>\n}\n\nfunction readExampleJson<T>(fileName: string): T {\n const candidates = [\n path.join(__esmDirname, '..', 'examples', fileName),\n path.join(process.cwd(), 'packages', 'core', 'src', 'modules', 'workflows', 'examples', fileName),\n path.join(process.cwd(), 'src', 'modules', 'workflows', 'examples', fileName),\n ]\n const filePath = candidates.find((candidate) => fs.existsSync(candidate))\n if (!filePath) {\n throw new Error(`Missing workflow seed file: ${fileName}`)\n }\n return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T\n}\n\nfunction requireString(value: unknown, label: string): string {\n if (typeof value === 'string' && value.trim().length > 0) return value\n throw new Error(`Invalid ${label} in workflow seed data.`)\n}\n\nasync function seedWorkflowDefinition(\n em: EntityManager,\n scope: WorkflowSeedScope,\n fileName: string,\n): Promise<boolean> {\n const seed = readExampleJson<WorkflowSeedDefinition>(fileName)\n const workflowId = requireString(seed.workflowId, 'workflowId')\n\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n if (existing) {\n // Check if the definition needs to be updated by comparing steps and transitions\n const seedStepCount = seed.definition.steps.length\n const existingStepCount = existing.definition.steps.length\n const seedTransitionCount = seed.definition.transitions.length\n const existingTransitionCount = existing.definition.transitions.length\n\n // Check for preConditions on transitions\n const seedHasTransitionPreConditions = seed.definition.transitions.some(\n (t: any) => t.preConditions && t.preConditions.length > 0\n )\n const existingHasTransitionPreConditions = existing.definition.transitions.some(\n (t: any) => t.preConditions && t.preConditions.length > 0\n )\n\n // Check for preConditions on START step\n const seedStartStep = seed.definition.steps.find((s: any) => s.stepType === 'START')\n const existingStartStep = existing.definition.steps.find((s: any) => s.stepType === 'START')\n const seedHasStartPreConditions = seedStartStep?.preConditions && seedStartStep.preConditions.length > 0\n const existingHasStartPreConditions = existingStartStep?.preConditions && existingStartStep.preConditions.length > 0\n\n // Update if structure has changed\n const needsUpdate =\n seedStepCount !== existingStepCount ||\n seedTransitionCount !== existingTransitionCount ||\n (seedHasStartPreConditions && !existingHasStartPreConditions) ||\n (seedHasTransitionPreConditions && !existingHasTransitionPreConditions)\n\n if (needsUpdate) {\n console.log(`[seed] Updating workflow ${workflowId} (steps: ${existingStepCount}\u2192${seedStepCount}, transitions: ${existingTransitionCount}\u2192${seedTransitionCount})`)\n existing.definition = seed.definition\n await em.flush()\n return true\n }\n\n return false\n }\n\n const workflow = em.create(WorkflowDefinition, {\n ...seed,\n workflowId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n em.persist(workflow)\n await em.flush()\n return true\n}\n\nasync function seedGuardRules(\n em: EntityManager,\n scope: WorkflowSeedScope,\n fileName: string,\n cache?: RuleDiscoveryCache | null,\n): Promise<{ seeded: number; skipped: number; updated: number }> {\n const seeds = readExampleJson<GuardRuleSeed[]>(fileName)\n if (!Array.isArray(seeds)) {\n throw new Error('Invalid guard rules seed data.')\n }\n\n let seeded = 0\n let skipped = 0\n let updated = 0\n for (const rule of seeds) {\n const ruleId = requireString(rule.ruleId, 'ruleId')\n const existing = await em.findOne(BusinessRule, {\n ruleId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n if (existing) {\n // Check if entityType or eventType needs updating\n const needsUpdate = existing.entityType !== rule.entityType || existing.eventType !== rule.eventType\n if (needsUpdate) {\n console.log(`[seed] Updating business rule ${ruleId}: entityType=${rule.entityType}, eventType=${rule.eventType}`)\n existing.entityType = rule.entityType\n existing.eventType = rule.eventType ?? null\n updated += 1\n } else {\n skipped += 1\n }\n continue\n }\n const entry = em.create(BusinessRule, {\n ruleId,\n ruleName: rule.ruleName,\n ruleType: rule.ruleType,\n entityType: rule.entityType,\n conditionExpression: rule.conditionExpression,\n eventType: rule.eventType,\n ruleCategory: rule.ruleCategory,\n description: rule.description,\n successActions: rule.successActions,\n failureActions: rule.failureActions,\n enabled: rule.enabled,\n priority: rule.priority,\n version: rule.version,\n effectiveFrom: rule.effectiveFrom,\n effectiveTo: rule.effectiveTo,\n createdBy: rule.createdBy,\n updatedBy: rule.updatedBy,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n em.persist(entry)\n seeded += 1\n }\n if (seeded > 0 || updated > 0) {\n await em.flush()\n await invalidateBusinessRuleDiscoveryCache(cache, scope.tenantId, scope.organizationId)\n }\n return { seeded, skipped, updated }\n}\n\nexport async function seedExampleWorkflows(\n em: EntityManager,\n scope: WorkflowSeedScope,\n options: { cache?: RuleDiscoveryCache | null } = {},\n): Promise<void> {\n // workflows.checkout-demo and workflows.simple-approval are now code-defined\n // (see packages/core/src/modules/workflows/workflows.ts). Seeding DB rows for\n // them would shadow the code definitions in the merge layer, so they are no\n // longer seeded here. Existing tenants are migrated via Migration20260428102318.\n await seedGuardRules(em, scope, 'guard-rules-example.json', options.cache)\n await seedWorkflowDefinition(em, scope, 'sales-pipeline-definition.json')\n await seedGuardRules(em, scope, 'order-approval-guard-rules.json', options.cache)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { resolveBusinessRuleDiscoveryCache } from "@open-mercato/core/modules/business_rules/lib/rule-engine";
|
|
1
2
|
import { seedExampleWorkflows } from "./lib/seeds.js";
|
|
2
3
|
const setup = {
|
|
3
4
|
seedDefaults: async (ctx) => {
|
|
4
5
|
const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId };
|
|
5
|
-
|
|
6
|
+
const cache = resolveBusinessRuleDiscoveryCache(ctx.container.resolve.bind(ctx.container));
|
|
7
|
+
await seedExampleWorkflows(ctx.em, scope, { cache });
|
|
6
8
|
},
|
|
7
9
|
defaultRoleFeatures: {
|
|
8
10
|
admin: ["workflows.*"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/workflows/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { seedExampleWorkflows } from './lib/seeds'\n\nexport const setup: ModuleSetupConfig = {\n seedDefaults: async (ctx) => {\n const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n await seedExampleWorkflows(ctx.em, scope)\n },\n\n defaultRoleFeatures: {\n admin: ['workflows.*'],\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,4BAA4B;AAE9B,MAAM,QAA2B;AAAA,EACtC,cAAc,OAAO,QAAQ;AAC3B,UAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAC3E,UAAM,qBAAqB,IAAI,IAAI,
|
|
4
|
+
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { resolveBusinessRuleDiscoveryCache } from '@open-mercato/core/modules/business_rules/lib/rule-engine'\nimport { seedExampleWorkflows } from './lib/seeds'\n\nexport const setup: ModuleSetupConfig = {\n seedDefaults: async (ctx) => {\n const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n const cache = resolveBusinessRuleDiscoveryCache(ctx.container.resolve.bind(ctx.container))\n await seedExampleWorkflows(ctx.em, scope, { cache })\n },\n\n defaultRoleFeatures: {\n admin: ['workflows.*'],\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yCAAyC;AAClD,SAAS,4BAA4B;AAE9B,MAAM,QAA2B;AAAA,EACtC,cAAc,OAAO,QAAQ;AAC3B,UAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAC3E,UAAM,QAAQ,kCAAkC,IAAI,UAAU,QAAQ,KAAK,IAAI,SAAS,CAAC;AACzF,UAAM,qBAAqB,IAAI,IAAI,OAAO,EAAE,MAAM,CAAC;AAAA,EACrD;AAAA,EAEA,qBAAqB;AAAA,IACnB,OAAO,CAAC,aAAa;AAAA,EACvB;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4254.1.7a123d970c",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4254.1.7a123d970c",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4254.1.7a123d970c",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4254.1.7a123d970c",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4254.1.7a123d970c",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4254.1.7a123d970c",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4254.1.7a123d970c",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -2,6 +2,32 @@ import { expect, type APIRequestContext } from '@playwright/test';
|
|
|
2
2
|
import { apiRequest } from './api';
|
|
3
3
|
import { expectId, readJsonSafe } from './generalFixtures';
|
|
4
4
|
|
|
5
|
+
const BASE_URL = process.env.BASE_URL?.trim() || null;
|
|
6
|
+
|
|
7
|
+
function resolveUrl(path: string): string {
|
|
8
|
+
return BASE_URL ? `${BASE_URL}${path}` : path;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Variant of {@link apiRequest} that sets the `om_selected_org` cookie so the
|
|
13
|
+
* server resolves `ctx.selectedOrganizationId` to a specific organization.
|
|
14
|
+
* Create routes scope new records to that organization, which lets a test place
|
|
15
|
+
* a fixture in an organization other than the caller's home org.
|
|
16
|
+
*/
|
|
17
|
+
export async function apiRequestWithSelectedOrg(
|
|
18
|
+
request: APIRequestContext,
|
|
19
|
+
method: string,
|
|
20
|
+
path: string,
|
|
21
|
+
options: { token: string; selectedOrgId: string; data?: unknown },
|
|
22
|
+
) {
|
|
23
|
+
const headers = {
|
|
24
|
+
Authorization: `Bearer ${options.token}`,
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
Cookie: `om_selected_org=${options.selectedOrgId}`,
|
|
27
|
+
};
|
|
28
|
+
return request.fetch(resolveUrl(path), { method, headers, data: options.data });
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
export async function createRoleFixture(
|
|
6
32
|
request: APIRequestContext,
|
|
7
33
|
token: string,
|
|
@@ -53,3 +79,75 @@ export async function deleteUserIfExists(
|
|
|
53
79
|
if (!token || !userId) return;
|
|
54
80
|
await apiRequest(request, 'DELETE', `/api/auth/users?id=${encodeURIComponent(userId)}`, { token }).catch(() => undefined);
|
|
55
81
|
}
|
|
82
|
+
|
|
83
|
+
export async function createOrganizationFixture(
|
|
84
|
+
request: APIRequestContext,
|
|
85
|
+
token: string,
|
|
86
|
+
input: { name: string; tenantId?: string },
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const payload: { name: string; tenantId?: string } = { name: input.name };
|
|
89
|
+
if (typeof input.tenantId === 'string' && input.tenantId.length > 0) {
|
|
90
|
+
payload.tenantId = input.tenantId;
|
|
91
|
+
}
|
|
92
|
+
const response = await apiRequest(request, 'POST', '/api/directory/organizations', {
|
|
93
|
+
token,
|
|
94
|
+
data: payload,
|
|
95
|
+
});
|
|
96
|
+
const body = await readJsonSafe<{ id?: string }>(response);
|
|
97
|
+
expect(response.status(), 'POST /api/directory/organizations should return 201').toBe(201);
|
|
98
|
+
return expectId(body?.id, 'Organization creation response should include id');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function deleteOrganizationIfExists(
|
|
102
|
+
request: APIRequestContext,
|
|
103
|
+
token: string | null,
|
|
104
|
+
organizationId: string | null,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
if (!token || !organizationId) return;
|
|
107
|
+
await apiRequest(request, 'DELETE', '/api/directory/organizations', {
|
|
108
|
+
token,
|
|
109
|
+
data: { id: organizationId },
|
|
110
|
+
}).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function setRoleAclFeatures(
|
|
114
|
+
request: APIRequestContext,
|
|
115
|
+
token: string,
|
|
116
|
+
input: { roleId: string; features: string[]; organizations?: string[] | null },
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const payload: { roleId: string; features: string[]; organizations?: string[] | null } = {
|
|
119
|
+
roleId: input.roleId,
|
|
120
|
+
features: input.features,
|
|
121
|
+
};
|
|
122
|
+
if (input.organizations !== undefined) {
|
|
123
|
+
payload.organizations = input.organizations;
|
|
124
|
+
}
|
|
125
|
+
const response = await apiRequest(request, 'PUT', '/api/auth/roles/acl', {
|
|
126
|
+
token,
|
|
127
|
+
data: payload,
|
|
128
|
+
});
|
|
129
|
+
const body = await readJsonSafe<{ ok?: boolean }>(response);
|
|
130
|
+
expect(response.status(), 'PUT /api/auth/roles/acl should return 200').toBe(200);
|
|
131
|
+
expect(body?.ok, 'Role ACL update should report ok=true').toBe(true);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function setUserAclVisibility(
|
|
135
|
+
request: APIRequestContext,
|
|
136
|
+
token: string,
|
|
137
|
+
input: { userId: string; organizations: string[] | null; features?: string[] },
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const payload: { userId: string; organizations: string[] | null; features?: string[] } = {
|
|
140
|
+
userId: input.userId,
|
|
141
|
+
organizations: input.organizations,
|
|
142
|
+
};
|
|
143
|
+
if (input.features !== undefined) {
|
|
144
|
+
payload.features = input.features;
|
|
145
|
+
}
|
|
146
|
+
const response = await apiRequest(request, 'PUT', '/api/auth/users/acl', {
|
|
147
|
+
token,
|
|
148
|
+
data: payload,
|
|
149
|
+
});
|
|
150
|
+
const body = await readJsonSafe<{ ok?: boolean }>(response);
|
|
151
|
+
expect(response.status(), 'PUT /api/auth/users/acl should return 200').toBe(200);
|
|
152
|
+
expect(body?.ok, 'User ACL update should report ok=true').toBe(true);
|
|
153
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Client } from 'pg';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database fixtures for the org-scope fail-open hardening tests.
|
|
7
|
+
*
|
|
8
|
+
* These helpers talk to Postgres directly via `pg` rather than bootstrapping the
|
|
9
|
+
* app DI container, because:
|
|
10
|
+
* - the directory create command denies non-super-admin actors (the only
|
|
11
|
+
* loginable accounts here), so orgs cannot be created over the API; and
|
|
12
|
+
* - granting `customers.*` through the ACL API requires a super-admin actor.
|
|
13
|
+
* Raw SQL keeps the test self-contained and avoids depending on built package
|
|
14
|
+
* `dist/` output for an in-process MikroORM bootstrap.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function resolveAppRoot(): string {
|
|
18
|
+
const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();
|
|
19
|
+
return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readEnvValue(key: string): string | undefined {
|
|
23
|
+
if (process.env[key]) return process.env[key];
|
|
24
|
+
const candidatePaths = [
|
|
25
|
+
path.resolve(resolveAppRoot(), '.env'),
|
|
26
|
+
path.resolve(process.cwd(), 'apps/mercato/.env'),
|
|
27
|
+
path.resolve(process.cwd(), '.env'),
|
|
28
|
+
];
|
|
29
|
+
for (const envPath of candidatePaths) {
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
32
|
+
const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));
|
|
33
|
+
if (match?.[1]) return match[1].trim();
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveDatabaseUrl(): string {
|
|
42
|
+
const url = readEnvValue('DATABASE_URL');
|
|
43
|
+
if (!url) throw new Error('[internal] DATABASE_URL is not configured for integration DB fixtures');
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {
|
|
48
|
+
const client = new Client({ connectionString: resolveDatabaseUrl() });
|
|
49
|
+
await client.connect();
|
|
50
|
+
try {
|
|
51
|
+
return await run(client);
|
|
52
|
+
} finally {
|
|
53
|
+
await client.end();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Nulls a user's home organization (`organization_id`) directly in the database.
|
|
59
|
+
*
|
|
60
|
+
* Required to construct the "floating restricted user" precondition: the JWT
|
|
61
|
+
* `auth.orgId` is minted from `users.organization_id` at login, so this MUST run
|
|
62
|
+
* BEFORE the user logs in.
|
|
63
|
+
*/
|
|
64
|
+
export async function clearUserHomeOrganization(userId: string): Promise<void> {
|
|
65
|
+
await withClient(async (client) => {
|
|
66
|
+
await client.query('update users set organization_id = null where id = $1', [userId]);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Upserts a per-user ACL row, setting the effective feature list and the
|
|
72
|
+
* organization-visibility list.
|
|
73
|
+
*
|
|
74
|
+
* `organizations` maps to `OrganizationScope.allowedIds` (write path) and
|
|
75
|
+
* `filterIds` (read path):
|
|
76
|
+
* - `[orgA]` => restricted to orgA (write fail-open precondition, #2239)
|
|
77
|
+
* - `[]` => restricted to zero orgs (read fail-open precondition, #2245)
|
|
78
|
+
* - `null` => unrestricted
|
|
79
|
+
*/
|
|
80
|
+
export async function setUserAclInDb(input: {
|
|
81
|
+
userId: string;
|
|
82
|
+
tenantId: string;
|
|
83
|
+
features: string[];
|
|
84
|
+
organizations: string[] | null;
|
|
85
|
+
}): Promise<void> {
|
|
86
|
+
await withClient(async (client) => {
|
|
87
|
+
const existing = await client.query<{ id: string }>(
|
|
88
|
+
'select id from user_acls where user_id = $1 and tenant_id = $2 limit 1',
|
|
89
|
+
[input.userId, input.tenantId],
|
|
90
|
+
);
|
|
91
|
+
const featuresJson = JSON.stringify(input.features);
|
|
92
|
+
const organizationsJson = input.organizations === null ? null : JSON.stringify(input.organizations);
|
|
93
|
+
if (existing.rows.length > 0) {
|
|
94
|
+
await client.query(
|
|
95
|
+
'update user_acls set features_json = $2::jsonb, organizations_json = $3::jsonb, is_super_admin = false, updated_at = now() where id = $1',
|
|
96
|
+
[existing.rows[0].id, featuresJson, organizationsJson],
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await client.query(
|
|
101
|
+
`insert into user_acls (id, user_id, tenant_id, features_json, organizations_json, is_super_admin, created_at)
|
|
102
|
+
values (gen_random_uuid(), $1, $2, $3::jsonb, $4::jsonb, false, now())`,
|
|
103
|
+
[input.userId, input.tenantId, featuresJson, organizationsJson],
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Removes any per-user ACL rows for the user (best-effort test cleanup). */
|
|
109
|
+
export async function deleteUserAclInDb(userId: string): Promise<void> {
|
|
110
|
+
if (!userId) return;
|
|
111
|
+
await withClient(async (client) => {
|
|
112
|
+
await client.query('delete from user_acls where user_id = $1', [userId]);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates an organization directly in the database within the given tenant.
|
|
118
|
+
*
|
|
119
|
+
* The directory create command routes through `enforceTenantSelection`, which
|
|
120
|
+
* denies the (non-super-admin) accounts loginable on this instance. Landing the
|
|
121
|
+
* org in a known tenant lets the floating user (created under it) share the
|
|
122
|
+
* tenant — required because cross-tenant access returns 404 before the
|
|
123
|
+
* org-scope guard ever runs.
|
|
124
|
+
*/
|
|
125
|
+
export async function createOrganizationInDb(input: { name: string; tenantId: string }): Promise<string> {
|
|
126
|
+
return withClient(async (client) => {
|
|
127
|
+
const result = await client.query<{ id: string }>(
|
|
128
|
+
`insert into organizations
|
|
129
|
+
(id, tenant_id, name, is_active, ancestor_ids, child_ids, descendant_ids, depth, created_at, updated_at)
|
|
130
|
+
values (gen_random_uuid(), $1, $2, true, '[]'::jsonb, '[]'::jsonb, '[]'::jsonb, 0, now(), now())
|
|
131
|
+
returning id`,
|
|
132
|
+
[input.tenantId, input.name],
|
|
133
|
+
);
|
|
134
|
+
return result.rows[0].id;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Hard-deletes an organization row (best-effort test cleanup). */
|
|
139
|
+
export async function deleteOrganizationInDb(organizationId: string | null): Promise<void> {
|
|
140
|
+
if (!organizationId) return;
|
|
141
|
+
await withClient(async (client) => {
|
|
142
|
+
await client.query('delete from organizations where id = $1', [organizationId]);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -47,6 +47,7 @@ export async function POST(req: Request) {
|
|
|
47
47
|
|
|
48
48
|
const container = await createRequestContainer()
|
|
49
49
|
const em = container.resolve('em') as EntityManager
|
|
50
|
+
const cache = ruleEngine.resolveBusinessRuleDiscoveryCache(container.resolve.bind(container))
|
|
50
51
|
let eventBus: EventBus | null = null
|
|
51
52
|
try {
|
|
52
53
|
eventBus = container.resolve('eventBus') as EventBus
|
|
@@ -92,7 +93,7 @@ export async function POST(req: Request) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
try {
|
|
95
|
-
const result = await ruleEngine.executeRules(em, context, { eventBus })
|
|
96
|
+
const result = await ruleEngine.executeRules(em, context, { eventBus, cache })
|
|
96
97
|
|
|
97
98
|
const response = {
|
|
98
99
|
allowed: result.allowed,
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
businessRuleFilterSchema,
|
|
16
16
|
ruleTypeSchema,
|
|
17
17
|
} from '../../data/validators'
|
|
18
|
+
import {
|
|
19
|
+
invalidateBusinessRuleDiscoveryCache,
|
|
20
|
+
resolveBusinessRuleDiscoveryCache,
|
|
21
|
+
} from '../../lib/rule-engine'
|
|
18
22
|
|
|
19
23
|
const querySchema = z.looseObject({
|
|
20
24
|
id: z.uuid().optional(),
|
|
@@ -180,6 +184,7 @@ export async function POST(req: Request) {
|
|
|
180
184
|
|
|
181
185
|
const container = await createRequestContainer()
|
|
182
186
|
const em = container.resolve('em') as EntityManager
|
|
187
|
+
const cache = resolveBusinessRuleDiscoveryCache(container.resolve.bind(container))
|
|
183
188
|
|
|
184
189
|
let body: any
|
|
185
190
|
try {
|
|
@@ -212,6 +217,7 @@ export async function POST(req: Request) {
|
|
|
212
217
|
|
|
213
218
|
try {
|
|
214
219
|
await em.persist(rule).flush()
|
|
220
|
+
await invalidateBusinessRuleDiscoveryCache(cache, rule.tenantId, rule.organizationId)
|
|
215
221
|
} catch (error) {
|
|
216
222
|
console.error('[business_rules.rules] Failed to persist new rule:', error)
|
|
217
223
|
return NextResponse.json(
|
|
@@ -231,6 +237,7 @@ export async function PUT(req: Request) {
|
|
|
231
237
|
|
|
232
238
|
const container = await createRequestContainer()
|
|
233
239
|
const em = container.resolve('em') as EntityManager
|
|
240
|
+
const cache = resolveBusinessRuleDiscoveryCache(container.resolve.bind(container))
|
|
234
241
|
|
|
235
242
|
let body: any
|
|
236
243
|
try {
|
|
@@ -274,6 +281,7 @@ export async function PUT(req: Request) {
|
|
|
274
281
|
|
|
275
282
|
try {
|
|
276
283
|
await em.persist(rule).flush()
|
|
284
|
+
await invalidateBusinessRuleDiscoveryCache(cache, rule.tenantId, rule.organizationId)
|
|
277
285
|
} catch (error) {
|
|
278
286
|
console.error('[business_rules.rules] Failed to persist rule update:', error)
|
|
279
287
|
return NextResponse.json(
|
|
@@ -300,6 +308,7 @@ export async function DELETE(req: Request) {
|
|
|
300
308
|
|
|
301
309
|
const container = await createRequestContainer()
|
|
302
310
|
const em = container.resolve('em') as EntityManager
|
|
311
|
+
const cache = resolveBusinessRuleDiscoveryCache(container.resolve.bind(container))
|
|
303
312
|
|
|
304
313
|
const rule = await em.findOne(BusinessRule, {
|
|
305
314
|
id,
|
|
@@ -314,6 +323,7 @@ export async function DELETE(req: Request) {
|
|
|
314
323
|
|
|
315
324
|
rule.deletedAt = new Date()
|
|
316
325
|
await em.persist(rule).flush()
|
|
326
|
+
await invalidateBusinessRuleDiscoveryCache(cache, rule.tenantId, rule.organizationId)
|
|
317
327
|
|
|
318
328
|
return NextResponse.json({ ok: true })
|
|
319
329
|
}
|