create-mercato-app 0.4.6-develop-af28b566dd → 0.4.6-develop-4d77832982

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/template/src/app/(backend)/backend/[...slug]/page.tsx +6 -2
  3. package/template/src/app/(backend)/backend/__tests__/backend-require-features.test.tsx +108 -0
  4. package/template/src/app/(backend)/backend/layout.tsx +30 -27
  5. package/template/src/bootstrap.ts +4 -0
  6. package/template/src/components/ClientBootstrap.tsx +2 -5
  7. package/template/src/components/ComponentOverridesBootstrap.tsx +20 -0
  8. package/template/src/modules/example/__integration__/TC-UMES-004.spec.ts +337 -0
  9. package/template/src/modules/example/__integration__/meta.ts +3 -0
  10. package/template/src/modules/example/api/customer-priorities/route.ts +154 -0
  11. package/template/src/modules/example/api/interceptors.ts +174 -0
  12. package/template/src/modules/example/api/todos/route.ts +1 -1
  13. package/template/src/modules/example/backend/umes-extensions/page.meta.ts +24 -0
  14. package/template/src/modules/example/backend/umes-extensions/page.tsx +365 -0
  15. package/template/src/modules/example/backend/umes-handlers/page.tsx +1 -1
  16. package/template/src/modules/example/data/enrichers.ts +32 -4
  17. package/template/src/modules/example/data/entities.ts +28 -0
  18. package/template/src/modules/example/data/validators.ts +22 -0
  19. package/template/src/modules/example/i18n/de.json +55 -0
  20. package/template/src/modules/example/i18n/en.json +55 -0
  21. package/template/src/modules/example/i18n/es.json +55 -0
  22. package/template/src/modules/example/i18n/pl.json +55 -0
  23. package/template/src/modules/example/migrations/Migration20260226161000_example.ts +15 -0
  24. package/template/src/modules/example/widgets/__tests__/injection-table.test.ts +39 -6
  25. package/template/src/modules/example/widgets/components.ts +23 -0
  26. package/template/src/modules/example/widgets/injection/crud-validation/widget.client.tsx +13 -13
  27. package/template/src/modules/example/widgets/injection/customer-priority-bulk-actions/widget.ts +67 -0
  28. package/template/src/modules/example/widgets/injection/customer-priority-column/widget.ts +23 -0
  29. package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.client.tsx +136 -0
  30. package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.ts +13 -0
  31. package/template/src/modules/example/widgets/injection/customer-priority-field/widget.ts +68 -0
  32. package/template/src/modules/example/widgets/injection/customer-priority-filter/widget.ts +25 -0
  33. package/template/src/modules/example/widgets/injection/customer-priority-row-action/widget.ts +26 -0
  34. package/template/src/modules/example/widgets/injection-table.ts +54 -1
