@open-mercato/core 0.6.4-develop.4106.1.12c205a613 → 0.6.4-develop.4113.1.5e87922616

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.
@@ -1,4 +1,4 @@
1
- [build:core] found 2644 entry points
1
+ [build:core] found 2645 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 172 entry points
4
4
  [build:core:generated] built successfully
@@ -1,4 +1,5 @@
1
1
  import { Currency, ExchangeRate } from "../data/entities.js";
2
+ const exchangeRateKey = (fromCurrencyCode, toCurrencyCode, date, source) => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`;
2
3
  class RateFetchingService {
3
4
  constructor(em) {
4
5
  this.em = em;
@@ -57,21 +58,38 @@ class RateFetchingService {
57
58
  });
58
59
  }
59
60
  async storeRates(rates, scope) {
61
+ if (rates.length === 0) return 0;
60
62
  let stored = 0;
61
63
  await this.em.transactional(async (em) => {
64
+ const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)));
65
+ const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)));
66
+ const sources = Array.from(new Set(rates.map((rate) => rate.source)));
67
+ const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(
68
+ (time) => new Date(time)
69
+ );
70
+ const existingRates = await em.find(ExchangeRate, {
71
+ organizationId: scope.organizationId,
72
+ tenantId: scope.tenantId,
73
+ fromCurrencyCode: { $in: fromCurrencyCodes },
74
+ toCurrencyCode: { $in: toCurrencyCodes },
75
+ date: { $in: dates },
76
+ source: { $in: sources }
77
+ });
78
+ const existingByKey = /* @__PURE__ */ new Map();
79
+ for (const existing of existingRates) {
80
+ existingByKey.set(
81
+ exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),
82
+ existing
83
+ );
84
+ }
85
+ const now = /* @__PURE__ */ new Date();
62
86
  for (const rate of rates) {
63
- const existing = await em.findOne(ExchangeRate, {
64
- organizationId: scope.organizationId,
65
- tenantId: scope.tenantId,
66
- fromCurrencyCode: rate.fromCurrencyCode,
67
- toCurrencyCode: rate.toCurrencyCode,
68
- date: rate.date,
69
- source: rate.source
70
- });
87
+ const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source);
88
+ const existing = existingByKey.get(key);
71
89
  if (existing) {
72
90
  existing.rate = rate.rate;
73
91
  existing.type = rate.type ?? null;
74
- existing.updatedAt = /* @__PURE__ */ new Date();
92
+ existing.updatedAt = now;
75
93
  em.persist(existing);
76
94
  } else {
77
95
  const newRate = em.create(ExchangeRate, {
@@ -84,10 +102,11 @@ class RateFetchingService {
84
102
  source: rate.source,
85
103
  type: rate.type ?? null,
86
104
  isActive: true,
87
- createdAt: /* @__PURE__ */ new Date(),
88
- updatedAt: /* @__PURE__ */ new Date()
105
+ createdAt: now,
106
+ updatedAt: now
89
107
  });
90
108
  em.persist(newRate);
109
+ existingByKey.set(key, newRate);
91
110
  }
92
111
  stored++;
93
112
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/currencies/services/rateFetchingService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n for (const providerSource of providerList) {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n result.errors.push(`Unknown provider: ${providerSource}`)\n continue\n }\n\n if (!provider.isAvailable()) {\n result.errors.push(`Provider not available: ${providerSource}`)\n continue\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n const stored = await this.storeRates(validRates, scope)\n\n result.byProvider[providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err: any) {\n const errorMsg = `${providerSource}: ${err.message}`\n result.errors.push(errorMsg)\n result.byProvider[providerSource] = {\n count: 0,\n errors: [err.message],\n }\n }\n }\n\n return result\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n let stored = 0\n\n await this.em.transactional(async (em) => {\n for (const rate of rates) {\n // Check if rate already exists\n const existing = await em.findOne(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n date: rate.date,\n source: rate.source,\n })\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = new Date()\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(newRate)\n }\n\n stored++\n }\n \n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { RateProvider, RateProviderResult } from './providers/base'\nimport { Currency, ExchangeRate } from '../data/entities'\n\nexport interface FetchResult {\n totalFetched: number\n byProvider: Record<string, { count: number; errors?: string[] }>\n errors: string[]\n}\n\nexport interface FetchOptions {\n providers?: string[]\n forceUpdate?: boolean\n}\n\nconst exchangeRateKey = (\n fromCurrencyCode: string,\n toCurrencyCode: string,\n date: Date,\n source: string\n): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`\n\nexport class RateFetchingService {\n private providers: Map<string, RateProvider>\n\n constructor(private em: EntityManager) {\n this.providers = new Map()\n }\n\n /**\n * Register a rate provider\n */\n registerProvider(provider: RateProvider): void {\n this.providers.set(provider.source, provider)\n }\n\n async fetchRatesForDate(\n date: Date,\n scope: { tenantId: string; organizationId: string },\n options: FetchOptions = {}\n ): Promise<FetchResult> {\n const result: FetchResult = {\n totalFetched: 0,\n byProvider: {},\n errors: [],\n }\n\n // Get existing currencies for validation\n const existingCurrencies = await this.getExistingCurrencies(scope)\n const currencyCodeSet = new Set(existingCurrencies.map((c) => c.code))\n\n // Determine which providers to use\n const providerList = options.providers?.length\n ? options.providers\n : Array.from(this.providers.keys())\n\n for (const providerSource of providerList) {\n const provider = this.providers.get(providerSource)\n\n if (!provider) {\n result.errors.push(`Unknown provider: ${providerSource}`)\n continue\n }\n\n if (!provider.isAvailable()) {\n result.errors.push(`Provider not available: ${providerSource}`)\n continue\n }\n\n try {\n const rates = await provider.fetchRates(date, scope, currencyCodeSet)\n\n // Filter: only currencies that exist in both directions\n const validRates = rates.filter(\n (r) =>\n currencyCodeSet.has(r.fromCurrencyCode) &&\n currencyCodeSet.has(r.toCurrencyCode)\n )\n\n const stored = await this.storeRates(validRates, scope)\n\n result.byProvider[providerSource] = { count: stored }\n result.totalFetched += stored\n } catch (err: any) {\n const errorMsg = `${providerSource}: ${err.message}`\n result.errors.push(errorMsg)\n result.byProvider[providerSource] = {\n count: 0,\n errors: [err.message],\n }\n }\n }\n\n return result\n }\n\n private async getExistingCurrencies(scope: {\n tenantId: string\n organizationId: string\n }): Promise<Currency[]> {\n return this.em.find(Currency, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n deletedAt: null,\n })\n }\n\n private async storeRates(\n rates: RateProviderResult[],\n scope: { tenantId: string; organizationId: string }\n ): Promise<number> {\n if (rates.length === 0) return 0\n\n let stored = 0\n\n await this.em.transactional(async (em) => {\n // Prefetch every existing rate that could match this batch in a single query,\n // then index by composite key so the per-rate loop never hits the database.\n const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))\n const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))\n const sources = Array.from(new Set(rates.map((rate) => rate.source)))\n const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(\n (time) => new Date(time)\n )\n\n const existingRates = await em.find(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: { $in: fromCurrencyCodes },\n toCurrencyCode: { $in: toCurrencyCodes },\n date: { $in: dates },\n source: { $in: sources },\n })\n\n const existingByKey = new Map<string, ExchangeRate>()\n for (const existing of existingRates) {\n existingByKey.set(\n exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),\n existing\n )\n }\n\n const now = new Date()\n for (const rate of rates) {\n const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)\n const existing = existingByKey.get(key)\n\n if (existing) {\n // Update existing rate\n existing.rate = rate.rate\n existing.type = rate.type ?? null\n existing.updatedAt = now\n em.persist(existing)\n } else {\n // Create new rate\n const newRate = em.create(ExchangeRate, {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n fromCurrencyCode: rate.fromCurrencyCode,\n toCurrencyCode: rate.toCurrencyCode,\n rate: rate.rate,\n date: rate.date,\n source: rate.source,\n type: rate.type ?? null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(newRate)\n // Track so duplicate keys within the same batch update in memory instead of double-inserting.\n existingByKey.set(key, newRate)\n }\n\n stored++\n }\n\n // Flush all changes at once\n await em.flush()\n })\n\n return stored\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -53,6 +53,17 @@ async function POST(req) {
53
53
  }
54
54
  await em.begin();
55
55
  try {
56
+ const defByKey = /* @__PURE__ */ new Map();
57
+ const keys = definitions.map((d) => d.key);
58
+ if (keys.length > 0) {
59
+ const existingDefs = await em.find(CustomFieldDef, {
60
+ entityId,
61
+ key: { $in: keys },
62
+ organizationId: auth.orgId ?? null,
63
+ tenantId: auth.tenantId ?? null
64
+ });
65
+ for (const existing of existingDefs) defByKey.set(existing.key, existing);
66
+ }
56
67
  for (const [idx, d] of definitions.entries()) {
57
68
  const where = {
58
69
  entityId,
@@ -60,8 +71,11 @@ async function POST(req) {
60
71
  organizationId: auth.orgId ?? null,
61
72
  tenantId: auth.tenantId ?? null
62
73
  };
63
- let def = await em.findOne(CustomFieldDef, where);
64
- if (!def) def = em.create(CustomFieldDef, { ...where, createdAt: /* @__PURE__ */ new Date() });
74
+ let def = defByKey.get(d.key);
75
+ if (!def) {
76
+ def = em.create(CustomFieldDef, { ...where, createdAt: /* @__PURE__ */ new Date() });
77
+ defByKey.set(d.key, def);
78
+ }
65
79
  def.kind = d.kind;
66
80
  const inCfg = d.configJson ?? {};
67
81
  const cfg = { ...inCfg };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/api/definitions.batch.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'\nimport { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { z } from 'zod'\nimport { invalidateDefinitionsCache } from './definitions.cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nconst batchSchema = z\n .object({\n entityId: z.string().regex(/^[a-z0-9_]+:[a-z0-9_]+$/),\n definitions: z.array(\n upsertCustomFieldDefSchema\n .omit({ entityId: true })\n .extend({\n configJson: z.any().optional(),\n })\n ),\n })\n .extend(customFieldEntityConfigSchema.shape)\n\ntype IncomingFieldset = z.infer<typeof customFieldEntityConfigSchema>['fieldsets']\n\nfunction cloneFieldsets(fieldsets?: IncomingFieldset): IncomingFieldset {\n if (!Array.isArray(fieldsets)) return undefined\n return fieldsets.map((fieldset) => ({\n code: fieldset.code,\n label: fieldset.label,\n icon: fieldset.icon,\n description: fieldset.description,\n groups: Array.isArray(fieldset.groups)\n ? fieldset.groups.map((group) => ({\n code: group.code,\n title: group.title,\n hint: group.hint,\n }))\n : undefined,\n }))\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n\n const parsed = batchSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, definitions, fieldsets, singleFieldsetPerRecord } = parsed.data\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n let cache: CacheStrategy | undefined\n try {\n cache = resolve('cache') as CacheStrategy\n } catch {}\n\n await em.begin()\n try {\n for (const [idx, d] of definitions.entries()) {\n const where: any = {\n entityId,\n key: d.key,\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n }\n let def = await em.findOne(CustomFieldDef, where)\n if (!def) def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })\n def.kind = d.kind\n\n const inCfg = (d as any).configJson ?? {}\n const cfg: Record<string, any> = { ...inCfg }\n if (cfg.label == null || String(cfg.label).trim() === '') cfg.label = d.key\n if (cfg.formEditable === undefined) cfg.formEditable = true\n if (cfg.listVisible === undefined) cfg.listVisible = true\n if (d.kind === 'multiline' && (cfg.editor == null || String(cfg.editor).trim() === '')) cfg.editor = 'markdown'\n cfg.priority = idx\n\n def.configJson = cfg\n def.isActive = d.isActive ?? true\n def.updatedAt = new Date()\n em.persist(def)\n }\n if (fieldsets !== undefined || singleFieldsetPerRecord !== undefined) {\n const scope: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let cfg = await em.findOne(CustomFieldEntityConfig, scope)\n if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...scope, createdAt: new Date() })\n const existing = normalizeEntityFieldsetConfig(cfg.configJson ?? {})\n const patch = mergeEntityFieldsetConfig(existing, {\n fieldsets: fieldsets !== undefined ? cloneFieldsets(fieldsets) ?? [] : undefined,\n singleFieldsetPerRecord,\n })\n cfg.configJson = {\n fieldsets: patch.fieldsets,\n singleFieldsetPerRecord: patch.singleFieldsetPerRecord,\n }\n cfg.updatedAt = new Date()\n cfg.isActive = true\n em.persist(cfg)\n }\n await em.flush()\n await em.commit()\n } catch (e) {\n try { await em.rollback() } catch {}\n return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })\n }\n\n await invalidateDefinitionsCache(cache, {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n entityIds: [entityId],\n })\n\n return NextResponse.json({ ok: true })\n}\n\nconst batchResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Batch upsert custom field definitions',\n methods: {\n POST: {\n summary: 'Save multiple custom field definitions',\n description: 'Creates or updates multiple definitions for a single entity in one transaction.',\n requestBody: {\n contentType: 'application/json',\n schema: batchSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Definitions saved',\n schema: batchResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { CustomFieldDef, CustomFieldEntityConfig } from '@open-mercato/core/modules/entities/data/entities'\nimport { customFieldEntityConfigSchema, upsertCustomFieldDefSchema } from '@open-mercato/core/modules/entities/data/validators'\nimport { z } from 'zod'\nimport { invalidateDefinitionsCache } from './definitions.cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { mergeEntityFieldsetConfig, normalizeEntityFieldsetConfig } from '../lib/fieldsets'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },\n}\n\nconst batchSchema = z\n .object({\n entityId: z.string().regex(/^[a-z0-9_]+:[a-z0-9_]+$/),\n definitions: z.array(\n upsertCustomFieldDefSchema\n .omit({ entityId: true })\n .extend({\n configJson: z.any().optional(),\n })\n ),\n })\n .extend(customFieldEntityConfigSchema.shape)\n\ntype IncomingFieldset = z.infer<typeof customFieldEntityConfigSchema>['fieldsets']\n\nfunction cloneFieldsets(fieldsets?: IncomingFieldset): IncomingFieldset {\n if (!Array.isArray(fieldsets)) return undefined\n return fieldsets.map((fieldset) => ({\n code: fieldset.code,\n label: fieldset.label,\n icon: fieldset.icon,\n description: fieldset.description,\n groups: Array.isArray(fieldset.groups)\n ? fieldset.groups.map((group) => ({\n code: group.code,\n title: group.title,\n hint: group.hint,\n }))\n : undefined,\n }))\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n let body: any\n try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) }\n\n const parsed = batchSchema.safeParse(body)\n if (!parsed.success) return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })\n const { entityId, definitions, fieldsets, singleFieldsetPerRecord } = parsed.data\n\n const container = await createRequestContainer()\n const { resolve } = container\n const em = resolve('em') as any\n let cache: CacheStrategy | undefined\n try {\n cache = resolve('cache') as CacheStrategy\n } catch {}\n\n await em.begin()\n try {\n // Prefetch every existing definition for this entity in a single query, then index\n // by key so the per-definition loop resolves create/update without round trips.\n const defByKey = new Map<string, any>()\n const keys = definitions.map((d) => d.key)\n if (keys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n entityId,\n key: { $in: keys },\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n })\n for (const existing of existingDefs) defByKey.set(existing.key, existing)\n }\n\n for (const [idx, d] of definitions.entries()) {\n const where: any = {\n entityId,\n key: d.key,\n organizationId: auth.orgId ?? null,\n tenantId: auth.tenantId ?? null,\n }\n let def = defByKey.get(d.key)\n if (!def) {\n def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })\n defByKey.set(d.key, def)\n }\n def.kind = d.kind\n\n const inCfg = (d as any).configJson ?? {}\n const cfg: Record<string, any> = { ...inCfg }\n if (cfg.label == null || String(cfg.label).trim() === '') cfg.label = d.key\n if (cfg.formEditable === undefined) cfg.formEditable = true\n if (cfg.listVisible === undefined) cfg.listVisible = true\n if (d.kind === 'multiline' && (cfg.editor == null || String(cfg.editor).trim() === '')) cfg.editor = 'markdown'\n cfg.priority = idx\n\n def.configJson = cfg\n def.isActive = d.isActive ?? true\n def.updatedAt = new Date()\n em.persist(def)\n }\n if (fieldsets !== undefined || singleFieldsetPerRecord !== undefined) {\n const scope: any = { entityId, organizationId: auth.orgId ?? null, tenantId: auth.tenantId ?? null }\n let cfg = await em.findOne(CustomFieldEntityConfig, scope)\n if (!cfg) cfg = em.create(CustomFieldEntityConfig, { ...scope, createdAt: new Date() })\n const existing = normalizeEntityFieldsetConfig(cfg.configJson ?? {})\n const patch = mergeEntityFieldsetConfig(existing, {\n fieldsets: fieldsets !== undefined ? cloneFieldsets(fieldsets) ?? [] : undefined,\n singleFieldsetPerRecord,\n })\n cfg.configJson = {\n fieldsets: patch.fieldsets,\n singleFieldsetPerRecord: patch.singleFieldsetPerRecord,\n }\n cfg.updatedAt = new Date()\n cfg.isActive = true\n em.persist(cfg)\n }\n await em.flush()\n await em.commit()\n } catch (e) {\n try { await em.rollback() } catch {}\n return NextResponse.json({ error: 'Failed to save definitions batch' }, { status: 500 })\n }\n\n await invalidateDefinitionsCache(cache, {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n entityIds: [entityId],\n })\n\n return NextResponse.json({ ok: true })\n}\n\nconst batchResponseSchema = z.object({\n ok: z.literal(true),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Entities',\n summary: 'Batch upsert custom field definitions',\n methods: {\n POST: {\n summary: 'Save multiple custom field definitions',\n description: 'Creates or updates multiple definitions for a single entity in one transaction.',\n requestBody: {\n contentType: 'application/json',\n schema: batchSchema,\n },\n responses: [\n {\n status: 200,\n description: 'Definitions saved',\n schema: batchResponseSchema,\n },\n {\n status: 400,\n description: 'Validation error',\n schema: z.object({\n error: z.string(),\n details: z.any().optional(),\n }),\n },\n {\n status: 401,\n description: 'Missing authentication',\n schema: z.object({ error: z.string() }),\n },\n {\n status: 500,\n description: 'Unexpected failure',\n schema: z.object({ error: z.string() }),\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -41,15 +41,24 @@ async function ensureCustomFieldDefinitions(em, sets, scope) {
41
41
  let created = 0;
42
42
  let updated = 0;
43
43
  let unchanged = 0;
44
+ const entityIds = Array.from(new Set(sets.map((set) => set.entity)));
45
+ const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))));
46
+ const existingByKey = /* @__PURE__ */ new Map();
47
+ if (entityIds.length > 0 && fieldKeys.length > 0) {
48
+ const existingDefs = await em.find(CustomFieldDef, {
49
+ entityId: { $in: entityIds },
50
+ organizationId: scope.organizationId,
51
+ tenantId: scope.tenantId,
52
+ key: { $in: fieldKeys }
53
+ });
54
+ for (const def of existingDefs) {
55
+ existingByKey.set(`${def.entityId}|${def.key}`, def);
56
+ }
57
+ }
58
+ let dirty = false;
44
59
  for (const set of sets) {
45
60
  for (const field of set.fields) {
46
- const where = {
47
- entityId: set.entity,
48
- organizationId: scope.organizationId,
49
- tenantId: scope.tenantId,
50
- key: field.key
51
- };
52
- const existing = await em.findOne(CustomFieldDef, where);
61
+ const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null;
53
62
  const configJson = {};
54
63
  for (const key of CONFIG_PASSTHROUGH_KEYS) {
55
64
  const value = field[key];
@@ -57,19 +66,20 @@ async function ensureCustomFieldDefinitions(em, sets, scope) {
57
66
  }
58
67
  if (!existing) {
59
68
  if (!scope.dryRun) {
60
- await em.persist(
61
- em.create(CustomFieldDef, {
62
- entityId: set.entity,
63
- organizationId: scope.organizationId,
64
- tenantId: scope.tenantId,
65
- key: field.key,
66
- kind: field.kind,
67
- configJson,
68
- isActive: true,
69
- createdAt: /* @__PURE__ */ new Date(),
70
- updatedAt: /* @__PURE__ */ new Date()
71
- })
72
- ).flush();
69
+ const createdDef = em.create(CustomFieldDef, {
70
+ entityId: set.entity,
71
+ organizationId: scope.organizationId,
72
+ tenantId: scope.tenantId,
73
+ key: field.key,
74
+ kind: field.kind,
75
+ configJson,
76
+ isActive: true,
77
+ createdAt: /* @__PURE__ */ new Date(),
78
+ updatedAt: /* @__PURE__ */ new Date()
79
+ });
80
+ em.persist(createdDef);
81
+ existingByKey.set(`${set.entity}|${field.key}`, createdDef);
82
+ dirty = true;
73
83
  }
74
84
  created++;
75
85
  continue;
@@ -91,11 +101,15 @@ async function ensureCustomFieldDefinitions(em, sets, scope) {
91
101
  existing.isActive = true;
92
102
  existing.updatedAt = /* @__PURE__ */ new Date();
93
103
  if (existing.deletedAt) existing.deletedAt = null;
94
- await em.flush();
104
+ em.persist(existing);
105
+ dirty = true;
95
106
  }
96
107
  updated++;
97
108
  }
98
109
  }
110
+ if (dirty) {
111
+ await em.flush();
112
+ }
99
113
  return { created, updated, unchanged };
100
114
  }
101
115
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/entities/lib/field-definitions.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomFieldDef } from '../data/entities'\nimport type { CustomFieldDefinition } from '@open-mercato/shared/modules/entities'\n\nexport type FieldSetInput = {\n entity: string\n fields: CustomFieldDefinition[]\n source?: string\n}\n\nexport type EnsureFieldDefinitionsOptions = {\n organizationId: string | null\n tenantId: string | null\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type EnsureFieldDefinitionsResult = {\n created: number\n updated: number\n unchanged: number\n}\n\nconst CONFIG_PASSTHROUGH_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'fieldset',\n 'fieldsets',\n 'group',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'dictionaryId',\n 'dictionaryInlineCreate',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n 'sourceMetadata',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction configEquals(a: unknown, b: unknown): boolean {\n return JSON.stringify(normalizeValue(a ?? null)) === JSON.stringify(normalizeValue(b ?? null))\n}\n\nexport async function ensureCustomFieldDefinitions(\n em: EntityManager,\n sets: FieldSetInput[],\n scope: EnsureFieldDefinitionsOptions\n): Promise<EnsureFieldDefinitionsResult> {\n let created = 0\n let updated = 0\n let unchanged = 0\n\n for (const set of sets) {\n for (const field of set.fields) {\n const where = {\n entityId: set.entity,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: field.key,\n }\n const existing = await em.findOne(CustomFieldDef, where)\n const configJson: Record<string, unknown> = {}\n\n for (const key of CONFIG_PASSTHROUGH_KEYS) {\n const value = field[key]\n if (value !== undefined) configJson[key] = value as unknown\n }\n\n if (!existing) {\n if (!scope.dryRun) {\n await em.persist(\n em.create(CustomFieldDef, {\n entityId: set.entity,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: field.key,\n kind: field.kind,\n configJson,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n ).flush()\n }\n created++\n continue\n }\n\n const kindChanged = existing.kind !== field.kind\n const configChanged = !configEquals(existing.configJson ?? null, configJson)\n const needsActivation = existing.isActive !== true || existing.deletedAt != null\n if (scope.createOnly) {\n unchanged++\n continue\n }\n if (!kindChanged && !configChanged && !needsActivation) {\n unchanged++\n continue\n }\n\n if (!scope.dryRun) {\n existing.kind = field.kind\n ;(existing as any).configJson = configJson\n existing.isActive = true\n existing.updatedAt = new Date()\n if (existing.deletedAt) existing.deletedAt = null\n await em.flush()\n }\n updated++\n }\n }\n\n return { created, updated, unchanged }\n}\n"],
5
- "mappings": "AACA,SAAS,sBAAsB;AAsB/B,MAAM,0BAA8D;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;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;AAEA,SAAS,eAAe,OAAyB;AAC/C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,SAAS,eAAe,IAAI,CAAC;AACzE,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,WAAO,OAAO,KAAK,KAAgC,EAChD,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EACjC,OAAgC,CAAC,KAAK,QAAQ;AAC7C,UAAI,GAAG,IAAI,eAAgB,MAAkC,GAAG,CAAC;AACjE,aAAO;AAAA,IACT,GAAG,CAAC,CAAC;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,aAAa,GAAY,GAAqB;AACrD,SAAO,KAAK,UAAU,eAAe,KAAK,IAAI,CAAC,MAAM,KAAK,UAAU,eAAe,KAAK,IAAI,CAAC;AAC/F;AAEA,eAAsB,6BACpB,IACA,MACA,OACuC;AACvC,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,YAAY;AAEhB,aAAW,OAAO,MAAM;AACtB,eAAW,SAAS,IAAI,QAAQ;AAC9B,YAAM,QAAQ;AAAA,QACZ,UAAU,IAAI;AAAA,QACd,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,KAAK,MAAM;AAAA,MACb;AACA,YAAM,WAAW,MAAM,GAAG,QAAQ,gBAAgB,KAAK;AACvD,YAAM,aAAsC,CAAC;AAE7C,iBAAW,OAAO,yBAAyB;AACzC,cAAM,QAAQ,MAAM,GAAG;AACvB,YAAI,UAAU,OAAW,YAAW,GAAG,IAAI;AAAA,MAC7C;AAEA,UAAI,CAAC,UAAU;AACb,YAAI,CAAC,MAAM,QAAQ;AACjB,gBAAM,GAAG;AAAA,YACP,GAAG,OAAO,gBAAgB;AAAA,cACxB,UAAU,IAAI;AAAA,cACd,gBAAgB,MAAM;AAAA,cACtB,UAAU,MAAM;AAAA,cAChB,KAAK,MAAM;AAAA,cACX,MAAM,MAAM;AAAA,cACZ;AAAA,cACA,UAAU;AAAA,cACV,WAAW,oBAAI,KAAK;AAAA,cACpB,WAAW,oBAAI,KAAK;AAAA,YACtB,CAAC;AAAA,UACH,EAAE,MAAM;AAAA,QACV;AACA;AACA;AAAA,MACF;AAEA,YAAM,cAAc,SAAS,SAAS,MAAM;AAC5C,YAAM,gBAAgB,CAAC,aAAa,SAAS,cAAc,MAAM,UAAU;AAC3E,YAAM,kBAAkB,SAAS,aAAa,QAAQ,SAAS,aAAa;AAC5E,UAAI,MAAM,YAAY;AACpB;AACA;AAAA,MACF;AACA,UAAI,CAAC,eAAe,CAAC,iBAAiB,CAAC,iBAAiB;AACtD;AACA;AAAA,MACF;AAEA,UAAI,CAAC,MAAM,QAAQ;AACjB,iBAAS,OAAO,MAAM;AACrB,QAAC,SAAiB,aAAa;AAChC,iBAAS,WAAW;AACpB,iBAAS,YAAY,oBAAI,KAAK;AAC9B,YAAI,SAAS,UAAW,UAAS,YAAY;AAC7C,cAAM,GAAG,MAAM;AAAA,MACjB;AACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,SAAS,UAAU;AACvC;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomFieldDef } from '../data/entities'\nimport type { CustomFieldDefinition } from '@open-mercato/shared/modules/entities'\n\nexport type FieldSetInput = {\n entity: string\n fields: CustomFieldDefinition[]\n source?: string\n}\n\nexport type EnsureFieldDefinitionsOptions = {\n organizationId: string | null\n tenantId: string | null\n dryRun?: boolean\n createOnly?: boolean\n}\n\nexport type EnsureFieldDefinitionsResult = {\n created: number\n updated: number\n unchanged: number\n}\n\nconst CONFIG_PASSTHROUGH_KEYS: Array<keyof CustomFieldDefinition> = [\n 'label',\n 'description',\n 'fieldset',\n 'fieldsets',\n 'group',\n 'options',\n 'optionsUrl',\n 'defaultValue',\n 'required',\n 'multi',\n 'filterable',\n 'formEditable',\n 'listVisible',\n 'indexed',\n 'editor',\n 'input',\n 'relatedEntityId',\n 'dictionaryId',\n 'dictionaryInlineCreate',\n 'validation',\n 'maxAttachmentSizeMb',\n 'acceptExtensions',\n 'sourceMetadata',\n]\n\nfunction normalizeValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map((item) => normalizeValue(item))\n if (value && typeof value === 'object') {\n return Object.keys(value as Record<string, unknown>)\n .sort((a, b) => a.localeCompare(b))\n .reduce<Record<string, unknown>>((acc, key) => {\n acc[key] = normalizeValue((value as Record<string, unknown>)[key])\n return acc\n }, {})\n }\n return value\n}\n\nfunction configEquals(a: unknown, b: unknown): boolean {\n return JSON.stringify(normalizeValue(a ?? null)) === JSON.stringify(normalizeValue(b ?? null))\n}\n\nexport async function ensureCustomFieldDefinitions(\n em: EntityManager,\n sets: FieldSetInput[],\n scope: EnsureFieldDefinitionsOptions\n): Promise<EnsureFieldDefinitionsResult> {\n let created = 0\n let updated = 0\n let unchanged = 0\n\n // Prefetch every existing definition the batch could touch in a single query,\n // then index by composite key so the nested loop never issues per-field lookups.\n const entityIds = Array.from(new Set(sets.map((set) => set.entity)))\n const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))))\n const existingByKey = new Map<string, CustomFieldDef>()\n if (entityIds.length > 0 && fieldKeys.length > 0) {\n const existingDefs = await em.find(CustomFieldDef, {\n entityId: { $in: entityIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: { $in: fieldKeys },\n })\n for (const def of existingDefs) {\n existingByKey.set(`${def.entityId}|${def.key}`, def)\n }\n }\n\n let dirty = false\n\n for (const set of sets) {\n for (const field of set.fields) {\n const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null\n const configJson: Record<string, unknown> = {}\n\n for (const key of CONFIG_PASSTHROUGH_KEYS) {\n const value = field[key]\n if (value !== undefined) configJson[key] = value as unknown\n }\n\n if (!existing) {\n if (!scope.dryRun) {\n const createdDef = em.create(CustomFieldDef, {\n entityId: set.entity,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n key: field.key,\n kind: field.kind,\n configJson,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(createdDef)\n // Track so duplicate (entity, key) pairs within the batch update in memory instead of double-inserting.\n existingByKey.set(`${set.entity}|${field.key}`, createdDef)\n dirty = true\n }\n created++\n continue\n }\n\n const kindChanged = existing.kind !== field.kind\n const configChanged = !configEquals(existing.configJson ?? null, configJson)\n const needsActivation = existing.isActive !== true || existing.deletedAt != null\n if (scope.createOnly) {\n unchanged++\n continue\n }\n if (!kindChanged && !configChanged && !needsActivation) {\n unchanged++\n continue\n }\n\n if (!scope.dryRun) {\n existing.kind = field.kind\n ;(existing as any).configJson = configJson\n existing.isActive = true\n existing.updatedAt = new Date()\n if (existing.deletedAt) existing.deletedAt = null\n em.persist(existing)\n dirty = true\n }\n updated++\n }\n }\n\n if (dirty) {\n // Single flush for the whole batch instead of one round trip per field.\n await em.flush()\n }\n\n return { created, updated, unchanged }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -210,15 +210,20 @@ async function saveRolePerspectives(em, cache, options) {
210
210
  const now = /* @__PURE__ */ new Date();
211
211
  const touchedRoleIds = /* @__PURE__ */ new Set();
212
212
  const results = [];
213
- for (const roleId of input.roleIds) {
214
- let record = await em.findOne(RolePerspective, {
215
- roleId,
213
+ const recordByRole = /* @__PURE__ */ new Map();
214
+ if (input.roleIds.length) {
215
+ const existingRecords = await em.find(RolePerspective, {
216
+ roleId: { $in: input.roleIds },
216
217
  tableId,
217
218
  tenantId,
218
219
  organizationId,
219
220
  name: input.name,
220
221
  deletedAt: null
221
222
  });
223
+ for (const existing of existingRecords) recordByRole.set(existing.roleId, existing);
224
+ }
225
+ for (const roleId of input.roleIds) {
226
+ let record = recordByRole.get(roleId) ?? null;
222
227
  if (!record) {
223
228
  record = em.create(RolePerspective, {
224
229
  roleId,
@@ -232,6 +237,7 @@ async function saveRolePerspectives(em, cache, options) {
232
237
  updatedAt: now
233
238
  });
234
239
  em.persist(record);
240
+ recordByRole.set(roleId, record);
235
241
  } else {
236
242
  record.settingsJson = input.settings;
237
243
  record.updatedAt = now;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/perspectives/services/perspectiveService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { Perspective, RolePerspective } from '../data/entities'\nimport type {\n PerspectiveSettings,\n PerspectiveSaveInput,\n RolePerspectiveSaveInput,\n} from '../data/validators'\n\nexport type PerspectiveScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\nexport type ResolvedPerspective = {\n id: string\n name: string\n tableId: string\n settings: PerspectiveSettings\n isDefault: boolean\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type ResolvedRolePerspective = {\n id: string\n roleId: string\n tableId: string\n name: string\n settings: PerspectiveSettings\n isDefault: boolean\n tenantId: string | null\n organizationId: string | null\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type PerspectivesState = {\n tableId: string\n personal: ResolvedPerspective[]\n personalDefaultId: string | null\n rolePerspectives: ResolvedRolePerspective[]\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000\n\nconst nullish = <T extends string | null | undefined>(value: T): string | null =>\n value == null ? null : value\n\nconst scopeKey = (scope: PerspectiveScope) =>\n `${scope.userId}:${scope.tenantId ?? 'null'}:${scope.organizationId ?? 'null'}`\n\nconst userCacheKey = (scope: PerspectiveScope, tableId: string, roleIds: string[]) =>\n `perspectives:user-state:${scopeKey(scope)}:${tableId}:${roleIds.sort((a, b) => a.localeCompare(b)).join(',')}`\n\nconst userTag = (scope: PerspectiveScope, tableId?: string) =>\n tableId\n ? `perspectives:user:${scopeKey(scope)}:${tableId}`\n : `perspectives:user:${scopeKey(scope)}`\n\nconst roleTag = (roleId: string, tableId?: string, tenantId?: string | null) => {\n const tenant = tenantId ?? 'null'\n return tableId ? `perspectives:role:${roleId}:${tenant}:${tableId}` : `perspectives:role:${roleId}:${tenant}`\n}\n\nfunction isResolvedPerspective(value: unknown): value is ResolvedPerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedPerspective>\n return typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.tableId === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isResolvedRolePerspective(value: unknown): value is ResolvedRolePerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedRolePerspective>\n return typeof record.id === 'string'\n && typeof record.roleId === 'string'\n && typeof record.tableId === 'string'\n && typeof record.name === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isPerspectivesState(value: unknown): value is PerspectivesState {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<PerspectivesState>\n if (typeof record.tableId !== 'string') return false\n if (!Array.isArray(record.personal) || record.personal.some((item) => !isResolvedPerspective(item))) return false\n if (record.personalDefaultId !== null && typeof record.personalDefaultId !== 'string') return false\n if (!Array.isArray(record.rolePerspectives) || record.rolePerspectives.some((item) => !isResolvedRolePerspective(item))) return false\n return true\n}\n\n/**\n * Defensive migration for legacy filter state shapes captured before the\n * advanced-filter tree (SPEC-048). Existing perspectives only store either\n * advanced-filter URL params (tree shape with `v:2` or a `root` key) or\n * undefined \u2014 this helper is a safety net for legacy `FilterValues`-shaped\n * records (flat key/value records of column filters) that could only appear\n * if old saved-view JSON were imported.\n *\n * - Tree-shaped state (`v:2` or `root` key) is passed through unchanged.\n * - Undefined / null filters are passed through unchanged.\n * - Legacy `FilterValues`-shaped records are dropped (set to `undefined`)\n * because there is no reliable mapping back to the new operator model;\n * the user sees an empty tree and can recreate.\n */\nexport function maybeMigrateLegacyFilterValues(settings: PerspectiveSettings): PerspectiveSettings {\n const filters = settings.filters\n if (!filters || typeof filters !== 'object') return settings\n const record = filters as Record<string, unknown>\n if ('v' in record && record.v === 2) return settings\n if ('root' in record) return settings\n if (typeof console !== 'undefined') {\n console.warn('[perspectives] Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n }\n return { ...settings, filters: undefined }\n}\n\nfunction toResolvedPerspective(entity: Perspective): ResolvedPerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n name: entity.name,\n tableId: entity.tableId,\n isDefault: !!entity.isDefault,\n settings,\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nfunction toResolvedRolePerspective(entity: RolePerspective): ResolvedRolePerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n roleId: entity.roleId,\n tableId: entity.tableId,\n name: entity.name,\n isDefault: !!entity.isDefault,\n settings,\n tenantId: nullish(entity.tenantId),\n organizationId: nullish(entity.organizationId),\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nexport async function loadPerspectivesState(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; roleIds?: string[] },\n): Promise<PerspectivesState> {\n const { scope, tableId } = options\n const roleIds = Array.isArray(options.roleIds) ? options.roleIds.filter((id) => id && id.length > 0) : []\n const uniqueRoles = Array.from(new Set(roleIds))\n const cacheKey = cache && uniqueRoles.length <= 16 ? userCacheKey(scope, tableId, uniqueRoles) : null\n\n if (cache && cacheKey) {\n const cached = await cache.get(cacheKey)\n if (cached && isPerspectivesState(cached)) return cached\n }\n\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const [personal, roleRecords] = await Promise.all([\n em.find(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n }, { orderBy: { updatedAt: 'desc' } }),\n uniqueRoles.length\n ? em.find(RolePerspective, {\n roleId: { $in: uniqueRoles as any },\n tableId,\n deletedAt: null,\n $and: [\n { $or: [{ tenantId }, { tenantId: null }] },\n { $or: [{ organizationId }, { organizationId: null }] },\n ],\n } as any, { orderBy: { updatedAt: 'desc' } })\n : [],\n ])\n\n const personalResolved = personal.map(toResolvedPerspective)\n const personalDefaultId = personalResolved.find((p) => p.isDefault)?.id ?? null\n const roleResolved = roleRecords.map(toResolvedRolePerspective)\n\n const state: PerspectivesState = {\n tableId,\n personal: personalResolved,\n personalDefaultId,\n rolePerspectives: roleResolved,\n }\n\n if (cache && cacheKey) {\n await cache.set(cacheKey, state, {\n ttl: CACHE_TTL_MS,\n tags: [\n userTag(scope, tableId),\n ...uniqueRoles.map((roleId) => roleTag(roleId, tableId, tenantId)),\n ],\n })\n }\n\n return state\n}\n\nexport async function saveUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; input: PerspectiveSaveInput },\n): Promise<ResolvedPerspective> {\n const { scope, tableId, input } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n let entity: Perspective | null = null\n if (input.perspectiveId) {\n entity = await em.findOne(Perspective, {\n id: input.perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!entity) {\n throw Object.assign(new Error('Perspective not found'), { code: 'NOT_FOUND' })\n }\n } else {\n entity = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n deletedAt: null,\n })\n }\n\n const now = new Date()\n if (!entity) {\n entity = em.create(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.isDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n } else {\n entity.name = input.name\n entity.settingsJson = input.settings\n entity.updatedAt = now\n if (input.isDefault === true) entity.isDefault = true\n if (input.isDefault === false) entity.isDefault = false\n }\n\n if (input.isDefault === true) {\n await em.nativeUpdate(\n Perspective,\n {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n entity.isDefault = true\n }\n\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return toResolvedPerspective(entity)\n}\n\nexport async function deleteUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; perspectiveId: string },\n): Promise<void> {\n const { scope, tableId, perspectiveId } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const existing = await em.findOne(Perspective, {\n id: perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!existing) return\n\n existing.deletedAt = new Date()\n existing.isDefault = false\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n}\n\nexport async function saveRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n input: RolePerspectiveSaveInput\n },\n): Promise<ResolvedRolePerspective[]> {\n const { tableId, input } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n const now = new Date()\n const touchedRoleIds = new Set<string>()\n\n const results: ResolvedRolePerspective[] = []\n\n for (const roleId of input.roleIds) {\n let record = await em.findOne(RolePerspective, {\n roleId,\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n deletedAt: null,\n })\n if (!record) {\n record = em.create(RolePerspective, {\n roleId,\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.setDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n } else {\n record.settingsJson = input.settings\n record.updatedAt = now\n if (input.setDefault === true) record.isDefault = true\n if (input.setDefault === false) record.isDefault = false\n }\n\n if (input.setDefault === true) {\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId,\n tableId,\n tenantId,\n organizationId,\n id: { $ne: record.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n record.isDefault = true\n }\n\n touchedRoleIds.add(roleId)\n results.push(toResolvedRolePerspective(record))\n }\n\n if (input.roleIds.length) {\n await em.flush()\n }\n\n if (cache?.deleteByTags && touchedRoleIds.size > 0) {\n const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return results\n}\n\nexport async function clearRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n roleIds: string[]\n },\n): Promise<void> {\n const { tableId, roleIds } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n if (!roleIds.length) return\n\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n { deletedAt: new Date(), isDefault: false },\n )\n\n if (cache?.deleteByTags) {\n const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { Perspective, RolePerspective } from '../data/entities'\nimport type {\n PerspectiveSettings,\n PerspectiveSaveInput,\n RolePerspectiveSaveInput,\n} from '../data/validators'\n\nexport type PerspectiveScope = {\n userId: string\n tenantId?: string | null\n organizationId?: string | null\n}\n\nexport type ResolvedPerspective = {\n id: string\n name: string\n tableId: string\n settings: PerspectiveSettings\n isDefault: boolean\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type ResolvedRolePerspective = {\n id: string\n roleId: string\n tableId: string\n name: string\n settings: PerspectiveSettings\n isDefault: boolean\n tenantId: string | null\n organizationId: string | null\n createdAt: string\n updatedAt?: string | null\n}\n\nexport type PerspectivesState = {\n tableId: string\n personal: ResolvedPerspective[]\n personalDefaultId: string | null\n rolePerspectives: ResolvedRolePerspective[]\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000\n\nconst nullish = <T extends string | null | undefined>(value: T): string | null =>\n value == null ? null : value\n\nconst scopeKey = (scope: PerspectiveScope) =>\n `${scope.userId}:${scope.tenantId ?? 'null'}:${scope.organizationId ?? 'null'}`\n\nconst userCacheKey = (scope: PerspectiveScope, tableId: string, roleIds: string[]) =>\n `perspectives:user-state:${scopeKey(scope)}:${tableId}:${roleIds.sort((a, b) => a.localeCompare(b)).join(',')}`\n\nconst userTag = (scope: PerspectiveScope, tableId?: string) =>\n tableId\n ? `perspectives:user:${scopeKey(scope)}:${tableId}`\n : `perspectives:user:${scopeKey(scope)}`\n\nconst roleTag = (roleId: string, tableId?: string, tenantId?: string | null) => {\n const tenant = tenantId ?? 'null'\n return tableId ? `perspectives:role:${roleId}:${tenant}:${tableId}` : `perspectives:role:${roleId}:${tenant}`\n}\n\nfunction isResolvedPerspective(value: unknown): value is ResolvedPerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedPerspective>\n return typeof record.id === 'string'\n && typeof record.name === 'string'\n && typeof record.tableId === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isResolvedRolePerspective(value: unknown): value is ResolvedRolePerspective {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<ResolvedRolePerspective>\n return typeof record.id === 'string'\n && typeof record.roleId === 'string'\n && typeof record.tableId === 'string'\n && typeof record.name === 'string'\n && typeof record.isDefault === 'boolean'\n && typeof record.createdAt === 'string'\n}\n\nfunction isPerspectivesState(value: unknown): value is PerspectivesState {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<PerspectivesState>\n if (typeof record.tableId !== 'string') return false\n if (!Array.isArray(record.personal) || record.personal.some((item) => !isResolvedPerspective(item))) return false\n if (record.personalDefaultId !== null && typeof record.personalDefaultId !== 'string') return false\n if (!Array.isArray(record.rolePerspectives) || record.rolePerspectives.some((item) => !isResolvedRolePerspective(item))) return false\n return true\n}\n\n/**\n * Defensive migration for legacy filter state shapes captured before the\n * advanced-filter tree (SPEC-048). Existing perspectives only store either\n * advanced-filter URL params (tree shape with `v:2` or a `root` key) or\n * undefined \u2014 this helper is a safety net for legacy `FilterValues`-shaped\n * records (flat key/value records of column filters) that could only appear\n * if old saved-view JSON were imported.\n *\n * - Tree-shaped state (`v:2` or `root` key) is passed through unchanged.\n * - Undefined / null filters are passed through unchanged.\n * - Legacy `FilterValues`-shaped records are dropped (set to `undefined`)\n * because there is no reliable mapping back to the new operator model;\n * the user sees an empty tree and can recreate.\n */\nexport function maybeMigrateLegacyFilterValues(settings: PerspectiveSettings): PerspectiveSettings {\n const filters = settings.filters\n if (!filters || typeof filters !== 'object') return settings\n const record = filters as Record<string, unknown>\n if ('v' in record && record.v === 2) return settings\n if ('root' in record) return settings\n if (typeof console !== 'undefined') {\n console.warn('[perspectives] Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.')\n }\n return { ...settings, filters: undefined }\n}\n\nfunction toResolvedPerspective(entity: Perspective): ResolvedPerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n name: entity.name,\n tableId: entity.tableId,\n isDefault: !!entity.isDefault,\n settings,\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nfunction toResolvedRolePerspective(entity: RolePerspective): ResolvedRolePerspective {\n const settings = maybeMigrateLegacyFilterValues((entity.settingsJson ?? {}) as PerspectiveSettings)\n return {\n id: entity.id,\n roleId: entity.roleId,\n tableId: entity.tableId,\n name: entity.name,\n isDefault: !!entity.isDefault,\n settings,\n tenantId: nullish(entity.tenantId),\n organizationId: nullish(entity.organizationId),\n createdAt: entity.createdAt.toISOString(),\n updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null,\n }\n}\n\nexport async function loadPerspectivesState(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; roleIds?: string[] },\n): Promise<PerspectivesState> {\n const { scope, tableId } = options\n const roleIds = Array.isArray(options.roleIds) ? options.roleIds.filter((id) => id && id.length > 0) : []\n const uniqueRoles = Array.from(new Set(roleIds))\n const cacheKey = cache && uniqueRoles.length <= 16 ? userCacheKey(scope, tableId, uniqueRoles) : null\n\n if (cache && cacheKey) {\n const cached = await cache.get(cacheKey)\n if (cached && isPerspectivesState(cached)) return cached\n }\n\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const [personal, roleRecords] = await Promise.all([\n em.find(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n }, { orderBy: { updatedAt: 'desc' } }),\n uniqueRoles.length\n ? em.find(RolePerspective, {\n roleId: { $in: uniqueRoles as any },\n tableId,\n deletedAt: null,\n $and: [\n { $or: [{ tenantId }, { tenantId: null }] },\n { $or: [{ organizationId }, { organizationId: null }] },\n ],\n } as any, { orderBy: { updatedAt: 'desc' } })\n : [],\n ])\n\n const personalResolved = personal.map(toResolvedPerspective)\n const personalDefaultId = personalResolved.find((p) => p.isDefault)?.id ?? null\n const roleResolved = roleRecords.map(toResolvedRolePerspective)\n\n const state: PerspectivesState = {\n tableId,\n personal: personalResolved,\n personalDefaultId,\n rolePerspectives: roleResolved,\n }\n\n if (cache && cacheKey) {\n await cache.set(cacheKey, state, {\n ttl: CACHE_TTL_MS,\n tags: [\n userTag(scope, tableId),\n ...uniqueRoles.map((roleId) => roleTag(roleId, tableId, tenantId)),\n ],\n })\n }\n\n return state\n}\n\nexport async function saveUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; input: PerspectiveSaveInput },\n): Promise<ResolvedPerspective> {\n const { scope, tableId, input } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n let entity: Perspective | null = null\n if (input.perspectiveId) {\n entity = await em.findOne(Perspective, {\n id: input.perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!entity) {\n throw Object.assign(new Error('Perspective not found'), { code: 'NOT_FOUND' })\n }\n } else {\n entity = await em.findOne(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n deletedAt: null,\n })\n }\n\n const now = new Date()\n if (!entity) {\n entity = em.create(Perspective, {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.isDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n } else {\n entity.name = input.name\n entity.settingsJson = input.settings\n entity.updatedAt = now\n if (input.isDefault === true) entity.isDefault = true\n if (input.isDefault === false) entity.isDefault = false\n }\n\n if (input.isDefault === true) {\n await em.nativeUpdate(\n Perspective,\n {\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n id: { $ne: entity.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n entity.isDefault = true\n }\n\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n\n return toResolvedPerspective(entity)\n}\n\nexport async function deleteUserPerspective(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: { scope: PerspectiveScope; tableId: string; perspectiveId: string },\n): Promise<void> {\n const { scope, tableId, perspectiveId } = options\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n\n const existing = await em.findOne(Perspective, {\n id: perspectiveId,\n userId: scope.userId,\n tenantId,\n organizationId,\n tableId,\n deletedAt: null,\n })\n if (!existing) return\n\n existing.deletedAt = new Date()\n existing.isDefault = false\n await em.flush()\n\n if (cache?.deleteByTags) {\n await cache.deleteByTags([userTag(scope, tableId)])\n }\n}\n\nexport async function saveRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n input: RolePerspectiveSaveInput\n },\n): Promise<ResolvedRolePerspective[]> {\n const { tableId, input } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n const now = new Date()\n const touchedRoleIds = new Set<string>()\n\n const results: ResolvedRolePerspective[] = []\n\n // Prefetch every matching role perspective in a single query, then index by role id\n // so the loop resolves create/update without a lookup per role.\n const recordByRole = new Map<string, RolePerspective>()\n if (input.roleIds.length) {\n const existingRecords = await em.find(RolePerspective, {\n roleId: { $in: input.roleIds },\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n deletedAt: null,\n })\n for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)\n }\n\n for (const roleId of input.roleIds) {\n let record = recordByRole.get(roleId) ?? null\n if (!record) {\n record = em.create(RolePerspective, {\n roleId,\n tableId,\n tenantId,\n organizationId,\n name: input.name,\n settingsJson: input.settings,\n isDefault: Boolean(input.setDefault),\n createdAt: now,\n updatedAt: now,\n })\n em.persist(record)\n recordByRole.set(roleId, record)\n } else {\n record.settingsJson = input.settings\n record.updatedAt = now\n if (input.setDefault === true) record.isDefault = true\n if (input.setDefault === false) record.isDefault = false\n }\n\n if (input.setDefault === true) {\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId,\n tableId,\n tenantId,\n organizationId,\n id: { $ne: record.id } as any,\n deletedAt: null,\n },\n { isDefault: false, updatedAt: now },\n )\n record.isDefault = true\n }\n\n touchedRoleIds.add(roleId)\n results.push(toResolvedRolePerspective(record))\n }\n\n if (input.roleIds.length) {\n await em.flush()\n }\n\n if (cache?.deleteByTags && touchedRoleIds.size > 0) {\n const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n\n return results\n}\n\nexport async function clearRolePerspectives(\n em: EntityManager,\n cache: CacheStrategy | null | undefined,\n options: {\n tableId: string\n tenantId?: string | null\n organizationId?: string | null\n roleIds: string[]\n },\n): Promise<void> {\n const { tableId, roleIds } = options\n const tenantId = options.tenantId ?? null\n const organizationId = options.organizationId ?? null\n if (!roleIds.length) return\n\n await em.nativeUpdate(\n RolePerspective,\n {\n roleId: { $in: roleIds as any },\n tableId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n { deletedAt: new Date(), isDefault: false },\n )\n\n if (cache?.deleteByTags) {\n const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId))\n await cache.deleteByTags(tags)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }