@open-mercato/core 0.6.5-develop.4670.1.afe50dfd5c → 0.6.5-develop.4691.1.bb409545b3
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/AGENTS.md +31 -0
- package/dist/helpers/integration/standaloneEnv.js +58 -0
- package/dist/helpers/integration/standaloneEnv.js.map +7 -0
- package/dist/helpers/integration/undoHarness.js +97 -2
- package/dist/helpers/integration/undoHarness.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +80 -83
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/entities/lib/helpers.js +79 -82
- package/dist/modules/entities/lib/helpers.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +50 -24
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +28 -15
- package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
- package/dist/modules/query_index/subscribers/upsert_one.js +31 -13
- package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -0
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +15 -0
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/standaloneEnv.ts +62 -0
- package/src/helpers/integration/undoHarness.ts +132 -1
- package/src/modules/customers/AGENTS.md +1 -0
- package/src/modules/customers/commands/deals.ts +106 -111
- package/src/modules/entities/lib/helpers.ts +43 -21
- package/src/modules/query_index/lib/indexer.ts +71 -24
- package/src/modules/query_index/subscribers/delete_one.ts +36 -16
- package/src/modules/query_index/subscribers/upsert_one.ts +44 -15
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +11 -0
- package/src/modules/workflows/lib/workflow-executor.ts +17 -0
|
@@ -8,7 +8,11 @@ import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/sub
|
|
|
8
8
|
export const metadata = { event: 'query_index.delete_one', persistent: false }
|
|
9
9
|
|
|
10
10
|
export default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {
|
|
11
|
-
|
|
11
|
+
// Forked EntityManager — this awaited subscriber runs synchronously on the request
|
|
12
|
+
// `em`; isolating it prevents our queries/writes from resetting the originating CRUD
|
|
13
|
+
// write's UnitOfWork and dropping its pending changes. See upsert_one.ts for detail.
|
|
14
|
+
const baseEm = ctx.resolve<any>('em')
|
|
15
|
+
const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm
|
|
12
16
|
const entityType = String(payload?.entityType || '')
|
|
13
17
|
const recordId = String(payload?.recordId || '')
|
|
14
18
|
if (!entityType || !recordId) return
|
|
@@ -73,24 +77,40 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// The projection row + token removal above are synchronous (the data engine
|
|
81
|
+
// awaits this subscriber) so list reads are consistent immediately. The coverage
|
|
82
|
+
// recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are
|
|
83
|
+
// secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.
|
|
76
84
|
const shouldRefreshCoverage = coverageDelayMs === undefined || coverageDelayMs >= 0
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
const coverageRefreshDelay = coverageDelayMs ?? 0
|
|
86
|
+
void (async () => {
|
|
79
87
|
try {
|
|
80
88
|
const bus = ctx.resolve<any>('eventBus')
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
if (shouldRefreshCoverage) {
|
|
90
|
+
await bus.emitEvent('query_index.coverage.refresh', {
|
|
91
|
+
entityType,
|
|
92
|
+
tenantId: tenantId ?? null,
|
|
93
|
+
organizationId: organizationId ?? null,
|
|
94
|
+
delayMs: coverageRefreshDelay,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
await bus.emitEvent('search.delete_record', { entityId: entityType, recordId, organizationId, tenantId })
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await recordIndexerError(
|
|
100
|
+
{ em },
|
|
101
|
+
{
|
|
102
|
+
source: 'query_index',
|
|
103
|
+
handler: 'event:query_index.delete_one:coverage_search',
|
|
104
|
+
error,
|
|
105
|
+
entityType,
|
|
106
|
+
recordId,
|
|
107
|
+
tenantId: tenantId ?? null,
|
|
108
|
+
organizationId: organizationId ?? null,
|
|
109
|
+
payload,
|
|
110
|
+
},
|
|
111
|
+
).catch(() => {})
|
|
112
|
+
}
|
|
113
|
+
})()
|
|
94
114
|
} catch (error) {
|
|
95
115
|
await recordIndexerError(
|
|
96
116
|
{ em },
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
|
|
2
|
-
import { upsertIndexRow } from '../lib/indexer'
|
|
2
|
+
import { upsertIndexRow, reindexSearchTokensForRecord } from '../lib/indexer'
|
|
3
3
|
import { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'
|
|
4
4
|
import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/subscriber-scope'
|
|
5
5
|
|
|
6
6
|
export const metadata = { event: 'query_index.upsert_one', persistent: false }
|
|
7
7
|
|
|
8
8
|
export default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {
|
|
9
|
-
|
|
9
|
+
// Run index maintenance on a FORKED EntityManager (fresh identity map + UnitOfWork)
|
|
10
|
+
// so it can never disturb the originating CRUD write's `em`. The data engine awaits
|
|
11
|
+
// this emit for read-your-writes consistency, which means the subscriber runs
|
|
12
|
+
// synchronously on the request `em`; sharing it would let our `em.find` / raw
|
|
13
|
+
// `getKysely()` queries reset the caller's UoW change-tracking and silently drop the
|
|
14
|
+
// caller's pending write (e.g. the deal's `setCustomFields` insert). The fork reads
|
|
15
|
+
// the same committed DB rows via the shared connection but keeps its own UoW.
|
|
16
|
+
const baseEm = ctx.resolve<any>('em')
|
|
17
|
+
const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm
|
|
10
18
|
const entityType = String(payload?.entityType || '')
|
|
11
19
|
const recordId = String(payload?.recordId || '')
|
|
12
20
|
if (!entityType || !recordId) return
|
|
@@ -28,14 +36,18 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
28
36
|
organizationId = resolvedScope.organizationId
|
|
29
37
|
tenantId = resolvedScope.tenantId
|
|
30
38
|
|
|
39
|
+
const searchTokenDoc = typeof payload?.searchTokenDoc === 'object' && payload.searchTokenDoc && !Array.isArray(payload.searchTokenDoc)
|
|
40
|
+
? (payload.searchTokenDoc as Record<string, unknown>)
|
|
41
|
+
: null
|
|
42
|
+
// Update the projection row synchronously so list reads (`customValues`) are
|
|
43
|
+
// consistent the moment the write returns; defer the heavy search-token rebuild.
|
|
31
44
|
const result = await upsertIndexRow(em, {
|
|
32
45
|
entityType,
|
|
33
46
|
recordId,
|
|
34
47
|
organizationId,
|
|
35
48
|
tenantId,
|
|
36
|
-
searchTokenDoc
|
|
37
|
-
|
|
38
|
-
: null,
|
|
49
|
+
searchTokenDoc,
|
|
50
|
+
deferSearchTokens: true,
|
|
39
51
|
})
|
|
40
52
|
if (!suppressCoverage) {
|
|
41
53
|
const doc = result.doc
|
|
@@ -84,16 +96,33 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
84
96
|
} catch {}
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
// Defer the heavy, eventually-consistent tail: search-token rebuild + vectorize +
|
|
100
|
+
// fulltext indexing. The data engine awaits this subscriber for projection
|
|
101
|
+
// consistency, so this work runs fire-and-forget to keep write latency bounded.
|
|
102
|
+
const deferredScope = { entityType, recordId, organizationId, tenantId }
|
|
103
|
+
const resolvedDoc = result.doc
|
|
104
|
+
void (async () => {
|
|
105
|
+
try {
|
|
106
|
+
await reindexSearchTokensForRecord(em, { ...deferredScope, doc: resolvedDoc, searchTokenDoc })
|
|
107
|
+
const bus = ctx.resolve<any>('eventBus')
|
|
108
|
+
await bus.emitEvent('query_index.vectorize_one', deferredScope)
|
|
109
|
+
await bus.emitEvent('search.index_record', { entityId: entityType, recordId, organizationId, tenantId })
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await recordIndexerError(
|
|
112
|
+
{ em },
|
|
113
|
+
{
|
|
114
|
+
source: 'query_index',
|
|
115
|
+
handler: 'event:query_index.upsert_one:search_tokens',
|
|
116
|
+
error,
|
|
117
|
+
entityType,
|
|
118
|
+
recordId,
|
|
119
|
+
tenantId: tenantId ?? null,
|
|
120
|
+
organizationId: organizationId ?? null,
|
|
121
|
+
payload,
|
|
122
|
+
},
|
|
123
|
+
).catch(() => {})
|
|
124
|
+
}
|
|
125
|
+
})()
|
|
97
126
|
} catch (error) {
|
|
98
127
|
await recordIndexerError(
|
|
99
128
|
{ em },
|
|
@@ -85,6 +85,13 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
85
85
|
const searchParams = useSearchParams()
|
|
86
86
|
const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
|
|
87
87
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
88
|
+
// Capture the record (incl. its optimistic-lock `updatedAt`) exactly ONCE per
|
|
89
|
+
// resource. The load effect's deps include identity-unstable values (`t`,
|
|
90
|
+
// `resolveFieldsetCode`), so without this guard a re-render would re-fetch and
|
|
91
|
+
// overwrite `initialValues.updatedAt` with a newer server value mid-edit —
|
|
92
|
+
// silently defeating optimistic locking (a concurrent change would no longer be
|
|
93
|
+
// detected) and making the conflict flaky.
|
|
94
|
+
const loadedResourceIdRef = React.useRef<string | null>(null)
|
|
88
95
|
const [tags, setTags] = React.useState<TagOption[]>([])
|
|
89
96
|
const [activeTab, setActiveTab] = React.useState<'details' | 'availability'>('details')
|
|
90
97
|
const [activeDetailTab, setActiveDetailTab] = React.useState<'notes' | 'activities'>('notes')
|
|
@@ -413,6 +420,9 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
413
420
|
|
|
414
421
|
React.useEffect(() => {
|
|
415
422
|
if (!resourceId || !resourceTypesLoaded) return
|
|
423
|
+
// Load once per resource — never re-fetch (and thereby refresh the captured
|
|
424
|
+
// optimistic-lock token) on subsequent re-renders. See loadedResourceIdRef.
|
|
425
|
+
if (loadedResourceIdRef.current === resourceId) return
|
|
416
426
|
setIsNotFound(false)
|
|
417
427
|
let cancelled = false
|
|
418
428
|
async function loadResource() {
|
|
@@ -429,6 +439,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
429
439
|
return
|
|
430
440
|
}
|
|
431
441
|
if (!cancelled) {
|
|
442
|
+
loadedResourceIdRef.current = resourceId ?? null
|
|
432
443
|
const customValues = extractCustomFieldEntries(resource)
|
|
433
444
|
setTags(Array.isArray(resource.tags) ? resource.tags : [])
|
|
434
445
|
setAvailabilityRuleSetId(
|
|
@@ -433,6 +433,14 @@ export async function executeWorkflow(
|
|
|
433
433
|
console.error(`[WORKFLOW] Transition rejected (instance: ${currentInstance.id}, workflow: ${currentInstance.workflowId}, step: ${currentInstance.currentStepId} → ${selectedTransition.toStepId}): ${rejectionMessage}`)
|
|
434
434
|
errors.push(rejectionMessage)
|
|
435
435
|
|
|
436
|
+
await completeWorkflow(trx, container, instanceId, 'FAILED', {
|
|
437
|
+
error: rejectionMessage,
|
|
438
|
+
})
|
|
439
|
+
events.push({
|
|
440
|
+
eventType: 'WORKFLOW_FAILED',
|
|
441
|
+
occurredAt: new Date(),
|
|
442
|
+
})
|
|
443
|
+
|
|
436
444
|
return {
|
|
437
445
|
status: 'FAILED',
|
|
438
446
|
currentStep: currentInstance.currentStepId,
|
|
@@ -501,6 +509,15 @@ export async function executeWorkflow(
|
|
|
501
509
|
},
|
|
502
510
|
})
|
|
503
511
|
|
|
512
|
+
await completeWorkflow(trx, container, instanceId, 'FAILED', {
|
|
513
|
+
error: errorMessage,
|
|
514
|
+
details: error instanceof WorkflowExecutionError ? error.details : undefined,
|
|
515
|
+
})
|
|
516
|
+
events.push({
|
|
517
|
+
eventType: 'WORKFLOW_FAILED',
|
|
518
|
+
occurredAt: new Date(),
|
|
519
|
+
})
|
|
520
|
+
|
|
504
521
|
return {
|
|
505
522
|
status: 'FAILED',
|
|
506
523
|
currentStep: currentInstance.currentStepId,
|