@open-mercato/core 0.4.5-develop-6bdcebbece → 0.4.5-develop-986cfd8c37

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/AGENTS.md CHANGED
@@ -247,6 +247,8 @@ Widget injection is the preferred way to build inter-module UI extensions. Avoid
247
247
  - Declare widgets under `widgets/injection/`
248
248
  - Map them to slots via `widgets/injection-table.ts`
249
249
  - Keep metadata in colocated `*.meta.ts` files
250
+ - For headless widgets (menu items, field/column/action declarations), export declarative payloads from `widget.ts` without a React `Widget` component
251
+ - Use `InjectionPosition` from `@open-mercato/shared/modules/widgets/injection-position` for deterministic before/after/first/last placement
250
252
 
251
253
  ### Spot IDs
252
254
 
@@ -254,9 +256,21 @@ Hosts expose consistent spot ids:
254
256
  - `crud-form:<entityId>` — forms
255
257
  - `data-table:<tableId>[:header|:footer]` — data tables
256
258
  - `admin.page:<path>:before|after` — admin pages
259
+ - `menu:sidebar:main` — main sidebar items/groups
260
+ - `menu:sidebar:settings` — settings sidebar
261
+ - `menu:sidebar:profile` — profile sidebar
262
+ - `menu:topbar:profile-dropdown` — user/profile dropdown
263
+ - `menu:topbar:actions` — header action area
257
264
 
258
265
  Widgets can opt into grouped cards or tabs via `placement.kind`.
259
266
 
267
+ ### Menu Injection
268
+
269
+ - Define menu widgets with `menuItems: InjectionMenuItem[]` and map them to one or more `menu:*` spots in `widgets/injection-table.ts`.
270
+ - Prefer stable `menuItems[].id` values (`<module>-<feature>-<action>`) because sidebar customization and tests rely on these IDs.
271
+ - Always use i18n keys for labels (`labelKey`), never hard-code user-facing text in widget payloads.
272
+ - When placing relative to an existing item, provide `placement: { position: InjectionPosition.Before|After, relativeTo: '<target-id>' }`.
273
+
260
274
  ## Custom Fields
261
275
 
262
276
  ### Declaration
@@ -408,6 +422,58 @@ Output to `apps/mercato/.mercato/generated/`. Never edit manually. Never import
408
422
 
409
423
  Run `npm run modules:prepare` or rely on `predev`/`prebuild`.
410
424
 
425
+ ## Response Enrichers
426
+
427
+ Response enrichers let a module add computed fields to another module's CRUD API responses (similar to GraphQL Federation).
428
+
429
+ ### Creating an Enricher
430
+
431
+ Create `data/enrichers.ts` in your module:
432
+
433
+ ```typescript
434
+ import type { ResponseEnricher } from '@open-mercato/shared/lib/crud/response-enricher'
435
+
436
+ const myEnricher: ResponseEnricher = {
437
+ id: 'mymodule.customer-metrics',
438
+ targetEntity: 'customers.person', // entity to enrich
439
+ features: ['mymodule.view'], // required ACL features
440
+ priority: 10, // higher runs first
441
+ timeout: 2000, // ms, default 2000
442
+ fallback: { _mymodule: { count: 0 } },// returned on failure
443
+ critical: false, // true = error propagates to client
444
+ async enrichOne(record, context) {
445
+ // Add fields to a single record
446
+ return { ...record, _mymodule: { count: 42 } }
447
+ },
448
+ async enrichMany(records, context) {
449
+ // Batch enrichment (prevents N+1)
450
+ return records.map(r => ({ ...r, _mymodule: { count: 42 } }))
451
+ },
452
+ }
453
+
454
+ export const enrichers: ResponseEnricher[] = [myEnricher]
455
+ ```
456
+
457
+ ### Opt-in on CRUD routes
458
+
459
+ Target entity routes must opt in via `enrichers` option:
460
+ ```typescript
461
+ const crud = makeCrudRoute({
462
+ // ...
463
+ enrichers: { entityId: 'customers.person' },
464
+ })
465
+ ```
466
+
467
+ ### Key Rules
468
+
469
+ - MUST implement `enrichMany()` for batch endpoints (prevents N+1 queries)
470
+ - MUST namespace enriched fields with `_moduleName` prefix (e.g. `_example.todoCount`)
471
+ - MUST use `features` array for ACL gating — enricher runs only if user has all listed features
472
+ - Export fields are stripped: `_meta` and `_`-prefixed fields are removed from CSV/Excel exports
473
+ - Enrichers run after `CrudHooks.afterList`, before HTTP response serialization
474
+ - `critical: true` propagates errors to the HTTP response; `false` (default) uses fallback silently
475
+ - Run `yarn generate` after adding `data/enrichers.ts` to auto-discover
476
+
411
477
  ## Upgrade Actions
