@open-mercato/core 0.6.6-develop.6416.1.888fffc007 → 0.6.6-develop.6422.1.b3d024f05c

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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js +5 -1
  3. package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
  4. package/dist/modules/messages/api/[id]/route.js +25 -2
  5. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  6. package/dist/modules/messages/api/openapi.js +1 -0
  7. package/dist/modules/messages/api/openapi.js.map +2 -2
  8. package/dist/modules/messages/commands/actions.js +8 -0
  9. package/dist/modules/messages/commands/actions.js.map +2 -2
  10. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +50 -24
  11. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  12. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js +1 -0
  13. package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js.map +2 -2
  14. package/dist/modules/messages/lib/constants.js +3 -1
  15. package/dist/modules/messages/lib/constants.js.map +2 -2
  16. package/dist/modules/progress/lib/progressServiceImpl.js +56 -24
  17. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  18. package/dist/modules/staff/api/timesheets/time-entries/route.js +3 -28
  19. package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
  20. package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js +44 -0
  21. package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js.map +7 -0
  22. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +1 -2
  23. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  24. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +1 -2
  25. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  26. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +1 -6
  27. package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
  28. package/dist/modules/sync_excel/lib/adapters/customers.js +6 -6
  29. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -1
  32. package/src/modules/messages/api/[id]/route.ts +30 -1
  33. package/src/modules/messages/api/openapi.ts +1 -0
  34. package/src/modules/messages/commands/actions.ts +14 -0
  35. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +53 -28
  36. package/src/modules/messages/components/message-detail/panels/composer-dialogs.tsx +1 -0
  37. package/src/modules/messages/components/message-detail/types.ts +1 -0
  38. package/src/modules/messages/lib/constants.ts +4 -0
  39. package/src/modules/progress/AGENTS.md +6 -0
  40. package/src/modules/progress/lib/progressServiceImpl.ts +85 -29
  41. package/src/modules/staff/api/timesheets/time-entries/route.ts +3 -32
  42. package/src/modules/staff/lib/timesheets/timeEntryListFilters.ts +64 -0
  43. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +1 -2
  44. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +2 -4
  45. package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +1 -7
  46. package/src/modules/sync_excel/lib/adapters/customers.ts +6 -6
@@ -82,6 +82,12 @@ Use stable, grep-friendly ids:
82
82
  | DataTable progress result handling | `packages/ui/src/backend/DataTable.tsx` |
83
83
  | Progress UI merge/polling | `packages/ui/src/backend/progress/useProgressPoll.ts` and `useProgressSse.ts` |
84
84
 
85
+ ## Environment
86
+
87
+ | Variable | Effect | Default |
88
+ |----------|--------|---------|
89
+ | `OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS` | Coalesces intermediate `progress.job.updated` flush+broadcasts per job: the service flushes and emits only when this many ms elapsed since the last broadcast **or** `progressPercent` advanced by ≥1; sub-threshold updates buffer in memory. Keeps bulk workers from firing one serialized `pg_notify` roundtrip + tenant-wide SSE fan-out per record. Terminal events (`created`/`started`/`completed`/`failed`/`cancelled`) are never throttled, so `ProgressTopBar` still converges and the 60s `STALE_JOB_TIMEOUT_SECONDS` heartbeat window is never exceeded. Set to `0` to restore per-record emission (tests/debugging). | `250` |
90
+
85
91
  ## Cross-References
86
92
 
87
93
  - **Queue worker rules**: `packages/queue/AGENTS.md`
