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,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 };