@@ -0,0 +1,154 @@
1
+ import { z } from 'zod'
2
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
+ import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
4
+ import { E } from '@/.mercato/generated/entities.ids.generated'
5
+ import { id, customer_id, priority, organization_id, tenant_id, created_at } from '@/.mercato/generated/entities/example_customer_priority'
6
+ import { ExampleCustomerPriority } from '../../data/entities'
7
+ import {
8
+ customerPriorityCreateSchema,
9
+ customerPriorityListSchema,
10
+ customerPriorityUpdateSchema,
11
+ } from '../../data/validators'
12
+ import {
13
+ createExampleCrudOpenApi,
14
+ createExamplePagedListResponseSchema,
15
+ exampleOkSchema,
16
+ } from '../openapi'
17
+
18
+ type PriorityListQuery = z.infer<typeof customerPriorityListSchema>
19
+
20
+ const customerPriorityListItemSchema = z.object({
21
+ id: z.string().uuid(),
22
+ customer_id: z.string().uuid(),
23
+ priority: z.enum(['low', 'normal', 'high', 'critical']),
24
+ tenant_id: z.string().uuid().nullable().optional(),
25
+ organization_id: z.string().uuid().nullable().optional(),
26
+ })
27
+
28
+ const customerPriorityCreateResponseSchema = z.object({
29
+ id: z.string().uuid(),
30
+ })
31
+
32
+ export const { metadata, GET, POST, PUT, DELETE } = makeCrudRoute({
33
+ metadata: {
34
+ GET: { requireAuth: true, requireFeatures: ['example.view'] },
35
+ POST: { requireAuth: true, requireFeatures: ['example.todos.manage'] },
36
+ PUT: { requireAuth: true, requireFeatures: ['example.todos.manage'] },
37
+ DELETE: { requireAuth: true, requireFeatures: ['example.todos.manage'] },
38
+ },
39
+ orm: {
40
+ entity: ExampleCustomerPriority,
41
+ idField: 'id',
42
+ orgField: 'organizationId',
43
+ tenantField: 'tenantId',
44
+ softDeleteField: 'deletedAt',
45
+ },
46
+ indexer: { entityType: E.example.example_customer_priority },
47
+ list: {
48
+ schema: customerPriorityListSchema,
49
+ entityId: E.example.example_customer_priority,
50
+ fields: [id, customer_id, priority, organization_id, tenant_id, created_at],
51
+ sortFieldMap: {
52
+ id,
53
+ customer_id,
54
+ priority,
55
+ created_at,
56
+ },
57
+ buildFilters: async (query: PriorityListQuery) => {
58
+ const filters: Record<string, unknown> = {}
59
+ if (query.id) filters.id = query.id
60
+ if (query.customerId) filters.customer_id = query.customerId
61
+ return filters
62
+ },
63
+ },
64
+ create: {
65
+ schema: customerPriorityCreateSchema,
66
+ mapToEntity: (input) => ({
67
+ customerId: input.customerId,
68
+ priority: input.priority,
69
+ }),
70
+ response: (entity) => ({ id: String(entity.id) }),
71
+ },
72
+ update: {
73
+ schema: customerPriorityUpdateSchema,
74
+ getId: (input) => input.id,
75
+ applyToEntity: (entity, input) => {
76
+ if (input.customerId) entity.customerId = input.customerId
77
+ if (input.priority) entity.priority = input.priority
78
+ },
79
+ response: () => ({ ok: true }),
80
+ },
81
+ del: {
82
+ idFrom: 'body',
83
+ softDelete: true,
84
+ response: () => ({ ok: true }),
85
+ },
86
+ hooks: {
87
+ afterCreate: async (entity, ctx) => {
88
+ if (!ctx.auth) return
89
+ await invalidateCrudCache(
90
+ ctx.container,
91
+ 'customers.person',
92
+ {
93
+ id: entity.customerId,
94
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
95
+ tenantId: ctx.auth.tenantId ?? null,
96
+ },
97
+ ctx.auth.tenantId ?? null,
98
+ 'example.customer-priority.create',
99
+ ['customers.customer_entity', 'customers.people'],
100
+ )
101
+ },
102
+ afterUpdate: async (entity, ctx) => {
103
+ if (!ctx.auth) return
104
+ await invalidateCrudCache(
105
+ ctx.container,
106
+ 'customers.person',
107
+ {
108
+ id: entity.customerId,
109
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
110
+ tenantId: ctx.auth.tenantId ?? null,
111
+ },
112
+ ctx.auth.tenantId ?? null,
113
+ 'example.customer-priority.update',
114
+ ['customers.customer_entity', 'customers.people'],
115
+ )
116
+ },
117
+ afterDelete: async (_id, ctx) => {
118
+ if (!ctx.auth) return
119
+ await invalidateCrudCache(
120
+ ctx.container,
121
+ 'customers.person',
122
+ {
123
+ organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null,
124
+ tenantId: ctx.auth.tenantId ?? null,
125
+ },
126
+ ctx.auth.tenantId ?? null,
127
+ 'example.customer-priority.delete',
128
+ ['customers.customer_entity', 'customers.people'],
129
+ )
130
+ },
131
+ },
132
+ })
133
+
134
+ export const openApi = createExampleCrudOpenApi({
135
+ resourceName: 'Customer Priority',
136
+ pluralName: 'Customer Priorities',
137
+ querySchema: customerPriorityListSchema,
138
+ listResponseSchema: createExamplePagedListResponseSchema(customerPriorityListItemSchema),
139
+ create: {
140
+ schema: customerPriorityCreateSchema,
141
+ responseSchema: customerPriorityCreateResponseSchema,
142
+ description: 'Creates or stores customer priority records for injected CRUD fields.',
143
+ },
144
+ update: {
145
+ schema: customerPriorityUpdateSchema,
146
+ responseSchema: exampleOkSchema,
147
+ description: 'Updates customer priority values.',
148
+ },
149
+ del: {
150
+ schema: z.object({ id: z.string().uuid() }),
151
+ responseSchema: exampleOkSchema,
152
+ description: 'Soft-deletes a customer priority record.',
153
+ },
154
+ })
@@ -0,0 +1,174 @@
1
+ import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
2
+ import { ExampleCustomerPriority } from '../data/entities'
3
+
4
+ type UnknownRecord = Record<string, unknown>
5
+
6
+ function readString(value: unknown): string | null {
7
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
8
+ }
9
+
10
+ export const interceptors: ApiInterceptor[] = [
11
+ {
12
+ id: 'example.block-test-todos',
13
+ targetRoute: 'example/todos',
14
+ methods: ['POST', 'PUT'],
15
+ priority: 100,
16
+ async before(request) {
17
+ const title = request.body?.title
18
+ if (typeof title === 'string' && title.includes('BLOCKED')) {
19
+ return {
20
+ ok: false,
21
+ statusCode: 422,
22
+ message: 'Todo titles containing "BLOCKED" are blocked by interceptor.',
23
+ }
24
+ }
25
+ return { ok: true }
26
+ },
27
+ },
28
+ {
29
+ id: 'example.todos-probe-timeout',
30
+ targetRoute: 'example/todos',
31
+ methods: ['GET'],
32
+ priority: 90,
33
+ timeoutMs: 100,
34
+ async before(request) {
35
+ const probe = readString(request.query?.interceptorProbe)
36
+ if (probe !== 'timeout') return { ok: true }
37
+ await new Promise((resolve) => setTimeout(resolve, 200))
38
+ return { ok: true }
39
+ },
40
+ },
41
+ {
42
+ id: 'example.todos-probe-crash',
43
+ targetRoute: 'example/todos',
44
+ methods: ['GET'],
45
+ priority: 89,
46
+ async before(request) {
47
+ const probe = readString(request.query?.interceptorProbe)
48
+ if (probe !== 'crash') return { ok: true }
49
+ throw new Error('Interceptor crash probe')
50
+ },
51
+ },
52
+ {
53
+ id: 'example.todos-probe-bad-query',
54
+ targetRoute: 'example/todos',
55
+ methods: ['GET'],
56
+ priority: 88,
57
+ async before(request) {
58
+ const probe = readString(request.query?.interceptorProbe)
59
+ if (probe !== 'bad-query') return { ok: true }
60
+ return {
61
+ ok: true,
62
+ query: {
63
+ ...(request.query ?? {}),
64
+ interceptorProbe: undefined,
65
+ page: 'not-a-number',
66
+ },
67
+ }
68
+ },
69
+ },
70
+ {
71
+ id: 'example.wildcard-probe',
72
+ targetRoute: 'example/*',
73
+ methods: ['GET'],
74
+ priority: 60,
75
+ async before(request) {
76
+ const probe = readString(request.query?.interceptorProbe)
77
+ if (probe !== 'wildcard') return { ok: true }
78
+ return {
79
+ ok: true,
80
+ metadata: { wildcardProbe: true },
81
+ }
82
+ },
83
+ async after(_request, response, context) {
84
+ if (!context.metadata?.wildcardProbe) return {}
85
+ return {
86
+ merge: {
87
+ _example: {
88
+ ...((response.body._example as Record<string, unknown> | undefined) ?? {}),
89
+ wildcardProbe: true,
90
+ },
91
+ },
92
+ }
93
+ },
94
+ },
95
+ {
96
+ id: 'example.todos-response-meta',
97
+ targetRoute: 'example/todos',
98
+ methods: ['GET'],
99
+ priority: 10,
100
+ async before() {
101
+ return {
102
+ ok: true,
103
+ metadata: { startedAt: Date.now() },
104
+ }
105
+ },
106
+ async after(_request, response, context) {
107
+ return {
108
+ merge: {
109
+ _example: {
110
+ ...((response.body._example as Record<string, unknown> | undefined) ?? {}),
111
+ interceptor: {
112
+ processedAt: new Date().toISOString(),
113
+ processingTimeMs: Math.max(0, Date.now() - Number(context.metadata?.startedAt ?? Date.now())),
114
+ },
115
+ },
116
+ },
117
+ }
118
+ },
119
+ },
120
+ {
121
+ id: 'example.customer-priority-filter',
122
+ targetRoute: 'customers/people',
123
+ methods: ['GET'],
124
+ priority: 70,
125
+ async before(request) {
126
+ const priority = readString(request.query?.examplePriority)
127
+ if (!priority) return { ok: true }
128
+ return {
129
+ ok: true,
130
+ query: {
131
+ ...(request.query ?? {}),
132
+ examplePriority: undefined,
133
+ },
134
+ metadata: {
135
+ examplePriority: priority,
136
+ },
137
+ }
138
+ },
139
+ async after(_request, response, context) {
140
+ const priority = readString(context.metadata?.examplePriority)
141
+ if (!priority) return {}
142
+ const responseBody = response.body as UnknownRecord
143
+ const items = Array.isArray(responseBody.items)
144
+ ? responseBody.items
145
+ : (Array.isArray(responseBody.data) ? responseBody.data : [])
146
+ if (items.length === 0) return {}
147
+ const customerIds = items
148
+ .map((item) => (item && typeof item === 'object' ? readString((item as UnknownRecord).id) : null))
149
+ .filter((id): id is string => typeof id === 'string' && id.length > 0)
150
+ if (customerIds.length === 0) return {}
151
+
152
+ const matches = await context.em.find(ExampleCustomerPriority, {
153
+ customerId: { $in: customerIds },
154
+ priority: priority as ExampleCustomerPriority['priority'],
155
+ organizationId: context.organizationId,
156
+ tenantId: context.tenantId,
157
+ deletedAt: null,
158
+ }, { fields: ['customerId'] })
159
+ const matchedIds = new Set(matches.map((entry) => entry.customerId))
160
+ const filtered = items.filter((item) => {
161
+ if (!item || typeof item !== 'object') return false
162
+ const id = readString((item as UnknownRecord).id)
163
+ return id ? matchedIds.has(id) : false
164
+ })
165
+ return {
166
+ replace: {
167
+ ...responseBody,
168
+ items: filtered,
169
+ total: filtered.length,
170
+ },
171
+ }
172
+ },
173
+ },
174
+ ]
@@ -34,7 +34,7 @@ const querySchema = z
34
34
  organizationId: z.string().uuid().optional(),