@@ -1,10 +1,30 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { ProgressJob } from '../data/entities'
3
- import type { ProgressService } from './progressService'
3
+ import type { ProgressService, ProgressServiceContext } from './progressService'
4
4
  import { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'
5
5
  import { PROGRESS_EVENTS } from './events'
6
6
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
7
 
8
+ const DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250
9
+
10
+ // Minimum elapsed time between coalesced `progress.job.updated` flush+broadcasts for a
11
+ // single job. Bulk workers call updateProgress/incrementProgress once per record, and
12
+ // every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip
13
+ // plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.
14
+ function resolveBroadcastMinIntervalMs(): number {
15
+ const raw = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS
16
+ if (raw == null || raw.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS
17
+ const parsed = Number.parseInt(raw, 10)
18
+ if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS
19
+ return parsed
20
+ }
21
+
22
+ type JobUpdateThrottleEntry = {
23
+ job: ProgressJob
24
+ lastBroadcastAt: number
25
+ lastBroadcastPercent: number
26
+ }
27
+
8
28
  function buildJobPayload(job: ProgressJob): Record<string, unknown> {
9
29
  return {
10
30
  jobId: job.id,
@@ -24,6 +44,60 @@ function buildJobPayload(job: ProgressJob): Record<string, unknown> {
24
44
  }
25
45
 
26
46
  export function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {
47
+ const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()
48
+ // Per-job coalescing state, scoped to this service instance (request/worker scope).
49
+ // The cached managed entity doubles as the in-memory buffer: intermediate updates mutate
50
+ // it without flushing, and a single flush+emit at the throttle boundary persists the
51
+ // accumulated change. Domain commands fork their own EntityManager, so no other operation
52
+ // queries this job on `em` between calls, keeping the buffered changes safe until flush.
53
+ const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()
54
+
55
+ function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {
56
+ return {
57
+ id: jobId,
58
+ tenantId: ctx.tenantId,
59
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
60
+ }
61
+ }
62
+
63
+ async function loadUpdatableJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob> {
64
+ const cached = jobUpdateThrottle.get(jobId)
65
+ if (cached) return cached.job
66
+ return em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx))
67
+ }
68
+
69
+ async function commitJobUpdate(job: ProgressJob, ctx: ProgressServiceContext): Promise<ProgressJob> {
70
+ const now = Date.now()
71
+ const cached = jobUpdateThrottle.get(job.id)
72
+ const shouldBroadcast =
73
+ broadcastMinIntervalMs <= 0 ||
74
+ cached == null ||
75
+ now - cached.lastBroadcastAt >= broadcastMinIntervalMs ||
76
+ Math.abs(job.progressPercent - cached.lastBroadcastPercent) >= 1
77
+
78
+ if (shouldBroadcast) {
79
+ await em.flush()
80
+ await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
81
+ ...buildJobPayload(job),
82
+ tenantId: ctx.tenantId,
83
+ organizationId: job.organizationId ?? null,
84
+ })
85
+ jobUpdateThrottle.set(job.id, { job, lastBroadcastAt: now, lastBroadcastPercent: job.progressPercent })
86
+ } else {
87
+ jobUpdateThrottle.set(job.id, {
88
+ job,
89
+ lastBroadcastAt: cached.lastBroadcastAt,
90
+ lastBroadcastPercent: cached.lastBroadcastPercent,
91
+ })
92
+ }
93
+
94
+ return job
95
+ }
96
+
97
+ function forgetJobThrottle(jobId: string) {
98
+ jobUpdateThrottle.delete(jobId)
99
+ }
100
+
27
101
  return {
28
102
  async createJob(input, ctx) {
29
103
  const job = em.create(ProgressJob, {
@@ -79,12 +153,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
79
153
  },
80
154
 
81
155
  async updateProgress(jobId, input, ctx) {
82
- const job = await em.findOneOrFail(ProgressJob, {
83
- id: jobId,
84
- tenantId: ctx.tenantId,
85
- ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
86
- })
156
+ const job = await loadUpdatableJob(jobId, ctx)
87
157
  if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
158
+ forgetJobThrottle(jobId)
88
159
  return job
89
160
  }
90
161
 
@@ -112,24 +183,13 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
112
183
 
113
184
  job.heartbeatAt = new Date()
114
185
 
115
- await em.flush()
116
-
117
- await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
118
- ...buildJobPayload(job),
119
- tenantId: ctx.tenantId,
120
- organizationId: job.organizationId ?? null,
121
- })
122
-
123
- return job
186
+ return commitJobUpdate(job, ctx)
124
187
  },
125
188
 
126
189
  async incrementProgress(jobId, delta, ctx) {
127
- const job = await em.findOneOrFail(ProgressJob, {
128
- id: jobId,
129
- tenantId: ctx.tenantId,
130
- ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
131
- })
190
+ const job = await loadUpdatableJob(jobId, ctx)
132
191
  if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
192
+ forgetJobThrottle(jobId)
133
193
  return job
134
194
  }
135
195
 
@@ -143,15 +203,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
143
203
  }
