retold-facto 0.0.4 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-facto",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Data warehouse and knowledge graph storage for the Retold ecosystem.",
5
5
  "main": "source/Retold-Facto.js",
6
6
  "bin": {
@@ -54,9 +54,9 @@
54
54
  },
55
55
  "homepage": "https://github.com/stevenvelozo/retold-facto",
56
56
  "devDependencies": {
57
- "chai": "^4.5.0",
57
+ "chai": "^6.2.2",
58
58
  "meadow-connection-sqlite": "^1.0.18",
59
- "puppeteer": "^24.8.0",
59
+ "puppeteer": "^24.40.0",
60
60
  "quackage": "^1.0.65",
61
61
  "stricture": "^4.0.2",
62
62
  "supertest": "^7.2.2"
@@ -64,21 +64,21 @@
64
64
  "dependencies": {
65
65
  "@codemirror/lang-markdown": "^6.5.0",
66
66
  "@codemirror/state": "^6.6.0",
67
- "@codemirror/view": "^6.40.0",
67
+ "@codemirror/view": "^6.41.0",
68
68
  "bibliograph": "^0.1.4",
69
69
  "codemirror": "^6.0.2",
70
70
  "fable": "^3.1.67",
71
71
  "fable-serviceproviderbase": "^3.0.19",
72
- "fast-xml-parser": "^5.5.8",
72
+ "fast-xml-parser": "^5.5.10",
73
73
  "meadow": "^2.0.33",
74
74
  "meadow-connection-manager": "^1.0.0",
75
75
  "meadow-connection-mysql": "^1.0.14",
76
76
  "meadow-endpoints": "^4.0.14",
77
- "meadow-integration": "^1.0.21",
77
+ "meadow-integration": "^1.0.24",
78
78
  "orator": "^6.0.4",
79
- "orator-serviceserver-restify": "^2.0.9",
79
+ "orator-serviceserver-restify": "^2.0.10",
80
80
  "orator-static-server": "^2.0.4",
81
- "pict": "^1.0.359",
81
+ "pict": "^1.0.361",
82
82
  "pict-router": "^1.0.9",
83
83
  "pict-section-flow": "^0.0.17",
84
84
  "pict-section-modal": "^0.0.1",
@@ -1794,50 +1794,32 @@ class RetoldFactoProjectionEngine extends libFableServiceProviderBase
1794
1794
 
1795
1795
  tmpLog.push(`[${new Date().toISOString()}] Pushing ${tmpRecordGUIDs.length} records via IntegrationAdapter to ${tmpServerURL}${tmpTargetEntityName}/Upsert`);
1796
1796
 
1797
- // Marshal and push records through the REST API
1798
- tmpAdapter.marshalSourceRecords(
1799
- (pMarshalError) =>
1797
+ // Use integrateRecords which fetches the schema first,
1798
+ // then marshals and pushes records in sequence.
1799
+ tmpAdapter.integrateRecords(
1800
+ (pIntegrateError) =>
1800
1801
  {
1801
- if (pMarshalError)
1802
+ let tmpMarshaledCount = Object.keys(tmpAdapter._MarshaledRecords).length;
1803
+
1804
+ if (pIntegrateError)
1802
1805
  {
1803
- tmpLog.push(`[${new Date().toISOString()}] Marshal error: ${pMarshalError.message}`);
1804
- pResponse.send(
1805
- {
1806
- Error: `Marshal error: ${pMarshalError.message}`,
1807
- RecordsProcessed: pRecords.length,
1808
- RecordsTransformed: tmpRecordGUIDs.length,
1809
- StagingFile: tmpStagingFile,
1810
- Log: tmpLog.join('\n')
1811
- });
1812
- return fNext();
1806
+ tmpLog.push(`[${new Date().toISOString()}] Integration error: ${pIntegrateError.message}`);
1813
1807
  }
1814
1808
 
1815
- let tmpMarshaledCount = Object.keys(tmpAdapter._MarshaledRecords).length;
1816
- tmpLog.push(`[${new Date().toISOString()}] Marshaled ${tmpMarshaledCount} records; pushing to server...`);
1809
+ tmpLog.push(`[${new Date().toISOString()}] Import complete: ${pRecords.length} source records, ${tmpRecordGUIDs.length} unique, ${tmpMarshaledCount} upserted`);
1817
1810
 
1818
- tmpAdapter.pushRecordsToServer(
1819
- (pPushError) =>
1820
- {
1821
- if (pPushError)
1822
- {
1823
- tmpLog.push(`[${new Date().toISOString()}] Push error: ${pPushError.message}`);
1824
- }
1825
-
1826
- tmpLog.push(`[${new Date().toISOString()}] Import complete: ${pRecords.length} source records, ${tmpRecordGUIDs.length} unique, ${tmpMarshaledCount} upserted`);
1827
-
1828
- pResponse.send(
1829
- {
1830
- Success: !pPushError,
1831
- RecordsProcessed: pRecords.length,
1832
- RecordsTransformed: tmpRecordGUIDs.length,
1833
- RecordsDeduplicated: tmpMappingOutcome.ParsedRowCount - tmpRecordGUIDs.length,
1834
- BadRecords: tmpMappingOutcome.BadRecords.length,
1835
- RecordsUpserted: tmpMarshaledCount,
1836
- StagingFile: tmpStagingFile,
1837
- Log: tmpLog.join('\n')
1838
- });
1839
- return fNext();
1840
- });
1811
+ pResponse.send(
1812
+ {
1813
+ Success: !pIntegrateError,
1814
+ RecordsProcessed: pRecords.length,
1815
+ RecordsTransformed: tmpRecordGUIDs.length,
1816
+ RecordsDeduplicated: tmpMappingOutcome.ParsedRowCount - tmpRecordGUIDs.length,
1817
+ BadRecords: tmpMappingOutcome.BadRecords.length,
1818
+ RecordsUpserted: tmpMarshaledCount,
1819
+ StagingFile: tmpStagingFile,
1820
+ Log: tmpLog.join('\n')
1821
+ });
1822
+ return fNext();
1841
1823
  });
1842
1824
  };
1843
1825
 
@@ -4113,5 +4113,344 @@ suite
4113
4113
  );
4114
4114
  }
