@open-mercato/core 0.6.7-develop.6673.1.a9bf54a072 → 0.6.7-develop.6676.1.7dad6df292

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 (23) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/audit_logs/api/audit-logs/access/route.js +5 -1
  3. package/dist/modules/audit_logs/api/audit-logs/access/route.js.map +2 -2
  4. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +5 -1
  5. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
  6. package/dist/modules/audit_logs/api/audit-logs/actions/route.js +5 -1
  7. package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
  8. package/dist/modules/audit_logs/api/audit-logs/readScope.js +12 -0
  9. package/dist/modules/audit_logs/api/audit-logs/readScope.js.map +7 -0
  10. package/dist/modules/directory/api/organization-switcher/route.js +84 -20
  11. package/dist/modules/directory/api/organization-switcher/route.js.map +2 -2
  12. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +5 -1
  13. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  14. package/dist/modules/directory/utils/organizationScope.js +11 -3
  15. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  16. package/package.json +7 -7
  17. package/src/modules/audit_logs/api/audit-logs/access/route.ts +5 -0
  18. package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +5 -0
  19. package/src/modules/audit_logs/api/audit-logs/actions/route.ts +5 -0
  20. package/src/modules/audit_logs/api/audit-logs/readScope.ts +28 -0
  21. package/src/modules/directory/api/organization-switcher/route.ts +97 -26
  22. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +4 -1
  23. package/src/modules/directory/utils/organizationScope.ts +9 -3
@@ -6,12 +6,19 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
6
6
  import { computeHierarchyForOrganizations, type ComputedHierarchy } from '@open-mercato/core/modules/directory/lib/hierarchy'
7
7
  import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
8
8
  import {
9
+ buildOrgScopeTenantCacheTag,
9
10
  getSelectedOrganizationFromRequest,
10
11
  getSelectedTenantFromRequest,
11
12
  resolveOrganizationScope,
12
13
  } from '@open-mercato/core/modules/directory/utils/organizationScope'
14
+ import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
13
15
  import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
14
- import { directoryTag, directoryErrorSchema, organizationSwitcherResponseSchema } from '../openapi'
16
+ import {
17
+ directoryTag,
18
+ directoryErrorSchema,
19
+ directoryIdSchema,
20
+ organizationSwitcherResponseSchema,
21
+ } from '../openapi'
15
22
  import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
16
23
  import type { EntityManager } from '@mikro-orm/postgresql'
17
24
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
@@ -19,6 +26,21 @@ import type { FilterQuery } from '@mikro-orm/core'
19
26
  import { createLogger } from '@open-mercato/shared/lib/logger'
20
27
 
21
28
  const logger = createLogger('directory').child({ component: 'organization-switcher' })
29
+ const ORGANIZATION_SWITCHER_CACHE_TTL_MS = 60_000
30
+ const organizationSwitcherCachePayloadSchema = organizationSwitcherResponseSchema.or(
31
+ organizationSwitcherResponseSchema.pick({
32
+ items: true,
33
+ selectedId: true,
34
+ canManage: true,
35
+ }),
36
+ )
37
+
38
+ function buildSelectionCacheKeyPart(rawSelected: string | null): string | null {
39
+ if (isAllOrganizationsSelection(rawSelected)) return 'mode:all'
40
+ if (rawSelected === null) return 'mode:none'
41
+ const parsedOrganizationId = directoryIdSchema.safeParse(rawSelected)
42
+ return parsedOrganizationId.success ? `org:${parsedOrganizationId.data.toLowerCase()}` : null
43
+ }
22
44
 
23
45
  type OrganizationMenuNode = {
24
46
  id: string
@@ -141,6 +163,49 @@ export async function GET(req: NextRequest) {
141
163
  })
142
164
  }
143
165
 
166
+ const rawSelected = getSelectedOrganizationFromRequest(req)
167
+ const requestedAll = isAllOrganizationsSelection(rawSelected)
168
+ const selectedKeyPart = buildSelectionCacheKeyPart(rawSelected)
169
+ const cacheKey = selectedKeyPart
170
+ ? `org-switcher:v1:${auth.sub}:${tenantId}:${selectedKeyPart}`
171
+ : null
172
+ let cache: CacheStrategy | null = null
173
+ if (!actorIsSuperAdmin && cacheKey) {
174
+ try {
175
+ cache = container.resolve<CacheStrategy>('cache')
176
+ } catch {
177
+ cache = null
178
+ }
179
+ }
180
+
181
+ const logAccess = async (payload: { items: unknown[]; selectedId: string | null }) => {
182
+ await logCrudAccess({
183
+ container,
184
+ auth,
185
+ request: req,
186
+ items: payload.items,
187
+ idField: 'id',
188
+ resourceKind: 'directory.organization_switcher',
189
+ organizationId: payload.selectedId,
190
+ tenantId,
191
+ query: Object.fromEntries(url.searchParams.entries()),
192
+ })
193
+ }
194
+
195
+ if (cache && cacheKey) {
196
+ const activeCache = cache
197
+ try {
198
+ const cached = await runWithCacheTenant(tenantId, () => activeCache.get(cacheKey))
199
+ const parsedCached = organizationSwitcherCachePayloadSchema.safeParse(cached)
200
+ if (parsedCached.success) {
201
+ await logAccess(parsedCached.data)
202
+ return NextResponse.json(parsedCached.data)
203
+ }
204
+ } catch (err) {
205
+ logger.warn('Failed to read organization switcher cache', { err })
206
+ }
207
+ }
208
+
144
209
  const scopedOrgId = actorTenantId && actorTenantId === tenantId ? auth.orgId ?? null : null