144
204
  }
145
205
 
146
- await em.flush()
147
-
148
- await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
149
- ...buildJobPayload(job),
150
- tenantId: ctx.tenantId,
151
- organizationId: job.organizationId ?? null,
152
- })
153
-
154
- return job
206
+ return commitJobUpdate(job, ctx)
155
207
  },
156
208
 
157
209
  async completeJob(jobId, input, ctx) {
@@ -182,6 +234,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
182
234
  organizationId: job.organizationId ?? null,
183
235
  })
184
236
 
237
+ forgetJobThrottle(jobId)
185
238
  return job
186
239
  },
187
240
 
@@ -210,6 +263,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
210
263
  organizationId: job.organizationId ?? null,
211
264
  })
212
265
 
266
+ forgetJobThrottle(jobId)
213
267
  return job
214
268
  },
215
269
 
@@ -238,6 +292,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
238
292
  organizationId: job.organizationId ?? null,
239
293
  })
240
294
 
295
+ forgetJobThrottle(jobId)
241
296
  return job
242
297
  },
243
298
 
@@ -266,6 +321,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
266
321
  organizationId: job.organizationId ?? null,
267
322
  })
268
323
 
324
+ forgetJobThrottle(jobId)
269
325
  return job
270
326
  },
271
327
 
@@ -4,6 +4,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
5
5
  import { StaffTimeEntry } from '../../../data/entities'
6
6
  import { staffTimeEntryCreateSchema, staffTimeEntryUpdateSchema } from '../../../data/validators'
7
+ import { buildTimeEntryListFilters, isParseableDateFilter } from '../../../lib/timesheets/timeEntryListFilters'
7
8
  import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../../openapi'
8
9
 
9
10
  const F = {
@@ -37,13 +38,6 @@ export const metadata = routeMetadata
37
38
 
38
39
  const rawBodySchema = z.object({}).passthrough()
39
40
 
40
- const isParseableDateFilter = (value: string): boolean => {
41
- const trimmed = value.trim()
42
- if (trimmed.length === 0) return true
43
- if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(trimmed)) return false
44
- return !Number.isNaN(new Date(trimmed).getTime())
45
- }
46
-
47
41
  const dateFilterSchema = z
48
42
  .string()
49
43
  .refine(isParseableDateFilter, { message: 'Invalid date' })
@@ -58,6 +52,7 @@ const listSchema = z
58
52
  to: dateFilterSchema,
59
53
  projectId: z.string().uuid().optional(),
60
54
  ids: z.string().optional(),
55
+ running: z.string().optional(),
61
56
  sortField: z.string().optional(),
62
57
  sortDir: z.enum(['asc', 'desc']).optional(),
63
58
  })
