@open-mercato/core 0.4.7-develop-74069040de → 0.4.7-develop-7116871242
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/agentic/standalone-guide.md +251 -0
- package/package.json +3 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Core Package — Standalone Developer Guide
|
|
2
|
+
|
|
3
|
+
`@open-mercato/core` contains all built-in business modules. This guide covers module development patterns for standalone apps that build on top of these modules.
|
|
4
|
+
|
|
5
|
+
## Auto-Discovery Paths
|
|
6
|
+
|
|
7
|
+
Place files in your module directory (`src/modules/<module>/`) — the framework discovers them automatically:
|
|
8
|
+
|
|
9
|
+
| Path Pattern | Becomes |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `frontend/<path>.tsx` | `/<path>` (public page) |
|
|
12
|
+
| `backend/<path>.tsx` | `/backend/<path>` (admin page) |
|
|
13
|
+
| `backend/page.tsx` | `/backend/<module>` (module root page) |
|
|
14
|
+
| `api/<method>/<path>.ts` | `/api/<path>` dispatched by HTTP method |
|
|
15
|
+
| `subscribers/*.ts` | Event subscriber (export `metadata` + default handler) |
|
|
16
|
+
| `workers/*.ts` | Background worker (export `metadata` + default handler) |
|
|
17
|
+
|
|
18
|
+
Run `yarn generate` after adding any auto-discovered file.
|
|
19
|
+
|
|
20
|
+
## Module Files Reference
|
|
21
|
+
|
|
22
|
+
| File | Export | Purpose |
|
|
23
|
+
|------|--------|---------|
|
|
24
|
+
| `index.ts` | `metadata` | Module metadata |
|
|
25
|
+
| `di.ts` | `register(container)` | DI registrations (Awilix) |
|
|
26
|
+
| `acl.ts` | `features` | Permission features: `['mod.view', 'mod.create', ...]` |
|
|
27
|
+
| `setup.ts` | `setup: ModuleSetupConfig` | Tenant init, role features, seed data |
|
|
28
|
+
| `ce.ts` | `entities` | Custom entities / custom field sets |
|
|
29
|
+
| `events.ts` | `eventsConfig` | Typed event declarations |
|
|
30
|
+
| `search.ts` | `searchConfig` | Search indexing config |
|
|
31
|
+
| `translations.ts` | `translatableFields` | Translatable fields per entity |
|
|
32
|
+
| `notifications.ts` | `notificationTypes` | Notification type definitions |
|
|
33
|
+
| `notifications.client.ts` | — | Client-side notification renderers |
|
|
34
|
+
| `notifications.handlers.ts` | `notificationHandlers` | Reactive notification side-effects |
|
|
35
|
+
| `data/entities.ts` | — | MikroORM entity classes |
|
|
36
|
+
| `data/validators.ts` | — | Zod validation schemas |
|
|
37
|
+
| `data/extensions.ts` | `extensions` | Entity extensions (cross-module links) |
|
|
38
|
+
| `data/enrichers.ts` | `enrichers` | Response enrichers |
|
|
39
|
+
| `api/interceptors.ts` | `interceptors` | API route interception hooks |
|
|
40
|
+
| `widgets/injection/` | — | Injected UI widgets |
|
|
41
|
+
| `widgets/injection-table.ts` | — | Widget-to-slot mappings |
|
|
42
|
+
| `widgets/components.ts` | `componentOverrides` | Component replacement/wrapper definitions |
|
|
43
|
+
|
|
44
|
+
## API Routes
|
|
45
|
+
|
|
46
|
+
Every API route file MUST export an `openApi` object:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createCrudOpenApiFactory } from '@open-mercato/shared/lib/openapi/crud'
|
|
50
|
+
const buildOpenApi = createCrudOpenApiFactory({ defaultTag: 'MyModule' })
|
|
51
|
+
|
|
52
|
+
export const openApi = buildOpenApi({
|
|
53
|
+
resourceName: 'Item',
|
|
54
|
+
querySchema: listQuerySchema,
|
|
55
|
+
listResponseSchema: createPagedListResponseSchema(itemSchema),
|
|
56
|
+
create: { schema: createSchema, description: 'Create item' },
|
|
57
|
+
update: { schema: updateSchema, responseSchema: okSchema },
|
|
58
|
+
del: { schema: deleteSchema, responseSchema: okSchema },
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### CRUD Routes with makeCrudRoute
|
|
63
|
+
|
|
64
|
+
Always set `indexer: { entityType }` for query index coverage:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
makeCrudRoute({
|
|
68
|
+
entity: MyEntity,
|
|
69
|
+
indexer: { entityType: 'my_module:my_entity' },
|
|
70
|
+
enrichers: { entityId: 'my_module.my_entity' }, // opt-in to enrichers
|
|
71
|
+
// ...
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Custom Write Routes
|
|
76
|
+
|
|
77
|
+
For non-CRUD write routes (`POST`/`PUT`/`PATCH`/`DELETE`), MUST wire mutation guards:
|
|
78
|
+
- Call `validateCrudMutationGuard` before mutation
|
|
79
|
+
- Call `runCrudMutationGuardAfterSuccess` after successful mutation
|
|
80
|
+
|
|
81
|
+
## Module Setup (`setup.ts`)
|
|
82
|
+
|
|
83
|
+
Required for tenant initialization:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
87
|
+
|
|
88
|
+
export const setup: ModuleSetupConfig = {
|
|
89
|
+
defaultRoleFeatures: {
|
|
90
|
+
superadmin: ['my_module.admin_only'],
|
|
91
|
+
admin: ['my_module.*'],
|
|
92
|
+
employee: ['my_module.view'],
|
|
93
|
+
},
|
|
94
|
+
async onTenantCreated({ em, tenantId, organizationId }) { /* settings, sequences */ },
|
|
95
|
+
async seedDefaults({ em, tenantId, container }) { /* dictionaries, statuses */ },
|
|
96
|
+
async seedExamples({ em, tenantId, container }) { /* demo data */ },
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Events
|
|
101
|
+
|
|
102
|
+
Declare events in the emitting module's `events.ts`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
106
|
+
const events = [
|
|
107
|
+
{ id: 'my_mod.item.created', label: 'Item Created', entity: 'item', category: 'crud' },
|
|
108
|
+
{ id: 'my_mod.item.updated', label: 'Item Updated', entity: 'item', category: 'crud' },
|
|
109
|
+
{ id: 'my_mod.item.deleted', label: 'Item Deleted', entity: 'item', category: 'crud' },
|
|
110
|
+
] as const
|
|
111
|
+
export const eventsConfig = createModuleEvents({ moduleId: 'my_mod', events })
|
|
112
|
+
export const emitMyModEvent = eventsConfig.emit
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
MUST use `as const` for type safety. Run `yarn generate` after adding.
|
|
116
|
+
|
|
117
|
+
### Subscribers
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// subscribers/item-created-notify.ts
|
|
121
|
+
export const metadata = { event: 'my_mod.item.created', persistent: true, id: 'item-created-notify' }
|
|
122
|
+
export default async function handler(payload, ctx) { /* one side effect per subscriber */ }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Widget Injection
|
|
126
|
+
|
|
127
|
+
The preferred way to extend other modules' UI without direct coupling.
|
|
128
|
+
|
|
129
|
+
### Structure
|
|
130
|
+
- Widgets: `widgets/injection/<WidgetName>/widget.tsx` (or `widget.ts` for headless)
|
|
131
|
+
- Mapping: `widgets/injection-table.ts`
|
|
132
|
+
|
|
133
|
+
### Spot IDs
|
|
134
|
+
- `crud-form:<entityId>` — inject into forms
|
|
135
|
+
- `crud-form:<entityId>:fields` — inject form fields
|
|
136
|
+
- `data-table:<tableId>:columns|row-actions|bulk-actions|filters` — inject into tables
|
|
137
|
+
- `menu:sidebar:main|settings|profile` — sidebar menu items
|
|
138
|
+
- `menu:topbar:profile-dropdown|actions` — topbar items
|
|
139
|
+
|
|
140
|
+
### Menu Injection (Headless)
|
|
141
|
+
```typescript
|
|
142
|
+
// widgets/injection/MyMenuItem/widget.ts
|
|
143
|
+
export const menuItems = [
|
|
144
|
+
{ id: 'my-mod-dashboard', labelKey: 'my_mod.menu.dashboard', icon: 'lucide:layout-dashboard',
|
|
145
|
+
href: '/backend/my-module', placement: { position: InjectionPosition.After, relativeTo: 'customers' } }
|
|
146
|
+
]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Map in `injection-table.ts`:
|
|
150
|
+
```typescript
|
|
151
|
+
export default [{ widgetId: 'MyMenuItem', spots: ['menu:sidebar:main'] }]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Response Enrichers
|
|
155
|
+
|
|
156
|
+
Add computed fields to another module's CRUD responses:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// data/enrichers.ts
|
|
160
|
+
export const enrichers: ResponseEnricher[] = [{
|
|
161
|
+
id: 'my_mod.customer-stats',
|
|
162
|
+
targetEntity: 'customers.person',
|
|
163
|
+
features: ['my_mod.view'],
|
|
164
|
+
priority: 10,
|
|
165
|
+
timeout: 2000,
|
|
166
|
+
fallback: { _my_mod: { count: 0 } },
|
|
167
|
+
async enrichMany(records, ctx) {
|
|
168
|
+
return records.map(r => ({ ...r, _my_mod: { count: 42 } }))
|
|
169
|
+
},
|
|
170
|
+
}]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
MUST implement `enrichMany()` for batch endpoints. MUST namespace with `_moduleName` prefix.
|
|
174
|
+
|
|
175
|
+
## API Interceptors
|
|
176
|
+
|
|
177
|
+
Hook into any route's before/after lifecycle:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// api/interceptors.ts
|
|
181
|
+
export const interceptors: ApiInterceptor[] = [{
|
|
182
|
+
id: 'my_mod.narrow-customers',
|
|
183
|
+
targetRoute: '/api/customers/people',
|
|
184
|
+
methods: ['GET'],
|
|
185
|
+
async before(ctx) { /* rewrite query.ids to narrow results */ },
|
|
186
|
+
async after(ctx, response) { /* transform response */ },
|
|
187
|
+
}]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Access Control (RBAC)
|
|
191
|
+
|
|
192
|
+
Declare features in `acl.ts`, guard with metadata:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// acl.ts
|
|
196
|
+
export const features = ['my_module.view', 'my_module.create', 'my_module.edit', 'my_module.delete']
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Always add matching `defaultRoleFeatures` in `setup.ts`.
|
|
200
|
+
|
|
201
|
+
Use declarative guards in page metadata: `requireAuth`, `requireRoles`, `requireFeatures`.
|
|
202
|
+
|
|
203
|
+
## Custom Fields & Entities
|
|
204
|
+
|
|
205
|
+
Declare in `ce.ts` using DSL helpers:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { defineFields, cf } from '@open-mercato/shared/modules/dsl'
|
|
209
|
+
export const entities = [{
|
|
210
|
+
entityId: 'my_module:my_entity',
|
|
211
|
+
fields: defineFields({ fields: [cf.text('notes'), cf.number('priority')] }),
|
|
212
|
+
}]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Entity Extensions (Cross-Module Links)
|
|
216
|
+
|
|
217
|
+
Extend another module's data without mutating their entities:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// data/extensions.ts
|
|
221
|
+
import { defineLink, entityId, linkable } from '@open-mercato/shared/modules/dsl'
|
|
222
|
+
export const extensions = [
|
|
223
|
+
defineLink({ source: entityId('my_module:my_entity'), target: linkable('customers:person') })
|
|
224
|
+
]
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Component Replacement
|
|
228
|
+
|
|
229
|
+
Override or wrap existing UI components:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// widgets/components.ts
|
|
233
|
+
export const componentOverrides = [{
|
|
234
|
+
handle: 'page:customers:detail',
|
|
235
|
+
mode: 'wrapper', // or 'replace', 'props'
|
|
236
|
+
component: MyCustomerDetailWrapper,
|
|
237
|
+
}]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Prefer `wrapper`/`props` modes over full `replace`.
|
|
241
|
+
|
|
242
|
+
## Command Pattern (Write Operations)
|
|
243
|
+
|
|
244
|
+
Implement writes via commands for undo/redo support:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
248
|
+
// Reference: @open-mercato/core customers/commands/people.ts
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Include `indexer: { entityType, cacheAliases }` in `emitCrudSideEffects` for query index refresh.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.7-develop-
|
|
3
|
+
"version": "0.4.7-develop-7116871242",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -217,10 +217,10 @@
|
|
|
217
217
|
"semver": "^7.6.3"
|
|
218
218
|
},
|
|
219
219
|
"peerDependencies": {
|
|
220
|
-
"@open-mercato/shared": "0.4.7-develop-
|
|
220
|
+
"@open-mercato/shared": "0.4.7-develop-7116871242"
|
|
221
221
|
},
|
|
222
222
|
"devDependencies": {
|
|
223
|
-
"@open-mercato/shared": "0.4.7-develop-
|
|
223
|
+
"@open-mercato/shared": "0.4.7-develop-7116871242",
|
|
224
224
|
"@testing-library/dom": "^10.4.1",
|
|
225
225
|
"@testing-library/jest-dom": "^6.9.1",
|
|
226
226
|
"@testing-library/react": "^16.3.1",
|