@dynamic-entity/server 2.2.0 → 2.3.1

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/express.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { deriveImportColumns, buildTemplateSpec, CORE_VERSION, suggestMapping, toCsv, escapeFormula, validateMappingPlan, applyMapping, createCsvReader, padRow } from '@dynamic-entity/core';
1
+ import { deriveImportColumns, buildTemplateSpec, CORE_VERSION, cellText, arrayBoundFor, suggestMapping, decimalMarkFor, toCsv, escapeFormula, validateMappingPlan, applyMapping, completeFirstLine, padRow, detectDelimiter, createCsvReader } from '@dynamic-entity/core';
2
2
  import express from 'express';
3
3
  import Busboy from 'busboy';
4
4
  import ExcelJS from 'exceljs';
@@ -235,17 +235,6 @@ function readPlanField(fields) {
235
235
  throw new ImportError("INVALID_PLAN", "The mapping plan is not valid JSON.", { cause });
236
236
  }
237
237
  }
238
-
239
- // src/cell-text.ts
240
- function sampleText(raw) {
241
- if (raw === null || raw === void 0) return "";
242
- if (raw instanceof Date) {
243
- if (Number.isNaN(raw.getTime())) return "";
244
- const iso = raw.toISOString();
245
- return raw.getTime() % 864e5 === 0 ? iso.slice(0, 10) : iso;
246
- }
247
- return typeof raw === "string" ? raw : String(raw);
248
- }
249
238
  var LOCAL_HEADER = 67324752;
250
239
  var CENTRAL_HEADER = 33639248;
251
240
  var END_OF_CENTRAL = 101010256;
@@ -562,6 +551,12 @@ async function guardZip(source, limits) {
562
551
  }
563
552
  }
564
553
  if (!sheet) {
554
+ if (!kept.has("xl/workbook.xml")) {
555
+ throw new ImportError(
556
+ "UNSUPPORTED_FORMAT",
557
+ "This is a zip archive that is not an Excel workbook. Upload an .xlsx or .csv file."
558
+ );
559
+ }
565
560
  throw new ImportError("MALFORMED_FILE", "This workbook has no worksheet.");
566
561
  }
567
562
  const ordered = [...WANTED, sheet.name].map((wanted) => kept.get(wanted)).filter((entry) => entry !== void 0);
@@ -656,13 +651,36 @@ function detectFormat(head) {
656
651
  }
657
652
  return "csv";
658
653
  }