@@ -100,31 +95,7 @@ const crud = makeCrudRoute({
100
95
  updatedAt: F.updated_at,
101
96
  durationMinutes: F.duration_minutes,
102
97
  },
103
- buildFilters: async (query) => {
104
- const filters: Record<string, unknown> = {}
105
- if (typeof query.ids === 'string' && query.ids.trim().length > 0) {
106
- const ids = query.ids
107
- .split(',')
108
- .map((value) => value.trim())
109
- .filter((value) => value.length > 0)
110
- if (ids.length > 0) {
111
- filters[F.id] = { $in: ids }
112
- }
113
- }
114
- if (typeof query.staffMemberId === 'string' && query.staffMemberId.length > 0) {
115
- filters[F.staff_member_id] = query.staffMemberId
116
- }
117
- if (typeof query.from === 'string' && query.from.length > 0 && isParseableDateFilter(query.from)) {
118
- filters[F.date] = { ...((filters[F.date] as Record<string, unknown>) ?? {}), $gte: query.from }
119
- }
120
- if (typeof query.to === 'string' && query.to.length > 0 && isParseableDateFilter(query.to)) {
121
- filters[F.date] = { ...((filters[F.date] as Record<string, unknown>) ?? {}), $lte: query.to }
122
- }
123
- if (typeof query.projectId === 'string' && query.projectId.length > 0) {
124
- filters[F.time_project_id] = query.projectId
125
- }
126
- return filters
127
- },
98
+ buildFilters: async (query) => buildTimeEntryListFilters(query),
128
99
  },
