@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customer_accounts/api/admin/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +9 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/events.js +1 -0
- package/dist/modules/customer_accounts/events.js.map +2 -2
- 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/notifications/api/unread-count/route.js +6 -3
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/AGENTS.md +1 -0
- package/src/modules/customer_accounts/api/admin/users-invite.ts +10 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +10 -0
- package/src/modules/customer_accounts/events.ts +1 -0
- 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/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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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, {
|