@elisra-devops/docgen-data-provider 1.75.0 → 1.77.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.
- package/bin/models/mewp-reporting.d.ts +120 -0
- package/bin/models/mewp-reporting.js +3 -0
- package/bin/models/mewp-reporting.js.map +1 -0
- package/bin/modules/ResultDataProvider.d.ts +46 -6
- package/bin/modules/ResultDataProvider.js +1156 -119
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +1285 -33
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/utils/mewpExternalIngestionUtils.d.ts +17 -0
- package/bin/utils/mewpExternalIngestionUtils.js +269 -0
- package/bin/utils/mewpExternalIngestionUtils.js.map +1 -0
- package/bin/utils/mewpExternalTableUtils.d.ts +36 -0
- package/bin/utils/mewpExternalTableUtils.js +320 -0
- package/bin/utils/mewpExternalTableUtils.js.map +1 -0
- package/package.json +10 -1
- package/src/models/mewp-reporting.ts +138 -0
- package/src/modules/ResultDataProvider.ts +1399 -166
- package/src/tests/modules/ResultDataProvider.test.ts +1471 -42
- package/src/utils/mewpExternalIngestionUtils.ts +349 -0
- package/src/utils/mewpExternalTableUtils.ts +461 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
3
|
+
import type { MewpExternalFileRef, MewpExternalTableValidationResult } from '../models/mewp-reporting';
|
|
4
|
+
import logger from './logger';
|
|
5
|
+
|
|
6
|
+
export type MewpExternalTableType = 'bugs' | 'l3l4';
|
|
7
|
+
|
|
8
|
+
interface MewpRequiredColumn {
|
|
9
|
+
label: string;
|
|
10
|
+
aliases: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MewpExternalRowsWithMeta {
|
|
14
|
+
rows: Array<Record<string, any>>;
|
|
15
|
+
meta: {
|
|
16
|
+
sourceName: string;
|
|
17
|
+
headerRow: 'A3' | 'A1' | '';
|
|
18
|
+
matchedRequiredColumns: number;
|
|
19
|
+
totalRequiredColumns: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class MewpExternalFileValidationError extends Error {
|
|
24
|
+
public readonly statusCode: number;
|
|
25
|
+
public readonly code: string;
|
|
26
|
+
public readonly details: MewpExternalTableValidationResult;
|
|
27
|
+
|
|
28
|
+
constructor(message: string, details: MewpExternalTableValidationResult) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'MewpExternalFileValidationError';
|
|
31
|
+
this.statusCode = 422;
|
|
32
|
+
this.code = 'MEWP_EXTERNAL_FILE_VALIDATION_FAILED';
|
|
33
|
+
this.details = details;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default class MewpExternalTableUtils {
|
|
38
|
+
private static readonly EXTERNAL_BUGS_REQUIRED_COLUMNS: MewpRequiredColumn[] = [
|
|
39
|
+
{ label: 'Elisra_SortIndex', aliases: ['elisrasortindex', 'elisra sortindex', 'elisra_sortindex'] },
|
|
40
|
+
{ label: 'SR', aliases: ['sr'] },
|
|
41
|
+
{ label: 'TargetWorkItemId', aliases: ['targetworkitemid', 'bugid', 'bug id'] },
|
|
42
|
+
{ label: 'Title', aliases: ['title', 'bugtitle', 'bug title'] },
|
|
43
|
+
{ label: 'TargetState', aliases: ['targetstate', 'state'] },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
private static readonly EXTERNAL_L3L4_REQUIRED_COLUMNS: MewpRequiredColumn[] = [
|
|
47
|
+
{ label: 'SR', aliases: ['sr'] },
|
|
48
|
+
{ label: 'AREA 34', aliases: ['area34', 'area 34'] },
|
|
49
|
+
{
|
|
50
|
+
label: 'TargetWorkItemId Level 3',
|
|
51
|
+
aliases: ['targetworkitemidlevel3', 'targetworkitemid level 3', 'targetworkitemidlevel 3'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'TargetTitleLevel3',
|
|
55
|
+
aliases: ['targettitlelevel3', 'targettitlelevel 3', 'targettitle level 3'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: 'TargetStateLevel 3',
|
|
59
|
+
aliases: ['targetstatelevel3', 'targetstatelevel 3', 'targetstate level 3'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'TargetWorkItemIdLevel 4',
|
|
63
|
+
aliases: ['targetworkitemidlevel4', 'targetworkitemid level 4', 'targetworkitemidlevel 4'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: 'TargetTitleLevel4',
|
|
67
|
+
aliases: ['targettitlelevel4', 'targettitlelevel 4', 'targettitle level 4'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'TargetStateLevel 4',
|
|
71
|
+
aliases: ['targetstatelevel4', 'targetstatelevel 4', 'targetstate level 4'],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
private static readonly ALLOWED_EXTERNAL_FILE_EXTENSIONS = new Set<string>(['.xlsx', '.xls', '.csv']);
|
|
76
|
+
private static readonly DEFAULT_EXTERNAL_FILE_MAX_BYTES = 20 * 1024 * 1024;
|
|
77
|
+
|
|
78
|
+
public getRequiredColumnLabels(tableType: MewpExternalTableType): string[] {
|
|
79
|
+
return this.getRequiredColumns(tableType).map((item) => item.label);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public getRequiredColumnCount(tableType: MewpExternalTableType): number {
|
|
83
|
+
return this.getRequiredColumns(tableType).length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public readExternalCell(row: Record<string, any>, aliases: string[]): any {
|
|
87
|
+
if (!row || typeof row !== 'object') return '';
|
|
88
|
+
const byNormalizedKey = new Map<string, any>();
|
|
89
|
+
for (const [key, value] of Object.entries(row)) {
|
|
90
|
+
byNormalizedKey.set(this.normalizeExternalColumnKey(key), value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const alias of aliases) {
|
|
94
|
+
const value = byNormalizedKey.get(this.normalizeExternalColumnKey(alias));
|
|
95
|
+
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async loadExternalTableRows(
|
|
103
|
+
source: MewpExternalFileRef | null | undefined,
|
|
104
|
+
tableType: MewpExternalTableType
|
|
105
|
+
): Promise<Array<Record<string, any>>> {
|
|
106
|
+
const { rows } = await this.loadExternalTableRowsWithMeta(source, tableType);
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async loadExternalTableRowsWithMeta(
|
|
111
|
+
source: MewpExternalFileRef | null | undefined,
|
|
112
|
+
tableType: MewpExternalTableType
|
|
113
|
+
): Promise<MewpExternalRowsWithMeta> {
|
|
114
|
+
const sourceName = String(source?.name || source?.objectName || source?.text || source?.url || '').trim();
|
|
115
|
+
const sourceUrl = this.resolveMewpExternalSourceUrl(source);
|
|
116
|
+
const extension = this.resolveMewpExternalSourceExtension(source);
|
|
117
|
+
const sourceIsolationCheck = this.validateMewpExternalSourceIsolation(source, sourceUrl);
|
|
118
|
+
const requiredColumns = this.getRequiredColumns(tableType);
|
|
119
|
+
|
|
120
|
+
if (!sourceName && !sourceUrl) {
|
|
121
|
+
return {
|
|
122
|
+
rows: [],
|
|
123
|
+
meta: {
|
|
124
|
+
sourceName: '',
|
|
125
|
+
headerRow: '',
|
|
126
|
+
matchedRequiredColumns: 0,
|
|
127
|
+
totalRequiredColumns: requiredColumns.length,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!sourceUrl) {
|
|
133
|
+
throw this.createMewpExternalValidationError(
|
|
134
|
+
tableType,
|
|
135
|
+
sourceName || 'unknown',
|
|
136
|
+
'',
|
|
137
|
+
0,
|
|
138
|
+
requiredColumns.length,
|
|
139
|
+
requiredColumns.map((item) => item.label),
|
|
140
|
+
`Missing file URL/object reference for '${sourceName || tableType}'`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!sourceIsolationCheck.valid) {
|
|
144
|
+
throw this.createMewpExternalValidationError(
|
|
145
|
+
tableType,
|
|
146
|
+
sourceName || sourceUrl,
|
|
147
|
+
'',
|
|
148
|
+
0,
|
|
149
|
+
requiredColumns.length,
|
|
150
|
+
requiredColumns.map((item) => item.label),
|
|
151
|
+
sourceIsolationCheck.message
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!MewpExternalTableUtils.ALLOWED_EXTERNAL_FILE_EXTENSIONS.has(extension)) {
|
|
156
|
+
throw this.createMewpExternalValidationError(
|
|
157
|
+
tableType,
|
|
158
|
+
sourceName || sourceUrl,
|
|
159
|
+
'',
|
|
160
|
+
0,
|
|
161
|
+
requiredColumns.length,
|
|
162
|
+
requiredColumns.map((item) => item.label),
|
|
163
|
+
`Unsupported file type '${extension || 'unknown'}'. Allowed: .xlsx, .xls, .csv`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parseRows = (sheet: XLSX.WorkSheet, startRowZeroBased: number) =>
|
|
168
|
+
XLSX.utils.sheet_to_json(sheet, {
|
|
169
|
+
defval: '',
|
|
170
|
+
raw: false,
|
|
171
|
+
range: startRowZeroBased,
|
|
172
|
+
}) as Array<Record<string, any>>;
|
|
173
|
+
|
|
174
|
+
const parseHeaderKeys = (sheet: XLSX.WorkSheet, startRowZeroBased: number): Set<string> => {
|
|
175
|
+
const matrix = XLSX.utils.sheet_to_json(sheet, {
|
|
176
|
+
header: 1,
|
|
177
|
+
raw: false,
|
|
178
|
+
range: startRowZeroBased,
|
|
179
|
+
}) as any[][];
|
|
180
|
+
const firstRow = Array.isArray(matrix?.[0]) ? matrix[0] : [];
|
|
181
|
+
const normalized = firstRow
|
|
182
|
+
.map((item) => this.normalizeExternalColumnKey(item))
|
|
183
|
+
.filter((item) => !!item);
|
|
184
|
+
return new Set(normalized);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const validateHeaders = (headerKeys: Set<string>) => {
|
|
188
|
+
const missingLabels: string[] = [];
|
|
189
|
+
let matched = 0;
|
|
190
|
+
for (const requiredColumn of requiredColumns) {
|
|
191
|
+
const hasMatch = requiredColumn.aliases
|
|
192
|
+
.map((alias) => this.normalizeExternalColumnKey(alias))
|
|
193
|
+
.some((alias) => headerKeys.has(alias));
|
|
194
|
+
if (hasMatch) {
|
|
195
|
+
matched += 1;
|
|
196
|
+
} else {
|
|
197
|
+
missingLabels.push(requiredColumn.label);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
matched,
|
|
202
|
+
total: requiredColumns.length,
|
|
203
|
+
missingLabels,
|
|
204
|
+
isValid: missingLabels.length === 0,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const response = await axios.get(sourceUrl, {
|
|
210
|
+
responseType: 'arraybuffer',
|
|
211
|
+
timeout: 45000,
|
|
212
|
+
});
|
|
213
|
+
const maxBytes = this.resolveMewpExternalMaxFileSize();
|
|
214
|
+
const declaredLength = Number(response?.headers?.['content-length'] || 0);
|
|
215
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
216
|
+
throw this.createMewpExternalValidationError(
|
|
217
|
+
tableType,
|
|
218
|
+
sourceName || sourceUrl,
|
|
219
|
+
'',
|
|
220
|
+
0,
|
|
221
|
+
requiredColumns.length,
|
|
222
|
+
requiredColumns.map((item) => item.label),
|
|
223
|
+
`File exceeds maximum allowed size (${maxBytes} bytes).`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const buffer = Buffer.from(response?.data || []);
|
|
228
|
+
if (!buffer.length) {
|
|
229
|
+
throw this.createMewpExternalValidationError(
|
|
230
|
+
tableType,
|
|
231
|
+
sourceName || sourceUrl,
|
|
232
|
+
'',
|
|
233
|
+
0,
|
|
234
|
+
requiredColumns.length,
|
|
235
|
+
requiredColumns.map((item) => item.label),
|
|
236
|
+
'File is empty'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (buffer.length > maxBytes) {
|
|
240
|
+
throw this.createMewpExternalValidationError(
|
|
241
|
+
tableType,
|
|
242
|
+
sourceName || sourceUrl,
|
|
243
|
+
'',
|
|
244
|
+
0,
|
|
245
|
+
requiredColumns.length,
|
|
246
|
+
requiredColumns.map((item) => item.label),
|
|
247
|
+
`File exceeds maximum allowed size (${maxBytes} bytes).`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
252
|
+
const firstSheetName = workbook?.SheetNames?.[0];
|
|
253
|
+
if (!firstSheetName) {
|
|
254
|
+
throw this.createMewpExternalValidationError(
|
|
255
|
+
tableType,
|
|
256
|
+
sourceName || sourceUrl,
|
|
257
|
+
'',
|
|
258
|
+
0,
|
|
259
|
+
requiredColumns.length,
|
|
260
|
+
requiredColumns.map((item) => item.label),
|
|
261
|
+
'No worksheet was found in the uploaded file'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
265
|
+
if (!sheet) {
|
|
266
|
+
throw this.createMewpExternalValidationError(
|
|
267
|
+
tableType,
|
|
268
|
+
sourceName || sourceUrl,
|
|
269
|
+
'',
|
|
270
|
+
0,
|
|
271
|
+
requiredColumns.length,
|
|
272
|
+
requiredColumns.map((item) => item.label),
|
|
273
|
+
'Worksheet data could not be read'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Expected header row is A3, but keep A1 fallback for backward compatibility.
|
|
278
|
+
const headerA3 = parseHeaderKeys(sheet, 2);
|
|
279
|
+
const headerA1 = parseHeaderKeys(sheet, 0);
|
|
280
|
+
const rowsFromA3 = parseRows(sheet, 2);
|
|
281
|
+
const rowsFromA1 = parseRows(sheet, 0);
|
|
282
|
+
const validationA3 = validateHeaders(headerA3);
|
|
283
|
+
const validationA1 = validateHeaders(headerA1);
|
|
284
|
+
|
|
285
|
+
if (validationA3.isValid) {
|
|
286
|
+
return {
|
|
287
|
+
rows: rowsFromA3,
|
|
288
|
+
meta: {
|
|
289
|
+
sourceName: sourceName || sourceUrl,
|
|
290
|
+
headerRow: 'A3',
|
|
291
|
+
matchedRequiredColumns: validationA3.matched,
|
|
292
|
+
totalRequiredColumns: validationA3.total,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (validationA1.isValid) {
|
|
297
|
+
return {
|
|
298
|
+
rows: rowsFromA1,
|
|
299
|
+
meta: {
|
|
300
|
+
sourceName: sourceName || sourceUrl,
|
|
301
|
+
headerRow: 'A1',
|
|
302
|
+
matchedRequiredColumns: validationA1.matched,
|
|
303
|
+
totalRequiredColumns: validationA1.total,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const best = validationA3.matched >= validationA1.matched ? validationA3 : validationA1;
|
|
309
|
+
throw this.createMewpExternalValidationError(
|
|
310
|
+
tableType,
|
|
311
|
+
sourceName || sourceUrl,
|
|
312
|
+
'',
|
|
313
|
+
best.matched,
|
|
314
|
+
best.total,
|
|
315
|
+
best.missingLabels,
|
|
316
|
+
`Missing required columns: ${best.missingLabels.join(', ')}. Expected header row at A3 (fallback A1 was also checked).`
|
|
317
|
+
);
|
|
318
|
+
} catch (error: any) {
|
|
319
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
const msg = String(error?.message || error || '').trim();
|
|
323
|
+
logger.warn(`Could not load external MEWP source '${sourceUrl}': ${msg}`);
|
|
324
|
+
throw this.createMewpExternalValidationError(
|
|
325
|
+
tableType,
|
|
326
|
+
sourceName || sourceUrl,
|
|
327
|
+
'',
|
|
328
|
+
0,
|
|
329
|
+
requiredColumns.length,
|
|
330
|
+
requiredColumns.map((item) => item.label),
|
|
331
|
+
`Unable to load or parse the file: ${msg}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private getRequiredColumns(tableType: MewpExternalTableType): MewpRequiredColumn[] {
|
|
337
|
+
return tableType === 'bugs'
|
|
338
|
+
? MewpExternalTableUtils.EXTERNAL_BUGS_REQUIRED_COLUMNS
|
|
339
|
+
: MewpExternalTableUtils.EXTERNAL_L3L4_REQUIRED_COLUMNS;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private createMewpExternalValidationError(
|
|
343
|
+
tableType: MewpExternalTableType,
|
|
344
|
+
sourceName: string,
|
|
345
|
+
headerRow: 'A3' | 'A1' | '',
|
|
346
|
+
matchedRequiredColumns: number,
|
|
347
|
+
totalRequiredColumns: number,
|
|
348
|
+
missingRequiredColumns: string[],
|
|
349
|
+
message: string
|
|
350
|
+
): MewpExternalFileValidationError {
|
|
351
|
+
const details: MewpExternalTableValidationResult = {
|
|
352
|
+
tableType,
|
|
353
|
+
sourceName,
|
|
354
|
+
valid: false,
|
|
355
|
+
headerRow,
|
|
356
|
+
matchedRequiredColumns,
|
|
357
|
+
totalRequiredColumns,
|
|
358
|
+
missingRequiredColumns,
|
|
359
|
+
rowCount: 0,
|
|
360
|
+
message,
|
|
361
|
+
};
|
|
362
|
+
return new MewpExternalFileValidationError(message, details);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private resolveMewpExternalSourceUrl(source: MewpExternalFileRef | null | undefined): string {
|
|
366
|
+
const directUrl = String(source?.url || '').trim();
|
|
367
|
+
if (directUrl) return directUrl;
|
|
368
|
+
|
|
369
|
+
const bucketName = String(source?.bucketName || '').trim();
|
|
370
|
+
const objectName = String(source?.objectName || source?.text || '').trim();
|
|
371
|
+
const minioBase = String(process.env.MINIOSERVER || '').trim().replace(/\/+$/g, '');
|
|
372
|
+
if (!bucketName || !objectName || !minioBase) return '';
|
|
373
|
+
const encodedObjectName = objectName
|
|
374
|
+
.split('/')
|
|
375
|
+
.map((segment) => encodeURIComponent(segment))
|
|
376
|
+
.join('/');
|
|
377
|
+
return `${minioBase}/${bucketName}/${encodedObjectName}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private validateMewpExternalSourceIsolation(
|
|
381
|
+
source: MewpExternalFileRef | null | undefined,
|
|
382
|
+
sourceUrl: string
|
|
383
|
+
): { valid: boolean; message: string } {
|
|
384
|
+
const dedicatedBucket = String(
|
|
385
|
+
process.env.MEWP_EXTERNAL_INGESTION_BUCKET || 'mewp-external-ingestion'
|
|
386
|
+
).trim();
|
|
387
|
+
const declaredSourceType = String(source?.sourceType || '').trim().toLowerCase();
|
|
388
|
+
if (declaredSourceType && declaredSourceType !== 'mewpexternalingestion') {
|
|
389
|
+
return {
|
|
390
|
+
valid: false,
|
|
391
|
+
message: `Unsupported sourceType '${source?.sourceType}'. Expected 'mewpExternalIngestion'.`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const inferred = this.extractMewpBucketAndObjectFromUrl(sourceUrl);
|
|
396
|
+
const bucketName = String(source?.bucketName || inferred.bucketName || '').trim();
|
|
397
|
+
const objectName = String(source?.objectName || source?.text || inferred.objectName || '').trim();
|
|
398
|
+
|
|
399
|
+
if (bucketName && bucketName !== dedicatedBucket) {
|
|
400
|
+
return {
|
|
401
|
+
valid: false,
|
|
402
|
+
message: `Invalid storage bucket '${bucketName}'. Expected '${dedicatedBucket}'.`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (objectName) {
|
|
407
|
+
const normalizedObject = objectName.toLowerCase();
|
|
408
|
+
if (!normalizedObject.includes('/mewp-external-ingestion/')) {
|
|
409
|
+
return {
|
|
410
|
+
valid: false,
|
|
411
|
+
message: `Invalid object path '${objectName}'. Expected '/mewp-external-ingestion/' prefix segment.`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { valid: true, message: '' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private extractMewpBucketAndObjectFromUrl(url: string): { bucketName: string; objectName: string } {
|
|
420
|
+
try {
|
|
421
|
+
const parsed = new URL(String(url || '').trim());
|
|
422
|
+
const segments = decodeURIComponent(parsed.pathname || '')
|
|
423
|
+
.replace(/^\/+/g, '')
|
|
424
|
+
.split('/')
|
|
425
|
+
.filter((item) => !!item);
|
|
426
|
+
if (segments.length < 2) return { bucketName: '', objectName: '' };
|
|
427
|
+
return {
|
|
428
|
+
bucketName: String(segments[0] || '').trim(),
|
|
429
|
+
objectName: segments.slice(1).join('/'),
|
|
430
|
+
};
|
|
431
|
+
} catch {
|
|
432
|
+
return { bucketName: '', objectName: '' };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private resolveMewpExternalSourceExtension(source: MewpExternalFileRef | null | undefined): string {
|
|
437
|
+
const candidates = [source?.name, source?.objectName, source?.text, source?.url]
|
|
438
|
+
.map((value) => String(value || '').trim())
|
|
439
|
+
.filter((value) => !!value);
|
|
440
|
+
for (const candidate of candidates) {
|
|
441
|
+
const clean = candidate.split('?')[0].split('#')[0];
|
|
442
|
+
const match = /\.([a-z0-9]+)$/i.exec(clean);
|
|
443
|
+
if (!match) continue;
|
|
444
|
+
return `.${String(match[1] || '').toLowerCase()}`;
|
|
445
|
+
}
|
|
446
|
+
return '';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private resolveMewpExternalMaxFileSize(): number {
|
|
450
|
+
const configured = Number(process.env.MEWP_EXTERNAL_MAX_FILE_SIZE_BYTES || 0);
|
|
451
|
+
if (Number.isFinite(configured) && configured > 0) return configured;
|
|
452
|
+
return MewpExternalTableUtils.DEFAULT_EXTERNAL_FILE_MAX_BYTES;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private normalizeExternalColumnKey(value: any): string {
|
|
456
|
+
return String(value || '')
|
|
457
|
+
.trim()
|
|
458
|
+
.toLowerCase()
|
|
459
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
460
|
+
}
|
|
461
|
+
}
|