@postxl/generators 1.13.0 → 1.14.0

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 (103) hide show
  1. package/dist/backend-authentication/generators/authentication-service.generator.js +1 -1
  2. package/dist/backend-core/backend.generator.js +0 -4
  3. package/dist/backend-core/backend.generator.js.map +1 -1
  4. package/dist/backend-core/modules/backend-module-xlport.generator.js +1 -1
  5. package/dist/backend-excel-io/excel-io.generator.d.ts +19 -0
  6. package/dist/backend-excel-io/excel-io.generator.js +106 -0
  7. package/dist/backend-excel-io/excel-io.generator.js.map +1 -0
  8. package/dist/backend-excel-io/generators/excel-io-service.generator.d.ts +9 -0
  9. package/dist/backend-excel-io/generators/excel-io-service.generator.js +680 -0
  10. package/dist/backend-excel-io/generators/excel-io-service.generator.js.map +1 -0
  11. package/dist/backend-excel-io/index.d.ts +2 -0
  12. package/dist/backend-excel-io/index.js +22 -0
  13. package/dist/backend-excel-io/index.js.map +1 -0
  14. package/dist/backend-excel-io/template/README.md +24 -0
  15. package/dist/backend-excel-io/template/excel-io.controller.ts +195 -0
  16. package/dist/backend-excel-io/template/excel-io.module.ts +17 -0
  17. package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js +148 -13
  18. package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js.map +1 -1
  19. package/dist/backend-import/generators/filter-error-rows.generator.d.ts +2 -0
  20. package/dist/backend-import/generators/filter-error-rows.generator.js +28 -0
  21. package/dist/backend-import/generators/filter-error-rows.generator.js.map +1 -0
  22. package/dist/backend-import/generators/import-service.generator.js +126 -2
  23. package/dist/backend-import/generators/import-service.generator.js.map +1 -1
  24. package/dist/backend-import/import.generator.js +2 -0
  25. package/dist/backend-import/import.generator.js.map +1 -1
  26. package/dist/backend-repositories/generators/model-repository.generator.js +17 -2
  27. package/dist/backend-repositories/generators/model-repository.generator.js.map +1 -1
  28. package/dist/backend-router-trpc/generators/app-routes.generator.js +5 -0
  29. package/dist/backend-router-trpc/generators/app-routes.generator.js.map +1 -1
  30. package/dist/backend-router-trpc/generators/excel-io-route.generator.d.ts +4 -0
  31. package/dist/backend-router-trpc/generators/excel-io-route.generator.js +35 -0
  32. package/dist/backend-router-trpc/generators/excel-io-route.generator.js.map +1 -0
  33. package/dist/backend-router-trpc/generators/trpc-plugin.generator.js +6 -0
  34. package/dist/backend-router-trpc/generators/trpc-plugin.generator.js.map +1 -1
  35. package/dist/backend-router-trpc/generators/trpc-router-module.generator.js +8 -1
  36. package/dist/backend-router-trpc/generators/trpc-router-module.generator.js.map +1 -1
  37. package/dist/backend-router-trpc/generators/trpc-shared.generator.js +9 -0
  38. package/dist/backend-router-trpc/generators/trpc-shared.generator.js.map +1 -1
  39. package/dist/backend-router-trpc/router-trpc.generator.d.ts +2 -1
  40. package/dist/backend-router-trpc/router-trpc.generator.js +4 -0
  41. package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
  42. package/dist/base/base.generator.js +0 -4
  43. package/dist/base/base.generator.js.map +1 -1
  44. package/dist/decoders/datamodel-decoder.generator.js +91 -1
  45. package/dist/decoders/datamodel-decoder.generator.js.map +1 -1
  46. package/dist/decoders/decoders.generator.d.ts +9 -0
  47. package/dist/decoders/decoders.generator.js +15 -0
  48. package/dist/decoders/decoders.generator.js.map +1 -1
  49. package/dist/devops/devops.generator.d.ts +5 -1
  50. package/dist/devops/devops.generator.js +5 -4
  51. package/dist/devops/devops.generator.js.map +1 -1
  52. package/dist/devops/generators/docker-compose-yml.generator.d.ts +1 -1
  53. package/dist/devops/generators/docker-compose-yml.generator.js +16 -1
  54. package/dist/devops/generators/docker-compose-yml.generator.js.map +1 -1
  55. package/dist/e2e/template/e2e/specs/example.spec.ts-snapshots/Navigate-to-homepage-and-take-snapshot-1-chromium-linux.png +0 -0
  56. package/dist/frontend-actions/generators/filter-utils.generator.js +1 -1
  57. package/dist/frontend-admin/admin.generator.d.ts +2 -0
  58. package/dist/frontend-admin/admin.generator.js +28 -0
  59. package/dist/frontend-admin/admin.generator.js.map +1 -1
  60. package/dist/frontend-admin/generators/admin-global-actions.generator.js +78 -0
  61. package/dist/frontend-admin/generators/admin-global-actions.generator.js.map +1 -1
  62. package/dist/frontend-admin/generators/admin-overview-page.generator.js +21 -1
  63. package/dist/frontend-admin/generators/admin-overview-page.generator.js.map +1 -1
  64. package/dist/frontend-admin/generators/admin-sidebar.generator.js +20 -0
  65. package/dist/frontend-admin/generators/admin-sidebar.generator.js.map +1 -1
  66. package/dist/frontend-admin/generators/excel-io-page.generator.d.ts +4 -0
  67. package/dist/frontend-admin/generators/excel-io-page.generator.js +258 -0
  68. package/dist/frontend-admin/generators/excel-io-page.generator.js.map +1 -0
  69. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.d.ts +1 -0
  70. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js +104 -0
  71. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js.map +1 -0
  72. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.d.ts +1 -0
  73. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js +1031 -0
  74. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js.map +1 -0
  75. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.d.ts +1 -0
  76. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js +77 -0
  77. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js.map +1 -0
  78. package/dist/frontend-admin/generators/import-review-page.generator.d.ts +7 -0
  79. package/dist/frontend-admin/generators/import-review-page.generator.js +180 -0
  80. package/dist/frontend-admin/generators/import-review-page.generator.js.map +1 -0
  81. package/dist/frontend-admin/generators/model-admin-page.generator.js +28 -51
  82. package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
  83. package/dist/frontend-admin/utils.js +25 -33
  84. package/dist/frontend-admin/utils.js.map +1 -1
  85. package/dist/frontend-core/frontend.generator.js +1 -2
  86. package/dist/frontend-core/frontend.generator.js.map +1 -1
  87. package/dist/frontend-core/template/src/components/admin/excel-io-actions.tsx +64 -0
  88. package/dist/frontend-core/template/src/components/admin/table-view-panel.tsx +41 -3
  89. package/dist/frontend-core/template/src/hooks/use-excel-io.ts +137 -0
  90. package/dist/frontend-core/template/src/hooks/use-import-review.ts +143 -0
  91. package/dist/frontend-core/template/src/lib/excel-download.ts +28 -0
  92. package/dist/frontend-core/types/hook.d.ts +1 -1
  93. package/dist/frontend-tables/generators/model-table.generator.js +21 -13
  94. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  95. package/dist/generators.js +2 -0
  96. package/dist/generators.js.map +1 -1
  97. package/dist/index.d.ts +1 -0
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/seed-data/seed-data.generator.d.ts +3 -0
  101. package/dist/seed-data/seed-data.generator.js +45 -1
  102. package/dist/seed-data/seed-data.generator.js.map +1 -1
  103. package/package.json +3 -3
