@friggframework/core 0.1.2 → 0.2.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.
@@ -1,500 +0,0 @@
1
- const _ = require('lodash');
2
- const moment = require('moment');
3
- const mongoose = require('mongoose');
4
- const Delegate = require('../Delegate');
5
- const SyncObject = require('../objects/sync/Sync');
6
- const ModuleManager = require('./ModuleManager');
7
- const { Integration, Sync } = require('@friggframework/models');
8
- const { debug } = require('@friggframework/logs');
9
- const { get } = require('@friggframework/assertions');
10
-
11
- class SyncManager extends Delegate {
12
- constructor(params) {
13
- super(params);
14
- this.primaryModule = getAndVerifyType(params, 'primary', ModuleManager);
15
- this.secondaryModule = getAndVerifyType(
16
- params,
17
- 'secondary',
18
- ModuleManager
19
- );
20
- this.SyncObjectClass = getAndVerifyType(
21
- params,
22
- 'syncObjectClass',
23
- SyncObject
24
- );
25
- this.ignoreEmptyMatchValues = get(
26
- params,
27
- 'ignoreEmptyMatchValues',
28
- true
29
- );
30
- this.isUnidirectionalSync = get(params, 'isUnidirectionalSync', false);
31
- this.useFirstMatchingDuplicate = get(
32
- params,
33
- 'useFirstMatchingDuplicate',
34
- true
35
- );
36
- this.omitEmptyStringsFromData = get(
37
- params,
38
- 'omitEmptyStringsFromData',
39
- true
40
- );
41
-
42
- this.integration = get(params, 'integration', null); // TODO Change to type validation
43
-
44
- this.syncMO = new Sync();
45
- }
46
-
47
- // calls getAllSyncObjects() on the modules and then finds the difference between each. The Primary Module
48
- // takes precedence unless the field is an empty string or null
49
- async initialSync() {
50
- const time0 = parseInt(moment().format('x'));
51
- const primaryEntityId = await this.primaryModule.entity.id;
52
- const secondaryEntityId = await this.secondaryModule.entity.id;
53
-
54
- // get array of sync objects
55
- let primaryArr = await this.primaryModule.getAllSyncObjects(
56
- this.SyncObjectClass
57
- );
58
- const primaryArrayInitialCount = primaryArr.length;
59
- const time1 = parseInt(moment().format('x'));
60
- debug(
61
- `${primaryArr.length} number of ${
62
- this.SyncObjectClass.name
63
- } retrieved from ${this.primaryModule.constructor.getName()} in ${
64
- time1 - time0
65
- } ms`
66
- );
67
- let secondaryArr = await this.secondaryModule.getAllSyncObjects(
68
- this.SyncObjectClass
69
- );
70
- const secondaryArrayInitialCount = secondaryArr.length;
71
- const time2 = parseInt(moment().format('x'));
72
- debug(
73
- `${secondaryArr.length} number of ${
74
- this.SyncObjectClass.name
75
- } retrieved from ${this.secondaryModule.constructor.getName()} in ${
76
- time2 - time1
77
- } ms`
78
- );
79
-
80
- // ignore the empty match values
81
- if (this.ignoreEmptyMatchValues) {
82
- const primaryCountBefore = primaryArr.length;
83
- primaryArr = primaryArr.filter((obj) => !obj.missingMatchData);
84
- const primaryCountAfter = primaryArr.length;
85
- const secondaryCountBefore = secondaryArr.length;
86
- secondaryArr = secondaryArr.filter((obj) => !obj.missingMatchData);
87
- const secondaryCountAfter = secondaryArr.length;
88
- debug(
89
- `Ignoring ${primaryCountBefore - primaryCountAfter} ${
90
- this.SyncObjectClass.name
91
- } objects from ${this.primaryModule.constructor.getName()}`
92
- );
93
- debug(
94
- `Ignoring ${secondaryCountBefore - secondaryCountAfter} ${
95
- this.SyncObjectClass.name
96
- } objects from ${this.secondaryModule.constructor.getName()}`
97
- );
98
- }
99
- if (this.useFirstMatchingDuplicate) {
100
- primaryArr = _.uniqBy(primaryArr, 'matchHash');
101
- debug(
102
- `${primaryArr.length} Objects remaining after removing duplicates from Primary Array`
103
- );
104
- secondaryArr = _.uniqBy(secondaryArr, 'matchHash');
105
- debug(
106
- `${secondaryArr.length} Objects remaining after removing duplicates from Secondary Array`
107
- );
108
- }
109
- const primaryUpdate = [];
110
- const secondaryUpdate = [];
111
- // PrimaryIntersection is an array where at least one matching object was found inside
112
- // SecondaryArray that matched the inspected object from Primary.
113
- // The only catch is, there will definitely be duplicates unless self filtered
114
- const primaryIntersection = primaryArr.filter((e1) =>
115
- secondaryArr.some((e2) => e1.equals(e2))
116
- );
117
- // SecondaryIntersection is an array where at least one matching object was found inside
118
- // primaryIntersection that matched the inspected object from secondaryArray.
119
- // The only catch is, there will definitely be duplicates unless self filtered
120
- const secondaryIntersection = secondaryArr.filter((e1) =>
121
- primaryIntersection.some((e2) => e1.equals(e2))
122
- );
123
- const secondaryCreate = primaryArr.filter(
124
- (e1) => !secondaryArr.some((e2) => e1.equals(e2))
125
- );
126
- const primaryCreate = secondaryArr.filter(
127
- (e1) => !primaryArr.some((e2) => e1.equals(e2))
128
- );
129
-
130
- // process the intersections and see which ones need to be updated.
131
- for (const primaryObj of primaryIntersection) {
132
- const secondaryObj = secondaryIntersection.find((e1) =>
133
- e1.equals(primaryObj)
134
- );
135
-
136
- let primaryUpdated = false;
137
- let secondaryUpdated = false;
138
-
139
- for (const key in primaryObj.data) {
140
- let valuesAreNotEquivalent = true; // Default to this just to be safe
141
- // Make sure we're not comparing a number 0 to a empty string/null/undefined.
142
- if (_.isEqual(primaryObj.data[key], secondaryObj.data[key])) {
143
- // This should basically tell us if both values are falsy, in which case we're good
144
- valuesAreNotEquivalent = false;
145
- } else if (
146
- typeof primaryObj.data[key] === 'number' ||
147
- typeof secondaryObj.data[key] === 'number'
148
- ) {
149
- // This should try comparing if at least one of the two are numbers
150
- valuesAreNotEquivalent =
151
- primaryObj.data[key] !== secondaryObj.data[key];
152
- } else if (!primaryObj.data[key] && !secondaryObj.data[key]) {
153
- valuesAreNotEquivalent = false;
154
- }
155
-
156
- if (valuesAreNotEquivalent) {
157
- if (
158
- primaryObj.dataKeyIsReplaceable(key) &&
159
- !secondaryObj.dataKeyIsReplaceable(key) &&
160
- !this.isUnidirectionalSync
161
- ) {
162
- primaryObj.data[key] = secondaryObj.data[key];
163
- primaryUpdated = true;
164
- } else if (!primaryObj.dataKeyIsReplaceable(key)) {
165
- secondaryObj.data[key] = primaryObj.data[key];
166
- secondaryUpdated = true;
167
- }
168
- }
169
- }
170
- if (primaryUpdated && !this.isUnidirectionalSync) {
171
- primaryUpdate.push(primaryObj);
172
- }
173
- if (secondaryUpdated) {
174
- secondaryUpdate.push(secondaryObj);
175
- }
176
-
177
- const createdObj = await this.createSyncDBObject(
178
- [primaryObj, secondaryObj],
179
- [primaryEntityId, secondaryEntityId]
180
- );
181
-
182
- primaryObj.setSyncId(createdObj.id);
183
- secondaryObj.setSyncId(createdObj.id);
184
- }
185
- debug(
186
- `Found ${
187
- primaryUpdate.length
188
- } for updating in ${this.primaryModule.constructor.getName()}`
189
- );
190
- debug(
191
- `Found ${
192
- primaryCreate.length
193
- } for creating in ${this.primaryModule.constructor.getName()}`
194
- );
195
- debug(
196
- `Found ${
197
- secondaryUpdate.length
198
- } for updating in ${this.secondaryModule.constructor.getName()}`
199
- );
200
- debug(
201
- `Found ${
202
- secondaryCreate.length
203
- } for creating in ${this.secondaryModule.constructor.getName()}`
204
- );
205
-
206
- const time3 = parseInt(moment().format('x'));
207
- debug(`Sorting complete in ${time3 - time2} ms`);
208
-
209
- // create the database entries for the
210
- if (!this.isUnidirectionalSync) {
211
- for (const secondaryObj of primaryCreate) {
212
- const createdObj = await this.createSyncDBObject(
213
- [secondaryObj],
214
- [secondaryEntityId, primaryEntityId]
215
- );
216
-
217
- secondaryObj.setSyncId(createdObj.id);
218
- }
219
- }
220
-
221
- for (const primaryObj of secondaryCreate) {
222
- const createdObj = await this.createSyncDBObject(
223
- [primaryObj],
224
- [primaryEntityId, secondaryEntityId]
225
- );
226
- primaryObj.setSyncId(createdObj.id);
227
- }
228
- const time4 = parseInt(moment().format('x'));
229
- debug(`Sync objects create in DB in ${time4 - time3} ms`);
230
-
231
- // call the batch update/creates
232
- let time5 = parseInt(moment().format('x'));
233
- let time6 = parseInt(moment().format('x'));
234
- if (!this.isUnidirectionalSync) {
235
- await this.primaryModule.batchUpdateSyncObjects(
236
- primaryUpdate,
237
- this
238
- );
239
- time5 = parseInt(moment().format('x'));
240
- debug(
241
- `Updated ${primaryUpdate.length} ${
242
- this.SyncObjectClass.name
243
- }s in ${this.primaryModule.constructor.getName()} in ${
244
- time5 - time4
245
- } ms`
246
- );
247
- await this.primaryModule.batchCreateSyncObjects(
248
- primaryCreate,
249
- this
250
- );
251
- time6 = parseInt(moment().format('x'));
252
- debug(
253
- `Created ${primaryCreate.length} ${
254
- this.SyncObjectClass.name
255
- }s in ${this.primaryModule.constructor.getName()} in ${
256
- time6 - time5
257
- } ms`
258
- );
259
- }
260
-
261
- await this.secondaryModule.batchUpdateSyncObjects(
262
- secondaryUpdate,
263
- this
264
- );
265
- const time7 = parseInt(moment().format('x'));
266
- debug(
267
- `Updated ${secondaryUpdate.length} ${
268
- this.SyncObjectClass.name
269
- }s in ${this.secondaryModule.constructor.getName()} in ${
270
- time7 - time6
271
- } ms`
272
- );
273
-
274
- await this.secondaryModule.batchCreateSyncObjects(
275
- secondaryCreate,
276
- this
277
- );
278
- const time8 = parseInt(moment().format('x'));
279
- debug(
280
- `${primaryArrayInitialCount} number of ${
281
- this.SyncObjectClass.name
282
- } objects retrieved from ${this.primaryModule.constructor.getName()} in ${
283
- time1 - time0
284
- } ms`
285
- );
286
- debug(
287
- `${secondaryArrayInitialCount} number of ${
288
- this.SyncObjectClass.name
289
- } objects retrieved from ${this.secondaryModule.constructor.getName()} in ${
290
- time2 - time1
291
- } ms`
292
- );
293
- debug(`Sorting complete in ${time3 - time2} ms`);
294
- debug(`Sync objects create in DB in ${time4 - time3} ms`);
295
- debug(
296
- `Updated ${primaryUpdate.length} ${
297
- this.SyncObjectClass.name
298
- }s in ${this.primaryModule.constructor.getName()} in ${
299
- time5 - time4
300
- } ms`
301
- );
302
- debug(
303
- `Created ${primaryCreate.length} ${
304
- this.SyncObjectClass.name
305
- }s in ${this.primaryModule.constructor.getName()} in ${
306
- time6 - time5
307
- } ms`
308
- );
309
- debug(
310
- `Updated ${secondaryUpdate.length} ${
311
- this.SyncObjectClass.name
312
- }s in ${this.secondaryModule.constructor.getName()} in ${
313
- time7 - time6
314
- } ms`
315
- );
316
- debug(
317
- `Created ${secondaryCreate.length} ${
318
- this.SyncObjectClass.name
319
- }s in ${this.secondaryModule.constructor.getName()} in ${
320
- time8 - time7
321
- } ms`
322
- );
323
- }
324
-
325
- async createSyncDBObject(objArr, entities) {
326
- const entityIds = entities.map(
327
- (ent) => ({ $elemMatch: { $eq: mongoose.Types.ObjectId(ent) } })
328
- // return {"$elemMatch": {"$eq": ent}};
329
- );
330
- const dataIdentifiers = [];
331
- for (const index in objArr) {
332
- dataIdentifiers.push({
333
- entity: entities[index],
334
- id: objArr[index].dataIdentifier,
335
- hash: objArr[index].dataIdentifierHash,
336
- });
337
- }
338
- const primaryObj = objArr[0];
339
-
340
- const createSyncObj = {
341
- name: primaryObj.getName(),
342
- entities,
343
- hash: primaryObj.getHashData({
344
- omitEmptyStringsFromData: this.omitEmptyStringsFromData,
345
- }),
346
- dataIdentifiers,
347
- };
348
- const filter = {
349
- name: primaryObj.getName(),
350
- dataIdentifiers: {
351
- $elemMatch: {
352
- id: primaryObj.dataIdentifier,
353
- entity: entities[0],
354
- },
355
- },
356
- entities: { $all: entityIds },
357
- // entities
358
- };
359
-
360
- return await this.syncMO.upsert(filter, createSyncObj);
361
- // return await this.syncMO.create(createSyncObj);
362
- }
363
-
364
- // Automatically syncs the objects with the secondary module if the object was updated
365
- async sync(syncObjects) {
366
- const batchUpdates = [];
367
- const batchCreates = [];
368
- const noChange = [];
369
- const primaryEntityId = await this.primaryModule.entity.id;
370
- const secondaryEntityId = await this.secondaryModule.entity.id;
371
-
372
- const secondaryModuleName = this.secondaryModule.constructor.getName();
373
- for (const primaryObj of syncObjects) {
374
- const dataHash = primaryObj.getHashData({
375
- omitEmptyStringsFromData: this.omitEmptyStringsFromData,
376
- });
377
-
378
- // get the sync object in the database if it exists
379
- let syncObj = await this.syncMO.getSyncObject(
380
- primaryObj.getName(),
381
- primaryObj.dataIdentifier,
382
- primaryEntityId
383
- );
384
-
385
- if (syncObj) {
386
- debug('Sync object found, evaluating...');
387
- const hashMatch = syncObj.hash === dataHash;
388
- const dataIdentifierLength = syncObj.dataIdentifiers.length;
389
-
390
- if (!hashMatch && dataIdentifierLength > 1) {
391
- debug(
392
- "Previously successful sync, but hashes don't match. Updating."
393
- );
394
- const secondaryObj = new this.SyncObjectClass({
395
- data: primaryObj.data,
396
- dataIdentifier:
397
- Sync.getEntityObjIdForEntityIdFromObject(
398
- syncObj,
399
- secondaryEntityId
400
- ),
401
- moduleName: secondaryModuleName,
402
- useMapping: false,
403
- });
404
- secondaryObj.setSyncId(syncObj.id);
405
- batchUpdates.push(secondaryObj);
406
- } else if (hashMatch && dataIdentifierLength > 1) {
407
- debug(
408
- 'Data hashes match, no updates or creates needed for this one.'
409
- );
410
- noChange.push(syncObj);
411
- }
412
-
413
- if (dataIdentifierLength === 1) {
414
- debug(
415
- "We have only one data Identifier, which means we don't have a record in the secondary app for whatever reason (failure or filter). So, creating."
416
- );
417
- primaryObj.setSyncId(syncObj.id);
418
- batchCreates.push(primaryObj);
419
- }
420
- } else {
421
- debug(
422
- "No sync object, so we'll try creating, first creating an object"
423
- );
424
- syncObj = await this.createSyncDBObject(
425
- [primaryObj],
426
- [primaryEntityId, secondaryEntityId]
427
- );
428
- primaryObj.setSyncId(syncObj.id);
429
- batchCreates.push(primaryObj);
430
- }
431
- }
432
- const updateRes =
433
- batchUpdates.length > 0
434
- ? await this.secondaryModule.batchUpdateSyncObjects(
435
- batchUpdates,
436
- this
437
- )
438
- : [];
439
- const createRes =
440
- batchCreates.length > 0
441
- ? await this.secondaryModule.batchCreateSyncObjects(
442
- batchCreates,
443
- this
444
- )
445
- : [];
446
- return updateRes.concat(createRes).concat(noChange);
447
- }
448
-
449
- // takes in:
450
- // 1. the Sync Id of an object in our database
451
- // 2. the object Id in the form of a json object for example:
452
- // {
453
- // companyId: 12,
454
- // saleId:524
455
- // }
456
- // 3. the module manager calling the function
457
- async confirmCreate(syncObj, createdId, moduleManager) {
458
- const dataIdentifier = {
459
- entity: await moduleManager.entity.id,
460
- id: createdId,
461
- hash: this.SyncObjectClass.hashJSON(createdId),
462
- };
463
- // No matter what, save the hash because why not?
464
- // TODO this is suboptimal because it does 2 DB requests where only 1 is needed
465
- // TODO If you want to get even more optimized, batch any/all updates together.
466
- // Also this is only needed because of the case where an "update" becomes a "create" when we find only
467
- // 1 data identifier. So, during `sync()`, if we see that the hashes don't match, we check for DataIDs and
468
- // decide to create in the "target" or "secondary" because we know it failed for some reason. We also want
469
- // to hold off on updating the hash in case the create fails for some reason again.
470
-
471
- await this.syncMO.update(syncObj.syncId, {
472
- hash: syncObj.getHashData({
473
- omitEmptyStringsFromData: this.omitEmptyStringsFromData,
474
- }),
475
- });
476
-
477
- const result = await this.syncMO.addDataIdentifier(
478
- syncObj.syncId,
479
- dataIdentifier
480
- );
481
-
482
- return result;
483
- }
484
-
485
- async confirmUpdate(syncObj) {
486
- debug(
487
- 'Successfully updated secondaryObject. Updating the hash in the DB'
488
- );
489
- const result = await this.syncMO.update(syncObj.syncId, {
490
- hash: syncObj.getHashData({
491
- omitEmptyStringsFromData: this.omitEmptyStringsFromData,
492
- }),
493
- });
494
- debug('Success');
495
-
496
- return result;
497
- }
498
- }
499
-
500
- module.exports = SyncManager;
@@ -1,80 +0,0 @@
1
- const md5 = require('md5');
2
- const { get } = require('@friggframework/assertions');
3
-
4
- /**
5
- * @file This file is meant to be the thing that enforces proper use of
6
- * the Association model.
7
- * For now, we're going to use the model directly and worry about proper use
8
- * later...
9
- */
10
- class Association {
11
- static Config = {
12
- name: 'Association',
13
-
14
- reverseModuleMap: {},
15
- };
16
- constructor(params) {
17
- this.data = {};
18
-
19
- let data = get(params, 'data');
20
- this.moduleName = get(params, 'moduleName');
21
- this.dataIdentifier = get(params, 'dataIdentifier');
22
-
23
- this.dataIdentifierHash = this.constructor.hashJSON(
24
- this.dataIdentifier
25
- );
26
-
27
- for (let key of this.constructor.Config.keys) {
28
- this.data[key] =
29
- this.constructor.Config.moduleMap[this.moduleName][key](data);
30
- }
31
-
32
- // matchHash is used to find matches between two sync objects
33
- let matchHashData = [];
34
- for (let key of this.constructor.Config.matchOn) {
35
- matchHashData.push(this.data[key]);
36
- }
37
- this.matchHash = this.constructor.hashJSON(matchHashData);
38
-
39
- this.syncId = null;
40
- }
41
-
42
- equals(syncObj) {
43
- return this.matchHash === syncObj.matchHash;
44
- }
45
- dataKeyIsReplaceable(key) {
46
- return this.data[key] === null || this.data[key] === '';
47
- }
48
-
49
- isModuleInMap(moduleName) {
50
- return this.constructor.Config.moduleMap[name];
51
- }
52
-
53
- getName() {
54
- return this.name;
55
- }
56
-
57
- getHashData() {
58
- let orderedData = [];
59
- for (let key of this.constructor.Config.keys) {
60
- orderedData.push(this.data[key]);
61
- }
62
-
63
- return this.constructor.hashJSON(orderedData);
64
- }
65
-
66
- setSyncId(syncId) {
67
- this.syncId = syncId;
68
- }
69
-
70
- reverseModuleMap(moduleName) {
71
- return this.constructor.Config.reverseModuleMap[moduleName](this.data);
72
- }
73
-
74
- static hashJSON(data) {
75
- let dataString = JSON.stringify(data, null, 2);
76
- return md5(dataString);
77
- }
78
- }
79
-
80
- module.exports = Association;
@@ -1,68 +0,0 @@
1
- const ModuleManager = require('../../managers/ModuleManager');
2
- const {
3
- RequiredPropertyError,
4
- } = require('@friggframework/errors/ValidationErrors');
5
- const { get, getAndVerifyType } = require('@friggframework/assertions');
6
-
7
- class Options {
8
- constructor(params) {
9
- this.module = getAndVerifyType(params, 'module', ModuleManager);
10
- this.integrations = getAndVerifyType(
11
- params,
12
- 'integrations',
13
- ModuleManager
14
- );
15
- this.isMany = Boolean(get(params, 'isMany', false));
16
- this.hasUserConfig = Boolean(get(params, 'hasUserConfig', false));
17
- this.requiresNewEntity = Boolean(
18
- get(params, 'requiresNewEntity', false)
19
- );
20
- if (!params.display) {
21
- throw new RequiredPropertyError({
22
- parent: this,
23
- key: 'display',
24
- });
25
- }
26
-
27
- this.display = {};
28
- this.display.name = get(params.display, 'name');
29
- this.display.description = get(params.display, 'description');
30
- this.display.detailsUrl = get(params.display, 'detailsUrl');
31
- this.display.icon = get(params.display, 'icon');
32
- this.keys = get(params, 'keys', []);
33
- }
34
-
35
- get() {
36
- return {
37
- type: this.module.getName(),
38
-
39
- // list of entities the module can connect to
40
- integrations: this.integrations.map((val) => val.getName()),
41
-
42
- // list of special data required to make an entity i.e. a shop id. This information should be sent back
43
- keys: this.keys,
44
-
45
- // Flag for if the User can configure any settings
46
- hasUserConfig: this.hasUserConfig,
47
-
48
- // if this integration can be used multiple times with the same integration pair. For example I want to
49
- // connect two different Etsy shops to the same Freshbooks account.
50
- isMany: this.isMany,
51
-
52
- // if this is true it means we need to create a new entity for every integration pair and not use an
53
- // existing one. This would be true for scenarios where the client wishes to have individual control over
54
- // the integerations it has connected to its app. They would want this to let their users only delete
55
- // single integrations without notifying our server.
56
- requiresNewEntity: this.requiresNewEntity,
57
-
58
- // this is information required for the display side of things on the front end
59
- display: this.display,
60
-
61
- // this is information for post-authentication config, using jsonSchema and uiSchema for display on the frontend
62
- // Maybe include but probably not, I like making someone make a follow-on request
63
- // configOptions: this.configOptions,
64
- };
65
- }
66
- }
67
-
68
- module.exports = Options;
@@ -1,32 +0,0 @@
1
- const IntegrationManager = require('../../managers/IntegrationManager');
2
- const { get, getAndVerifyType } = require('@friggframework/assertions');
3
-
4
- class Options {
5
- constructor(params) {
6
- this.integrationManager = getAndVerifyType(
7
- params,
8
- 'integrationManager',
9
- IntegrationManager
10
- );
11
- this.fromVersion = get(params, 'fromVersion');
12
- this.toVersion = get(params, 'toVersion');
13
- this.generalFunctions = get(params, 'generalFunctions', []);
14
- this.perIntegrationFunctions = get(
15
- params,
16
- 'perIntegrationFunctions',
17
- []
18
- );
19
- }
20
-
21
- get() {
22
- return {
23
- type: this.integrationManager.getName(),
24
- fromVersion: this.fromVersion,
25
- toVersion: this.toVersion,
26
- generalFunctions: this.generalFunctions,
27
- perIntegrationFunctions: this.perIntegrationFunctions,
28
- };
29
- }
30
- }
31
-
32
- module.exports = Options;