@open-mercato/core 0.6.4-develop.4113.1.5e87922616 → 0.6.4-develop.4133.1.48fc6c8f7b

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 (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/lib/sessionIntegrity.js +16 -13
  3. package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
  4. package/dist/modules/customers/api/utils.js +14 -9
  5. package/dist/modules/customers/api/utils.js.map +2 -2
  6. package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
  7. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
  8. package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
  9. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  10. package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
  11. package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
  12. package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
  13. package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
  14. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
  15. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  16. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
  17. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
  18. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
  19. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
  20. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
  21. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
  22. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
  23. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
  25. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
  27. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
  29. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  30. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
  31. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  32. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
  33. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  34. package/dist/modules/directory/utils/organizationScope.js +33 -20
  35. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  36. package/package.json +7 -7
  37. package/src/modules/auth/lib/sessionIntegrity.ts +37 -16
  38. package/src/modules/customers/api/utils.ts +17 -11
  39. package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
  40. package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
  41. package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
  42. package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
  43. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
  44. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
  45. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
  46. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
  47. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
  48. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
  49. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
  50. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
  51. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
  52. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
  53. package/src/modules/directory/utils/organizationScope.ts +51 -20
@@ -1,4 +1,4 @@
1
- [build:core] found 2645 entry points
1
+ [build:core] found 2648 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 172 entry points
4
4
  [build:core:generated] built successfully
@@ -34,25 +34,26 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
34
34
  return null;
35
35
  }
36
36
  }
37
- if (sessionId !== null) {
38
- const session = await findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null });
39
- if (!session) return null;
40
- if (session.expiresAt.getTime() < Date.now()) return null;
41
- }
42
- const user = await findOneWithDecryption(
37
+ const sessionPromise = sessionId !== null ? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null }) : Promise.resolve(null);
38
+ const userPromise = findOneWithDecryption(
43
39
  em,
44
40
  User,
45
41
  { id: subjectId, deletedAt: null },
46
42
  void 0,
47
43
  { tenantId: actorTenantId, organizationId: actorOrganizationId }
48
44
  );
45
+ const [session, user] = await Promise.all([sessionPromise, userPromise]);
46
+ if (sessionId !== null) {
47
+ if (!session) return null;
48
+ if (session.expiresAt.getTime() < Date.now()) return null;
49
+ }
49
50
  if (!user) return null;
50
51
  const currentTenantId = normalizeScopeId(user.tenantId ?? null);
51
52
  const currentOrganizationId = normalizeScopeId(user.organizationId ?? null);
52
53
  if (currentTenantId === INVALID_SCOPE || currentOrganizationId === INVALID_SCOPE || currentTenantId !== actorTenantId || currentOrganizationId !== actorOrganizationId) {
53
54
  return null;
54
55
  }
55
- const links = currentTenantId ? await findWithDecryption(
56
+ const linksPromise = currentTenantId ? findWithDecryption(
56
57
  em,
57
58
  UserRole,
58
59
  {
@@ -62,10 +63,12 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
62
63
  },
63
64
  { populate: ["role"] },
64
65
  { tenantId: currentTenantId, organizationId: currentOrganizationId }
65
- ) : [];
66
+ ) : Promise.resolve([]);
67
+ const userAclSuperAdminPromise = currentTenantId ? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId) : Promise.resolve(false);
68
+ const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise]);
66
69
  const linkedRoles = links.map((link) => link.role).filter((role) => !!role);
67
70
  const roles = linkedRoles.map((role) => role.name).filter((name) => typeof name === "string" && name.trim().length > 0);
68
- const isSuperAdmin = currentTenantId ? await hasSuperAdminFlag(em, user.id, linkedRoles, currentTenantId, currentOrganizationId) : false;
71
+ const isSuperAdmin = currentTenantId ? userAclSuperAdmin || await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId) : false;
69
72
  return {
70
73
  ...auth,
71
74
  sub: user.id,
@@ -75,7 +78,7 @@ async function resolveCanonicalStaffAuthContext(em, auth) {
75
78
  isSuperAdmin
76
79
  };
77
80
  }