@@ -0,0 +1,680 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateExcelIoService = generateExcelIoService;
4
+ function generateExcelIoService(context) {
5
+ const models = [...context.models.values()].sort((a, b) => a.name.localeCompare(b.name));
6
+ const keyFieldMapEntries = models
7
+ .flatMap((model) => [
8
+ ` ${model._conjugated.camelCase}: '${model.keyField.name}',`,
9
+ ` ${model._conjugated.camelCasePlural}: '${model.keyField.name}',`,
10
+ ])
11
+ .join('\n');
12
+ const modelNames = models.map((model) => `'${model._conjugated.camelCase}'`).join(',\n ');
13
+ const allModelsDataModel = models
14
+ .map((model) => ` ${model._conjugated.camelCasePlural}: await this.viewService.${model.view.service.variableName}.getList(user),`)
15
+ .join('\n');
16
+ const modelCases = models
17
+ .map((model) => `
18
+ case '${model._conjugated.camelCase}': {
19
+ const filters = ${model.types.filter.decoder.name}.parse(rawFilters ?? {})
20
+ const sort = ${model.types.sort.decoder.name}.optional().parse(rawSort)
21
+ const items = await this.viewService.${model.view.service.variableName}.getFiltered({ filters, sort, user })
22
+ return this.renderExcel({
23
+ dataModel: { ${model._conjugated.camelCasePlural}: items },
24
+ filename: \`${'${timeStamp()}'} ${model._conjugated.kebabCasePlural}.xlsx\`,
25
+ templateIds: ['${model._conjugated.camelCase}.xlsx', ...ALL_MODELS_TEMPLATE_IDS],
26
+ })
27
+ }`)
28
+ .join('');
29
+ const filterSortImports = models
30
+ .map((model) => ` ${model.types.filter.decoder.name},\n ${model.types.sort.decoder.name},`)
31
+ .join('\n');
32
+ return /*ts*/ `
33
+ import { DispatcherService } from '@actions/dispatcher.service'
34
+ import { flattenExcelDataModel, type DataModel, type RecordParseError } from '@decoders/data-model.decoder'
35
+ import type { Action_Import, ImportExecutionSummary } from '@import/import.service'
36
+ import { ImportService } from '@import/import.service'
37
+ import type { Delta } from '@import/detect-delta/models.detect-delta'
38
+ import { extractErrors } from '@import/detect-delta/models.detect-delta'
39
+ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'
40
+ import type { User } from '@types'
41
+ import {
42
+ ${filterSortImports}
43
+ } from '@types'
44
+ import { ViewService } from '@view/view.service'
45
+ import { XlPortService } from '@xlport/xlport.service'
46
+
47
+ import { randomUUID } from 'node:crypto'
48
+ import { Readable } from 'node:stream'
49
+ import z from 'zod'
50
+
51
+ import { ExhaustiveSwitchCheck, timeStamp } from '@postxl/utils'
52
+
53
+ const modelNameDecoder = z.enum([
54
+ ${modelNames}
55
+ ])
56
+
57
+ export type ExportModelName = z.infer<typeof modelNameDecoder>
58
+ const ALL_MODELS_TEMPLATE_IDS = ['data-model.xlsx'] as const
59
+
60
+ const PREVIEW_TTL_MS = 30 * 60 * 1000 // 30 minutes
61
+ const PREVIEW_MAX_ENTRIES = 50
62
+
63
+ const KEY_FIELD_MAP: Record<string, string> = {
64
+ ${keyFieldMapEntries}
65
+ }
66
+
67
+ type StoredPreview = {
68
+ delta: Delta
69
+ parseErrors: RecordParseError[]
70
+ includeDetailedUnchangedRecords: boolean
71
+ filename: string
72
+ createdAt: number
73
+ }
74
+
75
+ export type ImportPreviewModelSummary = {
76
+ create: number
77
+ update: number
78
+ delete: number
79
+ unchanged: number
80
+ errors: number
81
+ items: ImportPreviewItem[]
82
+ }
83
+
84
+ export type ImportPreviewItem = {
85
+ type: 'create' | 'update' | 'delete' | 'unchanged' | 'errors'
86
+ deltaIndex?: number
87
+ id: string
88
+ keyFieldValue: string
89
+ input: Record<string, unknown>
90
+ delta?: Record<string, { old: unknown; new: unknown }>
91
+ errors?: unknown[]
92
+ }
93
+
94
+ export type ImportPreviewResponse = {
95
+ previewSessionId: string
96
+ filename: string
97
+ summary: {
98
+ create: number
99
+ update: number
100
+ delete: number
101
+ unchanged: number
102
+ errors: number
103
+ }
104
+ models: Record<string, ImportPreviewModelSummary>
105
+ processingErrors?: string[]
106
+ }
107
+
108
+ export type ImportPreviewSessionResponse = {
109
+ previewSessionId: string
110
+ }
111
+
112
+ export type ImportPreviewOptions = {
113
+ includeDetailedUnchangedRecords?: boolean
114
+ }
115
+
116
+ export const zImportPreviewSessionRequestDecoder = z.object({
117
+ previewSessionId: z.string().min(1, 'Preview session id is required'),
118
+ })
119
+
120
+ export type ImportPreviewSessionRequest = z.infer<typeof zImportPreviewSessionRequestDecoder>
121
+
122
+ export const zImportExecuteRequestDecoder = z.object({
123
+ previewSessionId: z.string().min(1, 'Preview session id is required'),
124
+ ignoreErrors: z.boolean().default(false),
125
+ confirmDeletes: z.boolean().default(false),
126
+ selectedDeltaIndicesByModel: z.record(z.string(), z.array(z.number().int().min(0))).default({}),
127
+ })
128
+
129
+ export type ImportExecuteRequest = z.infer<typeof zImportExecuteRequestDecoder>
130
+
131
+ @Injectable()
132
+ export class ExcelIoService {
133
+ private readonly logger = new Logger(ExcelIoService.name)
134
+ private readonly previewStore = new Map<string, StoredPreview>()
135
+
136
+ constructor(
137
+ private readonly viewService: ViewService,
138
+ private readonly xlPortService: XlPortService,
139
+ @Inject(forwardRef(() => DispatcherService)) private readonly dispatcherService: DispatcherService,
140
+ @Inject(forwardRef(() => ImportService)) private readonly importService: ImportService,
141
+ ) {}
142
+
143
+ public parseModelName(model: string): ExportModelName {
144
+ return modelNameDecoder.parse(model)
145
+ }
146
+
147
+ public async exportAllModelsToExcel(user: User) {
148
+ this.logger.log('Exporting all models to Excel')
149
+
150
+ const dataModel: DataModel = {
151
+ ${allModelsDataModel}
152
+ }
153
+
154
+ return this.renderExcel({ dataModel, filename: \`${'${timeStamp()}'} all-models.xlsx\` })
155
+ }
156
+
157
+ public async exportModelToExcel({
158
+ model,
159
+ rawFilters,
160
+ rawSort,
161
+ user,
162
+ }: {
163
+ model: ExportModelName
164
+ rawFilters?: unknown
165
+ rawSort?: unknown
166
+ user: User
167
+ }) {
168
+ this.logger.log(\`Exporting model ${'${model}'} to Excel\`)
169
+
170
+ switch (model) {${modelCases}
171
+ default:
172
+ throw new ExhaustiveSwitchCheck(model)
173
+ }
174
+ }
175
+
176
+ public async importExcel({ data, filename, user }: { data: Buffer; filename: string; user: User }) {
177
+ this.logger.log('Importing uploaded Excel file through import action')
178
+
179
+ const action: Action_Import = {
180
+ scope: 'import',
181
+ type: 'excel',
182
+ payload: {
183
+ filename,
184
+ data,
185
+ },
186
+ }
187
+
188
+ return this.dispatcherService.dispatch({ action, user })
189
+ }
190
+
191
+ public async previewImportExcel({
192
+ data,
193
+ filename,
194
+ includeDetailedUnchangedRecords = false,
195
+ }: {
196
+ data: Buffer
197
+ filename: string
198
+ includeDetailedUnchangedRecords?: boolean
199
+ }): Promise<ImportPreviewSessionResponse> {
200
+ this.logger.log('Previewing Excel import (read-only)')
201
+
202
+ try {
203
+ const { delta, parseErrors } = await this.importService.previewExcelImport(data)
204
+ const previewSessionId = randomUUID()
205
+
206
+ this.cleanupExpiredPreviews()
207
+ this.previewStore.set(previewSessionId, {
208
+ delta,
209
+ parseErrors,
210
+ includeDetailedUnchangedRecords,
211
+ filename,
212
+ createdAt: Date.now(),
213
+ })
214
+
215
+ return { previewSessionId }
216
+ } catch (error) {
217
+ this.logger.error('Error during preview delta detection or schema parsing', error)
218
+ throw new Error(error instanceof Error ? error.message : String(error))
219
+ }
220
+ }
221
+
222
+ public getImportPreviewBySessionId({ previewSessionId }: ImportPreviewSessionRequest): ImportPreviewResponse {
223
+ this.logger.log(\`Retrieving import preview session: \${previewSessionId}\`)
224
+ const stored = this.getStoredPreview(previewSessionId)
225
+ return buildPreviewResponse(
226
+ previewSessionId,
227
+ stored.filename,
228
+ stored.delta,
229
+ stored.parseErrors,
230
+ stored.includeDetailedUnchangedRecords,
231
+ )
232
+ }
233
+
234
+ public async executeImportFromPreview({
235
+ previewSessionId,
236
+ ignoreErrors,
237
+ confirmDeletes,
238
+ selectedDeltaIndicesByModel,
239
+ user,
240
+ }: ImportExecuteRequest & { user: User }): Promise<ImportExecutionSummary> {
241
+ this.logger.log(\`Executing import from preview session: \${previewSessionId}\`)
242
+ const stored = this.getStoredPreview(previewSessionId)
243
+ const selectedDelta = filterDeltaBySelectedIndices(stored.delta, selectedDeltaIndicesByModel)
244
+
245
+ if (!deltaHasExecutableChanges(selectedDelta)) {
246
+ throw new Error('No executable rows selected. Select at least one create, update, or delete row.')
247
+ }
248
+
249
+ const hasDeletes = deltaHasDeletes(selectedDelta)
250
+ if (hasDeletes && !confirmDeletes) {
251
+ throw new Error('Import contains delete operations. You must confirm deletes before executing.')
252
+ }
253
+
254
+ const hasErrors = Object.keys(extractErrors(selectedDelta)).length > 0
255
+ if (hasErrors && !ignoreErrors) {
256
+ throw new Error('Import contains errors. Enable "Ignore errors" to skip errored rows, or fix the data.')
257
+ }
258
+
259
+ const action: Action_Import = {
260
+ scope: 'import',
261
+ type: 'reviewedExcel',
262
+ payload: {
263
+ filename: stored.filename,
264
+ delta: selectedDelta,
265
+ ignoreErrors,
266
+ },
267
+ }
268
+
269
+ const result = await this.dispatcherService.dispatch({ action, user })
270
+
271
+ this.previewStore.delete(previewSessionId)
272
+
273
+ return result as ImportExecutionSummary
274
+ }
275
+
276
+ private getStoredPreview(previewSessionId: string): StoredPreview {
277
+ this.cleanupExpiredPreviews()
278
+
279
+ const stored = this.previewStore.get(previewSessionId)
280
+ if (!stored) {
281
+ throw new Error('Invalid or expired preview session id')
282
+ }
283
+
284
+ if (Date.now() - stored.createdAt > PREVIEW_TTL_MS) {
285
+ this.previewStore.delete(previewSessionId)
286
+ throw new Error('Preview session has expired. Please upload and preview again.')
287
+ }
288
+
289
+ return stored
290
+ }
291
+
292
+ private cleanupExpiredPreviews() {
293
+ const now = Date.now()
294
+ for (const [key, value] of this.previewStore.entries()) {
295
+ if (now - value.createdAt > PREVIEW_TTL_MS) {
296
+ this.previewStore.delete(key)
297
+ }
298
+ }
299
+
300
+ if (this.previewStore.size >= PREVIEW_MAX_ENTRIES) {
301
+ const oldest = [...this.previewStore.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt)
302
+ const toRemove = oldest.slice(0, this.previewStore.size - PREVIEW_MAX_ENTRIES + 1)
303
+ for (const [key] of toRemove) {
304
+ this.previewStore.delete(key)
305
+ }
306
+ }
307
+ }
308
+
309
+ private async renderExcel({
310
+ dataModel,
311
+ filename,
312
+ templateIds = ALL_MODELS_TEMPLATE_IDS,
313
+ }: {
314
+ dataModel: DataModel
315
+ filename: string
316
+ templateIds?: readonly string[]
317
+ }) {
318
+ const data = flattenExcelDataModel(dataModel)
319
+ const file = await this.exportWithTemplateIds(data, templateIds)
320
+
321
+ return { filename, file }
322
+ }
323
+
324
+ private async exportWithTemplateIds(
325
+ data: ReturnType<typeof flattenExcelDataModel>,
326
+ templateIds: readonly string[],
327
+ ): Promise<Buffer> {
328
+ const errors: string[] = []
329
+
330
+ for (const templateId of templateIds) {
331
+ try {
332
+ const raw = await this.xlPortService.exportToFile({ templateId, data })
333
+ const buffer = await toBuffer(raw)
334
+ const validated = parseXlPortExportBuffer(buffer)
335
+ this.logger.log(\`Excel export succeeded via templateId(\${templateId})\`)
336
+ return validated
337
+ } catch (error) {
338
+ const message = error instanceof Error ? error.message : String(error)
339
+ errors.push(\`templateId(\${templateId}): \${message}\`)
340
+ this.logger.warn(\`Excel export attempt failed (templateId=\${templateId}): \${message}\`)
341
+ }
342
+ }
343
+
344
+ throw new Error(
345
+ \`Unable to export Excel via xlport templateId lookup. Tried \${templateIds.join(', ')}. Details: \${errors.join(' | ')}\`,
346
+ )
347
+ }
348
+ }
349
+
350
+ async function toBuffer(value: unknown): Promise<Buffer> {
351
+ if (Buffer.isBuffer(value)) {
352
+ return value
353
+ }
354
+
355
+ if (value instanceof Uint8Array) {
356
+ return Buffer.from(value)
357
+ }
358
+
359
+ if (typeof value === 'string') {
360
+ return Buffer.from(value)
361
+ }
362
+
363
+ if (value instanceof Readable) {
364
+ const asyncValue = asAsyncIterable(value)
365
+ const chunks: Buffer[] = []
366
+ for await (const chunk of asyncValue) {
367
+ if (Buffer.isBuffer(chunk)) {
368
+ chunks.push(chunk)
369
+ } else if (chunk instanceof Uint8Array) {
370
+ chunks.push(Buffer.from(chunk))
371
+ } else {
372
+ chunks.push(Buffer.from(String(chunk)))
373
+ }
374
+ }
375
+
376
+ const buffer = Buffer.concat(chunks)
377
+ if (buffer.length === 0) {
378
+ throw new Error('xlport returned an empty Excel stream')
379
+ }
380
+ return buffer
381
+ }
382
+
383
+ throw new Error('Unexpected xlport export response type')
384
+ }
385
+
386
+ function asAsyncIterable(value: unknown): AsyncIterable<unknown> {
387
+ if (!value || typeof value !== 'object') {
388
+ throw new TypeError('xlport stream value is not iterable')
389
+ }
390
+
391
+ const asyncIterator = (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator]
392
+ if (typeof asyncIterator !== 'function') {
393
+ throw new TypeError('xlport stream value is not async iterable')
394
+ }
395
+
396
+ return value as AsyncIterable<unknown>
397
+ }
398
+
399
+ type PreviewTotals = {
400
+ create: number
401
+ update: number
402
+ delete: number
403
+ unchanged: number
404
+ errors: number
405
+ }
406
+
407
+ function createPreviewTotals(): PreviewTotals {
408
+ return { create: 0, update: 0, delete: 0, unchanged: 0, errors: 0 }
409
+ }
410
+
411
+ function createModelSummary(): ImportPreviewModelSummary {
412
+ return {
413
+ create: 0,
414
+ update: 0,
415
+ delete: 0,
416
+ unchanged: 0,
417
+ errors: 0,
418
+ items: [],
419
+ }
420
+ }
421
+
422
+ function ensureModelSummary(
423
+ models: Record<string, ImportPreviewModelSummary>,
424
+ modelName: string,
425
+ ): ImportPreviewModelSummary {
426
+ const existing = models[modelName]
427
+ if (existing) {
428
+ return existing
429
+ }
430
+
431
+ const created = createModelSummary()
432
+ models[modelName] = created
433
+ return created
434
+ }
435
+
436
+ function toPreviewText(value: unknown): string {
437
+ if (value === null || value === undefined) {
438
+ return ''
439
+ }
440
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
441
+ return String(value) // NOSONAR
442
+ }
443
+ if (value instanceof Date) {
444
+ return value.toISOString()
445
+ }
446
+ if (typeof value === 'object') {
447
+ try {
448
+ return JSON.stringify(value)
449
+ } catch {
450
+ return '[object]'
451
+ }
452
+ }
453
+ return String(value) // NOSONAR
454
+ }
455
+
456
+ function getKeyFieldValue(modelName: string, inputRecord: Record<string, unknown>): string {
457
+ const keyFieldName = KEY_FIELD_MAP[modelName]
458
+ if (!keyFieldName) {
459
+ return ''
460
+ }
461
+ return toPreviewText(inputRecord[keyFieldName] ?? inputRecord[excelNameFor(keyFieldName)])
462
+ }
463
+
464
+ function incrementPreviewTotals(
465
+ modelSummary: ImportPreviewModelSummary,
466
+ totals: PreviewTotals,
467
+ type: ImportPreviewItem['type'],
468
+ ): void {
469
+ switch (type) {
470
+ case 'create':
471
+ modelSummary.create++
472
+ totals.create++
473
+ return
474
+ case 'update':
475
+ modelSummary.update++
476
+ totals.update++
477
+ return
478
+ case 'delete':
479
+ modelSummary.delete++
480
+ totals.delete++
481
+ return
482
+ case 'unchanged':
483
+ modelSummary.unchanged++
484
+ totals.unchanged++
485
+ return
486
+ case 'errors':
487
+ modelSummary.errors++
488
+ totals.errors++
489
+ return
490
+ }
491
+ }
492
+
493
+ type DeltaPreviewItemLike = {
494
+ type: ImportPreviewItem['type']
495
+ input: unknown
496
+ delta?: unknown
497
+ errors?: unknown[]
498
+ }
499
+
500
+ function appendDeltaPreviewItem(
501
+ modelName: string,
502
+ item: DeltaPreviewItemLike,
503
+ deltaIndex: number,
504
+ includeDetailedUnchangedRecords: boolean,
505
+ modelSummary: ImportPreviewModelSummary,
506
+ totals: PreviewTotals,
507
+ ): void {
508
+ incrementPreviewTotals(modelSummary, totals, item.type)
509
+
510
+ if (item.type === 'unchanged' && !includeDetailedUnchangedRecords) {
511
+ return
512
+ }
513
+
514
+ const inputRecord = item.input as Record<string, unknown>
515
+ const previewItem: ImportPreviewItem = {
516
+ type: item.type,
517
+ deltaIndex,
518
+ id: toPreviewText(inputRecord.id),
519
+ keyFieldValue: getKeyFieldValue(modelName, inputRecord),
520
+ input: inputRecord,
521
+ }
522
+
523
+ if (item.type === 'update') {
524
+ previewItem.delta = item.delta as Record<string, { old: unknown; new: unknown }>
525
+ }
526
+ if (item.type === 'errors' && 'errors' in item) {
527
+ previewItem.errors = item.errors as unknown[]
528
+ }
529
+
530
+ modelSummary.items.push(previewItem)
531
+ }
532
+
533
+ function appendParseErrorItem(
534
+ parseError: RecordParseError,
535
+ models: Record<string, ImportPreviewModelSummary>,
536
+ totals: PreviewTotals,
537
+ ): void {
538
+ const modelSummary = ensureModelSummary(models, parseError.model)
539
+ incrementPreviewTotals(modelSummary, totals, 'errors')
540
+ modelSummary.items.push({
541
+ type: 'errors',
542
+ id: '',
543
+ keyFieldValue: getKeyFieldValue(parseError.model, parseError.raw),
544
+ input: parseError.raw,
545
+ errors: parseError.issues.map((issue) => ({ error: 'parse-error', message: issue })),
546
+ })
547
+ }
548
+
549
+ function buildPreviewResponse(
550
+ previewSessionId: string,
551
+ filename: string,
552
+ delta: Delta,
553
+ parseErrors: RecordParseError[] = [],
554
+ includeDetailedUnchangedRecords = false,
555
+ ): ImportPreviewResponse {
556
+ const models: Record<string, ImportPreviewModelSummary> = {}
557
+ const totals = createPreviewTotals()
558
+
559
+ for (const [modelName, items] of Object.entries(delta)) {
560
+ if (!items?.length) {
561
+ continue
562
+ }
563
+
564
+ const modelSummary = ensureModelSummary(models, modelName)
565
+ for (const [deltaIndex, item] of items.entries()) {
566
+ appendDeltaPreviewItem(
567
+ modelName,
568
+ item as DeltaPreviewItemLike,
569
+ deltaIndex,
570
+ includeDetailedUnchangedRecords,
571
+ modelSummary,
572
+ totals,
573
+ )
574
+ }
575
+ }
576
+
577
+ // Inject per-record parse errors as 'errors' type items in the corresponding model
578
+ for (const parseError of parseErrors) {
579
+ appendParseErrorItem(parseError, models, totals)
580
+ }
581
+
582
+ return {
583
+ previewSessionId,
584
+ filename,
585
+ summary: {
586
+ create: totals.create,
587
+ update: totals.update,
588
+ delete: totals.delete,
589
+ unchanged: totals.unchanged,
590
+ errors: totals.errors,
591
+ },
592
+ models,
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Parse error raw data uses Excel column names (PascalCase/space-separated),
598
+ * but KEY_FIELD_MAP uses camelCase field names. This helper converts a camelCase
599
+ * field name to a likely Excel column name for lookup in raw data.
600
+ */
601
+ function excelNameFor(fieldName: string): string {
602
+ // Convert camelCase to Title Case with spaces (e.g., 'blogTitle' -> 'Blog Title', 'slug' -> 'Slug')
603
+ return fieldName
604
+ .replaceAll(/[A-Z]/g, ' $&')
605
+ .replace(/^./, s => s.toUpperCase())
606
+ .trim()
607
+ }
608
+
609
+ function deltaHasDeletes(delta: Delta): boolean {
610
+ for (const items of Object.values(delta)) {
611
+ if (items?.some((item: { type: string }) => item.type === 'delete')) {
612
+ return true
613
+ }
614
+ }
615
+ return false
616
+ }
617
+
618
+ function deltaHasExecutableChanges(delta: Delta): boolean {
619
+ for (const items of Object.values(delta)) {
620
+ if (!items?.length) {
621
+ continue
622
+ }
623
+
624
+ for (const item of items) {
625
+ if (item.type === 'create' || item.type === 'update' || item.type === 'delete') {
626
+ return true
627
+ }
628
+ }
629
+ }
630
+ return false
631
+ }
632
+
633
+ function filterDeltaBySelectedIndices(delta: Delta, selectedDeltaIndicesByModel: Record<string, number[]>): Delta {
634
+ const hasSelectionMap = Object.keys(selectedDeltaIndicesByModel).length > 0
635
+ if (!hasSelectionMap) {
636
+ return delta
637
+ }
638
+
639
+ const filteredEntries = Object.entries(delta).map(([modelName, items]) => {
640
+ if (!items?.length) {
641
+ return [modelName, items]
642
+ }
643
+
644
+ const selectedIndices = selectedDeltaIndicesByModel[modelName] ?? []
645
+ const selectedSet = new Set(selectedIndices)
646
+ const filteredItems = items.filter((item, index) => {
647
+ if (item.type !== 'create' && item.type !== 'update' && item.type !== 'delete') {
648
+ return false
649
+ }
650
+ return selectedSet.has(index)
651
+ })
652
+ return [modelName, filteredItems]
653
+ })
654
+
655
+ return Object.fromEntries(filteredEntries) as Delta
656
+ }
657
+
658
+ function parseXlPortExportBuffer(buffer: Buffer): Buffer {
659
+ if (buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b) {
660
+ return buffer
661
+ }
662
+
663
+ const text = buffer.toString('utf8').trim()
664
+ if (text.startsWith('{')) {
665
+ try {
666
+ const parsed = JSON.parse(text) as { message?: string; status?: string }
667
+ throw new Error(\`xlport returned JSON error (\${parsed.status ?? 'unknown'}): \${parsed.message ?? text}\`)
668
+ } catch (error) {
669
+ if (error instanceof SyntaxError) {
670
+ throw new Error(\`xlport returned non-xlsx payload: \${text.slice(0, 300)}\`)
671
+ }
672
+ throw error
673
+ }
674
+ }
675
+
676
+ throw new Error(\`xlport returned non-xlsx payload (\${buffer.length} bytes)\`)
677
+ }
678
+ `;
679
+ }
680
+ //# sourceMappingURL=excel-io-service.generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"excel-io-service.generator.js","sourceRoot":"","sources":["../../../src/backend-excel-io/generators/excel-io-service.generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './excel-io.generator';
2
+ export { generator as backendExcelIoGenerator, generatorId as backendExcelIoGeneratorId } from './excel-io.generator';
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.backendExcelIoGeneratorId = exports.backendExcelIoGenerator = void 0;
18
+ __exportStar(require("./excel-io.generator"), exports);
19
+ var excel_io_generator_1 = require("./excel-io.generator");
20
+ Object.defineProperty(exports, "backendExcelIoGenerator", { enumerable: true, get: function () { return excel_io_generator_1.generator; } });
21
+ Object.defineProperty(exports, "backendExcelIoGeneratorId", { enumerable: true, get: function () { return excel_io_generator_1.generatorId; } });
22
+ //# sourceMappingURL=index.js.map