659
- async function* csvRows(stream) {
654
+ async function* csvRows(stream, limits, found, forced) {
660
655
  const decoder = new TextDecoder("utf-8");
661
- const reader = createCsvReader();
656
+ const maxHeader = limits.maxCellLength * limits.maxColumns + limits.maxColumns;
657
+ let held = "";
658
+ let reader = null;
659
+ const open = (headerLine) => {
660
+ found.delimiter = forced ?? detectDelimiter(headerLine);
661
+ return createCsvReader({ delimiter: found.delimiter });
662
+ };
662
663
  for await (const chunk of stream) {
663
- for (const row of reader.push(decoder.decode(chunk, { stream: true }))) yield row;
664
+ const text = decoder.decode(chunk, { stream: true });
665
+ if (!reader) {
666
+ held += text;
667
+ const line = completeFirstLine(held);
668
+ if (line === null) {
669
+ if (held.length > maxHeader) {
670
+ throw new ImportError("SHEET_TOO_LARGE", "The header row is longer than any row may be.");
671
+ }
672
+ continue;
673
+ }
674
+ reader = open(line);
675
+ for (const row of reader.push(held)) yield row;
676
+ held = "";
677
+ continue;
678
+ }
679
+ for (const row of reader.push(text)) yield row;
664
680
  }
665
- for (const row of reader.push(decoder.decode())) yield row;
681
+ const rest = held + decoder.decode();
682
+ reader ??= open(completeFirstLine(rest) ?? rest);
683
+ for (const row of reader.push(rest)) yield row;
666
684
  for (const row of reader.end()) yield row;
667
685
  }
668
686
  async function* guardRows(rows, width, limits) {
@@ -697,10 +715,12 @@ async function readSheet(options) {
697
715
  const counted = limitBytes(toByteStream(options.stream), limits.maxBytes);
698
716
  const { head, stream } = await peek(counted, MAGIC_BYTES);
699
717
  const format = detectFormat(head);
700
- const rows = format === "xlsx" ? xlsxRows(stream, limits) : csvRows(stream);
718
+ const found = {};
719
+ const forced = /\.tsv$/i.test(options.filename ?? "") ? " " : void 0;
720
+ const rows = format === "xlsx" ? xlsxRows(stream, limits) : csvRows(stream, limits, found, forced);
701
721
  const iterator = rows[Symbol.asyncIterator]();
702
722
  const first = await iterator.next();
703
- const headers = first.done ? [] : first.value.map(sampleText);
723
+ const headers = first.done ? [] : first.value.map(cellText);
704
724
  if (headers.length > limits.maxColumns) {
705
725
  throw new ImportError(
706
726
  "SHEET_TOO_LARGE",
@@ -711,7 +731,8 @@ async function readSheet(options) {
711
731
  return {
712
732
  format,
713
733
  headers,
714
- rows: closing(guardRows(remaining, headers.length, limits), options.stream)
734
+ rows: closing(guardRows(remaining, headers.length, limits), options.stream),
735
+ ...found.delimiter ? { delimiter: found.delimiter } : {}
715
736
  };
716
737
  } catch (error) {
717
738
  destroySource(options.stream);
@@ -765,6 +786,7 @@ async function runImport(options) {
765
786
  filename: options.filename,
766
787
  limits
767
788
  });
789
+ const decimal = options.decimal ?? decimalMarkFor(sheet.delimiter);
768
790
  const errors = [];
769
791
  let errorCount = 0;
770
792
  let imported = 0;
@@ -780,6 +802,7 @@ async function runImport(options) {
780
802
  batch = [];
781
803
  const result = applyMapping(rows, plan, config, {
782
804
  lang: options.lang,
805
+ decimal,
783
806
  rules: options.rules,
784
807
  lookups: options.lookups,
785
808
  firstRowNumber: firstRow
@@ -842,19 +865,22 @@ async function previewSheet(options) {
842
865
  try {
843
866
  for await (const row of sheet.rows) {
844
867
  rowCount++;
845
- if (sample.length < limits.sampleRows) sample.push(row.map(sampleText));
868
+ if (sample.length < limits.sampleRows) sample.push(row.map(cellText));
846
869
  }
847
870
  } catch (error) {
848
871
  destroySource(options.stream);
849
872
  throw error;
850
873
  }
851
- const { columns } = deriveImportColumns(options.config, { lang: options.lang });
874
+ const arrayBound = arrayBoundFor(sheet.headers, options.config, null, options.lang);
875
+ const { columns } = deriveImportColumns(options.config, { lang: options.lang, maxArrayRows: arrayBound });
852
876
  return {
853
877
  headers: sheet.headers,
854
878
  sample,
855
879
  rowCount,
856
880
  suggestion: suggestMapping(sheet.headers, columns, options.config?.entity ?? ""),
857
- format: sheet.format
881
+ format: sheet.format,
882
+ ...sheet.delimiter ? { delimiter: sheet.delimiter } : {},
883
+ arrayBound
858
884
  };
859
885
  }
860
886
  var TEMPLATE_EXTENSION = { csv: "csv", xlsx: "xlsx" };
@@ -1031,6 +1057,8 @@ function createImportRouter(options) {
1031
1057
  sample: preview.sample,
1032
1058
  suggestion: preview.suggestion,
1033
1059
  rowCount: preview.rowCount,
1060
+ arrayBound: preview.arrayBound,
1061
+ ...preview.delimiter ? { delimiter: preview.delimiter } : {},
1034
1062
  engineVersion: CORE_VERSION
1035
1063
  };
1036
1064
  response.json(body);