@open-mercato/core 0.6.6-develop.6133.1.f01540250e → 0.6.6-develop.6140.1.d3fbb77495

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 (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/api/admin/users-invite.js +9 -0
  3. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/portal/users-invite.js +9 -0
  5. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  6. package/dist/modules/customer_accounts/events.js +1 -0
  7. package/dist/modules/customer_accounts/events.js.map +2 -2
  8. package/dist/modules/dashboards/api/layout/[itemId]/route.js +34 -1
  9. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  10. package/dist/modules/dashboards/api/layout/route.js +34 -1
  11. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  12. package/dist/modules/dashboards/api/roles/widgets/route.js +47 -1
  13. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  14. package/dist/modules/dashboards/api/users/widgets/route.js +47 -1
  15. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  16. package/dist/modules/directory/api/tenants/route.js +19 -23
  17. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  18. package/dist/modules/directory/api/tenants/tenant-cf-filter.js +72 -0
  19. package/dist/modules/directory/api/tenants/tenant-cf-filter.js.map +7 -0
  20. package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js +8 -6
  21. package/dist/modules/feature_toggles/components/FeatureToggleDetailsCard.js.map +2 -2
  22. package/dist/modules/integrations/api/[id]/health/route.js +33 -0
  23. package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
  24. package/dist/modules/notifications/api/unread-count/route.js +6 -3
  25. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  26. package/package.json +7 -7
  27. package/src/modules/customer_accounts/AGENTS.md +1 -0
  28. package/src/modules/customer_accounts/api/admin/users-invite.ts +10 -0
  29. package/src/modules/customer_accounts/api/portal/users-invite.ts +10 -0
  30. package/src/modules/customer_accounts/events.ts +1 -0
  31. package/src/modules/dashboards/api/layout/[itemId]/route.ts +36 -1
  32. package/src/modules/dashboards/api/layout/route.ts +36 -1
  33. package/src/modules/dashboards/api/roles/widgets/route.ts +49 -1
  34. package/src/modules/dashboards/api/users/widgets/route.ts +49 -1
  35. package/src/modules/directory/api/tenants/route.ts +25 -26
  36. package/src/modules/directory/api/tenants/tenant-cf-filter.ts +97 -0
  37. package/src/modules/feature_toggles/components/FeatureToggleDetailsCard.tsx +10 -8
  38. package/src/modules/integrations/api/[id]/health/route.ts +35 -0
  39. package/src/modules/notifications/api/unread-count/route.ts +6 -4
@@ -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,
@@ -29,10 +29,12 @@ export async function GET(req: Request) {
29
29
  const cacheKey = cache && userId ? buildUnreadCountCacheKey(userId) : null
30
30
 
31
31
  if (cache && cacheKey) {
32
- const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
33
- if (typeof cached === 'number') {
34
- return Response.json({ unreadCount: cached })
35
- }
32
+ try {
33
+ const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))
34
+ if (typeof cached === 'number') {
35
+ return Response.json({ unreadCount: cached })
36
+ }
37
+ } catch {}
36
38
  }
37
39
 
38
40
  const count = await em.count(Notification, {