@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,834 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: om-module-scaffold
|
|
3
|
-
description:
|
|
3
|
+
description: Scaffold a new module from scratch with all required files and conventions. Use when creating a new module, adding a new entity with CRUD, or bootstrapping module features (API routes, backend pages, DI, ACL, events, search). Triggers on "create module", "new module", "scaffold module", "add module", "bootstrap module", "generate module".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Scaffold
|
|
6
|
+
# Module Scaffold
|
|
7
7
|
|
|
8
|
-
Create
|
|
8
|
+
Create a new module with all required files following Open Mercato conventions. This skill generates the full module structure, wires it into the app, and runs required generators.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Table of Contents
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
1. [Gather Requirements](#1-gather-requirements)
|
|
13
|
+
2. [Scaffold Structure](#2-scaffold-structure)
|
|
14
|
+
3. [Create Entity](#3-create-entity)
|
|
15
|
+
4. [Create Validators](#4-create-validators)
|
|
16
|
+
5. [Create API Routes](#5-create-api-routes)
|
|
17
|
+
6. [Create Backend Pages](#6-create-backend-pages)
|
|
18
|
+
7. [Add Module Metadata](#7-add-module-metadata)
|
|
19
|
+
8. [Add ACL & Setup](#8-add-acl--setup)
|
|
20
|
+
9. [Add DI Registration](#9-add-di-registration)
|
|
21
|
+
10. [Add Events](#10-add-events)
|
|
22
|
+
11. [Optional Features](#11-optional-features)
|
|
23
|
+
12. [Wire & Verify](#12-wire--verify)
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. Gather Requirements
|
|
28
|
+
|
|
29
|
+
Before writing any code, ask the developer:
|
|
30
|
+
|
|
31
|
+
1. **Module name** — plural, snake_case (e.g., `tickets`, `fleet_vehicles`, `loyalty_points`)
|
|
32
|
+
2. **Primary entity name** — singular (e.g., `ticket`, `fleet_vehicle`, `loyalty_point`)
|
|
33
|
+
3. **Key fields** — beyond standard columns, what data does this entity store?
|
|
34
|
+
4. **Relationships** — does it reference entities from other modules? (FK IDs only, no ORM relations)
|
|
35
|
+
5. **Features needed**:
|
|
36
|
+
- [ ] CRUD API (almost always yes)
|
|
37
|
+
- [ ] Backend admin pages (almost always yes)
|
|
38
|
+
- [ ] Frontend public pages
|
|
39
|
+
- [ ] Search indexing
|
|
40
|
+
- [ ] Event publishing
|
|
41
|
+
- [ ] Background workers
|
|
42
|
+
- [ ] CLI commands
|
|
43
|
+
- [ ] Custom fields support
|
|
44
|
+
- [ ] **Sensitive / GDPR-relevant fields** (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — if yes, an `encryption.ts` declaring `defaultEncryptionMaps` is mandatory; see section 11 → Encryption maps
|
|
45
|
+
|
|
46
|
+
If the developer provides a brief description, infer reasonable defaults and confirm. When key fields include names, emails, phones, addresses, free-text comments, or external API keys, treat the encryption checkbox as `yes` by default and confirm with the user rather than skipping it silently.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. Scaffold Structure
|
|
51
|
+
|
|
52
|
+
Create the directory tree under `src/modules/<module_id>/`:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
src/modules/<module_id>/
|
|
56
|
+
├── index.ts # Module metadata + feature exports
|
|
57
|
+
├── acl.ts # Feature-based permissions
|
|
58
|
+
├── setup.ts # Tenant init, role features
|
|
59
|
+
├── di.ts # Awilix DI registrations
|
|
60
|
+
├── events.ts # Typed event declarations (if needed)
|
|
61
|
+
├── encryption.ts # Tenant data encryption maps (only if entity has sensitive/GDPR fields)
|
|
62
|
+
├── data/
|
|
63
|
+
│ ├── entities.ts # MikroORM entity classes
|
|
64
|
+
│ └── validators.ts # Zod validation schemas
|
|
65
|
+
├── api/
|
|
66
|
+
│ └── <entities>/
|
|
67
|
+
│ └── route.ts # All HTTP methods in one file: GET, POST, PUT, DELETE
|
|
68
|
+
└── backend/
|
|
69
|
+
├── page.tsx # List page → /backend/<module>
|
|
70
|
+
├── <entities>/
|
|
71
|
+
│ ├── new.tsx # Create page → /backend/<module>/<entities>/new
|
|
72
|
+
│ └── [id].tsx # Edit page → /backend/<module>/<entities>/<id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 3. Create Entity
|
|
78
|
+
|
|
79
|
+
**File**: `src/modules/<module_id>/data/entities.ts`
|
|
80
|
+
|
|
81
|
+
### Template
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'
|
|
85
|
+
import { v4 } from 'uuid'
|
|
86
|
+
|
|
87
|
+
@Entity({ tableName: '<entities>' }) // plural, snake_case
|
|
88
|
+
export class <Entity> {
|
|
89
|
+
@PrimaryKey({ type: 'uuid' })
|
|
90
|
+
id: string = v4()
|
|
91
|
+
|
|
92
|
+
@Index()
|
|
93
|
+
@Property({ type: 'uuid' })
|
|
94
|
+
organization_id!: string
|
|
95
|
+
|
|
96
|
+
@Index()
|
|
97
|
+
@Property({ type: 'uuid' })
|
|
98
|
+
tenant_id!: string
|
|
99
|
+
|
|
100
|
+
// --- Domain fields ---
|
|
101
|
+
|
|
102
|
+
@Property({ type: 'varchar', length: 255 })
|
|
103
|
+
name!: string
|
|
104
|
+
|
|
105
|
+
// Add domain-specific fields here
|
|
106
|
+
// Use appropriate types: varchar, text, int, float, boolean, uuid, jsonb, date
|
|
107
|
+
|
|
108
|
+
// --- Standard columns ---
|
|
109
|
+
|
|
110
|
+
@Property({ type: 'boolean', default: true })
|
|
111
|
+
is_active: boolean = true
|
|
112
|
+
|
|
113
|
+
@Property({ type: 'timestamptz' })
|
|
114
|
+
created_at: Date = new Date()
|
|
115
|
+
|
|
116
|
+
@Property({ type: 'timestamptz', onUpdate: () => new Date() })
|
|
117
|
+
updated_at: Date = new Date()
|
|
118
|
+
|
|
119
|
+
@Property({ type: 'timestamptz', nullable: true })
|
|
120
|
+
deleted_at: Date | null = null
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Entity Rules
|
|
125
|
+
|
|
126
|
+
- Table name: **plural, snake_case** — matches module ID
|
|
127
|
+
- PK: always `uuid` with `v4()` default
|
|
128
|
+
- MUST include `organization_id` + `tenant_id` with `@Index()`
|
|
129
|
+
- MUST include `created_at`, `updated_at`, `deleted_at`, `is_active`. The `updated_at` column is what OSS **optimistic locking** (default ON) compares — keep it on every user-editable entity, and make your CRUD GET/list responses return `updatedAt` so the UI can send the expected version.
|
|
130
|
+
- Entity decorators MUST come from `@mikro-orm/decorators/legacy`
|
|
131
|
+
- Cross-module references: store FK as `uuid` field (e.g., `customer_id`) — never use ORM `@ManyToOne`
|
|
132
|
+
- Use `@Property({ type: 'jsonb' })` for flexible/nested data
|
|
133
|
+
- Use `@Property({ type: 'varchar', length: N })` for bounded strings
|
|
134
|
+
- Use `@Property({ type: 'text' })` for unbounded text
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Create Validators
|
|
139
|
+
|
|
140
|
+
**File**: `src/modules/<module_id>/data/validators.ts`
|
|
141
|
+
|
|
142
|
+
### Template
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { z } from 'zod'
|
|
146
|
+
|
|
147
|
+
export const list<Entity>Schema = z.object({
|
|
148
|
+
search: z.string().optional(),
|
|
149
|
+
id: z.string().uuid().optional(),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
export const create<Entity>Schema = z.object({
|
|
153
|
+
name: z.string().min(1).max(255),
|
|
154
|
+
// Add domain fields matching entity
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
export const update<Entity>Schema = create<Entity>Schema.partial().extend({
|
|
158
|
+
id: z.string().uuid(),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
export type List<Entity>Query = z.infer<typeof list<Entity>Schema>
|
|
162
|
+
export type Create<Entity>Input = z.infer<typeof create<Entity>Schema>
|
|
163
|
+
export type Update<Entity>Input = z.infer<typeof update<Entity>Schema>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Rules
|
|
167
|
+
|
|
168
|
+
- Derive TypeScript types from zod via `z.infer<typeof schema>` — never duplicate
|
|
169
|
+
- Create schema has all required fields; update schema is `.partial()` with required `id`
|
|
170
|
+
- Never include `organization_id`, `tenant_id`, `created_at`, `updated_at` — these are system-managed
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 5. Create API Routes
|
|
175
|
+
|
|
176
|
+
Use `makeCrudRoute` for standard CRUD. All HTTP methods live in a single `route.ts` file.
|
|
177
|
+
|
|
178
|
+
**File**: `src/modules/<module_id>/api/<entities>/route.ts`
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
182
|
+
import { <Entity> } from '../../data/entities'
|
|
183
|
+
import {
|
|
184
|
+
list<Entity>Schema,
|
|
185
|
+
create<Entity>Schema,
|
|
186
|
+
update<Entity>Schema,
|
|
187
|
+
} from '../../data/validators'
|
|
188
|
+
|
|
189
|
+
export const metadata = {
|
|
190
|
+
GET: { requireAuth: true, requireFeatures: ['<module_id>.<entity>.view'] },
|
|
191
|
+
POST: { requireAuth: true, requireFeatures: ['<module_id>.<entity>.manage'] },
|
|
192
|
+
PUT: { requireAuth: true, requireFeatures: ['<module_id>.<entity>.manage'] },
|
|
193
|
+
DELETE: { requireAuth: true, requireFeatures: ['<module_id>.<entity>.manage'] },
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const crud = makeCrudRoute({
|
|
197
|
+
metadata,
|
|
198
|
+
orm: {
|
|
199
|
+
entity: <Entity>,
|
|
200
|
+
idField: 'id',
|
|
201
|
+
orgField: 'organizationId',
|
|
202
|
+
tenantField: 'tenantId',
|
|
203
|
+
},
|
|
204
|
+
indexer: { entityType: '<module_id>.<entity>' },
|
|
205
|
+
list: {
|
|
206
|
+
schema: list<Entity>Schema,
|
|
207
|
+
entityId: '<module_id>.<entity>',
|
|
208
|
+
fields: ['id', 'name', 'organization_id', 'tenant_id', 'created_at', 'updated_at'],
|
|
209
|
+
},
|
|
210
|
+
create: { schema: create<Entity>Schema },
|
|
211
|
+
update: { schema: update<Entity>Schema },
|
|
212
|
+
del: {},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
export const { GET, POST, PUT, DELETE } = crud
|
|
216
|
+
|
|
217
|
+
export const openApi = {
|
|
218
|
+
summary: '<Entity> CRUD',
|
|
219
|
+
tags: ['<Module Name>'],
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Rules
|
|
224
|
+
|
|
225
|
+
- All HTTP methods MUST live in a single `api/<entities>/route.ts` file
|
|
226
|
+
- MUST export `metadata` — missing it silently breaks route-level auth guards
|
|
227
|
+
- MUST export `openApi` for documentation generation
|
|
228
|
+
- MUST use `makeCrudRoute` with `indexer: { entityType }` for query engine coverage
|
|
229
|
+
- Use `orm`, `list`, `create`, `update`, `del` keys — `entity`/`entityId`/`operations`/`schema` at root level are not valid
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 6. Create Backend Pages
|
|
234
|
+
|
|
235
|
+
Use `CrudForm` and `DataTable` from `@open-mercato/ui`. See the `om-backend-ui-design` skill for full component reference.
|
|
236
|
+
|
|
237
|
+
> **Optimistic locking (default ON).** `CrudForm` in edit mode auto-derives the expected-version header from `initialValues.updatedAt` and applies it to **both** save and delete — so pass the loaded record's `updatedAt` into `initialValues`. For custom (non-`CrudForm`) list-row deletes or dialog mutations, wrap the call with `withScopedApiRequestHeaders(buildOptimisticLockHeader(record.updatedAt), () => deleteCrud(...))` and surface the 409 with `surfaceRecordConflict(err, t)` from `@open-mercato/ui/backend/conflicts`. Never leave a mutating edit/delete UI without a version header — concurrent edits would silently overwrite.
|
|
238
|
+
|
|
239
|
+
### Page Metadata & Sidebar Navigation
|
|
240
|
+
|
|
241
|
+
**File**: `src/modules/<module_id>/backend/page.meta.ts`
|
|
242
|
+
|
|
243
|
+
Icons MUST use components from `lucide-react`. Never use inline `React.createElement('svg', ...)` — it breaks after `yarn generate`.
|
|
244
|
+
|
|
245
|
+
For full field reference, settings pages, and anti-patterns, see [references/navigation-patterns.md](references/navigation-patterns.md).
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
import { Trophy } from 'lucide-react'
|
|
249
|
+
|
|
250
|
+
export const metadata = {
|
|
251
|
+
requireAuth: true,
|
|
252
|
+
requireFeatures: ['<module_id>.view'],
|
|
253
|
+
pageTitle: '<Module Name>',
|
|
254
|
+
pageTitleKey: '<module_id>.nav.title',
|
|
255
|
+
pageGroup: '<Module Name>', // Sidebar section name
|
|
256
|
+
pageGroupKey: '<module_id>.nav.group', // i18n key — items with same key grouped together
|
|
257
|
+
pageOrder: 100, // Sort within group (lower = higher)
|
|
258
|
+
icon: <Trophy className="size-4" />,
|
|
259
|
+
breadcrumb: [{ label: '<Module Name>', labelKey: '<module_id>.nav.title' }],
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### List Page
|
|
264
|
+
|
|
265
|
+
**File**: `src/modules/<module_id>/backend/page.tsx`
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
'use client'
|
|
269
|
+
import * as React from 'react'
|
|
270
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
271
|
+
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
272
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
273
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
274
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
275
|
+
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
276
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
277
|
+
|
|
278
|
+
type <Entity> = { id: string; name: string; organizationId: string; tenantId: string }
|
|
279
|
+
|
|
280
|
+
type <Entity>ListResponse = {
|
|
281
|
+
items: <Entity>[]
|
|
282
|
+
total: number
|
|
283
|
+
page: number
|
|
284
|
+
pageSize: number
|
|
285
|
+
totalPages: number
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const PAGE_SIZE = 20
|
|
289
|
+
|
|
290
|
+
export default function <Module>ListPage() {
|
|
291
|
+
const t = useT()
|
|
292
|
+
const scopeVersion = useOrganizationScopeVersion()
|
|
293
|
+
const [rows, setRows] = React.useState<<Entity>[]>([])
|
|
294
|
+
const [page, setPage] = React.useState(1)
|
|
295
|
+
const [total, setTotal] = React.useState(0)
|
|
296
|
+
const [totalPages, setTotalPages] = React.useState(1)
|
|
297
|
+
const [isLoading, setIsLoading] = React.useState(true)
|
|
298
|
+
|
|
299
|
+
const columns = React.useMemo<ColumnDef<<Entity>>[]>(() => [
|
|
300
|
+
{ accessorKey: 'name', header: t('<module_id>.list.columns.name') },
|
|
301
|
+
], [t])
|
|
302
|
+
|
|
303
|
+
React.useEffect(() => {
|
|
304
|
+
let cancelled = false
|
|
305
|
+
async function load() {
|
|
306
|
+
setIsLoading(true)
|
|
307
|
+
try {
|
|
308
|
+
const params = new URLSearchParams()
|
|
309
|
+
params.set('page', String(page))
|
|
310
|
+
params.set('pageSize', String(PAGE_SIZE))
|
|
311
|
+
const fallback: <Entity>ListResponse = { items: [], total: 0, page, pageSize: PAGE_SIZE, totalPages: 1 }
|
|
312
|
+
const call = await apiCall<<Entity>ListResponse>(
|
|
313
|
+
`/api/<module_id>/<entities>?${params.toString()}`,
|
|
314
|
+
undefined,
|
|
315
|
+
{ fallback },
|
|
316
|
+
)
|
|
317
|
+
if (!call.ok) {
|
|
318
|
+
flash(t('<module_id>.list.error.loadFailed'), 'error')
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
const payload = call.result ?? fallback
|
|
322
|
+
if (!cancelled) {
|
|
323
|
+
setRows(Array.isArray(payload.items) ? payload.items : [])
|
|
324
|
+
setTotal(payload.total || 0)
|
|
325
|
+
setTotalPages(payload.totalPages || 1)
|
|
326
|
+
}
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (!cancelled) {
|
|
329
|
+
flash(err instanceof Error ? err.message : t('<module_id>.list.error.loadFailed'), 'error')
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
if (!cancelled) setIsLoading(false)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
load()
|
|
336
|
+
return () => { cancelled = true }
|
|
337
|
+
}, [page, scopeVersion, t])
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Page>
|
|
341
|
+
<PageBody>
|
|
342
|
+
<DataTable<<Entity>>
|
|
343
|
+
title={t('<module_id>.list.title')}
|
|
344
|
+
columns={columns}
|
|
345
|
+
data={rows}
|
|
346
|
+
isLoading={isLoading}
|
|
347
|
+
pagination={{ page, pageSize: PAGE_SIZE, total, totalPages, onPageChange: setPage }}
|
|
348
|
+
/>
|
|
349
|
+
</PageBody>
|
|
350
|
+
</Page>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export const metadata = {
|
|
355
|
+
requireAuth: true,
|
|
356
|
+
requireFeatures: ['<module_id>.<entity>.view'],
|
|
357
|
+
pageTitle: '<Module Name>',
|
|
358
|
+
pageTitleKey: '<module_id>.nav.title',
|
|
359
|
+
pageGroup: '<Module Name>',
|
|
360
|
+
pageGroupKey: '<module_id>.nav.group',
|
|
361
|
+
pageOrder: 100,
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Create Page
|
|
366
|
+
|
|
367
|
+
**File**: `src/modules/<module_id>/backend/<entities>/new.tsx`
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
'use client'
|
|
371
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
372
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
373
|
+
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
374
|
+
import { useRouter } from 'next/navigation'
|
|
375
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
376
|
+
|
|
377
|
+
type <Entity> = { id: string; name: string }
|
|
378
|
+
|
|
379
|
+
export default function Create<Entity>Page() {
|
|
380
|
+
const t = useT()
|
|
381
|
+
const router = useRouter()
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<Page>
|
|
385
|
+
<PageBody>
|
|
386
|
+
<CrudForm
|
|
387
|
+
title={t('<module_id>.create.title')}
|
|
388
|
+
backHref="/backend/<module_id>"
|
|
389
|
+
fields={[
|
|
390
|
+
{ id: 'name', label: t('<module_id>.fields.name'), type: 'text', required: true },
|
|
391
|
+
]}
|
|
392
|
+
onSubmit={async (values) => {
|
|
393
|
+
const { result } = await createCrud<<Entity>>('<module_id>/<entities>', values)
|
|
394
|
+
router.push(`/backend/<module_id>/<entities>/${result.id}`)
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
</PageBody>
|
|
398
|
+
</Page>
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export const metadata = {
|
|
403
|
+
requireAuth: true,
|
|
404
|
+
requireFeatures: ['<module_id>.<entity>.manage'],
|
|
405
|
+
pageTitle: 'Create <Entity>',
|
|
406
|
+
pageTitleKey: '<module_id>.create.title',
|
|
407
|
+
pageGroup: '<Module Name>',
|
|
408
|
+
pageGroupKey: '<module_id>.nav.group',
|
|
409
|
+
navHidden: true,
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Edit Page
|
|
414
|
+
|
|
415
|
+
**File**: `src/modules/<module_id>/backend/<entities>/[id].tsx`
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
'use client'
|
|
419
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
420
|
+
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
|
|
421
|
+
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
422
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
423
|
+
import { useQuery } from '@tanstack/react-query'
|
|
424
|
+
import { useRouter } from 'next/navigation'
|
|
425
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
426
|
+
|
|
427
|
+
type <Entity> = { id: string; name: string }
|
|
428
|
+
type <Entity>DetailResponse = { items: <Entity>[]; total: number; page: number; pageSize: number; totalPages: number }
|
|
429
|
+
|
|
430
|
+
export default function Edit<Entity>Page({ params }: { params: { id: string } }) {
|
|
431
|
+
const t = useT()
|
|
432
|
+
const router = useRouter()
|
|
433
|
+
const { data: response, isLoading } = useQuery({
|
|
434
|
+
queryKey: ['<module_id>', '<entities>', params.id],
|
|
435
|
+
queryFn: () => apiCall<<Entity>DetailResponse>(`<module_id>/<entities>?id=${params.id}`),
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<Page>
|
|
440
|
+
<PageBody>
|
|
441
|
+
<CrudForm
|
|
442
|
+
title={t('<module_id>.edit.title')}
|
|
443
|
+
backHref="/backend/<module_id>"
|
|
444
|
+
fields={[
|
|
445
|
+
{ id: 'name', label: t('<module_id>.fields.name'), type: 'text', required: true },
|
|
446
|
+
]}
|
|
447
|
+
isLoading={isLoading}
|
|
448
|
+
initialValues={response?.items?.[0] ?? undefined}
|
|
449
|
+
onSubmit={async (values) => {
|
|
450
|
+
await updateCrud('<module_id>/<entities>', { id: params.id, ...values })
|
|
451
|
+
router.push('/backend/<module_id>')
|
|
452
|
+
}}
|
|
453
|
+
onDelete={async () => {
|
|
454
|
+
await deleteCrud('<module_id>/<entities>', params.id)
|
|
455
|
+
router.push('/backend/<module_id>')
|
|
456
|
+
}}
|
|
457
|
+
/>
|
|
458
|
+
</PageBody>
|
|
459
|
+
</Page>
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export const metadata = {
|
|
464
|
+
requireAuth: true,
|
|
465
|
+
requireFeatures: ['<module_id>.<entity>.manage'],
|
|
466
|
+
pageTitle: 'Edit <Entity>',
|
|
467
|
+
pageTitleKey: '<module_id>.edit.title',
|
|
468
|
+
pageGroup: '<Module Name>',
|
|
469
|
+
pageGroupKey: '<module_id>.nav.group',
|
|
470
|
+
navHidden: true,
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
16
475
|
|
|
17
|
-
|
|
476
|
+
## 7. Add Module Metadata
|
|
18
477
|
|
|
19
|
-
|
|
478
|
+
**File**: `src/modules/<module_id>/index.ts`
|
|
20
479
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
480
|
+
```typescript
|
|
481
|
+
import type { ModuleInfo } from '@open-mercato/shared/modules/registry'
|
|
482
|
+
|
|
483
|
+
export const metadata: ModuleInfo = {
|
|
484
|
+
name: '<module_id>',
|
|
485
|
+
title: '<Module Name>',
|
|
486
|
+
version: '0.1.0',
|
|
487
|
+
description: '<What this module does>',
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export { features } from './acl'
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 8. Add ACL & Setup
|
|
496
|
+
|
|
497
|
+
### ACL Features
|
|
498
|
+
|
|
499
|
+
**File**: `src/modules/<module_id>/acl.ts`
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
export const features = [
|
|
503
|
+
{ id: '<module_id>.<entity>.view', title: 'View <entities>', module: '<module_id>' },
|
|
504
|
+
{ id: '<module_id>.<entity>.manage', title: 'Manage <entities>', module: '<module_id>' },
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
export default features
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Setup (Tenant Init + Default Roles)
|
|
511
|
+
|
|
512
|
+
**File**: `src/modules/<module_id>/setup.ts`
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
516
|
+
|
|
517
|
+
export const setup: ModuleSetupConfig = {
|
|
518
|
+
defaultRoleFeatures: {
|
|
519
|
+
superadmin: ['<module_id>.<entity>.view', '<module_id>.<entity>.manage'],
|
|
520
|
+
admin: ['<module_id>.<entity>.view', '<module_id>.<entity>.manage'],
|
|
521
|
+
user: ['<module_id>.<entity>.view'],
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export default setup
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Rules
|
|
529
|
+
|
|
530
|
+
- Feature IDs follow `<module_id>.<entity>.<action>` (view / manage per entity, not global create/update/delete)
|
|
531
|
+
- Add `export default features` — the generator reads `.default ?? .features` with an empty fallback, so the named export alone works, but adding the default export ensures both import styles resolve cleanly
|
|
532
|
+
- MUST declare `defaultRoleFeatures` for every feature in `acl.ts`
|
|
533
|
+
- Feature IDs are FROZEN once deployed — cannot rename without data migration
|
|
534
|
+
- After adding features run `yarn mercato auth sync-role-acls` so existing tenants receive the grants
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## 9. Add DI Registration
|
|
539
|
+
|
|
540
|
+
**File**: `src/modules/<module_id>/di.ts`
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
544
|
+
|
|
545
|
+
export function register(container: AppContainer): void {
|
|
546
|
+
// Register module services here using Awilix
|
|
547
|
+
// Example:
|
|
548
|
+
// import { asFunction } from 'awilix'
|
|
549
|
+
// container.register({
|
|
550
|
+
// <module_id>Service: asFunction(createService).scoped(),
|
|
551
|
+
// })
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## 10. Add Events
|
|
558
|
+
|
|
559
|
+
**File**: `src/modules/<module_id>/events.ts`
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
563
|
+
|
|
564
|
+
const events = [
|
|
565
|
+
{ id: '<module_id>.<entity>.created', label: '<Entity> Created', entity: '<entity>', category: 'crud' as const },
|
|
566
|
+
{ id: '<module_id>.<entity>.updated', label: '<Entity> Updated', entity: '<entity>', category: 'crud' as const },
|
|
567
|
+
{ id: '<module_id>.<entity>.deleted', label: '<Entity> Deleted', entity: '<entity>', category: 'crud' as const },
|
|
568
|
+
] as const
|
|
569
|
+
|
|
570
|
+
export const eventsConfig = createModuleEvents({ moduleId: '<module_id>', events })
|
|
571
|
+
export const emit<Module>Event = eventsConfig.emit
|
|
572
|
+
export type <Module>EventId = typeof events[number]['id']
|
|
573
|
+
export default eventsConfig
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Event Rules
|
|
577
|
+
|
|
578
|
+
- `createModuleEvents` takes `{ moduleId, events }` — NOT a flat keyed object. Using the old keyed-object shape crashes `/login` at startup because the generated events registry cannot read the module
|
|
579
|
+
- Event IDs: `module.entity.action` (singular entity, past tense action, dots as separators)
|
|
580
|
+
- Declare `label`, `entity`, and `category` on each event — they populate the workflow trigger UI
|
|
581
|
+
- Add `clientBroadcast: true` to an event definition to bridge it to the browser via SSE
|
|
582
|
+
- Event ID contracts are FROZEN once deployed — adding new events is safe; renaming or removing is a breaking change
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## 11. Optional Features
|
|
587
|
+
|
|
588
|
+
### Search Configuration
|
|
589
|
+
|
|
590
|
+
**File**: `src/modules/<module_id>/search.ts`
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import type { SearchModuleConfig } from '@open-mercato/shared/modules/search'
|
|
594
|
+
|
|
595
|
+
export const searchConfig: SearchModuleConfig = {
|
|
596
|
+
entities: {
|
|
597
|
+
'<module_id>.<entity>': {
|
|
598
|
+
fields: ['name'], // Fields to index for fulltext search
|
|
599
|
+
// Additional search config as needed
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Translations
|
|
606
|
+
|
|
607
|
+
**File**: `src/modules/<module_id>/translations.ts`
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
export const translatableFields = {
|
|
611
|
+
'<entity>': ['name', 'description'], // Fields that support i18n
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### CLI Commands
|
|
616
|
+
|
|
617
|
+
**File**: `src/modules/<module_id>/cli.ts`
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
export default function registerCli(program: any) {
|
|
621
|
+
program
|
|
622
|
+
.command('<module_id>:seed')
|
|
623
|
+
.description('Seed sample <entities>')
|
|
624
|
+
.action(async () => {
|
|
625
|
+
// Implementation
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Response Enrichers
|
|
631
|
+
|
|
632
|
+
Use enrichers to add computed fields to another module's API responses without coupling the modules.
|
|
633
|
+
|
|
634
|
+
**File**: `src/modules/<module_id>/data/enrichers.ts`
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
import type { ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
|
|
638
|
+
|
|
639
|
+
const <entity>Enricher: ResponseEnricher = {
|
|
640
|
+
id: '<module_id>.<entity>-enricher',
|
|
641
|
+
targetEntity: '<other_module>.<entity>',
|
|
642
|
+
features: ['<module_id>.<entity>.view'],
|
|
643
|
+
timeout: 2000,
|
|
644
|
+
fallback: { _<module_id>: {} },
|
|
645
|
+
async enrichOne(record, context) {
|
|
646
|
+
return { ...record, _<module_id>: { /* computed fields */ } }
|
|
647
|
+
},
|
|
648
|
+
async enrichMany(records, context) {
|
|
649
|
+
return records.map(r => ({ ...r, _<module_id>: { /* computed fields */ } }))
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export const enrichers: ResponseEnricher[] = [<entity>Enricher]
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Rules:**
|
|
657
|
+
- MUST implement `enrichOne` (required by the `ResponseEnricher` interface)
|
|
658
|
+
- MUST implement `enrichMany` for list endpoints to prevent N+1 queries
|
|
659
|
+
- Namespace enriched fields with `_<module_id>` prefix
|
|
660
|
+
- The target route must opt in: `makeCrudRoute({ ..., enrichers: { entityId: '<other_module>.<entity>' } })`
|
|
661
|
+
- Run `yarn generate` after adding `data/enrichers.ts`
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
### Encryption maps (sensitive / GDPR-relevant fields)
|
|
666
|
+
|
|
667
|
+
**Mandatory** when the entity stores PII, contact info, addresses, free-text notes about people, integration credentials, secrets, or anything subject to a data-processing agreement. Do NOT hand-roll AES, KMS calls, or "TODO encrypt later" stubs — the framework provides per-tenant DEKs and a declarative field-level map.
|
|
668
|
+
|
|
669
|
+
**File**: `src/modules/<module_id>/encryption.ts`
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
|
|
673
|
+
|
|
674
|
+
export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
|
|
675
|
+
{
|
|
676
|
+
entityId: '<module_id>:<entity>', // matches data/entities.ts table id, colon-separated
|
|
677
|
+
fields: [
|
|
678
|
+
{ field: 'first_name' },
|
|
679
|
+
{ field: 'last_name' },
|
|
680
|
+
{ field: 'phone' },
|
|
681
|
+
// Add a hashField for deterministic equality lookups (e.g. login by email):
|
|
682
|
+
{ field: 'email', hashField: 'email_hash' },
|
|
683
|
+
],
|
|
684
|
+
},
|
|
685
|
+
]
|
|
686
|
+
|
|
687
|
+
export default defaultEncryptionMaps
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Read paths** — never `em.find` an encrypted column directly:
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
694
|
+
|
|
695
|
+
// Signature: (em, entityName, where, options?, scope?) — MikroORM FindOptions in slot 4
|
|
696
|
+
// (pass `undefined` when none), decryption scope in slot 5.
|
|
697
|
+
const records = await findWithDecryption(em, '<Entity>', filter, undefined, { tenantId, organizationId })
|
|
698
|
+
const single = await findOneWithDecryption(em, '<Entity>', { id }, undefined, { tenantId, organizationId })
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
**Apply to existing tenants** after declaring or updating maps:
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
yarn mercato entities seed-encryption --tenant <tenantId> [--organization <orgId>]
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
New tenants pick up `defaultEncryptionMaps` automatically during `auth:setup`. Toggling the **Encrypted** flag for a field only applies to data written **after** the change — historical plaintext rows stay as they were until backfilled via `yarn mercato entities rotate-encryption-key --tenant <tenantId> --org <organizationId>` (without `--old-key` the command only encrypts plaintext and skips already-encrypted fields). Use `yarn mercato entities decrypt-database` to roll back. For end-to-end usage and admin UI flows see <https://docs.open-mercato.dev/user-guide/encryption>.
|
|
708
|
+
|
|
709
|
+
> Tip: when `email` (or any other column) needs deterministic lookups while encrypted, declare a sibling `hashField` in the map and add a matching `varchar` column to the entity. The framework keeps the hash in sync on writes; queries can target the hash instead of the cleartext column.
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 12. Wire & Verify
|
|
714
|
+
|
|
715
|
+
### Step 1: Register in modules.ts
|
|
716
|
+
|
|
717
|
+
Add to `src/modules.ts`:
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
{ id: '<module_id>', from: '@app' },
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Step 2: Run Generators
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
yarn generate # Discover module files, update .mercato/generated/
|
|
727
|
+
yarn db:generate # Probe/create migration for the new entity
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Step 3: Review Migration
|
|
731
|
+
|
|
732
|
+
Check the generated migration file in `src/modules/<module_id>/migrations/`. Verify:
|
|
733
|
+
- Table name is correct (plural, snake_case)
|
|
734
|
+
- All columns present with correct types
|
|
735
|
+
- Indexes on `organization_id`, `tenant_id`
|
|
736
|
+
- No unexpected changes
|
|
737
|
+
- `migrations/.snapshot-open-mercato.json` was updated to the post-change schema
|
|
738
|
+
- Unrelated generated migrations were deleted from the diff
|
|
739
|
+
|
|
740
|
+
### Step 4: Apply & Test
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
yarn db:migrate # Apply migration only after explicit user confirmation
|
|
744
|
+
yarn dev # Start dev server
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Step 5: Run Post-Scaffold Validation Gate
|
|
748
|
+
|
|
749
|
+
After every structural module change, run **in order** before committing:
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# 1. Re-emit generated registries with the new module
|
|
753
|
+
yarn generate
|
|
754
|
+
|
|
755
|
+
# 2. Purge stale structural cache (nav, module-graph fingerprints)
|
|
756
|
+
yarn mercato configs cache structural --all-tenants
|
|
757
|
+
|
|
758
|
+
# 3. Grant ACL features declared in acl.ts to existing roles
|
|
759
|
+
yarn mercato auth sync-role-acls
|
|
760
|
+
|
|
761
|
+
# 4. Type-check all files — catches API mismatches before they reach runtime
|
|
762
|
+
yarn typecheck
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
> **Why this matters**: A malformed `events.ts` (for example, using the old keyed-object shape for `createModuleEvents`) will crash `/login` and every other page because generated registries import all active module files at startup. A bad scaffold can make the whole admin inaccessible. Running `yarn typecheck` after `yarn generate` catches this before it ships.
|
|
766
|
+
|
|
767
|
+
### Step 6: Verify
|
|
768
|
+
|
|
769
|
+
- [ ] Module appears in admin sidebar (if menu item added)
|
|
770
|
+
- [ ] List page loads at `/backend/<module_id>`
|
|
771
|
+
- [ ] Create form works at `/backend/<module_id>/<entities>/new`
|
|
772
|
+
- [ ] Edit form loads existing record
|
|
773
|
+
- [ ] Delete works from list page
|
|
774
|
+
- [ ] ACL features appear in role management
|
|
775
|
+
- [ ] `/login` still loads after structural changes
|
|
776
|
+
|
|
777
|
+
### Self-Review Checklist
|
|
778
|
+
|
|
779
|
+
- [ ] Module ID is plural, snake_case
|
|
780
|
+
- [ ] Entity class has `organization_id`, `tenant_id`, standard columns
|
|
781
|
+
- [ ] Validators use zod with `z.infer` for types
|
|
782
|
+
- [ ] API routes live in `api/<entities>/route.ts` (not `api/get/`, `api/post/`, etc.)
|
|
783
|
+
- [ ] `makeCrudRoute` uses `{ metadata, orm, list, create, update, del }` — not `{ entity, entityId, operations, schema }`
|
|
784
|
+
- [ ] API route exports `metadata`, named `{ GET, POST, PUT, DELETE }`, and `openApi`
|
|
785
|
+
- [ ] `DataTable` receives explicit `data`, `isLoading`, `error`, `pagination` — not `apiPath` or `createHref`
|
|
786
|
+
- [ ] `CrudForm` uses `onSubmit` with `createCrud`/`updateCrud` and `onDelete` with `deleteCrud` — not `apiPath`, `mode`, or `resourceId`
|
|
787
|
+
- [ ] `events.ts` uses `createModuleEvents({ moduleId, events: [...] })` array shape — not a keyed object
|
|
788
|
+
- [ ] `events.ts` has `export default eventsConfig`
|
|
789
|
+
- [ ] `acl.ts` exports `features` (named export is sufficient; default export is recommended for broad import compatibility)
|
|
790
|
+
- [ ] ACL feature IDs use `<module>.<entity>.view` / `<module>.<entity>.manage` pattern
|
|
791
|
+
- [ ] `setup.ts` grants every feature in `acl.ts` to at least `admin` and `superadmin`
|
|
792
|
+
- [ ] Sidebar icon uses `lucide-react` component (not inline SVG / `React.createElement`)
|
|
793
|
+
- [ ] `page.meta.ts` includes `pageGroup` + `pageGroupKey` for sidebar grouping
|
|
794
|
+
- [ ] `page.meta.ts` includes `pageOrder` for sort position
|
|
795
|
+
- [ ] All related pages share the same `pageGroupKey`
|
|
796
|
+
- [ ] Settings pages (if any) have `pageContext: 'settings' as const` and `navHidden: true`
|
|
797
|
+
- [ ] Module registered in `src/modules.ts` with `from: '@app'`
|
|
798
|
+
- [ ] Post-scaffold gate run: `yarn generate` → `yarn mercato configs cache structural --all-tenants` → `yarn mercato auth sync-role-acls` → `yarn typecheck`
|
|
799
|
+
- [ ] Migration SQL is scoped to this entity and `.snapshot-open-mercato.json` is updated
|
|
800
|
+
- [ ] No `any` types
|
|
801
|
+
- [ ] No hardcoded user-facing strings
|
|
802
|
+
- [ ] No direct ORM relationships to other modules
|
|
803
|
+
- [ ] `/login` still loads after all changes
|
|
804
|
+
|
|
805
|
+
---
|
|
28
806
|
|
|
29
807
|
## Rules
|
|
30
808
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
809
|
+
- **MUST** use plural, snake_case for module ID and folder name
|
|
810
|
+
- **MUST** include `organization_id` and `tenant_id` on all tenant-scoped entities
|
|
811
|
+
- **MUST** include standard columns (`id`, `created_at`, `updated_at`, `deleted_at`, `is_active`)
|
|
812
|
+
- **MUST** validate all inputs with zod schemas in `data/validators.ts`
|
|
813
|
+
- **MUST** place all HTTP method handlers in a single `api/<entities>/route.ts` — not separate `api/get/`, `api/post/` files
|
|
814
|
+
- **MUST** use `makeCrudRoute` with `{ metadata, orm, list, create, update, del }` — not `{ entity, entityId, operations, schema }`
|
|
815
|
+
- **MUST** export `metadata`, named method handlers `{ GET, POST, PUT, DELETE }`, and `openApi` from every route file
|
|
816
|
+
- **MUST** use `CrudForm` with explicit `onSubmit` / `onDelete` handlers — not `apiPath`, `mode`, or `resourceId` props
|
|
817
|
+
- **MUST** use `DataTable` with explicit `data`, `isLoading`, `error`, `pagination` — not `apiPath`, `createHref`, or `extensionTableId`
|
|
818
|
+
- **MUST** use `createModuleEvents({ moduleId, events: [...] })` array shape — NEVER the old keyed-object `{ 'id': { description, payload } }` shape
|
|
819
|
+
- **MUST** add `export default eventsConfig` in `events.ts`
|
|
820
|
+
- **MUST** export `features` from `acl.ts` (named export is sufficient; adding `export default features` is recommended for broad import compatibility)
|
|
821
|
+
- **MUST** use `<module>.<entity>.view` / `<module>.<entity>.manage` feature ID pattern
|
|
822
|
+
- **MUST** include `pageGroup` and `pageGroupKey` on list/root backend pages for sidebar grouping
|
|
823
|
+
- **MUST** use `as const` on `pageContext` values (e.g., `pageContext: 'settings' as const`)
|
|
824
|
+
- **MUST** declare ACL features and wire them in `setup.ts` `defaultRoleFeatures`
|
|
825
|
+
- **MUST** register module in `src/modules.ts` with `from: '@app'`
|
|
826
|
+
- **MUST** run the post-scaffold validation gate after creating module files: `yarn generate` → `yarn mercato configs cache structural --all-tenants` → `yarn mercato auth sync-role-acls` → `yarn typecheck`
|
|
827
|
+
- **MUST** verify `/login` still loads after every structural change
|
|
828
|
+
- **MUST** create or keep a scoped migration after creating/modifying entities and update `.snapshot-open-mercato.json`
|
|
829
|
+
- **MUST NOT** commit unrelated migrations emitted by `yarn db:generate`
|
|
830
|
+
- **MUST NOT** run `yarn db:migrate` without explicit user confirmation
|
|
831
|
+
- **MUST NOT** create ORM relationships (`@ManyToOne`, `@OneToMany`) to entities in other modules
|
|
832
|
+
- **MUST NOT** edit `.mercato/generated/*` files manually
|
|
833
|
+
- **MUST** declare `<module>/encryption.ts` exporting `defaultEncryptionMaps` whenever the entity stores sensitive / GDPR-relevant fields (PII, contact info, addresses, free-text notes about people, integration credentials, secrets) — and read those columns via `findWithDecryption` / `findOneWithDecryption`
|
|
834
|
+
- **MUST NOT** hand-roll AES/KMS calls or store "we'll encrypt this later" plaintext for sensitive columns — use the encryption-maps mechanism described in section 11 → Encryption maps
|