@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
|
@@ -131,29 +131,56 @@ export function getSelectedTenantFromRequest(
|
|
|
131
131
|
return parseSelectedTenantCookie(header)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const unique = Array.from(new Set(
|
|
134
|
+
function normalizeOrganizationIds(ids: string[]): string[] {
|
|
135
|
+
return Array.from(new Set(
|
|
137
136
|
ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {
|
|
138
137
|
if (!value) return false
|
|
139
138
|
if (isAllOrganizationsSelection(value)) return false
|
|
140
139
|
return true
|
|
141
140
|
})
|
|
142
141
|
))
|
|
143
|
-
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Map each organization id to itself plus its persisted descendant ids. Only
|
|
145
|
+
// orgs that exist for the tenant and are not soft-deleted are included, so an
|
|
146
|
+
// unknown/inaccessible id simply has no entry (matching the per-id query that
|
|
147
|
+
// returned an empty set for it).
|
|
148
|
+
type OrgDescendantMap = Map<string, string[]>
|
|
149
|
+
|
|
150
|
+
// Issue #2228 — single round-trip for org-scope resolution. Instead of issuing
|
|
151
|
+
// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4
|
|
152
|
+
// sequential queries per request: accessible set, fallback set, selected set),
|
|
153
|
+
// gather every candidate id up front and fetch their descendant expansions in
|
|
154
|
+
// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.
|
|
155
|
+
async function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {
|
|
156
|
+
const unique = normalizeOrganizationIds(ids)
|
|
157
|
+
if (!unique.length) return new Map()
|
|
144
158
|
const filter: FilterQuery<Organization> = {
|
|
145
159
|
tenant: tenantId,
|
|
146
160
|
id: { $in: unique },
|
|
147
161
|
deletedAt: null,
|
|
148
162
|
}
|
|
149
163
|
const orgs = await em.find(Organization, filter)
|
|
150
|
-
const
|
|
164
|
+
const map: OrgDescendantMap = new Map()
|
|
151
165
|
for (const org of orgs) {
|
|
152
166
|
const id = String(org.id)
|
|
153
|
-
|
|
167
|
+
const expansion = [id]
|
|
154
168
|
if (Array.isArray(org.descendantIds)) {
|
|
155
|
-
for (const desc of org.descendantIds)
|
|
169
|
+
for (const desc of org.descendantIds) expansion.push(String(desc))
|
|
156
170
|
}
|
|
171
|
+
map.set(id, expansion)
|
|
172
|
+
}
|
|
173
|
+
return map
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {
|
|
177
|
+
const set = new Set<string>()
|
|
178
|
+
for (const value of ids) {
|
|
179
|
+
const id = normalizeOrganizationId(value)
|
|
180
|
+
if (!id || isAllOrganizationsSelection(id)) continue
|
|
181
|
+
const expansion = map.get(id)
|
|
182
|
+
if (!expansion) continue
|
|
183
|
+
for (const entry of expansion) set.add(entry)
|
|
157
184
|
}
|
|
158
185
|
return set
|
|
159
186
|
}
|
|
@@ -214,14 +241,18 @@ export async function resolveOrganizationScope({
|
|
|
214
241
|
|
|
215
242
|
const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null
|
|
216
243
|
const fallbackOrgId = accountOrgId ?? null
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
|
|
245
|
+
// Every id that could be expanded below — accessible set, fallback (account)
|
|
246
|
+
// org, and the requested selection — is known up front, so fetch them all in
|
|
247
|
+
// a single `organizations` query and expand from the in-memory map.
|
|
248
|
+
const candidateIds = [
|
|
249
|
+
...(accessibleList ?? []),
|
|
250
|
+
...(fallbackOrgId ? [fallbackOrgId] : []),
|
|
251
|
+
...(normalizedSelectedId ? [normalizedSelectedId] : []),
|
|
252
|
+
]
|
|
253
|
+
const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)
|
|
254
|
+
const loadFallbackSet = (): Set<string> | null =>
|
|
255
|
+
fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null
|
|
225
256
|
|
|
226
257
|
let allowedSet: Set<string> | null = null
|
|
227
258
|
if (accessibleList === null) {
|
|
@@ -229,11 +260,11 @@ export async function resolveOrganizationScope({
|
|
|
229
260
|
} else if (accessibleList.length === 0) {
|
|
230
261
|
allowedSet = new Set()
|
|
231
262
|
} else {
|
|
232
|
-
allowedSet =
|
|
263
|
+
allowedSet = expandWithDescendants(orgDescendants, accessibleList)
|
|
233
264
|
}
|
|
234
265
|
|
|
235
266
|
if (allowedSet && allowedSet.size === 0 && fallbackOrgId) {
|
|
236
|
-
const computed =
|
|
267
|
+
const computed = loadFallbackSet()
|
|
237
268
|
if (computed && computed.size > 0) {
|
|
238
269
|
allowedSet = computed
|
|
239
270
|
}
|
|
@@ -256,17 +287,17 @@ export async function resolveOrganizationScope({
|
|
|
256
287
|
|
|
257
288
|
let filterSet: Set<string> | null = null
|
|
258
289
|
if (effectiveSelected) {
|
|
259
|
-
filterSet =
|
|
290
|
+
filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])
|
|
260
291
|
} else if (allowedSet !== null) {
|
|
261
292
|
filterSet = allowedSet
|
|
262
293
|
} else if (widenToAllOrgs) {
|
|
263
294
|
filterSet = null
|
|
264
295
|
} else if (auth.orgId) {
|
|
265
|
-
filterSet =
|
|
296
|
+
filterSet = loadFallbackSet()
|
|
266
297
|
}
|
|
267
298
|
|
|
268
299
|
if ((!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {
|
|
269
|
-
const computed =
|
|
300
|
+
const computed = loadFallbackSet()
|
|
270
301
|
if (computed && computed.size > 0) {
|
|
271
302
|
filterSet = computed
|
|
272
303
|
if (!effectiveSelected) {
|
|
@@ -65,6 +65,20 @@ export async function POST(req: Request) {
|
|
|
65
65
|
|
|
66
66
|
await em.begin()
|
|
67
67
|
try {
|
|
68
|
+
// Prefetch every existing definition for this entity in a single query, then index
|
|
69
|
+
// by key so the per-definition loop resolves create/update without round trips.
|
|
70
|
+
const defByKey = new Map<string, any>()
|
|
71
|
+
const keys = definitions.map((d) => d.key)
|
|
72
|
+
if (keys.length > 0) {
|
|
73
|
+
const existingDefs = await em.find(CustomFieldDef, {
|
|
74
|
+
entityId,
|
|
75
|
+
key: { $in: keys },
|
|
76
|
+
organizationId: auth.orgId ?? null,
|
|
77
|
+
tenantId: auth.tenantId ?? null,
|
|
78
|
+
})
|
|
79
|
+
for (const existing of existingDefs) defByKey.set(existing.key, existing)
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
for (const [idx, d] of definitions.entries()) {
|
|
69
83
|
const where: any = {
|
|
70
84
|
entityId,
|
|
@@ -72,8 +86,11 @@ export async function POST(req: Request) {
|
|
|
72
86
|
organizationId: auth.orgId ?? null,
|
|
73
87
|
tenantId: auth.tenantId ?? null,
|
|
74
88
|
}
|
|
75
|
-
let def =
|
|
76
|
-
if (!def)
|
|
89
|
+
let def = defByKey.get(d.key)
|
|
90
|
+
if (!def) {
|
|
91
|
+
def = em.create(CustomFieldDef, { ...where, createdAt: new Date() })
|
|
92
|
+
defByKey.set(d.key, def)
|
|
93
|
+
}
|
|
77
94
|
def.kind = d.kind
|
|
78
95
|
|
|
79
96
|
const inCfg = (d as any).configJson ?? {}
|
|
@@ -73,15 +73,28 @@ export async function ensureCustomFieldDefinitions(
|
|
|
73
73
|
let updated = 0
|
|
74
74
|
let unchanged = 0
|
|
75
75
|
|
|
76
|
+
// Prefetch every existing definition the batch could touch in a single query,
|
|
77
|
+
// then index by composite key so the nested loop never issues per-field lookups.
|
|
78
|
+
const entityIds = Array.from(new Set(sets.map((set) => set.entity)))
|
|
79
|
+
const fieldKeys = Array.from(new Set(sets.flatMap((set) => set.fields.map((field) => field.key))))
|
|
80
|
+
const existingByKey = new Map<string, CustomFieldDef>()
|
|
81
|
+
if (entityIds.length > 0 && fieldKeys.length > 0) {
|
|
82
|
+
const existingDefs = await em.find(CustomFieldDef, {
|
|
83
|
+
entityId: { $in: entityIds },
|
|
84
|
+
organizationId: scope.organizationId,
|
|
85
|
+
tenantId: scope.tenantId,
|
|
86
|
+
key: { $in: fieldKeys },
|
|
87
|
+
})
|
|
88
|
+
for (const def of existingDefs) {
|
|
89
|
+
existingByKey.set(`${def.entityId}|${def.key}`, def)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let dirty = false
|
|
94
|
+
|
|
76
95
|
for (const set of sets) {
|
|
77
96
|
for (const field of set.fields) {
|
|
78
|
-
const
|
|
79
|
-
entityId: set.entity,
|
|
80
|
-
organizationId: scope.organizationId,
|
|
81
|
-
tenantId: scope.tenantId,
|
|
82
|
-
key: field.key,
|
|
83
|
-
}
|
|
84
|
-
const existing = await em.findOne(CustomFieldDef, where)
|
|
97
|
+
const existing = existingByKey.get(`${set.entity}|${field.key}`) ?? null
|
|
85
98
|
const configJson: Record<string, unknown> = {}
|
|
86
99
|
|
|
87
100
|
for (const key of CONFIG_PASSTHROUGH_KEYS) {
|
|
@@ -91,19 +104,21 @@ export async function ensureCustomFieldDefinitions(
|
|
|
91
104
|
|
|
92
105
|
if (!existing) {
|
|
93
106
|
if (!scope.dryRun) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
).
|
|
107
|
+
const createdDef = em.create(CustomFieldDef, {
|
|
108
|
+
entityId: set.entity,
|
|
109
|
+
organizationId: scope.organizationId,
|
|
110
|
+
tenantId: scope.tenantId,
|
|
111
|
+
key: field.key,
|
|
112
|
+
kind: field.kind,
|
|
113
|
+
configJson,
|
|
114
|
+
isActive: true,
|
|
115
|
+
createdAt: new Date(),
|
|
116
|
+
updatedAt: new Date(),
|
|
117
|
+
})
|
|
118
|
+
em.persist(createdDef)
|
|
119
|
+
// Track so duplicate (entity, key) pairs within the batch update in memory instead of double-inserting.
|
|
120
|
+
existingByKey.set(`${set.entity}|${field.key}`, createdDef)
|
|
121
|
+
dirty = true
|
|
107
122
|
}
|
|
108
123
|
created++
|
|
109
124
|
continue
|
|
@@ -127,11 +142,17 @@ export async function ensureCustomFieldDefinitions(
|
|
|
127
142
|
existing.isActive = true
|
|
128
143
|
existing.updatedAt = new Date()
|
|
129
144
|
if (existing.deletedAt) existing.deletedAt = null
|
|
130
|
-
|
|
145
|
+
em.persist(existing)
|
|
146
|
+
dirty = true
|
|
131
147
|
}
|
|
132
148
|
updated++
|
|
133
149
|
}
|
|
134
150
|
}
|
|
135
151
|
|
|
152
|
+
if (dirty) {
|
|
153
|
+
// Single flush for the whole batch instead of one round trip per field.
|
|
154
|
+
await em.flush()
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
return { created, updated, unchanged }
|
|
137
158
|
}
|
|
@@ -339,15 +339,23 @@ export async function saveRolePerspectives(
|
|
|
339
339
|
|
|
340
340
|
const results: ResolvedRolePerspective[] = []
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
// Prefetch every matching role perspective in a single query, then index by role id
|
|
343
|
+
// so the loop resolves create/update without a lookup per role.
|
|
344
|
+
const recordByRole = new Map<string, RolePerspective>()
|
|
345
|
+
if (input.roleIds.length) {
|
|
346
|
+
const existingRecords = await em.find(RolePerspective, {
|
|
347
|
+
roleId: { $in: input.roleIds },
|
|
345
348
|
tableId,
|
|
346
349
|
tenantId,
|
|
347
350
|
organizationId,
|
|
348
351
|
name: input.name,
|
|
349
352
|
deletedAt: null,
|
|
350
353
|
})
|
|
354
|
+
for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const roleId of input.roleIds) {
|
|
358
|
+
let record = recordByRole.get(roleId) ?? null
|
|
351
359
|
if (!record) {
|
|
352
360
|
record = em.create(RolePerspective, {
|
|
353
361
|
roleId,
|
|
@@ -361,6 +369,7 @@ export async function saveRolePerspectives(
|
|
|
361
369
|
updatedAt: now,
|
|
362
370
|
})
|
|
363
371
|
em.persist(record)
|
|
372
|
+
recordByRole.set(roleId, record)
|
|
364
373
|
} else {
|
|
365
374
|
record.settingsJson = input.settings
|
|
366
375
|
record.updatedAt = now
|