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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +80 -0
- package/source/Pict-Section-DataImport-CSS.js +73 -0
- package/source/Pict-Section-DataImport.js +54 -0
- package/source/providers/Pict-Provider-DataImport-Meadow.js +192 -0
- package/source/providers/Pict-Provider-DataImport.js +149 -0
- package/source/seams/DataImport-Parser-Delimited.js +128 -0
- package/source/seams/DataImport-Parser-FixedWidth.js +87 -0
- package/source/seams/DataImport-Parser-Xlsx.js +81 -0
- package/source/seams/DataImport-ParserProvider.js +123 -0
- package/source/seams/DataImport-PushTarget.js +82 -0
- package/source/seams/DataImport-SchemaProvider.js +72 -0
- package/source/seams/DataImport-StateStore.js +123 -0
- package/source/services/DataImport-ComprehensionBuilder.js +123 -0
- package/source/services/DataImport-Session.js +101 -0
- package/source/views/PictView-DataImport-Wizard.js +657 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// DataImport-SchemaProvider — the abstract target-schema seam, plus the built-in config-schema
|
|
2
|
+
// adapter (a complex, non-Meadow schema supplied entirely by host configuration).
|
|
3
|
+
//
|
|
4
|
+
// getSchema() returns the target entities + fields the mapping step maps columns ONTO:
|
|
5
|
+
// { Entities: [ { Entity, GUIDName, Fields: [ { Name, Type, Size, Required, IsGUID, ForeignKeyEntity,
|
|
6
|
+
// FKGUIDTemplate } ] } ], Order: [ entityName, ... ] }
|
|
7
|
+
// The Meadow adapter (which fetches <URLPrefix><Entity>/Schema) lives in Pict-Provider-DataImport-Meadow.js
|
|
8
|
+
// so this file — and the whole core — stays free of any Meadow/EntityProvider dependency.
|
|
9
|
+
|
|
10
|
+
class DataImportSchemaProvider
|
|
11
|
+
{
|
|
12
|
+
constructor(pPict, pOptions)
|
|
13
|
+
{
|
|
14
|
+
this.pict = pPict;
|
|
15
|
+
this.options = pOptions || {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {Record<string, any>} pContext
|
|
20
|
+
* @return {Promise<{Entities:Array<any>, Order:Array<string>}>}
|
|
21
|
+
*/
|
|
22
|
+
getSchema(pContext)
|
|
23
|
+
{
|
|
24
|
+
return Promise.reject(new Error('pict-section-dataimport: SchemaProvider does not implement getSchema().'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Normalize a raw schema descriptor: default GUIDName, ensure Fields arrays + an Order. @param {Record<string, any>} pSchema */
|
|
28
|
+
_normalizeSchema(pSchema)
|
|
29
|
+
{
|
|
30
|
+
const tmpEntities = (pSchema && Array.isArray(pSchema.Entities)) ? pSchema.Entities : [];
|
|
31
|
+
const tmpNormalized = tmpEntities.map((pEntity) =>
|
|
32
|
+
{
|
|
33
|
+
return {
|
|
34
|
+
Entity: pEntity.Entity,
|
|
35
|
+
GUIDName: pEntity.GUIDName || `GUID${pEntity.Entity}`,
|
|
36
|
+
GUIDTemplateDefault: pEntity.GUIDTemplateDefault || '',
|
|
37
|
+
Fields: Array.isArray(pEntity.Fields) ? pEntity.Fields.map((pField) =>
|
|
38
|
+
{
|
|
39
|
+
return Object.assign({
|
|
40
|
+
Name: pField.Name,
|
|
41
|
+
Type: pField.Type || 'string',
|
|
42
|
+
Size: (typeof pField.Size === 'number') ? pField.Size : 0,
|
|
43
|
+
Required: !!pField.Required,
|
|
44
|
+
IsGUID: (pField.IsGUID !== undefined) ? !!pField.IsGUID : (pField.Name === (pEntity.GUIDName || `GUID${pEntity.Entity}`)),
|
|
45
|
+
ForeignKeyEntity: pField.ForeignKeyEntity || '',
|
|
46
|
+
FKGUIDTemplate: pField.FKGUIDTemplate || '',
|
|
47
|
+
}, pField);
|
|
48
|
+
}) : [],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
const tmpOrder = (pSchema && Array.isArray(pSchema.Order) && pSchema.Order.length > 0)
|
|
52
|
+
? pSchema.Order
|
|
53
|
+
: tmpNormalized.map((pEntity) => pEntity.Entity);
|
|
54
|
+
return { Entities: tmpNormalized, Order: tmpOrder };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Built-in adapter: the target schema is a descriptor passed directly in host config (the
|
|
60
|
+
* "complex REST schema defined by configuration" case). No network — just normalization.
|
|
61
|
+
*/
|
|
62
|
+
class DataImportSchemaProviderConfig extends DataImportSchemaProvider
|
|
63
|
+
{
|
|
64
|
+
getSchema()
|
|
65
|
+
{
|
|
66
|
+
const tmpSchema = this.options.Schema || { Entities: [] };
|
|
67
|
+
return Promise.resolve(this._normalizeSchema(tmpSchema));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = DataImportSchemaProvider;
|
|
72
|
+
module.exports.DataImportSchemaProviderConfig = DataImportSchemaProviderConfig;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// DataImport-StateStore — the abstract persistence seam, plus built-in memory + localStorage adapters.
|
|
2
|
+
//
|
|
3
|
+
// This is what lets a user reload a partially-mapped dataset: the wizard serializes its session (file
|
|
4
|
+
// ref, parse config, detected columns, the mapping) and save()s it; load()/list() bring it back. A
|
|
5
|
+
// host can supply a custom store (or the lighter PersistenceHook on the wizard config) to persist
|
|
6
|
+
// sessions on a server instead.
|
|
7
|
+
|
|
8
|
+
class DataImportStateStore
|
|
9
|
+
{
|
|
10
|
+
constructor(pPict, pOptions)
|
|
11
|
+
{
|
|
12
|
+
this.pict = pPict;
|
|
13
|
+
this.options = pOptions || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @param {string} pSessionId @param {Record<string, any>} pSerializedSession @return {Promise<void>} */
|
|
17
|
+
save(pSessionId, pSerializedSession) { return Promise.reject(new Error('not implemented')); }
|
|
18
|
+
/** @param {string} pSessionId @return {Promise<Record<string, any>|null>} */
|
|
19
|
+
load(pSessionId) { return Promise.reject(new Error('not implemented')); }
|
|
20
|
+
/** @return {Promise<Array<{SessionId:string, Title:string, UpdatedAt:number}>>} */
|
|
21
|
+
list() { return Promise.reject(new Error('not implemented')); }
|
|
22
|
+
/** @param {string} pSessionId @return {Promise<void>} */
|
|
23
|
+
remove(pSessionId) { return Promise.reject(new Error('not implemented')); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Ephemeral in-process store (fine for tests + transient sessions). */
|
|
27
|
+
class DataImportStateStoreMemory extends DataImportStateStore
|
|
28
|
+
{
|
|
29
|
+
constructor(pPict, pOptions)
|
|
30
|
+
{
|
|
31
|
+
super(pPict, pOptions);
|
|
32
|
+
this._map = {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
save(pSessionId, pSerializedSession)
|
|
36
|
+
{
|
|
37
|
+
this._map[pSessionId] = JSON.parse(JSON.stringify(pSerializedSession));
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
load(pSessionId)
|
|
41
|
+
{
|
|
42
|
+
return Promise.resolve(this._map[pSessionId] ? JSON.parse(JSON.stringify(this._map[pSessionId])) : null);
|
|
43
|
+
}
|
|
44
|
+
list()
|
|
45
|
+
{
|
|
46
|
+
return Promise.resolve(Object.keys(this._map).map((pKey) =>
|
|
47
|
+
{
|
|
48
|
+
const tmpSession = this._map[pKey] || {};
|
|
49
|
+
return { SessionId: pKey, Title: tmpSession.Title || pKey, UpdatedAt: tmpSession.UpdatedAt || 0 };
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
remove(pSessionId)
|
|
53
|
+
{
|
|
54
|
+
delete this._map[pSessionId];
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Browser localStorage store, keyed by an options.KeyPrefix (default 'dataimport:'). */
|
|
60
|
+
class DataImportStateStoreLocal extends DataImportStateStore
|
|
61
|
+
{
|
|
62
|
+
get _prefix() { return this.options.KeyPrefix || 'dataimport:'; }
|
|
63
|
+
|
|
64
|
+
_storage()
|
|
65
|
+
{
|
|
66
|
+
if (typeof localStorage === 'undefined') { throw new Error('pict-section-dataimport: localStorage is not available in this environment.'); }
|
|
67
|
+
return localStorage;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
save(pSessionId, pSerializedSession)
|
|
71
|
+
{
|
|
72
|
+
try { this._storage().setItem(this._prefix + pSessionId, JSON.stringify(pSerializedSession)); return Promise.resolve(); }
|
|
73
|
+
catch (pError) { return Promise.reject(pError); }
|
|
74
|
+
}
|
|
75
|
+
load(pSessionId)
|
|
76
|
+
{
|
|
77
|
+
try
|
|
78
|
+
{
|
|
79
|
+
const tmpRaw = this._storage().getItem(this._prefix + pSessionId);
|
|
80
|
+
return Promise.resolve(tmpRaw ? JSON.parse(tmpRaw) : null);
|
|
81
|
+
}
|
|
82
|
+
catch (pError) { return Promise.reject(pError); }
|
|
83
|
+
}
|
|
84
|
+
list()
|
|
85
|
+
{
|
|
86
|
+
try
|
|
87
|
+
{
|
|
88
|
+
const tmpStorage = this._storage();
|
|
89
|
+
const tmpSessions = [];
|
|
90
|
+
for (let i = 0; i < tmpStorage.length; i++)
|
|
91
|
+
{
|
|
92
|
+
const tmpKey = tmpStorage.key(i);
|
|
93
|
+
if (tmpKey && tmpKey.indexOf(this._prefix) === 0)
|
|
94
|
+
{
|
|
95
|
+
let tmpSession = {};
|
|
96
|
+
try { tmpSession = JSON.parse(tmpStorage.getItem(tmpKey)) || {}; } catch (pError) { tmpSession = {}; }
|
|
97
|
+
tmpSessions.push({ SessionId: tmpKey.slice(this._prefix.length), Title: tmpSession.Title || tmpKey, UpdatedAt: tmpSession.UpdatedAt || 0 });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return Promise.resolve(tmpSessions);
|
|
101
|
+
}
|
|
102
|
+
catch (pError) { return Promise.reject(pError); }
|
|
103
|
+
}
|
|
104
|
+
remove(pSessionId)
|
|
105
|
+
{
|
|
106
|
+
try { this._storage().removeItem(this._prefix + pSessionId); return Promise.resolve(); }
|
|
107
|
+
catch (pError) { return Promise.reject(pError); }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Adapter that wraps a host-supplied PersistenceHook { save, load, list, remove } (e.g. server persistence). */
|
|
112
|
+
class DataImportStateStoreHook extends DataImportStateStore
|
|
113
|
+
{
|
|
114
|
+
save(pSessionId, pSerializedSession) { return Promise.resolve(this.options.Hook.save(pSessionId, pSerializedSession)); }
|
|
115
|
+
load(pSessionId) { return Promise.resolve(this.options.Hook.load(pSessionId)); }
|
|
116
|
+
list() { return Promise.resolve(this.options.Hook.list ? this.options.Hook.list() : []); }
|
|
117
|
+
remove(pSessionId) { return Promise.resolve(this.options.Hook.remove ? this.options.Hook.remove(pSessionId) : undefined); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = DataImportStateStore;
|
|
121
|
+
module.exports.DataImportStateStoreMemory = DataImportStateStoreMemory;
|
|
122
|
+
module.exports.DataImportStateStoreLocal = DataImportStateStoreLocal;
|
|
123
|
+
module.exports.DataImportStateStoreHook = DataImportStateStoreHook;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// DataImport-ComprehensionBuilder — turns a mapping + parsed rows into a Comprehension, using
|
|
2
|
+
// meadow-integration's browser-safe transform engine (reached via the clean Meadow-Integration-Engine
|
|
3
|
+
// re-export, which pulls in NO server-only code — verified). This is the only client-side piece that
|
|
4
|
+
// needs the engine, and it's the heart of the module: it runs the canonical transformRecord pipeline
|
|
5
|
+
// (Solvers + MultipleGUIDUniqueness fan-out) so the comprehensions are byte-identical to what the
|
|
6
|
+
// meadow-integration CLI would produce from the same mapping files.
|
|
7
|
+
|
|
8
|
+
const libEngine = require('meadow-integration/source/Meadow-Integration-Engine.js');
|
|
9
|
+
|
|
10
|
+
class DataImportComprehensionBuilder
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} pPict
|
|
14
|
+
* @param {any} [pTransform] - inject a transform engine (tests); else one is built from the engine export.
|
|
15
|
+
*/
|
|
16
|
+
constructor(pPict, pTransform)
|
|
17
|
+
{
|
|
18
|
+
this.pict = pPict;
|
|
19
|
+
this.transform = pTransform || new libEngine.MeadowIntegrationTabularTransform(pPict);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a comprehension from a multi-entity mapping + parsed rows.
|
|
24
|
+
* @param {Record<string, any>} pMapping - { Order:[entityName,…], Entities:{ <name>: <entityMapping> } }
|
|
25
|
+
* where each entityMapping is the meadow mapping shape: { Entity, GUIDName?, GUIDTemplate, Mappings,
|
|
26
|
+
* Solvers?, MultipleGUIDUniqueness?, ManyfestAddresses? }.
|
|
27
|
+
* @param {Array<Record<string, any>>} pRows
|
|
28
|
+
* @return {{Comprehension:Record<string, any>, Report:Record<string, any>}}
|
|
29
|
+
*/
|
|
30
|
+
build(pMapping, pRows)
|
|
31
|
+
{
|
|
32
|
+
const tmpRows = Array.isArray(pRows) ? pRows : [];
|
|
33
|
+
const tmpEntities = (pMapping && pMapping.Entities) || {};
|
|
34
|
+
const tmpOrder = (pMapping && Array.isArray(pMapping.Order) && pMapping.Order.length > 0)
|
|
35
|
+
? pMapping.Order
|
|
36
|
+
: Object.keys(tmpEntities);
|
|
37
|
+
|
|
38
|
+
const tmpComprehension = {};
|
|
39
|
+
const tmpReport = { ParsedRowCount: tmpRows.length, EntityCounts: {}, BadRecords: [] };
|
|
40
|
+
|
|
41
|
+
// Process entities IN ORDER so the comprehension's keys come out referenced-before-referrer
|
|
42
|
+
// (the push paths do no topo-sort — emission order is load-bearing for FK resolution).
|
|
43
|
+
for (let e = 0; e < tmpOrder.length; e++)
|
|
44
|
+
{
|
|
45
|
+
const tmpEntityKey = tmpOrder[e];
|
|
46
|
+
const tmpEntityMapping = tmpEntities[tmpEntityKey];
|
|
47
|
+
if (!tmpEntityMapping || !tmpEntityMapping.Entity) { continue; }
|
|
48
|
+
|
|
49
|
+
const tmpOutcome = this.transform.newMappingOutcomeObject();
|
|
50
|
+
// Set a truthy ImplicitConfiguration so the engine's implicit-generation branch (which would
|
|
51
|
+
// otherwise dereference an undefined variable) never runs; our explicit mapping is authoritative.
|
|
52
|
+
tmpOutcome.ImplicitConfiguration = {};
|
|
53
|
+
tmpOutcome.ExplicitConfiguration = tmpEntityMapping;
|
|
54
|
+
this.transform.initializeMappingOutcomeObject(tmpOutcome);
|
|
55
|
+
|
|
56
|
+
for (let r = 0; r < tmpRows.length; r++)
|
|
57
|
+
{
|
|
58
|
+
try { this.transform.transformRecord(tmpRows[r], tmpOutcome); }
|
|
59
|
+
catch (pError)
|
|
60
|
+
{
|
|
61
|
+
tmpReport.BadRecords.push({ Entity: tmpEntityMapping.Entity, RowIndex: r, Reason: pError.message || String(pError), Row: tmpRows[r] });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tmpEntityName = tmpEntityMapping.Entity;
|
|
66
|
+
tmpComprehension[tmpEntityName] = tmpOutcome.Comprehension[tmpEntityName] || {};
|
|
67
|
+
tmpReport.EntityCounts[tmpEntityName] = Object.keys(tmpComprehension[tmpEntityName]).length;
|
|
68
|
+
(tmpOutcome.BadRecords || []).forEach((pBadRow) =>
|
|
69
|
+
{
|
|
70
|
+
tmpReport.BadRecords.push({ Entity: tmpEntityName, Reason: 'No valid GUID generated for record', Row: pBadRow });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { Comprehension: tmpComprehension, Report: tmpReport };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pre-flight FK-ordering check: flag any `GUID<Other>` mapping field whose referenced entity appears
|
|
79
|
+
* LATER than (or not in) the Order — those FKs would silently resolve to NULL/0 at push time.
|
|
80
|
+
* @param {Record<string, any>} pMapping
|
|
81
|
+
* @return {Array<{Entity:string, Field:string, ReferencedEntity:string, Message:string}>}
|
|
82
|
+
*/
|
|
83
|
+
validateForeignKeyOrder(pMapping)
|
|
84
|
+
{
|
|
85
|
+
const tmpEntities = (pMapping && pMapping.Entities) || {};
|
|
86
|
+
const tmpOrder = (pMapping && Array.isArray(pMapping.Order) && pMapping.Order.length > 0)
|
|
87
|
+
? pMapping.Order
|
|
88
|
+
: Object.keys(tmpEntities);
|
|
89
|
+
const tmpEntityNames = tmpOrder
|
|
90
|
+
.map((pKey) => tmpEntities[pKey] && tmpEntities[pKey].Entity)
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
const tmpIndexByEntity = {};
|
|
93
|
+
tmpEntityNames.forEach((pName, pIndex) => { tmpIndexByEntity[pName] = pIndex; });
|
|
94
|
+
|
|
95
|
+
const tmpWarnings = [];
|
|
96
|
+
tmpOrder.forEach((pKey, pIndex) =>
|
|
97
|
+
{
|
|
98
|
+
const tmpEntityMapping = tmpEntities[pKey];
|
|
99
|
+
if (!tmpEntityMapping || !tmpEntityMapping.Mappings) { return; }
|
|
100
|
+
const tmpGUIDName = tmpEntityMapping.GUIDName || `GUID${tmpEntityMapping.Entity}`;
|
|
101
|
+
Object.keys(tmpEntityMapping.Mappings).forEach((pFieldName) =>
|
|
102
|
+
{
|
|
103
|
+
if (pFieldName === tmpGUIDName) { return; } // the entity's own GUID, not an FK
|
|
104
|
+
if (pFieldName.indexOf('GUID') !== 0) { return; } // only GUID<Other> fields are FKs
|
|
105
|
+
const tmpReferenced = pFieldName.slice(4);
|
|
106
|
+
if (!tmpReferenced || tmpReferenced === tmpEntityMapping.Entity) { return; }
|
|
107
|
+
if (!(tmpReferenced in tmpIndexByEntity)) { return; } // references something outside this import — fine
|
|
108
|
+
if (tmpIndexByEntity[tmpReferenced] >= pIndex)
|
|
109
|
+
{
|
|
110
|
+
tmpWarnings.push({
|
|
111
|
+
Entity: tmpEntityMapping.Entity,
|
|
112
|
+
Field: pFieldName,
|
|
113
|
+
ReferencedEntity: tmpReferenced,
|
|
114
|
+
Message: `"${tmpEntityMapping.Entity}.${pFieldName}" references "${tmpReferenced}", which is generated after it — reorder so "${tmpReferenced}" comes first, or its foreign key will be unresolved.`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return tmpWarnings;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = DataImportComprehensionBuilder;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// DataImport-Session — the per-import-session state shape + (de)serialization helpers.
|
|
2
|
+
//
|
|
3
|
+
// One session lives at AppData.DataImport.Sessions[<wizardHash>] and is fully JSON-serializable: the
|
|
4
|
+
// live uploaded file handle is referenced indirectly (File.Ref), never embedded, so a StateStore can
|
|
5
|
+
// persist a partially-mapped session and reload it later. `Mapping` is the durable artifact;
|
|
6
|
+
// `Comprehension` is a recomputable cache (re-derivable from Mapping + re-parsed rows).
|
|
7
|
+
|
|
8
|
+
/** @return {number} A timestamp (plain module code — Date.now is fine here). */
|
|
9
|
+
const now = () => Date.now();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a fresh import session.
|
|
13
|
+
* @param {string} pSessionId @param {Record<string, any>} pConfig
|
|
14
|
+
* @return {Record<string, any>}
|
|
15
|
+
*/
|
|
16
|
+
const newImportSession = (pSessionId, pConfig) =>
|
|
17
|
+
{
|
|
18
|
+
const tmpConfig = pConfig || {};
|
|
19
|
+
const tmpTimestamp = now();
|
|
20
|
+
return {
|
|
21
|
+
SessionId: pSessionId,
|
|
22
|
+
Title: tmpConfig.Title || pSessionId,
|
|
23
|
+
SchemaVersion: 1,
|
|
24
|
+
CreatedAt: tmpTimestamp,
|
|
25
|
+
UpdatedAt: tmpTimestamp,
|
|
26
|
+
|
|
27
|
+
CurrentStep: 'upload', // upload | detect | mapping | generate | push
|
|
28
|
+
StepStatus: { upload: 'active', detect: 'pending', mapping: 'pending', generate: 'pending', push: 'pending' },
|
|
29
|
+
|
|
30
|
+
// File REF (not the live handle/bytes): { Ref, Name, Size, Type, Kind, Stored, StorageRef }.
|
|
31
|
+
File: null,
|
|
32
|
+
|
|
33
|
+
ParseConfig:
|
|
34
|
+
{
|
|
35
|
+
Kind: null, // csv | tsv | fixedwidth | xlsx
|
|
36
|
+
Delimited: { Delimiter: ',', HasHeader: true },
|
|
37
|
+
FixedWidth: { Columns: [], HasHeader: false },
|
|
38
|
+
Xlsx: { HasHeader: true },
|
|
39
|
+
SampleRowLimit: 25,
|
|
40
|
+
},
|
|
41
|
+
DetectedColumns: [],
|
|
42
|
+
SampleRows: [],
|
|
43
|
+
RawLines: [], // fixed-width ruler source (when no column spec yet)
|
|
44
|
+
RowCountEstimate: 0,
|
|
45
|
+
|
|
46
|
+
Mapping:
|
|
47
|
+
{
|
|
48
|
+
TargetSchemaSource: tmpConfig.SchemaSource || 'meadow',
|
|
49
|
+
Order: [],
|
|
50
|
+
Entities: {}, // <entityName>: { Entity, GUIDName, GUIDTemplate, Mappings, Solvers, MultipleGUIDUniqueness, _ColumnBindings }
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
Comprehension: null, // recomputable cache: { <Entity>: { <guid>: record } }
|
|
54
|
+
GenerationReport: null, // { ParsedRowCount, EntityCounts, BadRecords }
|
|
55
|
+
|
|
56
|
+
Push:
|
|
57
|
+
{
|
|
58
|
+
Mode: tmpConfig.PushMode || 'entityprovider',
|
|
59
|
+
Status: 'idle', // idle | running | complete | error
|
|
60
|
+
Progress: { Total: 0, Pushed: 0 },
|
|
61
|
+
Result: null,
|
|
62
|
+
Error: null,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A JSON-safe snapshot for persistence. The Comprehension can be huge + is recomputable, so it is
|
|
69
|
+
* dropped by default (pass pIncludeComprehension to keep it).
|
|
70
|
+
* @param {Record<string, any>} pSession @param {boolean} [pIncludeComprehension]
|
|
71
|
+
* @return {Record<string, any>}
|
|
72
|
+
*/
|
|
73
|
+
const serialize = (pSession, pIncludeComprehension) =>
|
|
74
|
+
{
|
|
75
|
+
const tmpSnapshot = JSON.parse(JSON.stringify(pSession || {}));
|
|
76
|
+
if (!pIncludeComprehension) { tmpSnapshot.Comprehension = null; }
|
|
77
|
+
tmpSnapshot.UpdatedAt = now();
|
|
78
|
+
return tmpSnapshot;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rebuild a session object from a snapshot (filling any new fields a newer SchemaVersion added).
|
|
83
|
+
* @param {Record<string, any>} pSnapshot
|
|
84
|
+
* @return {Record<string, any>}
|
|
85
|
+
*/
|
|
86
|
+
const hydrate = (pSnapshot) =>
|
|
87
|
+
{
|
|
88
|
+
const tmpSnapshot = pSnapshot || {};
|
|
89
|
+
const tmpBase = newImportSession(tmpSnapshot.SessionId || 'import', {});
|
|
90
|
+
// Deep-merge the saved fields over the fresh base (saved values win; new base fields survive).
|
|
91
|
+
return Object.assign(tmpBase, tmpSnapshot, {
|
|
92
|
+
ParseConfig: Object.assign({}, tmpBase.ParseConfig, tmpSnapshot.ParseConfig),
|
|
93
|
+
Mapping: Object.assign({}, tmpBase.Mapping, tmpSnapshot.Mapping),
|
|
94
|
+
Push: Object.assign({}, tmpBase.Push, tmpSnapshot.Push),
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Mark a session's UpdatedAt. @param {Record<string, any>} pSession */
|
|
99
|
+
const touch = (pSession) => { if (pSession) { pSession.UpdatedAt = now(); } return pSession; };
|
|
100
|
+
|
|
101
|
+
module.exports = { newImportSession, serialize, hydrate, touch };
|