412
478
 
413
479
  Declare once per version in `src/modules/configs/lib/upgrade-actions.ts`. Keep them idempotent, reuse module helpers. Access guarded by `configs.manage`.
@@ -9,7 +9,8 @@ import { User } from "@open-mercato/core/modules/auth/data/entities";
9
9
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
10
10
  import { buildPasswordSchema } from "@open-mercato/shared/lib/auth/passwordPolicy";
11
11
  const profileResponseSchema = z.object({
12
- email: z.string().email()
12
+ email: z.string().email(),
13
+ roles: z.array(z.string())
13
14
  });
14
15
  const passwordSchema = buildPasswordSchema();
15
16
  const updateSchema = z.object({
@@ -56,7 +57,7 @@ async function GET(req) {
56
57
  if (!user) {
57
58
  return NextResponse.json({ error: translate("auth.users.form.errors.notFound", "User not found") }, { status: 404 });
58
59
  }
59
- return NextResponse.json({ email: String(user.email) });
60
+ return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] });
60
61
  } catch (err) {
61
62
  console.error("auth.profile.load failed", err);
62
63
  return NextResponse.json({ error: translate("auth.profile.form.errors.load", "Failed to load profile.") }, { status: 400 });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/profile/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchema = z.object({\n email: z.string().email().optional(),\n password: passwordSchema.optional(),\n}).refine((data) => Boolean(data.email || data.password), {\n message: 'Provide an email or password.',\n path: ['email'],\n})\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n return NextResponse.json({ email: String(user.email) })\n } catch (err) {\n console.error('auth.profile.load failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const authService = container.resolve('authService') as AuthService\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('auth.profile.update failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address for the signed-in user.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchema = z.object({\n email: z.string().email().optional(),\n password: passwordSchema.optional(),\n}).refine((data) => Boolean(data.email || data.password), {\n message: 'Provide an email or password.',\n path: ['email'],\n})\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] })\n } catch (err) {\n console.error('auth.profile.load failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = updateSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const authService = container.resolve('authService') as AuthService\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (err instanceof CrudHttpError) {\n return NextResponse.json(err.body, { status: err.status })\n }\n console.error('auth.profile.update failed', err)\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address for the signed-in user.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -102,11 +102,17 @@ function applySidebarPreference(groups, settings) {
102
102
  if (!orderIndex.has(id)) orderIndex.set(id, idx);
103
103
  });
104
104
  const hiddenSet = new Set(normalized.hiddenItems ?? []);