35
35
  createdFrom: z.string().optional(),
36
36
  createdTo: z.string().optional(),
37
- format: z.enum(['json', 'csv']).optional().default('json'),
37
+ format: z.enum(['json', 'csv']).optional(),
38
38
  })
39
39
  .passthrough()
40
40
 
@@ -0,0 +1,24 @@
1
+ import React from 'react'
2
+
3
+ const puzzleIcon = React.createElement(
4
+ 'svg',
5
+ { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },
6
+ React.createElement('path', { d: 'M12 2v6m0 0a2 2 0 1 0 0 4m0-4a2 2 0 1 1 0-4m0 8v6m0-6a2 2 0 1 0 0 4m0-4a2 2 0 1 1 0-4M2 12h6m0 0a2 2 0 1 0 4 0m-4 0a2 2 0 1 1-4 0m8 0h6m-6 0a2 2 0 1 0 4 0m-4 0a2 2 0 1 1 4 0' }),
7
+ )
8
+
9
+ export const metadata = {
10
+ requireAuth: true,
11
+ requireFeatures: ['example.todos.view'],
12
+ pageTitle: 'Phase E-H handlers',
13
+ pageTitleKey: 'example.menu.umesExtensions',
14
+ pageGroup: 'Example',
15
+ pageGroupKey: 'example.nav.group',
16
+ pageOrder: 20600,
17
+ icon: puzzleIcon,
18
+ breadcrumb: [
19
+ { label: 'General tasks', labelKey: 'example.todos.page.title', href: '/backend/todos' },
20
+ { label: 'Phase E-H extensions', labelKey: 'example.umes.extensions.title' },
21
+ ],
22
+ }
23
+
24
+ export default metadata