@open-mercato/core 0.8.1-develop.7241.1.fad7b2ce2a → 0.8.1-develop.7261.1.5d42353f61
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/attachments/lib/storage.js +15 -1
- package/dist/modules/attachments/lib/storage.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +3 -2
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +2 -1
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/di.js +6 -6
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/lib/tenantHint.js +29 -0
- package/dist/modules/auth/lib/tenantHint.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +11 -3
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +27 -14
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +9 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +8 -8
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +3 -3
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +18 -7
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +23 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +28 -24
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +41 -66
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +5 -0
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +3 -7
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +52 -3
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/storage.ts +24 -1
- package/src/modules/auth/api/reset/confirm.ts +3 -2
- package/src/modules/auth/api/reset.ts +2 -1
- package/src/modules/auth/di.ts +6 -6
- package/src/modules/auth/lib/tenantHint.ts +49 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +19 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/api/admin/users.ts +30 -14
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +11 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +11 -11
- package/src/modules/customer_accounts/di.ts +3 -3
- package/src/modules/customer_accounts/i18n/de.json +1 -0
- package/src/modules/customer_accounts/i18n/en.json +1 -0
- package/src/modules/customer_accounts/i18n/es.json +1 -0
- package/src/modules/customer_accounts/i18n/ko.json +1 -0
- package/src/modules/customer_accounts/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/lib/customerAuth.ts +36 -7
- package/src/modules/customer_accounts/services/customerSessionService.ts +28 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +49 -39
- package/src/modules/customers/backend/customers/deals/map/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +69 -56
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +8 -2
- package/src/modules/customers/commands/companies.ts +8 -0
- package/src/modules/customers/components/formConfig.tsx +3 -3
- package/src/modules/customers/data/validators.ts +2 -1
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/lib/sync-engine.ts +23 -12
- package/src/modules/data_sync/lib/sync-schedule-service.ts +54 -3
- package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/progress/lib/progressServiceImpl.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { raw } from '@mikro-orm/core'\nimport type { EntityData, FilterQuery } from '@mikro-orm/core'\nimport { ProgressJob, type ProgressJobStatus } from '../data/entities'\nimport type { ProgressService, ProgressServiceContext } from './progressService'\nimport {\n calculateEta,\n calculateProgressPercent,\n HEARTBEAT_INTERVAL_MS,\n STALE_JOB_TIMEOUT_SECONDS,\n STALE_PENDING_TIMEOUT_SECONDS,\n STALE_SWEEP_ERROR_PREFIX,\n} from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250\n\n// Minimum elapsed time between coalesced `progress.job.updated` broadcasts for a\n// single job. Bulk workers call updateProgress/incrementProgress once per record, and\n// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip\n// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.\n// Persistence is throttled separately: heartbeats hit the database at least every\n// HEARTBEAT_INTERVAL_MS regardless of this knob, so a high broadcast interval can\n// never starve the stale-job sweep of heartbeats.\nfunction resolveBroadcastMinIntervalMs(): number {\n const rawValue = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS\n if (rawValue == null || rawValue.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n return parsed\n}\n\n// Statuses a given transition is allowed to move FROM. Every write below is a\n// compare-and-swap (`nativeUpdate` guarded on status), so concurrent writers in other\n// processes can never resurrect a terminal job or double-apply a transition \u2014 the\n// UPDATE that matches zero rows simply lost the race and must not emit events.\nconst TERMINAL_STATUSES: readonly ProgressJobStatus[] = ['completed', 'failed', 'cancelled']\nconst PROGRESS_WRITABLE_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\n// `failed` is restartable/completable so a job wrongly swept as stale (worker alive but\n// slow) or retried by an at-least-once queue converges to its true outcome.\nconst START_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'failed']\nconst COMPLETE_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\nconst FAIL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\nconst CANCEL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\n\ntype JobUpdateThrottleEntry = {\n job: ProgressJob\n lastBroadcastAt: number\n lastPersistedAt: number\n lastBroadcastPercent: number\n pendingDelta: number\n absoluteCountsPending: boolean\n}\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()\n // Per-job coalescing state, scoped to this service instance (request/worker scope).\n // The cached entity is a DETACHED snapshot (loaded with disableIdentityMap) that doubles\n // as the in-memory buffer: intermediate updates mutate it without touching the shared\n // identity map or UnitOfWork, so an unrelated em.flush() elsewhere can never write the\n // buffered values. Persistence happens exclusively through the CAS nativeUpdate below.\n const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()\n\n function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {\n return {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n }\n\n async function loadFreshJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null> {\n return em.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n }\n\n async function ensureThrottleEntry(jobId: string, ctx: ProgressServiceContext): Promise<JobUpdateThrottleEntry> {\n const cached = jobUpdateThrottle.get(jobId)\n if (cached) return cached\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n const entry: JobUpdateThrottleEntry = {\n job,\n lastBroadcastAt: 0,\n lastPersistedAt: 0,\n lastBroadcastPercent: job.progressPercent,\n pendingDelta: 0,\n absoluteCountsPending: false,\n }\n jobUpdateThrottle.set(jobId, entry)\n return entry\n }\n\n function forgetJobThrottle(jobId: string) {\n jobUpdateThrottle.delete(jobId)\n }\n\n function buildBufferedCountData(entry: JobUpdateThrottleEntry): EntityData<ProgressJob> {\n if (entry.absoluteCountsPending || !Number.isSafeInteger(entry.pendingDelta)) {\n return { processedCount: entry.job.processedCount }\n }\n if (entry.pendingDelta !== 0) {\n const processedCount = raw(`processed_count + ${entry.pendingDelta}`)\n const totalCount = entry.job.totalCount\n if (Number.isSafeInteger(totalCount) && totalCount != null && totalCount > 0) {\n return {\n processedCount,\n progressPercent: raw(\n `least(100, round(((processed_count + ${entry.pendingDelta})::numeric / ${totalCount}) * 100))`,\n ),\n }\n }\n return { processedCount }\n }\n return {}\n }\n\n // The start transition (START_FROM_STATUSES) doubles as the recovery path for jobs a\n // stale sweep flipped to `failed` while their producer was alive but slow. Callers pass\n // the EM to run against so the forked heartbeat path can reuse the same CAS.\n //\n // `staleSweptOnly` is for the revive paths: a live write proves the producer is alive,\n // but it does NOT prove the recorded failure was the sweep's. Narrowing the CAS to rows\n // the sweep tagged keeps a genuine failure's message and stack intact instead of\n // resurrecting the job and erasing why it died. `startJob` stays unrestricted so an\n // at-least-once queue can still retry a genuinely failed job from the top.\n async function startJobViaCas(\n targetEm: EntityManager,\n job: ProgressJob,\n ctx: ProgressServiceContext,\n options: { staleSweptOnly?: boolean } = {},\n ): Promise<ProgressJob | null> {\n const now = new Date()\n // Progress counts are absolute and survive across deliveries, so the elapsed window\n // calculateEta divides them by has to survive too. Resetting it on every revive makes\n // a job with hours of work left report seconds remaining.\n const startedAt = job.startedAt ?? now\n const filter: Record<string, unknown> = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: START_FROM_STATUSES as ProgressJobStatus[] },\n }\n if (options.staleSweptOnly) filter.errorMessage = { $like: `${STALE_SWEEP_ERROR_PREFIX}%` }\n const affected = await targetEm.nativeUpdate(ProgressJob, filter as FilterQuery<ProgressJob>, {\n status: 'running',\n startedAt,\n heartbeatAt: now,\n finishedAt: null,\n errorMessage: null,\n errorStack: null,\n updatedAt: now,\n })\n if (affected === 0) return null\n\n job.status = 'running'\n job.startedAt = startedAt\n job.heartbeatAt = now\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n }\n\n async function persistAndMaybeBroadcast(entry: JobUpdateThrottleEntry, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const job = entry.job\n const now = Date.now()\n const shouldBroadcast =\n broadcastMinIntervalMs <= 0 ||\n now - entry.lastBroadcastAt >= broadcastMinIntervalMs ||\n Math.abs(job.progressPercent - entry.lastBroadcastPercent) >= 1\n const shouldPersist = shouldBroadcast || now - entry.lastPersistedAt >= HEARTBEAT_INTERVAL_MS\n\n if (!shouldPersist) return job\n\n const usesAtomicIncrement =\n !entry.absoluteCountsPending && Number.isSafeInteger(entry.pendingDelta) && entry.pendingDelta !== 0\n const data: EntityData<ProgressJob> = {\n progressPercent: job.progressPercent,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n meta: job.meta ?? null,\n heartbeatAt: job.heartbeatAt ?? new Date(),\n updatedAt: new Date(),\n ...buildBufferedCountData(entry),\n }\n const writableFilter = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>\n let affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n\n if (affected === 0) {\n // A stale sweep may have flipped a live job to `failed` between writes. Revive it\n // through the start CAS and retry once so the buffered delta isn't silently dropped.\n const fresh = await loadFreshJob(job.id, ctx)\n let revived = false\n if (fresh && fresh.status === 'failed' && (await startJobViaCas(em, fresh, ctx, { staleSweptOnly: true }))) {\n revived = true\n job.status = 'running'\n job.startedAt = fresh.startedAt\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n }\n if (affected === 0) {\n // The job reached a genuinely terminal state in another process; stop writing to it.\n forgetJobThrottle(job.id)\n const latest = revived ? await loadFreshJob(job.id, ctx) : fresh\n return latest ?? job\n }\n }\n\n const persistedJob = usesAtomicIncrement ? (await loadFreshJob(job.id, ctx)) ?? job : job\n entry.job = persistedJob\n entry.pendingDelta = 0\n entry.absoluteCountsPending = false\n entry.lastPersistedAt = now\n\n if (shouldBroadcast) {\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(persistedJob),\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n entry.lastBroadcastAt = now\n entry.lastBroadcastPercent = persistedJob.progressPercent\n }\n\n return persistedJob\n }\n\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (job.status === 'running' || job.status === 'cancelled' || job.status === 'completed') {\n return job\n }\n\n const started = await startJobViaCas(em, job, ctx)\n if (started) return started\n\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n },\n\n async touchJobHeartbeat(jobId, ctx) {\n // Forked EM: keepalive timers call this while the shared EM may be mid-transaction\n // inside a producer's own writes, so the heartbeat must not join that unit of work.\n const fork = em.fork()\n const now = new Date()\n const affected = await fork.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n heartbeatAt: now,\n updatedAt: now,\n })\n if (affected > 0) return\n\n const job = await fork.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (!job || job.status !== 'failed') return\n // A live producer heartbeating a `failed` job means a stale sweep false-positived;\n // revive through the start CAS so the card recovers without waiting for a batch boundary.\n await startJobViaCas(fork, job, ctx, { staleSweptOnly: true })\n },\n\n async updateProgress(jobId, input, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n // `failed` may be a false-positive stale sweep on a slow-but-alive producer; the\n // fact that this write is happening proves the producer is alive, so revive and\n // apply the update. `completed`/`cancelled` stay terminal, and a failure the sweep\n // did not write keeps its diagnostics (see startJobViaCas).\n const revived = job.status === 'failed' && (await startJobViaCas(em, job, ctx, { staleSweptOnly: true }))\n if (!revived) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n entry.absoluteCountsPending = true\n entry.pendingDelta = 0\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n let job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n let writable = false\n if (job.status === 'failed') {\n writable = (await startJobViaCas(em, job, ctx, { staleSweptOnly: true })) != null\n if (!writable) {\n // The cached snapshot's status lags the row whenever another writer \u2014 typically\n // the forked heartbeat in touchJobHeartbeat \u2014 already revived this job: the start\n // CAS then misses because the row is `running`, not `failed`. Increments are\n // relative, so returning here would lose the caller's delta for good (unlike\n // updateProgress, whose next absolute write self-corrects). Unlike\n // persistAndMaybeBroadcast, the re-read happens after the CAS rather than before\n // it: startJobViaCas filters on database state, so retrying it against the fresh\n // row could not change whether it matches.\n const fresh = await loadFreshJob(jobId, ctx)\n if (fresh) {\n job = fresh\n writable = PROGRESS_WRITABLE_STATUSES.includes(fresh.status)\n // Adopting the fresh row cannot desync entry.pendingDelta, which is provably 0\n // here: this guard is only reachable on a cached terminal status, and the only\n // writers that cache one \u2014 ensureThrottleEntry's initial load and\n // persistAndMaybeBroadcast's post-persist assignment \u2014 both leave the buffer empty.\n if (writable) entry.job = fresh\n }\n }\n }\n if (!writable) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n job.processedCount += delta\n entry.pendingDelta += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'completed',\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n ...(input?.resultSummary ? { resultSummary: input.resultSummary } : {}),\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n progressPercent: 100,\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: COMPLETE_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'completed'\n snapshot.finishedAt = now\n snapshot.progressPercent = 100\n snapshot.etaSeconds = 0\n if (input?.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(persistedJob),\n resultSummary: persistedJob.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async failJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'failed',\n finishedAt: now,\n errorMessage: input.errorMessage,\n errorStack: input.errorStack,\n ...(input.resultSummary ? { resultSummary: input.resultSummary } : {}),\n updatedAt: now,\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: FAIL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'failed'\n snapshot.finishedAt = now\n snapshot.errorMessage = input.errorMessage\n snapshot.errorStack = input.errorStack\n if (input.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(persistedJob),\n errorMessage: persistedJob.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (TERMINAL_STATUSES.includes(job.status)) {\n // The job finished while the user clicked cancel \u2014 a benign race, not an error.\n return job\n }\n if (!job.cancellable) {\n throw new Error(`Job ${jobId} is not cancellable`)\n }\n\n const now = new Date()\n const cancelledNow = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'pending',\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n })\n\n let requested = 0\n if (cancelledNow === 0) {\n requested = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'running',\n } as FilterQuery<ProgressJob>, {\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n updatedAt: now,\n })\n }\n\n if (cancelledNow === 0 && requested === 0) {\n // Raced into a terminal state between the read and the CAS.\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n forgetJobThrottle(jobId)\n if (cancelledNow > 0) {\n job.status = 'cancelled'\n job.finishedAt = now\n job.etaSeconds = 0\n }\n job.cancelRequestedAt = now\n job.cancelledByUserId = ctx.userId ?? null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const now = new Date()\n const cancelRequestedAt = job.cancelRequestedAt ?? now\n const cancelledByUserId = job.cancelledByUserId ?? ctx.userId ?? null\n const finishedAt = job.finishedAt ?? now\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: CANCEL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt,\n cancelledByUserId,\n finishedAt,\n etaSeconds: 0,\n updatedAt: now,\n })\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n job.status = 'cancelled'\n job.cancelRequestedAt = cancelRequestedAt\n job.cancelledByUserId = cancelledByUserId\n job.finishedAt = finishedAt\n job.etaSeconds = 0\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n // disableIdentityMap forces a fresh read: a managed copy of the job in this\n // EntityManager (loaded by updateProgress) must never mask a cancellation\n // requested from another process.\n const job = await findOneWithDecryption(em, ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }, { disableIdentityMap: true })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const now = new Date()\n const cutoff = new Date(now.getTime() - timeoutSeconds * 1000)\n const scope = {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }\n let failedCount = 0\n\n const staleFilter = {\n status: 'running' as ProgressJobStatus,\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n }\n const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true })\n\n for (const job of staleRunning) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} no heartbeat for ${timeoutSeconds} seconds`\n // Per-row CAS (re-checking the staleness condition): exactly one concurrent\n // sweeper wins, and a heartbeat that landed after the SELECT aborts the failure.\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...staleFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n // Jobs stuck in `pending` (enqueue failed, worker died before startJob) have no\n // heartbeat, so they need their own sweep or they stay in the active list forever.\n // A late queue delivery still recovers such a job: startJob transitions failed \u2192 running.\n const pendingCutoff = new Date(now.getTime() - STALE_PENDING_TIMEOUT_SECONDS * 1000)\n const stalePendingFilter = {\n status: 'pending' as ProgressJobStatus,\n createdAt: { $lt: pendingCutoff },\n }\n const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true })\n\n for (const job of stalePending) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...stalePendingFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n return failedCount\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { raw } from '@mikro-orm/core'\nimport type { EntityData, FilterQuery } from '@mikro-orm/core'\nimport { ProgressJob, type ProgressJobStatus } from '../data/entities'\nimport type { ProgressService, ProgressServiceContext } from './progressService'\nimport {\n calculateEta,\n calculateProgressPercent,\n HEARTBEAT_INTERVAL_MS,\n STALE_JOB_TIMEOUT_SECONDS,\n STALE_PENDING_TIMEOUT_SECONDS,\n STALE_SWEEP_ERROR_PREFIX,\n} from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250\n\n// Minimum elapsed time between coalesced `progress.job.updated` broadcasts for a\n// single job. Bulk workers call updateProgress/incrementProgress once per record, and\n// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip\n// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.\n// Persistence is throttled separately: heartbeats hit the database at least every\n// HEARTBEAT_INTERVAL_MS regardless of this knob, so a high broadcast interval can\n// never starve the stale-job sweep of heartbeats.\nfunction resolveBroadcastMinIntervalMs(): number {\n const rawValue = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS\n if (rawValue == null || rawValue.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n return parsed\n}\n\n// Statuses a given transition is allowed to move FROM. Every write below is a\n// compare-and-swap (`nativeUpdate` guarded on status), so concurrent writers in other\n// processes can never resurrect a terminal job or double-apply a transition \u2014 the\n// UPDATE that matches zero rows simply lost the race and must not emit events.\nconst TERMINAL_STATUSES: readonly ProgressJobStatus[] = ['completed', 'failed', 'cancelled']\nconst PROGRESS_WRITABLE_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\n// `failed` is restartable/completable so a job wrongly swept as stale (worker alive but\n// slow) or retried by an at-least-once queue converges to its true outcome.\nconst START_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'failed']\nconst COMPLETE_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\nconst FAIL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\nconst CANCEL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\n\ntype JobUpdateThrottleEntry = {\n job: ProgressJob\n lastBroadcastAt: number\n lastPersistedAt: number\n lastBroadcastPercent: number\n pendingDelta: number\n absoluteCountsPending: boolean\n}\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()\n // Per-job coalescing state, scoped to this service instance (request/worker scope).\n // The cached entity is a DETACHED snapshot (loaded with disableIdentityMap) that doubles\n // as the in-memory buffer: intermediate updates mutate it without touching the shared\n // identity map or UnitOfWork, so an unrelated em.flush() elsewhere can never write the\n // buffered values. Persistence happens exclusively through the CAS nativeUpdate below.\n const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()\n\n function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {\n return {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n }\n\n async function loadFreshJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null> {\n return em.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n }\n\n async function ensureThrottleEntry(jobId: string, ctx: ProgressServiceContext): Promise<JobUpdateThrottleEntry> {\n const cached = jobUpdateThrottle.get(jobId)\n if (cached) return cached\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n const entry: JobUpdateThrottleEntry = {\n job,\n lastBroadcastAt: 0,\n lastPersistedAt: 0,\n lastBroadcastPercent: job.progressPercent,\n pendingDelta: 0,\n absoluteCountsPending: false,\n }\n jobUpdateThrottle.set(jobId, entry)\n return entry\n }\n\n function forgetJobThrottle(jobId: string) {\n jobUpdateThrottle.delete(jobId)\n }\n\n function buildBufferedCountData(entry: JobUpdateThrottleEntry): EntityData<ProgressJob> {\n if (entry.absoluteCountsPending || !Number.isSafeInteger(entry.pendingDelta)) {\n return { processedCount: entry.job.processedCount }\n }\n if (entry.pendingDelta !== 0) {\n const processedCount = raw(`processed_count + ${entry.pendingDelta}`)\n const totalCount = entry.job.totalCount\n if (Number.isSafeInteger(totalCount) && totalCount != null && totalCount > 0) {\n return {\n processedCount,\n progressPercent: raw(\n `least(100, round(((processed_count + ${entry.pendingDelta})::numeric / ${totalCount}) * 100))`,\n ),\n }\n }\n return { processedCount }\n }\n return {}\n }\n\n // The start transition (START_FROM_STATUSES) doubles as the recovery path for jobs a\n // stale sweep flipped to `failed` while their producer was alive but slow. Callers pass\n // the EM to run against so the forked heartbeat path can reuse the same CAS.\n //\n // `staleSweptOnly` is for the revive paths: a live write proves the producer is alive,\n // but it does NOT prove the recorded failure was the sweep's. Narrowing the CAS to rows\n // the sweep tagged keeps a genuine failure's message and stack intact instead of\n // resurrecting the job and erasing why it died. `startJob` stays unrestricted so an\n // at-least-once queue can still retry a genuinely failed job from the top.\n async function startJobViaCas(\n targetEm: EntityManager,\n job: ProgressJob,\n ctx: ProgressServiceContext,\n options: { staleSweptOnly?: boolean } = {},\n ): Promise<ProgressJob | null> {\n const now = new Date()\n // Progress counts are absolute and survive across deliveries, so the elapsed window\n // calculateEta divides them by has to survive too. Resetting it on every revive makes\n // a job with hours of work left report seconds remaining.\n const startedAt = job.startedAt ?? now\n const filter: Record<string, unknown> = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: START_FROM_STATUSES as ProgressJobStatus[] },\n }\n if (options.staleSweptOnly) filter.errorMessage = { $like: `${STALE_SWEEP_ERROR_PREFIX}%` }\n const affected = await targetEm.nativeUpdate(ProgressJob, filter as FilterQuery<ProgressJob>, {\n status: 'running',\n startedAt,\n heartbeatAt: now,\n finishedAt: null,\n errorMessage: null,\n errorStack: null,\n updatedAt: now,\n })\n if (affected === 0) return null\n\n job.status = 'running'\n job.startedAt = startedAt\n job.heartbeatAt = now\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n }\n\n async function persistAndMaybeBroadcast(entry: JobUpdateThrottleEntry, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const job = entry.job\n const now = Date.now()\n const shouldBroadcast =\n broadcastMinIntervalMs <= 0 ||\n now - entry.lastBroadcastAt >= broadcastMinIntervalMs ||\n Math.abs(job.progressPercent - entry.lastBroadcastPercent) >= 1\n const shouldPersist = shouldBroadcast || now - entry.lastPersistedAt >= HEARTBEAT_INTERVAL_MS\n\n if (!shouldPersist) return job\n\n const usesAtomicIncrement =\n !entry.absoluteCountsPending && Number.isSafeInteger(entry.pendingDelta) && entry.pendingDelta !== 0\n const data: EntityData<ProgressJob> = {\n progressPercent: job.progressPercent,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n meta: job.meta ?? null,\n heartbeatAt: job.heartbeatAt ?? new Date(),\n updatedAt: new Date(),\n ...buildBufferedCountData(entry),\n }\n const writableFilter = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>\n let affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n\n if (affected === 0) {\n // A stale sweep may have flipped a live job to `failed` between writes. Revive it\n // through the start CAS and retry once so the buffered delta isn't silently dropped.\n const fresh = await loadFreshJob(job.id, ctx)\n let revived = false\n if (fresh && fresh.status === 'failed' && (await startJobViaCas(em, fresh, ctx, { staleSweptOnly: true }))) {\n revived = true\n job.status = 'running'\n job.startedAt = fresh.startedAt\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n }\n if (affected === 0) {\n // The job reached a genuinely terminal state in another process; stop writing to it.\n forgetJobThrottle(job.id)\n const latest = revived ? await loadFreshJob(job.id, ctx) : fresh\n return latest ?? job\n }\n }\n\n const persistedJob = usesAtomicIncrement ? (await loadFreshJob(job.id, ctx)) ?? job : job\n entry.job = persistedJob\n entry.pendingDelta = 0\n entry.absoluteCountsPending = false\n entry.lastPersistedAt = now\n\n if (shouldBroadcast) {\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(persistedJob),\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n entry.lastBroadcastAt = now\n entry.lastBroadcastPercent = persistedJob.progressPercent\n }\n\n return persistedJob\n }\n\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (job.status === 'running' || job.status === 'cancelled' || job.status === 'completed') {\n return job\n }\n\n const started = await startJobViaCas(em, job, ctx)\n if (started) return started\n\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n },\n\n async touchJobHeartbeat(jobId, ctx) {\n // Forked EM: keepalive timers call this while the shared EM may be mid-transaction\n // inside a producer's own writes, so the heartbeat must not join that unit of work.\n const fork = em.fork()\n const now = new Date()\n const affected = await fork.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n heartbeatAt: now,\n updatedAt: now,\n })\n if (affected > 0) return\n\n const job = await fork.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (!job || job.status !== 'failed') return\n // A live producer heartbeating a `failed` job means a stale sweep false-positived;\n // revive through the start CAS so the card recovers without waiting for a batch boundary.\n await startJobViaCas(fork, job, ctx, { staleSweptOnly: true })\n },\n\n async updateProgress(jobId, input, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n // `failed` may be a false-positive stale sweep on a slow-but-alive producer; the\n // fact that this write is happening proves the producer is alive, so revive and\n // apply the update. `completed`/`cancelled` stay terminal, and a failure the sweep\n // did not write keeps its diagnostics (see startJobViaCas).\n const revived = job.status === 'failed' && (await startJobViaCas(em, job, ctx, { staleSweptOnly: true }))\n if (!revived) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n entry.absoluteCountsPending = true\n entry.pendingDelta = 0\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n let job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n let writable = false\n if (job.status === 'failed') {\n writable = (await startJobViaCas(em, job, ctx, { staleSweptOnly: true })) != null\n if (!writable) {\n // The cached snapshot's status lags the row whenever another writer \u2014 typically\n // the forked heartbeat in touchJobHeartbeat \u2014 already revived this job: the start\n // CAS then misses because the row is `running`, not `failed`. Increments are\n // relative, so returning here would lose the caller's delta for good (unlike\n // updateProgress, whose next absolute write self-corrects). Unlike\n // persistAndMaybeBroadcast, the re-read happens after the CAS rather than before\n // it: startJobViaCas filters on database state, so retrying it against the fresh\n // row could not change whether it matches.\n const fresh = await loadFreshJob(jobId, ctx)\n if (fresh) {\n job = fresh\n writable = PROGRESS_WRITABLE_STATUSES.includes(fresh.status)\n // Adopting the fresh row cannot desync entry.pendingDelta, which is provably 0\n // here: this guard is only reachable on a cached terminal status, and the only\n // writers that cache one \u2014 ensureThrottleEntry's initial load and\n // persistAndMaybeBroadcast's post-persist assignment \u2014 both leave the buffer empty.\n if (writable) entry.job = fresh\n }\n }\n }\n if (!writable) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n job.processedCount += delta\n entry.pendingDelta += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'completed',\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n ...(input?.resultSummary ? { resultSummary: input.resultSummary } : {}),\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n progressPercent: 100,\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: COMPLETE_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'completed'\n snapshot.finishedAt = now\n snapshot.progressPercent = 100\n snapshot.etaSeconds = 0\n if (input?.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(persistedJob),\n resultSummary: persistedJob.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async failJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'failed',\n finishedAt: now,\n errorMessage: input.errorMessage,\n errorStack: input.errorStack,\n ...(input.resultSummary ? { resultSummary: input.resultSummary } : {}),\n updatedAt: now,\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: FAIL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'failed'\n snapshot.finishedAt = now\n snapshot.errorMessage = input.errorMessage\n snapshot.errorStack = input.errorStack\n if (input.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(persistedJob),\n errorMessage: persistedJob.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (TERMINAL_STATUSES.includes(job.status)) {\n // The job finished while the user clicked cancel \u2014 a benign race, not an error.\n return job\n }\n if (!job.cancellable) {\n throw new Error(`Job ${jobId} is not cancellable`)\n }\n\n const now = new Date()\n const cancelledNow = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'pending',\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n })\n\n let requested = 0\n if (cancelledNow === 0) {\n requested = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'running',\n } as FilterQuery<ProgressJob>, {\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n updatedAt: now,\n })\n }\n\n if (cancelledNow === 0 && requested === 0) {\n // Raced into a terminal state between the read and the CAS.\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n forgetJobThrottle(jobId)\n if (cancelledNow > 0) {\n job.status = 'cancelled'\n job.finishedAt = now\n job.etaSeconds = 0\n }\n job.cancelRequestedAt = now\n job.cancelledByUserId = ctx.userId ?? null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const now = new Date()\n const cancelRequestedAt = job.cancelRequestedAt ?? now\n const cancelledByUserId = job.cancelledByUserId ?? ctx.userId ?? null\n const finishedAt = job.finishedAt ?? now\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: CANCEL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt,\n cancelledByUserId,\n finishedAt,\n etaSeconds: 0,\n updatedAt: now,\n })\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n job.status = 'cancelled'\n job.cancelRequestedAt = cancelRequestedAt\n job.cancelledByUserId = cancelledByUserId\n job.finishedAt = finishedAt\n job.etaSeconds = 0\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n // Forked EM, for the same reason touchJobHeartbeat forks: this is polled from the\n // keepalive timer, which spans the producer's own transactional writes on the shared\n // EM, so an unawaited poll would issue a SELECT into an open em.begin()/em.commit()\n // window and interleave with its UnitOfWork. Every caller only reads the flag, so a\n // fork costs nothing and no caller depends on joining an ambient transaction.\n //\n // disableIdentityMap forces a fresh read: a managed copy of the job in this\n // EntityManager (loaded by updateProgress) must never mask a cancellation\n // requested from another process.\n const job = await findOneWithDecryption(em.fork(), ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }, { disableIdentityMap: true })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const now = new Date()\n const cutoff = new Date(now.getTime() - timeoutSeconds * 1000)\n const scope = {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }\n let failedCount = 0\n\n const staleFilter = {\n status: 'running' as ProgressJobStatus,\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n }\n const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true })\n\n for (const job of staleRunning) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} no heartbeat for ${timeoutSeconds} seconds`\n // Per-row CAS (re-checking the staleness condition): exactly one concurrent\n // sweeper wins, and a heartbeat that landed after the SELECT aborts the failure.\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...staleFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n // Jobs stuck in `pending` (enqueue failed, worker died before startJob) have no\n // heartbeat, so they need their own sweep or they stay in the active list forever.\n // A late queue delivery still recovers such a job: startJob transitions failed \u2192 running.\n const pendingCutoff = new Date(now.getTime() - STALE_PENDING_TIMEOUT_SECONDS * 1000)\n const stalePendingFilter = {\n status: 'pending' as ProgressJobStatus,\n createdAt: { $lt: pendingCutoff },\n }\n const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true })\n\n for (const job of stalePending) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...stalePendingFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n return failedCount\n },\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.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7261.1.5d42353f61",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -253,16 +253,16 @@
|
|
|
253
253
|
"zod": "^4.4.3"
|
|
254
254
|
},
|
|
255
255
|
"peerDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.8.1-develop.
|
|
257
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
258
|
-
"@open-mercato/ui": "0.8.1-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.8.1-develop.7261.1.5d42353f61",
|
|
257
|
+
"@open-mercato/shared": "0.8.1-develop.7261.1.5d42353f61",
|
|
258
|
+
"@open-mercato/ui": "0.8.1-develop.7261.1.5d42353f61",
|
|
259
259
|
"react": "^19.0.0",
|
|
260
260
|
"react-dom": "^19.0.0"
|
|
261
261
|
},
|
|
262
262
|
"devDependencies": {
|
|
263
|
-
"@open-mercato/ai-assistant": "0.8.1-develop.
|
|
264
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
265
|
-
"@open-mercato/ui": "0.8.1-develop.
|
|
263
|
+
"@open-mercato/ai-assistant": "0.8.1-develop.7261.1.5d42353f61",
|
|
264
|
+
"@open-mercato/shared": "0.8.1-develop.7261.1.5d42353f61",
|
|
265
|
+
"@open-mercato/ui": "0.8.1-develop.7261.1.5d42353f61",
|
|
266
266
|
"@testing-library/dom": "^10.4.1",
|
|
267
267
|
"@testing-library/jest-dom": "^7.0.1",
|
|
268
268
|
"@testing-library/react": "^16.3.3",
|
|
@@ -4,13 +4,36 @@ import { randomUUID } from 'crypto'
|
|
|
4
4
|
import { resolvePartitionEnvKey } from './partitionEnv'
|
|
5
5
|
import { resolveContainedPath, resolveLegacyPublicRoot } from './pathContainment'
|
|
6
6
|
|
|
7
|
+
export const STORAGE_ROOT_ENV_KEY = 'ATTACHMENTS_STORAGE_ROOT'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the base directory that holds every partition directory. Deployment
|
|
11
|
+
* configuration (`ATTACHMENTS_STORAGE_ROOT`) wins over the historical
|
|
12
|
+
* `process.cwd()`-relative default, so the same attachment row resolves to the
|
|
13
|
+
* same file regardless of the directory the process was started from. A
|
|
14
|
+
* relative value is rejected rather than resolved against `process.cwd()`,
|
|
15
|
+
* because `LocalStorageDriver.store()` creates missing directories and a typo
|
|
16
|
+
* would otherwise silently produce a second, empty store.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveStorageRoot(): string {
|
|
19
|
+
const envPath = process.env[STORAGE_ROOT_ENV_KEY]
|
|
20
|
+
if (envPath && envPath.trim().length > 0) {
|
|
21
|
+
const trimmed = envPath.trim()
|
|
22
|
+
if (!path.isAbsolute(trimmed)) {
|
|
23
|
+
throw new Error(`[internal] ${STORAGE_ROOT_ENV_KEY} must be an absolute path, received: ${trimmed}`)
|
|
24
|
+
}
|
|
25
|
+
return path.resolve(trimmed)
|
|
26
|
+
}
|
|
27
|
+
return path.join(process.cwd(), 'storage', 'attachments')
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export function resolvePartitionRoot(code: string): string {
|
|
8
31
|
const envKey = resolvePartitionEnvKey(code)
|
|
9
32
|
const envPath = process.env[envKey]
|
|
10
33
|
if (envPath && envPath.trim().length > 0) {
|
|
11
34
|
return path.resolve(envPath)
|
|
12
35
|
}
|
|
13
|
-
return path.join(
|
|
36
|
+
return path.join(resolveStorageRoot(), code)
|
|
14
37
|
}
|
|
15
38
|
|
|
16
39
|
function sanitizeFileName(fileName: string): string {
|
|
@@ -12,6 +12,7 @@ import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/
|
|
|
12
12
|
import { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
|
|
13
13
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
14
|
import { emitAuthEvent } from '@open-mercato/core/modules/auth/events'
|
|
15
|
+
import { withTenantHintPath } from '@open-mercato/core/modules/auth/lib/tenantHint'
|
|
15
16
|
|
|
16
17
|
const logger = createLogger('auth').child({ component: 'reset-confirm' })
|
|
17
18
|
|
|
@@ -60,7 +61,7 @@ export async function POST(req: Request) {
|
|
|
60
61
|
} catch (err) {
|
|
61
62
|
logger.error('Failed to create notification', { err })
|
|
62
63
|
}
|
|
63
|
-
return NextResponse.json({ ok: true, redirect: '/login' })
|
|
64
|
+
return NextResponse.json({ ok: true, redirect: withTenantHintPath('/login', user.tenantId) })
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export const metadata = { requireAuth: false }
|
|
@@ -81,7 +82,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
81
82
|
methods: {
|
|
82
83
|
POST: {
|
|
83
84
|
summary: 'Complete password reset',
|
|
84
|
-
description: 'Validates the reset token and updates the user password.',
|
|
85
|
+
description: 'Validates the reset token and updates the user password. The returned `redirect` carries the resolved user\'s tenant as a navigation hint (`/login?tenant=...`) so a tenant-bound user lands on the tenant login entry; tenantless users get `/login`.',
|
|
85
86
|
requestBody: {
|
|
86
87
|
contentType: 'application/x-www-form-urlencoded',
|
|
87
88
|
schema: confirmPasswordResetSchema,
|
|
@@ -14,6 +14,7 @@ import { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers
|
|
|
14
14
|
import { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'
|
|
15
15
|
import { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'
|
|
16
16
|
import { mapSecurityEmailUrlError, toSecurityEmailUrl } from '@open-mercato/shared/lib/url'
|
|
17
|
+
import { withTenantHintUrl } from '@open-mercato/core/modules/auth/lib/tenantHint'
|
|
17
18
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
18
19
|
import { emitAuthEvent } from '@open-mercato/core/modules/auth/events'
|
|
19
20
|
|
|
@@ -57,7 +58,7 @@ export async function POST(req: Request) {
|
|
|
57
58
|
const resReq = await auth.requestPasswordReset(parsed.data.email)
|
|
58
59
|
if (!resReq) return NextResponse.json({ ok: true })
|
|
59
60
|
const { user, token } = resReq
|
|
60
|
-
const resetUrl = resetUrlTemplate.replace('__token__', token)
|
|
61
|
+
const resetUrl = withTenantHintUrl(resetUrlTemplate.replace('__token__', token), user.tenantId)
|
|
61
62
|
void emitAuthEvent('auth.password.reset.requested', {
|
|
62
63
|
id: String(user.id),
|
|
63
64
|
tenantId: user.tenantId ? String(user.tenantId) : null,
|
package/src/modules/auth/di.ts
CHANGED
|
@@ -40,25 +40,25 @@ export function register(container: AppContainer) {
|
|
|
40
40
|
// Register or override core auth service
|
|
41
41
|
container.register({ authService: asClass(AuthService).scoped() })
|
|
42
42
|
container.register({
|
|
43
|
-
authPrincipalService: asFunction(function authPrincipalServiceFactory(em: EntityManager) {
|
|
43
|
+
authPrincipalService: asFunction(function authPrincipalServiceFactory(cradle: { em: EntityManager }) {
|
|
44
44
|
return new DefaultAuthPrincipalService(
|
|
45
|
-
em,
|
|
45
|
+
cradle.em,
|
|
46
46
|
resolveOptionalOrganizationHierarchyService(container),
|
|
47
47
|
)
|
|
48
|
-
}).scoped(),
|
|
48
|
+
}).scoped().proxy(),
|
|
49
49
|
})
|
|
50
50
|
// Resolve optional infrastructure lazily so Auth still works when Directory
|
|
51
51
|
// is disabled and in lean CLI/test containers without a CacheStrategy.
|
|
52
52
|
// Setting `OM_RBAC_DEFAULT_CACHE=on` opts into the in-process fallback;
|
|
53
53
|
// an explicitly registered cache always wins.
|
|
54
54
|
container.register({
|
|
55
|
-
rbacService: asFunction(function rbacServiceFactory(em: EntityManager) {
|
|
55
|
+
rbacService: asFunction(function rbacServiceFactory(cradle: { em: EntityManager }) {
|
|
56
56
|
const configuredCache = resolveOptionalCache(container)
|
|
57
57
|
return new RbacService(
|
|
58
|
-
em,
|
|
58
|
+
cradle.em,
|
|
59
59
|
configuredCache ?? (isRbacDefaultCacheEnabled() ? createRbacFallbackCache() : undefined),
|
|
60
60
|
resolveOptionalOrganizationHierarchyService(container),
|
|
61
61
|
)
|
|
62
|
-
}).scoped(),
|
|
62
|
+
}).scoped().proxy(),
|
|
63
63
|
})
|
|
64
64
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant continuity hints for unauthenticated auth navigation.
|
|
3
|
+
*
|
|
4
|
+
* The `tenant` query parameter is a navigation hint only — it tells the login
|
|
5
|
+
* entry which tenant the user was already resolved to so it can show tenant
|
|
6
|
+
* branding instead of the generic form. It is never authorization, and it must
|
|
7
|
+
* only ever be derived from a server-resolved user, never from request input.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TENANT_HINT_PARAM = 'tenant'
|
|
11
|
+
|
|
12
|
+
export type TenantHintSource = string | null | undefined
|
|
13
|
+
|
|
14
|
+
function normalizeTenantHint(tenantId: TenantHintSource): string | null {
|
|
15
|
+
const value = tenantId?.trim()
|
|
16
|
+
return value ? value : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Adds the tenant hint to an absolute URL (password reset and invitation email
|
|
21
|
+
* links). Returns the URL unchanged for tenantless users or an unparsable URL,
|
|
22
|
+
* so a hint can never cost a user their working link.
|
|
23
|
+
*/
|
|
24
|
+
export function withTenantHintUrl(url: string, tenantId: TenantHintSource): string {
|
|
25
|
+
const tenant = normalizeTenantHint(tenantId)
|
|
26
|
+
if (!tenant) return url
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url)
|
|
29
|
+
parsed.searchParams.set(TENANT_HINT_PARAM, tenant)
|
|
30
|
+
return parsed.toString()
|
|
31
|
+
} catch {
|
|
32
|
+
return url
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Adds the tenant hint to an app-relative path (the redirect handed back to the
|
|
38
|
+
* browser after a successful reset). Returns the path unchanged for tenantless
|
|
39
|
+
* users. Encoding and replace-don't-duplicate semantics match
|
|
40
|
+
* {@link withTenantHintUrl}.
|
|
41
|
+
*/
|
|
42
|
+
export function withTenantHintPath(path: string, tenantId: TenantHintSource): string {
|
|
43
|
+
const tenant = normalizeTenantHint(tenantId)
|
|
44
|
+
if (!tenant) return path
|
|
45
|
+
const [pathname, existingQuery = ''] = path.split('?')
|
|
46
|
+
const params = new URLSearchParams(existingQuery)
|
|
47
|
+
params.set(TENANT_HINT_PARAM, tenant)
|
|
48
|
+
return `${pathname}?${params.toString()}`
|
|
49
|
+
}
|
|
@@ -935,6 +935,7 @@ export default function EditCatalogProductPage({
|
|
|
935
935
|
values={values as ProductFormValues}
|
|
936
936
|
setValue={setValue}
|
|
937
937
|
errors={errors}
|
|
938
|
+
variantCount={variants.length}
|
|
938
939
|
/>
|
|
939
940
|
),
|
|
940
941
|
},
|
|
@@ -1628,6 +1629,10 @@ type ProductVariantsSectionProps = Omit<
|
|
|
1628
1629
|
|
|
1629
1630
|
type ProductDimensionsSectionProps = ProductFormGroupProps;
|
|
1630
1631
|
|
|
1632
|
+
type ProductOptionsSectionProps = ProductFormGroupProps & {
|
|
1633
|
+
variantCount?: number;
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1631
1636
|
function ProductDetailsSection({
|
|
1632
1637
|
values,
|
|
1633
1638
|
setValue,
|
|
@@ -1923,7 +1928,11 @@ function ProductMetadataSection({ values, setValue }: ProductFormGroupProps) {
|
|
|
1923
1928
|
);
|
|
1924
1929
|
}
|
|
1925
1930
|
|
|
1926
|
-
function ProductOptionsSection({
|
|
1931
|
+
function ProductOptionsSection({
|
|
1932
|
+
values,
|
|
1933
|
+
setValue,
|
|
1934
|
+
variantCount = 0,
|
|
1935
|
+
}: ProductOptionsSectionProps) {
|
|
1927
1936
|
const t = useT();
|
|
1928
1937
|
const [schemaDialogOpen, setSchemaDialogOpen] = React.useState(false);
|
|
1929
1938
|
const [schemaTemplates, setSchemaTemplates] = React.useState<
|
|
@@ -2186,10 +2195,15 @@ function ProductOptionsSection({ values, setValue }: ProductFormGroupProps) {
|
|
|
2186
2195
|
))}
|
|
2187
2196
|
{!values.options?.length ? (
|
|
2188
2197
|
<p className="text-sm text-muted-foreground">
|
|
2189
|
-
{
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2198
|
+
{variantCount > 0
|
|
2199
|
+
? t(
|
|
2200
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants",
|
|
2201
|
+
"This product has variants without an option schema. Options are optional.",
|
|
2202
|
+
)
|
|
2203
|
+
: t(
|
|
2204
|
+
"catalog.products.create.optionsBuilder.empty",
|
|
2205
|
+
"No options yet. Add your first option to generate variants.",
|
|
2206
|
+
)}
|
|
2193
2207
|
</p>
|
|
2194
2208
|
) : null}
|
|
2195
2209
|
</div>
|
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
"catalog.products.edit.metadata.title": "Metadaten",
|
|
531
531
|
"catalog.products.edit.metadata.valuePlaceholder": "Wert",
|
|
532
532
|
"catalog.products.edit.offers.deleteError": "Vertriebskanal-Angebot konnte nicht entfernt werden.",
|
|
533
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants": "Dieses Produkt hat Varianten ohne Optionsschema. Optionen sind nicht erforderlich.",
|
|
533
534
|
"catalog.products.edit.save": "Änderungen speichern",
|
|
534
535
|
"catalog.products.edit.schemas.apply": "Anwenden",
|
|
535
536
|
"catalog.products.edit.schemas.delete": "Schema löschen",
|
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
"catalog.products.edit.metadata.title": "Metadata",
|
|
531
531
|
"catalog.products.edit.metadata.valuePlaceholder": "Value",
|
|
532
532
|
"catalog.products.edit.offers.deleteError": "Failed to remove sales channel offer.",
|
|
533
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants": "This product has variants without an option schema. Options are optional.",
|
|
533
534
|
"catalog.products.edit.save": "Save changes",
|
|
534
535
|
"catalog.products.edit.schemas.apply": "Apply",
|
|
535
536
|
"catalog.products.edit.schemas.delete": "Delete schema",
|
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
"catalog.products.edit.metadata.title": "Metadatos",
|
|
531
531
|
"catalog.products.edit.metadata.valuePlaceholder": "Valor",
|
|
532
532
|
"catalog.products.edit.offers.deleteError": "No se pudo eliminar la oferta del canal de ventas.",
|
|
533
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants": "Este producto tiene variantes sin un esquema de opciones. Las opciones son opcionales.",
|
|
533
534
|
"catalog.products.edit.save": "Guardar cambios",
|
|
534
535
|
"catalog.products.edit.schemas.apply": "Aplicar",
|
|
535
536
|
"catalog.products.edit.schemas.delete": "Eliminar esquema",
|
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
"catalog.products.edit.metadata.title": "메타데이터",
|
|
531
531
|
"catalog.products.edit.metadata.valuePlaceholder": "값",
|
|
532
532
|
"catalog.products.edit.offers.deleteError": "판매 채널 오퍼 제거에 실패했습니다.",
|
|
533
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants": "이 상품에는 옵션 스키마 없이 변형이 있습니다. 옵션은 선택 사항입니다.",
|
|
533
534
|
"catalog.products.edit.save": "변경 사항 저장",
|
|
534
535
|
"catalog.products.edit.schemas.apply": "적용",
|
|
535
536
|
"catalog.products.edit.schemas.delete": "스키마 삭제",
|
|
@@ -530,6 +530,7 @@
|
|
|
530
530
|
"catalog.products.edit.metadata.title": "Metadane",
|
|
531
531
|
"catalog.products.edit.metadata.valuePlaceholder": "Wartość",
|
|
532
532
|
"catalog.products.edit.offers.deleteError": "Nie udało się usunąć oferty kanału sprzedaży.",
|
|
533
|
+
"catalog.products.edit.optionsBuilder.emptyWithVariants": "Ten produkt ma warianty bez schematu opcji. Opcje są opcjonalne.",
|
|
533
534
|
"catalog.products.edit.save": "Zapisz zmiany",
|
|
534
535
|
"catalog.products.edit.schemas.apply": "Zastosuj",
|
|
535
536
|
"catalog.products.edit.schemas.delete": "Usuń schemat",
|
|
@@ -6,6 +6,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
6
6
|
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
7
|
import { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'
|
|
8
8
|
import { CustomerUser, CustomerUserRole, CustomerRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
|
|
9
|
+
import { Organization } from '@open-mercato/core/modules/directory/data/entities'
|
|
9
10
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
11
|
import { adminCreateUserSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
|
|
11
12
|
import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
|
|
@@ -181,20 +182,33 @@ export async function GET(req: Request) {
|
|
|
181
182
|
else rolesByUserId.set(linkUserId, [entry])
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
185
|
+
const pageOrganizationIds = Array.from(new Set(
|
|
186
|
+
users.map((user) => user.organizationId).filter((organizationId): organizationId is string => !!organizationId),
|
|
187
|
+
))
|
|
188
|
+
const organizations = pageOrganizationIds.length > 0
|
|
189
|
+
? await em.find(Organization, { id: { $in: pageOrganizationIds }, tenant: auth.tenantId, deletedAt: null })
|
|
190
|
+
: []
|
|
191
|
+
const organizationNameById = new Map(organizations.map((organization) => [organization.id, organization.name]))
|
|
192
|
+
|
|
193
|
+
const items = users.map((user) => {
|
|
194
|
+
const organizationId = user.organizationId ?? null
|
|
195
|
+
return {
|
|
196
|
+
id: user.id,
|
|
197
|
+
email: user.email,
|
|
198
|
+
displayName: user.displayName,
|
|
199
|
+
emailVerified: !!user.emailVerifiedAt,
|
|
200
|
+
isActive: user.isActive,
|
|
201
|
+
lockedUntil: user.lockedUntil || null,
|
|
202
|
+
lastLoginAt: user.lastLoginAt || null,
|
|
203
|
+
customerEntityId: user.customerEntityId || null,
|
|
204
|
+
personEntityId: user.personEntityId || null,
|
|
205
|
+
organizationId,
|
|
206
|
+
organizationName: organizationId ? organizationNameById.get(organizationId) ?? null : null,
|
|
207
|
+
createdAt: user.createdAt,
|
|
208
|
+
updatedAt: user.updatedAt || null,
|
|
209
|
+
roles: rolesByUserId.get(user.id) ?? [],
|
|
210
|
+
}
|
|
211
|
+
})
|
|
198
212
|
|
|
199
213
|
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
|
200
214
|
|
|
@@ -335,6 +349,8 @@ const userSchema = z.object({
|
|
|
335
349
|
lastLoginAt: z.string().datetime().nullable(),
|
|
336
350
|
customerEntityId: z.string().uuid().nullable(),
|
|
337
351
|
personEntityId: z.string().uuid().nullable(),
|
|
352
|
+
organizationId: z.string().uuid().nullable().describe('Organization the customer user belongs to'),
|
|
353
|
+
organizationName: z.string().nullable().describe('Resolved organization display name'),
|
|
338
354
|
createdAt: z.string().datetime(),
|
|
339
355
|
updatedAt: z.string().datetime().nullable(),
|
|
340
356
|
roles: z.array(roleSchema),
|
package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx
CHANGED
|
@@ -36,6 +36,8 @@ type UserRow = {
|
|
|
36
36
|
updatedAt?: string | null
|
|
37
37
|
personEntityId: string | null
|
|
38
38
|
customerEntityId: string | null
|
|
39
|
+
organizationId: string | null
|
|
40
|
+
organizationName: string | null
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
type UsersResponse = {
|
|
@@ -440,6 +442,15 @@ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: Po
|
|
|
440
442
|
accessorKey: 'email',
|
|
441
443
|
header: t('customer_accounts.admin.columns.email', 'Email'),
|
|
442
444
|
},
|
|
445
|
+
{
|
|
446
|
+
accessorKey: 'organizationName',
|
|
447
|
+
header: t('customer_accounts.admin.columns.organization', 'Organization'),
|
|
448
|
+
cell: ({ row }) => {
|
|
449
|
+
const label = row.original.organizationName || row.original.organizationId
|
|
450
|
+
if (!label) return noValue
|
|
451
|
+
return <span className="text-sm">{label}</span>
|
|
452
|
+
},
|
|
453
|
+
},
|
|
443
454
|
{
|
|
444
455
|
accessorKey: 'emailVerified',
|
|
445
456
|
header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),
|