105
+ const resolveItemKey = (item) => {
106
+ const candidate = item.id?.trim();
107
+ if (candidate && candidate.length > 0) return candidate;
108
+ return item.href;
109
+ };
105
110
  const applyItems = (items) => {
106
111
  return items.map((item) => {
107
- const override = normalized.itemLabels?.[item.href];
112
+ const itemKey = resolveItemKey(item);
113
+ const override = normalized.itemLabels?.[itemKey] ?? normalized.itemLabels?.[item.href];
108
114
  const nextChildren = item.children ? applyItems(item.children) : void 0;
109
- const hidden = hiddenSet.has(item.href);
115
+ const hidden = hiddenSet.has(itemKey) || hiddenSet.has(item.href);
110
116
  const next = {
111
117
  ...item,
112
118
  title: override && override.trim().length > 0 ? override.trim() : item.defaultTitle,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/services/sidebarPreferencesService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleSidebarPreference, User, UserSidebarPreference } from '../data/entities'\nimport {\n SIDEBAR_PREFERENCES_VERSION,\n SidebarPreferencesSettings,\n normalizeSidebarSettings,\n} from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\nexport type SidebarPreferenceScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n locale: string\n}\n\nexport type RoleSidebarPreferenceScope = {\n roleId: string\n tenantId?: string | null\n locale: string\n}\n\nexport type SidebarItemLike<T = Record<string, unknown>> = {\n href: string\n title: string\n defaultTitle: string\n children?: SidebarItemLike<T>[]\n} & T\n\nexport type SidebarGroupLike<T = Record<string, unknown>> = {\n id: string\n name: string\n defaultName: string\n items: SidebarItemLike<T>[]\n weight?: number\n} & T\n\nexport async function loadSidebarPreference(\n em: EntityManager,\n scope: SidebarPreferenceScope,\n): Promise<SidebarPreferencesSettings> {\n const { userId, tenantId, organizationId, locale } = normalizeScope(scope)\n const existing = await em.findOne(UserSidebarPreference, { user: userId, tenantId, organizationId, locale })\n return normalizeSidebarSettings(existing?.settingsJson as SidebarPreferencesSettings | undefined)\n}\n\nexport async function saveSidebarPreference(\n em: EntityManager,\n scope: SidebarPreferenceScope,\n input: SidebarPreferencesSettings,\n): Promise<SidebarPreferencesSettings> {\n const normalized = normalizeSidebarSettings({\n ...input,\n version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,\n })\n const { userId, tenantId, organizationId, locale } = normalizeScope(scope)\n let pref = await em.findOne(UserSidebarPreference, { user: userId, tenantId, organizationId, locale })\n if (!pref) {\n pref = em.create(UserSidebarPreference, {\n user: em.getReference(User, userId),\n tenantId,\n organizationId,\n locale,\n settingsJson: normalized,\n createdAt: new Date(),\n })\n } else {\n pref.settingsJson = normalized\n }\n await em.flush()\n return normalized\n}\n\nexport async function loadRoleSidebarPreferences(\n em: EntityManager,\n options: { roleIds: string[]; tenantId?: string | null; locale: string },\n): Promise<Map<string, SidebarPreferencesSettings>> {\n if (!options.roleIds.length) return new Map()\n const tenantId = options.tenantId ?? null\n const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }\n const prefs = await em.find(RoleSidebarPreference, {\n role: { $in: options.roleIds },\n tenantId: tenantFilter,\n locale: options.locale,\n })\n const map = new Map<string, SidebarPreferencesSettings>()\n for (const pref of prefs) {\n const key = pref.role.id\n if (tenantId !== null) {\n const existing = map.get(key)\n if (existing && pref.tenantId === null) continue\n if (!existing || pref.tenantId === tenantId) {\n map.set(key, normalizeSidebarSettings(pref.settingsJson as SidebarPreferencesSettings | undefined))\n }\n continue\n }\n map.set(key, normalizeSidebarSettings(pref.settingsJson as SidebarPreferencesSettings | undefined))\n }\n return map\n}\n\nexport async function loadFirstRoleSidebarPreference(\n em: EntityManager,\n options: { roleIds: string[]; tenantId?: string | null; locale: string },\n): Promise<SidebarPreferencesSettings | null> {\n if (!options.roleIds.length) return null\n const tenantId = options.tenantId ?? null\n const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }\n const prefs = await em.find(RoleSidebarPreference, {\n role: { $in: options.roleIds },\n tenantId: tenantFilter,\n locale: options.locale,\n })\n if (!prefs.length) return null\n const ordered = options.roleIds\n .map((id) => {\n if (tenantId !== null) {\n const specific = prefs.find((pref) => pref.role.id === id && pref.tenantId === tenantId)\n if (specific) return specific\n }\n return prefs.find((pref) => pref.role.id === id && pref.tenantId === null)\n })\n .filter(Boolean) as RoleSidebarPreference[]\n const first = ordered[0] ?? prefs[0]\n return normalizeSidebarSettings(first?.settingsJson as SidebarPreferencesSettings | undefined)\n}\n\nexport async function saveRoleSidebarPreference(\n em: EntityManager,\n scope: RoleSidebarPreferenceScope,\n input: SidebarPreferencesSettings,\n): Promise<SidebarPreferencesSettings> {\n const normalized = normalizeSidebarSettings({\n ...input,\n version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,\n })\n const { roleId, tenantId, locale } = normalizeRoleScope(scope)\n let pref = await em.findOne(RoleSidebarPreference, { role: roleId, tenantId, locale })\n if (!pref) {\n pref = em.create(RoleSidebarPreference, {\n role: em.getReference(Role, roleId),\n tenantId,\n locale,\n settingsJson: normalized,\n createdAt: new Date(),\n })\n } else {\n pref.settingsJson = normalized\n }\n await em.flush()\n return normalized\n}\n\nexport function applySidebarPreference<T extends SidebarGroupLike>(\n groups: T[],\n settings?: SidebarPreferencesSettings | null,\n): T[] {\n const normalized = normalizeSidebarSettings(settings)\n const orderIndex = new Map<string, number>()\n normalized.groupOrder?.forEach((id, idx) => {\n if (!orderIndex.has(id)) orderIndex.set(id, idx)\n })\n const hiddenSet = new Set(normalized.hiddenItems ?? [])\n const applyItems = <TI extends SidebarItemLike>(items: TI[]): TI[] => {\n return items.map((item) => {\n const override = normalized.itemLabels?.[item.href]\n const nextChildren = item.children ? applyItems(item.children) : undefined\n const hidden = hiddenSet.has(item.href)\n const next = {\n ...item,\n title: override && override.trim().length > 0 ? override.trim() : item.defaultTitle,\n children: nextChildren,\n } as TI & { hidden?: boolean }\n next.hidden = hidden\n return next\n })\n }\n const mapped = groups.map((group) => {\n const override = normalized.groupLabels?.[group.id]\n return {\n ...group,\n name: override && override.trim().length > 0 ? override.trim() : group.defaultName,\n items: applyItems(group.items),\n }\n })\n mapped.sort((a, b) => {\n const ao = orderIndex.has(a.id) ? orderIndex.get(a.id)! : Number.POSITIVE_INFINITY\n const bo = orderIndex.has(b.id) ? orderIndex.get(b.id)! : Number.POSITIVE_INFINITY\n if (ao !== bo) return ao - bo\n const aw = typeof a.weight === 'number' ? a.weight : 10_000\n const bw = typeof b.weight === 'number' ? b.weight : 10_000\n if (aw !== bw) return aw - bw\n return a.defaultName.localeCompare(b.defaultName)\n })\n return mapped\n}\n\nfunction normalizeScope(scope: SidebarPreferenceScope) {\n return {\n userId: scope.userId,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n locale: scope.locale,\n }\n}\n\nfunction normalizeRoleScope(scope: RoleSidebarPreferenceScope) {\n return {\n roleId: scope.roleId,\n tenantId: scope.tenantId ?? null,\n locale: scope.locale,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleSidebarPreference, User, UserSidebarPreference } from '../data/entities'\nimport {\n SIDEBAR_PREFERENCES_VERSION,\n SidebarPreferencesSettings,\n normalizeSidebarSettings,\n} from '@open-mercato/shared/modules/navigation/sidebarPreferences'\n\nexport type SidebarPreferenceScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n locale: string\n}\n\nexport type RoleSidebarPreferenceScope = {\n roleId: string\n tenantId?: string | null\n locale: string\n}\n\nexport type SidebarItemLike<T = Record<string, unknown>> = {\n id?: string\n href: string\n title: string\n defaultTitle: string\n children?: SidebarItemLike<T>[]\n} & T\n\nexport type SidebarGroupLike<T = Record<string, unknown>> = {\n id: string\n name: string\n defaultName: string\n items: SidebarItemLike<T>[]\n weight?: number\n} & T\n\nexport async function loadSidebarPreference(\n em: EntityManager,\n scope: SidebarPreferenceScope,\n): Promise<SidebarPreferencesSettings> {\n const { userId, tenantId, organizationId, locale } = normalizeScope(scope)\n const existing = await em.findOne(UserSidebarPreference, { user: userId, tenantId, organizationId, locale })\n return normalizeSidebarSettings(existing?.settingsJson as SidebarPreferencesSettings | undefined)\n}\n\nexport async function saveSidebarPreference(\n em: EntityManager,\n scope: SidebarPreferenceScope,\n input: SidebarPreferencesSettings,\n): Promise<SidebarPreferencesSettings> {\n const normalized = normalizeSidebarSettings({\n ...input,\n version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,\n })\n const { userId, tenantId, organizationId, locale } = normalizeScope(scope)\n let pref = await em.findOne(UserSidebarPreference, { user: userId, tenantId, organizationId, locale })\n if (!pref) {\n pref = em.create(UserSidebarPreference, {\n user: em.getReference(User, userId),\n tenantId,\n organizationId,\n locale,\n settingsJson: normalized,\n createdAt: new Date(),\n })\n } else {\n pref.settingsJson = normalized\n }\n await em.flush()\n return normalized\n}\n\nexport async function loadRoleSidebarPreferences(\n em: EntityManager,\n options: { roleIds: string[]; tenantId?: string | null; locale: string },\n): Promise<Map<string, SidebarPreferencesSettings>> {\n if (!options.roleIds.length) return new Map()\n const tenantId = options.tenantId ?? null\n const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }\n const prefs = await em.find(RoleSidebarPreference, {\n role: { $in: options.roleIds },\n tenantId: tenantFilter,\n locale: options.locale,\n })\n const map = new Map<string, SidebarPreferencesSettings>()\n for (const pref of prefs) {\n const key = pref.role.id\n if (tenantId !== null) {\n const existing = map.get(key)\n if (existing && pref.tenantId === null) continue\n if (!existing || pref.tenantId === tenantId) {\n map.set(key, normalizeSidebarSettings(pref.settingsJson as SidebarPreferencesSettings | undefined))\n }\n continue\n }\n map.set(key, normalizeSidebarSettings(pref.settingsJson as SidebarPreferencesSettings | undefined))\n }\n return map\n}\n\nexport async function loadFirstRoleSidebarPreference(\n em: EntityManager,\n options: { roleIds: string[]; tenantId?: string | null; locale: string },\n): Promise<SidebarPreferencesSettings | null> {\n if (!options.roleIds.length) return null\n const tenantId = options.tenantId ?? null\n const tenantFilter = tenantId === null ? null : { $in: [tenantId, null] }\n const prefs = await em.find(RoleSidebarPreference, {\n role: { $in: options.roleIds },\n tenantId: tenantFilter,\n locale: options.locale,\n })\n if (!prefs.length) return null\n const ordered = options.roleIds\n .map((id) => {\n if (tenantId !== null) {\n const specific = prefs.find((pref) => pref.role.id === id && pref.tenantId === tenantId)\n if (specific) return specific\n }\n return prefs.find((pref) => pref.role.id === id && pref.tenantId === null)\n })\n .filter(Boolean) as RoleSidebarPreference[]\n const first = ordered[0] ?? prefs[0]\n return normalizeSidebarSettings(first?.settingsJson as SidebarPreferencesSettings | undefined)\n}\n\nexport async function saveRoleSidebarPreference(\n em: EntityManager,\n scope: RoleSidebarPreferenceScope,\n input: SidebarPreferencesSettings,\n): Promise<SidebarPreferencesSettings> {\n const normalized = normalizeSidebarSettings({\n ...input,\n version: input?.version ?? SIDEBAR_PREFERENCES_VERSION,\n })\n const { roleId, tenantId, locale } = normalizeRoleScope(scope)\n let pref = await em.findOne(RoleSidebarPreference, { role: roleId, tenantId, locale })\n if (!pref) {\n pref = em.create(RoleSidebarPreference, {\n role: em.getReference(Role, roleId),\n tenantId,\n locale,\n settingsJson: normalized,\n createdAt: new Date(),\n })\n } else {\n pref.settingsJson = normalized\n }\n await em.flush()\n return normalized\n}\n\nexport function applySidebarPreference<T extends SidebarGroupLike>(\n groups: T[],\n settings?: SidebarPreferencesSettings | null,\n): T[] {\n const normalized = normalizeSidebarSettings(settings)\n const orderIndex = new Map<string, number>()\n normalized.groupOrder?.forEach((id, idx) => {\n if (!orderIndex.has(id)) orderIndex.set(id, idx)\n })\n const hiddenSet = new Set(normalized.hiddenItems ?? [])\n const resolveItemKey = (item: SidebarItemLike): string => {\n const candidate = item.id?.trim()\n if (candidate && candidate.length > 0) return candidate\n return item.href\n }\n const applyItems = <TI extends SidebarItemLike>(items: TI[]): TI[] => {\n return items.map((item) => {\n const itemKey = resolveItemKey(item)\n const override = normalized.itemLabels?.[itemKey] ?? normalized.itemLabels?.[item.href]\n const nextChildren = item.children ? applyItems(item.children) : undefined\n const hidden = hiddenSet.has(itemKey) || hiddenSet.has(item.href)\n const next = {\n ...item,\n title: override && override.trim().length > 0 ? override.trim() : item.defaultTitle,\n children: nextChildren,\n } as TI & { hidden?: boolean }\n next.hidden = hidden\n return next\n })\n }\n const mapped = groups.map((group) => {\n const override = normalized.groupLabels?.[group.id]\n return {\n ...group,\n name: override && override.trim().length > 0 ? override.trim() : group.defaultName,\n items: applyItems(group.items),\n }\n })\n mapped.sort((a, b) => {\n const ao = orderIndex.has(a.id) ? orderIndex.get(a.id)! : Number.POSITIVE_INFINITY\n const bo = orderIndex.has(b.id) ? orderIndex.get(b.id)! : Number.POSITIVE_INFINITY\n if (ao !== bo) return ao - bo\n const aw = typeof a.weight === 'number' ? a.weight : 10_000\n const bw = typeof b.weight === 'number' ? b.weight : 10_000\n if (aw !== bw) return aw - bw\n return a.defaultName.localeCompare(b.defaultName)\n })\n return mapped\n}\n\nfunction normalizeScope(scope: SidebarPreferenceScope) {\n return {\n userId: scope.userId,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n locale: scope.locale,\n }\n}\n\nfunction normalizeRoleScope(scope: RoleSidebarPreferenceScope) {\n return {\n roleId: scope.roleId,\n tenantId: scope.tenantId ?? null,\n locale: scope.locale,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -52,6 +52,7 @@ const crud = makeCrudRoute({
52
52
  tenantField: "tenantId",
53
53
  softDeleteField: "deletedAt"
54
54
  },
55
+ enrichers: { entityId: "customers.person" },
55
56
  list: {
56
57
  schema: listSchema,
57
58
  entityId: E.customers.customer_entity,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/api/people/route.ts"],
4
- "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CustomerEntity } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { personCreateSchema, personUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../utils'\nimport { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any, ctx) => {\n const filters: Record<string, any> = { kind: { $eq: 'person' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n filters.display_name = { $ilike: `%${escapeLikePattern(query.search)}%` }\n }\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }\n } else if (emailContains) {\n filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as any\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_person_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore custom field filter errors; fall back to base filters\n }\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_person_profile,\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item: any) => {\n if (!item) return item\n const normalized = { ...item }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.people.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n personId: result?.personId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.people.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.people.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.person_required', 'Person id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst personListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst personCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n personId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Person',\n pluralName: 'People',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(personListItemSchema),\n create: {\n schema: personCreateSchema,\n responseSchema: personCreateResponseSchema,\n description: 'Creates a person contact using scoped organization and tenant identifiers.',\n },\n update: {\n schema: personUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates contact details or custom fields for a person.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a person by id. Request body or query may provide the identifier.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CustomerEntity } from '../../data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { personCreateSchema, personUpdateSchema } from '../../data/validators'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { withScopedPayload } from '../utils'\nimport { buildCustomFieldFiltersFromQuery, extractAllCustomFieldEntries, splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport {\n createCustomersCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n email: z.string().optional(),\n emailStartsWith: z.string().optional(),\n emailContains: z.string().optional(),\n status: z.string().optional(),\n lifecycleStage: z.string().optional(),\n source: z.string().optional(),\n hasEmail: z.string().optional(),\n hasPhone: z.string().optional(),\n hasNextInteraction: z.string().optional(),\n createdFrom: z.string().optional(),\n createdTo: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n id: z.string().uuid().optional(),\n tagIds: z.string().optional(),\n tagIdsEmpty: z.string().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.people.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerEntity,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n enrichers: { entityId: 'customers.person' },\n list: {\n schema: listSchema,\n entityId: E.customers.customer_entity,\n fields: [\n 'id',\n 'display_name',\n 'description',\n 'owner_user_id',\n 'primary_email',\n 'primary_phone',\n 'status',\n 'lifecycle_stage',\n 'source',\n 'next_interaction_at',\n 'next_interaction_name',\n 'next_interaction_ref_id',\n 'next_interaction_icon',\n 'next_interaction_color',\n 'organization_id',\n 'tenant_id',\n 'kind',\n 'created_at',\n ],\n sortFieldMap: {\n name: 'display_name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n },\n buildFilters: async (query: any, ctx) => {\n const filters: Record<string, any> = { kind: { $eq: 'person' } }\n if (query.id) filters.id = { $eq: query.id }\n if (query.search) {\n filters.display_name = { $ilike: `%${escapeLikePattern(query.search)}%` }\n }\n const email = typeof query.email === 'string' ? query.email.trim().toLowerCase() : ''\n const emailStartsWith = typeof query.emailStartsWith === 'string' ? query.emailStartsWith.trim().toLowerCase() : ''\n const emailContains = typeof query.emailContains === 'string' ? query.emailContains.trim().toLowerCase() : ''\n if (email) {\n filters.primary_email = { $eq: email }\n } else if (emailStartsWith) {\n filters.primary_email = { $ilike: `${escapeLikePattern(emailStartsWith)}%` }\n } else if (emailContains) {\n filters.primary_email = { $ilike: `%${escapeLikePattern(emailContains)}%` }\n }\n if (query.status) {\n filters.status = { $eq: query.status }\n }\n if (query.lifecycleStage) {\n filters.lifecycle_stage = { $eq: query.lifecycleStage }\n }\n if (query.source) {\n filters.source = { $eq: query.source }\n }\n const tagIdsRaw = typeof query.tagIds === 'string' ? query.tagIds : ''\n const tagIds = tagIdsRaw\n .split(',')\n .map((value: string) => value.trim())\n .filter((value: string) => value.length > 0)\n const tagIdsEmpty = parseBooleanToken(query.tagIdsEmpty) === true\n if (tagIdsEmpty) {\n filters.id = { $eq: '00000000-0000-0000-0000-000000000000' }\n } else if (tagIds.length > 0) {\n filters['tag_assignments.tag_id'] = { $in: tagIds }\n }\n const hasEmail = parseBooleanToken(query.hasEmail)\n if (!email && !emailStartsWith && !emailContains && hasEmail !== null) {\n filters.primary_email = { $exists: hasEmail }\n }\n const hasPhone = parseBooleanToken(query.hasPhone)\n if (hasPhone !== null) {\n filters.primary_phone = { $exists: hasPhone }\n }\n const hasNextInteraction = parseBooleanToken(query.hasNextInteraction)\n if (hasNextInteraction !== null) {\n filters.next_interaction_at = { $exists: hasNextInteraction }\n }\n const createdRange: Record<string, Date> = {}\n if (query.createdFrom) {\n const from = new Date(query.createdFrom)\n if (!Number.isNaN(from.getTime())) createdRange.$gte = from\n }\n if (query.createdTo) {\n const to = new Date(query.createdTo)\n if (!Number.isNaN(to.getTime())) createdRange.$lte = to\n }\n if (Object.keys(createdRange).length) {\n filters.created_at = createdRange\n }\n if (ctx) {\n try {\n const em = ctx.container.resolve('em') as any\n const cfFilters = await buildCustomFieldFiltersFromQuery({\n entityIds: [E.customers.customer_entity, E.customers.customer_person_profile],\n query,\n em,\n tenantId: ctx.auth?.tenantId ?? null,\n })\n Object.assign(filters, cfFilters)\n } catch {\n // ignore custom field filter errors; fall back to base filters\n }\n }\n return filters\n },\n customFieldSources: [\n {\n entityId: E.customers.customer_person_profile,\n table: 'customer_people',\n alias: 'person_profile',\n recordIdColumn: 'id',\n join: { fromField: 'id', toField: 'entity_id' },\n },\n ],\n joins: [\n {\n alias: 'tag_assignments',\n table: 'customer_tag_assignments',\n from: { field: 'id' },\n to: { field: 'entity_id' },\n type: 'left',\n },\n ],\n transformItem: (item: any) => {\n if (!item) return item\n const normalized = { ...item }\n delete normalized.kind\n const cfEntries = extractAllCustomFieldEntries(item)\n for (const key of Object.keys(normalized)) {\n if (key.startsWith('cf:')) {\n delete normalized[key]\n }\n }\n return { ...normalized, ...cfEntries }\n },\n },\n actions: {\n create: {\n commandId: 'customers.people.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personCreateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: ({ result }) => ({\n id: result?.entityId ?? result?.id ?? null,\n personId: result?.personId ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'customers.people.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n const { base, custom } = splitCustomFieldPayload(scoped)\n const parsed = personUpdateSchema.parse(base)\n return Object.keys(custom).length ? { ...parsed, customFields: custom } : parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.people.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) throw new CrudHttpError(400, { error: translate('customers.errors.person_required', 'Person id is required') })\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nconst personListItemSchema = z.object({\n id: z.string().uuid(),\n display_name: z.string().optional(),\n description: z.string().nullable().optional(),\n owner_user_id: z.string().uuid().nullable().optional(),\n primary_email: z.string().nullable().optional(),\n primary_phone: z.string().nullable().optional(),\n status: z.string().nullable().optional(),\n lifecycle_stage: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n next_interaction_at: z.string().nullable().optional(),\n next_interaction_name: z.string().nullable().optional(),\n next_interaction_ref_id: z.string().nullable().optional(),\n next_interaction_icon: z.string().nullable().optional(),\n next_interaction_color: z.string().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n created_at: z.string().nullable().optional(),\n})\n\nconst personCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n personId: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Person',\n pluralName: 'People',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(personListItemSchema),\n create: {\n schema: personCreateSchema,\n responseSchema: personCreateResponseSchema,\n description: 'Creates a person contact using scoped organization and tenant identifiers.',\n },\n update: {\n schema: personUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates contact details or custom fields for a person.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Deletes a person by id. Request body or query may provide the identifier.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,8 @@ import {
5
5
  getCoreInjectionTables,
6
6
  invalidateInjectionWidgetCache,
7
7
  loadAllInjectionWidgets,
8
+ loadInjectionDataWidgetById,
9
+ loadInjectionDataWidgetsForSpot,
8
10
  loadInjectionWidgetById,
9
11
  loadInjectionWidgetsForSpot
10
12
  } from "@open-mercato/shared/modules/widgets/injection-loader";
@@ -13,6 +15,8 @@ export {
13
15
  getCoreInjectionWidgets,
14
16
  invalidateInjectionWidgetCache,
15
17
  loadAllInjectionWidgets,
18
+ loadInjectionDataWidgetById,
19
+ loadInjectionDataWidgetsForSpot,
16
20
  loadInjectionWidgetById,
17
21
  loadInjectionWidgetsForSpot,
18
22
  registerCoreInjectionTables,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/widgets/lib/injection.ts"],
4
- "sourcesContent": ["// Re-export from shared for backward compatibility\n// New code should import directly from @open-mercato/shared/modules/widgets/injection-loader\nexport {\n registerCoreInjectionWidgets,\n getCoreInjectionWidgets,\n registerCoreInjectionTables,\n getCoreInjectionTables,\n invalidateInjectionWidgetCache,\n loadAllInjectionWidgets,\n loadInjectionWidgetById,\n loadInjectionWidgetsForSpot,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\n"],
5
- "mappings": "AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;",
4
+ "sourcesContent": ["// Re-export from shared for backward compatibility\n// New code should import directly from @open-mercato/shared/modules/widgets/injection-loader\nexport {\n registerCoreInjectionWidgets,\n getCoreInjectionWidgets,\n registerCoreInjectionTables,\n getCoreInjectionTables,\n invalidateInjectionWidgetCache,\n loadAllInjectionWidgets,\n loadInjectionDataWidgetById,\n loadInjectionDataWidgetsForSpot,\n loadInjectionWidgetById,\n loadInjectionWidgetsForSpot,\n type LoadedInjectionDataWidget,\n type LoadedInjectionWidget,\n} from '@open-mercato/shared/modules/widgets/injection-loader'\n"],
5
+ "mappings": "AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.4.5-develop-6bdcebbece",
3
+ "version": "0.4.5-develop-986cfd8c37",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -207,7 +207,7 @@
207
207
  }
208
208
  },
209
209
  "dependencies": {
210
- "@open-mercato/shared": "0.4.5-develop-6bdcebbece",
210
+ "@open-mercato/shared": "0.4.5-develop-986cfd8c37",
211
211
  "@types/semver": "^7.5.8",
212
212
  "@xyflow/react": "^12.6.0",
213
213
  "ai": "^6.0.0",
@@ -15,6 +15,7 @@ import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolic
15
15
 
16
16
  const profileResponseSchema = z.object({
17
17
  email: z.string().email(),
18
+ roles: z.array(z.string()),
18
19
  })
19
20
 
20
21
  const passwordSchema = buildPasswordSchema()
@@ -67,7 +68,7 @@ export async function GET(req: Request) {
67
68
  if (!user) {
68
69
  return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })
69
70
  }
70
- return NextResponse.json({ email: String(user.email) })
71
+ return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] })
71
72
  } catch (err) {
72
73
  console.error('auth.profile.load failed', err)
73
74
  return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })
