@open-mercato/core 0.6.4-develop.4110.1.836aafde58 → 0.6.4-develop.4121.1.0d7f20d229
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/lib/sessionIntegrity.js +16 -13
- package/dist/modules/auth/lib/sessionIntegrity.js.map +2 -2
- package/dist/modules/currencies/services/rateFetchingService.js +30 -11
- package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
- package/dist/modules/customers/api/utils.js +14 -9
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +33 -20
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +16 -2
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/lib/field-definitions.js +35 -21
- package/dist/modules/entities/lib/field-definitions.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +9 -3
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/lib/sessionIntegrity.ts +37 -16
- package/src/modules/currencies/services/rateFetchingService.ts +44 -13
- package/src/modules/customers/api/utils.ts +17 -11
- package/src/modules/directory/utils/organizationScope.ts +51 -20
- package/src/modules/entities/api/definitions.batch.ts +19 -2
- package/src/modules/entities/lib/field-definitions.ts +42 -21
- package/src/modules/perspectives/services/perspectiveService.ts +12 -3
|
@@ -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 =
|
|
64
|
-
if (!def)
|
|
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 =
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,gBAAgB,+BAA+B;AACxD,SAAS,+BAA+B,kCAAkC;AAC1E,SAAS,SAAS;AAClB,SAAS,kCAAkC;AAE3C,SAAS,2BAA2B,qCAAqC;AAElE,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,6BAA6B,EAAE;AAC9E;AAEA,MAAM,cAAc,EACjB,OAAO;AAAA,EACN,UAAU,EAAE,OAAO,EAAE,MAAM,yBAAyB;AAAA,EACpD,aAAa,EAAE;AAAA,IACb,2BACG,KAAK,EAAE,UAAU,KAAK,CAAC,EACvB,OAAO;AAAA,MACN,YAAY,EAAE,IAAI,EAAE,SAAS;AAAA,IAC/B,CAAC;AAAA,EACL;AACF,CAAC,EACA,OAAO,8BAA8B,KAAK;AAI7C,SAAS,eAAe,WAAgD;AACtE,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO;AACtC,SAAO,UAAU,IAAI,CAAC,cAAc;AAAA,IAClC,MAAM,SAAS;AAAA,IACf,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS;AAAA,IACf,aAAa,SAAS;AAAA,IACtB,QAAQ,MAAM,QAAQ,SAAS,MAAM,IACjC,SAAS,OAAO,IAAI,CAAC,WAAW;AAAA,MAC9B,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,MACb,MAAM,MAAM;AAAA,IACd,EAAE,IACF;AAAA,EACN,EAAE;AACJ;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,MAAO,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC7F,MAAI;AACJ,MAAI;AAAE,WAAO,MAAM,IAAI,KAAK;AAAA,EAAE,QAAQ;AAAE,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAAE;AAE7G,QAAM,SAAS,YAAY,UAAU,IAAI;AACzC,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9H,QAAM,EAAE,UAAU,aAAa,WAAW,wBAAwB,IAAI,OAAO;AAE7E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,KAAK,QAAQ,IAAI;AACvB,MAAI;AACJ,MAAI;AACF,YAAQ,QAAQ,OAAO;AAAA,EACzB,QAAQ;AAAA,EAAC;AAET,QAAM,GAAG,MAAM;AACf,MAAI;
|
|
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": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,gBAAgB,+BAA+B;AACxD,SAAS,+BAA+B,kCAAkC;AAC1E,SAAS,SAAS;AAClB,SAAS,kCAAkC;AAE3C,SAAS,2BAA2B,qCAAqC;AAElE,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,6BAA6B,EAAE;AAC9E;AAEA,MAAM,cAAc,EACjB,OAAO;AAAA,EACN,UAAU,EAAE,OAAO,EAAE,MAAM,yBAAyB;AAAA,EACpD,aAAa,EAAE;AAAA,IACb,2BACG,KAAK,EAAE,UAAU,KAAK,CAAC,EACvB,OAAO;AAAA,MACN,YAAY,EAAE,IAAI,EAAE,SAAS;AAAA,IAC/B,CAAC;AAAA,EACL;AACF,CAAC,EACA,OAAO,8BAA8B,KAAK;AAI7C,SAAS,eAAe,WAAgD;AACtE,MAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO;AACtC,SAAO,UAAU,IAAI,CAAC,cAAc;AAAA,IAClC,MAAM,SAAS;AAAA,IACf,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS;AAAA,IACf,aAAa,SAAS;AAAA,IACtB,QAAQ,MAAM,QAAQ,SAAS,MAAM,IACjC,SAAS,OAAO,IAAI,CAAC,WAAW;AAAA,MAC9B,MAAM,MAAM;AAAA,MACZ,OAAO,MAAM;AAAA,MACb,MAAM,MAAM;AAAA,IACd,EAAE,IACF;AAAA,EACN,EAAE;AACJ;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,QAAQ,CAAC,KAAK,MAAO,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC7F,MAAI;AACJ,MAAI;AAAE,WAAO,MAAM,IAAI,KAAK;AAAA,EAAE,QAAQ;AAAE,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAAE;AAE7G,QAAM,SAAS,YAAY,UAAU,IAAI;AACzC,MAAI,CAAC,OAAO,QAAS,QAAO,aAAa,KAAK,EAAE,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9H,QAAM,EAAE,UAAU,aAAa,WAAW,wBAAwB,IAAI,OAAO;AAE7E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,KAAK,QAAQ,IAAI;AACvB,MAAI;AACJ,MAAI;AACF,YAAQ,QAAQ,OAAO;AAAA,EACzB,QAAQ;AAAA,EAAC;AAET,QAAM,GAAG,MAAM;AACf,MAAI;AAGF,UAAM,WAAW,oBAAI,IAAiB;AACtC,UAAM,OAAO,YAAY,IAAI,CAAC,MAAM,EAAE,GAAG;AACzC,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,eAAe,MAAM,GAAG,KAAK,gBAAgB;AAAA,QACjD;AAAA,QACA,KAAK,EAAE,KAAK,KAAK;AAAA,QACjB,gBAAgB,KAAK,SAAS;AAAA,QAC9B,UAAU,KAAK,YAAY;AAAA,MAC7B,CAAC;AACD,iBAAW,YAAY,aAAc,UAAS,IAAI,SAAS,KAAK,QAAQ;AAAA,IAC1E;AAEA,eAAW,CAAC,KAAK,CAAC,KAAK,YAAY,QAAQ,GAAG;AAC5C,YAAM,QAAa;AAAA,QACjB;AAAA,QACA,KAAK,EAAE;AAAA,QACP,gBAAgB,KAAK,SAAS;AAAA,QAC9B,UAAU,KAAK,YAAY;AAAA,MAC7B;AACA,UAAI,MAAM,SAAS,IAAI,EAAE,GAAG;AAC5B,UAAI,CAAC,KAAK;AACR,cAAM,GAAG,OAAO,gBAAgB,EAAE,GAAG,OAAO,WAAW,oBAAI,KAAK,EAAE,CAAC;AACnE,iBAAS,IAAI,EAAE,KAAK,GAAG;AAAA,MACzB;AACA,UAAI,OAAO,EAAE;AAEb,YAAM,QAAS,EAAU,cAAc,CAAC;AACxC,YAAM,MAA2B,EAAE,GAAG,MAAM;AAC5C,UAAI,IAAI,SAAS,QAAQ,OAAO,IAAI,KAAK,EAAE,KAAK,MAAM,GAAI,KAAI,QAAQ,EAAE;AACxE,UAAI,IAAI,iBAAiB,OAAW,KAAI,eAAe;AACvD,UAAI,IAAI,gBAAgB,OAAW,KAAI,cAAc;AACrD,UAAI,EAAE,SAAS,gBAAgB,IAAI,UAAU,QAAQ,OAAO,IAAI,MAAM,EAAE,KAAK,MAAM,IAAK,KAAI,SAAS;AACrG,UAAI,WAAW;AAEf,UAAI,aAAa;AACjB,UAAI,WAAW,EAAE,YAAY;AAC7B,UAAI,YAAY,oBAAI,KAAK;AACzB,SAAG,QAAQ,GAAG;AAAA,IAChB;AACA,QAAI,cAAc,UAAa,4BAA4B,QAAW;AACpE,YAAM,QAAa,EAAE,UAAU,gBAAgB,KAAK,SAAS,MAAM,UAAU,KAAK,YAAY,KAAK;AACnG,UAAI,MAAM,MAAM,GAAG,QAAQ,yBAAyB,KAAK;AACzD,UAAI,CAAC,IAAK,OAAM,GAAG,OAAO,yBAAyB,EAAE,GAAG,OAAO,WAAW,oBAAI,KAAK,EAAE,CAAC;AACtF,YAAM,WAAW,8BAA8B,IAAI,cAAc,CAAC,CAAC;AACnE,YAAM,QAAQ,0BAA0B,UAAU;AAAA,QAChD,WAAW,cAAc,SAAY,eAAe,SAAS,KAAK,CAAC,IAAI;AAAA,QACvE;AAAA,MACF,CAAC;AACD,UAAI,aAAa;AAAA,QACf,WAAW,MAAM;AAAA,QACjB,yBAAyB,MAAM;AAAA,MACjC;AACA,UAAI,YAAY,oBAAI,KAAK;AACzB,UAAI,WAAW;AACf,SAAG,QAAQ,GAAG;AAAA,IAChB;AACA,UAAM,GAAG,MAAM;AACf,UAAM,GAAG,OAAO;AAAA,EAClB,SAAS,GAAG;AACV,QAAI;AAAE,YAAM,GAAG,SAAS;AAAA,IAAE,QAAQ;AAAA,IAAC;AACnC,WAAO,aAAa,KAAK,EAAE,OAAO,mCAAmC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzF;AAEA,QAAM,2BAA2B,OAAO;AAAA,IACtC,UAAU,KAAK,YAAY;AAAA,IAC3B,gBAAgB,KAAK,SAAS;AAAA,IAC9B,WAAW,CAAC,QAAQ;AAAA,EACtB,CAAC;AAED,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,IAAI,EAAE,QAAQ,IAAI;AACpB,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO;AAAA,YACf,OAAO,EAAE,OAAO;AAAA,YAChB,SAAS,EAAE,IAAI,EAAE,SAAS;AAAA,UAC5B,CAAC;AAAA,QACH;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAAA,QACxC;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.4121.1.0d7f20d229",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -243,16 +243,16 @@
|
|
|
243
243
|
"zod": "^4.4.3"
|
|
244
244
|
},
|
|
245
245
|
"peerDependencies": {
|
|
246
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
247
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
248
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
246
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4121.1.0d7f20d229",
|
|
247
|
+
"@open-mercato/shared": "0.6.4-develop.4121.1.0d7f20d229",
|
|
248
|
+
"@open-mercato/ui": "0.6.4-develop.4121.1.0d7f20d229",
|
|
249
249
|
"react": "^19.0.0",
|
|
250
250
|
"react-dom": "^19.0.0"
|
|
251
251
|
},
|
|
252
252
|
"devDependencies": {
|
|
253
|
-
"@open-mercato/ai-assistant": "0.6.4-develop.
|
|
254
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
255
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
253
|
+
"@open-mercato/ai-assistant": "0.6.4-develop.4121.1.0d7f20d229",
|
|
254
|
+
"@open-mercato/shared": "0.6.4-develop.4121.1.0d7f20d229",
|
|
255
|
+
"@open-mercato/ui": "0.6.4-develop.4121.1.0d7f20d229",
|
|
256
256
|
"@testing-library/dom": "^10.4.1",
|
|
257
257
|
"@testing-library/jest-dom": "^6.9.1",
|
|
258
258
|
"@testing-library/react": "^16.3.1",
|
|
@@ -64,19 +64,28 @@ export async function resolveCanonicalStaffAuthContext(
|
|
|
64
64
|
return null
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
// The session-revocation check and the user load are independent (neither reads
|
|
68
|
+
// the other's result), so they run concurrently to collapse two sequential DB
|
|
69
|
+
// round-trips into one. The `em` here is a fresh request-scoped EntityManager
|
|
70
|
+
// (resolved per request, never inside an explicit transaction), so concurrent
|
|
71
|
+
// reads on it are safe.
|
|
72
|
+
const sessionPromise = sessionId !== null
|
|
73
|
+
? findOneWithDecryption(em, Session, { id: sessionId, deletedAt: null })
|
|
74
|
+
: Promise.resolve(null)
|
|
75
|
+
const userPromise = findOneWithDecryption(
|
|
74
76
|
em,
|
|
75
77
|
User,
|
|
76
78
|
{ id: subjectId, deletedAt: null },
|
|
77
79
|
undefined,
|
|
78
80
|
{ tenantId: actorTenantId, organizationId: actorOrganizationId },
|
|
79
81
|
)
|
|
82
|
+
const [session, user] = await Promise.all([sessionPromise, userPromise])
|
|
83
|
+
|
|
84
|
+
if (sessionId !== null) {
|
|
85
|
+
if (!session) return null
|
|
86
|
+
if (session.expiresAt.getTime() < Date.now()) return null
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
if (!user) return null
|
|
81
90
|
|
|
82
91
|
const currentTenantId = normalizeScopeId(user.tenantId ?? null)
|
|
@@ -90,8 +99,12 @@ export async function resolveCanonicalStaffAuthContext(
|
|
|
90
99
|
return null
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
// Role links and the per-user super-admin flag are likewise independent, so they
|
|
103
|
+
// run concurrently. The role-level super-admin lookup depends on the resolved
|
|
104
|
+
// role ids, so it stays sequential after the links resolve (and is skipped
|
|
105
|
+
// entirely when the per-user flag already grants super-admin).
|
|
106
|
+
const linksPromise = currentTenantId
|
|
107
|
+
? findWithDecryption(
|
|
95
108
|
em,
|
|
96
109
|
UserRole,
|
|
97
110
|
{
|
|
@@ -102,7 +115,11 @@ export async function resolveCanonicalStaffAuthContext(
|
|
|
102
115
|
{ populate: ['role'] },
|
|
103
116
|
{ tenantId: currentTenantId, organizationId: currentOrganizationId },
|
|
104
117
|
)
|
|
105
|
-
: []
|
|
118
|
+
: Promise.resolve([] as UserRole[])
|
|
119
|
+
const userAclSuperAdminPromise = currentTenantId
|
|
120
|
+
? userAclGrantsSuperAdmin(em, user.id, currentTenantId, currentOrganizationId)
|
|
121
|
+
: Promise.resolve(false)
|
|
122
|
+
const [links, userAclSuperAdmin] = await Promise.all([linksPromise, userAclSuperAdminPromise])
|
|
106
123
|
|
|
107
124
|
const linkedRoles = links
|
|
108
125
|
.map((link) => link.role)
|
|
@@ -113,7 +130,7 @@ export async function resolveCanonicalStaffAuthContext(
|
|
|
113
130
|
.filter((name): name is string => typeof name === 'string' && name.trim().length > 0)
|
|
114
131
|
|
|
115
132
|
const isSuperAdmin = currentTenantId
|
|
116
|
-
? await
|
|
133
|
+
? userAclSuperAdmin || (await roleAclGrantsSuperAdmin(em, linkedRoles, currentTenantId, currentOrganizationId))
|
|
117
134
|
: false
|
|
118
135
|
|
|
119
136
|
return {
|
|
@@ -126,10 +143,9 @@ export async function resolveCanonicalStaffAuthContext(
|
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
async function
|
|
146
|
+
async function userAclGrantsSuperAdmin(
|
|
130
147
|
em: EntityManager,
|
|
131
148
|
userId: string,
|
|
132
|
-
linkedRoles: Role[],
|
|
133
149
|
tenantId: string,
|
|
134
150
|
organizationId: string | null,
|
|
135
151
|
): Promise<boolean> {
|
|
@@ -145,10 +161,15 @@ async function hasSuperAdminFlag(
|
|
|
145
161
|
undefined,
|
|
146
162
|
{ tenantId, organizationId },
|
|
147
163
|
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
164
|
+
return !!(userAcl && (userAcl as { isSuperAdmin?: boolean }).isSuperAdmin === true)
|
|
165
|
+
}
|
|
151
166
|
|
|
167
|
+
async function roleAclGrantsSuperAdmin(
|
|
168
|
+
em: EntityManager,
|
|
169
|
+
linkedRoles: Role[],
|
|
170
|
+
tenantId: string,
|
|
171
|
+
organizationId: string | null,
|
|
172
|
+
): Promise<boolean> {
|
|
152
173
|
const roleIds = Array.from(
|
|
153
174
|
new Set(
|
|
154
175
|
linkedRoles
|
|
@@ -13,6 +13,13 @@ export interface FetchOptions {
|
|
|
13
13
|
forceUpdate?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const exchangeRateKey = (
|
|
17
|
+
fromCurrencyCode: string,
|
|
18
|
+
toCurrencyCode: string,
|
|
19
|
+
date: Date,
|
|
20
|
+
source: string
|
|
21
|
+
): string => `${fromCurrencyCode}|${toCurrencyCode}|${date.getTime()}|${source}`
|
|
22
|
+
|
|
16
23
|
export class RateFetchingService {
|
|
17
24
|
private providers: Map<string, RateProvider>
|
|
18
25
|
|
|
@@ -103,25 +110,47 @@ export class RateFetchingService {
|
|
|
103
110
|
rates: RateProviderResult[],
|
|
104
111
|
scope: { tenantId: string; organizationId: string }
|
|
105
112
|
): Promise<number> {
|
|
113
|
+
if (rates.length === 0) return 0
|
|
114
|
+
|
|
106
115
|
let stored = 0
|
|
107
116
|
|
|
108
117
|
await this.em.transactional(async (em) => {
|
|
118
|
+
// Prefetch every existing rate that could match this batch in a single query,
|
|
119
|
+
// then index by composite key so the per-rate loop never hits the database.
|
|
120
|
+
const fromCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.fromCurrencyCode)))
|
|
121
|
+
const toCurrencyCodes = Array.from(new Set(rates.map((rate) => rate.toCurrencyCode)))
|
|
122
|
+
const sources = Array.from(new Set(rates.map((rate) => rate.source)))
|
|
123
|
+
const dates = Array.from(new Set(rates.map((rate) => rate.date.getTime()))).map(
|
|
124
|
+
(time) => new Date(time)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const existingRates = await em.find(ExchangeRate, {
|
|
128
|
+
organizationId: scope.organizationId,
|
|
129
|
+
tenantId: scope.tenantId,
|
|
130
|
+
fromCurrencyCode: { $in: fromCurrencyCodes },
|
|
131
|
+
toCurrencyCode: { $in: toCurrencyCodes },
|
|
132
|
+
date: { $in: dates },
|
|
133
|
+
source: { $in: sources },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const existingByKey = new Map<string, ExchangeRate>()
|
|
137
|
+
for (const existing of existingRates) {
|
|
138
|
+
existingByKey.set(
|
|
139
|
+
exchangeRateKey(existing.fromCurrencyCode, existing.toCurrencyCode, existing.date, existing.source),
|
|
140
|
+
existing
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const now = new Date()
|
|
109
145
|
for (const rate of rates) {
|
|
110
|
-
|
|
111
|
-
const existing =
|
|
112
|
-
organizationId: scope.organizationId,
|
|
113
|
-
tenantId: scope.tenantId,
|
|
114
|
-
fromCurrencyCode: rate.fromCurrencyCode,
|
|
115
|
-
toCurrencyCode: rate.toCurrencyCode,
|
|
116
|
-
date: rate.date,
|
|
117
|
-
source: rate.source,
|
|
118
|
-
})
|
|
146
|
+
const key = exchangeRateKey(rate.fromCurrencyCode, rate.toCurrencyCode, rate.date, rate.source)
|
|
147
|
+
const existing = existingByKey.get(key)
|
|
119
148
|
|
|
120
149
|
if (existing) {
|
|
121
150
|
// Update existing rate
|
|
122
151
|
existing.rate = rate.rate
|
|
123
152
|
existing.type = rate.type ?? null
|
|
124
|
-
existing.updatedAt =
|
|
153
|
+
existing.updatedAt = now
|
|
125
154
|
em.persist(existing)
|
|
126
155
|
} else {
|
|
127
156
|
// Create new rate
|
|
@@ -135,15 +164,17 @@ export class RateFetchingService {
|
|
|
135
164
|
source: rate.source,
|
|
136
165
|
type: rate.type ?? null,
|
|
137
166
|
isActive: true,
|
|
138
|
-
createdAt:
|
|
139
|
-
updatedAt:
|
|
167
|
+
createdAt: now,
|
|
168
|
+
updatedAt: now,
|
|
140
169
|
})
|
|
141
170
|
em.persist(newRate)
|
|
171
|
+
// Track so duplicate keys within the same batch update in memory instead of double-inserting.
|
|
172
|
+
existingByKey.set(key, newRate)
|
|
142
173
|
}
|
|
143
174
|
|
|
144
175
|
stored++
|
|
145
176
|
}
|
|
146
|
-
|
|
177
|
+
|
|
147
178
|
// Flush all changes at once
|
|
148
179
|
await em.flush()
|
|
149
180
|
})
|
|
@@ -191,18 +191,24 @@ export async function findMatchingEntityIdsBySearchTokensAcrossSources({
|
|
|
191
191
|
if (!trimmed) return null
|
|
192
192
|
|
|
193
193
|
const enrichedSources = await enrichSearchSourcesWithCustomFieldTokens(ctx, sources)
|
|
194
|
+
const perSource = await Promise.all(
|
|
195
|
+
enrichedSources.map(async (source) => {
|
|
196
|
+
const rawIds = await findSearchTokenEntityIds({
|
|
197
|
+
ctx,
|
|
198
|
+
entityType: source.entityType,
|
|
199
|
+
fields: source.fields,
|
|
200
|
+
query: trimmed,
|
|
201
|
+
})
|
|
202
|
+
if (rawIds === null) return null
|
|
203
|
+
return source.mapToEntityIds
|
|
204
|
+
? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })
|
|
205
|
+
: rawIds
|
|
206
|
+
}),
|
|
207
|
+
)
|
|
208
|
+
|
|
194
209
|
const matchedIds = new Set<string>()
|
|
195
|
-
for (const
|
|
196
|
-
|
|
197
|
-
ctx,
|
|
198
|
-
entityType: source.entityType,
|
|
199
|
-
fields: source.fields,
|
|
200
|
-
query: trimmed,
|
|
201
|
-
})
|
|
202
|
-
if (rawIds === null) return null
|
|
203
|
-
const entityIds = source.mapToEntityIds
|
|
204
|
-
? await mapScopedEntityIds({ ctx, ids: rawIds, config: source.mapToEntityIds })
|
|
205
|
-
: rawIds
|
|
210
|
+
for (const entityIds of perSource) {
|
|
211
|
+
if (entityIds === null) return null
|
|
206
212
|
entityIds.forEach((id) => matchedIds.add(id))
|
|
207
213
|
}
|
|
208
214
|
|