78
- async function hasSuperAdminFlag(em, userId, linkedRoles, tenantId, organizationId) {
81
+ async function userAclGrantsSuperAdmin(em, userId, tenantId, organizationId) {
79
82
  const userAcl = await findOneWithDecryption(
80
83
  em,
81
84
  UserAcl,
@@ -88,9 +91,9 @@ async function hasSuperAdminFlag(em, userId, linkedRoles, tenantId, organization
88
91
  void 0,
89
92
  { tenantId, organizationId }
90
93
  );
91
- if (userAcl && userAcl.isSuperAdmin === true) {
92
- return true;
93
- }
94
+ return !!(userAcl && userAcl.isSuperAdmin === true);
95
+ }
96
+ async function roleAclGrantsSuperAdmin(em, linkedRoles, tenantId, organizationId) {
94
97
  const roleIds = Array.from(
95
98
  new Set(
96
99
  linkedRoles.map((role) => role?.id ? String(role.id) : null).filter((id) => typeof id === "string" && id.length > 0)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/lib/sessionIntegrity.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Role, RoleAcl, Session, User, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\nconst INVALID_SCOPE = Symbol('invalid-scope')\n\ntype NormalizedScopeId = string | null | typeof INVALID_SCOPE\n\nfunction normalizeScopeId(value: unknown): NormalizedScopeId {\n if (value === null || value === undefined) return null\n if (typeof value !== 'string') return INVALID_SCOPE\n const trimmed = value.trim()\n if (!trimmed) return null\n return UUID_RE.test(trimmed) ? trimmed : INVALID_SCOPE\n}\n\nfunction resolveActorTenantId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorTenantId = (auth as { actorTenantId?: unknown }).actorTenantId\n return normalizeScopeId(actorTenantId ?? auth.tenantId ?? null)\n}\n\nfunction resolveActorOrganizationId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorOrgId = (auth as { actorOrgId?: unknown }).actorOrgId\n return normalizeScopeId(actorOrgId ?? auth.orgId ?? null)\n}\n\nexport async function resolveCanonicalStaffAuthContext(\n em: EntityManager,\n auth: AuthContext,\n): Promise<AuthContext> {\n if (!auth) return null\n if (auth.isApiKey) return auth\n\n const subjectId = normalizeScopeId(auth.sub)\n const actorTenantId = resolveActorTenantId(auth)\n const actorOrganizationId = resolveActorOrganizationId(auth)\n if (\n subjectId === INVALID_SCOPE ||\n actorTenantId === INVALID_SCOPE ||\n actorOrganizationId === INVALID_SCOPE\n ) {\n return null\n }\n\n // Session binding: when the JWT carries an `sid` claim, require the referenced session to\n // still exist (not soft-deleted, not expired). This is what makes logout / password-reset\n // actually invalidate an already-issued JWT.\n //\n // Legacy tokens (pre-migration, without `sid`) are allowed through during the grace period\n // (controlled by JWT_LEGACY_GRACE_MINUTES) so that rolling deployments don't force-logout\n // every user. Once the grace period expires these tokens will fail signature verification\n // in `verifyJwt` before reaching this point.\n const sessionId = normalizeScopeId(typeof auth.sid === 'string' ? auth.sid : null)\n if (sessionId === INVALID_SCOPE) return null\n if (sessionId === null) {\n // Legacy token without sid \u2014 allow only if it was verified via the legacy fallback path.\n // The `_legacyToken` flag is set by `verifyJwt` when a token passes raw-secret verification\n // but fails audience-derived verification. Without this flag, reject.\n if ((auth as Record<string, unknown>)._legacyToken === true) {\n // Allow through without session validation \u2014 the token will expire naturally\n } else {\n return null\n }\n }\n if (sessionId !== null) {\n const session = await findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n }\n\n const user = await findOneWithDecryption(\n em,\n User,\n { id: subjectId, deletedAt: null },\n undefined,\n { tenantId: actorTenantId, organizationId: actorOrganizationId },\n )\n if (!user) return null\n\n const currentTenantId = normalizeScopeId(user.tenantId ?? null)\n const currentOrganizationId = normalizeScopeId(user.organizationId ?? null)\n if (\n currentTenantId === INVALID_SCOPE ||\n currentOrganizationId === INVALID_SCOPE ||\n currentTenantId !== actorTenantId ||\n currentOrganizationId !== actorOrganizationId\n ) {\n return null\n }\n\n const links = currentTenantId\n ? await findWithDecryption(\n em,\n UserRole,\n {\n user: user.id,\n deletedAt: null,\n role: { tenantId: currentTenantId, deletedAt: null } as unknown as Role,\n } as never,\n { populate: ['role'] },\n { tenantId: currentTenantId, organizationId: currentOrganizationId },\n )\n : []\n\n const linkedRoles = links\n .map((link) => link.role)\n .filter((role): role is Role => !!role)\n\n const roles = linkedRoles\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n\n const isSuperAdmin = currentTenantId\n ? await hasSuperAdminFlag(em, user.id, linkedRoles, currentTenantId, currentOrganizationId)\n : false\n\n return {\n ...auth,\n sub: user.id,\n tenantId: currentTenantId,\n orgId: currentOrganizationId,\n roles,\n isSuperAdmin,\n }\n}\n\nasync function hasSuperAdminFlag(\n em: EntityManager,\n userId: string,\n linkedRoles: Role[],\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const userAcl = await findOneWithDecryption(\n em,\n UserAcl,\n {\n user: userId,\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n if (userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true) {\n return true\n }\n\n const roleIds = Array.from(\n new Set(\n linkedRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n if (!roleIds.length) return false\n\n const roleAcl = await findOneWithDecryption(\n em,\n RoleAcl,\n {\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n role: { $in: roleIds },\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(roleAcl && (roleAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nexport async function isAuthContextValid(\n em: EntityManager,\n auth: AuthContext,\n): Promise<boolean> {\n return (await resolveCanonicalStaffAuthContext(em, auth)) !== null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Role, RoleAcl, Session, User, UserAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i\nconst INVALID_SCOPE = Symbol('invalid-scope')\n\ntype NormalizedScopeId = string | null | typeof INVALID_SCOPE\n\nfunction normalizeScopeId(value: unknown): NormalizedScopeId {\n if (value === null || value === undefined) return null\n if (typeof value !== 'string') return INVALID_SCOPE\n const trimmed = value.trim()\n if (!trimmed) return null\n return UUID_RE.test(trimmed) ? trimmed : INVALID_SCOPE\n}\n\nfunction resolveActorTenantId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorTenantId = (auth as { actorTenantId?: unknown }).actorTenantId\n return normalizeScopeId(actorTenantId ?? auth.tenantId ?? null)\n}\n\nfunction resolveActorOrganizationId(auth: NonNullable<AuthContext>): NormalizedScopeId {\n const actorOrgId = (auth as { actorOrgId?: unknown }).actorOrgId\n return normalizeScopeId(actorOrgId ?? auth.orgId ?? null)\n}\n\nexport async function resolveCanonicalStaffAuthContext(\n em: EntityManager,\n auth: AuthContext,\n): Promise<AuthContext> {\n if (!auth) return null\n if (auth.isApiKey) return auth\n\n const subjectId = normalizeScopeId(auth.sub)\n const actorTenantId = resolveActorTenantId(auth)\n const actorOrganizationId = resolveActorOrganizationId(auth)\n if (\n subjectId === INVALID_SCOPE ||\n actorTenantId === INVALID_SCOPE ||\n actorOrganizationId === INVALID_SCOPE\n ) {\n return null\n }\n\n // Session binding: when the JWT carries an `sid` claim, require the referenced session to\n // still exist (not soft-deleted, not expired). This is what makes logout / password-reset\n // actually invalidate an already-issued JWT.\n //\n // Legacy tokens (pre-migration, without `sid`) are allowed through during the grace period\n // (controlled by JWT_LEGACY_GRACE_MINUTES) so that rolling deployments don't force-logout\n // every user. Once the grace period expires these tokens will fail signature verification\n // in `verifyJwt` before reaching this point.\n const sessionId = normalizeScopeId(typeof auth.sid === 'string' ? auth.sid : null)\n if (sessionId === INVALID_SCOPE) return null\n if (sessionId === null) {\n // Legacy token without sid \u2014 allow only if it was verified via the legacy fallback path.\n // The `_legacyToken` flag is set by `verifyJwt` when a token passes raw-secret verification\n // but fails audience-derived verification. Without this flag, reject.\n if ((auth as Record<string, unknown>)._legacyToken === true) {\n // Allow through without session validation \u2014 the token will expire naturally\n } else {\n return null\n }\n }\n // The session-revocation check and the user load are independent (neither reads\n // the other's result), so they run concurrently to collapse two sequential DB\n // round-trips into one. The `em` here is a fresh request-scoped EntityManager\n // (resolved per request, never inside an explicit transaction), so concurrent\n // reads on it are safe.\n const sessionPromise = sessionId !== null\n ? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })\n : Promise.resolve(null)\n const userPromise = findOneWithDecryption(\n em,\n User,\n { id: subjectId, deletedAt: null },\n undefined,\n { tenantId: actorTenantId, organizationId: actorOrganizationId },\n )\n const [session, user] = await Promise.all([sessionPromise, userPromise])\n\n if (sessionId !== null) {\n if (!session) return null\n if (session.expiresAt.getTime() < Date.now()) return null\n }\n\n if (!user) return null\n\n const currentTenantId = normalizeScopeId(user.tenantId ?? null)\n const currentOrganizationId = normalizeScopeId(user.organizationId ?? null)\n if (\n currentTenantId === INVALID_SCOPE ||\n currentOrganizationId === INVALID_SCOPE ||\n currentTenantId !== actorTenantId ||\n currentOrganizationId !== actorOrganizationId\n ) {\n return null\n }\n\n // Role links and the per-user super-admin flag are likewise independent, so they\n // run concurrently. The role-level super-admin lookup depends on the resolved\n // role ids, so it stays sequential after the links resolve (and is skipped\n // entirely when the per-user flag already grants super-admin).\n const linksPromise = currentTenantId\n ? findWithDecryption(\n em,\n UserRole,\n {\n user: user.id,\n deletedAt: null,\n role: { tenantId: currentTenantId, deletedAt: null } as unknown as Role,\n } as never,\n { populate: ['role'] },\n { tenantId: currentTenantId, organizationId: currentOrganizationId },\n )\n : Promise.resolve([] as UserRole[])\n const userAclSuperAdminPromise = currentTenantId\n ? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId)\n : Promise.resolve(false)\n const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise])\n\n const linkedRoles = links\n .map((link) => link.role)\n .filter((role): role is Role => !!role)\n\n const roles = linkedRoles\n .map((role) => role.name)\n .filter((name): name is string => typeof name === 'string' && name.trim().length > 0)\n\n const isSuperAdmin = currentTenantId\n ? userAclSuperAdmin || (await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId))\n : false\n\n return {\n ...auth,\n sub: user.id,\n tenantId: currentTenantId,\n orgId: currentOrganizationId,\n roles,\n isSuperAdmin,\n }\n}\n\nasync function userAclGrantsSuperAdmin(\n em: EntityManager,\n userId: string,\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const userAcl = await findOneWithDecryption(\n em,\n UserAcl,\n {\n user: userId,\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nasync function roleAclGrantsSuperAdmin(\n em: EntityManager,\n linkedRoles: Role[],\n tenantId: string,\n organizationId: string | null,\n): Promise<boolean> {\n const roleIds = Array.from(\n new Set(\n linkedRoles\n .map((role) => (role?.id ? String(role.id) : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0),\n ),\n )\n if (!roleIds.length) return false\n\n const roleAcl = await findOneWithDecryption(\n em,\n RoleAcl,\n {\n tenantId,\n isSuperAdmin: true,\n deletedAt: null,\n role: { $in: roleIds },\n } as never,\n undefined,\n { tenantId, organizationId },\n )\n return !!(roleAcl && (roleAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)\n}\n\nexport async function isAuthContextValid(\n em: EntityManager,\n auth: AuthContext,\n): Promise<boolean> {\n return (await resolveCanonicalStaffAuthContext(em, auth)) !== null\n}\n"],
5
+ "mappings": "AAEA,SAAS,uBAAuB,0BAA0B;AAC1D,SAAe,SAAS,SAAS,MAAM,SAAS,gBAAgB;AAEhE,MAAM,UAAU;AAChB,MAAM,gBAAgB,uBAAO,eAAe;AAI5C,SAAS,iBAAiB,OAAmC;AAC3D,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,KAAK,OAAO,IAAI,UAAU;AAC3C;AAEA,SAAS,qBAAqB,MAAmD;AAC/E,QAAM,gBAAiB,KAAqC;AAC5D,SAAO,iBAAiB,iBAAiB,KAAK,YAAY,IAAI;AAChE;AAEA,SAAS,2BAA2B,MAAmD;AACrF,QAAM,aAAc,KAAkC;AACtD,SAAO,iBAAiB,cAAc,KAAK,SAAS,IAAI;AAC1D;AAEA,eAAsB,iCACpB,IACA,MACsB;AACtB,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,SAAU,QAAO;AAE1B,QAAM,YAAY,iBAAiB,KAAK,GAAG;AAC3C,QAAM,gBAAgB,qBAAqB,IAAI;AAC/C,QAAM,sBAAsB,2BAA2B,IAAI;AAC3D,MACE,cAAc,iBACd,kBAAkB,iBAClB,wBAAwB,eACxB;AACA,WAAO;AAAA,EACT;AAUA,QAAM,YAAY,iBAAiB,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM,IAAI;AACjF,MAAI,cAAc,cAAe,QAAO;AACxC,MAAI,cAAc,MAAM;AAItB,QAAK,KAAiC,iBAAiB,MAAM;AAAA,IAE7D,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AAMA,QAAM,iBAAiB,cAAc,OACjC,sBAAsB,IAAI,SAAS,EAAE,IAAI,WAAW,WAAW,KAAK,CAAC,IACrE,QAAQ,QAAQ,IAAI;AACxB,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,WAAW,WAAW,KAAK;AAAA,IACjC;AAAA,IACA,EAAE,UAAU,eAAe,gBAAgB,oBAAoB;AAAA,EACjE;AACA,QAAM,CAAC,SAAS,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,gBAAgB,WAAW,CAAC;AAEvE,MAAI,cAAc,MAAM;AACtB,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,UAAU,QAAQ,IAAI,KAAK,IAAI,EAAG,QAAO;AAAA,EACvD;AAEA,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,kBAAkB,iBAAiB,KAAK,YAAY,IAAI;AAC9D,QAAM,wBAAwB,iBAAiB,KAAK,kBAAkB,IAAI;AAC1E,MACE,oBAAoB,iBACpB,0BAA0B,iBAC1B,oBAAoB,iBACpB,0BAA0B,qBAC1B;AACA,WAAO;AAAA,EACT;AAMA,QAAM,eAAe,kBACjB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAM,KAAK;AAAA,MACX,WAAW;AAAA,MACX,MAAM,EAAE,UAAU,iBAAiB,WAAW,KAAK;AAAA,IACrD;AAAA,IACA,EAAE,UAAU,CAAC,MAAM,EAAE;AAAA,IACrB,EAAE,UAAU,iBAAiB,gBAAgB,sBAAsB;AAAA,EACrE,IACA,QAAQ,QAAQ,CAAC,CAAe;AACpC,QAAM,2BAA2B,kBAC7B,wBAAwB,IAAI,KAAK,IAAI,iBAAiB,qBAAqB,IAC3E,QAAQ,QAAQ,KAAK;AACzB,QAAM,CAAC,OAAO,iBAAiB,IAAI,MAAM,QAAQ,IAAI,CAAC,cAAc,wBAAwB,CAAC;AAE7F,QAAM,cAAc,MACjB,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,OAAO,CAAC,SAAuB,CAAC,CAAC,IAAI;AAExC,QAAM,QAAQ,YACX,IAAI,CAAC,SAAS,KAAK,IAAI,EACvB,OAAO,CAAC,SAAyB,OAAO,SAAS,YAAY,KAAK,KAAK,EAAE,SAAS,CAAC;AAEtF,QAAM,eAAe,kBACjB,qBAAsB,MAAM,wBAAwB,IAAI,aAAa,iBAAiB,qBAAqB,IAC3G;AAEJ,SAAO;AAAA,IACL,GAAG;AAAA,IACH,KAAK,KAAK;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,IACP;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAe,wBACb,IACA,QACA,UACA,gBACkB;AAClB,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN;AAAA,MACA,cAAc;AAAA,MACd,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,SAAO,CAAC,EAAE,WAAY,QAAuC,iBAAiB;AAChF;AAEA,eAAe,wBACb,IACA,aACA,UACA,gBACkB;AAClB,QAAM,UAAU,MAAM;AAAA,IACpB,IAAI;AAAA,MACF,YACG,IAAI,CAAC,SAAU,MAAM,KAAK,OAAO,KAAK,EAAE,IAAI,IAAK,EACjD,OAAO,CAAC,OAAqB,OAAO,OAAO,YAAY,GAAG,SAAS,CAAC;AAAA,IACzE;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAE5B,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,MACA,cAAc;AAAA,MACd,WAAW;AAAA,MACX,MAAM,EAAE,KAAK,QAAQ;AAAA,IACvB;AAAA,IACA;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,EAC7B;AACA,SAAO,CAAC,EAAE,WAAY,QAAuC,iBAAiB;AAChF;AAEA,eAAsB,mBACpB,IACA,MACkB;AAClB,SAAQ,MAAM,iCAAiC,IAAI,IAAI,MAAO;AAChE;",
6
6
  "names": []
7
7
  }
@@ -117,16 +117,21 @@ async function findMatchingEntityIdsBySearchTokensAcrossSources({
117
117
  const trimmed = query.trim();
118
118
  if (!trimmed) return null;
119
119
  const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources);
120
+ const perSource = await Promise.all(
121
+ enrichedSources.map(async (source) => {
122
+ const rawIds = await findSearchTokenEntityIds({
123
+ ctx,
124
+ entityType: source.entityType,
125
+ fields: source.fields,
126
+ query: trimmed
127
+ });
128
+ if (rawIds === null) return null;
129
+ return source.mapToEntityIds ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds }) : rawIds;
130
+ })
131
+ );
120
132
  const matchedIds = /* @__PURE__ */ new Set();
121
- for (const source of enrichedSources) {
122
- const rawIds = await findSearchTokenEntityIds({
123
- ctx,
124
- entityType: source.entityType,
125
- fields: source.fields,
126
- query: trimmed
127
- });
128
- if (rawIds === null) return null;
129
- const entityIds = source.mapToEntityIds ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds }) : rawIds;
133
+ for (const entityIds of perSource) {
134
+ if (entityIds === null) return null;
130
135
  entityIds.forEach((id) => matchedIds.add(id));
131
136
  }
132
137
  return Array.from(matchedIds);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/api/utils.ts"],
4
- "sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { SortDir } from '@open-mercato/shared/lib/query/types'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\ntype SearchTokenMatchInput = {\n ctx: CrudCtx\n entityType: string\n fields: string[]\n query: string\n}\n\ntype SearchTokenSource = {\n entityType: string\n fields: string[]\n mapToEntityIds?: {\n table: string\n sourceColumn?: string\n targetColumn: string\n tenantColumn?: string\n organizationColumn?: string\n }\n}\n\nasync function enrichSearchSourcesWithCustomFieldTokens(\n ctx: CrudCtx,\n sources: SearchTokenSource[],\n): Promise<SearchTokenSource[]> {\n const entityTypes = Array.from(\n new Set(\n sources\n .map((source) => source.entityType)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n if (!entityTypes.length) return sources\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let defsQuery = db\n .selectFrom('custom_field_defs')\n .select(['entity_id', 'key', 'kind'])\n .where('entity_id', 'in', entityTypes)\n .where('is_active', '=', true)\n\n const tenantScope = ctx.auth?.tenantId ?? null\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('tenant_id', '=', tenantScope),\n eb('tenant_id', 'is', null),\n ]))\n\n if (ctx.selectedOrganizationId) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', '=', ctx.selectedOrganizationId),\n eb('organization_id', 'is', null),\n ]))\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', 'in', ctx.organizationIds),\n eb('organization_id', 'is', null),\n ]))\n }\n\n const customFieldKeysByEntity = new Map<string, Set<string>>()\n const rows = await defsQuery.execute()\n for (const row of rows as Array<{ entity_id?: unknown; key?: unknown; kind?: unknown }>) {\n if (row.kind === 'attachment') continue\n const entityType = typeof row.entity_id === 'string' ? row.entity_id : null\n const key = typeof row.key === 'string' ? row.key.trim() : ''\n if (!entityType || !key) continue\n const bucket = customFieldKeysByEntity.get(entityType) ?? new Set<string>()\n bucket.add(`cf:${key}`)\n customFieldKeysByEntity.set(entityType, bucket)\n }\n\n return sources.map((source) => {\n const customFieldKeys = customFieldKeysByEntity.get(source.entityType)\n return {\n ...source,\n fields: Array.from(new Set([\n 'search_text',\n ...source.fields,\n ...(customFieldKeys ? Array.from(customFieldKeys) : []),\n ])),\n }\n })\n}\n\nasync function findSearchTokenEntityIds({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const tokens = tokenizeText(trimmed, resolveSearchConfig())\n if (!tokens.hashes.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let searchQuery = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('field', 'in', fields)\n .where('token_hash', 'in', tokens.hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)\n\n if (ctx.auth?.tenantId !== undefined) {\n searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)\n }\n\n const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nasync function mapScopedEntityIds({\n ctx,\n ids,\n config,\n}: {\n ctx: CrudCtx\n ids: string[]\n config: NonNullable<SearchTokenSource['mapToEntityIds']>\n}): Promise<string[]> {\n if (!ids.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n const sourceColumn = config.sourceColumn ?? 'id'\n const tenantColumn = config.tenantColumn ?? 'tenant_id'\n const organizationColumn = config.organizationColumn ?? 'organization_id'\n\n let mapQuery = db\n .selectFrom(config.table)\n .select(config.targetColumn)\n .where(sourceColumn, 'in', ids)\n\n if (ctx.auth?.tenantId !== undefined) {\n mapQuery = mapQuery.where(sql<boolean>`${sql.ref(tenantColumn)} is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n mapQuery = mapQuery.where(organizationColumn, '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n mapQuery = mapQuery.where(organizationColumn, 'in', ctx.organizationIds)\n }\n\n const rows = await mapQuery.execute() as Array<Record<string, unknown>>\n return rows\n .map((row) => {\n const value = row[config.targetColumn]\n return typeof value === 'string' ? value : null\n })\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nexport async function findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n sources,\n query,\n}: {\n ctx: CrudCtx\n sources: SearchTokenSource[]\n query: string\n}): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)\n const matchedIds = new Set<string>()\n for (const source of enrichedSources) {\n const rawIds = await findSearchTokenEntityIds({\n ctx,\n entityType: source.entityType,\n fields: source.fields,\n query: trimmed,\n })\n if (rawIds === null) return null\n const entityIds = source.mapToEntityIds\n ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })\n : rawIds\n entityIds.forEach((id) => matchedIds.add(id))\n }\n\n return Array.from(matchedIds)\n}\n\nexport async function findMatchingEntityIdsBySearchTokens({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n return findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query,\n sources: [{ entityType, fields }],\n })\n}\n\nexport function applyEntityIdRestriction(\n filters: Record<string, unknown>,\n ids: string[] | null,\n): void {\n if (ids === null) return\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n\n if (currentEq) {\n filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID }\n}\n\nexport function applyEntityIdExclusion(\n filters: Record<string, unknown>,\n ids: string[],\n): void {\n const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.length > 0)))\n if (!uniqueIds.length) return\n\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown; $nin?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n const currentIn = Array.isArray(currentIdFilter?.$in)\n ? currentIdFilter.$in.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : null\n const currentNotIn = Array.isArray(currentIdFilter?.$nin)\n ? currentIdFilter.$nin.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n\n if (currentEq) {\n filters.id = uniqueIds.includes(currentEq) ? { $eq: NO_MATCH_ID } : { $eq: currentEq }\n return\n }\n\n if (currentIn) {\n const nextIds = currentIn.filter((id) => !uniqueIds.includes(id))\n filters.id = nextIds.length > 0 ? { $in: nextIds } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = {\n ...(currentIdFilter ?? {}),\n $nin: Array.from(new Set([...currentNotIn, ...uniqueIds])),\n }\n}\n\nexport async function findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId,\n filters,\n customFieldSources,\n joins,\n}: {\n ctx: CrudCtx\n entityId: EntityId\n filters: Record<string, unknown>\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n}): Promise<string[]> {\n const qe = ctx.container.resolve('queryEngine') as QueryEngine\n const ids = new Set<string>()\n const pageSize = 100\n let page = 1\n let total = 0\n\n do {\n const result = await qe.query(entityId, {\n fields: ['id'],\n filters,\n page: { page, pageSize },\n sort: [{ field: 'id', dir: SortDir.Asc }],\n tenantId: ctx.auth?.tenantId ?? undefined,\n organizationId: ctx.selectedOrganizationId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n customFieldSources,\n joins,\n })\n\n total = result.total ?? 0\n for (const item of result.items ?? []) {\n const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null\n if (typeof id === 'string' && id.length > 0) {\n ids.add(id)\n }\n }\n if (!result.items?.length) break\n page += 1\n } while (ids.size < total)\n\n return Array.from(ids)\n}\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { QueryCustomFieldSource, QueryJoinEdge, QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'\nimport { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'\nimport { SortDir } from '@open-mercato/shared/lib/query/types'\n\nconst { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({\n messages: {\n tenantRequired: { key: 'customers.errors.tenant_required', fallback: 'Tenant context is required' },\n organizationRequired: { key: 'customers.errors.organization_required', fallback: 'Organization context is required' },\n },\n})\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\ntype SearchTokenMatchInput = {\n ctx: CrudCtx\n entityType: string\n fields: string[]\n query: string\n}\n\ntype SearchTokenSource = {\n entityType: string\n fields: string[]\n mapToEntityIds?: {\n table: string\n sourceColumn?: string\n targetColumn: string\n tenantColumn?: string\n organizationColumn?: string\n }\n}\n\nasync function enrichSearchSourcesWithCustomFieldTokens(\n ctx: CrudCtx,\n sources: SearchTokenSource[],\n): Promise<SearchTokenSource[]> {\n const entityTypes = Array.from(\n new Set(\n sources\n .map((source) => source.entityType)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n if (!entityTypes.length) return sources\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let defsQuery = db\n .selectFrom('custom_field_defs')\n .select(['entity_id', 'key', 'kind'])\n .where('entity_id', 'in', entityTypes)\n .where('is_active', '=', true)\n\n const tenantScope = ctx.auth?.tenantId ?? null\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('tenant_id', '=', tenantScope),\n eb('tenant_id', 'is', null),\n ]))\n\n if (ctx.selectedOrganizationId) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', '=', ctx.selectedOrganizationId),\n eb('organization_id', 'is', null),\n ]))\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n defsQuery = defsQuery.where((eb: any) => eb.or([\n eb('organization_id', 'in', ctx.organizationIds),\n eb('organization_id', 'is', null),\n ]))\n }\n\n const customFieldKeysByEntity = new Map<string, Set<string>>()\n const rows = await defsQuery.execute()\n for (const row of rows as Array<{ entity_id?: unknown; key?: unknown; kind?: unknown }>) {\n if (row.kind === 'attachment') continue\n const entityType = typeof row.entity_id === 'string' ? row.entity_id : null\n const key = typeof row.key === 'string' ? row.key.trim() : ''\n if (!entityType || !key) continue\n const bucket = customFieldKeysByEntity.get(entityType) ?? new Set<string>()\n bucket.add(`cf:${key}`)\n customFieldKeysByEntity.set(entityType, bucket)\n }\n\n return sources.map((source) => {\n const customFieldKeys = customFieldKeysByEntity.get(source.entityType)\n return {\n ...source,\n fields: Array.from(new Set([\n 'search_text',\n ...source.fields,\n ...(customFieldKeys ? Array.from(customFieldKeys) : []),\n ])),\n }\n })\n}\n\nasync function findSearchTokenEntityIds({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const tokens = tokenizeText(trimmed, resolveSearchConfig())\n if (!tokens.hashes.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n let searchQuery = db\n .selectFrom('search_tokens')\n .select('entity_id')\n .where('entity_type', '=', entityType)\n .where('field', 'in', fields)\n .where('token_hash', 'in', tokens.hashes)\n .groupBy('entity_id')\n .having(sql<boolean>`count(distinct token_hash) >= ${tokens.hashes.length}`)\n\n if (ctx.auth?.tenantId !== undefined) {\n searchQuery = searchQuery.where(sql<boolean>`tenant_id is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n searchQuery = searchQuery.where('organization_id', '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n searchQuery = searchQuery.where('organization_id', 'in', ctx.organizationIds)\n }\n\n const rows = await searchQuery.execute() as Array<{ entity_id?: unknown }>\n return rows\n .map((row) => (typeof row.entity_id === 'string' ? row.entity_id : null))\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nasync function mapScopedEntityIds({\n ctx,\n ids,\n config,\n}: {\n ctx: CrudCtx\n ids: string[]\n config: NonNullable<SearchTokenSource['mapToEntityIds']>\n}): Promise<string[]> {\n if (!ids.length) return []\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = em.getKysely<any>() as any\n const sourceColumn = config.sourceColumn ?? 'id'\n const tenantColumn = config.tenantColumn ?? 'tenant_id'\n const organizationColumn = config.organizationColumn ?? 'organization_id'\n\n let mapQuery = db\n .selectFrom(config.table)\n .select(config.targetColumn)\n .where(sourceColumn, 'in', ids)\n\n if (ctx.auth?.tenantId !== undefined) {\n mapQuery = mapQuery.where(sql<boolean>`${sql.ref(tenantColumn)} is not distinct from ${ctx.auth?.tenantId ?? null}`)\n }\n if (ctx.selectedOrganizationId) {\n mapQuery = mapQuery.where(organizationColumn, '=', ctx.selectedOrganizationId)\n } else if (Array.isArray(ctx.organizationIds) && ctx.organizationIds.length > 0) {\n mapQuery = mapQuery.where(organizationColumn, 'in', ctx.organizationIds)\n }\n\n const rows = await mapQuery.execute() as Array<Record<string, unknown>>\n return rows\n .map((row) => {\n const value = row[config.targetColumn]\n return typeof value === 'string' ? value : null\n })\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n}\n\nexport async function findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n sources,\n query,\n}: {\n ctx: CrudCtx\n sources: SearchTokenSource[]\n query: string\n}): Promise<string[] | null> {\n const trimmed = query.trim()\n if (!trimmed) return null\n\n const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)\n const perSource = await Promise.all(\n enrichedSources.map(async (source) => {\n const rawIds = await findSearchTokenEntityIds({\n ctx,\n entityType: source.entityType,\n fields: source.fields,\n query: trimmed,\n })\n if (rawIds === null) return null\n return source.mapToEntityIds\n ? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })\n : rawIds\n }),\n )\n\n const matchedIds = new Set<string>()\n for (const entityIds of perSource) {\n if (entityIds === null) return null\n entityIds.forEach((id) => matchedIds.add(id))\n }\n\n return Array.from(matchedIds)\n}\n\nexport async function findMatchingEntityIdsBySearchTokens({\n ctx,\n entityType,\n fields,\n query,\n}: SearchTokenMatchInput): Promise<string[] | null> {\n return findMatchingEntityIdsBySearchTokensAcrossSources({\n ctx,\n query,\n sources: [{ entityType, fields }],\n })\n}\n\nexport function applyEntityIdRestriction(\n filters: Record<string, unknown>,\n ids: string[] | null,\n): void {\n if (ids === null) return\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n\n if (currentEq) {\n filters.id = ids.includes(currentEq) ? { $eq: currentEq } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = ids.length > 0 ? { $in: ids } : { $eq: NO_MATCH_ID }\n}\n\nexport function applyEntityIdExclusion(\n filters: Record<string, unknown>,\n ids: string[],\n): void {\n const uniqueIds = Array.from(new Set(ids.filter((id) => typeof id === 'string' && id.length > 0)))\n if (!uniqueIds.length) return\n\n const currentIdFilter =\n filters.id && typeof filters.id === 'object' && !Array.isArray(filters.id)\n ? (filters.id as { $eq?: unknown; $in?: unknown; $nin?: unknown })\n : null\n const currentEq = typeof currentIdFilter?.$eq === 'string' ? currentIdFilter.$eq : null\n const currentIn = Array.isArray(currentIdFilter?.$in)\n ? currentIdFilter.$in.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : null\n const currentNotIn = Array.isArray(currentIdFilter?.$nin)\n ? currentIdFilter.$nin.filter((value): value is string => typeof value === 'string' && value.length > 0)\n : []\n\n if (currentEq) {\n filters.id = uniqueIds.includes(currentEq) ? { $eq: NO_MATCH_ID } : { $eq: currentEq }\n return\n }\n\n if (currentIn) {\n const nextIds = currentIn.filter((id) => !uniqueIds.includes(id))\n filters.id = nextIds.length > 0 ? { $in: nextIds } : { $eq: NO_MATCH_ID }\n return\n }\n\n filters.id = {\n ...(currentIdFilter ?? {}),\n $nin: Array.from(new Set([...currentNotIn, ...uniqueIds])),\n }\n}\n\nexport async function findMatchingEntityIdsWithQueryEngine({\n ctx,\n entityId,\n filters,\n customFieldSources,\n joins,\n}: {\n ctx: CrudCtx\n entityId: EntityId\n filters: Record<string, unknown>\n customFieldSources?: QueryCustomFieldSource[]\n joins?: QueryJoinEdge[]\n}): Promise<string[]> {\n const qe = ctx.container.resolve('queryEngine') as QueryEngine\n const ids = new Set<string>()\n const pageSize = 100\n let page = 1\n let total = 0\n\n do {\n const result = await qe.query(entityId, {\n fields: ['id'],\n filters,\n page: { page, pageSize },\n sort: [{ field: 'id', dir: SortDir.Asc }],\n tenantId: ctx.auth?.tenantId ?? undefined,\n organizationId: ctx.selectedOrganizationId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n customFieldSources,\n joins,\n })\n\n total = result.total ?? 0\n for (const item of result.items ?? []) {\n const id = item && typeof item === 'object' ? (item as Record<string, unknown>).id : null\n if (typeof id === 'string' && id.length > 0) {\n ids.add(id)\n }\n }\n if (!result.items?.length) break\n page += 1\n } while (ids.size < total)\n\n return Array.from(ids)\n}\n\nexport { withScopedPayload, parseScopedCommandInput }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,137 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
+ import {
7
+ createWidgetDataService,
8
+ WidgetDataValidationError
9
+ } from "../../../../services/widgetDataService.js";
10
+ import { runWidgetDataBatch } from "../../../../lib/widgetDataBatch.js";
11
+ import { dashboardsTag, dashboardsErrorSchema } from "../../../openapi.js";
12
+ import { widgetDataRequestSchema, widgetDataResponseSchema } from "../schema.js";
13
+ const metadata = {
14
+ POST: { requireAuth: true, requireFeatures: ["analytics.view"] }
15
+ };
16
+ const MAX_BATCH_SIZE = 50;
17
+ const widgetDataBatchRequestSchema = z.object({
18
+ requests: z.array(
19
+ z.object({
20
+ id: z.string().min(1),
21
+ request: widgetDataRequestSchema
22
+ })
23
+ ).min(1).max(MAX_BATCH_SIZE)
24
+ });
25
+ const widgetDataBatchResponseSchema = z.object({
26
+ results: z.array(
27
+ z.discriminatedUnion("ok", [
28
+ z.object({
29
+ id: z.string(),
30
+ ok: z.literal(true),
31
+ data: widgetDataResponseSchema
32
+ }),
33
+ z.object({
34
+ id: z.string(),
35
+ ok: z.literal(false),
36
+ error: z.string()
37
+ })
38
+ ])
39
+ )
40
+ });
41
+ async function POST(req) {
42
+ const auth = await getAuthFromRequest(req);
43
+ if (!auth) {
44
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
45
+ }
46
+ let body;
47
+ try {
48
+ body = await req.json();
49
+ } catch {
50
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
51
+ }
52
+ const parsed = widgetDataBatchRequestSchema.safeParse(body);
53
+ if (!parsed.success) {
54
+ return NextResponse.json(
55
+ { error: "Invalid request payload", issues: parsed.error.issues },
56
+ { status: 400 }
57
+ );
58
+ }
59
+ const tenantId = auth.tenantId ?? null;
60
+ if (!tenantId) {
61
+ return NextResponse.json({ error: "Tenant context is required" }, { status: 400 });
62
+ }
63
+ const container = await createRequestContainer();
64
+ const analyticsRegistry = container.resolve("analyticsRegistry");
65
+ const em = container.resolve("em").fork({
66
+ clear: true,
67
+ freshEventManager: true,
68
+ useContext: true
69
+ });
70
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
71
+ const organizationIds = (() => {
72
+ if (scope?.selectedId) return [scope.selectedId];
73
+ if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds;
74
+ if (scope?.allowedIds === null) return void 0;
75
+ if (auth.orgId) return [auth.orgId];
76
+ return void 0;
77
+ })();
78
+ const cache = container.resolve("cache");
79
+ const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache);
80
+ const rbacService = container.resolve("rbacService");
81
+ try {
82
+ const results = await runWidgetDataBatch(parsed.data.requests, {
83
+ getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),
84
+ checkFeatures: (features) => {
85
+ if (features.length === 0) return Promise.resolve(true);
86
+ return rbacService.userHasAllFeatures(auth.sub, features, {
87
+ tenantId,
88
+ organizationId: auth.orgId
89
+ });
90
+ },
91
+ fetchOne: (request) => service.fetchWidgetData(request),
92
+ describeError: (error) => error instanceof WidgetDataValidationError ? error.message : "An error occurred while processing your request"
93
+ });
94
+ return NextResponse.json({ results });
95
+ } catch (err) {
96
+ console.error("[widgets/data/batch] Error:", err);
97
+ return NextResponse.json(
98
+ { error: "An error occurred while processing your request" },
99
+ { status: 500 }
100
+ );
101
+ }
102
+ }
103
+ const widgetDataBatchPostDoc = {
104
+ summary: "Fetch aggregated data for multiple dashboard widgets in one request",
105
+ description: "Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.",
106
+ tags: [dashboardsTag],
107
+ requestBody: {
108
+ contentType: "application/json",
109
+ schema: widgetDataBatchRequestSchema,
110
+ description: "A list of id-keyed widget data requests to resolve together."
111
+ },
112
+ responses: [
113
+ {
114
+ status: 200,
115
+ description: "Per-widget aggregation results keyed by request id.",
116
+ schema: widgetDataBatchResponseSchema
117
+ }
118
+ ],
119
+ errors: [
120
+ { status: 400, description: "Invalid request payload", schema: dashboardsErrorSchema },
121
+ { status: 401, description: "Authentication required", schema: dashboardsErrorSchema },
122
+ { status: 500, description: "Internal server error", schema: dashboardsErrorSchema }
123
+ ]
124
+ };
125
+ const openApi = {
126
+ tag: dashboardsTag,
127
+ summary: "Batch widget data aggregation endpoint",
128
+ methods: {
129
+ POST: widgetDataBatchPostDoc
130
+ }
131
+ };
132
+ export {
133
+ POST,
134
+ metadata,
135
+ openApi
136
+ };
137
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/dashboards/api/widgets/data/batch/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../../services/widgetDataService'\nimport { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'\nimport type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from '../schema'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst MAX_BATCH_SIZE = 50\n\nconst widgetDataBatchRequestSchema = z.object({\n requests: z\n .array(\n z.object({\n id: z.string().min(1),\n request: widgetDataRequestSchema,\n }),\n )\n .min(1)\n .max(MAX_BATCH_SIZE),\n})\n\nconst widgetDataBatchResponseSchema = z.object({\n results: z.array(\n z.discriminatedUnion('ok', [\n z.object({\n id: z.string(),\n ok: z.literal(true),\n data: widgetDataResponseSchema,\n }),\n z.object({\n id: z.string(),\n ok: z.literal(false),\n error: z.string(),\n }),\n ]),\n ),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataBatchRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n // Build the per-request DI/RBAC/org-scope stack exactly once for the whole\n // batch instead of once per widget (see issue #2273).\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n\n try {\n const results = await runWidgetDataBatch(parsed.data.requests as Array<{ id: string; request: WidgetDataRequest }>, {\n getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),\n checkFeatures: (features) => {\n if (features.length === 0) return Promise.resolve(true)\n return rbacService.userHasAllFeatures(auth.sub, features, {\n tenantId,\n organizationId: auth.orgId,\n })\n },\n fetchOne: (request) => service.fetchWidgetData(request),\n describeError: (error) =>\n error instanceof WidgetDataValidationError\n ? error.message\n : 'An error occurred while processing your request',\n })\n return NextResponse.json({ results })\n } catch (err) {\n console.error('[widgets/data/batch] Error:', err)\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataBatchPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for multiple dashboard widgets in one request',\n description:\n 'Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataBatchRequestSchema,\n description: 'A list of id-keyed widget data requests to resolve together.',\n },\n responses: [\n {\n status: 200,\n description: 'Per-widget aggregation results keyed by request id.',\n schema: widgetDataBatchResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Batch widget data aggregation endpoint',\n methods: {\n POST: widgetDataBatchPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,5 +1,4 @@
1
1
  import { NextResponse } from "next/server";
2
- import { z } from "zod";
3
2
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
4
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -8,83 +7,10 @@ import {
8
7
  WidgetDataValidationError
9
8
  } from "../../../services/widgetDataService.js";
10
9
  import { dashboardsTag, dashboardsErrorSchema } from "../../openapi.js";
10
+ import { widgetDataRequestSchema, widgetDataResponseSchema } from "./schema.js";
11
11
  const metadata = {
12
12
  POST: { requireAuth: true, requireFeatures: ["analytics.view"] }
13
13
  };
14
- const aggregateFunctionSchema = z.enum(["count", "sum", "avg", "min", "max"]);
15
- const dateGranularitySchema = z.enum(["day", "week", "month", "quarter", "year"]);
16
- const dateRangePresetSchema = z.enum([
17
- "today",
18
- "yesterday",
19
- "this_week",
20
- "last_week",
21
- "this_month",
22
- "last_month",
23
- "this_quarter",
24
- "last_quarter",
25
- "this_year",
26
- "last_year",
27
- "last_7_days",
28
- "last_30_days",
29
- "last_90_days"
30
- ]);
31
- const filterOperatorSchema = z.enum([
32
- "eq",
33
- "neq",
34
- "gt",
35
- "gte",
36
- "lt",
37
- "lte",
38
- "in",
39
- "not_in",
40
- "is_null",
41
- "is_not_null"
42
- ]);
43
- const widgetDataRequestSchema = z.object({
44
- entityType: z.string().min(1),
45
- metric: z.object({
46
- field: z.string().min(1),
47
- aggregate: aggregateFunctionSchema
48
- }),
49
- groupBy: z.object({
50
- field: z.string().min(1),
51
- granularity: dateGranularitySchema.optional(),
52
- limit: z.number().int().min(1).max(100).optional(),
53
- resolveLabels: z.boolean().optional()
54
- }).optional(),
55
- filters: z.array(
56
- z.object({
57
- field: z.string().min(1),
58
- operator: filterOperatorSchema,
59
- value: z.unknown().optional()
60
- })
61
- ).optional(),
62
- dateRange: z.object({
63
- field: z.string().min(1),
64
- preset: dateRangePresetSchema
65
- }).optional(),
66
- comparison: z.object({
67
- type: z.enum(["previous_period", "previous_year"])
68
- }).optional()
69
- });
70
- const widgetDataItemSchema = z.object({
71
- groupKey: z.unknown(),
72
- groupLabel: z.string().optional(),
73
- value: z.number().nullable()
74
- });
75
- const widgetDataResponseSchema = z.object({
76
- value: z.number().nullable(),
77
- data: z.array(widgetDataItemSchema),
78
- comparison: z.object({
79
- value: z.number().nullable(),
80
- change: z.number(),
81
- direction: z.enum(["up", "down", "unchanged"])
82
- }).optional(),
83
- metadata: z.object({
84
- fetchedAt: z.string(),
85
- recordCount: z.number()
86
- })
87
- });
88
14
  async function POST(req) {
89
15
  const auth = await getAuthFromRequest(req);
90
16
  if (!auth) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/widgets/data/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])\nconst dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])\nconst dateRangePresetSchema = z.enum([\n 'today',\n 'yesterday',\n 'this_week',\n 'last_week',\n 'this_month',\n 'last_month',\n 'this_quarter',\n 'last_quarter',\n 'this_year',\n 'last_year',\n 'last_7_days',\n 'last_30_days',\n 'last_90_days',\n])\n\nconst filterOperatorSchema = z.enum([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'in',\n 'not_in',\n 'is_null',\n 'is_not_null',\n])\n\nconst widgetDataRequestSchema = z.object({\n entityType: z.string().min(1),\n metric: z.object({\n field: z.string().min(1),\n aggregate: aggregateFunctionSchema,\n }),\n groupBy: z\n .object({\n field: z.string().min(1),\n granularity: dateGranularitySchema.optional(),\n limit: z.number().int().min(1).max(100).optional(),\n resolveLabels: z.boolean().optional(),\n })\n .optional(),\n filters: z\n .array(\n z.object({\n field: z.string().min(1),\n operator: filterOperatorSchema,\n value: z.unknown().optional(),\n }),\n )\n .optional(),\n dateRange: z\n .object({\n field: z.string().min(1),\n preset: dateRangePresetSchema,\n })\n .optional(),\n comparison: z\n .object({\n type: z.enum(['previous_period', 'previous_year']),\n })\n .optional(),\n})\n\nconst widgetDataItemSchema = z.object({\n groupKey: z.unknown(),\n groupLabel: z.string().optional(),\n value: z.number().nullable(),\n})\n\nconst widgetDataResponseSchema = z.object({\n value: z.number().nullable(),\n data: z.array(widgetDataItemSchema),\n comparison: z\n .object({\n value: z.number().nullable(),\n change: z.number(),\n direction: z.enum(['up', 'down', 'unchanged']),\n })\n .optional(),\n metadata: z.object({\n fetchedAt: z.string(),\n recordCount: z.number(),\n }),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(parsed.data as WidgetDataRequest)\n return NextResponse.json(result)\n } catch (err) {\n console.error('[widgets/data] Error:', err)\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,0CAA0C;AACnD;AAAA,EACE;AAAA,EAEA;AAAA,OACK;AAGP,SAAS,eAAe,6BAA6B;AAE9C,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACjE;AAEA,MAAM,0BAA0B,EAAE,KAAK,CAAC,SAAS,OAAO,OAAO,OAAO,KAAK,CAAC;AAC5E,MAAM,wBAAwB,EAAE,KAAK,CAAC,OAAO,QAAQ,SAAS,WAAW,MAAM,CAAC;AAChF,MAAM,wBAAwB,EAAE,KAAK;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,uBAAuB,EAAE,KAAK;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,0BAA0B,EAAE,OAAO;AAAA,EACvC,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,QAAQ,EAAE,OAAO;AAAA,IACf,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,WAAW;AAAA,EACb,CAAC;AAAA,EACD,SAAS,EACN,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,aAAa,sBAAsB,SAAS;AAAA,IAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,IACjD,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,EACtC,CAAC,EACA,SAAS;AAAA,EACZ,SAAS,EACN;AAAA,IACC,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACvB,UAAU;AAAA,MACV,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC9B,CAAC;AAAA,EACH,EACC,SAAS;AAAA,EACZ,WAAW,EACR,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,QAAQ;AAAA,EACV,CAAC,EACA,SAAS;AAAA,EACZ,YAAY,EACT,OAAO;AAAA,IACN,MAAM,EAAE,KAAK,CAAC,mBAAmB,eAAe,CAAC;AAAA,EACnD,CAAC,EACA,SAAS;AACd,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,UAAU,EAAE,QAAQ;AAAA,EACpB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,SAAS;AAC7B,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,MAAM,EAAE,MAAM,oBAAoB;AAAA,EAClC,YAAY,EACT,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,IAC3B,QAAQ,EAAE,OAAO;AAAA,IACjB,WAAW,EAAE,KAAK,CAAC,MAAM,QAAQ,WAAW,CAAC;AAAA,EAC/C,CAAC,EACA,SAAS;AAAA,EACZ,UAAU,EAAE,OAAO;AAAA,IACjB,WAAW,EAAE,OAAO;AAAA,IACpB,aAAa,EAAE,OAAO;AAAA,EACxB,CAAC;AACH,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1E;AAEA,QAAM,SAAS,wBAAwB,UAAU,IAAI;AACrD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,2BAA2B,QAAQ,OAAO,MAAM,OAAO;AAAA,MAChE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,oBAAoB,UAAU,QAA2B,mBAAmB;AAElF,QAAM,iBAAiB,kBAAkB,oBAAoB,OAAO,KAAK,UAAU;AACnF,MAAI,kBAAkB,eAAe,SAAS,GAAG;AAC/C,UAAM,cAAc,UAAU,QAM3B,aAAa;AAChB,UAAM,YAAY,MAAM,YAAY,mBAAmB,KAAK,KAAK,gBAAgB;AAAA,MAC/E,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AACD,QAAI,CAAC,WAAW;AACd,aAAO,aAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAAA,IACzD,OAAO;AAAA,IACP,mBAAmB;AAAA,IACnB,YAAY;AAAA,EACd,CAAC;AAED,QAAM,WAAW,KAAK,YAAY;AAClC,MAAI,CAAC,UAAU;AACb,WAAO,aAAa,KAAK,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACnF;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AAExF,QAAM,mBAAmB,MAAM;AAC7B,QAAI,OAAO,WAAY,QAAO,CAAC,MAAM,UAAU;AAC/C,QAAI,MAAM,QAAQ,OAAO,SAAS,KAAK,MAAM,UAAU,SAAS,EAAG,QAAO,MAAM;AAChF,QAAI,OAAO,eAAe,KAAM,QAAO;AACvC,QAAI,KAAK,MAAO,QAAO,CAAC,KAAK,KAAK;AAClC,WAAO;AAAA,EACT,GAAG;AAEH,MAAI;AACF,UAAM,QAAQ,UAAU,QAAuB,OAAO;AACtD,UAAM,UAAU,wBAAwB,IAAI,EAAE,UAAU,gBAAgB,GAAG,mBAAmB,KAAK;AACnG,UAAM,SAAS,MAAM,QAAQ,gBAAgB,OAAO,IAAyB;AAC7E,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAK;AACZ,YAAQ,MAAM,yBAAyB,GAAG;AAC1C,QAAI,eAAe,2BAA2B;AAC5C,aAAO,aAAa,KAAK,EAAE,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClE;AACA,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,kDAAkD;AAAA,MAC3D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEA,MAAM,oBAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,aACE;AAAA,EACF,MAAM,CAAC,aAAa;AAAA,EACpB,aAAa;AAAA,IACX,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT;AAAA,MACE,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,sBAAsB;AAAA,IACrF,EAAE,QAAQ,KAAK,aAAa,2BAA2B,QAAQ,sBAAsB;AAAA,IACrF,EAAE,QAAQ,KAAK,aAAa,kCAAkC,QAAQ,sBAAsB;AAAA,IAC5F,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,sBAAsB;AAAA,EACrF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,EACR;AACF;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from './schema'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(parsed.data as WidgetDataRequest)\n return NextResponse.json(result)\n } catch (err) {\n console.error('[widgets/data] Error:', err)\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }