@friggframework/core 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/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@friggframework/eslint-config"
3
+ }
package/Delegate.js ADDED
@@ -0,0 +1,29 @@
1
+ const { get } = require('@friggframework/assertions');
2
+
3
+ class Delegate {
4
+ constructor(params) {
5
+ this.delegate = get(params, 'delegate', null);
6
+ this.delegateTypes = [];
7
+ }
8
+
9
+ async notify(delegateString, object = null) {
10
+ if (!this.delegateTypes.includes(delegateString)) {
11
+ throw new Error(
12
+ `delegateString:${delegateString} is not defined in delegateTypes`
13
+ );
14
+ }
15
+ if (this.delegate) {
16
+ await this.delegate.receiveNotification(
17
+ this,
18
+ delegateString,
19
+ object
20
+ );
21
+ }
22
+ }
23
+
24
+ async receiveNotification(notifier, delegateString, object = null) {
25
+ // ...
26
+ }
27
+ }
28
+
29
+ module.exports = Delegate;
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Left Hook Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/Requester.js ADDED
@@ -0,0 +1,153 @@
1
+ const fetch = require('node-fetch');
2
+ const Delegate = require('./Delegate');
3
+ const { FetchError } = require('@friggframework/errors/FetchError');
4
+ const { get } = require('@friggframework/assertions');
5
+
6
+ class Requester extends Delegate {
7
+ constructor(params) {
8
+ super(params);
9
+ this.backOff = get(params, 'backOff', [1, 3, 10, 30, 60, 180]);
10
+ this.isRefreshable = false;
11
+ this.refreshCount = 0;
12
+ this.DLGT_INVALID_AUTH = 'INVALID_AUTH';
13
+ this.delegateTypes.push(this.DLGT_INVALID_AUTH);
14
+
15
+ // Allow passing in the fetch function
16
+ // Instance methods can use this.fetch without differentiating
17
+ this.fetch = get(params, 'fetch', fetch);
18
+ }
19
+
20
+ parsedBody = async (resp) => {
21
+ const contentType = resp.headers.get('Content-Type') || '';
22
+
23
+ if (
24
+ contentType.match(/^application\/json/) ||
25
+ contentType.match(/^application\/vnd.api\+json/)
26
+ ) {
27
+ return resp.json();
28
+ }
29
+
30
+ return resp.text();
31
+ };
32
+
33
+ async _request(url, options, i = 0) {
34
+ let encodedUrl = encodeURI(url);
35
+ if (options.query) {
36
+ let queryBuild = '?';
37
+ for (const key in options.query) {
38
+ queryBuild += `${encodeURIComponent(key)}=${encodeURIComponent(
39
+ options.query[key]
40
+ )}&`;
41
+ }
42
+ encodedUrl += queryBuild.slice(0, -1);
43
+ }
44
+
45
+ options.headers = await this.addAuthHeaders(options.headers);
46
+
47
+ const response = await this.fetch(encodedUrl, options);
48
+ const { status } = response;
49
+
50
+ // If the status is retriable and there are back off requests left, retry the request
51
+ if ((status === 429 || status >= 500) && i < this.backOff.length) {
52
+ const delay = this.backOff[i] * 1000;
53
+ await new Promise((resolve) => setTimeout(resolve, delay));
54
+ return this._request(url, options, i + 1);
55
+ } else if (status === 401) {
56
+ if (!this.isRefreshable || this.refreshCount > 0) {
57
+ await this.notify(this.DLGT_INVALID_AUTH);
58
+ } else {
59
+ this.refreshCount++;
60
+ // this.isRefreshable = false; // Set so that if we 401 during refresh request, we hit the above block
61
+ await this.refreshAuth();
62
+ // this.isRefreshable = true;// Set so that we can retry later? in case it's a fast expiring auth
63
+ this.refreshCount = 0;
64
+ return this._request(url, options, i + 1); // Retries
65
+ }
66
+ }
67
+
68
+ // If the error wasn't retried, throw.
69
+ if (status >= 400) {
70
+ throw await FetchError.create({
71
+ resource: encodedUrl,
72
+ init: options,
73
+ response,
74
+ });
75
+ }
76
+
77
+ return options.returnFullRes
78
+ ? response
79
+ : await this.parsedBody(response);
80
+ }
81
+
82
+ async _get(options) {
83
+ const fetchOptions = {
84
+ method: 'GET',
85
+ credentials: 'include',
86
+ headers: options.headers || {},
87
+ query: options.query || {},
88
+ returnFullRes: options.returnFullRes || false,
89
+ };
90
+
91
+ const res = await this._request(options.url, fetchOptions);
92
+ return res;
93
+ }
94
+
95
+ async _post(options, stringify = true) {
96
+ const fetchOptions = {
97
+ method: 'POST',
98
+ credentials: 'include',
99
+ headers: options.headers || {},
100
+ query: options.query || {},
101
+ body: JSON.stringify(options.body),
102
+ returnFullRes: options.returnFullRes || false,
103
+ };
104
+ if (!stringify) {
105
+ fetchOptions.body = options.body;
106
+ }
107
+ const res = await this._request(options.url, fetchOptions);
108
+ return res;
109
+ }
110
+
111
+ async _patch(options) {
112
+ const fetchOptions = {
113
+ method: 'PATCH',
114
+ credentials: 'include',
115
+ headers: options.headers || {},
116
+ query: options.query || {},
117
+ body: JSON.stringify(options.body),
118
+ returnFullRes: options.returnFullRes || false,
119
+ };
120
+ const res = await this._request(options.url, fetchOptions);
121
+ return res;
122
+ }
123
+
124
+ async _put(options) {
125
+ const fetchOptions = {
126
+ method: 'PUT',
127
+ credentials: 'include',
128
+ headers: options.headers || {},
129
+ query: options.query || {},
130
+ body: JSON.stringify(options.body),
131
+ returnFullRes: options.returnFullRes || false,
132
+ };
133
+ const res = await this._request(options.url, fetchOptions);
134
+ return res;
135
+ }
136
+
137
+ async _delete(options) {
138
+ const fetchOptions = {
139
+ method: 'DELETE',
140
+ credentials: 'include',
141
+ headers: options.headers || {},
142
+ query: options.query || {},
143
+ returnFullRes: options.returnFullRes || true,
144
+ };
145
+ return this._request(options.url, fetchOptions);
146
+ }
147
+
148
+ async refreshAuth() {
149
+ throw new Error('refreshAuth not yet defined in child of Requester');
150
+ }
151
+ }
152
+
153
+ module.exports = Requester;
package/Worker.js ADDED
@@ -0,0 +1,91 @@
1
+ const AWS = require('aws-sdk');
2
+ const _ = require('lodash');
3
+ const {
4
+ RequiredPropertyError,
5
+ } = require('@friggframework/errors/ValidationErrors');
6
+ const { get } = require('@friggframework/assertions');
7
+
8
+ AWS.config.update({ region: process.env.AWS_REGION });
9
+ const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
10
+
11
+ class Worker {
12
+ constructor(params) {
13
+ super(params);
14
+ }
15
+
16
+ async getQueueURL(params) {
17
+ // Passing params in because there will be multiple QueueNames
18
+ // let params = {
19
+ // QueueName: process.env.QueueName
20
+ // };
21
+ return new Promise((resolve, reject) => {
22
+ sqs.getQueueUrl(params, (err, data) => {
23
+ if (err) {
24
+ reject(err);
25
+ } else {
26
+ resolve(data.QueueUrl);
27
+ }
28
+ });
29
+ });
30
+ }
31
+
32
+ async run(params) {
33
+ const records = get(params, 'Records');
34
+
35
+ for (const record of records) {
36
+ const runParams = JSON.parse(record.body);
37
+ this._validateParams(runParams);
38
+ await this._run(runParams);
39
+ }
40
+ }
41
+
42
+ async _run(params) {
43
+ // validate params and instantiate any class to do work based on the
44
+ // parameters
45
+ }
46
+
47
+ // returns the message id
48
+ async send(params, delay = 0) {
49
+ this._validateParams(params);
50
+
51
+ const queueURL = params.QueueUrl;
52
+
53
+ const messageParams = _.omit(params, 'QueueUrl');
54
+ const args = {
55
+ DelaySeconds: delay,
56
+ MessageBody: JSON.stringify(messageParams),
57
+ QueueUrl: queueURL,
58
+ };
59
+ return this.sendAsyncSQSMessage(args);
60
+ }
61
+
62
+ async sendAsyncSQSMessage(params) {
63
+ return new Promise((resolve, reject) => {
64
+ sqs.sendMessage(params, (err, data) => {
65
+ if (err) {
66
+ reject(err);
67
+ } else {
68
+ resolve(data.MessageId);
69
+ }
70
+ });
71
+ });
72
+ }
73
+
74
+ // Throw an exception if the params do not validate
75
+ _validateParams(params) {}
76
+
77
+ _verifyParamExists(params, param) {
78
+ if (!(param in params)) {
79
+ throw new RequiredPropertyError({
80
+ parent: this,
81
+ key: param,
82
+ });
83
+ }
84
+ }
85
+
86
+ // async deleteSQSMessage(id){
87
+
88
+ // }
89
+ }
90
+
91
+ module.exports = Worker;
@@ -0,0 +1,30 @@
1
+ const Requester = require('../Requester.js');
2
+
3
+ class ApiKeyBase extends Requester {
4
+ constructor(params) {
5
+ super(params);
6
+ this.API_KEY_NAME = 'key';
7
+ this.API_KEY_VALUE = null;
8
+ }
9
+
10
+ async addAuthHeaders(headers) {
11
+ if (this.API_KEY_VALUE) {
12
+ headers[this.API_KEY_NAME] = this.API_KEY_VALUE;
13
+ }
14
+ return headers;
15
+ }
16
+
17
+ isAuthenticated() {
18
+ return (
19
+ this.API_KEY_VALUE !== null &&
20
+ this.API_KEY_VALUE !== undefined &&
21
+ this.API_KEY_VALUE.trim().length() > 0
22
+ );
23
+ }
24
+
25
+ setApiKey(api_key) {
26
+ this.API_KEY_VALUE = api_key;
27
+ }
28
+ }
29
+
30
+ module.exports = ApiKeyBase;
@@ -0,0 +1,203 @@
1
+ const moment = require('moment');
2
+ const Requester = require('../Requester.js');
3
+ const { get } = require('@friggframework/assertions');
4
+
5
+ class OAuth2Base extends Requester {
6
+ constructor(params) {
7
+ super(params);
8
+ this.DLGT_TOKEN_UPDATE = 'TOKEN_UPDATE';
9
+ this.DLGT_TOKEN_DEAUTHORIZED = 'TOKEN_DEAUTHORIZED';
10
+
11
+ this.delegateTypes.push(this.DLGT_TOKEN_UPDATE);
12
+ this.delegateTypes.push(this.DLGT_TOKEN_DEAUTHORIZED);
13
+
14
+ this.grantType = get(params, 'grantType', 'authorization_code');
15
+ this.key = get(params, 'key', null);
16
+ this.secret = get(params, 'secret', null);
17
+ this.redirectUri = get(params, 'redirectUri', null);
18
+ this.authorizationUri = get(params, 'authorizationUri', null);
19
+ this.baseURL = get(params, 'baseURL', null);
20
+ this.access_token = get(params, 'access_token', null);
21
+ this.refresh_token = get(params, 'refresh_token', null);
22
+ this.accessTokenExpire = get(params, 'accessTokenExpire', null);
23
+ this.refreshTokenExpire = get(params, 'refreshTokenExpire', null);
24
+ this.audience = get(params, 'audience', null);
25
+ this.username = get(params, 'username', null);
26
+ this.password = get(params, 'password', null);
27
+
28
+ this.isRefreshable = true;
29
+ }
30
+
31
+ async setTokens(params) {
32
+ this.access_token = get(params, 'access_token');
33
+ this.refresh_token = get(params, 'refresh_token', null);
34
+ const accessExpiresIn = get(params, 'expires_in', null);
35
+ const refreshExpiresIn = get(
36
+ params,
37
+ 'x_refresh_token_expires_in',
38
+ null
39
+ );
40
+
41
+ this.accessTokenExpire = moment().add(accessExpiresIn, 'seconds');
42
+ this.refreshTokenExpire = moment().add(refreshExpiresIn, 'seconds');
43
+
44
+ await this.notify(this.DLGT_TOKEN_UPDATE);
45
+ }
46
+
47
+ getAuthorizationUri() {
48
+ return this.authorizationUri;
49
+ }
50
+
51
+ // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
52
+ // will need to be defined in the child class before super(params)
53
+ async getTokenFromCode(code) {
54
+ const params = new URLSearchParams();
55
+ params.append('grant_type', 'authorization_code');
56
+ params.append('client_id', this.client_id);
57
+ params.append('client_secret', this.client_secret);
58
+ params.append('redirect_uri', this.redirect_uri);
59
+ params.append('code', code);
60
+ const options = {
61
+ body: params,
62
+ headers: {
63
+ 'Content-Type': 'application/x-www-form-urlencoded',
64
+ },
65
+ url: this.tokenUri,
66
+ };
67
+ const response = await this._post(options, false);
68
+ await this.setTokens(response);
69
+ return response;
70
+ }
71
+
72
+ // REPLACE getTokenFromCode IN THE CHILD IF NEEDED
73
+ // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
74
+ // will need to be defined in the child class before super(params)
75
+ async getTokenFromCodeBasicAuthHeader(code) {
76
+ const params = new URLSearchParams();
77
+ params.append('grant_type', 'authorization_code');
78
+ params.append('client_id', this.client_id);
79
+ params.append('redirect_uri', this.redirect_uri);
80
+ params.append('code', code);
81
+
82
+ const options = {
83
+ body: params,
84
+ headers: {
85
+ 'Content-Type': 'application/x-www-form-urlencoded',
86
+ Authorization: `Basic ${Buffer.from(
87
+ `${this.client_id}:${this.client_secret}`
88
+ ).toString('base64')}`,
89
+ },
90
+ url: this.tokenUri,
91
+ };
92
+
93
+ const response = await this._post(options, false);
94
+ await this.setTokens(response);
95
+ return response;
96
+ }
97
+
98
+ // this.client_id, this.client_secret, this.redirect_uri, and this.tokenUri
99
+ // will need to be defined in the child class before super(params)
100
+ async refreshAccessToken(refreshTokenObject) {
101
+ this.access_token = undefined;
102
+ const params = new URLSearchParams();
103
+ params.append('grant_type', 'refresh_token');
104
+ params.append('client_id', this.client_id);
105
+ params.append('client_secret', this.client_secret);
106
+ params.append('refresh_token', refreshTokenObject.refresh_token);
107
+ params.append('redirect_uri', this.redirect_uri);
108
+
109
+ const options = {
110
+ body: params,
111
+ url: this.tokenUri,
112
+ };
113
+ const response = await this._post(options, false);
114
+ await this.setTokens(response);
115
+ return response;
116
+ }
117
+
118
+ async addAuthHeaders(headers) {
119
+ if (this.access_token) {
120
+ headers.Authorization = `Bearer ${this.access_token}`;
121
+ }
122
+
123
+ return headers;
124
+ }
125
+
126
+ isAuthenticated() {
127
+ return (
128
+ this.accessToken !== null &&
129
+ this.refreshToken !== null &&
130
+ this.accessTokenExpire &&
131
+ this.refreshTokenExpire
132
+ );
133
+ }
134
+
135
+ async refreshAuth() {
136
+ try {
137
+ if (this.grantType !== 'client_credentials') {
138
+ await this.refreshAccessToken({
139
+ refresh_token: this.refresh_token,
140
+ });
141
+ } else {
142
+ await this.getTokenFromClientCredentials();
143
+ }
144
+ } catch {
145
+ await this.notify(this.DLGT_INVALID_AUTH);
146
+ }
147
+ }
148
+
149
+ async getTokenFromUsernamePassword() {
150
+ try {
151
+ const url = this.tokenUri;
152
+
153
+ const body = {
154
+ username: this.username,
155
+ password: this.password,
156
+ grant_type: 'password',
157
+ };
158
+ const headers = {
159
+ 'Content-Type': 'application/json',
160
+ };
161
+
162
+ const tokenRes = await this._post({
163
+ url,
164
+ body,
165
+ headers,
166
+ });
167
+
168
+ await this.setTokens(tokenRes);
169
+ return tokenRes;
170
+ } catch {
171
+ await this.notify(this.DLGT_INVALID_AUTH);
172
+ }
173
+ }
174
+
175
+ async getTokenFromClientCredentials() {
176
+ try {
177
+ const url = this.tokenUri;
178
+
179
+ const body = {
180
+ audience: this.audience,
181
+ client_id: this.client_id,
182
+ client_secret: this.client_secret,
183
+ grant_type: 'client_credentials',
184
+ };
185
+ const headers = {
186
+ 'Content-Type': 'application/json',
187
+ };
188
+
189
+ const tokenRes = await this._post({
190
+ url,
191
+ body,
192
+ headers,
193
+ });
194
+
195
+ await this.setTokens(tokenRes);
196
+ return tokenRes;
197
+ } catch {
198
+ await this.notify(this.DLGT_INVALID_AUTH);
199
+ }
200
+ }
201
+ }
202
+
203
+ module.exports = OAuth2Base;
@@ -0,0 +1,22 @@
1
+ const Delegate = require('../Delegate');
2
+
3
+ class IntegrationConfigManager extends Delegate {
4
+ constructor(params) {
5
+ super(params);
6
+ this.primary = null;
7
+ this.options = [];
8
+ }
9
+
10
+ async getIntegrationOptions() {
11
+ return {
12
+ entities: {
13
+ primary: this.primary.getName(),
14
+ options: this.options.map((val) => val.get()),
15
+ authorized: [],
16
+ },
17
+ integrations: [],
18
+ };
19
+ }
20
+ }
21
+
22
+ module.exports = IntegrationConfigManager;