@open-mercato/core 0.6.6-develop.6135.1.95b98004bd → 0.6.6-develop.6141.1.630752a1ca
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 +1 -1
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +34 -1
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +34 -1
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +47 -1
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/route.js +19 -23
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js +72 -0
- package/dist/modules/directory/api/tenants/tenant-cf-filter.js.map +7 -0
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js +8 -6
- package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +33 -0
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +2 -1
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +36 -1
- package/src/modules/dashboards/api/layout/route.ts +36 -1
- package/src/modules/dashboards/api/roles/widgets/route.ts +49 -1
- package/src/modules/dashboards/api/users/widgets/route.ts +49 -1
- package/src/modules/directory/api/tenants/route.ts +25 -26
- package/src/modules/directory/api/tenants/tenant-cf-filter.ts +97 -0
- package/src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx +10 -8
- package/src/modules/integrations/api/[id]/health/route.ts +35 -0
- package/src/modules/perspectives/api/[tableId]/route.ts +2 -1
|
@@ -5,6 +5,10 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { DashboardLayout } from '@open-mercato/core/modules/dashboards/data/entities'
|
|
6
6
|
import { dashboardLayoutItemPatchSchema } from '@open-mercato/core/modules/dashboards/data/validators'
|
|
7
7
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
8
|
+
import {
|
|
9
|
+
runCrudMutationGuardAfterSuccess,
|
|
10
|
+
validateCrudMutationGuard,
|
|
11
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
8
12
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
9
13
|
import {
|
|
10
14
|
dashboardsTag,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
} from '../../openapi'
|
|
15
19
|
|
|
16
20
|
const DEFAULT_SIZE = 'md'
|
|
21
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
17
22
|
|
|
18
23
|
export const metadata = {
|
|
19
24
|
PATCH: { requireAuth: true, requireFeatures: ['dashboards.configure'] },
|
|
@@ -39,7 +44,8 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
39
44
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const
|
|
47
|
+
const container = await createRequestContainer()
|
|
48
|
+
const { resolve } = container
|
|
43
49
|
const em = resolve('em') as any
|
|
44
50
|
const rbac = resolve('rbacService') as any
|
|
45
51
|
|
|
@@ -54,6 +60,21 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
54
60
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
64
|
+
tenantId: scope.tenantId ?? '',
|
|
65
|
+
organizationId: scope.organizationId,
|
|
66
|
+
userId: scope.userId,
|
|
67
|
+
resourceKind: RESOURCE_KIND,
|
|
68
|
+
resourceId: layoutItemId,
|
|
69
|
+
operation: 'update',
|
|
70
|
+
requestMethod: req.method,
|
|
71
|
+
requestHeaders: req.headers,
|
|
72
|
+
mutationPayload: { id: layoutItemId, size: parsed.data.size, settings: parsed.data.settings },
|
|
73
|
+
})
|
|
74
|
+
if (guardResult && !guardResult.ok) {
|
|
75
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
const layout = await em.findOne(DashboardLayout, {
|
|
58
79
|
userId: scope.userId,
|
|
59
80
|
tenantId: scope.tenantId,
|
|
@@ -77,6 +98,20 @@ export async function PATCH(req: Request, ctx: { params?: { itemId?: string } })
|
|
|
77
98
|
}
|
|
78
99
|
await em.flush()
|
|
79
100
|
|
|
101
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
102
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
103
|
+
tenantId: scope.tenantId ?? '',
|
|
104
|
+
organizationId: scope.organizationId,
|
|
105
|
+
userId: scope.userId,
|
|
106
|
+
resourceKind: RESOURCE_KIND,
|
|
107
|
+
resourceId: layoutItemId,
|
|
108
|
+
operation: 'update',
|
|
109
|
+
requestMethod: req.method,
|
|
110
|
+
requestHeaders: req.headers,
|
|
111
|
+
metadata: guardResult.metadata ?? null,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
return NextResponse.json({ ok: true })
|
|
81
116
|
}
|
|
82
117
|
|
|
@@ -9,6 +9,10 @@ import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/l
|
|
|
9
9
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
10
10
|
import { User } from '@open-mercato/core/modules/auth/data/entities'
|
|
11
11
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
import {
|
|
14
18
|
dashboardsTag,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
} from '../openapi'
|
|
19
23
|
|
|
20
24
|
const DEFAULT_SIZE = 'md'
|
|
25
|
+
const RESOURCE_KIND = 'dashboards.layout'
|
|
21
26
|
|
|
22
27
|
export const metadata = {
|
|
23
28
|
GET: { requireAuth: true, requireFeatures: ['dashboards.view'] },
|
|
@@ -206,7 +211,8 @@ export async function PUT(req: Request) {
|
|
|
206
211
|
return NextResponse.json({ error: 'Invalid layout payload', issues: parsed.error.issues }, { status: 400 })
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
const
|
|
214
|
+
const container = await createRequestContainer()
|
|
215
|
+
const { resolve } = container
|
|
210
216
|
const em = (resolve('em') as any).fork({ clear: true, freshEventManager: true, useContext: true })
|
|
211
217
|
const rbac = resolve('rbacService') as any
|
|
212
218
|
|
|
@@ -221,6 +227,21 @@ export async function PUT(req: Request) {
|
|
|
221
227
|
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
222
228
|
}
|
|
223
229
|
|
|
230
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
231
|
+
tenantId: scope.tenantId ?? '',
|
|
232
|
+
organizationId: scope.organizationId,
|
|
233
|
+
userId: scope.userId,
|
|
234
|
+
resourceKind: RESOURCE_KIND,
|
|
235
|
+
resourceId: scope.userId,
|
|
236
|
+
operation: 'update',
|
|
237
|
+
requestMethod: req.method,
|
|
238
|
+
requestHeaders: req.headers,
|
|
239
|
+
mutationPayload: { items: parsed.data.items },
|
|
240
|
+
})
|
|
241
|
+
if (guardResult && !guardResult.ok) {
|
|
242
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
243
|
+
}
|
|
244
|
+
|
|
224
245
|
const widgets = await loadAllWidgets()
|
|
225
246
|
const allowedIds = await resolveAllowedWidgetIds(
|
|
226
247
|
em,
|
|
@@ -266,6 +287,20 @@ export async function PUT(req: Request) {
|
|
|
266
287
|
}
|
|
267
288
|
await em.flush()
|
|
268
289
|
|
|
290
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
291
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
292
|
+
tenantId: scope.tenantId ?? '',
|
|
293
|
+
organizationId: scope.organizationId,
|
|
294
|
+
userId: scope.userId,
|
|
295
|
+
resourceKind: RESOURCE_KIND,
|
|
296
|
+
resourceId: scope.userId,
|
|
297
|
+
operation: 'update',
|
|
298
|
+
requestMethod: req.method,
|
|
299
|
+
requestHeaders: req.headers,
|
|
300
|
+
metadata: guardResult.metadata ?? null,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
269
304
|
return NextResponse.json({ ok: true })
|
|
270
305
|
}
|
|
271
306
|
|
|
@@ -8,6 +8,10 @@ import { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/
|
|
|
8
8
|
import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
|
|
9
9
|
import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
|
|
10
10
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
11
|
+
import {
|
|
12
|
+
runCrudMutationGuardAfterSuccess,
|
|
13
|
+
validateCrudMutationGuard,
|
|
14
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
11
15
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
16
|
import {
|
|
13
17
|
dashboardsTag,
|
|
@@ -17,6 +21,7 @@ import {
|
|
|
17
21
|
} from '../../openapi'
|
|
18
22
|
|
|
19
23
|
const FEATURE = 'dashboards.admin.assign-widgets'
|
|
24
|
+
const RESOURCE_KIND = 'dashboards.roleWidgets'
|
|
20
25
|
|
|
21
26
|
function pickBestRecord(records: DashboardRoleWidgets[], tenantId: string | null, organizationId: string | null): DashboardRoleWidgets | null {
|
|
22
27
|
let best: DashboardRoleWidgets | null = null
|
|
@@ -99,7 +104,8 @@ export async function PUT(req: Request) {
|
|
|
99
104
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const
|
|
107
|
+
const container = await createRequestContainer()
|
|
108
|
+
const { resolve } = container
|
|
103
109
|
const em = resolve('em') as any
|
|
104
110
|
const rbac = resolve('rbacService') as any
|
|
105
111
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
|
|
@@ -119,6 +125,21 @@ export async function PUT(req: Request) {
|
|
|
119
125
|
return NextResponse.json({ error: 'Role not found' }, { status: 404 })
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
129
|
+
tenantId: tenantId ?? '',
|
|
130
|
+
organizationId,
|
|
131
|
+
userId: String(auth.sub),
|
|
132
|
+
resourceKind: RESOURCE_KIND,
|
|
133
|
+
resourceId: parsed.data.roleId,
|
|
134
|
+
operation: 'update',
|
|
135
|
+
requestMethod: req.method,
|
|
136
|
+
requestHeaders: req.headers,
|
|
137
|
+
mutationPayload: { roleId: parsed.data.roleId, widgetIds },
|
|
138
|
+
})
|
|
139
|
+
if (guardResult && !guardResult.ok) {
|
|
140
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
let record = await em.findOne(DashboardRoleWidgets, {
|
|
123
144
|
roleId: parsed.data.roleId,
|
|
124
145
|
tenantId,
|
|
@@ -130,6 +151,19 @@ export async function PUT(req: Request) {
|
|
|
130
151
|
if (record) {
|
|
131
152
|
await em.remove(record).flush()
|
|
132
153
|
}
|
|
154
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
155
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
156
|
+
tenantId: tenantId ?? '',
|
|
157
|
+
organizationId,
|
|
158
|
+
userId: String(auth.sub),
|
|
159
|
+
resourceKind: RESOURCE_KIND,
|
|
160
|
+
resourceId: parsed.data.roleId,
|
|
161
|
+
operation: 'update',
|
|
162
|
+
requestMethod: req.method,
|
|
163
|
+
requestHeaders: req.headers,
|
|
164
|
+
metadata: guardResult.metadata ?? null,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
133
167
|
return NextResponse.json({ ok: true, widgetIds: [] })
|
|
134
168
|
}
|
|
135
169
|
|
|
@@ -146,6 +180,20 @@ export async function PUT(req: Request) {
|
|
|
146
180
|
}
|
|
147
181
|
await em.flush()
|
|
148
182
|
|
|
183
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
184
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
185
|
+
tenantId: tenantId ?? '',
|
|
186
|
+
organizationId,
|
|
187
|
+
userId: String(auth.sub),
|
|
188
|
+
resourceKind: RESOURCE_KIND,
|
|
189
|
+
resourceId: parsed.data.roleId,
|
|
190
|
+
operation: 'update',
|
|
191
|
+
requestMethod: req.method,
|
|
192
|
+
requestHeaders: req.headers,
|
|
193
|
+
metadata: guardResult.metadata ?? null,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
return NextResponse.json({ ok: true, widgetIds })
|
|
150
198
|
}
|
|
151
199
|
|
|
@@ -9,6 +9,10 @@ import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widget
|
|
|
9
9
|
import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
|
|
10
10
|
import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
|
|
11
11
|
import { hasFeature } from '@open-mercato/shared/security/features'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
import {
|
|
14
18
|
dashboardsTag,
|
|
@@ -18,6 +22,7 @@ import {
|
|
|
18
22
|
} from '../../openapi'
|
|
19
23
|
|
|
20
24
|
const FEATURE = 'dashboards.admin.assign-widgets'
|
|
25
|
+
const RESOURCE_KIND = 'dashboards.userWidgets'
|
|
21
26
|
|
|
22
27
|
export const metadata = {
|
|
23
28
|
GET: { requireAuth: true, requireFeatures: [FEATURE] },
|
|
@@ -99,7 +104,8 @@ export async function PUT(req: Request) {
|
|
|
99
104
|
return NextResponse.json({ error: 'Invalid payload', issues: parsed.error.issues }, { status: 400 })
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const
|
|
107
|
+
const container = await createRequestContainer()
|
|
108
|
+
const { resolve } = container
|
|
103
109
|
const em = resolve('em') as any
|
|
104
110
|
const rbac = resolve('rbacService') as any
|
|
105
111
|
const acl = await rbac.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
|
|
@@ -119,6 +125,21 @@ export async function PUT(req: Request) {
|
|
|
119
125
|
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
129
|
+
tenantId: tenantId ?? '',
|
|
130
|
+
organizationId,
|
|
131
|
+
userId: String(auth.sub),
|
|
132
|
+
resourceKind: RESOURCE_KIND,
|
|
133
|
+
resourceId: parsed.data.userId,
|
|
134
|
+
operation: 'update',
|
|
135
|
+
requestMethod: req.method,
|
|
136
|
+
requestHeaders: req.headers,
|
|
137
|
+
mutationPayload: { userId: parsed.data.userId, mode: parsed.data.mode, widgetIds },
|
|
138
|
+
})
|
|
139
|
+
if (guardResult && !guardResult.ok) {
|
|
140
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
let record = await em.findOne(DashboardUserWidgets, {
|
|
123
144
|
userId: parsed.data.userId,
|
|
124
145
|
tenantId,
|
|
@@ -130,6 +151,19 @@ export async function PUT(req: Request) {
|
|
|
130
151
|
if (record) {
|
|
131
152
|
await em.remove(record).flush()
|
|
132
153
|
}
|
|
154
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
155
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
156
|
+
tenantId: tenantId ?? '',
|
|
157
|
+
organizationId,
|
|
158
|
+
userId: String(auth.sub),
|
|
159
|
+
resourceKind: RESOURCE_KIND,
|
|
160
|
+
resourceId: parsed.data.userId,
|
|
161
|
+
operation: 'update',
|
|
162
|
+
requestMethod: req.method,
|
|
163
|
+
requestHeaders: req.headers,
|
|
164
|
+
metadata: guardResult.metadata ?? null,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
133
167
|
return NextResponse.json({ ok: true, mode: 'inherit', widgetIds: [] })
|
|
134
168
|
}
|
|
135
169
|
|
|
@@ -148,6 +182,20 @@ export async function PUT(req: Request) {
|
|
|
148
182
|
}
|
|
149
183
|
await em.flush()
|
|
150
184
|
|
|
185
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
186
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
187
|
+
tenantId: tenantId ?? '',
|
|
188
|
+
organizationId,
|
|
189
|
+
userId: String(auth.sub),
|
|
190
|
+
resourceKind: RESOURCE_KIND,
|
|
191
|
+
resourceId: parsed.data.userId,
|
|
192
|
+
operation: 'update',
|
|
193
|
+
requestMethod: req.method,
|
|
194
|
+
requestHeaders: req.headers,
|
|
195
|
+
metadata: guardResult.metadata ?? null,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
151
199
|
return NextResponse.json({ ok: true, mode: 'override', widgetIds })
|
|
152
200
|
}
|
|
153
201
|
|
|
@@ -6,6 +6,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
6
6
|
import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
7
7
|
import { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'
|
|
8
8
|
import { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
9
|
+
import { resolveRecordIdsForCustomFieldFilters } from './tenant-cf-filter'
|
|
9
10
|
import { E } from '#generated/entities.ids.generated'
|
|
10
11
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
11
12
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
@@ -87,11 +88,6 @@ const toRow = (tenant: Tenant, cf: Record<string, unknown>): TenantRow => ({
|
|
|
87
88
|
...cf,
|
|
88
89
|
})
|
|
89
90
|
|
|
90
|
-
const matchesValue = (current: unknown, expected: unknown): boolean => {
|
|
91
|
-
if (Array.isArray(current)) return current.some((item) => item === expected)
|
|
92
|
-
return current === expected
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
export async function GET(req: Request) {
|
|
96
92
|
const auth = await getAuthFromRequest(req)
|
|
97
93
|
if (!auth) {
|
|
@@ -180,28 +176,31 @@ export async function GET(req: Request) {
|
|
|
180
176
|
let cfValues: Record<string, Record<string, unknown>>
|
|
181
177
|
|
|
182
178
|
if (cfFilterEntries.length) {
|
|
183
|
-
// Custom-field
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const value = payload[`cf_${key}`]
|
|
192
|
-
if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
|
|
193
|
-
const maybeIn = (expected as { $in?: unknown[] }).$in
|
|
194
|
-
if (Array.isArray(maybeIn)) {
|
|
195
|
-
if (value === undefined || value === null) return false
|
|
196
|
-
if (Array.isArray(value)) return value.some((val) => maybeIn.includes(val))
|
|
197
|
-
return maybeIn.includes(value)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return matchesValue(value, expected)
|
|
201
|
-
})
|
|
179
|
+
// Custom-field values live in a separate store, so first resolve the bounded
|
|
180
|
+
// set of matching tenant ids from that store, then push pagination to the
|
|
181
|
+
// database via findAndCount instead of loading the whole tenant table.
|
|
182
|
+
const matchedIds = await resolveRecordIdsForCustomFieldFilters({
|
|
183
|
+
em,
|
|
184
|
+
entityId: E.directory.tenant,
|
|
185
|
+
tenantId: auth.tenantId ?? null,
|
|
186
|
+
filters: cfFilterEntries,
|
|
202
187
|
})
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
const existingId = where.id
|
|
189
|
+
const idFilter = typeof existingId === 'string'
|
|
190
|
+
? (matchedIds.has(existingId) ? [existingId] : [])
|
|
191
|
+
: Array.from(matchedIds)
|
|
192
|
+
|
|
193
|
+
if (!idFilter.length) {
|
|
194
|
+
total = 0
|
|
195
|
+
pageTenants = []
|
|
196
|
+
cfValues = {}
|
|
197
|
+
} else {
|
|
198
|
+
where.id = { $in: idFilter }
|
|
199
|
+
const [rows, count] = await em.findAndCount(Tenant, where, { orderBy, limit: pageSize, offset: start })
|
|
200
|
+
total = count
|
|
201
|
+
pageTenants = rows
|
|
202
|
+
cfValues = await loadCfValues(rows)
|
|
203
|
+
}
|
|
205
204
|
} else {
|
|
206
205
|
// No JS-side filtering applies, so pagination is pushed to the database
|
|
207
206
|
// instead of fetching the whole table and slicing in memory.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { FilterQuery } from '@mikro-orm/core'
|
|
3
|
+
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
|
+
import { CustomFieldDef, CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'
|
|
5
|
+
|
|
6
|
+
export type CustomFieldFilterEntry = readonly [string, unknown]
|
|
7
|
+
|
|
8
|
+
type ValueColumn = 'valueText' | 'valueMultiline' | 'valueInt' | 'valueFloat' | 'valueBool'
|
|
9
|
+
|
|
10
|
+
const valueColumnForKind = (kind: string | null | undefined): ValueColumn => {
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case 'integer':
|
|
13
|
+
return 'valueInt'
|
|
14
|
+
case 'float':
|
|
15
|
+
return 'valueFloat'
|
|
16
|
+
case 'boolean':
|
|
17
|
+
return 'valueBool'
|
|
18
|
+
case 'multiline':
|
|
19
|
+
return 'valueMultiline'
|
|
20
|
+
default:
|
|
21
|
+
return 'valueText'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expectedValuesFor = (condition: unknown): unknown[] => {
|
|
26
|
+
if (condition && typeof condition === 'object' && !Array.isArray(condition)) {
|
|
27
|
+
const maybeIn = (condition as { $in?: unknown[] }).$in
|
|
28
|
+
if (Array.isArray(maybeIn)) return maybeIn
|
|
29
|
+
}
|
|
30
|
+
return [condition]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const intersect = (current: Set<string> | null, next: Set<string>): Set<string> => {
|
|
34
|
+
if (current === null) return next
|
|
35
|
+
const result = new Set<string>()
|
|
36
|
+
for (const id of next) {
|
|
37
|
+
if (current.has(id)) result.add(id)
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the bounded set of record ids that satisfy the given custom-field
|
|
44
|
+
* filters by querying the custom-field value store directly, instead of loading
|
|
45
|
+
* every entity row and matching the values in memory. Filters are combined with
|
|
46
|
+
* AND semantics. Returns an empty set when any filter matches nothing.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveRecordIdsForCustomFieldFilters(opts: {
|
|
49
|
+
em: EntityManager
|
|
50
|
+
entityId: EntityId
|
|
51
|
+
tenantId: string | null
|
|
52
|
+
filters: CustomFieldFilterEntry[]
|
|
53
|
+
}): Promise<Set<string>> {
|
|
54
|
+
const { em, entityId, tenantId, filters } = opts
|
|
55
|
+
if (!filters.length) return new Set<string>()
|
|
56
|
+
|
|
57
|
+
const keys = Array.from(new Set(filters.map(([key]) => key)))
|
|
58
|
+
const tenantScope: Record<string, unknown> = tenantId
|
|
59
|
+
? { tenantId: { $in: [tenantId, null] } }
|
|
60
|
+
: { tenantId: null }
|
|
61
|
+
|
|
62
|
+
const defs = await em.find(CustomFieldDef, {
|
|
63
|
+
entityId: String(entityId),
|
|
64
|
+
key: { $in: keys },
|
|
65
|
+
isActive: true,
|
|
66
|
+
deletedAt: null,
|
|
67
|
+
...tenantScope,
|
|
68
|
+
} as FilterQuery<CustomFieldDef>)
|
|
69
|
+
const kindByKey = new Map<string, string>()
|
|
70
|
+
for (const def of defs) {
|
|
71
|
+
if (!kindByKey.has(def.key)) kindByKey.set(def.key, def.kind)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let matched: Set<string> | null = null
|
|
75
|
+
for (const [key, condition] of filters) {
|
|
76
|
+
const column = valueColumnForKind(kindByKey.get(key))
|
|
77
|
+
const expectedValues = expectedValuesFor(condition).filter((value) => value !== undefined && value !== null)
|
|
78
|
+
if (!expectedValues.length) return new Set<string>()
|
|
79
|
+
|
|
80
|
+
const rows = await em.find(
|
|
81
|
+
CustomFieldValue,
|
|
82
|
+
{
|
|
83
|
+
entityId: String(entityId),
|
|
84
|
+
fieldKey: key,
|
|
85
|
+
deletedAt: null,
|
|
86
|
+
[column]: { $in: expectedValues },
|
|
87
|
+
...tenantScope,
|
|
88
|
+
} as FilterQuery<CustomFieldValue>,
|
|
89
|
+
{ fields: ['recordId'] },
|
|
90
|
+
)
|
|
91
|
+
const ids = new Set<string>(rows.map((row) => String(row.recordId)))
|
|
92
|
+
matched = intersect(matched, ids)
|
|
93
|
+
if (matched.size === 0) return matched
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return matched ?? new Set<string>()
|
|
97
|
+
}
|
|
@@ -7,30 +7,32 @@ type FeatureToggleDetailsCardProps = {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function DefaultValueDisplay({ featureToggleItem }: { featureToggleItem?: FeatureToggle }) {
|
|
10
|
+
const t = useT()
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const defaultValue = featureToggleItem?.defaultValue
|
|
13
|
+
if (defaultValue === null || defaultValue === undefined) {
|
|
12
14
|
return <p className="text-base text-muted-foreground">-</p>
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
switch (featureToggleItem
|
|
17
|
+
switch (featureToggleItem?.type) {
|
|
16
18
|
case 'boolean':
|
|
17
19
|
return (
|
|
18
20
|
<p className="text-base font-semibold text-foreground">
|
|
19
|
-
{
|
|
21
|
+
{defaultValue ? t('feature_toggles.values.true', 'True') : t('feature_toggles.values.false', 'False')}
|
|
20
22
|
</p>
|
|
21
23
|
)
|
|
22
24
|
|
|
23
25
|
case 'string':
|
|
24
26
|
return (
|
|
25
27
|
<p className="text-base font-semibold text-foreground">
|
|
26
|
-
{
|
|
28
|
+
{defaultValue}
|
|
27
29
|
</p>
|
|
28
30
|
)
|
|
29
31
|
|
|
30
32
|
case 'number':
|
|
31
33
|
return (
|
|
32
34
|
<p className="text-base font-semibold text-foreground">
|
|
33
|
-
{
|
|
35
|
+
{defaultValue}
|
|
34
36
|
</p>
|
|
35
37
|
)
|
|
36
38
|
|
|
@@ -38,9 +40,9 @@ function DefaultValueDisplay({ featureToggleItem }: { featureToggleItem?: Featur
|
|
|
38
40
|
return (
|
|
39
41
|
<div className="text-base font-semibold text-foreground">
|
|
40
42
|
<pre className="bg-muted/30 p-2 rounded text-sm font-mono overflow-x-auto whitespace-pre-wrap">
|
|
41
|
-
{typeof
|
|
42
|
-
? JSON.stringify(
|
|
43
|
-
:
|
|
43
|
+
{typeof defaultValue === 'object'
|
|
44
|
+
? JSON.stringify(defaultValue, null, 2)
|
|
45
|
+
: defaultValue}
|
|
44
46
|
</pre>
|
|
45
47
|
</div>
|
|
46
48
|
)
|
|
@@ -4,6 +4,11 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { getIntegration } from '@open-mercato/shared/modules/integrations/types'
|
|
6
6
|
import type { IntegrationHealthService } from '../../../lib/health-service'
|
|
7
|
+
import {
|
|
8
|
+
resolveUserFeatures,
|
|
9
|
+
runIntegrationMutationGuardAfterSuccess,
|
|
10
|
+
runIntegrationMutationGuards,
|
|
11
|
+
} from '../../guards'
|
|
7
12
|
|
|
8
13
|
const idParamsSchema = z.object({ id: z.string().min(1) })
|
|
9
14
|
|
|
@@ -37,6 +42,25 @@ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
const container = await createRequestContainer()
|
|
45
|
+
const guardResult = await runIntegrationMutationGuards(
|
|
46
|
+
container,
|
|
47
|
+
{
|
|
48
|
+
tenantId: auth.tenantId,
|
|
49
|
+
organizationId: auth.orgId,
|
|
50
|
+
userId: auth.sub ?? '',
|
|
51
|
+
resourceKind: 'integrations.integration',
|
|
52
|
+
resourceId: integration.id,
|
|
53
|
+
operation: 'update',
|
|
54
|
+
requestMethod: req.method,
|
|
55
|
+
requestHeaders: req.headers,
|
|
56
|
+
mutationPayload: { integrationId: integration.id },
|
|
57
|
+
},
|
|
58
|
+
resolveUserFeatures(auth),
|
|
59
|
+
)
|
|
60
|
+
if (!guardResult.ok) {
|
|
61
|
+
return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
const healthService = container.resolve('integrationHealthService') as IntegrationHealthService
|
|
41
65
|
|
|
42
66
|
const result = await healthService.runHealthCheck(
|
|
@@ -44,6 +68,17 @@ export async function POST(req: Request, ctx: { params?: Promise<{ id?: string }
|
|
|
44
68
|
{ organizationId: auth.orgId as string, tenantId: auth.tenantId },
|
|
45
69
|
)
|
|
46
70
|
|
|
71
|
+
await runIntegrationMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
72
|
+
tenantId: auth.tenantId,
|
|
73
|
+
organizationId: auth.orgId,
|
|
74
|
+
userId: auth.sub ?? '',
|
|
75
|
+
resourceKind: 'integrations.integration',
|
|
76
|
+
resourceId: integration.id,
|
|
77
|
+
operation: 'update',
|
|
78
|
+
requestMethod: req.method,
|
|
79
|
+
requestHeaders: req.headers,
|
|
80
|
+
})
|
|
81
|
+
|
|
47
82
|
return NextResponse.json({
|
|
48
83
|
status: result.status,
|
|
49
84
|
message: result.message ?? null,
|
|
@@ -75,9 +75,10 @@ export async function GET(_req: Request, ctx: { params: { tableId: string } }) {
|
|
|
75
75
|
const assignedRoleNames = Array.isArray(auth.roles)
|
|
76
76
|
? Array.from(new Set(auth.roles.filter((role): role is string => typeof role === 'string' && role.trim().length > 0)))
|
|
77
77
|
: []
|
|
78
|
-
const assignedRoles = assignedRoleNames.length
|
|
78
|
+
const assignedRoles = assignedRoleNames.length && auth.tenantId
|
|
79
79
|
? await em.find(Role, {
|
|
80
80
|
name: { $in: assignedRoleNames as any },
|
|
81
|
+
tenantId: auth.tenantId,
|
|
81
82
|
deletedAt: null,
|
|
82
83
|
} as any, { orderBy: { name: 'asc' } })
|
|
83
84
|
: []
|