@@ -20,6 +20,7 @@ export type RoleSidebarPreferenceScope = {
20
20
  }
21
21
 
22
22
  export type SidebarItemLike<T = Record<string, unknown>> = {
23
+ id?: string
23
24
  href: string
24
25
  title: string
25
26
  defaultTitle: string
@@ -160,11 +161,17 @@ export function applySidebarPreference<T extends SidebarGroupLike>(
160
161
  if (!orderIndex.has(id)) orderIndex.set(id, idx)
161
162
  })
162
163
  const hiddenSet = new Set(normalized.hiddenItems ?? [])
164
+ const resolveItemKey = (item: SidebarItemLike): string => {
165
+ const candidate = item.id?.trim()
166
+ if (candidate && candidate.length > 0) return candidate
167
+ return item.href
168
+ }
163
169
  const applyItems = <TI extends SidebarItemLike>(items: TI[]): TI[] => {
164
170
  return items.map((item) => {
165
- const override = normalized.itemLabels?.[item.href]
171
+ const itemKey = resolveItemKey(item)
172
+ const override = normalized.itemLabels?.[itemKey] ?? normalized.itemLabels?.[item.href]
166
173
  const nextChildren = item.children ? applyItems(item.children) : undefined
167
- const hidden = hiddenSet.has(item.href)
174
+ const hidden = hiddenSet.has(itemKey) || hiddenSet.has(item.href)
168
175
  const next = {
169
176
  ...item,
170
177
  title: override && override.trim().length > 0 ? override.trim() : item.defaultTitle,
@@ -60,6 +60,7 @@ const crud = makeCrudRoute({
60
60
  tenantField: 'tenantId',
61
61
  softDeleteField: 'deletedAt',
62
62
  },
63
+ enrichers: { entityId: 'customers.person' },
63
64
  list: {
64
65
  schema: listSchema,
65
66
  entityId: E.customers.customer_entity,