Skip to content

Commit 27b61b8

Browse files
committed
Refactor to move validateAndEmit() logic to common.js
1 parent 245ea12 commit 27b61b8

File tree

3 files changed

+88
-73
lines changed

3 files changed

+88
-73
lines changed
Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const quickbooks = require("../quickbooks.app");
22
const { createHmac } = require("crypto");
3-
const { SUPPORTED_WEBHOOK_OPERATIONS } = require("../constants");
3+
const {
4+
WEBHOOK_ENTITIES,
5+
WEBHOOK_OPERATIONS,
6+
SUPPORTED_WEBHOOK_OPERATIONS,
7+
} = require("../constants");
48

59
module.exports = {
610
props: {
@@ -19,6 +23,9 @@ module.exports = {
1923
},
2024
},
2125
methods: {
26+
companyId(event) {
27+
return event.body.eventNotifications[0].realmId;
28+
},
2229
getSupportedOperations(entityName) {
2330
return SUPPORTED_WEBHOOK_OPERATIONS[entityName];
2431
},
@@ -37,23 +44,24 @@ module.exports = {
3744
return pastTenseVersion[operations];
3845
}
3946
},
40-
sendHttpResponse(event, entities) {
41-
this.http.respond({
42-
status: 200,
43-
body: entities,
44-
headers: {
45-
"Content-Type": event.headers["Content-Type"],
46-
},
47-
});
47+
verifyWebhookRequest(event) {
48+
const token = this.webhookVerifierToken;
49+
const payload = event.bodyRaw;
50+
const header = event.headers["intuit-signature"];
51+
const hash = createHmac("sha256", token).update(payload)
52+
.digest("hex");
53+
const convertedHeader = Buffer.from(header, "base64").toString("hex");
54+
return hash === convertedHeader;
4855
},
49-
isValidSource(event, webhookCompanyId) {
56+
isValidSource(event) {
5057
const isWebhookValid = this.verifyWebhookRequest(event);
5158
if (!isWebhookValid) {
5259
console.log(`Error: Webhook did not pass verification. Try reentering the verifier token,
5360
making sure it's from the correct section on the Intuit Developer Dashboard.`);
5461
return false;
5562
}
5663

64+
const webhookCompanyId = this.companyId(event);
5765
const connectedCompanyId = this.quickbooks.companyId();
5866
if (webhookCompanyId !== connectedCompanyId) {
5967
console.log(`Error: Cannot retrieve record details for incoming webhook. The QuickBooks company id
@@ -63,36 +71,54 @@ module.exports = {
6371
}
6472
return true;
6573
},
66-
verifyWebhookRequest(event) {
67-
const token = this.webhookVerifierToken;
68-
const payload = event.bodyRaw;
69-
const header = event.headers["intuit-signature"];
70-
const hash = createHmac("sha256", token).update(payload)
71-
.digest("hex");
72-
const convertedHeader = Buffer.from(header, "base64").toString("hex");
73-
return hash === convertedHeader;
74+
getEntities() {
75+
return WEBHOOK_ENTITIES;
7476
},
75-
async validateAndEmit(entity) {
76-
// individual source modules can redefine this method to specify criteria
77-
// for which events to emit
78-
await this.processEvent(entity);
77+
getOperations() {
78+
return WEBHOOK_OPERATIONS;
7979
},
80-
async processEvent(entity) {
80+
isEntityRelevant(entity) {
8181
const {
8282
name,
83-
id,
8483
operation,
85-
lastUpdated,
8684
} = entity;
87-
// Unless the record has been deleted, use the id received in the webhook
88-
// to get the full record data
89-
const eventToEmit = {
90-
event_notification: entity,
91-
record_details: operation === "Delete"
92-
? {}
93-
: await this.quickbooks.getRecordDetails(name, id),
85+
const relevantEntities = this.getEntities();
86+
const relevantOperations = this.getOperations();
87+
88+
if (!relevantEntities.includes(name)) {
89+
console.log(`Skipping '${operation} ${name}' event. (Accepted entities: ${relevantEntities.join(", ")})`);
90+
return false;
91+
}
92+
if (!relevantOperations.includes(operation)) {
93+
console.log(`Skipping '${operation} ${name}' event. (Accepted operations: ${relevantOperations.join(", ")})`);
94+
return false;
95+
}
96+
return true;
97+
},
98+
async generateEvent(entity, event) {
99+
const eventDetails = {
100+
...entity,
101+
companyId: this.companyId(event),
94102
};
95103

104+
// Unless the record has been deleted, use the id received in the webhook
105+
// to get the full record data from QuickBooks
106+
const recordDetails = entity.operation === "Delete"
107+
? {}
108+
: await this.quickbooks.getRecordDetails(entity.name, entity.id);
109+
110+
return {
111+
eventDetails,
112+
recordDetails,
113+
};
114+
},
115+
generateMeta(event) {
116+
const {
117+
name,
118+
id,
119+
operation,
120+
lastUpdated,
121+
} = event.eventDetails;
96122
const summary = `${name} ${id} ${this.toPastTense(operation)}`;
97123
const ts = lastUpdated
98124
? Date.parse(lastUpdated)
@@ -103,26 +129,35 @@ module.exports = {
103129
operation,
104130
ts,
105131
].join("-");
106-
this.$emit(eventToEmit, {
132+
return {
107133
id: eventId,
108134
summary,
109135
ts,
136+
};
137+
},
138+
async processEvent(event) {
139+
const { entities } = event.body.eventNotifications[0].dataChangeEvent;
140+
141+
const events = await Promise.all(entities
142+
.filter(this.isEntityRelevant)
143+
.map(async (entity) => {
144+
// Generate events asynchronously to fetch multiple records from the API at the same time
145+
return await this.generateEvent(entity, event);
146+
}));
147+
148+
events.forEach((event) => {
149+
const meta = this.generateMeta(event);
150+
this.$emit(event, meta);
110151
});
111152
},
153+
112154
},
113155
async run(event) {
114-
const { entities } = event.body.eventNotifications[0].dataChangeEvent;
115-
this.sendHttpResponse(event, entities);
116-
117-
const webhookCompanyId = event.body.eventNotifications[0].realmId;
118-
if (!this.isValidSource(event, webhookCompanyId)) {
156+
if (!this.isValidSource(event)) {
119157
console.log("Skipping event from unrecognized source.");
120158
return;
121159
}
122160

123-
await Promise.all(entities.map((entity) => {
124-
entity.realmId = webhookCompanyId;
125-
return this.validateAndEmit(entity);
126-
}));
161+
return this.processEvent(event);
127162
},
128163
};

components/quickbooks/sources/custom-webhook-events/custom-webhook-events.js

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module.exports = {
1010
type: "source",
1111
props: {
1212
...common.props,
13-
namesToEmit: {
13+
entitiesToEmit: {
1414
propDefinition: [
1515
quickbooks,
1616
"webhookNames",
@@ -25,20 +25,11 @@ module.exports = {
2525
},
2626
methods: {
2727
...common.methods,
28-
async validateAndEmit(entity) {
29-
// only emit events that match the entity names and operations indicated by the user
30-
// but if the props are left empty, emit all events rather than filtering them all out
31-
// (it would a hassle for the user to select every option if they wanted to emit everything)
32-
if (this.namesToEmit.length > 0 && !this.namesToEmit.includes(entity.name)) {
33-
console.log(`Entity Type '${entity.name}' not found in list of selected Entity Types`);
34-
return;
35-
}
36-
if (this.operationsToEmit.length > 0
37-
&& !this.operationsToEmit.includes(entity.operation)) {
38-
console.log(`Operation '${entity.operation}' not found in list of selected Operations`);
39-
return;
40-
}
41-
await this.processEvent(entity);
28+
getEntities() {
29+
return this.entitiesToEmit;
30+
},
31+
getOperations() {
32+
return this.operationsToEmit;
4233
},
4334
},
4435
};

components/quickbooks/sources/new-or-modified-customer/new-or-modified-customer.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,11 @@ module.exports = {
2727
},
2828
methods: {
2929
...common.methods,
30-
async validateAndEmit(entity) {
31-
// only emit events that match the specified entity name and operation
32-
// but if the operations prop is left empty, emit all events rather
33-
// than filtering them all out
34-
// (it would a hassle for the user to select every single option
35-
// if they wanted to emit everything)
36-
if (entity.name !== sourceEntity) {
37-
console.log(`${entity.name} webhook received and ignored, since it is not a Customer`);
38-
return;
39-
}
40-
if (this.operationsToEmit.length > 0
41-
&& !this.operationsToEmit.includes(entity.operation)) {
42-
console.log(`Operation '${entity.operation}' not found in list of selected Operations`);
43-
return;
44-
}
45-
await this.processEvent(entity);
30+
getEntities() {
31+
return [sourceEntity];
32+
},
33+
getOperations() {
34+
return this.operationsToEmit;
4635
},
4736
},
4837
};

0 commit comments

Comments
 (0)