pict-section-dataimport 1.0.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.
@@ -0,0 +1,149 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ const libPictViewWizard = require('../views/PictView-DataImport-Wizard.js');
4
+ const libComprehensionBuilder = require('../services/DataImport-ComprehensionBuilder.js');
5
+ const libDataImportCSS = require('../Pict-Section-DataImport-CSS.js');
6
+
7
+ const libParserDelimited = require('../seams/DataImport-Parser-Delimited.js');
8
+ const libParserFixedWidth = require('../seams/DataImport-Parser-FixedWidth.js');
9
+ const libParserXlsx = require('../seams/DataImport-Parser-Xlsx.js');
10
+ const libSchemaProvider = require('../seams/DataImport-SchemaProvider.js');
11
+ const libPushTarget = require('../seams/DataImport-PushTarget.js');
12
+ const libStateStore = require('../seams/DataImport-StateStore.js');
13
+
14
+ /** @type {Record<string, any>} */
15
+ const _DEFAULT_CONFIGURATION =
16
+ {
17
+ ProviderIdentifier: 'Pict-Section-DataImport',
18
+
19
+ AutoInitialize: true,
20
+ AutoInitializeOrdinal: 0,
21
+ };
22
+
23
+ /**
24
+ * The pict-section-dataimport provider — the embeddable API surface. Registers the wizard CSS, then
25
+ * createImportWizard(hash, config) drops the whole upload -> validate -> map -> generate -> push flow
26
+ * into the host's DOM. The four seams (Parser / Schema / Push / State) are normalized here from
27
+ * strings, instances, or functions into live objects the wizard view consumes.
28
+ */
29
+ class PictProviderDataImport extends libPictProvider
30
+ {
31
+ constructor(pFable, pOptions, pServiceHash)
32
+ {
33
+ let tmpOptions = Object.assign({}, _DEFAULT_CONFIGURATION, pOptions);
34
+ super(pFable, tmpOptions, pServiceHash);
35
+
36
+ if (this.pict && this.pict.CSSMap && typeof this.pict.CSSMap.addCSS === 'function')
37
+ {
38
+ this.pict.CSSMap.addCSS('Pict-Section-DataImport-CSS', libDataImportCSS, 500);
39
+ }
40
+ this._meadowAdapter = null;
41
+ }
42
+
43
+ /** Lazily require the Meadow seam adapters (only when a Meadow schema source / EntityProvider push is used). */
44
+ _meadow()
45
+ {
46
+ if (!this._meadowAdapter) { this._meadowAdapter = require('./Pict-Provider-DataImport-Meadow.js'); }
47
+ return this._meadowAdapter;
48
+ }
49
+
50
+ /**
51
+ * Create (or reconfigure + reuse) an embedded import wizard.
52
+ * @param {string} pWizardHash @param {Record<string, any>} pConfig
53
+ * @return {any} The wizard view instance.
54
+ */
55
+ createImportWizard(pWizardHash, pConfig)
56
+ {
57
+ const tmpConfig = Object.assign(
58
+ {
59
+ DestinationAddress: `#${pWizardHash}`,
60
+ RenderMode: 'wizard',
61
+ URLPrefix: '/1.0/',
62
+ GUIDPrefix: 'INTG-DEF',
63
+ EntityGUIDPrefix: '',
64
+ AllowGUIDTruncation: false,
65
+ AllowedFileTypes: [ 'csv', 'tsv', 'xlsx', 'fixedwidth' ],
66
+ SchemaSource: 'meadow',
67
+ PushMode: 'entityprovider',
68
+ StateStore: 'memory',
69
+ },
70
+ pConfig || {},
71
+ { WizardHash: pWizardHash });
72
+
73
+ // Resolve the seams onto the config — the wizard view reads these directly.
74
+ tmpConfig.ResolvedParsers = this._resolveParsers(tmpConfig);
75
+ tmpConfig.ResolvedSchemaProvider = this._resolveSchemaProvider(tmpConfig);
76
+ tmpConfig.ResolvedPushTarget = this._resolvePushTarget(tmpConfig);
77
+ tmpConfig.ResolvedStateStore = this._resolveStateStore(tmpConfig);
78
+ tmpConfig.ComprehensionBuilder = new libComprehensionBuilder(this.pict);
79
+
80
+ if (this.pict.views[pWizardHash])
81
+ {
82
+ Object.assign(this.pict.views[pWizardHash].options, tmpConfig);
83
+ return this.pict.views[pWizardHash];
84
+ }
85
+ return this.pict.addView(pWizardHash, tmpConfig, libPictViewWizard);
86
+ }
87
+
88
+ /** Build the parser registry { kind: instance }, merging any host-supplied custom parsers. */
89
+ _resolveParsers(pConfig)
90
+ {
91
+ const tmpParsers =
92
+ {
93
+ csv: new libParserDelimited(this.pict, { Kind: 'csv' }),
94
+ tsv: new libParserDelimited(this.pict, { Kind: 'tsv' }),
95
+ fixedwidth: new libParserFixedWidth(this.pict, {}),
96
+ xlsx: new libParserXlsx(this.pict, {}),
97
+ };
98
+ if (pConfig.Parsers && typeof pConfig.Parsers === 'object')
99
+ {
100
+ Object.keys(pConfig.Parsers).forEach((pKind) => { tmpParsers[pKind] = pConfig.Parsers[pKind]; });
101
+ }
102
+ return tmpParsers;
103
+ }
104
+
105
+ /** Normalize SchemaSource: 'meadow' | 'config' | instance | function(ctx)->Promise<schema>. */
106
+ _resolveSchemaProvider(pConfig)
107
+ {
108
+ const tmpSource = pConfig.SchemaSource;
109
+ if (tmpSource && typeof tmpSource.getSchema === 'function') { return tmpSource; }
110
+ if (typeof tmpSource === 'function') { return { getSchema: (pContext) => Promise.resolve(tmpSource(pContext)) }; }
111
+ if (tmpSource === 'meadow')
112
+ {
113
+ return new (this._meadow().DataImportSchemaProviderMeadow)(this.pict, { MeadowEntities: pConfig.MeadowEntities || [], URLPrefix: pConfig.URLPrefix });
114
+ }
115
+ // default: a config-supplied schema descriptor
116
+ return new libSchemaProvider.DataImportSchemaProviderConfig(this.pict, { Schema: pConfig.Schema || { Entities: [] } });
117
+ }
118
+
119
+ /** Normalize PushMode: 'entityprovider' | 'comprehension' | instance | function(comp,ctx)->Promise<result>. */
120
+ _resolvePushTarget(pConfig)
121
+ {
122
+ const tmpMode = pConfig.PushMode;
123
+ if (tmpMode && typeof tmpMode.push === 'function') { return tmpMode; }
124
+ if (typeof tmpMode === 'function') { return { push: (pComprehension, pContext) => Promise.resolve(tmpMode(pComprehension, pContext)) }; }
125
+ if (tmpMode === 'entityprovider')
126
+ {
127
+ return new (this._meadow().DataImportPushTargetEntityProvider)(this.pict, {});
128
+ }
129
+ // default: POST the whole comprehension to an endpoint
130
+ return new libPushTarget.DataImportPushTargetComprehension(this.pict, { URL: pConfig.ComprehensionPushURL });
131
+ }
132
+
133
+ /** Normalize StateStore: 'memory' | 'localStorage' | instance, or a PersistenceHook on the config. */
134
+ _resolveStateStore(pConfig)
135
+ {
136
+ if (pConfig.PersistenceHook && typeof pConfig.PersistenceHook.save === 'function')
137
+ {
138
+ return new libStateStore.DataImportStateStoreHook(this.pict, { Hook: pConfig.PersistenceHook });
139
+ }
140
+ const tmpStore = pConfig.StateStore;
141
+ if (tmpStore && typeof tmpStore.save === 'function') { return tmpStore; }
142
+ if (tmpStore === 'localStorage') { return new libStateStore.DataImportStateStoreLocal(this.pict, { KeyPrefix: pConfig.StateStoreKeyPrefix }); }
143
+ return new libStateStore.DataImportStateStoreMemory(this.pict, {});
144
+ }
145
+ }
146
+
147
+ module.exports = PictProviderDataImport;
148
+
149
+ module.exports.default_configuration = _DEFAULT_CONFIGURATION;
@@ -0,0 +1,128 @@
1
+ // DataImport-Parser-Delimited — CSV + TSV parser (hand-rolled, RFC-4180-aware, no dependency).
2
+ //
3
+ // Handles quoted fields, embedded delimiters + newlines inside quotes, and doubled "" escapes. One
4
+ // class serves both CSV and TSV; the delimiter comes from ParseConfig.Delimited.Delimiter (defaulting
5
+ // from the instance Kind). Registered as a 'csv' instance and a 'tsv' instance by the provider.
6
+
7
+ const libParserProvider = require('./DataImport-ParserProvider.js');
8
+
9
+ class DataImportParserDelimited extends libParserProvider
10
+ {
11
+ get Kind()
12
+ {
13
+ return this.options.Kind || 'csv';
14
+ }
15
+
16
+ /** @param {Record<string, any>} pParseConfig @return {{Delimiter:string, HasHeader:boolean, SampleRowLimit:number}} */
17
+ _settings(pParseConfig)
18
+ {
19
+ const tmpDelimitedConfig = (pParseConfig && pParseConfig.Delimited) || {};
20
+ const tmpDefaultDelimiter = (this.Kind === 'tsv') ? '\t' : ',';
21
+ return {
22
+ Delimiter: tmpDelimitedConfig.Delimiter || tmpDefaultDelimiter,
23
+ HasHeader: (tmpDelimitedConfig.HasHeader !== false),
24
+ SampleRowLimit: (pParseConfig && pParseConfig.SampleRowLimit) || 25,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Parse delimited text into an array of cell-arrays (one per record), quote-aware + multiline-safe.
30
+ * @param {string} pText @param {string} pDelimiter @return {Array<Array<string>>}
31
+ */
32
+ parseDelimited(pText, pDelimiter)
33
+ {
34
+ const tmpRows = [];
35
+ let tmpRow = [];
36
+ let tmpField = '';
37
+ let tmpInQuotes = false;
38
+ const tmpText = (pText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
39
+ let tmpRowHasContent = false;
40
+
41
+ for (let i = 0; i < tmpText.length; i++)
42
+ {
43
+ const tmpChar = tmpText[i];
44
+ if (tmpInQuotes)
45
+ {
46
+ if (tmpChar === '"')
47
+ {
48
+ if (tmpText[i + 1] === '"') { tmpField += '"'; i++; } // escaped doubled quote
49
+ else { tmpInQuotes = false; }
50
+ }
51
+ else { tmpField += tmpChar; }
52
+ }
53
+ else if (tmpChar === '"')
54
+ {
55
+ tmpInQuotes = true;
56
+ tmpRowHasContent = true;
57
+ }
58
+ else if (tmpChar === pDelimiter)
59
+ {
60
+ tmpRow.push(tmpField);
61
+ tmpField = '';
62
+ tmpRowHasContent = true;
63
+ }
64
+ else if (tmpChar === '\n')
65
+ {
66
+ tmpRow.push(tmpField);
67
+ // Skip wholly-empty lines (a trailing newline shouldn't yield a blank record).
68
+ if (tmpRowHasContent || tmpRow.length > 1 || tmpRow[0] !== '') { tmpRows.push(tmpRow); }
69
+ tmpRow = [];
70
+ tmpField = '';
71
+ tmpRowHasContent = false;
72
+ }
73
+ else
74
+ {
75
+ tmpField += tmpChar;
76
+ tmpRowHasContent = true;
77
+ }
78
+ }
79
+ // Flush the final field/row if the file didn't end on a newline.
80
+ if (tmpField !== '' || tmpRow.length > 0)
81
+ {
82
+ tmpRow.push(tmpField);
83
+ if (tmpRowHasContent || tmpRow.length > 1 || tmpRow[0] !== '') { tmpRows.push(tmpRow); }
84
+ }
85
+ return tmpRows;
86
+ }
87
+
88
+ /** @param {number} pCount @return {Array<string>} Generated column names (Column1, Column2, …). */
89
+ _generatedHeader(pCount)
90
+ {
91
+ const tmpHeader = [];
92
+ for (let i = 0; i < pCount; i++) { tmpHeader.push(`Column${i + 1}`); }
93
+ return tmpHeader;
94
+ }
95
+
96
+ detect(pFileHandle, pParseConfig)
97
+ {
98
+ const tmpSettings = this._settings(pParseConfig);
99
+ return this._readText(pFileHandle).then((pText) =>
100
+ {
101
+ const tmpRows = this.parseDelimited(pText, tmpSettings.Delimiter);
102
+ if (tmpRows.length === 0) { return { Columns: [], SampleRows: [], RowCountEstimate: 0 }; }
103
+ const tmpHeader = tmpSettings.HasHeader ? tmpRows[0].map((pCell) => String(pCell).trim()) : this._generatedHeader(tmpRows[0].length);
104
+ const tmpDataRows = tmpSettings.HasHeader ? tmpRows.slice(1) : tmpRows;
105
+ return this._buildDetection(tmpHeader, tmpDataRows, tmpSettings.SampleRowLimit);
106
+ });
107
+ }
108
+
109
+ parse(pFileHandle, pParseConfig)
110
+ {
111
+ const tmpSettings = this._settings(pParseConfig);
112
+ return this._readText(pFileHandle).then((pText) =>
113
+ {
114
+ const tmpRows = this.parseDelimited(pText, tmpSettings.Delimiter);
115
+ if (tmpRows.length === 0) { return []; }
116
+ const tmpHeader = tmpSettings.HasHeader ? tmpRows[0].map((pCell) => String(pCell).trim()) : this._generatedHeader(tmpRows[0].length);
117
+ const tmpDataRows = tmpSettings.HasHeader ? tmpRows.slice(1) : tmpRows;
118
+ return tmpDataRows.map((pRow) =>
119
+ {
120
+ const tmpObject = {};
121
+ for (let c = 0; c < tmpHeader.length; c++) { tmpObject[tmpHeader[c]] = (pRow[c] !== undefined) ? pRow[c] : ''; }
122
+ return tmpObject;
123
+ });
124
+ });
125
+ }
126
+ }
127
+
128
+ module.exports = DataImportParserDelimited;
@@ -0,0 +1,87 @@
1
+ // DataImport-Parser-FixedWidth — fixed-width / column-position file parser (hand-rolled, no dependency).
2
+ //
3
+ // Fixed-width files have no reliable auto-detect, so detect() with NO column spec returns the raw
4
+ // lines (so the validate UI can show a monospaced ruler for the user to define boundaries). Once
5
+ // ParseConfig.FixedWidth.Columns is set (each { Name, Start, End } — End exclusive, 0-based), detect()
6
+ // and parse() slice + trim each line into fields.
7
+
8
+ const libParserProvider = require('./DataImport-ParserProvider.js');
9
+
10
+ class DataImportParserFixedWidth extends libParserProvider
11
+ {
12
+ get Kind()
13
+ {
14
+ return this.options.Kind || 'fixedwidth';
15
+ }
16
+
17
+ /** @param {Record<string, any>} pParseConfig @return {{Columns:Array<any>, HasHeader:boolean, SampleRowLimit:number}} */
18
+ _settings(pParseConfig)
19
+ {
20
+ const tmpFixedConfig = (pParseConfig && pParseConfig.FixedWidth) || {};
21
+ return {
22
+ Columns: Array.isArray(tmpFixedConfig.Columns) ? tmpFixedConfig.Columns : [],
23
+ HasHeader: !!tmpFixedConfig.HasHeader,
24
+ SampleRowLimit: (pParseConfig && pParseConfig.SampleRowLimit) || 25,
25
+ };
26
+ }
27
+
28
+ /** @param {string} pText @return {Array<string>} Non-empty lines. */
29
+ _lines(pText)
30
+ {
31
+ return (pText || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n').filter((pLine) => pLine.length > 0);
32
+ }
33
+
34
+ /** Slice one line into a cell array per the column spec. @param {string} pLine @param {Array<any>} pColumns @return {Array<string>} */
35
+ _sliceLine(pLine, pColumns)
36
+ {
37
+ return pColumns.map((pColumn) =>
38
+ {
39
+ const tmpStart = (typeof pColumn.Start === 'number') ? pColumn.Start : 0;
40
+ const tmpEnd = (typeof pColumn.End === 'number') ? pColumn.End : pLine.length;
41
+ return pLine.substring(tmpStart, tmpEnd).trim();
42
+ });
43
+ }
44
+
45
+ detect(pFileHandle, pParseConfig)
46
+ {
47
+ const tmpSettings = this._settings(pParseConfig);
48
+ return this._readText(pFileHandle).then((pText) =>
49
+ {
50
+ const tmpLines = this._lines(pText);
51
+ // No column spec yet — hand the raw lines back so the UI can render a boundary ruler.
52
+ if (tmpSettings.Columns.length === 0)
53
+ {
54
+ return { Columns: [], SampleRows: [], RowCountEstimate: tmpLines.length, RawLines: tmpLines.slice(0, tmpSettings.SampleRowLimit) };
55
+ }
56
+ const tmpHeader = tmpSettings.Columns.map((pColumn) => pColumn.Name);
57
+ const tmpDataLines = tmpSettings.HasHeader ? tmpLines.slice(1) : tmpLines;
58
+ const tmpDataRows = tmpDataLines.map((pLine) => this._sliceLine(pLine, tmpSettings.Columns));
59
+ const tmpDetection = this._buildDetection(tmpHeader, tmpDataRows, tmpSettings.SampleRowLimit);
60
+ tmpDetection.RawLines = tmpLines.slice(0, tmpSettings.SampleRowLimit);
61
+ return tmpDetection;
62
+ });
63
+ }
64
+
65
+ parse(pFileHandle, pParseConfig)
66
+ {
67
+ const tmpSettings = this._settings(pParseConfig);
68
+ if (tmpSettings.Columns.length === 0)
69
+ {
70
+ return Promise.reject(new Error('pict-section-dataimport: fixed-width parse requires a column spec (ParseConfig.FixedWidth.Columns).'));
71
+ }
72
+ return this._readText(pFileHandle).then((pText) =>
73
+ {
74
+ const tmpHeader = tmpSettings.Columns.map((pColumn) => pColumn.Name);
75
+ const tmpDataLines = tmpSettings.HasHeader ? this._lines(pText).slice(1) : this._lines(pText);
76
+ return tmpDataLines.map((pLine) =>
77
+ {
78
+ const tmpCells = this._sliceLine(pLine, tmpSettings.Columns);
79
+ const tmpObject = {};
80
+ for (let c = 0; c < tmpHeader.length; c++) { tmpObject[tmpHeader[c]] = tmpCells[c]; }
81
+ return tmpObject;
82
+ });
83
+ });
84
+ }
85
+ }
86
+
87
+ module.exports = DataImportParserFixedWidth;
@@ -0,0 +1,81 @@
1
+ // DataImport-Parser-Xlsx — Excel parser, wrapping SheetJS (xlsx). LAZY-required so the ~1MB library
2
+ // is only pulled into the bundle/process when xlsx import is actually used; xlsx is an OPTIONAL peer
3
+ // dependency (the default AllowedFileTypes can omit 'xlsx' entirely). Reads the first worksheet.
4
+
5
+ const libParserProvider = require('./DataImport-ParserProvider.js');
6
+
7
+ class DataImportParserXlsx extends libParserProvider
8
+ {
9
+ get Kind()
10
+ {
11
+ return this.options.Kind || 'xlsx';
12
+ }
13
+
14
+ /** @return {any} The xlsx library, or throws a friendly error if it isn't installed. */
15
+ _xlsx()
16
+ {
17
+ if (this._xlsxLib) { return this._xlsxLib; }
18
+ try { this._xlsxLib = require('xlsx'); }
19
+ catch (pError) { throw new Error('pict-section-dataimport: xlsx import needs the optional "xlsx" (SheetJS) dependency installed.'); }
20
+ return this._xlsxLib;
21
+ }
22
+
23
+ /** @param {Record<string, any>} pParseConfig @return {Promise<Array<Array<any>>>} The sheet as array-of-arrays. */
24
+ _readSheetMatrix(pFileHandle, pParseConfig)
25
+ {
26
+ const tmpXLSX = this._xlsx();
27
+ return this._readArrayBuffer(pFileHandle).then((pBuffer) =>
28
+ {
29
+ const tmpWorkbook = tmpXLSX.read(pBuffer, { type: 'array' });
30
+ const tmpSheetName = (pParseConfig && pParseConfig.Xlsx && pParseConfig.Xlsx.SheetName) || tmpWorkbook.SheetNames[0];
31
+ const tmpSheet = tmpWorkbook.Sheets[tmpSheetName];
32
+ if (!tmpSheet) { return []; }
33
+ // header:1 → array-of-arrays; blank cells become '' so columns line up.
34
+ return tmpXLSX.utils.sheet_to_json(tmpSheet, { header: 1, defval: '', blankrows: false });
35
+ });
36
+ }
37
+
38
+ _settings(pParseConfig)
39
+ {
40
+ const tmpXlsxConfig = (pParseConfig && pParseConfig.Xlsx) || {};
41
+ return {
42
+ HasHeader: (tmpXlsxConfig.HasHeader !== false),
43
+ SampleRowLimit: (pParseConfig && pParseConfig.SampleRowLimit) || 25,
44
+ };
45
+ }
46
+
47
+ detect(pFileHandle, pParseConfig)
48
+ {
49
+ const tmpSettings = this._settings(pParseConfig);
50
+ return this._readSheetMatrix(pFileHandle, pParseConfig).then((pMatrix) =>
51
+ {
52
+ if (!pMatrix || pMatrix.length === 0) { return { Columns: [], SampleRows: [], RowCountEstimate: 0 }; }
53
+ const tmpHeader = tmpSettings.HasHeader
54
+ ? pMatrix[0].map((pCell) => String(pCell).trim())
55
+ : pMatrix[0].map((pCell, pIndex) => `Column${pIndex + 1}`);
56
+ const tmpDataRows = tmpSettings.HasHeader ? pMatrix.slice(1) : pMatrix;
57
+ return this._buildDetection(tmpHeader, tmpDataRows, tmpSettings.SampleRowLimit);
58
+ });
59
+ }
60
+
61
+ parse(pFileHandle, pParseConfig)
62
+ {
63
+ const tmpSettings = this._settings(pParseConfig);
64
+ return this._readSheetMatrix(pFileHandle, pParseConfig).then((pMatrix) =>
65
+ {
66
+ if (!pMatrix || pMatrix.length === 0) { return []; }
67
+ const tmpHeader = tmpSettings.HasHeader
68
+ ? pMatrix[0].map((pCell) => String(pCell).trim())
69
+ : pMatrix[0].map((pCell, pIndex) => `Column${pIndex + 1}`);
70
+ const tmpDataRows = tmpSettings.HasHeader ? pMatrix.slice(1) : pMatrix;
71
+ return tmpDataRows.map((pRow) =>
72
+ {
73
+ const tmpObject = {};
74
+ for (let c = 0; c < tmpHeader.length; c++) { tmpObject[tmpHeader[c]] = (pRow[c] !== undefined) ? pRow[c] : ''; }
75
+ return tmpObject;
76
+ });
77
+ });
78
+ }
79
+ }
80
+
81
+ module.exports = DataImportParserXlsx;
@@ -0,0 +1,123 @@
1
+ // DataImport-ParserProvider — the abstract file-parser seam.
2
+ //
3
+ // A parser turns an uploaded file handle (the pict-section-upload shape: { Name, Size, Type,
4
+ // getText(cb), getArrayBuffer(cb) }) into (a) a DETECTION result for the validate/map UI — detected
5
+ // columns + sample rows + an estimated row count — and (b) a full row stream for comprehension
6
+ // generation. Hosts can register custom parsers (e.g. EBCDIC, a bespoke binary) by extending this and
7
+ // registering the instance under a file kind. The module stays format-agnostic behind this seam.
8
+
9
+ class DataImportParserProvider
10
+ {
11
+ /**
12
+ * @param {any} pPict @param {Record<string, any>} pOptions
13
+ */
14
+ constructor(pPict, pOptions)
15
+ {
16
+ this.pict = pPict;
17
+ this.options = pOptions || {};
18
+ }
19
+
20
+ /** @return {string} The file kind this parser handles ('csv' | 'tsv' | 'fixedwidth' | 'xlsx' | custom). */
21
+ get Kind()
22
+ {
23
+ return this.options.Kind || 'base';
24
+ }
25
+
26
+ /**
27
+ * Inspect a file: return the detected columns, a sample of rows, and an estimated total row count.
28
+ * @param {Record<string, any>} pFileHandle @param {Record<string, any>} pParseConfig
29
+ * @return {Promise<{Columns:Array<any>, SampleRows:Array<any>, RowCountEstimate:number, RawLines?:Array<string>}>}
30
+ */
31
+ detect(pFileHandle, pParseConfig)
32
+ {
33
+ return Promise.reject(new Error(`pict-section-dataimport: parser [${this.Kind}] does not implement detect().`));
34
+ }
35
+
36
+ /**
37
+ * Parse the whole file into an array of row objects (keyed by column name).
38
+ * @param {Record<string, any>} pFileHandle @param {Record<string, any>} pParseConfig
39
+ * @return {Promise<Array<Record<string, any>>>}
40
+ */
41
+ parse(pFileHandle, pParseConfig)
42
+ {
43
+ return Promise.reject(new Error(`pict-section-dataimport: parser [${this.Kind}] does not implement parse().`));
44
+ }
45
+
46
+ /** Read a file handle's text content as a Promise. @param {Record<string, any>} pFileHandle @return {Promise<string>} */
47
+ _readText(pFileHandle)
48
+ {
49
+ return new Promise((resolve, reject) =>
50
+ {
51
+ if (!pFileHandle || typeof pFileHandle.getText !== 'function')
52
+ {
53
+ return reject(new Error('pict-section-dataimport: file handle has no getText().'));
54
+ }
55
+ pFileHandle.getText((pError, pText) => (pError ? reject(pError) : resolve(pText || '')));
56
+ });
57
+ }
58
+
59
+ /** Read a file handle's bytes as a Promise. @param {Record<string, any>} pFileHandle @return {Promise<ArrayBuffer>} */
60
+ _readArrayBuffer(pFileHandle)
61
+ {
62
+ return new Promise((resolve, reject) =>
63
+ {
64
+ if (!pFileHandle || typeof pFileHandle.getArrayBuffer !== 'function')
65
+ {
66
+ return reject(new Error('pict-section-dataimport: file handle has no getArrayBuffer().'));
67
+ }
68
+ pFileHandle.getArrayBuffer((pError, pBuffer) => (pError ? reject(pError) : resolve(pBuffer)));
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Cheap value-type inference for the detected-columns UI (presentational only — never coerces data).
74
+ * @param {Array<any>} pValues @return {string} 'integer' | 'number' | 'boolean' | 'date' | 'string'
75
+ */
76
+ _inferType(pValues)
77
+ {
78
+ const tmpSample = (pValues || []).filter((pValue) => pValue !== undefined && pValue !== null && String(pValue).trim() !== '');
79
+ if (tmpSample.length === 0) { return 'string'; }
80
+ let tmpAllInteger = true;
81
+ let tmpAllNumber = true;
82
+ let tmpAllBoolean = true;
83
+ let tmpAllDate = true;
84
+ for (let i = 0; i < tmpSample.length; i++)
85
+ {
86
+ const tmpValue = String(tmpSample[i]).trim();
87
+ if (!/^-?\d+$/.test(tmpValue)) { tmpAllInteger = false; }
88
+ if (!/^-?\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(tmpValue)) { tmpAllNumber = false; }
89
+ if (!/^(true|false|yes|no|0|1)$/i.test(tmpValue)) { tmpAllBoolean = false; }
90
+ if (isNaN(Date.parse(tmpValue))) { tmpAllDate = false; }
91
+ }
92
+ if (tmpAllInteger) { return 'integer'; }
93
+ if (tmpAllNumber) { return 'number'; }
94
+ if (tmpAllBoolean) { return 'boolean'; }
95
+ if (tmpAllDate) { return 'date'; }
96
+ return 'string';
97
+ }
98
+
99
+ /**
100
+ * Build the {Columns, SampleRows, RowCountEstimate} detection result from a header + data rows.
101
+ * @param {Array<string>} pHeader @param {Array<Array<any>>} pDataRows @param {number} pSampleLimit
102
+ * @return {{Columns:Array<any>, SampleRows:Array<any>, RowCountEstimate:number}}
103
+ */
104
+ _buildDetection(pHeader, pDataRows, pSampleLimit)
105
+ {
106
+ const tmpLimit = pSampleLimit || 25;
107
+ const tmpSampleRows = [];
108
+ for (let i = 0; i < Math.min(pDataRows.length, tmpLimit); i++)
109
+ {
110
+ const tmpRowObject = {};
111
+ for (let c = 0; c < pHeader.length; c++) { tmpRowObject[pHeader[c]] = pDataRows[i][c]; }
112
+ tmpSampleRows.push(tmpRowObject);
113
+ }
114
+ const tmpColumns = pHeader.map((pName, pIndex) =>
115
+ {
116
+ const tmpColumnValues = pDataRows.slice(0, tmpLimit).map((pRow) => pRow[pIndex]);
117
+ return { Index: pIndex, SourceName: pName, InferredType: this._inferType(tmpColumnValues), SampleValues: tmpColumnValues.slice(0, 3) };
118
+ });
119
+ return { Columns: tmpColumns, SampleRows: tmpSampleRows, RowCountEstimate: pDataRows.length };
120
+ }
121
+ }
122
+
123
+ module.exports = DataImportParserProvider;
@@ -0,0 +1,82 @@
1
+ // DataImport-PushTarget — the abstract push seam, plus the built-in Comprehension-POST adapter.
2
+ //
3
+ // push() takes the generated comprehension + a context (GUID prefixes, server URL, the entity Order,
4
+ // an onProgress hook) and lands it somewhere, returning { Success, EntitiesPushed, Message }. The
5
+ // POST adapter sends the whole comprehension to a /Comprehension/Push endpoint (the server owns the
6
+ // meadow-integration engine) — so this path needs no client-side EntityProvider. The direct
7
+ // per-record EntityProvider push adapter lives in Pict-Provider-DataImport-Meadow.js.
8
+
9
+ class DataImportPushTarget
10
+ {
11
+ constructor(pPict, pOptions)
12
+ {
13
+ this.pict = pPict;
14
+ this.options = pOptions || {};
15
+ }
16
+
17
+ /**
18
+ * @param {Record<string, any>} pComprehension @param {Record<string, any>} pContext
19
+ * @return {Promise<{Success:boolean, EntitiesPushed:Array<string>, Message:string}>}
20
+ */
21
+ push(pComprehension, pContext)
22
+ {
23
+ return Promise.reject(new Error('pict-section-dataimport: PushTarget does not implement push().'));
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Built-in adapter: POST the whole comprehension to a Comprehension/Push endpoint. The request body
29
+ * matches meadow-integration's Endpoint-ComprehensionPush contract:
30
+ * { Comprehension, GUIDPrefix, EntityGUIDPrefix, ServerURL } -> { Success, EntitiesPushed, Message }
31
+ * A host can inject a PostFunction (url, body) => Promise<responseJson> for tests / custom transport;
32
+ * otherwise it uses fetch, falling back to the pict EntityProvider rest client.
33
+ */
34
+ class DataImportPushTargetComprehension extends DataImportPushTarget
35
+ {
36
+ push(pComprehension, pContext)
37
+ {
38
+ const tmpContext = pContext || {};
39
+ const tmpURL = this.options.URL || tmpContext.ComprehensionPushURL || '/1.0/Comprehension/Push';
40
+ const tmpBody = {
41
+ Comprehension: pComprehension,
42
+ GUIDPrefix: tmpContext.GUIDPrefix,
43
+ EntityGUIDPrefix: tmpContext.EntityGUIDPrefix,
44
+ ServerURL: tmpContext.ServerURL,
45
+ };
46
+ const fPost = (typeof this.options.PostFunction === 'function') ? this.options.PostFunction : this._defaultPost.bind(this);
47
+ return Promise.resolve(fPost(tmpURL, tmpBody)).then((pResponse) =>
48
+ {
49
+ const tmpResponse = pResponse || {};
50
+ return {
51
+ Success: (tmpResponse.Success !== undefined) ? tmpResponse.Success : true,
52
+ EntitiesPushed: Array.isArray(tmpResponse.EntitiesPushed) ? tmpResponse.EntitiesPushed : Object.keys(pComprehension || {}),
53
+ Message: tmpResponse.Message || 'Comprehension posted.',
54
+ };
55
+ });
56
+ }
57
+
58
+ /** Default JSON POST: fetch when available, else the pict EntityProvider rest client. */
59
+ _defaultPost(pURL, pBody)
60
+ {
61
+ if (typeof fetch === 'function')
62
+ {
63
+ return fetch(pURL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(pBody) })
64
+ .then((pResult) => pResult.json());
65
+ }
66
+ if (this.pict && this.pict.EntityProvider && this.pict.EntityProvider.restClient && typeof this.pict.EntityProvider.restClient.postJSON === 'function')
67
+ {
68
+ return new Promise((resolve, reject) =>
69
+ {
70
+ this.pict.EntityProvider.restClient.postJSON({ url: pURL, body: pBody }, (pError, pResponse, pResponseBody) =>
71
+ {
72
+ if (pError) { return reject(pError); }
73
+ resolve(pResponseBody || (pResponse && pResponse.body) || {});
74
+ });
75
+ });
76
+ }
77
+ return Promise.reject(new Error('pict-section-dataimport: no fetch / rest client available to POST the comprehension.'));
78
+ }
79
+ }
80
+
81
+ module.exports = DataImportPushTarget;
82
+ module.exports.DataImportPushTargetComprehension = DataImportPushTargetComprehension;