@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.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/dashboards/api/layout/[itemId]/route.js +34 -1
  3. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  4. package/dist/modules/dashboards/api/layout/route.js +34 -1
  5. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  6. package/dist/modules/dashboards/api/roles/widgets/route.js +47 -1
  7. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  8. package/dist/modules/dashboards/api/users/widgets/route.js +47 -1
  9. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  10. package/dist/modules/directory/api/tenants/route.js +19 -23
  11. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  12. package/dist/modules/directory/api/tenants/tenant-cf-filter.js +72 -0
  13. package/dist/modules/directory/api/tenants/tenant-cf-filter.js.map +7 -0
  14. package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js +8 -6
  15. package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js.map +2 -2
  16. package/dist/modules/integrations/api/[id]/health/route.js +33 -0
  17. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  18. package/dist/modules/perspectives/api/[tableId]/route.js +2 -1
  19. package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
  20. package/package.json +7 -7
  21. package/src/modules/dashboards/api/layout/[itemId]/route.ts +36 -1
  22. package/src/modules/dashboards/api/layout/route.ts +36 -1
  23. package/src/modules/dashboards/api/roles/widgets/route.ts +49 -1
  24. package/src/modules/dashboards/api/users/widgets/route.ts +49 -1
  25. package/src/modules/directory/api/tenants/route.ts +25 -26
  26. package/src/modules/directory/api/tenants/tenant-cf-filter.ts +97 -0
  27. package/src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx +10 -8
  28. package/src/modules/integrations/api/[id]/health/route.ts +35 -0
  29. 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 { resolve } = await createRequestContainer()
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 { resolve } = await createRequestContainer()
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 { resolve } = await createRequestContainer()
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 { resolve } = await createRequestContainer()
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 filters are matched in JS against separately-loaded values,
184
- // so the full result set must be fetched before filtering and paging.
185
- const all = await em.find(Tenant, where, { orderBy })
186
- cfValues = await loadCfValues(all)
187
- const filtered = all.filter((tenant) => {
188
- const rid = String(tenant.id)
189
- const payload = cfValues[rid] ?? {}
190
- return cfFilterEntries.every(([key, expected]) => {
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
- total = filtered.length
204
- pageTenants = filtered.slice(start, start + pageSize)
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
- if (!featureToggleItem?.defaultValue) {
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.type) {
17
+ switch (featureToggleItem?.type) {
16
18
  case 'boolean':
17
19
  return (
18
20
  <p className="text-base font-semibold text-foreground">
19
- {featureToggleItem.defaultValue ? useT()('feature_toggles.values.true', 'True') : useT()('feature_toggles.values.false', 'False')}
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
- {featureToggleItem.defaultValue}
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
- {featureToggleItem.defaultValue}
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 featureToggleItem.defaultValue === 'object'
42
- ? JSON.stringify(featureToggleItem.defaultValue, null, 2)
43
- : featureToggleItem.defaultValue}
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
  : []