129
100
  actions: {
130
101
  create: {
@@ -0,0 +1,64 @@
1
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
2
+
3
+ export type TimeEntryListFilterQuery = {
4
+ ids?: string
5
+ staffMemberId?: string
6
+ from?: string
7
+ to?: string
8
+ projectId?: string
9
+ running?: string
10
+ }
11
+
12
+ const ID_COLUMN = 'id'
13
+ const STAFF_MEMBER_COLUMN = 'staff_member_id'
14
+ const DATE_COLUMN = 'date'
15
+ const STARTED_AT_COLUMN = 'started_at'
16
+ const ENDED_AT_COLUMN = 'ended_at'
17
+ const PROJECT_COLUMN = 'time_project_id'
18
+
19
+ export function isParseableDateFilter(value: string): boolean {
20
+ const trimmed = value.trim()
21
+ if (trimmed.length === 0) return true
22
+ if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(trimmed)) return false
23
+ return !Number.isNaN(new Date(trimmed).getTime())
24
+ }
25
+
26
+ /**
27
+ * Builds the query-engine filter map for the time-entries list route.
28
+ *
29
+ * The `running` flag matches the currently-open timer regardless of its date
30
+ * (`started_at IS NOT NULL AND ended_at IS NULL`). A timer started before
31
+ * midnight is still running the next day, so a `from=today&to=today` lookup
32
+ * would miss it and leave the entry invisible/uncontrollable (issue #3717).
33
+ * This mirrors the single-active-timer check in the `start_timer` command so
34
+ * the UI can always surface and stop an orphaned overnight timer.
35
+ */
36
+ export function buildTimeEntryListFilters(query: TimeEntryListFilterQuery): Record<string, unknown> {
37
+ const filters: Record<string, unknown> = {}
38
+ if (typeof query.ids === 'string' && query.ids.trim().length > 0) {
39
+ const ids = query.ids
40
+ .split(',')
41
+ .map((value) => value.trim())
42
+ .filter((value) => value.length > 0)
43
+ if (ids.length > 0) {
44
+ filters[ID_COLUMN] = { $in: ids }
45
+ }
46
+ }
47
+ if (typeof query.staffMemberId === 'string' && query.staffMemberId.length > 0) {
48
+ filters[STAFF_MEMBER_COLUMN] = query.staffMemberId
49
+ }
50
+ if (typeof query.from === 'string' && query.from.length > 0 && isParseableDateFilter(query.from)) {
51
+ filters[DATE_COLUMN] = { ...((filters[DATE_COLUMN] as Record<string, unknown>) ?? {}), $gte: query.from }
52
+ }
53
+ if (typeof query.to === 'string' && query.to.length > 0 && isParseableDateFilter(query.to)) {
54
+ filters[DATE_COLUMN] = { ...((filters[DATE_COLUMN] as Record<string, unknown>) ?? {}), $lte: query.to }
55
+ }
56
+ if (typeof query.projectId === 'string' && query.projectId.length > 0) {
57
+ filters[PROJECT_COLUMN] = query.projectId
58
+ }
59
+ if (parseBooleanToken(query.running ?? null) === true) {
60
+ filters[STARTED_AT_COLUMN] = { $ne: null }
61
+ filters[ENDED_AT_COLUMN] = null
62
+ }
63
+ return filters
64
+ }
@@ -102,10 +102,9 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
102
102
  useEffect(() => {
103
103
  if (!staffMemberId) return
104
104
 
105
- const today = getToday()
106
105
  const checkActiveTimer = async () => {
107
106
  const response = await apiCall(
108
- `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&from=${today}&to=${today}&pageSize=50`,
107
+ `/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&running=true&pageSize=50`,
109
108
  )
110
109
  if (!response.ok) return
111
110
 
@@ -91,9 +91,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
91
91
  const memberId = selfRes.member?.id ?? null
92
92
  setStaffMemberId(memberId)
93
93
 
94
- const today = new Date().toISOString().slice(0, 10)
95
-
96
- // Project details depend on the assignment ids and active entries depend on the staff
94
+ // Project details depend on the assignment ids and the active timer depends on the staff
97
95
  // member id, but the two requests are independent of each other — fetch them together.
98
96
  const [projectsRes, entriesRes] = await Promise.all([
99
97
  projectIds.length > 0
@@ -105,7 +103,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
105
103
  : Promise.resolve<{ items?: Array<Record<string, unknown>> }>({ items: [] }),
106
104
  memberId
107
105
  ? readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
108
- `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=100`,
106
+ `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=100`,
109
107
  undefined,
110
108
  { errorMessage: '', fallback: { items: [] } },
111
109
  )
@@ -39,11 +39,6 @@ function formatElapsed(seconds: number): string {
39
39
  return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
40
40
  }
41
41
 
42
- function getToday(): string {
43
- const now = new Date()
44
- return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
45
- }
46
-
47
42
  function TimerSidebarIndicator() {
48
43
  const t = useT()
49
44
  // Initialise from sessionStorage so the indicator doesn't flash away on navigation
@@ -63,9 +58,8 @@ function TimerSidebarIndicator() {
63
58
  const memberId = selfRes.result?.member?.id
64
59
  if (!memberId) { updateTimer(null); return }
65
60
 
66
- const today = getToday()
67
61
  const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(
68
- `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&from=${today}&to=${today}&pageSize=50`,
62
+ `/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=50`,
69
63
  )
70
64
  const items = (res.result?.items ?? []) as Array<Record<string, unknown>>
71
65
  const active = items.find((e) => e.started_at && !e.ended_at)
@@ -1048,15 +1048,15 @@ export const syncExcelCustomersAdapter: DataSyncAdapter = {
1048
1048
  const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0
1049
1049
  const commandContext = buildCommandContext(container, input.scope)
1050
1050
  const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope)
1051
+ const emailDedupeIndex = await buildEmailDedupeIndex({
1052
+ em,
1053
+ rows: document.rows.slice(startOffset),
1054
+ mapping: input.mapping,
1055
+ scope: input.scope,
1056
+ })
1051
1057
 
1052
1058
  for (let offset = startOffset, batchIndex = 0; offset < document.rows.length; offset += input.batchSize, batchIndex += 1) {
1053
1059
  const batchRows = document.rows.slice(offset, offset + input.batchSize)
1054
- const emailDedupeIndex = await buildEmailDedupeIndex({
1055
- em,
1056
- rows: batchRows,
1057
- mapping: input.mapping,
1058
- scope: input.scope,
1059
- })
1060
1060
  const items: ImportItem[] = []
1061
1061
 
1062
1062
  for (let index = 0; index < batchRows.length; index += 1) {