@open-mercato/core 0.6.7-develop.6653.1.b5bc7194a0 → 0.6.7-develop.6660.1.90e1e2eef6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/business_rules/lib/value-resolver.js +7 -0
  3. package/dist/modules/business_rules/lib/value-resolver.js.map +2 -2
  4. package/dist/modules/currencies/commands/currencies.js +22 -8
  5. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  6. package/dist/modules/currencies/commands/exchange-rates.js +22 -8
  7. package/dist/modules/currencies/commands/exchange-rates.js.map +2 -2
  8. package/dist/modules/currencies/commands/scope.js +22 -0
  9. package/dist/modules/currencies/commands/scope.js.map +7 -0
  10. package/dist/modules/customers/components/detail/ActivitiesSection.js +5 -4
  11. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ActivityHistorySection.js +5 -4
  13. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  14. package/dist/modules/feature_toggles/api/global/route.js +6 -0
  15. package/dist/modules/feature_toggles/api/global/route.js.map +2 -2
  16. package/dist/modules/feature_toggles/commands/global.js +9 -9
  17. package/dist/modules/feature_toggles/commands/global.js.map +2 -2
  18. package/dist/modules/query_index/lib/subscriber-scope.js +63 -10
  19. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  20. package/dist/modules/query_index/subscribers/delete_one.js +20 -6
  21. package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
  22. package/dist/modules/query_index/subscribers/upsert_one.js +8 -3
  23. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  24. package/dist/modules/workflows/api/definitions/[id]/customize/route.js +2 -0
  25. package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
  26. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +4 -0
  27. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
  28. package/package.json +7 -7
  29. package/src/modules/business_rules/lib/value-resolver.ts +16 -0
  30. package/src/modules/currencies/commands/currencies.ts +26 -8
  31. package/src/modules/currencies/commands/exchange-rates.ts +26 -8
  32. package/src/modules/currencies/commands/scope.ts +32 -0
  33. package/src/modules/customers/components/detail/ActivitiesSection.tsx +10 -6
  34. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +6 -3
  35. package/src/modules/feature_toggles/api/global/route.ts +7 -1
  36. package/src/modules/feature_toggles/commands/global.ts +9 -12
  37. package/src/modules/query_index/lib/subscriber-scope.ts +98 -14
  38. package/src/modules/query_index/subscribers/delete_one.ts +21 -10
  39. package/src/modules/query_index/subscribers/upsert_one.ts +8 -3
  40. package/src/modules/workflows/AGENTS.md +22 -0
  41. package/src/modules/workflows/api/definitions/[id]/customize/route.ts +9 -0
  42. package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +7 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/workflows/api/definitions/%5Bid%5D/reset-to-code/route.ts"],
