@open-mercato/cli 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7
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 +4 -4
- package/build.mjs +7 -24
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +88 -32
- package/dist/agentic/codex/enforcement-rules.md +16 -2
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +50 -19
- package/dist/agentic/cursor/rules/entity-guard.mdc +21 -9
- package/dist/agentic/cursor/rules/generated-guard.mdc +5 -3
- package/dist/agentic/cursor/rules/open-mercato.mdc +29 -3
- package/dist/agentic/guides/cache.md +45 -0
- package/dist/agentic/guides/core.md +453 -0
- package/dist/agentic/guides/events.md +117 -0
- package/dist/agentic/guides/module-facts.json +3833 -115419
- package/dist/agentic/guides/module-system.md +127 -12
- package/dist/agentic/guides/modules/ai_assistant.md +28 -227
- package/dist/agentic/guides/modules/api_docs.md +2 -58
- package/dist/agentic/guides/modules/api_keys.md +7 -92
- package/dist/agentic/guides/modules/attachments.md +14 -119
- package/dist/agentic/guides/modules/audit_logs.md +10 -131
- package/dist/agentic/guides/modules/auth.md +33 -276
- package/dist/agentic/guides/modules/business_rules.md +14 -149
- package/dist/agentic/guides/modules/catalog.md +43 -475
- package/dist/agentic/guides/modules/channel_gmail.md +3 -82
- package/dist/agentic/guides/modules/channel_imap.md +3 -82
- package/dist/agentic/guides/modules/checkout.md +27 -299
- package/dist/agentic/guides/modules/communication_channels.md +33 -315
- package/dist/agentic/guides/modules/configs.md +8 -104
- package/dist/agentic/guides/modules/content.md +2 -57
- package/dist/agentic/guides/modules/currencies.md +17 -152
- package/dist/agentic/guides/modules/customer_accounts.md +45 -387
- package/dist/agentic/guides/modules/customers.md +85 -746
- package/dist/agentic/guides/modules/dashboards.md +10 -155
- package/dist/agentic/guides/modules/data_sync.md +16 -149
- package/dist/agentic/guides/modules/dictionaries.md +12 -107
- package/dist/agentic/guides/modules/directory.md +15 -148
- package/dist/agentic/guides/modules/entities.md +13 -145
- package/dist/agentic/guides/modules/events.md +2 -59
- package/dist/agentic/guides/modules/feature_toggles.md +9 -119
- package/dist/agentic/guides/modules/gateway_stripe.md +4 -92
- package/dist/agentic/guides/modules/generators.md +2 -45
- package/dist/agentic/guides/modules/inbox_ops.md +27 -207
- package/dist/agentic/guides/modules/integrations.md +16 -146
- package/dist/agentic/guides/modules/messages.md +24 -223
- package/dist/agentic/guides/modules/notifications.md +8 -96
- package/dist/agentic/guides/modules/onboarding.md +6 -77
- package/dist/agentic/guides/modules/payment_gateways.md +21 -164
- package/dist/agentic/guides/modules/perspectives.md +7 -77
- package/dist/agentic/guides/modules/planner.md +18 -136
- package/dist/agentic/guides/modules/portal.md +2 -93
- package/dist/agentic/guides/modules/progress.md +14 -104
- package/dist/agentic/guides/modules/query_index.md +15 -154
- package/dist/agentic/guides/modules/record_locks.md +21 -206
- package/dist/agentic/guides/modules/resources.md +30 -197
- package/dist/agentic/guides/modules/sales.md +84 -678
- package/dist/agentic/guides/modules/scheduler.md +14 -133
- package/dist/agentic/guides/modules/search.md +6 -170
- package/dist/agentic/guides/modules/security.md +35 -294
- package/dist/agentic/guides/modules/shipping_carriers.md +18 -134
- package/dist/agentic/guides/modules/sso.md +26 -151
- package/dist/agentic/guides/modules/staff.md +57 -369
- package/dist/agentic/guides/modules/storage_s3.md +5 -95
- package/dist/agentic/guides/modules/sync_akeneo.md +3 -85
- package/dist/agentic/guides/modules/sync_excel.md +6 -75
- package/dist/agentic/guides/modules/system_status_overlays.md +2 -60
- package/dist/agentic/guides/modules/translations.md +10 -104
- package/dist/agentic/guides/modules/webhooks.md +24 -203
- package/dist/agentic/guides/modules/wms.md +46 -441
- package/dist/agentic/guides/modules/workflows.md +43 -302
- package/dist/agentic/guides/queue.md +53 -0
- package/dist/agentic/guides/search.md +115 -0
- package/dist/agentic/guides/shared.md +108 -0
- package/dist/agentic/guides/ui.md +347 -0
- package/dist/agentic/shared/AGENTS.md.template +311 -124
- package/dist/agentic/shared/ai/agentic.config.json +0 -2
- package/dist/agentic/shared/ai/lessons.md +3 -26
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +27 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +0 -4
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +241 -20
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +260 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +601 -16
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +146 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +310 -14
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +97 -15
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +84 -0
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +193 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +152 -33
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +708 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +34 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +762 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +820 -20
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +50 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +97 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +843 -19
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +271 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +63 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +467 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +101 -0
- package/dist/agentic/shared/ai/skills/tiers.json +21 -143
- package/dist/agentic/shared/ai/skills/tiers.schema.json +40 -52
- package/dist/agentic/shared/ai/specs/README.md +3 -4
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +10 -226
- package/dist/agentic/shared/ai/trackers/github.md +4 -108
- package/dist/agentic/shared/scripts/install-skills.sh +694 -4
- package/dist/bin.js +1 -13
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +8 -88
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +3 -13
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +186 -418
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +0 -20
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-facts-discovery.js +7 -41
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +45 -1387
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-registry.js +5 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/resolver.js +0 -2
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/testing/integration.js +0 -5
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +1 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +11 -13
- package/src/bin.ts +1 -33
- package/src/lib/__integration__/TC-INT-008.spec.ts +9 -107
- package/src/lib/__tests__/agentic-init.test.ts +5 -41
- package/src/lib/agentic-init.ts +3 -15
- package/src/lib/agentic-setup.ts +224 -530
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +2 -88
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +0 -1
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +10 -54
- package/src/lib/generators/__tests__/module-subset.test.ts +0 -18
- package/src/lib/generators/extensions/injection-widgets.ts +0 -51
- package/src/lib/generators/module-facts-discovery.ts +7 -58
- package/src/lib/generators/module-facts.ts +46 -1851
- package/src/lib/generators/module-registry.ts +6 -16
- package/src/lib/resolver.ts +0 -3
- package/src/lib/testing/__tests__/integration.test.ts +0 -4
- package/src/lib/testing/integration.ts +0 -5
- package/src/mercato.ts +1 -26
- package/dist/agentic/guides/ai-workflows.md +0 -76
- package/dist/agentic/guides/architecture.md +0 -107
- package/dist/agentic/guides/backend-ui.md +0 -80
- package/dist/agentic/guides/contracts.md +0 -92
- package/dist/agentic/guides/extensions.md +0 -121
- package/dist/agentic/guides/framework-extension-points.md +0 -36
- package/dist/agentic/guides/integrations.md +0 -81
- package/dist/agentic/guides/modules/design_system.md +0 -102
- package/dist/agentic/guides/spec-delivery.md +0 -22
- package/dist/agentic/guides/testing-debugging.md +0 -80
- package/dist/agentic/guides/upstream/AGENTS.md +0 -298
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +0 -316
- package/dist/agentic/guides/upstream/manifest.json +0 -8
- package/dist/agentic/shared/ai/harness/README.md +0 -50
- package/dist/agentic/shared/ai/harness/RELEASE.md +0 -57
- package/dist/agentic/shared/ai/harness/cases.json +0 -13680
- package/dist/agentic/shared/ai/harness/cases.schema.json +0 -133
- package/dist/agentic/shared/ai/harness/fixtures/index.json +0 -49
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +0 -162
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +0 -21
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +0 -96
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +0 -172
- package/dist/agentic/shared/ai/harness/release-matrix.json +0 -158
- package/dist/agentic/shared/ai/harness/release-result.schema.json +0 -219
- package/dist/agentic/shared/ai/harness/result.schema.json +0 -76
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +0 -12
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +0 -73
- package/dist/agentic/shared/ai/harness/validators.json +0 -78
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +0 -2121
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +0 -1096
- package/dist/agentic/shared/ai/lessons/_template.md +0 -16
- package/dist/agentic/shared/ai/review-checklist.md +0 -36
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +0 -21
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +0 -98
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +0 -16
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +0 -53
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +0 -11
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +0 -30
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +0 -12
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +0 -13
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +0 -28
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +0 -22
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +0 -12
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +0 -19
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +0 -13
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +0 -14
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +0 -10
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +0 -17
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +0 -29
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +0 -100
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +0 -16
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +0 -17
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +0 -25
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +0 -11
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +0 -13
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +0 -18
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +0 -20
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +0 -33
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +0 -40
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +0 -27
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +0 -24
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +0 -16
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +0 -14
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +0 -15
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +0 -12
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +0 -34
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +0 -4
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +0 -11
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +0 -31
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +0 -39
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +0 -43
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +0 -19
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +0 -172
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +0 -12
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +0 -83
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +0 -21
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +0 -11
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +0 -22
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +0 -13
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +0 -41
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +0 -19
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +0 -44
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +0 -39
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +0 -20
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +0 -57
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +0 -11
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +0 -486
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +0 -18
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +0 -27
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +0 -13
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +0 -41
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +0 -11
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +0 -15
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +0 -16
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +0 -14
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +0 -187
- package/dist/agentic/shared/scripts/check-lessons.mjs +0 -178
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +0 -2958
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +0 -329
- package/dist/agentic/shared/scripts/framework-context.mjs +0 -888
- package/dist/agentic/shared/scripts/install-skills.mjs +0 -1029
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +0 -222
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +0 -1774
- package/dist/lib/generators/module-extension-facts.js +0 -1840
- package/dist/lib/generators/module-extension-facts.js.map +0 -7
- package/dist/lib/generators/module-fact-sources.js +0 -70
- package/dist/lib/generators/module-fact-sources.js.map +0 -7
- package/dist/lib/generators/module-override-targets.js +0 -688
- package/dist/lib/generators/module-override-targets.js.map +0 -7
- package/dist/lib/load-env.js +0 -24
- package/dist/lib/load-env.js.map +0 -7
- package/dist/lib/telemetry-init.js +0 -358
- package/dist/lib/telemetry-init.js.map +0 -7
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +0 -386
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +0 -184
- package/src/lib/__tests__/load-env.test.ts +0 -65
- package/src/lib/__tests__/telemetry-init.test.ts +0 -298
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +0 -92
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +0 -762
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +0 -404
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +0 -108
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +0 -561
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +0 -119
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +0 -149
- package/src/lib/generators/__tests__/module-override-targets.test.ts +0 -304
- package/src/lib/generators/module-extension-facts.ts +0 -2365
- package/src/lib/generators/module-fact-sources.ts +0 -108
- package/src/lib/generators/module-override-targets.ts +0 -863
- package/src/lib/load-env.ts +0 -30
- package/src/lib/telemetry-init.ts +0 -461
|
@@ -1,34 +1,858 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: om-system-extension
|
|
3
|
-
description: Extend
|
|
3
|
+
description: Extend core modules using the Universal Module Extension System (UMES). Use when adding columns/fields/filters to existing tables/forms, enriching API responses, intercepting API routes, blocking/validating mutations, replacing UI components, injecting menu items, or reacting to domain events. Triggers on "extend", "add column to", "add field to", "inject into", "intercept", "enrich", "hook into", "customize", "override component", "add menu item", "react to event", "block mutation", "validate before save".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# System Extension — UMES Wizard
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Extend any core module without modifying its source code. This skill guides you through the Universal Module Extension System (UMES) — selecting the right mechanism, generating all required files, and wiring everything correctly.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
For full type contracts, see `references/extension-contracts.md`.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Table of Contents
|
|
13
13
|
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
18
|
-
5.
|
|
19
|
-
6.
|
|
14
|
+
1. [Decision Tree](#1-decision-tree)
|
|
15
|
+
2. [Response Enrichers](#2-response-enrichers)
|
|
16
|
+
3. [Widget Injection — Fields](#3-widget-injection--fields)
|
|
17
|
+
4. [Widget Injection — Columns](#4-widget-injection--columns)
|
|
18
|
+
5. [Widget Injection — Filters](#5-widget-injection--filters)
|
|
19
|
+
6. [Widget Injection — Row Actions & Bulk Actions](#6-widget-injection--row-actions--bulk-actions)
|
|
20
|
+
7. [Widget Injection — Menu Items](#7-widget-injection--menu-items)
|
|
21
|
+
8. [API Interceptors](#8-api-interceptors)
|
|
22
|
+
9. [Mutation Guards](#9-mutation-guards)
|
|
23
|
+
10. [Component Replacement](#10-component-replacement)
|
|
24
|
+
11. [Event Subscribers](#11-event-subscribers)
|
|
25
|
+
12. [The Triad Pattern](#12-the-triad-pattern)
|
|
26
|
+
13. [Wiring & Verification](#13-wiring--verification)
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Decision Tree
|
|
31
|
+
|
|
32
|
+
Ask what the developer wants to achieve. Match to the correct mechanism(s).
|
|
33
|
+
|
|
34
|
+
| Goal | Mechanism(s) Required | Section |
|
|
35
|
+
|------|----------------------|---------|
|
|
36
|
+
| **Add data to another module's API response** | Response Enricher | §2 |
|
|
37
|
+
| **Add a field to another module's form** | Response Enricher + Field Widget + injection-table (Triad) | §12 |
|
|
38
|
+
| **Add a column to another module's table** | Response Enricher + Column Widget + injection-table (Triad) | §12 |
|
|
39
|
+
| **Add a filter to another module's table** | Filter Widget + injection-table + API Interceptor (for server filters) | §5 + §8 |
|
|
40
|
+
| **Add row/bulk actions to another module's table** | Row Action / Bulk Action Widget + injection-table | §6 |
|
|
41
|
+
| **Add a menu item to sidebar/topbar** | Menu Item Widget + injection-table | §7 |
|
|
42
|
+
| **Validate/block a request before it reaches an API route** | API Interceptor (before hook) | §8 |
|
|
43
|
+
| **Transform/enrich an API response after it returns** | API Interceptor (after hook) or Response Enricher | §8 or §2 |
|
|
44
|
+
| **Block/validate mutations before entity persistence** | Mutation Guard | §9 |
|
|
45
|
+
| **Replace or wrap a UI component** | Component Replacement | §10 |
|
|
46
|
+
| **React to domain events (after entity create/update/delete)** | Event Subscriber | §11 |
|
|
47
|
+
| **Add a tab/section to a detail page** | Widget Injection (tab kind) + injection-table | §6 |
|
|
48
|
+
|
|
49
|
+
**When multiple mechanisms are needed** (e.g., "add a column"), follow the **Triad Pattern** (§12) which wires enricher → widget → injection-table as a coordinated set.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Response Enrichers
|
|
54
|
+
|
|
55
|
+
**Purpose**: Add computed fields to another module's API response. Fields are namespaced under `_<yourModule>` to avoid collisions.
|
|
56
|
+
|
|
57
|
+
**File**: `src/modules/<your-module>/data/enrichers.ts`
|
|
58
|
+
|
|
59
|
+
### Template
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'
|
|
63
|
+
|
|
64
|
+
const enricher: ResponseEnricher = {
|
|
65
|
+
id: '<your-module>.<enricher-name>',
|
|
66
|
+
targetEntity: '<target-module>.<entity>', // e.g., 'customers.person'
|
|
67
|
+
priority: 50,
|
|
68
|
+
timeout: 2000,
|
|
69
|
+
fallback: { _<your-module>: {} },
|
|
70
|
+
|
|
71
|
+
async enrichOne(record, context: EnricherContext) {
|
|
72
|
+
const em = context.em as EntityManager
|
|
73
|
+
// Fetch your data for this single record
|
|
74
|
+
const data = await em.findOne(YourEntity, {
|
|
75
|
+
foreignId: record.id,
|
|
76
|
+
organizationId: context.organizationId,
|
|
77
|
+
})
|
|
78
|
+
return {
|
|
79
|
+
...record,
|
|
80
|
+
_<your-module>: {
|
|
81
|
+
fieldName: data?.value ?? null,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// REQUIRED for list endpoints — prevents N+1 queries
|
|
87
|
+
async enrichMany(records, context: EnricherContext) {
|
|
88
|
+
const em = context.em as EntityManager
|
|
89
|
+
const ids = records.map(r => r.id)
|
|
90
|
+
// Single batch query for ALL records
|
|
91
|
+
const items = await em.find(YourEntity, {
|
|
92
|
+
foreignId: { $in: ids },
|
|
93
|
+
organizationId: context.organizationId,
|
|
94
|
+
})
|
|
95
|
+
const byForeignId = new Map(items.map(i => [i.foreignId, i]))
|
|
96
|
+
return records.map(r => ({
|
|
97
|
+
...r,
|
|
98
|
+
_<your-module>: {
|
|
99
|
+
fieldName: byForeignId.get(r.id)?.value ?? null,
|
|
100
|
+
},
|
|
101
|
+
}))
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const enrichers = [enricher]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Rules
|
|
109
|
+
|
|
110
|
+
- **MUST** implement `enrichMany` — without it, list endpoints cause N+1 queries
|
|
111
|
+
- **MUST** namespace all added fields under `_<your-module>` prefix
|
|
112
|
+
- **MUST NOT** modify existing fields — enrichers are additive-only
|
|
113
|
+
- **MUST** use batch queries (`$in`) in `enrichMany`, never per-record lookups
|
|
114
|
+
- Set `critical: false` (default) so enricher failures don't break the target API
|
|
115
|
+
- Set `timeout` to prevent slow external calls from blocking responses
|
|
116
|
+
- Set `fallback` to provide safe defaults when enricher times out
|
|
117
|
+
|
|
118
|
+
### Context Available
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface EnricherContext {
|
|
122
|
+
organizationId: string // Current tenant org
|
|
123
|
+
tenantId: string // Current tenant
|
|
124
|
+
userId: string // Authenticated user
|
|
125
|
+
em: EntityManager // Read-only database access
|
|
126
|
+
container: AwilixContainer // DI container
|
|
127
|
+
requestedFields?: string[] // Sparse fieldset request
|
|
128
|
+
userFeatures?: string[] // User's ACL features
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 3. Widget Injection — Fields
|
|
135
|
+
|
|
136
|
+
**Purpose**: Add an editable field to another module's CrudForm.
|
|
137
|
+
|
|
138
|
+
**File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
|
|
139
|
+
|
|
140
|
+
### Template
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import type { InjectionFieldWidget } from '@open-mercato/shared/modules/widgets'
|
|
144
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
145
|
+
|
|
146
|
+
const widget: InjectionFieldWidget = {
|
|
147
|
+
metadata: { id: '<your-module>.injection.<field-name>', priority: 50 },
|
|
148
|
+
fields: [
|
|
149
|
+
{
|
|
150
|
+
id: '_<your-module>.<fieldName>', // Matches enricher namespace
|
|
151
|
+
label: '<your-module>.fields.<fieldName>', // i18n key
|
|
152
|
+
type: 'select', // text | textarea | number | select | checkbox | date | custom
|
|
153
|
+
group: 'details', // Target group in CrudForm
|
|
154
|
+
options: [
|
|
155
|
+
{ value: 'option1', label: '<your-module>.options.option1' },
|
|
156
|
+
{ value: 'option2', label: '<your-module>.options.option2' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
eventHandlers: {
|
|
161
|
+
onSave: async (data, context) => {
|
|
162
|
+
const resourceId = (context as Record<string, unknown>).resourceId as string
|
|
163
|
+
const value = (data as Record<string, unknown>)['_<your-module>.<fieldName>']
|
|
164
|
+
|
|
165
|
+
// Upsert pattern — idempotent save
|
|
166
|
+
const existing = await readApiResultOrThrow<{ items: Array<{ id: string }> }>(
|
|
167
|
+
`/api/<your-module>/resource?foreignId=${resourceId}`,
|
|
168
|
+
)
|
|
169
|
+
if (existing?.items?.[0]?.id) {
|
|
170
|
+
await readApiResultOrThrow(`/api/<your-module>/resource`, {
|
|
171
|
+
method: 'PUT',
|
|
172
|
+
body: JSON.stringify({ id: existing.items[0].id, foreignId: resourceId, value }),
|
|
173
|
+
})
|
|
174
|
+
} else {
|
|
175
|
+
await readApiResultOrThrow(`/api/<your-module>/resource`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
body: JSON.stringify({ foreignId: resourceId, value }),
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default widget
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Rules
|
|
188
|
+
|
|
189
|
+
- Field `id` MUST match the enricher namespace path (e.g., `_example.priority`)
|
|
190
|
+
- `onSave` endpoints MUST be idempotent (use upsert pattern)
|
|
191
|
+
- Widget `onSave` fires BEFORE the core form save — design for partial failure
|
|
192
|
+
- Always use i18n keys for `label` and option labels — never hardcode strings
|
|
193
|
+
- The field reads its initial value from the enriched API response automatically
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 4. Widget Injection — Columns
|
|
198
|
+
|
|
199
|
+
**Purpose**: Add a column to another module's DataTable.
|
|
200
|
+
|
|
201
|
+
**File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
|
|
202
|
+
|
|
203
|
+
### Template
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import type { InjectionColumnWidget } from '@open-mercato/shared/modules/widgets'
|
|
207
|
+
|
|
208
|
+
const widget: InjectionColumnWidget = {
|
|
209
|
+
metadata: { id: '<your-module>.injection.<column-name>', priority: 40 },
|
|
210
|
+
columns: [
|
|
211
|
+
{
|
|
212
|
+
id: '<your-module>_<fieldName>',
|
|
213
|
+
header: '<your-module>.columns.<fieldName>', // i18n key
|
|
214
|
+
accessorKey: '_<your-module>.<fieldName>', // Path to enriched data
|
|
215
|
+
sortable: false, // MUST be false for enriched-only fields
|
|
216
|
+
cell: ({ getValue }) => {
|
|
217
|
+
const value = getValue()
|
|
218
|
+
return typeof value === 'string' ? value : '—'
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default widget
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Rules
|
|
228
|
+
|
|
229
|
+
- `accessorKey` MUST point to enriched field path (e.g., `_example.priority`)
|
|
230
|
+
- `sortable` MUST be `false` for enriched-only fields (not in database index)
|
|
231
|
+
- Requires a matching Response Enricher that provides the data (Triad Pattern §12)
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 5. Widget Injection — Filters
|
|
236
|
+
|
|
237
|
+
**Purpose**: Add a filter control to another module's DataTable filter bar.
|
|
238
|
+
|
|
239
|
+
**File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
|
|
240
|
+
|
|
241
|
+
### Template
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import type { InjectionFilterWidget } from '@open-mercato/shared/modules/widgets'
|
|
245
|
+
|
|
246
|
+
const widget: InjectionFilterWidget = {
|
|
247
|
+
metadata: { id: '<your-module>.injection.<filter-name>', priority: 35 },
|
|
248
|
+
filters: [
|
|
249
|
+
{
|
|
250
|
+
id: '<your-module><FilterName>',
|
|
251
|
+
label: '<your-module>.filters.<filterName>', // i18n key
|
|
252
|
+
type: 'select', // select | text | date | dateRange | boolean
|
|
253
|
+
strategy: 'server', // 'server' = sent as query param, 'client' = filtered locally
|
|
254
|
+
queryParam: '<your-module><FilterName>',
|
|
255
|
+
options: [
|
|
256
|
+
{ value: 'value1', label: '<your-module>.options.value1' },
|
|
257
|
+
{ value: 'value2', label: '<your-module>.options.value2' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default widget
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Server-Side Filtering
|
|
267
|
+
|
|
268
|
+
When `strategy: 'server'`, the filter value is sent as a query parameter. You need an **API Interceptor** to process it:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// api/interceptors.ts
|
|
272
|
+
const filterInterceptor: ApiInterceptor = {
|
|
273
|
+
id: '<your-module>.filter-by-<filterName>',
|
|
274
|
+
targetRoute: '<target-module>/<entities>', // e.g., 'customers/people'
|
|
275
|
+
methods: ['GET'],
|
|
276
|
+
priority: 50,
|
|
277
|
+
async before(request, context) {
|
|
278
|
+
const filterValue = request.query?.['<your-module><FilterName>']
|
|
279
|
+
if (!filterValue) return { ok: true }
|
|
280
|
+
|
|
281
|
+
// Query your data to find matching target IDs
|
|
282
|
+
const em = context.em as EntityManager
|
|
283
|
+
const matches = await em.find(YourEntity, {
|
|
284
|
+
fieldName: filterValue,
|
|
285
|
+
organizationId: context.organizationId,
|
|
286
|
+
})
|
|
287
|
+
const matchingIds = matches.map(m => m.foreignId)
|
|
288
|
+
|
|
289
|
+
if (matchingIds.length === 0) {
|
|
290
|
+
return { ok: true, query: { ...request.query, ids: 'NONE' } }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Narrow results by rewriting the ids query parameter
|
|
294
|
+
const existingIds = request.query?.ids as string | undefined
|
|
295
|
+
const narrowedIds = existingIds
|
|
296
|
+
? matchingIds.filter(id => existingIds.split(',').includes(id))
|
|
297
|
+
: matchingIds
|
|
298
|
+
return { ok: true, query: { ...request.query, ids: narrowedIds.join(',') } }
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Rules
|
|
304
|
+
|
|
305
|
+
- Server filters require a matching API Interceptor to handle the `queryParam`
|
|
306
|
+
- Prefer `ids` query narrowing over post-filtering response arrays
|
|
307
|
+
- Return `ids: 'NONE'` to return empty results when no matches found
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## 6. Widget Injection — Row Actions & Bulk Actions
|
|
312
|
+
|
|
313
|
+
**Purpose**: Add context menu actions or bulk operations to another module's DataTable.
|
|
314
|
+
|
|
315
|
+
### Row Action Template
|
|
316
|
+
|
|
317
|
+
**File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import type { InjectionRowActionWidget } from '@open-mercato/shared/modules/widgets'
|
|
321
|
+
import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
322
|
+
|
|
323
|
+
const widget: InjectionRowActionWidget = {
|
|
324
|
+
metadata: { id: '<your-module>.injection.<action-name>', priority: 30 },
|
|
325
|
+
rowActions: [
|
|
326
|
+
{
|
|
327
|
+
id: '<your-module>.<entity>.<action>',
|
|
328
|
+
label: '<your-module>.actions.<actionName>', // i18n key
|
|
329
|
+
icon: 'CheckSquare', // Lucide icon name
|
|
330
|
+
features: ['<your-module>.<action>'], // ACL gating
|
|
331
|
+
placement: { position: InjectionPosition.After, relativeTo: 'edit' },
|
|
332
|
+
onSelect: (row, context) => {
|
|
333
|
+
const id = (row as Record<string, unknown>).id as string
|
|
334
|
+
const navigate = (context as { navigate?: (path: string) => void }).navigate
|
|
335
|
+
navigate?.(`/backend/<your-module>/resource/${id}`)
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default widget
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Bulk Action Template
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import type { InjectionBulkActionWidget } from '@open-mercato/shared/modules/widgets'
|
|
348
|
+
|
|
349
|
+
const widget: InjectionBulkActionWidget = {
|
|
350
|
+
metadata: { id: '<your-module>.injection.bulk-<action-name>', priority: 30 },
|
|
351
|
+
bulkActions: [
|
|
352
|
+
{
|
|
353
|
+
id: '<your-module>.bulk.<action>',
|
|
354
|
+
label: '<your-module>.actions.bulk<ActionName>',
|
|
355
|
+
features: ['<your-module>.<action>'],
|
|
356
|
+
onExecute: async (selectedRows, context) => {
|
|
357
|
+
const ids = selectedRows.map(r => (r as Record<string, unknown>).id)
|
|
358
|
+
await readApiResultOrThrow(`/api/<your-module>/bulk-action`, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
body: JSON.stringify({ targetIds: ids }),
|
|
361
|
+
})
|
|
362
|
+
;(context as { refresh?: () => void }).refresh?.()
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default widget
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Tab Widget Template (Detail Pages)
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import type { InjectionWidget } from '@open-mercato/shared/modules/widgets'
|
|
375
|
+
|
|
376
|
+
const widget: InjectionWidget = {
|
|
377
|
+
metadata: { id: '<your-module>.injection.<tab-name>', priority: 40 },
|
|
378
|
+
component: () => import('./widget.client'),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export default widget
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Then create the client component at `widget.client.tsx`:
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
'use client'
|
|
388
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
389
|
+
|
|
390
|
+
export default function MyTabContent({ context }: { context: Record<string, unknown> }) {
|
|
391
|
+
const t = useT()
|
|
392
|
+
const resourceId = context.resourceId as string
|
|
393
|
+
// Fetch and display your data
|
|
394
|
+
return <div>...</div>
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Rules
|
|
399
|
+
|
|
400
|
+
- Use `InjectionPosition` for relative placement — never hardcode positions
|
|
401
|
+
- Always set `features` for ACL-gated actions
|
|
402
|
+
- Row action `id` must be stable for integration testing
|
|
403
|
+
- Bulk action `onExecute` should call `refresh()` after mutation
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 7. Widget Injection — Menu Items
|
|
408
|
+
|
|
409
|
+
**Purpose**: Add items to sidebar, topbar, or profile dropdown.
|
|
410
|
+
|
|
411
|
+
**File**: `src/modules/<your-module>/widgets/injection/<widget-name>/widget.ts`
|
|
412
|
+
|
|
413
|
+
### Template
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import type { InjectionMenuItemWidget } from '@open-mercato/shared/modules/widgets'
|
|
417
|
+
import { InjectionPosition } from '@open-mercato/shared/modules/widgets/injection-position'
|
|
22
418
|
|
|
23
|
-
|
|
419
|
+
const widget: InjectionMenuItemWidget = {
|
|
420
|
+
metadata: { id: '<your-module>.injection.menus' },
|
|
421
|
+
menuItems: [
|
|
422
|
+
{
|
|
423
|
+
id: '<your-module>-<page>-link',
|
|
424
|
+
labelKey: '<your-module>.menu.<pageName>', // i18n key
|
|
425
|
+
label: 'Fallback Label', // Fallback if i18n missing
|
|
426
|
+
icon: 'LayoutDashboard', // Lucide icon name
|
|
427
|
+
href: '/backend/<your-module>',
|
|
428
|
+
features: ['<your-module>.view'], // ACL gating
|
|
429
|
+
groupId: '<your-module>.nav.group',
|
|
430
|
+
groupLabelKey: '<your-module>.nav.group',
|
|
431
|
+
placement: { position: InjectionPosition.Last },
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
}
|
|
24
435
|
|
|
25
|
-
|
|
436
|
+
export default widget
|
|
437
|
+
```
|
|
26
438
|
|
|
27
|
-
|
|
439
|
+
### Available Spot IDs
|
|
440
|
+
|
|
441
|
+
| Spot ID | Location |
|
|
442
|
+
|---------|----------|
|
|
443
|
+
| `menu:sidebar:main` | Main sidebar navigation |
|
|
444
|
+
| `menu:sidebar:settings` | Settings sidebar |
|
|
445
|
+
| `menu:sidebar:profile` | Profile sidebar |
|
|
446
|
+
| `menu:topbar:profile-dropdown` | User profile dropdown |
|
|
447
|
+
| `menu:topbar:actions` | Top bar action area |
|
|
448
|
+
|
|
449
|
+
### Rules
|
|
450
|
+
|
|
451
|
+
- Use `labelKey` (i18n) instead of `label` whenever possible
|
|
452
|
+
- Always set `features` for permission-gated items
|
|
453
|
+
- Use `groupId` + `groupLabelKey` to group related menu items
|
|
454
|
+
- Menu `id` must be stable for integration tests
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## 8. API Interceptors
|
|
459
|
+
|
|
460
|
+
**Purpose**: Hook into API routes to validate, transform, or enrich requests/responses without modifying the route.
|
|
461
|
+
|
|
462
|
+
**File**: `src/modules/<your-module>/api/interceptors.ts`
|
|
463
|
+
|
|
464
|
+
### Template
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
|
|
468
|
+
|
|
469
|
+
const interceptors: ApiInterceptor[] = [
|
|
470
|
+
{
|
|
471
|
+
id: '<your-module>.validate-<action>',
|
|
472
|
+
targetRoute: '<target-module>/<entities>', // e.g., 'customers/people'
|
|
473
|
+
methods: ['POST', 'PUT'],
|
|
474
|
+
priority: 50, // Lower = earlier execution
|
|
475
|
+
timeoutMs: 5000,
|
|
476
|
+
|
|
477
|
+
async before(request, context) {
|
|
478
|
+
// Validate request
|
|
479
|
+
const value = request.body?.someField
|
|
480
|
+
if (!value) {
|
|
481
|
+
return { ok: false, statusCode: 422, message: 'someField is required' }
|
|
482
|
+
}
|
|
483
|
+
// Optionally rewrite body or query
|
|
484
|
+
return { ok: true, body: { ...request.body, normalizedField: String(value).trim() } }
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
async after(request, response, context) {
|
|
488
|
+
// Optionally enrich response
|
|
489
|
+
return {
|
|
490
|
+
merge: {
|
|
491
|
+
_<your-module>: { processedAt: Date.now() },
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
export { interceptors }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Rules
|
|
502
|
+
|
|
503
|
+
- `before` hook: return `{ ok: false, message }` to reject — never throw errors
|
|
504
|
+
- `after` hook: use `merge` to add fields, `replace` to swap entire response body
|
|
505
|
+
- Prefer exact `targetRoute` over wildcards (`*`) — wildcards match too broadly
|
|
506
|
+
- For filtering: rewrite `query.ids` (comma-separated UUIDs) — never post-filter response arrays
|
|
507
|
+
- Set `features` for permission-gated interceptors
|
|
508
|
+
- Interceptors run BEFORE sync event subscribers and mutation guards in the pipeline
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 9. Mutation Guards
|
|
513
|
+
|
|
514
|
+
**Purpose**: Block or validate mutations at the entity level before database persistence. Runs after interceptors and before ORM flush.
|
|
515
|
+
|
|
516
|
+
**File**: `src/modules/<your-module>/data/guards.ts`
|
|
517
|
+
|
|
518
|
+
### Template
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import type { MutationGuard, MutationGuardInput, MutationGuardResult } from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
522
|
+
|
|
523
|
+
const guard: MutationGuard = {
|
|
524
|
+
id: '<your-module>.<guard-name>',
|
|
525
|
+
targetEntity: '<target-module>.<entity>', // or '*' for all entities
|
|
526
|
+
operations: ['create', 'update'], // create | update | delete
|
|
527
|
+
priority: 50, // Lower = earlier execution
|
|
528
|
+
|
|
529
|
+
async validate(input: MutationGuardInput): Promise<MutationGuardResult> {
|
|
530
|
+
// input.resourceId is null for create operations
|
|
531
|
+
// input.mutationPayload contains the data being saved
|
|
532
|
+
|
|
533
|
+
if (someConditionFails) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
status: 422,
|
|
537
|
+
message: 'Validation failed: reason',
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Optionally transform payload
|
|
542
|
+
return {
|
|
543
|
+
ok: true,
|
|
544
|
+
modifiedPayload: { ...input.mutationPayload, normalizedField: 'value' },
|
|
545
|
+
shouldRunAfterSuccess: true,
|
|
546
|
+
metadata: { originalValue: input.mutationPayload?.field },
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
async afterSuccess(input) {
|
|
551
|
+
// Runs after successful mutation — for cleanup, cache invalidation, logging
|
|
552
|
+
// input.metadata contains what you passed from validate()
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const guards = [guard]
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Rules
|
|
560
|
+
|
|
561
|
+
- `resourceId` is `null` for create operations — handle this case
|
|
562
|
+
- Return a new object for `modifiedPayload` — never mutate `input.mutationPayload` in place
|
|
563
|
+
- Guards with `targetEntity: '*'` run on EVERY entity mutation — use sparingly
|
|
564
|
+
- `afterSuccess` only runs when `shouldRunAfterSuccess: true` in the validate result
|
|
565
|
+
- Guard errors should return structured `{ ok: false, message }` — never throw
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 10. Component Replacement
|
|
570
|
+
|
|
571
|
+
**Purpose**: Replace, wrap, or transform props of registered UI components without forking source code.
|
|
572
|
+
|
|
573
|
+
**File**: `src/modules/<your-module>/widgets/components.ts`
|
|
574
|
+
|
|
575
|
+
### Template
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
import React from 'react'
|
|
579
|
+
import type { ComponentOverride } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
580
|
+
import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
581
|
+
|
|
582
|
+
export const componentOverrides: ComponentOverride[] = [
|
|
583
|
+
// Mode 1: Wrapper — decorate existing component (safest)
|
|
584
|
+
{
|
|
585
|
+
target: { componentId: ComponentReplacementHandles.section('ui.detail', 'NotesSection') },
|
|
586
|
+
priority: 50,
|
|
587
|
+
metadata: { module: '<your-module>' },
|
|
588
|
+
wrapper: (Original) => {
|
|
589
|
+
const Wrapped = (props: any) =>
|
|
590
|
+
React.createElement(
|
|
591
|
+
'div',
|
|
592
|
+
{ className: 'border border-blue-200 rounded-md p-2' },
|
|
593
|
+
React.createElement(Original, props),
|
|
594
|
+
)
|
|
595
|
+
Wrapped.displayName = '<YourModule>NotesWrapper'
|
|
596
|
+
return Wrapped
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
// Mode 2: Props transform — modify incoming props
|
|
601
|
+
{
|
|
602
|
+
target: { componentId: ComponentReplacementHandles.dataTable('customers.people') },
|
|
603
|
+
priority: 40,
|
|
604
|
+
metadata: { module: '<your-module>' },
|
|
605
|
+
propsTransform: (props: any) => ({
|
|
606
|
+
...props,
|
|
607
|
+
defaultPageSize: 25,
|
|
608
|
+
}),
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
// Mode 3: Replace — full component swap (highest risk)
|
|
612
|
+
{
|
|
613
|
+
target: { componentId: ComponentReplacementHandles.section('sales.order', 'ShipmentDialog') },
|
|
614
|
+
priority: 50,
|
|
615
|
+
metadata: { module: '<your-module>' },
|
|
616
|
+
replacement: React.lazy(() => import('./CustomShipmentDialog')),
|
|
617
|
+
propsSchema: ShipmentDialogPropsSchema, // Zod schema for validation
|
|
618
|
+
},
|
|
619
|
+
]
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Handle IDs
|
|
623
|
+
|
|
624
|
+
| Handle | Format | Example |
|
|
625
|
+
|--------|--------|---------|
|
|
626
|
+
| `page` | `page:<path>` | `page:backend/customers/people` |
|
|
627
|
+
| `dataTable` | `data-table:<tableId>` | `data-table:customers.people` |
|
|
628
|
+
| `crudForm` | `crud-form:<entityId>` | `crud-form:customers.person` |
|
|
629
|
+
| `section` | `section:<scope>.<sectionId>` | `section:ui.detail.NotesSection` |
|
|
630
|
+
|
|
631
|
+
### Rules
|
|
632
|
+
|
|
633
|
+
- Prefer `wrapper` mode — it preserves the original component and is least likely to break
|
|
634
|
+
- `replacement` mode REQUIRES a `propsSchema` (Zod) for dev-mode contract validation
|
|
635
|
+
- Always set `displayName` on wrapper components for React DevTools debugging
|
|
636
|
+
- Wrapper composition: lower priority = innermost, higher priority = outermost
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## 11. Event Subscribers
|
|
641
|
+
|
|
642
|
+
**Purpose**: React to domain events emitted by other modules (e.g., after entity creation).
|
|
643
|
+
|
|
644
|
+
**File**: `src/modules/<your-module>/subscribers/<subscriber-name>.ts`
|
|
645
|
+
|
|
646
|
+
### Template
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
export const metadata = {
|
|
650
|
+
event: 'customers.person.created', // module.entity.action (past tense)
|
|
651
|
+
persistent: true, // true = survives server restart (uses queue)
|
|
652
|
+
id: '<your-module>:on-customer-created',
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export default async function handler(payload: Record<string, unknown>, ctx: unknown) {
|
|
656
|
+
const { resourceId, organizationId, tenantId } = payload as {
|
|
657
|
+
resourceId: string
|
|
658
|
+
organizationId: string
|
|
659
|
+
tenantId: string
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Perform side effects
|
|
663
|
+
// Examples: create related records, send notifications, sync external systems
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Sync Subscribers (Before-Event)
|
|
668
|
+
|
|
669
|
+
For subscribers that need to run **before** a mutation completes and can block it:
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
export const metadata = {
|
|
673
|
+
event: 'customers.person.creating', // .creating = before event (present tense)
|
|
674
|
+
persistent: false,
|
|
675
|
+
id: '<your-module>:validate-customer-create',
|
|
676
|
+
sync: true, // Run synchronously in request pipeline
|
|
677
|
+
priority: 50, // Lower = earlier
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export default async function handler(payload: Record<string, unknown>) {
|
|
681
|
+
const data = payload as { mutationPayload?: Record<string, unknown> }
|
|
682
|
+
|
|
683
|
+
if (someConditionFails(data.mutationPayload)) {
|
|
684
|
+
return { ok: false, status: 422, message: 'Cannot create: reason' }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Optionally modify the mutation data
|
|
688
|
+
return { ok: true, modifiedPayload: { ...data.mutationPayload, enrichedField: 'value' } }
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Event Naming Convention
|
|
693
|
+
|
|
694
|
+
| Event | Timing | Can Block? |
|
|
695
|
+
|-------|--------|-----------|
|
|
696
|
+
| `module.entity.creating` | Before create | Yes (sync only) |
|
|
697
|
+
| `module.entity.created` | After create | No |
|
|
698
|
+
| `module.entity.updating` | Before update | Yes (sync only) |
|
|
699
|
+
| `module.entity.updated` | After update | No |
|
|
700
|
+
| `module.entity.deleting` | Before delete | Yes (sync only) |
|
|
701
|
+
| `module.entity.deleted` | After delete | No |
|
|
702
|
+
|
|
703
|
+
### Rules
|
|
704
|
+
|
|
705
|
+
- After-events (`.created`, `.updated`, `.deleted`) cannot block — they are fire-and-forget
|
|
706
|
+
- Before-events (`.creating`, `.updating`, `.deleting`) require `sync: true` to block mutations
|
|
707
|
+
- Subscribers MUST be idempotent — events may be delivered more than once
|
|
708
|
+
- Use `persistent: true` for critical side effects that must survive restarts
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 12. The Triad Pattern
|
|
713
|
+
|
|
714
|
+
When extending another module's UI with data from your module, you need three coordinated pieces:
|
|
715
|
+
|
|
716
|
+
```
|
|
717
|
+
┌─────────────────┐ ┌──────────────────┐ ┌───────────────────┐
|
|
718
|
+
│ 1. ENRICHER │────▶│ 2. WIDGET │────▶│ 3. INJECTION │
|
|
719
|
+
│ (data/ │ │ (widgets/ │ │ TABLE │
|
|
720
|
+
│ enrichers.ts) │ │ injection/ │ │ (widgets/ │
|
|
721
|
+
│ │ │ <name>/ │ │ injection- │
|
|
722
|
+
│ Adds _<module> │ │ widget.ts) │ │ table.ts) │
|
|
723
|
+
│ fields to API │ │ │ │ │
|
|
724
|
+
│ response │ │ Renders the │ │ Maps widget to │
|
|
725
|
+
│ │ │ enriched data │ │ target spot ID │
|
|
726
|
+
└─────────────────┘ └──────────────────┘ └───────────────────┘
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Example: Add "Priority" field to Customers form
|
|
730
|
+
|
|
731
|
+
**Step 1 — Enricher** (`data/enrichers.ts`):
|
|
732
|
+
```typescript
|
|
733
|
+
const enricher: ResponseEnricher = {
|
|
734
|
+
id: 'priorities.customer-priority',
|
|
735
|
+
targetEntity: 'customers.person',
|
|
736
|
+
priority: 50,
|
|
737
|
+
async enrichOne(record, context) {
|
|
738
|
+
const priority = await em.findOne(CustomerPriority, { customerId: record.id })
|
|
739
|
+
return { ...record, _priorities: { level: priority?.level ?? 'normal' } }
|
|
740
|
+
},
|
|
741
|
+
async enrichMany(records, context) {
|
|
742
|
+
const items = await em.find(CustomerPriority, { customerId: { $in: records.map(r => r.id) } })
|
|
743
|
+
const byId = new Map(items.map(i => [i.customerId, i.level]))
|
|
744
|
+
return records.map(r => ({ ...r, _priorities: { level: byId.get(r.id) ?? 'normal' } }))
|
|
745
|
+
},
|
|
746
|
+
}
|
|
747
|
+
export const enrichers = [enricher]
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
**Step 2 — Field Widget** (`widgets/injection/customer-priority-field/widget.ts`):
|
|
751
|
+
```typescript
|
|
752
|
+
const widget: InjectionFieldWidget = {
|
|
753
|
+
metadata: { id: 'priorities.injection.customer-priority-field', priority: 50 },
|
|
754
|
+
fields: [{
|
|
755
|
+
id: '_priorities.level',
|
|
756
|
+
label: 'priorities.fields.level',
|
|
757
|
+
type: 'select',
|
|
758
|
+
group: 'details',
|
|
759
|
+
options: [
|
|
760
|
+
{ value: 'low', label: 'priorities.options.low' },
|
|
761
|
+
{ value: 'normal', label: 'priorities.options.normal' },
|
|
762
|
+
{ value: 'high', label: 'priorities.options.high' },
|
|
763
|
+
],
|
|
764
|
+
}],
|
|
765
|
+
eventHandlers: {
|
|
766
|
+
onSave: async (data, context) => {
|
|
767
|
+
const customerId = (context as any).resourceId
|
|
768
|
+
const level = (data as any)['_priorities.level']
|
|
769
|
+
await readApiResultOrThrow('/api/priorities/customer-priorities', {
|
|
770
|
+
method: 'POST',
|
|
771
|
+
body: JSON.stringify({ customerId, level }),
|
|
772
|
+
})
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
}
|
|
776
|
+
export default widget
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
**Step 3 — Injection Table** (`widgets/injection-table.ts`):
|
|
780
|
+
```typescript
|
|
781
|
+
export const widgetInjections = {
|
|
782
|
+
'crud-form:customers.person:fields': {
|
|
783
|
+
widgetId: 'priorities.injection.customer-priority-field',
|
|
784
|
+
priority: 50,
|
|
785
|
+
},
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
**Step 4 — Run `yarn generate`** to wire everything up.
|
|
790
|
+
|
|
791
|
+
### Triad for Columns
|
|
792
|
+
|
|
793
|
+
Same pattern but with Column Widget instead of Field Widget:
|
|
794
|
+
|
|
795
|
+
| Spot ID Pattern | Widget Type |
|
|
796
|
+
|----------------|-------------|
|
|
797
|
+
| `crud-form:<entityId>:fields` | `InjectionFieldWidget` |
|
|
798
|
+
| `data-table:<tableId>:columns` | `InjectionColumnWidget` |
|
|
799
|
+
| `data-table:<tableId>:row-actions` | `InjectionRowActionWidget` |
|
|
800
|
+
| `data-table:<tableId>:bulk-actions` | `InjectionBulkActionWidget` |
|
|
801
|
+
| `data-table:<tableId>:filters` | `InjectionFilterWidget` |
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## 13. Wiring & Verification
|
|
806
|
+
|
|
807
|
+
### File Checklist
|
|
808
|
+
|
|
809
|
+
After implementing an extension, verify all files exist:
|
|
810
|
+
|
|
811
|
+
| File | Required When |
|
|
812
|
+
|------|--------------|
|
|
813
|
+
| `data/enrichers.ts` | Adding data to another module's API response |
|
|
814
|
+
| `widgets/injection/<name>/widget.ts` | Adding UI elements (fields, columns, actions, menus) |
|
|
815
|
+
| `widgets/injection-table.ts` | Mapping widgets to target spots |
|
|
816
|
+
| `widgets/components.ts` | Replacing/wrapping UI components |
|
|
817
|
+
| `api/interceptors.ts` | Intercepting API routes |
|
|
818
|
+
| `data/guards.ts` | Blocking/validating mutations |
|
|
819
|
+
| `subscribers/<name>.ts` | Reacting to domain events |
|
|
820
|
+
|
|
821
|
+
### Post-Implementation Steps
|
|
822
|
+
|
|
823
|
+
1. **Run `yarn generate`** — registers new enrichers, widgets, interceptors, guards
|
|
824
|
+
2. **Run `yarn dev`** — verify extension appears in target module UI
|
|
825
|
+
3. **Check browser console** — look for warnings about invalid spot IDs or missing enrichers
|
|
826
|
+
4. **Test the full flow** — create/edit/delete in the target module, verify your extension works
|
|
827
|
+
|
|
828
|
+
### Common Pitfalls
|
|
829
|
+
|
|
830
|
+
| Pitfall | Symptom | Fix |
|
|
831
|
+
|---------|---------|-----|
|
|
832
|
+
| Missing `enrichMany` | Slow list pages, N+1 queries | Implement batch enrichment with `$in` query |
|
|
833
|
+
| Wrong spot ID | Widget doesn't appear | Check exact spot ID format in target module |
|
|
834
|
+
| Missing `yarn generate` | Extension not discovered | Run `yarn generate` after adding files |
|
|
835
|
+
| Hardcoded strings | i18n warnings | Use `labelKey` / i18n keys everywhere |
|
|
836
|
+
| Missing `features` | Extension visible to all users | Add ACL `features` array |
|
|
837
|
+
| `onSave` not idempotent | Duplicate records on retry | Use upsert pattern (check-then-create-or-update) |
|
|
838
|
+
| `sortable: true` on enriched column | Sort doesn't work | Set `sortable: false` for enriched-only fields |
|
|
839
|
+
| Throw in interceptor | 500 error | Return `{ ok: false, message }` instead |
|
|
840
|
+
| Missing injection-table entry | Widget exists but not rendered | Add mapping in `injection-table.ts` |
|
|
841
|
+
|
|
842
|
+
---
|
|
28
843
|
|
|
29
844
|
## Rules
|
|
30
845
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
846
|
+
- **MUST** run `yarn generate` after adding any extension file
|
|
847
|
+
- **MUST** use i18n keys for all user-facing strings
|
|
848
|
+
- **MUST** implement `enrichMany` when creating Response Enrichers
|
|
849
|
+
- **MUST** namespace enriched fields under `_<your-module>` prefix
|
|
850
|
+
- **MUST** make `onSave` endpoints idempotent
|
|
851
|
+
- **MUST** use `{ ok: false, message }` pattern instead of throwing errors in interceptors/guards
|
|
852
|
+
- **MUST** set `sortable: false` on columns backed by enriched data only
|
|
853
|
+
- **MUST NOT** modify existing fields in enrichers — additive only
|
|
854
|
+
- **MUST NOT** directly import entities from other modules — use EntityManager queries
|
|
855
|
+
- **MUST NOT** use wildcard interceptor routes unless absolutely necessary
|
|
856
|
+
- Prefer Response Enrichers over API Interceptor `after` hooks for adding data to responses
|
|
857
|
+
- Prefer Mutation Guards over sync before-event subscribers for blocking mutations
|
|
858
|
+
- When extending UI and data together, always follow the Triad Pattern (enricher → widget → injection-table)
|