@open-mercato/core 0.6.6-develop.6255.1.a6ee4a57c1 → 0.6.6-develop.6256.1.9fc16aedc4
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js +20 -9
- package/dist/modules/perspectives/api/[tableId]/[perspectiveId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js +43 -6
- package/dist/modules/perspectives/api/[tableId]/roles/[roleId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/[tableId]/route.js +21 -4
- package/dist/modules/perspectives/api/[tableId]/route.js.map +2 -2
- package/dist/modules/perspectives/api/openapi.js +1 -0
- package/dist/modules/perspectives/api/openapi.js.map +2 -2
- package/dist/modules/perspectives/data/validators.js +2 -0
- package/dist/modules/perspectives/data/validators.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +135 -6
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/perspectives/api/[tableId]/[perspectiveId]/route.ts +20 -9
- package/src/modules/perspectives/api/[tableId]/roles/[roleId]/route.ts +45 -6
- package/src/modules/perspectives/api/[tableId]/route.ts +23 -1
- package/src/modules/perspectives/api/openapi.ts +1 -1
- package/src/modules/perspectives/data/validators.ts +2 -0
- package/src/modules/perspectives/services/perspectiveService.ts +171 -7
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import type { CacheStrategy } from '@open-mercato/cache'
|
|
3
|
-
import {
|
|
3
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import {
|
|
5
|
+
buildOptimisticLockConflictBody,
|
|
6
|
+
enforceCommandOptimisticLock,
|
|
7
|
+
enforceRecordGoneIsConflict,
|
|
8
|
+
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
4
9
|
import { Perspective, RolePerspective } from '../data/entities'
|
|
5
10
|
import type {
|
|
6
11
|
PerspectiveSettings,
|
|
@@ -44,6 +49,50 @@ export type PerspectivesState = {
|
|
|
44
49
|
rolePerspectives: ResolvedRolePerspective[]
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
type ExpectedUpdatedAtById = Record<string, string | Date | null | undefined>
|
|
53
|
+
|
|
54
|
+
const PERSPECTIVE_RESOURCE_KIND = 'perspectives.perspective'
|
|
55
|
+
const ROLE_PERSPECTIVE_RESOURCE_KIND = 'perspectives.role_perspective'
|
|
56
|
+
|
|
57
|
+
function resolveRoleLockInput(
|
|
58
|
+
expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,
|
|
59
|
+
roleId: string,
|
|
60
|
+
request: Request | Headers | null | undefined,
|
|
61
|
+
): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {
|
|
62
|
+
if (expectedUpdatedAtByRoleId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByRoleId, roleId)) {
|
|
63
|
+
return { expected: expectedUpdatedAtByRoleId[roleId] ?? null, request: null }
|
|
64
|
+
}
|
|
65
|
+
return { expected: undefined, request: request ?? null }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveRoleRecordLockInput(
|
|
69
|
+
expectedUpdatedAtByPerspectiveId: ExpectedUpdatedAtById | null | undefined,
|
|
70
|
+
expectedUpdatedAtByRoleId: ExpectedUpdatedAtById | null | undefined,
|
|
71
|
+
record: RolePerspective,
|
|
72
|
+
request: Request | Headers | null | undefined,
|
|
73
|
+
): Pick<Parameters<typeof enforceCommandOptimisticLock>[0], 'expected' | 'request'> {
|
|
74
|
+
if (expectedUpdatedAtByPerspectiveId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByPerspectiveId, record.id)) {
|
|
75
|
+
return { expected: expectedUpdatedAtByPerspectiveId[record.id] ?? null, request: null }
|
|
76
|
+
}
|
|
77
|
+
return resolveRoleLockInput(expectedUpdatedAtByRoleId, record.roleId, request)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function firstExpectedUpdatedAt(expectedUpdatedAtById: ExpectedUpdatedAtById | null | undefined): string | Date | null {
|
|
81
|
+
if (!expectedUpdatedAtById) return null
|
|
82
|
+
for (const value of Object.values(expectedUpdatedAtById)) {
|
|
83
|
+
if (value instanceof Date) return value
|
|
84
|
+
if (typeof value === 'string' && value.trim().length > 0) return value
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function throwMissingRoleRecordVersionConflict(record: RolePerspective): never {
|
|
90
|
+
const current = record.updatedAt instanceof Date && Number.isFinite(record.updatedAt.getTime())
|
|
91
|
+
? record.updatedAt.toISOString()
|
|
92
|
+
: new Date(0).toISOString()
|
|
93
|
+
throw new CrudHttpError(409, buildOptimisticLockConflictBody(current, current))
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
const CACHE_TTL_MS = 5 * 60 * 1000
|
|
48
97
|
|
|
49
98
|
const nullish = <T extends string | null | undefined>(value: T): string | null =>
|
|
@@ -242,7 +291,7 @@ export async function saveUserPerspective(
|
|
|
242
291
|
throw Object.assign(new Error('Perspective not found'), { code: 'NOT_FOUND' })
|
|
243
292
|
}
|
|
244
293
|
enforceCommandOptimisticLock({
|
|
245
|
-
resourceKind:
|
|
294
|
+
resourceKind: PERSPECTIVE_RESOURCE_KIND,
|
|
246
295
|
resourceId: entity.id,
|
|
247
296
|
current: entity.updatedAt ?? null,
|
|
248
297
|
request: options.request ?? null,
|
|
@@ -308,7 +357,12 @@ export async function saveUserPerspective(
|
|
|
308
357
|
export async function deleteUserPerspective(
|
|
309
358
|
em: EntityManager,
|
|
310
359
|
cache: CacheStrategy | null | undefined,
|
|
311
|
-
options: {
|
|
360
|
+
options: {
|
|
361
|
+
scope: PerspectiveScope
|
|
362
|
+
tableId: string
|
|
363
|
+
perspectiveId: string
|
|
364
|
+
request?: Request | Headers | null
|
|
365
|
+
},
|
|
312
366
|
): Promise<boolean> {
|
|
313
367
|
const { scope, tableId, perspectiveId } = options
|
|
314
368
|
const tenantId = scope.tenantId ?? null
|
|
@@ -322,7 +376,21 @@ export async function deleteUserPerspective(
|
|
|
322
376
|
tableId,
|
|
323
377
|
deletedAt: null,
|
|
324
378
|
})
|
|
325
|
-
if (!existing)
|
|
379
|
+
if (!existing) {
|
|
380
|
+
enforceRecordGoneIsConflict({
|
|
381
|
+
resourceKind: PERSPECTIVE_RESOURCE_KIND,
|
|
382
|
+
resourceId: perspectiveId,
|
|
383
|
+
request: options.request ?? null,
|
|
384
|
+
})
|
|
385
|
+
return false
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
enforceCommandOptimisticLock({
|
|
389
|
+
resourceKind: PERSPECTIVE_RESOURCE_KIND,
|
|
390
|
+
resourceId: existing.id,
|
|
391
|
+
current: existing.updatedAt ?? null,
|
|
392
|
+
request: options.request ?? null,
|
|
393
|
+
})
|
|
326
394
|
|
|
327
395
|
existing.deletedAt = new Date()
|
|
328
396
|
existing.isDefault = false
|
|
@@ -343,6 +411,9 @@ export async function saveRolePerspectives(
|
|
|
343
411
|
tenantId?: string | null
|
|
344
412
|
organizationId?: string | null
|
|
345
413
|
input: RolePerspectiveSaveInput
|
|
414
|
+
expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById
|
|
415
|
+
expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById
|
|
416
|
+
request?: Request | Headers | null
|
|
346
417
|
},
|
|
347
418
|
): Promise<ResolvedRolePerspective[]> {
|
|
348
419
|
const { tableId, input } = options
|
|
@@ -351,7 +422,7 @@ export async function saveRolePerspectives(
|
|
|
351
422
|
const now = new Date()
|
|
352
423
|
const touchedRoleIds = new Set<string>()
|
|
353
424
|
|
|
354
|
-
const
|
|
425
|
+
const resultRecords: RolePerspective[] = []
|
|
355
426
|
|
|
356
427
|
// Prefetch every matching role perspective in a single query, then index by role id
|
|
357
428
|
// so the loop resolves create/update without a lookup per role.
|
|
@@ -367,6 +438,22 @@ export async function saveRolePerspectives(
|
|
|
367
438
|
})
|
|
368
439
|
for (const existing of existingRecords) recordByRole.set(existing.roleId, existing)
|
|
369
440
|
}
|
|
441
|
+
const defaultRecordsByRole = new Map<string, RolePerspective[]>()
|
|
442
|
+
if (input.setDefault === true && input.roleIds.length) {
|
|
443
|
+
const existingDefaultRecords = await em.find(RolePerspective, {
|
|
444
|
+
roleId: { $in: input.roleIds },
|
|
445
|
+
tableId,
|
|
446
|
+
tenantId,
|
|
447
|
+
organizationId,
|
|
448
|
+
isDefault: true,
|
|
449
|
+
deletedAt: null,
|
|
450
|
+
})
|
|
451
|
+
for (const existing of existingDefaultRecords) {
|
|
452
|
+
const records = defaultRecordsByRole.get(existing.roleId) ?? []
|
|
453
|
+
records.push(existing)
|
|
454
|
+
defaultRecordsByRole.set(existing.roleId, records)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
370
457
|
|
|
371
458
|
for (const roleId of input.roleIds) {
|
|
372
459
|
let record = recordByRole.get(roleId) ?? null
|
|
@@ -385,6 +472,17 @@ export async function saveRolePerspectives(
|
|
|
385
472
|
em.persist(record)
|
|
386
473
|
recordByRole.set(roleId, record)
|
|
387
474
|
} else {
|
|
475
|
+
enforceCommandOptimisticLock({
|
|
476
|
+
resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
|
|
477
|
+
resourceId: record.id,
|
|
478
|
+
current: record.updatedAt ?? null,
|
|
479
|
+
...resolveRoleRecordLockInput(
|
|
480
|
+
options.expectedUpdatedAtByPerspectiveId,
|
|
481
|
+
options.expectedUpdatedAtByRoleId,
|
|
482
|
+
record,
|
|
483
|
+
options.request ?? null,
|
|
484
|
+
),
|
|
485
|
+
})
|
|
388
486
|
record.settingsJson = input.settings
|
|
389
487
|
record.updatedAt = now
|
|
390
488
|
if (input.setDefault === true) record.isDefault = true
|
|
@@ -392,6 +490,20 @@ export async function saveRolePerspectives(
|
|
|
392
490
|
}
|
|
393
491
|
|
|
394
492
|
if (input.setDefault === true) {
|
|
493
|
+
for (const defaultRecord of defaultRecordsByRole.get(roleId) ?? []) {
|
|
494
|
+
if (defaultRecord.id === record.id) continue
|
|
495
|
+
enforceCommandOptimisticLock({
|
|
496
|
+
resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
|
|
497
|
+
resourceId: defaultRecord.id,
|
|
498
|
+
current: defaultRecord.updatedAt ?? null,
|
|
499
|
+
...resolveRoleRecordLockInput(
|
|
500
|
+
options.expectedUpdatedAtByPerspectiveId,
|
|
501
|
+
options.expectedUpdatedAtByRoleId,
|
|
502
|
+
defaultRecord,
|
|
503
|
+
options.request ?? null,
|
|
504
|
+
),
|
|
505
|
+
})
|
|
506
|
+
}
|
|
395
507
|
await em.nativeUpdate(
|
|
396
508
|
RolePerspective,
|
|
397
509
|
{
|
|
@@ -400,6 +512,7 @@ export async function saveRolePerspectives(
|
|
|
400
512
|
tenantId,
|
|
401
513
|
organizationId,
|
|
402
514
|
id: { $ne: record.id } as any,
|
|
515
|
+
isDefault: true,
|
|
403
516
|
deletedAt: null,
|
|
404
517
|
},
|
|
405
518
|
{ isDefault: false, updatedAt: now },
|
|
@@ -408,7 +521,7 @@ export async function saveRolePerspectives(
|
|
|
408
521
|
}
|
|
409
522
|
|
|
410
523
|
touchedRoleIds.add(roleId)
|
|
411
|
-
|
|
524
|
+
resultRecords.push(record)
|
|
412
525
|
}
|
|
413
526
|
|
|
414
527
|
if (input.roleIds.length) {
|
|
@@ -420,7 +533,7 @@ export async function saveRolePerspectives(
|
|
|
420
533
|
await cache.deleteByTags(tags)
|
|
421
534
|
}
|
|
422
535
|
|
|
423
|
-
return
|
|
536
|
+
return resultRecords.map(toResolvedRolePerspective)
|
|
424
537
|
}
|
|
425
538
|
|
|
426
539
|
export async function clearRolePerspectives(
|
|
@@ -431,6 +544,9 @@ export async function clearRolePerspectives(
|
|
|
431
544
|
tenantId?: string | null
|
|
432
545
|
organizationId?: string | null
|
|
433
546
|
roleIds: string[]
|
|
547
|
+
expectedUpdatedAtByRoleId?: ExpectedUpdatedAtById
|
|
548
|
+
expectedUpdatedAtByPerspectiveId?: ExpectedUpdatedAtById
|
|
549
|
+
request?: Request | Headers | null
|
|
434
550
|
},
|
|
435
551
|
): Promise<number> {
|
|
436
552
|
const { tableId, roleIds } = options
|
|
@@ -438,6 +554,54 @@ export async function clearRolePerspectives(
|
|
|
438
554
|
const organizationId = options.organizationId ?? null
|
|
439
555
|
if (!roleIds.length) return 0
|
|
440
556
|
|
|
557
|
+
const existingRecords = await em.find(RolePerspective, {
|
|
558
|
+
roleId: { $in: roleIds as any },
|
|
559
|
+
tableId,
|
|
560
|
+
tenantId,
|
|
561
|
+
organizationId,
|
|
562
|
+
deletedAt: null,
|
|
563
|
+
})
|
|
564
|
+
const recordsByRole = new Map<string, RolePerspective[]>()
|
|
565
|
+
for (const record of existingRecords) {
|
|
566
|
+
const records = recordsByRole.get(record.roleId) ?? []
|
|
567
|
+
records.push(record)
|
|
568
|
+
recordsByRole.set(record.roleId, records)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (const roleId of roleIds) {
|
|
572
|
+
const records = recordsByRole.get(roleId) ?? []
|
|
573
|
+
const lockInput = resolveRoleLockInput(options.expectedUpdatedAtByRoleId, roleId, options.request ?? null)
|
|
574
|
+
if (!records.length) {
|
|
575
|
+
const expected = firstExpectedUpdatedAt(options.expectedUpdatedAtByPerspectiveId)
|
|
576
|
+
enforceRecordGoneIsConflict({
|
|
577
|
+
resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
|
|
578
|
+
resourceId: roleId,
|
|
579
|
+
expected: expected ?? lockInput.expected,
|
|
580
|
+
request: expected ? null : lockInput.request,
|
|
581
|
+
})
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
for (const record of records) {
|
|
585
|
+
if (
|
|
586
|
+
options.expectedUpdatedAtByPerspectiveId
|
|
587
|
+
&& !Object.prototype.hasOwnProperty.call(options.expectedUpdatedAtByPerspectiveId, record.id)
|
|
588
|
+
) {
|
|
589
|
+
throwMissingRoleRecordVersionConflict(record)
|
|
590
|
+
}
|
|
591
|
+
enforceCommandOptimisticLock({
|
|
592
|
+
resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
|
|
593
|
+
resourceId: record.id,
|
|
594
|
+
current: record.updatedAt ?? null,
|
|
595
|
+
...resolveRoleRecordLockInput(
|
|
596
|
+
options.expectedUpdatedAtByPerspectiveId,
|
|
597
|
+
options.expectedUpdatedAtByRoleId,
|
|
598
|
+
record,
|
|
599
|
+
options.request ?? null,
|
|
600
|
+
),
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
441
605
|
const affected = await em.nativeUpdate(
|
|
442
606
|
RolePerspective,
|
|
443
607
|
{
|