4
- "sourcesContent": ["/**\n * Reset Customized Workflow Definition to Code\n *\n * POST /api/workflows/definitions/[id]/reset-to-code\n *\n * Deletes the DB override row for a code-based workflow definition,\n * reverting it back to the original code registry version.\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { WorkflowDefinition, WorkflowInstance } from '../../../../data/entities'\nimport { serializeCodeWorkflowDefinition } from '../../serialize'\nimport { getCodeWorkflow } from '../../../../lib/code-registry'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.definitions.edit'],\n}\n\ninterface RouteContext {\n params: Promise<{\n id: string\n }>\n}\n\n/**\n * POST /api/workflows/definitions/[id]/reset-to-code\n *\n * Reset a customized code workflow definition back to its original code version.\n */\nexport async function POST(\n request: NextRequest,\n context: RouteContext\n) {\n try {\n const params = await context.params\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n // Check edit permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.definitions.edit'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n // Find existing definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: params.id,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!definition) {\n return NextResponse.json(\n { error: 'Workflow definition not found' },\n { status: 404 }\n )\n }\n\n // Verify this is a code-based override\n if (!definition.codeWorkflowId) {\n return NextResponse.json(\n { error: 'This workflow definition is not a code-based override and cannot be reset' },\n { status: 400 }\n )\n }\n\n // Check if there are active workflow instances using this definition\n const activeInstances = await em.count(WorkflowInstance, {\n definitionId: definition.id,\n status: { $in: ['RUNNING', 'WAITING'] },\n })\n\n if (activeInstances > 0) {\n return NextResponse.json(\n {\n error: `Cannot reset workflow definition with ${activeInstances} active instance(s)`,\n },\n { status: 409 }\n )\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId: organizationId ?? null,\n userId: auth.sub ?? '',\n resourceKind: 'workflows.definition',\n resourceId: String(definition.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: request.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n // Look up the original code definition before deleting\n const codeDef = getCodeWorkflow(definition.codeWorkflowId)\n\n // Snapshot identifying fields before remove() detaches the entity\n const removedSnapshot = {\n id: String(definition.id),\n workflowId: definition.workflowId,\n codeWorkflowId: definition.codeWorkflowId,\n tenantId: definition.tenantId,\n organizationId: definition.organizationId,\n }\n\n // Hard-delete the DB override row\n em.remove(definition)\n await em.flush()\n\n if (guardResult?.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId: organizationId ?? null,\n userId: auth.sub ?? '',\n resourceKind: 'workflows.definition',\n resourceId: removedSnapshot.id,\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: request.headers,\n metadata: guardResult.metadata,\n })\n }\n\n try {\n const eventBus = container.resolve('eventBus') as\n | { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> }\n | undefined\n if (eventBus && typeof eventBus.emitEvent === 'function') {\n await eventBus.emitEvent(\n 'workflows.definition.reset_to_code',\n {\n id: removedSnapshot.id,\n workflowId: removedSnapshot.workflowId,\n codeWorkflowId: removedSnapshot.codeWorkflowId,\n tenantId: removedSnapshot.tenantId,\n organizationId: removedSnapshot.organizationId,\n userId: auth.sub ?? null,\n },\n {\n tenantId: removedSnapshot.tenantId,\n organizationId: removedSnapshot.organizationId,\n persistent: true,\n },\n )\n }\n } catch (eventError) {\n logger.error('Failed to emit workflows.definition.reset_to_code event', { err: eventError })\n }\n\n if (!codeDef) {\n return NextResponse.json({\n message: 'Workflow definition reset to code version (code definition no longer registered)',\n data: null,\n })\n }\n\n const syntheticId = `code:${codeDef.workflowId}`\n\n return NextResponse.json({\n data: serializeCodeWorkflowDefinition(codeDef, syntheticId),\n message: 'Workflow definition reset to code version',\n })\n } catch (error) {\n logger.error('Error resetting workflow definition to code', { err: error })\n return NextResponse.json(\n { error: 'Failed to reset workflow definition to code' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n POST: {\n summary: 'Reset workflow definition to code version',\n description: 'Deletes the DB override for a code-based workflow definition, reverting it to the original code registry version. Cannot be reset if there are active instances.',\n tags: ['Workflows'],\n pathParams: z.object({\n id: z.string().uuid(),\n }),\n responses: [\n {\n status: 200,\n description: 'Workflow definition reset to code version',\n example: {\n data: {\n id: 'code:checkout-flow',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Code-defined checkout workflow',\n version: 1,\n source: 'code',\n isCodeBased: true,\n },\n message: 'Workflow definition reset to code version',\n },\n },\n {\n status: 400,\n description: 'Definition is not a code-based override',\n example: {\n error: 'This workflow definition is not a code-based override and cannot be reset',\n },\n },\n {\n status: 404,\n description: 'Workflow definition not found',\n example: {\n error: 'Workflow definition not found',\n },\n },\n {\n status: 409,\n description: 'Cannot reset - active workflow instances exist',\n example: {\n error: 'Cannot reset workflow definition with 3 active instance(s)',\n },\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Reset Customized Workflow Definition to Code\n *\n * POST /api/workflows/definitions/[id]/reset-to-code\n *\n * Deletes the DB override row for a code-based workflow definition,\n * reverting it back to the original code registry version.\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { validateCrudMutationGuard, runCrudMutationGuardAfterSuccess } from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { WorkflowDefinition, WorkflowInstance } from '../../../../data/entities'\nimport { serializeCodeWorkflowDefinition } from '../../serialize'\nimport { getCodeWorkflow } from '../../../../lib/code-registry'\nimport { invalidateTriggerCache } from '../../../../lib/event-trigger-service'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.definitions.edit'],\n}\n\ninterface RouteContext {\n params: Promise<{\n id: string\n }>\n}\n\n/**\n * POST /api/workflows/definitions/[id]/reset-to-code\n *\n * Reset a customized code workflow definition back to its original code version.\n */\nexport async function POST(\n request: NextRequest,\n context: RouteContext\n) {\n try {\n const params = await context.params\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n // Check edit permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.definitions.edit'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n // Find existing definition\n const definition = await em.findOne(WorkflowDefinition, {\n id: params.id,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!definition) {\n return NextResponse.json(\n { error: 'Workflow definition not found' },\n { status: 404 }\n )\n }\n\n // Verify this is a code-based override\n if (!definition.codeWorkflowId) {\n return NextResponse.json(\n { error: 'This workflow definition is not a code-based override and cannot be reset' },\n { status: 400 }\n )\n }\n\n // Check if there are active workflow instances using this definition\n const activeInstances = await em.count(WorkflowInstance, {\n definitionId: definition.id,\n status: { $in: ['RUNNING', 'WAITING'] },\n })\n\n if (activeInstances > 0) {\n return NextResponse.json(\n {\n error: `Cannot reset workflow definition with ${activeInstances} active instance(s)`,\n },\n { status: 409 }\n )\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: tenantId ?? '',\n organizationId: organizationId ?? null,\n userId: auth.sub ?? '',\n resourceKind: 'workflows.definition',\n resourceId: String(definition.id),\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: request.headers,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n // Look up the original code definition before deleting\n const codeDef = getCodeWorkflow(definition.codeWorkflowId)\n\n // Snapshot identifying fields before remove() detaches the entity\n const removedSnapshot = {\n id: String(definition.id),\n workflowId: definition.workflowId,\n codeWorkflowId: definition.codeWorkflowId,\n tenantId: definition.tenantId,\n organizationId: definition.organizationId,\n }\n\n // Hard-delete the DB override row\n em.remove(definition)\n await em.flush()\n\n // Trigger ownership falls back to the code registry, so the cached snapshot\n // still holds the embedded triggers of a row that no longer exists (#4425).\n if (removedSnapshot.tenantId) {\n invalidateTriggerCache(removedSnapshot.tenantId, removedSnapshot.organizationId ?? undefined)\n }\n\n if (guardResult?.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: tenantId ?? '',\n organizationId: organizationId ?? null,\n userId: auth.sub ?? '',\n resourceKind: 'workflows.definition',\n resourceId: removedSnapshot.id,\n operation: 'custom',\n requestMethod: 'POST',\n requestHeaders: request.headers,\n metadata: guardResult.metadata,\n })\n }\n\n try {\n const eventBus = container.resolve('eventBus') as\n | { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> }\n | undefined\n if (eventBus && typeof eventBus.emitEvent === 'function') {\n await eventBus.emitEvent(\n 'workflows.definition.reset_to_code',\n {\n id: removedSnapshot.id,\n workflowId: removedSnapshot.workflowId,\n codeWorkflowId: removedSnapshot.codeWorkflowId,\n tenantId: removedSnapshot.tenantId,\n organizationId: removedSnapshot.organizationId,\n userId: auth.sub ?? null,\n },\n {\n tenantId: removedSnapshot.tenantId,\n organizationId: removedSnapshot.organizationId,\n persistent: true,\n },\n )\n }\n } catch (eventError) {\n logger.error('Failed to emit workflows.definition.reset_to_code event', { err: eventError })\n }\n\n if (!codeDef) {\n return NextResponse.json({\n message: 'Workflow definition reset to code version (code definition no longer registered)',\n data: null,\n })\n }\n\n const syntheticId = `code:${codeDef.workflowId}`\n\n return NextResponse.json({\n data: serializeCodeWorkflowDefinition(codeDef, syntheticId),\n message: 'Workflow definition reset to code version',\n })\n } catch (error) {\n logger.error('Error resetting workflow definition to code', { err: error })\n return NextResponse.json(\n { error: 'Failed to reset workflow definition to code' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n POST: {\n summary: 'Reset workflow definition to code version',\n description: 'Deletes the DB override for a code-based workflow definition, reverting it to the original code registry version. Cannot be reset if there are active instances.',\n tags: ['Workflows'],\n pathParams: z.object({\n id: z.string().uuid(),\n }),\n responses: [\n {\n status: 200,\n description: 'Workflow definition reset to code version',\n example: {\n data: {\n id: 'code:checkout-flow',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Code-defined checkout workflow',\n version: 1,\n source: 'code',\n isCodeBased: true,\n },\n message: 'Workflow definition reset to code version',\n },\n },\n {\n status: 400,\n description: 'Definition is not a code-based override',\n example: {\n error: 'This workflow definition is not a code-based override and cannot be reset',\n },\n },\n {\n status: 404,\n description: 'Workflow definition not found',\n example: {\n error: 'Workflow definition not found',\n },\n },\n {\n status: 409,\n description: 'Cannot reset - active workflow instances exist',\n example: {\n error: 'Cannot reset workflow definition with 3 active instance(s)',\n },\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AASA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,2BAA2B,wCAAwC;AAC5E,SAAS,oBAAoB,wBAAwB;AACrD,SAAS,uCAAuC;AAChD,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AAEhC,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,4BAA4B;AAChD;AAaA,eAAsB,KACpB,SACA,SACA;AACA,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ;AAC7B,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,OAAO,MAAM,mBAAmB,OAAO;AAE7C,QAAI,CAAC,MAAM;AACT,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AAEA,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAC;AACnF,UAAM,WAAW,KAAK;AACtB,UAAM,iBAAiB,OAAO,cAAc,KAAK;AAGjD,UAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,UAAM,gBAAgB,MAAM,YAAY;AAAA,MACtC,KAAK;AAAA,MACL,CAAC,4BAA4B;AAAA,MAC7B;AAAA,QACE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,eAAe;AAClB,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,2BAA2B;AAAA,QACpC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,UAAM,aAAa,MAAM,GAAG,QAAQ,oBAAoB;AAAA,MACtD,IAAI,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAED,QAAI,CAAC,YAAY;AACf,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,gCAAgC;AAAA,QACzC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,QAAI,CAAC,WAAW,gBAAgB;AAC9B,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,4EAA4E;AAAA,QACrF,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,UAAM,kBAAkB,MAAM,GAAG,MAAM,kBAAkB;AAAA,MACvD,cAAc,WAAW;AAAA,MACzB,QAAQ,EAAE,KAAK,CAAC,WAAW,SAAS,EAAE;AAAA,IACxC,CAAC;AAED,QAAI,kBAAkB,GAAG;AACvB,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO,yCAAyC,eAAe;AAAA,QACjE;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,cAAc,MAAM,0BAA0B,WAAW;AAAA,MAC7D,UAAU,YAAY;AAAA,MACtB,gBAAgB,kBAAkB;AAAA,MAClC,QAAQ,KAAK,OAAO;AAAA,MACpB,cAAc;AAAA,MACd,YAAY,OAAO,WAAW,EAAE;AAAA,MAChC,WAAW;AAAA,MACX,eAAe;AAAA,MACf,gBAAgB,QAAQ;AAAA,IAC1B,CAAC;AACD,QAAI,eAAe,CAAC,YAAY,IAAI;AAClC,aAAO,aAAa,KAAK,YAAY,MAAM,EAAE,QAAQ,YAAY,OAAO,CAAC;AAAA,IAC3E;AAGA,UAAM,UAAU,gBAAgB,WAAW,cAAc;AAGzD,UAAM,kBAAkB;AAAA,MACtB,IAAI,OAAO,WAAW,EAAE;AAAA,MACxB,YAAY,WAAW;AAAA,MACvB,gBAAgB,WAAW;AAAA,MAC3B,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW;AAAA,IAC7B;AAGA,OAAG,OAAO,UAAU;AACpB,UAAM,GAAG,MAAM;AAIf,QAAI,gBAAgB,UAAU;AAC5B,6BAAuB,gBAAgB,UAAU,gBAAgB,kBAAkB,MAAS;AAAA,IAC9F;AAEA,QAAI,aAAa,uBAAuB;AACtC,YAAM,iCAAiC,WAAW;AAAA,QAChD,UAAU,YAAY;AAAA,QACtB,gBAAgB,kBAAkB;AAAA,QAClC,QAAQ,KAAK,OAAO;AAAA,QACpB,cAAc;AAAA,QACd,YAAY,gBAAgB;AAAA,QAC5B,WAAW;AAAA,QACX,eAAe;AAAA,QACf,gBAAgB,QAAQ;AAAA,QACxB,UAAU,YAAY;AAAA,MACxB,CAAC;AAAA,IACH;AAEA,QAAI;AACF,YAAM,WAAW,UAAU,QAAQ,UAAU;AAG7C,UAAI,YAAY,OAAO,SAAS,cAAc,YAAY;AACxD,cAAM,SAAS;AAAA,UACb;AAAA,UACA;AAAA,YACE,IAAI,gBAAgB;AAAA,YACpB,YAAY,gBAAgB;AAAA,YAC5B,gBAAgB,gBAAgB;AAAA,YAChC,UAAU,gBAAgB;AAAA,YAC1B,gBAAgB,gBAAgB;AAAA,YAChC,QAAQ,KAAK,OAAO;AAAA,UACtB;AAAA,UACA;AAAA,YACE,UAAU,gBAAgB;AAAA,YAC1B,gBAAgB,gBAAgB;AAAA,YAChC,YAAY;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,YAAY;AACnB,aAAO,MAAM,2DAA2D,EAAE,KAAK,WAAW,CAAC;AAAA,IAC7F;AAEA,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK;AAAA,QACvB,SAAS;AAAA,QACT,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,UAAM,cAAc,QAAQ,QAAQ,UAAU;AAE9C,WAAO,aAAa,KAAK;AAAA,MACvB,MAAM,gCAAgC,SAAS,WAAW;AAAA,MAC1D,SAAS;AAAA,IACX,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,+CAA+C,EAAE,KAAK,MAAM,CAAC;AAC1E,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,8CAA8C;AAAA,MACvD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,WAAW;AAAA,MAClB,YAAY,EAAE,OAAO;AAAA,QACnB,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,MACtB,CAAC;AAAA,MACD,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,MAAM;AAAA,cACJ,IAAI;AAAA,cACJ,YAAY;AAAA,cACZ,cAAc;AAAA,cACd,aAAa;AAAA,cACb,SAAS;AAAA,cACT,QAAQ;AAAA,cACR,aAAa;AAAA,YACf;AAAA,YACA,SAAS;AAAA,UACX;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,OAAO;AAAA,UACT;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,OAAO;AAAA,UACT;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
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.7-develop.6653.1.b5bc7194a0",
3
+ "version": "0.6.7-develop.6660.1.90e1e2eef6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,16 +254,16 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.7-develop.6653.1.b5bc7194a0",
258
- "@open-mercato/shared": "0.6.7-develop.6653.1.b5bc7194a0",
259
- "@open-mercato/ui": "0.6.7-develop.6653.1.b5bc7194a0",
257
+ "@open-mercato/ai-assistant": "0.6.7-develop.6660.1.90e1e2eef6",
258
+ "@open-mercato/shared": "0.6.7-develop.6660.1.90e1e2eef6",
259
+ "@open-mercato/ui": "0.6.7-develop.6660.1.90e1e2eef6",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.7-develop.6653.1.b5bc7194a0",
265
- "@open-mercato/shared": "0.6.7-develop.6653.1.b5bc7194a0",
266
- "@open-mercato/ui": "0.6.7-develop.6653.1.b5bc7194a0",
264
+ "@open-mercato/ai-assistant": "0.6.7-develop.6660.1.90e1e2eef6",
265
+ "@open-mercato/shared": "0.6.7-develop.6660.1.90e1e2eef6",
266
+ "@open-mercato/ui": "0.6.7-develop.6660.1.90e1e2eef6",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^6.9.1",
269
269
  "@testing-library/react": "^16.3.1",
@@ -26,9 +26,17 @@ export function resolveSpecialValue(template: string, context: EvaluationContext
26
26
  return getNestedValue(context, path)
27
27
  }
28
28
 
29
+ // Field paths come from rule authors, so a segment could name a prototype key.
30
+ // Rejecting these — and requiring own-property access — keeps traversal on the
31
+ // caller's own data instead of the prototype chain.
32
+ const DANGEROUS_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype'])
33
+
29
34
  /**
30
35
  * Get nested value from object using dot notation
31
36
  * Supports: 'user.name', 'items[0].quantity', 'data.values[2]'
37
+ *
38
+ * Segments naming a prototype key, or resolving to an inherited rather than an
39
+ * own property, yield `undefined`.
32
40
  */
33
41
  export function getNestedValue(obj: any, path: string): any {
34
42
  if (!obj || !path) {
@@ -46,6 +54,14 @@ export function getNestedValue(obj: any, path: string): any {
46
54
  return undefined
47
55
  }
48
56
 
57
+ if (DANGEROUS_KEYS.has(key)) {
58
+ return undefined
59
+ }
60
+
61
+ if (!Object.prototype.hasOwnProperty.call(result, key)) {
62
+ return undefined
63
+ }
64
+
49
65
  result = result[key]
50
66
  }
51
67
 
@@ -18,6 +18,7 @@ import {
18
18
  } from '../data/validators'
19
19
  import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
20
20
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
21
+ import { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'
21
22
 
22
23
  const currencyCrudEvents: CrudEventsConfig = {
23
24
  module: 'currencies',
@@ -48,9 +49,17 @@ type CurrencySnapshot = {
48
49
 
49
50
  type CurrencyUndoPayload = UndoPayload<CurrencySnapshot>
50
51
 
51
- async function loadCurrencySnapshot(em: EntityManager, id: string): Promise<CurrencySnapshot | null> {
52
- const record = await em.findOne(Currency, { id })
52
+ async function loadCurrencySnapshot(
53
+ em: EntityManager,
54
+ id: string,
55
+ ctx: Parameters<typeof buildCurrencyCommandWhere>[0],
56
+ ): Promise<CurrencySnapshot | null> {
57
+ const record = await em.findOne(
58
+ Currency,
59
+ buildCurrencyCommandWhere<Currency>(ctx, { id }),
60
+ )
53
61
  if (!record) return null
62
+ ensureCurrencyCommandScope(ctx, record)
54
63
  return {
55
64
  id: record.id,
56
65
  organizationId: record.organizationId,
@@ -91,6 +100,7 @@ const createCurrencyCommand: CommandHandler<CurrencyCreateInput, { currencyId: s
91
100
  id: 'currencies.currencies.create',
92
101
  async execute(input, ctx) {
93
102
  const parsed = currencyCreateSchema.parse(input)
103
+ ensureCurrencyCommandScope(ctx, parsed)
94
104
 
95
105
  const em = (ctx.container.resolve('em') as EntityManager).fork()
96
106
 
@@ -159,7 +169,7 @@ const createCurrencyCommand: CommandHandler<CurrencyCreateInput, { currencyId: s
159
169
  },
160
170
  captureAfter: async (_input, result, ctx) => {
161
171
  const em = (ctx.container.resolve('em') as EntityManager).fork()
162
- return loadCurrencySnapshot(em, result.currencyId)
172
+ return loadCurrencySnapshot(em, result.currencyId, ctx)
163
173
  },
164
174
  buildLog: async ({ snapshots }) => {
165
175
  const after = snapshots.after as CurrencySnapshot | undefined
@@ -204,7 +214,7 @@ const updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: s
204
214
  async prepare(input, ctx) {
205
215
  requireId(input.id, 'Currency ID is required')
206
216
  const em = ctx.container.resolve('em') as EntityManager
207
- const before = await loadCurrencySnapshot(em, input.id)
217
+ const before = await loadCurrencySnapshot(em, input.id, ctx)
208
218
  return { before }
209
219
  },
210
220
  async execute(input, ctx) {
@@ -212,10 +222,14 @@ const updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: s
212
222
  requireId(parsed.id, 'Currency ID is required')
213
223
 
214
224
  const em = (ctx.container.resolve('em') as EntityManager).fork()
215
- const record = await em.findOne(Currency, { id: parsed.id, deletedAt: null })
225
+ const record = await em.findOne(
226
+ Currency,
227
+ buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),
228
+ )
216
229
  if (!record) {
217
230
  throw new CrudHttpError(404, { error: 'Currency not found' })
218
231
  }
232
+ ensureCurrencyCommandScope(ctx, record)
219
233
 
220
234
  // Check code uniqueness if changing code
221
235
  if (parsed.code && parsed.code !== record.code) {
@@ -289,7 +303,7 @@ const updateCurrencyCommand: CommandHandler<CurrencyUpdateInput, { currencyId: s
289
303
  },
290
304
  captureAfter: async (_input, result, ctx) => {
291
305
  const em = (ctx.container.resolve('em') as EntityManager).fork()
292
- return loadCurrencySnapshot(em, result.currencyId)
306
+ return loadCurrencySnapshot(em, result.currencyId, ctx)
293
307
  },
294
308
  buildLog: async ({ snapshots, result }) => {
295
309
  const before = snapshots.before as CurrencySnapshot | undefined
@@ -334,7 +348,7 @@ const deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: s
334
348
  async prepare(input, ctx) {
335
349
  requireId(input.id, 'Currency ID is required')
336
350
  const em = ctx.container.resolve('em') as EntityManager
337
- const before = await loadCurrencySnapshot(em, input.id)
351
+ const before = await loadCurrencySnapshot(em, input.id, ctx)
338
352
  return { before }
339
353
  },
340
354
  async execute(input, ctx) {
@@ -342,10 +356,14 @@ const deleteCurrencyCommand: CommandHandler<CurrencyDeleteInput, { currencyId: s
342
356
  requireId(parsed.id, 'Currency ID is required')
343
357
 
344
358
  const em = (ctx.container.resolve('em') as EntityManager).fork()
345
- const record = await em.findOne(Currency, { id: parsed.id, deletedAt: null })
359
+ const record = await em.findOne(
360
+ Currency,
361
+ buildCurrencyCommandWhere<Currency>(ctx, { id: parsed.id }),
362
+ )
346
363
  if (!record) {
347
364
  throw new CrudHttpError(404, { error: 'Currency not found' })
348
365
  }
366
+ ensureCurrencyCommandScope(ctx, record)
349
367
 
350
368
  // Prevent deleting base currency
351
369
  if (record.isBase) {
@@ -17,6 +17,7 @@ import {
17
17
  } from '../data/validators'
18
18
  import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
19
19
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
20
+ import { buildCurrencyCommandWhere, ensureCurrencyCommandScope } from './scope'
20
21
 
21
22
  const exchangeRateCrudEvents: CrudEventsConfig = {
22
23
  module: 'currencies',
@@ -46,9 +47,17 @@ type ExchangeRateSnapshot = {
46
47
 
47
48
  type ExchangeRateUndoPayload = UndoPayload<ExchangeRateSnapshot>
48
49
 
49
- async function loadExchangeRateSnapshot(em: EntityManager, id: string): Promise<ExchangeRateSnapshot | null> {
50
- const record = await em.findOne(ExchangeRate, { id })
50
+ async function loadExchangeRateSnapshot(
51
+ em: EntityManager,
52
+ id: string,
53
+ ctx: Parameters<typeof buildCurrencyCommandWhere>[0],
54
+ ): Promise<ExchangeRateSnapshot | null> {
55
+ const record = await em.findOne(
56
+ ExchangeRate,
57
+ buildCurrencyCommandWhere<ExchangeRate>(ctx, { id }),
58
+ )
51
59
  if (!record) return null
60
+ ensureCurrencyCommandScope(ctx, record)
52
61
  return {
53
62
  id: record.id,
54
63
  organizationId: record.organizationId,
@@ -97,6 +106,7 @@ const createExchangeRateCommand: CommandHandler<ExchangeRateCreateInput, { excha
97
106
  id: 'currencies.exchange_rates.create',
98
107
  async execute(input, ctx) {
99
108
  const parsed = exchangeRateCreateSchema.parse(input)
109
+ ensureCurrencyCommandScope(ctx, parsed)
100
110
 
101
111
  const em = (ctx.container.resolve('em') as EntityManager).fork()
102
112
 
@@ -158,7 +168,7 @@ const createExchangeRateCommand: CommandHandler<ExchangeRateCreateInput, { excha
158
168
  },
159
169
  captureAfter: async (_input, result, ctx) => {
160
170
  const em = (ctx.container.resolve('em') as EntityManager).fork()
161
- return loadExchangeRateSnapshot(em, result.exchangeRateId)
171
+ return loadExchangeRateSnapshot(em, result.exchangeRateId, ctx)
162
172
  },
163
173
  buildLog: async ({ snapshots }) => {
164
174
  const after = snapshots.after as ExchangeRateSnapshot | undefined
@@ -198,7 +208,7 @@ const updateExchangeRateCommand: CommandHandler<ExchangeRateUpdateInput, { excha
198
208
  async prepare(input, ctx) {
199
209
  requireId(input.id, 'Exchange rate ID is required')
200
210
  const em = ctx.container.resolve('em') as EntityManager
201
- const before = await loadExchangeRateSnapshot(em, input.id)
211
+ const before = await loadExchangeRateSnapshot(em, input.id, ctx)
202
212
  return { before }
203
213
  },
204
214
  async execute(input, ctx) {
@@ -206,10 +216,14 @@ const updateExchangeRateCommand: CommandHandler<ExchangeRateUpdateInput, { excha
206
216
  requireId(parsed.id, 'Exchange rate ID is required')
207
217
 
208
218
  const em = (ctx.container.resolve('em') as EntityManager).fork()
209
- const record = await em.findOne(ExchangeRate, { id: parsed.id, deletedAt: null })
219
+ const record = await em.findOne(
220
+ ExchangeRate,
221
+ buildCurrencyCommandWhere<ExchangeRate>(ctx, { id: parsed.id }),
222
+ )
210
223
  if (!record) {
211
224
  throw new CrudHttpError(404, { error: 'Exchange rate not found' })
212
225
  }
226
+ ensureCurrencyCommandScope(ctx, record)
213
227
 
214
228
  // Validate currencies if changed
215
229
  const fromCode = parsed.fromCurrencyCode ?? record.fromCurrencyCode
@@ -288,7 +302,7 @@ const updateExchangeRateCommand: CommandHandler<ExchangeRateUpdateInput, { excha
288
302
  },
289
303
  captureAfter: async (_input, result, ctx) => {
290
304
  const em = (ctx.container.resolve('em') as EntityManager).fork()
291
- return loadExchangeRateSnapshot(em, result.exchangeRateId)
305
+ return loadExchangeRateSnapshot(em, result.exchangeRateId, ctx)
292
306
  },
293
307
  buildLog: async ({ snapshots, result }) => {
294
308
  const before = snapshots.before as ExchangeRateSnapshot | undefined
@@ -332,7 +346,7 @@ const deleteExchangeRateCommand: CommandHandler<ExchangeRateDeleteInput, { excha
332
346
  async prepare(input, ctx) {
333
347
  requireId(input.id, 'Exchange rate ID is required')
334
348
  const em = ctx.container.resolve('em') as EntityManager
335
- const before = await loadExchangeRateSnapshot(em, input.id)
349
+ const before = await loadExchangeRateSnapshot(em, input.id, ctx)
336
350
  return { before }
337
351
  },
338
352
  async execute(input, ctx) {
@@ -340,10 +354,14 @@ const deleteExchangeRateCommand: CommandHandler<ExchangeRateDeleteInput, { excha
340
354
  requireId(parsed.id, 'Exchange rate ID is required')
341
355
 
342
356
  const em = (ctx.container.resolve('em') as EntityManager).fork()
343
- const record = await em.findOne(ExchangeRate, { id: parsed.id, deletedAt: null })
357
+ const record = await em.findOne(
358
+ ExchangeRate,
359
+ buildCurrencyCommandWhere<ExchangeRate>(ctx, { id: parsed.id }),
360
+ )
344
361
  if (!record) {
345
362
  throw new CrudHttpError(404, { error: 'Exchange rate not found' })
346
363
  }
364
+ ensureCurrencyCommandScope(ctx, record)
347
365
 
348
366
  record.deletedAt = new Date()
349
367
  record.isActive = false
@@ -0,0 +1,32 @@
1
+ import type { FilterQuery } from '@mikro-orm/core'
2
+ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
+ import { buildScopedWhere } from '@open-mercato/shared/lib/api/crud'
4
+ import {
5
+ ensureOrganizationScope,
6
+ ensureTenantScope,
7
+ } from '@open-mercato/shared/lib/commands/scope'
8
+
9
+ type CurrencyCommandScopedRecord = {
10
+ organizationId: string
11
+ tenantId: string
12
+ }
13
+
14
+ export function buildCurrencyCommandWhere<T extends object>(
15
+ ctx: CommandRuntimeContext,
16
+ base: FilterQuery<T>,
17
+ ): FilterQuery<T> {
18
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined
19
+ return buildScopedWhere(base as Record<string, unknown>, {
20
+ organizationId,
21
+ organizationIds: ctx.organizationIds ?? undefined,
22
+ tenantId: ctx.auth?.tenantId ?? undefined,
23
+ }) as FilterQuery<T>
24
+ }
25
+
26
+ export function ensureCurrencyCommandScope(
27
+ ctx: CommandRuntimeContext,
28
+ record: CurrencyCommandScopedRecord,
29
+ ): void {
30
+ ensureTenantScope(ctx, record.tenantId)
31
+ ensureOrganizationScope(ctx, record.organizationId)
32
+ }
@@ -36,6 +36,8 @@ export type ActivitiesSectionProps = {
36
36
  runGuardedMutation?: GuardedMutationRunner
37
37
  refreshKey?: number
38
38
  onEditActivity?: (activity: InteractionSummary) => void
39
+ /** Interaction type hidden from the timeline by default ('task' unless overridden); pass null to show every type. */
40
+ excludeInteractionType?: string | null
39
41
  }
40
42
 
41
43
  function toDateOnly(value: string | null | undefined): string {
@@ -109,6 +111,7 @@ export function ActivitiesSection({
109
111
  refreshKey = 0,
110
112
  onEditActivity,
111
113
  runGuardedMutation,
114
+ excludeInteractionType = 'task',
112
115
  }: ActivitiesSectionProps) {
113
116
  const t = useT()
114
117
  const [filterTypes, setFilterTypes] = React.useState<string[]>([])
@@ -169,17 +172,18 @@ export function ActivitiesSection({
169
172
  setLoading(true)
170
173
  try {
171
174
  // Always fetch canonical interactions (new activities are always created here)
172
- const taskFilterActive = filterTypes.includes('task')
175
+ const excludedFilterActive = excludeInteractionType ? filterTypes.includes(excludeInteractionType) : true
173
176
  const canonicalParams = new URLSearchParams({
174
177
  entityId,
175
178
  limit: '50',
176
179
  sortField: 'occurredAt',
177
180
  sortDir: 'desc',
178
181
  })
179
- // Hide tasks from the activity timeline by default — they have their own tab
180
- // but lift the exclusion when the user explicitly toggled the Task chip on
181
- // (mirrors `ActivityHistorySection.tsx` after the #1805 fix).
182
- if (!taskFilterActive) canonicalParams.set('excludeInteractionType', 'task')
182
+ // Hide the configured type (tasks by default — they have their own tab)
183
+ // from the activity timeline, but lift the exclusion when the user
184
+ // explicitly toggled that type's chip on (mirrors
185
+ // `ActivityHistorySection.tsx` after the #1805 fix).
186
+ if (excludeInteractionType && !excludedFilterActive) canonicalParams.set('excludeInteractionType', excludeInteractionType)
183
187
  if (dealId) canonicalParams.set('dealId', dealId)
184
188
  if (filterTypes.length > 0) canonicalParams.set('type', filterTypes.join(','))
185
189
  if (filterDateFrom) canonicalParams.set('from', filterDateFrom)
@@ -261,7 +265,7 @@ export function ActivitiesSection({
261
265
  } finally {
262
266
  setLoading(false)
263
267
  }
264
- }, [dealId, entityId, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])
268
+ }, [dealId, entityId, excludeInteractionType, filterDateFrom, filterDateTo, filterTypes, loadedPages, useCanonicalInteractions, refreshKey, t])
265
269
 
266
270
  React.useEffect(() => {
267
271
  setLoadedPages(1)
@@ -30,6 +30,8 @@ type ActivityHistorySectionProps = {
30
30
  /** Optional guarded-mutation runner so per-row mutations route through the parent's
31
31
  * `useGuardedMutation` and emit retry-last-mutation context. */
32
32
  runMutation?: GuardedMutationRunner
33
+ /** Interaction type hidden from the history by default ('task' unless overridden); pass null to show every type. */
34
+ excludeInteractionType?: string | null
33
35
  }
34
36
 
35
37
  type InteractionListResponse = {
@@ -170,6 +172,7 @@ export function ActivityHistorySection({
170
172
  refreshKey = 0,
171
173
  onEditActivity,
172
174
  runMutation,
175
+ excludeInteractionType = 'task',
173
176
  }: ActivityHistorySectionProps) {
174
177
  const t = useT()
175
178
  const [searchInput, setSearchInput] = React.useState('')
@@ -231,14 +234,14 @@ export function ActivityHistorySection({
231
234
  let firstPageHasMore = false
232
235
  let pagesLoaded = 0
233
236
 
234
- const taskFilterActive = activeTypes.includes('task')
237
+ const excludedFilterActive = excludeInteractionType ? activeTypes.includes(excludeInteractionType) : true
235
238
  do {
236
239
  const params = new URLSearchParams({
237
240
  entityId,
238
241
  limit: String(pageSize),
239
242
  from: rangeStart,
240
243
  })
241
- if (!taskFilterActive) params.set('excludeInteractionType', 'task')
244
+ if (excludeInteractionType && !excludedFilterActive) params.set('excludeInteractionType', excludeInteractionType)
242
245
  if (activeTypes.length > 0) params.set('type', activeTypes.join(','))
243
246
  if (search) params.set('search', search)
244
247
  if (sortMode === 'recent') {
@@ -310,7 +313,7 @@ export function ActivityHistorySection({
310
313
  } finally {
311
314
  if (!isStale()) setLoading(false)
312
315
  }
313
- }, [activeTypes, dateRange, entityId, loadedPages, search, sortMode, t, useCanonicalInteractions])
316
+ }, [activeTypes, dateRange, entityId, excludeInteractionType, loadedPages, search, sortMode, t, useCanonicalInteractions])
314
317
 
315
318
  React.useEffect(() => {
316
319
  const controller = new AbortController()
@@ -103,13 +103,19 @@ const crud = makeCrudRoute({
103
103
  entity: FeatureToggle,
104
104
  idField: 'id',
105
105
  orgField: null,
106
- tenantField: "tenantId",
106
+ // The query engine requires the caller's tenant context even when the
107
+ // underlying entity is global; the list below explicitly disables its
108
+ // automatic tenant predicate.
109
+ tenantField: 'tenantId',
107
110
  softDeleteField: 'deletedAt'
108
111
  },
109
112
  indexer: { entityType: E.feature_toggles.feature_toggle },
110
113
  list: {
111
114
  schema: listQuerySchema,
112
115
  entityId: E.feature_toggles.feature_toggle,
116
+ // FeatureToggle rows and their query-index projections are global
117
+ // (null/null scope), so filtering them by the actor's tenant hides them.
118
+ omitAutomaticTenantOrgScope: true,
113
119
  fields: listFields,
114
120
  sortFieldMap: {
115
121
  id: 'id',
@@ -52,14 +52,11 @@ const featureToggleCrudIndexer = { entityType: E.feature_toggles.feature_toggle
52
52
 
53
53
  const FEATURE_TOGGLE_LOCK_RESOURCE_KIND = 'feature_toggles.feature_toggle'
54
54
 
55
- function featureToggleIdentifiers(
56
- toggle: FeatureToggle | ToggleSnapshot,
57
- ctx: { auth?: { tenantId?: string | null } | null },
58
- ) {
55
+ function featureToggleIdentifiers(toggle: FeatureToggle | ToggleSnapshot) {
59
56
  return {
60
57
  id: toggle.id,
61
58
  organizationId: null,
62
- tenantId: ctx.auth?.tenantId ?? null,
59
+ tenantId: null,
63
60
  }
64
61
  }
65
62
 
@@ -113,7 +110,7 @@ const createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string
113
110
  dataEngine,
114
111
  action: 'created',
115
112
  entity: toggle,
116
- identifiers: featureToggleIdentifiers(toggle, ctx),
113
+ identifiers: featureToggleIdentifiers(toggle),
117
114
  syncOrigin: ctx.syncOrigin,
118
115
  indexer: featureToggleCrudIndexer,
119
116
  })
@@ -157,7 +154,7 @@ const createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string
157
154
  dataEngine,
158
155
  action: 'deleted',
159
156
  entity: toggle,
160
- identifiers: featureToggleIdentifiers(toggle, ctx),
157
+ identifiers: featureToggleIdentifiers(toggle),
161
158
  syncOrigin: ctx.syncOrigin,
162
159
  indexer: featureToggleCrudIndexer,
163
160
  })
@@ -196,7 +193,7 @@ const createToggleCommand: CommandHandler<ToggleCreateInput, { toggleId: string
196
193
  dataEngine,
197
194
  action: 'created',
198
195
  entity: toggle,
199
- identifiers: featureToggleIdentifiers(toggle, ctx),
196
+ identifiers: featureToggleIdentifiers(toggle),
200
197
  syncOrigin: ctx.syncOrigin,
201
198
  indexer: featureToggleCrudIndexer,
202
199
  })
@@ -245,7 +242,7 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
245
242
  dataEngine,
246
243
  action: 'updated',
247
244
  entity: toggle,
248
- identifiers: featureToggleIdentifiers(toggle, ctx),
245
+ identifiers: featureToggleIdentifiers(toggle),
249
246
  syncOrigin: ctx.syncOrigin,
250
247
  indexer: featureToggleCrudIndexer,
251
248
  })
@@ -328,7 +325,7 @@ const updateToggleCommand: CommandHandler<ToggleUpdateInput, { toggleId: string
328
325
  dataEngine,
329
326
  action: 'updated',
330
327
  entity: toggle,
331
- identifiers: featureToggleIdentifiers(toggle, ctx),
328
+ identifiers: featureToggleIdentifiers(toggle),
332
329
  syncOrigin: ctx.syncOrigin,
333
330
  indexer: featureToggleCrudIndexer,
334
331
  })
@@ -369,7 +366,7 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
369
366
  dataEngine,
370
367
  action: 'deleted',
371
368
  entity: toggle,
372
- identifiers: featureToggleIdentifiers(toggle, ctx),
369
+ identifiers: featureToggleIdentifiers(toggle),
373
370
  syncOrigin: ctx.syncOrigin,
374
371
  indexer: featureToggleCrudIndexer,
375
372
  })
@@ -443,7 +440,7 @@ const deleteToggleCommand: CommandHandler<{ body?: Record<string, unknown>; quer
443
440
  dataEngine,
444
441
  action: 'updated',
445
442
  entity: toggle,
446
- identifiers: featureToggleIdentifiers(toggle, ctx),
443
+ identifiers: featureToggleIdentifiers(toggle),
447
444
  syncOrigin: ctx.syncOrigin,
448
445
  indexer: featureToggleCrudIndexer,
449
446
  })