145
210
  const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: scopedOrgId })
146
211
  const aclIsSuperAdmin = acl?.isSuperAdmin === true
@@ -163,9 +228,7 @@ export async function GET(req: NextRequest) {
163
228
  { orderBy: { name: 'ASC' } },
164
229
  )
165
230
  const hierarchy = computeHierarchyForOrganizations(orgEntities, tenantId)
166
- const rawSelected = getSelectedOrganizationFromRequest(req)
167
231
  let hasSelectionCookie = rawSelected !== null
168
- const requestedAll = isAllOrganizationsSelection(rawSelected)
169
232
  const scope = await resolveOrganizationScope({
170
233
  em,
171
234
  rbac,
@@ -194,32 +257,40 @@ export async function GET(req: NextRequest) {
194
257
  }
195
258
 
196
259
  const showMenu = menuData.nodes.length > 0 || hasManageFeature || effectiveIsSuperAdmin
197
- if (!showMenu) {
198
- return NextResponse.json({ items: [], selectedId: null, canManage: false })
199
- }
260
+ const response = showMenu
261
+ ? {
262
+ items: menuData.nodes,
263
+ selectedId,
264
+ canManage: !!hasManageFeature,
265
+ canViewAllOrganizations: accessible === null,
266
+ tenantId,
267
+ tenants: tenantRecords,
268
+ isSuperAdmin: effectiveIsSuperAdmin,
269
+ }
270
+ : {
271
+ items: [],
272
+ selectedId: null,
273
+ canManage: false,
274
+ }
200
275
 
201
- const canViewAllOrganizations = accessible === null
202
- const response = {
203
- items: menuData.nodes,
204
- selectedId,
205
- canManage: !!hasManageFeature,
206
- canViewAllOrganizations,
207
- tenantId,
208
- tenants: tenantRecords,
209
- isSuperAdmin: effectiveIsSuperAdmin,
276
+ if (cache && cacheKey) {
277
+ const activeCache = cache
278
+ try {
279
+ await runWithCacheTenant(tenantId, () =>
280
+ activeCache.set(cacheKey, response, {
281
+ ttl: ORGANIZATION_SWITCHER_CACHE_TTL_MS,
282
+ tags: [
283
+ buildOrgScopeTenantCacheTag(tenantId),
284
+ `rbac:user:${auth.sub}`,
285
+ ],
286
+ }),
287
+ )
288
+ } catch (err) {
289
+ logger.warn('Failed to write organization switcher cache', { err })
290
+ }
210
291
  }
211
292
 
212
- await logCrudAccess({
213
- container,
214
- auth,
215
- request: req,
216
- items: response.items,
217
- idField: 'id',
218
- resourceKind: 'directory.organization_switcher',
219
- organizationId: response.selectedId,
220
- tenantId,
221
- query: Object.fromEntries(url.searchParams.entries()),
222
- })
293
+ await logAccess(response)
223
294
 
224
295
  return NextResponse.json(response)
225
296
  } catch (err) {
@@ -8,6 +8,7 @@
8
8
  // for races where the event fires after a request reads the cache.
9
9
 
10
10
  import { buildOrgScopeTenantCacheTag } from '@open-mercato/core/modules/directory/utils/organizationScope'
11
+ import { runWithCacheTenant } from '@open-mercato/cache'
11
12
 
12
13
  type CacheService = {
13
14
  deleteByTags(tags: string[]): Promise<number>
@@ -34,7 +35,9 @@ export default async function handle(
34
35
  }
35
36
  if (!cache) return
36
37
  try {
37
- await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])
38
+ await runWithCacheTenant(tenantId, () =>
39
+ cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),
40
+ )
38
41
  } catch {
39
42
  // best-effort; TTL is the backstop.
40
43
  }
@@ -5,7 +5,7 @@ import { Organization } from '@open-mercato/core/modules/directory/data/entities
5
5
  import { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'
6
6
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
7
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
8
- import type { CacheStrategy } from '@open-mercato/cache'
8
+ import { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
9
9
  import { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'
10
10
  import { createLogger } from '@open-mercato/shared/lib/logger'
11
11
 
@@ -104,11 +104,15 @@ function resolveCacheFromContainer(container: AwilixContainer | null | undefined
104
104
  export async function invalidateOrganizationScopeCacheForUser(
105
105
  container: AwilixContainer,
106
106
  userId: string,
107
+ tenantId?: string | null,
107
108
  ): Promise<void> {
108
109
  const cache = resolveCacheFromContainer(container)
109
110
  if (!cache?.deleteByTags) return
110
111
  try {
111
- await cache.deleteByTags([buildOrgScopeUserCacheTag(userId)])
112
+ const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId
113
+ await runWithCacheTenant(cacheTenantId, () =>
114
+ cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),
115
+ )
112
116
  } catch (err) {
113
117
  logger.warn('Cache invalidate user failed', { err })
114
118
  }
@@ -121,7 +125,9 @@ export async function invalidateOrganizationScopeCacheForTenant(
121
125
  const cache = resolveCacheFromContainer(container)
122
126
  if (!cache?.deleteByTags) return
123
127
  try {
124
- await cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)])
128
+ await runWithCacheTenant(tenantId, () =>
129
+ cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),
130
+ )
125
131
  } catch (err) {
126
132
  logger.warn('Cache invalidate tenant failed', { err })
127
133
  }