4115
4115
  );
4116
+
4117
+ suite
4118
+ (
4119
+ 'Projection Deploy and Import Pipeline',
4120
+ function()
4121
+ {
4122
+ this.timeout(10000);
4123
+
4124
+ let _PipelineSourceID = 0;
4125
+ let _PipelineRawDatasetID = 0;
4126
+ let _PipelineProjectionDatasetID = 0;
4127
+ let _PipelineStoreConnectionID = 0;
4128
+ let _PipelineProjectionStoreID = 0;
4129
+ let _PipelineMappingID = 0;
4130
+
4131
+ test
4132
+ (
4133
+ 'Create a source for the pipeline test',
4134
+ function(fDone)
4135
+ {
4136
+ _SuperTest
4137
+ .post('/1.0/Source')
4138
+ .send({ Name: 'Pipeline Test Source', Type: 'API', URL: 'https://example.com', Protocol: 'HTTPS', Active: 1 })
4139
+ .expect(200)
4140
+ .end(
4141
+ (pError, pResponse) =>
4142
+ {
4143
+ if (pError) return fDone(pError);
4144
+ _PipelineSourceID = pResponse.body.IDSource;
4145
+ Expect(_PipelineSourceID).to.be.greaterThan(0);
4146
+ return fDone();
4147
+ });
4148
+ }
4149
+ );
4150
+
4151
+ test
4152
+ (
4153
+ 'Create a raw dataset and ingest records with JSON content',
4154
+ function(fDone)
4155
+ {
4156
+ _SuperTest
4157
+ .post('/1.0/Dataset')
4158
+ .send({ Name: 'Pipeline Raw Dataset', Type: 'Raw', Description: 'Raw data for pipeline test' })
4159
+ .expect(200)
4160
+ .end(
4161
+ (pError, pResponse) =>
4162
+ {
4163
+ if (pError) return fDone(pError);
4164
+ _PipelineRawDatasetID = pResponse.body.IDDataset;
4165
+ Expect(_PipelineRawDatasetID).to.be.greaterThan(0);
4166
+
4167
+ let tmpJSONContent = JSON.stringify([
4168
+ { Country: 'USA', Capital: 'Washington' },
4169
+ { Country: 'Canada', Capital: 'Ottawa' },
4170
+ { Country: 'Mexico', Capital: 'Mexico City' }
4171
+ ]);
4172
+
4173
+ _SuperTest
4174
+ .post('/facto/ingest/file')
4175
+ .send(
4176
+ {
4177
+ IDDataset: _PipelineRawDatasetID,
4178
+ IDSource: _PipelineSourceID,
4179
+ Format: 'json',
4180
+ Type: 'country-data',
4181
+ Content: tmpJSONContent
4182
+ })
4183
+ .expect(200)
4184
+ .end(
4185
+ (pIngestError, pIngestResponse) =>
4186
+ {
4187
+ if (pIngestError) return fDone(pIngestError);
4188
+ Expect(pIngestResponse.body.Ingested).to.equal(3);
4189
+ return fDone();
4190
+ });
4191
+ });
4192
+ }
4193
+ );
4194
+
4195
+ test
4196
+ (
4197
+ 'Create a Projection dataset with MicroDDL schema',
4198
+ function(fDone)
4199
+ {
4200
+ let tmpDDL = '! CountryFlat\n@ IDCountryFlat\n% GUIDCountryFlat\n$ CountryName 120\n$ CapitalCity 120';
4201
+
4202
+ _SuperTest
4203
+ .post('/1.0/Dataset')
4204
+ .send(
4205
+ {
4206
+ Name: 'CountryFlat Projection',
4207
+ Type: 'Projection',
4208
+ Description: 'Flat country projection',
4209
+ SchemaDefinition: tmpDDL
4210
+ })
4211
+ .expect(200)
4212
+ .end(
4213
+ (pError, pResponse) =>
4214
+ {
4215
+ if (pError) return fDone(pError);
4216
+ _PipelineProjectionDatasetID = pResponse.body.IDDataset;
4217
+ Expect(_PipelineProjectionDatasetID).to.be.greaterThan(0);
4218
+ return fDone();
4219
+ });
4220
+ }
4221
+ );
4222
+
4223
+ test
4224
+ (
4225
+ 'Create a StoreConnection (SQLite :memory:)',
4226
+ function(fDone)
4227
+ {
4228
+ _SuperTest
4229
+ .post('/1.0/StoreConnection')
4230
+ .send({ Name: 'Pipeline SQLite Store', Type: 'SQLite', Config: JSON.stringify({ SQLiteFilePath: ':memory:' }), Status: 'OK' })
4231
+ .expect(200)
4232
+ .end(
4233
+ (pError, pResponse) =>
4234
+ {
4235
+ if (pError) return fDone(pError);
4236
+ _PipelineStoreConnectionID = pResponse.body.IDStoreConnection;
4237
+ Expect(_PipelineStoreConnectionID).to.be.greaterThan(0);
4238
+ return fDone();
4239
+ });
4240
+ }
4241
+ );
4242
+
4243
+ test
4244
+ (
4245
+ 'Deploy the projection schema to the store',
4246
+ function(fDone)
4247
+ {
4248
+ _SuperTest
4249
+ .post(`/facto/projection/${_PipelineProjectionDatasetID}/deploy`)
4250
+ .send({ IDStoreConnection: _PipelineStoreConnectionID, TargetTableName: 'CountryFlat' })
4251
+ .expect(200)
4252
+ .end(
4253
+ (pError, pResponse) =>
4254
+ {
4255
+ if (pError) return fDone(pError);
4256
+ Expect(pResponse.body.Success).to.equal(true);
4257
+ // Extract the projection store ID
4258
+ let tmpPS = pResponse.body.ProjectionStore || {};
4259
+ _PipelineProjectionStoreID = tmpPS.IDProjectionStore || 0;
4260
+
4261
+ // If not directly available, query for it
4262
+ if (!_PipelineProjectionStoreID)
4263
+ {
4264
+ _SuperTest
4265
+ .get(`/facto/projection/${_PipelineProjectionDatasetID}/stores`)
4266
+ .expect(200)
4267
+ .end(
4268
+ (pStoreError, pStoreResponse) =>
4269
+ {
4270
+ if (pStoreError) return fDone(pStoreError);
4271
+ Expect(pStoreResponse.body.Stores.length).to.be.greaterThan(0);
4272
+ _PipelineProjectionStoreID = pStoreResponse.body.Stores[0].IDProjectionStore;
4273
+ Expect(_PipelineProjectionStoreID).to.be.greaterThan(0);
4274
+ return fDone();
4275
+ });
4276
+ }
4277
+ else
4278
+ {
4279
+ Expect(_PipelineProjectionStoreID).to.be.greaterThan(0);
4280
+ return fDone();
4281
+ }
4282
+ });
4283
+ }
4284
+ );
4285
+
4286
+ test
4287
+ (
4288
+ 'Bug 1: Entity routes should be accessible after deploy (GET /1.0/CountryFlat/Schema)',
4289
+ function(fDone)
4290
+ {
4291
+ // After deploy, _saveProjectionStore should have called
4292
+ // _registerProjectionEntity which registers Meadow entity
4293
+ // routes on the restify server.
4294
+ // Bug: the doCreate/doUpdate callback returned 4 args but
4295
+ // the handler only accepted 3, so registration was skipped
4296
+ // and this endpoint would 404.
4297
+ _SuperTest
4298
+ .get('/1.0/CountryFlat/Schema')
4299
+ .expect(200)
4300
+ .end(
4301
+ (pError, pResponse) =>
4302
+ {
4303
+ if (pError) return fDone(pError);
4304
+ // The schema endpoint should return entity metadata, not a 404
4305
+ Expect(pResponse.body).to.be.an('object');
4306
+ Expect(pResponse.body.title).to.equal('CountryFlat');
4307
+ return fDone();
4308
+ });
4309
+ }
4310
+ );
4311
+
4312
+ test
4313
+ (
4314
+ 'Create a mapping with template expressions',
4315
+ function(fDone)
4316
+ {
4317
+ let tmpMappingConfig = JSON.stringify(
4318
+ {
4319
+ Entity: 'CountryFlat',
4320
+ GUIDTemplate: '{~D:Record.Country~}',
4321
+ Mappings:
4322
+ {
4323
+ CountryName: '{~D:Record.Country~}',
4324
+ CapitalCity: '{~D:Record.Capital~}'
4325
+ }
4326
+ });
4327
+
4328
+ _SuperTest
4329
+ .post(`/facto/projection/${_PipelineProjectionDatasetID}/mapping`)
4330
+ .send(
4331
+ {
4332
+ IDSource: _PipelineSourceID,
4333
+ Name: 'Country Mapping',
4334
+ MappingConfiguration: tmpMappingConfig
4335
+ })
4336
+ .expect(200)
4337
+ .end(
4338
+ (pError, pResponse) =>
4339
+ {
4340
+ if (pError) return fDone(pError);
4341
+ Expect(pResponse.body.Success).to.equal(true);
4342
+
4343
+ // The mapping ID may be nested in a Meadow query result
4344
+ let tmpMapping = pResponse.body.Mapping || {};
4345
+ _PipelineMappingID = tmpMapping.IDProjectionMapping || 0;
4346
+
4347
+ // Fall back to querying for the mapping
4348
+ if (!_PipelineMappingID)
4349
+ {
4350
+ _SuperTest
4351
+ .get(`/facto/projection/${_PipelineProjectionDatasetID}/mappings`)
4352
+ .expect(200)
4353
+ .end(
4354
+ (pMapError, pMapResponse) =>
4355
+ {
4356
+ if (pMapError) return fDone(pMapError);
4357
+ Expect(pMapResponse.body.Mappings.length).to.be.greaterThan(0);
4358
+ _PipelineMappingID = pMapResponse.body.Mappings[0].IDProjectionMapping;
4359
+ Expect(_PipelineMappingID).to.be.greaterThan(0);
4360
+ return fDone();
4361
+ });
4362
+ }
4363
+ else
4364
+ {
4365
+ Expect(_PipelineMappingID).to.be.greaterThan(0);
4366
+ return fDone();
4367
+ }
4368
+ });
4369
+ }
4370
+ );
4371
+
4372
+ test
4373
+ (
4374
+ 'Execute the import via integrateRecords',
4375
+ function(fDone)
4376
+ {
4377
+ _SuperTest
4378
+ .post(`/facto/projection/${_PipelineProjectionDatasetID}/import`)
4379
+ .send(
4380
+ {
4381
+ IDProjectionMapping: _PipelineMappingID,
4382
+ IDProjectionStore: _PipelineProjectionStoreID,
4383
+ IDSource: _PipelineSourceID
4384
+ })
4385
+ .expect(200)
4386
+ .end(
4387
+ (pError, pResponse) =>
4388
+ {
4389
+ if (pError) return fDone(pError);
4390
+ Expect(pResponse.body.Success, `Import should succeed. Response: ${JSON.stringify(pResponse.body).substring(0, 1000)}`).to.equal(true);
4391
+ Expect(pResponse.body.RecordsProcessed).to.equal(3);
4392
+ Expect(pResponse.body.RecordsTransformed).to.be.greaterThan(0);
4393
+ Expect(pResponse.body.RecordsUpserted).to.be.greaterThan(0);
4394
+ return fDone();
4395
+ });
4396
+ }
4397
+ );
4398
+
4399
+ test
4400
+ (
4401
+ 'Bug 2: Projected records should have field values populated (not empty)',
4402
+ function(fDone)
4403
+ {
4404
+ // After import, the projected records in the CountryFlat
4405
+ // table should have CountryName and CapitalCity populated.
4406
+ // Bug: using marshalSourceRecords + pushRecordsToServer
4407
+ // separately bypassed the schema fetch step needed by the
4408
+ // IntegrationAdapter to marshal field values, leaving them
4409
+ // empty. Using integrateRecords fixes this.
4410
+ _SuperTest
4411
+ .get('/1.0/CountryFlats/0/10')
4412
+ .expect(200)
4413
+ .end(
4414
+ (pError, pResponse) =>
4415
+ {
4416
+ if (pError) return fDone(pError);
4417
+ Expect(pResponse.body).to.be.an('array');
4418
+ Expect(pResponse.body.length).to.equal(3);
4419
+
4420
+ // Find the USA record and verify field values
4421
+ let tmpUSA = pResponse.body.find((pRecord) => { return pRecord.CountryName === 'USA'; });
4422
+ Expect(tmpUSA).to.be.an('object');
4423
+ Expect(tmpUSA.CountryName).to.equal('USA');
4424
+ Expect(tmpUSA.CapitalCity).to.equal('Washington');
4425
+
4426
+ // Verify another record
4427
+ let tmpCanada = pResponse.body.find((pRecord) => { return pRecord.CountryName === 'Canada'; });
4428
+ Expect(tmpCanada).to.be.an('object');
4429
+ Expect(tmpCanada.CapitalCity).to.equal('Ottawa');
4430
+
4431
+ return fDone();
4432
+ });
4433
+ }
4434
+ );
4435
+
4436
+ test
4437
+ (
4438
+ 'Verify projected record count matches source record count',
4439
+ function(fDone)
4440
+ {
4441
+ _SuperTest
4442
+ .get('/1.0/CountryFlats/Count')
4443
+ .expect(200)
4444
+ .end(
4445
+ (pError, pResponse) =>
4446
+ {
4447
+ if (pError) return fDone(pError);
4448
+ Expect(pResponse.body.Count).to.equal(3);
4449
+ return fDone();
4450
+ });
4451
+ }
4452
+ );
4453
+ }
4454
+ );
4116
4455
  }
4117
4456
  );