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.
- package/package.json +1 -1
- package/template/src/app/(backend)/backend/[...slug]/page.tsx +6 -2
- package/template/src/app/(backend)/backend/__tests__/backend-require-features.test.tsx +108 -0
- package/template/src/app/(backend)/backend/layout.tsx +30 -27
- package/template/src/bootstrap.ts +4 -0
- package/template/src/components/ClientBootstrap.tsx +2 -5
- package/template/src/components/ComponentOverridesBootstrap.tsx +20 -0
- package/template/src/modules/example/__integration__/TC-UMES-004.spec.ts +337 -0
- package/template/src/modules/example/__integration__/meta.ts +3 -0
- package/template/src/modules/example/api/customer-priorities/route.ts +154 -0
- package/template/src/modules/example/api/interceptors.ts +174 -0
- package/template/src/modules/example/api/todos/route.ts +1 -1
- package/template/src/modules/example/backend/umes-extensions/page.meta.ts +24 -0
- package/template/src/modules/example/backend/umes-extensions/page.tsx +365 -0
- package/template/src/modules/example/backend/umes-handlers/page.tsx +1 -1
- package/template/src/modules/example/data/enrichers.ts +32 -4
- package/template/src/modules/example/data/entities.ts +28 -0
- package/template/src/modules/example/data/validators.ts +22 -0
- package/template/src/modules/example/i18n/de.json +55 -0
- package/template/src/modules/example/i18n/en.json +55 -0
- package/template/src/modules/example/i18n/es.json +55 -0
- package/template/src/modules/example/i18n/pl.json +55 -0
- package/template/src/modules/example/migrations/Migration20260226161000_example.ts +15 -0
- package/template/src/modules/example/widgets/__tests__/injection-table.test.ts +39 -6
- package/template/src/modules/example/widgets/components.ts +23 -0
- package/template/src/modules/example/widgets/injection/crud-validation/widget.client.tsx +13 -13
- package/template/src/modules/example/widgets/injection/customer-priority-bulk-actions/widget.ts +67 -0
- package/template/src/modules/example/widgets/injection/customer-priority-column/widget.ts +23 -0
- package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.client.tsx +136 -0
- package/template/src/modules/example/widgets/injection/customer-priority-detail/widget.ts +13 -0
- package/template/src/modules/example/widgets/injection/customer-priority-field/widget.ts +68 -0
- package/template/src/modules/example/widgets/injection/customer-priority-filter/widget.ts +25 -0
- package/template/src/modules/example/widgets/injection/customer-priority-row-action/widget.ts +26 -0
- 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()
|
|
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
|