@solidstarters/solid-core 1.2.89 → 1.2.90
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/dist/controllers/import-transaction-error-log.controller.d.ts +41 -0
- package/dist/controllers/import-transaction-error-log.controller.d.ts.map +1 -0
- package/dist/controllers/import-transaction-error-log.controller.js +179 -0
- package/dist/controllers/import-transaction-error-log.controller.js.map +1 -0
- package/dist/controllers/import-transaction.controller.d.ts +46 -0
- package/dist/controllers/import-transaction.controller.d.ts.map +1 -0
- package/dist/controllers/import-transaction.controller.js +236 -0
- package/dist/controllers/import-transaction.controller.js.map +1 -0
- package/dist/dtos/create-import-transaction-error-log.dto.d.ts +10 -0
- package/dist/dtos/create-import-transaction-error-log.dto.d.ts.map +1 -0
- package/dist/dtos/create-import-transaction-error-log.dto.js +65 -0
- package/dist/dtos/create-import-transaction-error-log.dto.js.map +1 -0
- package/dist/dtos/create-import-transaction.dto.d.ts +14 -0
- package/dist/dtos/create-import-transaction.dto.d.ts.map +1 -0
- package/dist/dtos/create-import-transaction.dto.js +90 -0
- package/dist/dtos/create-import-transaction.dto.js.map +1 -0
- package/dist/dtos/import-instructions.dto.d.ts +19 -0
- package/dist/dtos/import-instructions.dto.d.ts.map +1 -0
- package/dist/dtos/import-instructions.dto.js +110 -0
- package/dist/dtos/import-instructions.dto.js.map +1 -0
- package/dist/dtos/update-import-transaction-error-log.dto.d.ts +11 -0
- package/dist/dtos/update-import-transaction-error-log.dto.d.ts.map +1 -0
- package/dist/dtos/update-import-transaction-error-log.dto.js +72 -0
- package/dist/dtos/update-import-transaction-error-log.dto.js.map +1 -0
- package/dist/dtos/update-import-transaction.dto.d.ts +15 -0
- package/dist/dtos/update-import-transaction.dto.d.ts.map +1 -0
- package/dist/dtos/update-import-transaction.dto.js +91 -0
- package/dist/dtos/update-import-transaction.dto.js.map +1 -0
- package/dist/entities/import-transaction-error-log.entity.d.ts +11 -0
- package/dist/entities/import-transaction-error-log.entity.d.ts.map +1 -0
- package/dist/entities/import-transaction-error-log.entity.js +53 -0
- package/dist/entities/import-transaction-error-log.entity.js.map +1 -0
- package/dist/entities/import-transaction.entity.d.ts +11 -0
- package/dist/entities/import-transaction.entity.d.ts.map +1 -0
- package/dist/entities/import-transaction.entity.js +55 -0
- package/dist/entities/import-transaction.entity.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +316 -0
- package/dist/services/csv.service.d.ts +11 -1
- package/dist/services/csv.service.d.ts.map +1 -1
- package/dist/services/csv.service.js +72 -5
- package/dist/services/csv.service.js.map +1 -1
- package/dist/services/excel.service.d.ts +11 -1
- package/dist/services/excel.service.d.ts.map +1 -1
- package/dist/services/excel.service.js +68 -10
- package/dist/services/excel.service.js.map +1 -1
- package/dist/services/import-transaction-error-log.service.d.ts +22 -0
- package/dist/services/import-transaction-error-log.service.d.ts.map +1 -0
- package/dist/services/import-transaction-error-log.service.js +56 -0
- package/dist/services/import-transaction-error-log.service.js.map +1 -0
- package/dist/services/import-transaction.service.d.ts +64 -0
- package/dist/services/import-transaction.service.d.ts.map +1 -0
- package/dist/services/import-transaction.service.js +231 -0
- package/dist/services/import-transaction.service.js.map +1 -0
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +13 -1
- package/dist/solid-core.module.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/controllers/import-transaction-error-log.controller.ts +93 -0
- package/src/controllers/import-transaction.controller.ts +128 -0
- package/src/dtos/create-import-transaction-error-log.dto.ts +34 -0
- package/src/dtos/create-import-transaction.dto.ts +50 -0
- package/src/dtos/import-instructions.dto.ts +66 -0
- package/src/dtos/update-import-transaction-error-log.dto.ts +39 -0
- package/src/dtos/update-import-transaction.dto.ts +52 -0
- package/src/entities/import-transaction-error-log.entity.ts +22 -0
- package/src/entities/import-transaction.entity.ts +22 -0
- package/src/index.ts +2 -0
- package/src/seeders/seed-data/solid-core-metadata.json +318 -2
- package/src/services/csv.service.ts +116 -7
- package/src/services/excel.service.ts +109 -64
- package/src/services/export_issues.txt +5 -1
- package/src/services/import-transaction-error-log.service.ts +34 -0
- package/src/services/import-transaction.service.ts +281 -0
- package/src/solid-core.module.ts +13 -1
|
@@ -3,87 +3,69 @@ import * as ExcelJS from 'exceljs';
|
|
|
3
3
|
import { PassThrough, Readable } from 'stream';
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
export interface ExcelReadOptions {
|
|
7
|
+
pageSize?: number; // Number of records per page
|
|
8
|
+
hasHeaderRow?: boolean; // Whether the first row contains headers
|
|
9
|
+
providedHeaders?: string[]; // Custom headers if hasHeaderRow is false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PAGE_SIZE = 100; // Default page size if not provided
|
|
13
|
+
|
|
14
|
+
export interface ExcelReadResult {
|
|
15
|
+
headers: string[]; // Headers of the Excel file
|
|
16
|
+
data: Record<string, any>[]; // Data records in the current page
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
@Injectable()
|
|
7
20
|
export class ExcelService {
|
|
8
21
|
private logger = new Logger(ExcelService.name);
|
|
9
|
-
// Sample JSON data
|
|
10
|
-
// const jsonData = [
|
|
11
|
-
// { id: 1, name: 'John Doe', age: 25, email: 'john@example.com' },
|
|
12
|
-
// { id: 2, name: 'Jane Doe', age: 28, email: 'jane@example.com' },
|
|
13
|
-
// { id: 3, name: 'Alice Smith', age: 30, email: 'alice@example.com' }
|
|
14
|
-
// ];
|
|
15
|
-
|
|
16
|
-
// public async createExcelFromJson(data: any[], fileName: string) {
|
|
17
|
-
// const workbook = new ExcelJS.Workbook();
|
|
18
|
-
// const worksheet = workbook.addWorksheet('Data');
|
|
19
|
-
|
|
20
|
-
// // Define Columns (Header)
|
|
21
|
-
// worksheet.columns = Object.keys(data[0]).map((key) => ({
|
|
22
|
-
// header: key.toUpperCase(), // Convert header names to uppercase
|
|
23
|
-
// key: key,
|
|
24
|
-
// width: 20, // Set column width
|
|
25
|
-
// }));
|
|
26
|
-
|
|
27
|
-
// // Add Data Rows
|
|
28
|
-
// data.forEach((item) => {
|
|
29
|
-
// worksheet.addRow(item);
|
|
30
|
-
// });
|
|
31
|
-
|
|
32
|
-
// // Apply basic formatting
|
|
33
|
-
// worksheet.getRow(1).font = { bold: true }; // Make headers bold
|
|
34
|
-
|
|
35
|
-
// // Save file
|
|
36
|
-
// await workbook.xlsx.writeFile(fileName);
|
|
37
|
-
// this.logger.log(`✅ Excel file "${fileName}" created successfully!`);
|
|
38
|
-
// // console.log(`✅ Excel file "${fileName}" created successfully!`);
|
|
39
|
-
// }
|
|
40
|
-
|
|
41
|
-
// public async createExcelStreamFromJson(data: any[]): Promise<Readable> {
|
|
42
|
-
// const passThrough = new PassThrough(); // Stream to pipe data
|
|
43
|
-
// const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passThrough });
|
|
44
|
-
// const worksheet = workbook.addWorksheet('Data');
|
|
45
|
-
// worksheet.columns = Object.keys(data[0]).map((key) => ({
|
|
46
|
-
// header: key.toUpperCase(),
|
|
47
|
-
// key: key,
|
|
48
|
-
// width: 20,
|
|
49
|
-
// }));
|
|
50
|
-
|
|
51
|
-
// data.forEach((item) => {
|
|
52
|
-
// worksheet.addRow(item);
|
|
53
|
-
// });
|
|
54
|
-
|
|
55
|
-
// worksheet.getRow(1).font = { bold: true };
|
|
56
|
-
|
|
57
|
-
// await workbook.commit();
|
|
58
|
-
// return passThrough;
|
|
59
|
-
// }
|
|
60
|
-
|
|
61
22
|
|
|
62
23
|
public async createExcelStream(
|
|
63
24
|
getDataRecords: (chunkIndex: number, chunkSize: number) => Promise<any[]>,
|
|
64
|
-
chunkSize: number
|
|
25
|
+
chunkSize: number = 100,
|
|
26
|
+
headers: string[] = []
|
|
65
27
|
): Promise<Readable> {
|
|
28
|
+
// Validations
|
|
29
|
+
// If neither headers nor data records function is provided, throw an error
|
|
30
|
+
if (headers.length === 0 && typeof getDataRecords !== 'function') {
|
|
31
|
+
throw new Error('Either headers or data records function must be provided.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If data records function is provided, chunkSize must be greater than 0
|
|
35
|
+
if (getDataRecords && chunkSize <= 0) {
|
|
36
|
+
throw new Error('Chunk size must be greater than 0 when data records function is provided.');
|
|
37
|
+
}
|
|
38
|
+
|
|
66
39
|
const passThrough = new PassThrough(); // Create streaming pipe
|
|
67
40
|
try {
|
|
68
41
|
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: passThrough });
|
|
69
42
|
const worksheet = workbook.addWorksheet('Data');
|
|
70
43
|
|
|
71
|
-
|
|
72
|
-
|
|
44
|
+
// If headers are provided, use them;
|
|
45
|
+
if (headers.length > 0) {
|
|
46
|
+
worksheet.columns = headers.map((header) => ({
|
|
47
|
+
header: header, // Convert header names to uppercase
|
|
48
|
+
key: header,
|
|
49
|
+
width: 20, // Set column width
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ✅ If no data loader provided, write only headers and finish
|
|
54
|
+
if (typeof getDataRecords !== 'function') {
|
|
55
|
+
// worksheet.addRow(
|
|
56
|
+
// headers.reduce((acc, header) => ({ ...acc, [header]: '' }), {})
|
|
57
|
+
// ).commit(); // Write a dummy record with headers
|
|
58
|
+
|
|
59
|
+
await workbook.commit();
|
|
60
|
+
return passThrough;
|
|
61
|
+
}
|
|
73
62
|
|
|
63
|
+
// Write the data records in chunks
|
|
64
|
+
let chunkIndex = 0;
|
|
74
65
|
while (true) {
|
|
75
66
|
const records = await getDataRecords(chunkIndex, chunkSize); // Fetch chunked data
|
|
76
67
|
if (records.length === 0) break; // Stop if no more records
|
|
77
68
|
|
|
78
|
-
if (!isHeaderWritten) {
|
|
79
|
-
worksheet.columns = Object.keys(records[0]).map((key) => ({
|
|
80
|
-
header: key.toUpperCase(),
|
|
81
|
-
key: key,
|
|
82
|
-
width: 20,
|
|
83
|
-
}));
|
|
84
|
-
isHeaderWritten = true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
69
|
records.forEach((item) => {
|
|
88
70
|
worksheet.addRow(item).commit(); // Commit each row immediately
|
|
89
71
|
});
|
|
@@ -102,4 +84,67 @@ export class ExcelService {
|
|
|
102
84
|
return passThrough; // Return streaming response
|
|
103
85
|
}
|
|
104
86
|
|
|
87
|
+
public async *readExcelInPagesFromStream(
|
|
88
|
+
stream: Readable,
|
|
89
|
+
options?: ExcelReadOptions
|
|
90
|
+
): AsyncGenerator<ExcelReadResult> {
|
|
91
|
+
const { pageSize = DEFAULT_PAGE_SIZE, hasHeaderRow = true, providedHeaders = [] } = options || {};
|
|
92
|
+
const workbookReader = new ExcelJS.stream.xlsx.WorkbookReader(stream, {});
|
|
93
|
+
|
|
94
|
+
let headers: string[] = [];
|
|
95
|
+
let page: Record<string, any>[] = [];
|
|
96
|
+
let isFirstRow = true;
|
|
97
|
+
let hasYieldedData = false;
|
|
98
|
+
|
|
99
|
+
for await (const worksheet of workbookReader) {
|
|
100
|
+
for await (const row of worksheet) {
|
|
101
|
+
const values = Array.isArray(row.values) ? row.values.slice(1) : [];
|
|
102
|
+
|
|
103
|
+
if (isFirstRow) {
|
|
104
|
+
isFirstRow = false;
|
|
105
|
+
|
|
106
|
+
if (hasHeaderRow) {
|
|
107
|
+
headers = values.map(v => v?.toString().trim() || '');
|
|
108
|
+
continue;
|
|
109
|
+
} else if (providedHeaders.length) {
|
|
110
|
+
headers = providedHeaders;
|
|
111
|
+
} else {
|
|
112
|
+
headers = values.map((_, idx) => `${idx}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
while (values.length < headers.length) values.push(null);
|
|
117
|
+
if (values.length > headers.length) values.length = headers.length;
|
|
118
|
+
|
|
119
|
+
const record = headers.reduce((acc, key, i) => {
|
|
120
|
+
acc[key] = values[i] ?? null;
|
|
121
|
+
return acc;
|
|
122
|
+
}, {} as Record<string, any>);
|
|
123
|
+
|
|
124
|
+
if (Object.values(record).every(v => v === null || v === '')) continue;
|
|
125
|
+
|
|
126
|
+
page.push(record);
|
|
127
|
+
|
|
128
|
+
if (page.length === pageSize) {
|
|
129
|
+
yield { headers, data: page };
|
|
130
|
+
hasYieldedData = true;
|
|
131
|
+
page = [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Optional: break if only processing first worksheet
|
|
136
|
+
// break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (page.length > 0) {
|
|
140
|
+
yield { headers, data: page };
|
|
141
|
+
hasYieldedData = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ✅ Yield headers with empty data if only headers were found
|
|
145
|
+
if (!hasYieldedData && headers.length > 0) {
|
|
146
|
+
yield { headers, data: [] };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
105
150
|
}
|
|
@@ -3,4 +3,8 @@
|
|
|
3
3
|
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
4
|
- startExport should either return the data or return the transaction id
|
|
5
5
|
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
-
- show the userKey
|
|
6
|
+
- show the userKey
|
|
7
|
+
4. Exclude the media fields from the fields shown in the template creation
|
|
8
|
+
5. Allow provision to export the data without creating a template
|
|
9
|
+
6. Also for template creation i.e do we want the option to keep the templates at a user level instead of at the application level.
|
|
10
|
+
7. Do we want to save the filter against the template.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
|
|
3
|
+
import { DiscoveryService, ModuleRef } from "@nestjs/core";
|
|
4
|
+
import { EntityManager, Repository } from 'typeorm';
|
|
5
|
+
|
|
6
|
+
import { CRUDService } from 'src/services/crud.service';
|
|
7
|
+
import { ModelMetadataService } from 'src/services/model-metadata.service';
|
|
8
|
+
import { ModuleMetadataService } from 'src/services/module-metadata.service';
|
|
9
|
+
import { ConfigService } from '@nestjs/config';
|
|
10
|
+
import { FileService } from 'src/services/file.service';
|
|
11
|
+
import { CrudHelperService } from 'src/services/crud-helper.service';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import { ImportTransactionErrorLog } from '../entities/import-transaction-error-log.entity';
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class ImportTransactionErrorLogService extends CRUDService<ImportTransactionErrorLog>{
|
|
18
|
+
constructor(
|
|
19
|
+
readonly modelMetadataService: ModelMetadataService,
|
|
20
|
+
readonly moduleMetadataService: ModuleMetadataService,
|
|
21
|
+
readonly configService: ConfigService,
|
|
22
|
+
readonly fileService: FileService,
|
|
23
|
+
readonly discoveryService: DiscoveryService,
|
|
24
|
+
readonly crudHelperService: CrudHelperService,
|
|
25
|
+
@InjectEntityManager()
|
|
26
|
+
readonly entityManager: EntityManager,
|
|
27
|
+
@InjectRepository(ImportTransactionErrorLog, 'default')
|
|
28
|
+
readonly repo: Repository<ImportTransactionErrorLog>,
|
|
29
|
+
readonly moduleRef: ModuleRef
|
|
30
|
+
|
|
31
|
+
) {
|
|
32
|
+
super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService,entityManager, repo, 'importTransactionErrorLog', 'solid-core', moduleRef);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService, ModuleRef } from "@nestjs/core";
|
|
3
|
+
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
|
|
4
|
+
import { EntityManager, Repository } from 'typeorm';
|
|
5
|
+
|
|
6
|
+
import { ConfigService } from '@nestjs/config';
|
|
7
|
+
import { CrudHelperService } from 'src/services/crud-helper.service';
|
|
8
|
+
import { CRUDService } from 'src/services/crud.service';
|
|
9
|
+
import { FileService } from 'src/services/file.service';
|
|
10
|
+
import { ModelMetadataService } from 'src/services/model-metadata.service';
|
|
11
|
+
import { ModuleMetadataService } from 'src/services/module-metadata.service';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import { HttpService } from '@nestjs/axios';
|
|
15
|
+
import { SolidFieldType } from 'src/dtos/create-field-metadata.dto';
|
|
16
|
+
import { ImportInstructionsResponseDto, StandardImportInstructionsResponseDto } from 'src/dtos/import-instructions.dto';
|
|
17
|
+
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
18
|
+
import { MediaWithFullUrl } from 'src/interfaces';
|
|
19
|
+
import { Readable } from 'stream';
|
|
20
|
+
import { ImportTransaction } from '../entities/import-transaction.entity';
|
|
21
|
+
import { CsvService } from './csv.service';
|
|
22
|
+
import { ExcelService } from './excel.service';
|
|
23
|
+
|
|
24
|
+
interface ImportTemplateFileInfo {
|
|
25
|
+
stream: NodeJS.ReadableStream;
|
|
26
|
+
fileName: string;
|
|
27
|
+
mimeType: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export enum ImportFormat {
|
|
31
|
+
CSV = 'csv',
|
|
32
|
+
EXCEL = 'excel',
|
|
33
|
+
}
|
|
34
|
+
export interface ImportMappingInfo {
|
|
35
|
+
sampleImportedRecordInfo: SampleImportedRecordInfo[];
|
|
36
|
+
importableFields: ImportableFieldInfo[];
|
|
37
|
+
}
|
|
38
|
+
export interface SampleImportedRecordInfo {
|
|
39
|
+
cellHeader: string; // The header of the cell in the import file
|
|
40
|
+
cellValue: string; // The value of the cell in the import file
|
|
41
|
+
defaultMappedFieldName: string; // The default mapped field name in the model metadata
|
|
42
|
+
}
|
|
43
|
+
export interface ImportableFieldInfo {
|
|
44
|
+
name: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ImportReadResult {
|
|
49
|
+
headers: string[]; // Headers of the CSV file
|
|
50
|
+
data: Record<string, any>[]; // Data records in the current page
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Injectable()
|
|
54
|
+
export class ImportTransactionService extends CRUDService<ImportTransaction> {
|
|
55
|
+
constructor(
|
|
56
|
+
readonly modelMetadataService: ModelMetadataService,
|
|
57
|
+
readonly moduleMetadataService: ModuleMetadataService,
|
|
58
|
+
readonly configService: ConfigService,
|
|
59
|
+
readonly fileService: FileService,
|
|
60
|
+
readonly discoveryService: DiscoveryService,
|
|
61
|
+
readonly crudHelperService: CrudHelperService,
|
|
62
|
+
@InjectEntityManager()
|
|
63
|
+
readonly entityManager: EntityManager,
|
|
64
|
+
@InjectRepository(ImportTransaction, 'default')
|
|
65
|
+
readonly repo: Repository<ImportTransaction>,
|
|
66
|
+
readonly moduleRef: ModuleRef,
|
|
67
|
+
readonly excelService: ExcelService,
|
|
68
|
+
readonly csvService: CsvService,
|
|
69
|
+
readonly httpService: HttpService
|
|
70
|
+
|
|
71
|
+
) {
|
|
72
|
+
super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService, entityManager, repo, 'importTransaction', 'solid-core', moduleRef);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private readonly logger = new Logger(ImportTransactionService.name);
|
|
76
|
+
saveImportMapping(arg0: number) {
|
|
77
|
+
throw new Error('Method not implemented.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getImportMappingInfo(importTransactionId: number): Promise<ImportMappingInfo> {
|
|
81
|
+
// Load the import transaction for the given ID
|
|
82
|
+
const importTransaction = await this.findOne(importTransactionId, {
|
|
83
|
+
populate: ['modelMetadata', 'modelMetadata.fields'],
|
|
84
|
+
populateMedia: ['fileLocation'],
|
|
85
|
+
});
|
|
86
|
+
if (!importTransaction) {
|
|
87
|
+
throw new Error(`Import transaction with ID ${importTransactionId} not found.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get all the importable fields from the model metadata
|
|
91
|
+
const importableFields: ImportableFieldInfo[] = this.fieldsAllowedForImport(importTransaction.modelMetadata.fields).map(field => ({
|
|
92
|
+
name: field.name,
|
|
93
|
+
displayName: field.displayName,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
// Get the import file stream for the import transaction
|
|
97
|
+
const importFileMediaObject = importTransaction['_media']['fileLocation'][0] as MediaWithFullUrl; // Since there can be only one fileLocation, we can safely access the first element
|
|
98
|
+
if (!importFileMediaObject) {
|
|
99
|
+
throw new Error(`Import file for transaction ID ${importTransactionId} not found.`);
|
|
100
|
+
}
|
|
101
|
+
const importFileStream = await this.getImportFileStream(importFileMediaObject);
|
|
102
|
+
|
|
103
|
+
// Get a sample of records from the import file
|
|
104
|
+
const sampleRecord = await this.getFileRecordsSample(importFileStream, importFileMediaObject.mimeType);
|
|
105
|
+
|
|
106
|
+
// Convert sampleRecord to the format required for SampleImportedRecordInfo
|
|
107
|
+
const wrappedRecords: SampleImportedRecordInfo[] = sampleRecord.data.map((record: Record<string, any>) => {
|
|
108
|
+
return Object.entries(record).map(([key, value]) => ({
|
|
109
|
+
cellHeader: key,
|
|
110
|
+
cellValue: value,
|
|
111
|
+
defaultMappedFieldName: importableFields.find(field => field.displayName === key)?.name || '',
|
|
112
|
+
}));
|
|
113
|
+
}).flat();
|
|
114
|
+
|
|
115
|
+
// for await (const page of this.csvService.readCsvInPagesFromStream(importFileStream)) {
|
|
116
|
+
// // await dbService.bulkInsert(page);
|
|
117
|
+
// }
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
sampleImportedRecordInfo: wrappedRecords, // This will hold the sample data from the file
|
|
121
|
+
importableFields: importableFields, // This will hold the fields that can be imported
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async getFileRecordsSample(importFileStream: Readable, mimeType: string): Promise<ImportReadResult> {
|
|
126
|
+
// Depending upon the mime type of the file, read the file in pages
|
|
127
|
+
// For CSV files, use the csvService to read the file in pages
|
|
128
|
+
// For Excel files, use the excelService to read the file in pages
|
|
129
|
+
if (mimeType === 'text/csv') {
|
|
130
|
+
const generator = this.csvService.readCsvInPagesFromStream(importFileStream, { pageSize: 1 });
|
|
131
|
+
const firstRecord = await generator.next(); // Get the first record to extract headers and sample data
|
|
132
|
+
return firstRecord.value;
|
|
133
|
+
} else if (mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
|
134
|
+
const generator = this.excelService.readExcelInPagesFromStream(importFileStream, { pageSize: 1 });
|
|
135
|
+
const firstRecord = await generator.next(); // Get the first record to extract headers and sample data
|
|
136
|
+
return firstRecord.value;
|
|
137
|
+
}
|
|
138
|
+
else { // If the file is neither CSV nor Excel, throw an error
|
|
139
|
+
throw new Error(`Unsupported file type: ${mimeType}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async getImportFileStream(importFileMediaObject: MediaWithFullUrl): Promise<Readable> {
|
|
144
|
+
const fileUrl = importFileMediaObject['_full_url'];
|
|
145
|
+
const mimeType = importFileMediaObject['mimeType'];
|
|
146
|
+
if (!fileUrl) {
|
|
147
|
+
throw new Error(`File URL ${fileUrl} not found.`);
|
|
148
|
+
}
|
|
149
|
+
// From the file URL, convert the file URL to a readable stream using nestjs http service and axios
|
|
150
|
+
const fileUrlResponse = await this.httpService.axiosRef.get(fileUrl, {
|
|
151
|
+
responseType: 'stream',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!fileUrlResponse || !fileUrlResponse.data) {
|
|
155
|
+
throw new Error(`Failed to read file from URL: ${fileUrl}`);
|
|
156
|
+
}
|
|
157
|
+
// fileUrlResponse.data is a Node.js Readable stream
|
|
158
|
+
return fileUrlResponse.data;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async getImportInstructions(modelMetadataId: number): Promise<ImportInstructionsResponseDto> {
|
|
162
|
+
// Load the model metadata for the given ID
|
|
163
|
+
const modelMetadata = await this.modelMetadataService.findOne(modelMetadataId, {
|
|
164
|
+
populate: ['fields'],
|
|
165
|
+
});
|
|
166
|
+
if (!modelMetadata) {
|
|
167
|
+
throw new Error(`Model metadata with ID ${modelMetadataId} not found.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create the standard import instructions
|
|
171
|
+
const standardInstructions: StandardImportInstructionsResponseDto = {
|
|
172
|
+
requiredFields: [],
|
|
173
|
+
dateFields: [],
|
|
174
|
+
dateTimeFields: [],
|
|
175
|
+
numberFields: [],
|
|
176
|
+
emailFields: [],
|
|
177
|
+
regexFields: [],
|
|
178
|
+
jsonFields: [],
|
|
179
|
+
booleanFields: [],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Iterate through the fields and populate the standard instructions
|
|
183
|
+
for (const field of modelMetadata.fields) {
|
|
184
|
+
if (field.isSystem) continue; // Skip system fields
|
|
185
|
+
if (field.required) {
|
|
186
|
+
standardInstructions.requiredFields.push(field.displayName);
|
|
187
|
+
}
|
|
188
|
+
if (field.type === SolidFieldType.date) {
|
|
189
|
+
standardInstructions.dateFields.push(field.displayName);
|
|
190
|
+
}
|
|
191
|
+
if (field.type === SolidFieldType.datetime) {
|
|
192
|
+
standardInstructions.dateTimeFields.push(field.displayName);
|
|
193
|
+
}
|
|
194
|
+
if ([SolidFieldType.bigint, SolidFieldType.int, SolidFieldType.decimal].includes(field.type as SolidFieldType)) {
|
|
195
|
+
standardInstructions.numberFields.push(field.displayName);
|
|
196
|
+
}
|
|
197
|
+
if (field.type === SolidFieldType.email) {
|
|
198
|
+
standardInstructions.emailFields.push(field.displayName);
|
|
199
|
+
}
|
|
200
|
+
if (field.regexPattern) {
|
|
201
|
+
standardInstructions.regexFields.push({
|
|
202
|
+
fieldName: field.displayName,
|
|
203
|
+
regexPattern: field.regexPattern,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (field.type === SolidFieldType.json) {
|
|
207
|
+
standardInstructions.jsonFields.push(field.displayName);
|
|
208
|
+
}
|
|
209
|
+
if (field.type === SolidFieldType.boolean) {
|
|
210
|
+
standardInstructions.booleanFields.push(field.displayName);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Create the custom instructions
|
|
215
|
+
const customInstructions: string[] = [];
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
standard: standardInstructions,
|
|
219
|
+
custom: customInstructions,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* This method is used to return a csv / excel template for the import transaction
|
|
225
|
+
* It will contain the display names of the fields in the header row
|
|
226
|
+
* @param modelMetadataId
|
|
227
|
+
*/
|
|
228
|
+
async getImportTemplate(modelMetadataId: number, format: ImportFormat = ImportFormat.CSV): Promise<ImportTemplateFileInfo> {
|
|
229
|
+
// Load the model metadata for the given ID
|
|
230
|
+
const modelMetadata = await this.modelMetadataService.findOne(modelMetadataId, {
|
|
231
|
+
populate: ['fields'],
|
|
232
|
+
});
|
|
233
|
+
if (!modelMetadata) {
|
|
234
|
+
throw new Error(`Model metadata with ID ${modelMetadataId} not found.`);
|
|
235
|
+
}
|
|
236
|
+
// Create a header row with the display names of the fields, excluding the media fields,computed fields
|
|
237
|
+
const headers = this.fieldsAllowedForImport(modelMetadata.fields)
|
|
238
|
+
.map(field => field.displayName);
|
|
239
|
+
|
|
240
|
+
// Depending on the format, generate the template
|
|
241
|
+
if (format === ImportFormat.CSV) {
|
|
242
|
+
const stream = await this.csvService.createCsvStream(null, 0, headers); // Create a CSV stream with the header row
|
|
243
|
+
const fileName = `${modelMetadata.singularName}-import-template.csv`;
|
|
244
|
+
const mimeType = 'text/csv';
|
|
245
|
+
return {
|
|
246
|
+
stream,
|
|
247
|
+
fileName,
|
|
248
|
+
mimeType,
|
|
249
|
+
};
|
|
250
|
+
} else if (format === ImportFormat.EXCEL) {
|
|
251
|
+
const stream = await this.excelService.createExcelStream(null, 0, headers); // Create an Excel stream with the header row
|
|
252
|
+
const fileName = `${modelMetadata.singularName}-import-template.xlsx`;
|
|
253
|
+
// Set the MIME type for Excel files
|
|
254
|
+
const mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
255
|
+
return {
|
|
256
|
+
stream,
|
|
257
|
+
fileName,
|
|
258
|
+
mimeType,
|
|
259
|
+
};
|
|
260
|
+
} else {
|
|
261
|
+
throw new Error(`Unsupported import format: ${format}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fieldsAllowedForImport(fields: FieldMetadata[]): FieldMetadata[] {
|
|
267
|
+
// Filter out fields that are not allowed for import
|
|
268
|
+
return fields.filter(field =>
|
|
269
|
+
field.type !== SolidFieldType.mediaMultiple && // Exclude media multiple fields
|
|
270
|
+
field.type !== SolidFieldType.mediaSingle &&
|
|
271
|
+
field.type !== SolidFieldType.computed && // Exclude computed fields
|
|
272
|
+
field.type !== SolidFieldType.password &&
|
|
273
|
+
field.type !== SolidFieldType.richText &&
|
|
274
|
+
field.type !== SolidFieldType.uuid &&
|
|
275
|
+
field.isSystem !== true // Exclude system fields
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
}
|
package/src/solid-core.module.ts
CHANGED
|
@@ -171,6 +171,12 @@ import { ExcelService } from './services/excel.service';
|
|
|
171
171
|
import { CsvService } from './services/csv.service';
|
|
172
172
|
import { ClsModule } from 'nestjs-cls';
|
|
173
173
|
import { FieldRepository } from './repository/field.repository';
|
|
174
|
+
import { ImportTransaction } from './entities/import-transaction.entity';
|
|
175
|
+
import { ImportTransactionService } from './services/import-transaction.service';
|
|
176
|
+
import { ImportTransactionController } from './controllers/import-transaction.controller';
|
|
177
|
+
import { ImportTransactionErrorLog } from './entities/import-transaction-error-log.entity';
|
|
178
|
+
import { ImportTransactionErrorLogService } from './services/import-transaction-error-log.service';
|
|
179
|
+
import { ImportTransactionErrorLogController } from './controllers/import-transaction-error-log.controller';
|
|
174
180
|
|
|
175
181
|
|
|
176
182
|
@Global()
|
|
@@ -229,6 +235,8 @@ import { FieldRepository } from './repository/field.repository';
|
|
|
229
235
|
middleware: {
|
|
230
236
|
mount: true,
|
|
231
237
|
}}),
|
|
238
|
+
TypeOrmModule.forFeature([ImportTransaction]),
|
|
239
|
+
TypeOrmModule.forFeature([ImportTransactionErrorLog]),
|
|
232
240
|
],
|
|
233
241
|
controllers: [
|
|
234
242
|
ModuleMetadataController,
|
|
@@ -262,6 +270,8 @@ import { FieldRepository } from './repository/field.repository';
|
|
|
262
270
|
ChatterMessageDetailsController,
|
|
263
271
|
ExportTemplateController,
|
|
264
272
|
ExportTransactionController,
|
|
273
|
+
ImportTransactionController,
|
|
274
|
+
ImportTransactionErrorLogController,
|
|
265
275
|
],
|
|
266
276
|
providers: [
|
|
267
277
|
{
|
|
@@ -370,7 +380,9 @@ import { FieldRepository } from './repository/field.repository';
|
|
|
370
380
|
ExportTransactionService,
|
|
371
381
|
ExcelService,
|
|
372
382
|
CsvService,
|
|
373
|
-
FieldRepository
|
|
383
|
+
FieldRepository,
|
|
384
|
+
ImportTransactionService,
|
|
385
|
+
ImportTransactionErrorLogService,
|
|
374
386
|
],
|
|
375
387
|
exports: [
|
|
376
388
|
ModuleMetadataService,
|