@suprsend/node-sdk 0.1.1 → 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.
package/src/utils.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import os from "os";
2
2
  import fs from "fs";
3
+ import { Validator } from "jsonschema";
3
4
  import { v4 as uuidv4 } from "uuid";
5
+ import {
6
+ WORKFLOW_RUNTIME_KEYS_POTENTIAL_SIZE_IN_BYTES,
7
+ ATTACHMENT_URL_POTENTIAL_SIZE_IN_BYTES,
8
+ ATTACHMENT_UPLOAD_ENABLED,
9
+ ALLOW_ATTACHMENTS_IN_BULK_API,
10
+ } from "./constants";
11
+ import { cloneDeep } from "lodash";
12
+
13
+ const workflow_schema = require("./request_json/workflow.json");
14
+ const event_schema = require("./request_json/event.json");
4
15
 
5
16
  export function base64Encode(file) {
6
17
  var body = fs.readFileSync(file);
@@ -26,6 +37,13 @@ export class SuprsendError extends Error {
26
37
  }
27
38
  }
28
39
 
40
+ export class SuprsendConfigError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "SuprsendConfigError";
44
+ }
45
+ }
46
+
29
47
  export function is_string(value) {
30
48
  return typeof value === "string";
31
49
  }
@@ -39,6 +57,8 @@ export function is_empty(value) {
39
57
  return Object.keys(value) <= 0;
40
58
  } else if (Array.isArray(value)) {
41
59
  return value.length <= 0;
60
+ } else {
61
+ return !value;
42
62
  }
43
63
  }
44
64
 
@@ -53,3 +73,119 @@ export function uuid() {
53
73
  export function epoch_milliseconds() {
54
74
  return Math.round(Date.now());
55
75
  }
76
+
77
+ export function validate_workflow_body_schema(body) {
78
+ if (!body?.data) {
79
+ body.data = {};
80
+ }
81
+ if (!(body.data instanceof Object)) {
82
+ throw new SuprsendError("data must be a object");
83
+ }
84
+ const schema = workflow_schema;
85
+ var v = new Validator();
86
+ const validated_data = v.validate(body, schema);
87
+ if (validated_data.valid) {
88
+ return body;
89
+ } else {
90
+ const error_obj = validated_data.errors[0];
91
+ const error_msg = `${error_obj.property} ${error_obj.message}`;
92
+ throw new SuprsendError(error_msg);
93
+ }
94
+ }
95
+
96
+ export function validate_track_event_schema(body) {
97
+ if (!body?.properties) {
98
+ body.properties = {};
99
+ }
100
+ const schema = event_schema;
101
+ var v = new Validator();
102
+ const validated_data = v.validate(body, schema);
103
+ if (validated_data.valid) {
104
+ return body;
105
+ } else {
106
+ const error_obj = validated_data.errors[0];
107
+ const error_msg = `${error_obj.property} ${error_obj.message}`;
108
+ throw new SuprsendError(error_msg);
109
+ }
110
+ }
111
+
112
+ export function get_apparent_workflow_body_size(body, is_part_of_bulk) {
113
+ let extra_bytes = WORKFLOW_RUNTIME_KEYS_POTENTIAL_SIZE_IN_BYTES;
114
+ let apparent_body = body;
115
+ if (body?.data["$attachments"]) {
116
+ const num_attachments = body.data["$attachments"].length;
117
+ if (is_part_of_bulk) {
118
+ if (ALLOW_ATTACHMENTS_IN_BULK_API) {
119
+ if (ATTACHMENT_UPLOAD_ENABLED) {
120
+ extra_bytes +=
121
+ num_attachments * ATTACHMENT_URL_POTENTIAL_SIZE_IN_BYTES;
122
+ apparent_body = cloneDeep(body);
123
+ for (let attach_data of apparent_body["data"]["$attachments"]) {
124
+ delete attach_data["data"];
125
+ }
126
+ } else {
127
+ // pass
128
+ }
129
+ } else {
130
+ apparent_body = cloneDeep(body);
131
+ delete apparent_body["data"]["$attachments"];
132
+ }
133
+ } else {
134
+ if (ATTACHMENT_UPLOAD_ENABLED) {
135
+ extra_bytes += num_attachments * ATTACHMENT_URL_POTENTIAL_SIZE_IN_BYTES;
136
+ apparent_body = cloneDeep(body);
137
+ for (let attach_data of apparent_body["data"]["$attachments"]) {
138
+ delete attach_data["data"];
139
+ }
140
+ } else {
141
+ // pass
142
+ }
143
+ }
144
+ }
145
+ const body_size = JSON.stringify(apparent_body).length;
146
+ const apparent_body_size = body_size + extra_bytes;
147
+ return apparent_body_size;
148
+ }
149
+
150
+ export function get_apparent_event_size(event, is_part_of_bulk) {
151
+ let extra_bytes = 0;
152
+ let apparent_body = event;
153
+ if (event?.properties?.["$attachments"]) {
154
+ const num_attachments = event.properties["$attachments"].length;
155
+ if (is_part_of_bulk) {
156
+ if (ALLOW_ATTACHMENTS_IN_BULK_API) {
157
+ if (ATTACHMENT_UPLOAD_ENABLED) {
158
+ extra_bytes +=
159
+ num_attachments * ATTACHMENT_URL_POTENTIAL_SIZE_IN_BYTES;
160
+ apparent_body = cloneDeep(event);
161
+ for (let attach_data of apparent_body["properties"]["$attachments"]) {
162
+ delete attach_data["data"];
163
+ }
164
+ } else {
165
+ // pass
166
+ }
167
+ } else {
168
+ apparent_body = cloneDeep(body);
169
+ delete apparent_body["properties"]["$attachments"];
170
+ }
171
+ } else {
172
+ if (ATTACHMENT_UPLOAD_ENABLED) {
173
+ extra_bytes += num_attachments * ATTACHMENT_URL_POTENTIAL_SIZE_IN_BYTES;
174
+ apparent_body = cloneDeep(body);
175
+ for (let attach_data of apparent_body["properties"]["$attachments"]) {
176
+ delete attach_data["data"];
177
+ }
178
+ } else {
179
+ // pass
180
+ }
181
+ }
182
+ }
183
+ const body_size = JSON.stringify(apparent_body).length;
184
+ const apparent_size = body_size + extra_bytes;
185
+ return apparent_size;
186
+ }
187
+
188
+ export function get_apparent_identity_event_size(event) {
189
+ const body_size = JSON.stringify(event);
190
+ return body_size;
191
+ }
package/src/workflow.js CHANGED
@@ -1,85 +1,134 @@
1
- import get_request_signature from "./signature";
2
- import { Validator } from "jsonschema";
3
- import { SuprsendError } from "./utils";
4
1
  import axios from "axios";
2
+ import get_request_signature from "./signature";
3
+ import {
4
+ SuprsendError,
5
+ validate_workflow_body_schema,
6
+ get_apparent_workflow_body_size,
7
+ } from "./utils";
8
+ import get_attachment_json_for_file from "./attachment";
9
+ import {
10
+ BODY_MAX_APPARENT_SIZE_IN_BYTES,
11
+ BODY_MAX_APPARENT_SIZE_IN_BYTES_READABLE,
12
+ } from "./constants";
13
+
14
+ export default class Workflow {
15
+ constructor(body, idempotency_key) {
16
+ if (!(body instanceof Object)) {
17
+ throw new SuprsendError("workflow body must be a json/dictionary");
18
+ }
19
+ this.body = body;
20
+ this.idempotency_key = idempotency_key;
21
+ }
5
22
 
6
- const workflow_schema = require("./request_json/workflow.json");
23
+ add_attachment(file_path = "") {
24
+ if (!this.body.data) {
25
+ this.body.data = {};
26
+ }
27
+ if (!(this.body instanceof Object)) {
28
+ throw new SuprsendError("data must be a dictionary");
29
+ }
30
+ const attachment = get_attachment_json_for_file(file_path);
7
31
 
8
- class Workflow {
9
- constructor(ss_instance, data) {
10
- this.ss_instance = ss_instance;
11
- this.data = data;
32
+ if (!this.body.data["$attachments"]) {
33
+ this.body["data"]["$attachments"] = [];
34
+ }
35
+ this.body["data"]["$attachments"].push(attachment);
36
+ }
37
+
38
+ get_final_json(config, is_part_of_bulk = false) {
39
+ // add idempotency key in body if present
40
+ if (this.idempotency_key) {
41
+ this.body["$idempotency_key"] = this.idempotency_key;
42
+ }
43
+ this.body = validate_workflow_body_schema(this.body);
44
+ const apparent_size = get_apparent_workflow_body_size(
45
+ this.body,
46
+ is_part_of_bulk
47
+ ); // review
48
+ if (apparent_size > BODY_MAX_APPARENT_SIZE_IN_BYTES) {
49
+ throw new SuprsendError(
50
+ `workflow body (discounting attachment if any) too big - ${apparent_size} Bytes, must not cross ${BODY_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
51
+ );
52
+ }
53
+ return [this.body, apparent_size];
54
+ }
55
+ }
56
+
57
+ export class _WorkflowTrigger {
58
+ constructor(config) {
59
+ this.config = config;
12
60
  this.url = this._get_url();
13
61
  }
14
62
 
15
63
  _get_url() {
16
64
  let url_template = "/trigger/";
17
- if (this.ss_instance.include_signature_param) {
18
- if (this.ss_instance.auth_enabled) {
65
+ if (this.config.include_signature_param) {
66
+ if (this.config.auth_enabled) {
19
67
  url_template = url_template + "?verify=true";
20
68
  } else {
21
69
  url_template = url_template + "?verify=false";
22
70
  }
23
71
  }
24
- const url_formatted = `${this.ss_instance.base_url}${this.ss_instance.env_key}${url_template}`;
72
+ const url_formatted = `${this.config.base_url}${this.config.workspace_key}${url_template}`;
25
73
  return url_formatted;
26
74
  }
27
75
 
28
76
  _get_headers() {
29
77
  return {
30
- "Content-Type": "application/json",
78
+ "Content-Type": "application/json; charset=utf-8",
31
79
  Date: new Date().toUTCString(),
32
- "User-Agent": this.ss_instance.user_agent,
80
+ "User-Agent": this.config.user_agent,
33
81
  };
34
82
  }
35
83
 
36
- async execute_workflow() {
84
+ trigger(workflow) {
85
+ const is_part_of_bulk = false;
86
+ const [workflow_body, body_size] = workflow.get_final_json(
87
+ this.config,
88
+ is_part_of_bulk
89
+ );
90
+ return this.send(workflow_body);
91
+ }
92
+
93
+ async send(workflow_body) {
37
94
  const headers = this._get_headers();
38
- const content_text = JSON.stringify(this.data);
39
- if (this.ss_instance.auth_enabled) {
95
+ const content_text = JSON.stringify(workflow_body);
96
+ if (this.config.auth_enabled) {
40
97
  const signature = get_request_signature(
41
98
  this.url,
42
99
  "POST",
43
100
  content_text,
44
101
  headers,
45
- this.ss_instance.env_secret
102
+ this.config.workspace_secret
46
103
  );
47
- headers["Authorization"] = `${this.ss_instance.env_key}:${signature}`;
104
+ headers["Authorization"] = `${this.config.workspace_key}:${signature}`;
48
105
  }
106
+
49
107
  try {
50
108
  const response = await axios.post(this.url, content_text, { headers });
51
- return {
52
- status_code: response.status,
53
- success: true,
54
- message: response.statusText,
55
- };
109
+ const ok_response = Math.floor(response.status / 100) == 2;
110
+ if (ok_response) {
111
+ return {
112
+ success: true,
113
+ status: "success",
114
+ status_code: response.status,
115
+ message: response.statusText,
116
+ };
117
+ } else {
118
+ return {
119
+ success: false,
120
+ status: "fail",
121
+ status_code: response.status,
122
+ message: response.statusText,
123
+ };
124
+ }
56
125
  } catch (err) {
57
126
  return {
58
- status_code: 400,
59
127
  success: false,
128
+ status: "fail",
129
+ status_code: err.status || 500,
60
130
  message: err.message,
61
131
  };
62
132
  }
63
133
  }
64
-
65
- validate_data() {
66
- if (!this.data?.data) {
67
- this.data.data = {};
68
- }
69
- if (!(this.data.data instanceof Object)) {
70
- throw new SuprsendError("data must be a object");
71
- }
72
- const schema = workflow_schema;
73
- var v = new Validator();
74
- const validated_data = v.validate(this.data, schema);
75
- if (validated_data.valid) {
76
- return this.data;
77
- } else {
78
- const error_obj = validated_data.errors[0];
79
- const error_msg = `${error_obj.property} ${error_obj.message}`;
80
- throw new SuprsendError(error_msg);
81
- }
82
- }
83
134
  }
84
-
85
- export default Workflow;
@@ -0,0 +1,234 @@
1
+ import {
2
+ BODY_MAX_APPARENT_SIZE_IN_BYTES,
3
+ BODY_MAX_APPARENT_SIZE_IN_BYTES_READABLE,
4
+ MAX_WORKFLOWS_IN_BULK_API,
5
+ ALLOW_ATTACHMENTS_IN_BULK_API,
6
+ } from "./constants";
7
+ import { SuprsendError } from "./utils";
8
+ import Workflow from "./workflow";
9
+ import { cloneDeep } from "lodash";
10
+ import axios from "axios";
11
+ import BulkResponse from "./bulk_response";
12
+ import get_request_signature from "./signature";
13
+
14
+ export class BulkWorkflowsFactory {
15
+ constructor(config) {
16
+ this.config = config;
17
+ }
18
+
19
+ new_instance() {
20
+ return new BulkWorkflows(this.config);
21
+ }
22
+ }
23
+
24
+ class _BulkWorkflowsChunk {
25
+ constructor(config) {
26
+ this.config = config;
27
+ this.__chunk = [];
28
+ this.__url = this.__get_url();
29
+ this.__headers = this.__common_headers();
30
+
31
+ this.__running_size = 0;
32
+ this.__running_length = 0;
33
+ this.response;
34
+ }
35
+
36
+ __get_url() {
37
+ let url_template = "/trigger/";
38
+ if (this.config.include_signature_param) {
39
+ if (this.config.auth_enabled) {
40
+ url_template = url_template + "?verify=true";
41
+ } else {
42
+ url_template = url_template + "?verify=false";
43
+ }
44
+ }
45
+ const url_formatted = `${this.config.base_url}${this.config.workspace_key}${url_template}`;
46
+ return url_formatted;
47
+ }
48
+
49
+ __common_headers() {
50
+ return {
51
+ "Content-Type": "application/json; charset=utf-8",
52
+ "User-Agent": this.config.user_agent,
53
+ };
54
+ }
55
+
56
+ __dynamic_headers() {
57
+ return {
58
+ Date: new Date().toUTCString(),
59
+ };
60
+ }
61
+
62
+ __add_body_to_chunk(body, body_size) {
63
+ // First add size, then body to reduce effects of race condition
64
+ this.__running_size += body_size;
65
+ this.__chunk.push(body);
66
+ this.__running_length += 1;
67
+ }
68
+
69
+ __check_limit_reached() {
70
+ if (
71
+ this.__running_length >= MAX_WORKFLOWS_IN_BULK_API ||
72
+ this.__running_size >= BODY_MAX_APPARENT_SIZE_IN_BYTES
73
+ ) {
74
+ return true;
75
+ } else {
76
+ return false;
77
+ }
78
+ }
79
+
80
+ try_to_add_into_chunk(body, body_size) {
81
+ if (!body) {
82
+ return true;
83
+ }
84
+ if (this.__check_limit_reached()) {
85
+ return false;
86
+ }
87
+ if (body_size > BODY_MAX_APPARENT_SIZE_IN_BYTES) {
88
+ throw new SuprsendError(
89
+ `workflow body (discounting attachment if any) too big - ${body_size} Bytes, must not cross ${BODY_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
90
+ );
91
+ }
92
+ if (this.__running_size + body_size > BODY_MAX_APPARENT_SIZE_IN_BYTES) {
93
+ return false;
94
+ }
95
+ if (!ALLOW_ATTACHMENTS_IN_BULK_API) {
96
+ delete body.data["$attachments"];
97
+ }
98
+
99
+ this.__add_body_to_chunk(body, body_size);
100
+ return true;
101
+ }
102
+
103
+ async trigger() {
104
+ const headers = { ...this.__headers, ...this.__dynamic_headers() };
105
+ const content_text = JSON.stringify(this.__chunk);
106
+ // Based on whether signature is required or not, add Authorization header
107
+ if (this.config.auth_enabled) {
108
+ const signature = get_request_signature(
109
+ this.__url,
110
+ "POST",
111
+ content_text,
112
+ headers,
113
+ this.config.workspace_secret
114
+ );
115
+ headers["Authorization"] = `${this.config.workspace_key}:${signature}`;
116
+ }
117
+ try {
118
+ const response = await axios.post(this.__url, content_text, { headers });
119
+ const ok_response = Math.floor(response.status / 100) == 2;
120
+ if (ok_response) {
121
+ this.response = {
122
+ status: "success",
123
+ status_code: response.status,
124
+ total: this.__chunk.length,
125
+ success: this.__chunk.length,
126
+ failure: 0,
127
+ failed_records: [],
128
+ };
129
+ } else {
130
+ this.response = {
131
+ status: "fail",
132
+ status_code: response.status,
133
+ total: this.__chunk.length,
134
+ success: 0,
135
+ failure: this.__chunk.length,
136
+ failed_records: this.__chunk.map((item) => ({
137
+ record: item,
138
+ error: response.statusText,
139
+ code: response.status,
140
+ })),
141
+ };
142
+ }
143
+ } catch (err) {
144
+ const error_status = err.status || 500;
145
+ return {
146
+ status: "fail",
147
+ status_code: error_status,
148
+ message: err.message,
149
+ total: this.__chunk.length,
150
+ success: 0,
151
+ failure: this.__chunk.length,
152
+ failed_records: this.__chunk.map((item) => ({
153
+ record: item,
154
+ error: err.message,
155
+ code: error_status,
156
+ })),
157
+ };
158
+ }
159
+ }
160
+ }
161
+
162
+ class BulkWorkflows {
163
+ constructor(config) {
164
+ this.config = config;
165
+ this.__workflows = [];
166
+ this.__pending_records = [];
167
+ this.chunks = [];
168
+ this.response = new BulkResponse();
169
+ }
170
+
171
+ __validate_workflows() {
172
+ if (!this.__workflows) {
173
+ throw new SuprsendError("workflow list is empty in bulk request");
174
+ }
175
+ for (let wf of this.__workflows) {
176
+ const is_part_of_bulk = true;
177
+ const [wf_body, body_size] = wf.get_final_json(
178
+ this.config,
179
+ is_part_of_bulk
180
+ );
181
+ this.__pending_records.push([wf_body, body_size]);
182
+ }
183
+ }
184
+
185
+ __chunkify(start_idx = 0) {
186
+ const curr_chunk = new _BulkWorkflowsChunk(this.config);
187
+ this.chunks.push(curr_chunk);
188
+ const entries = this.__pending_records.slice(start_idx).entries();
189
+ for (const [rel_idx, rec] of entries) {
190
+ const is_added = curr_chunk.try_to_add_into_chunk(rec[0], rec[1]);
191
+ if (!is_added) {
192
+ // create chunks from remaining records
193
+ this.__chunkify(start_idx + rel_idx);
194
+ // Don't forget to break. As current loop must not continue further
195
+ break;
196
+ }
197
+ }
198
+ }
199
+
200
+ append(...workflows) {
201
+ if (!workflows) {
202
+ throw new SuprsendError(
203
+ "workflow list empty. must pass one or more valid workflow instances"
204
+ );
205
+ }
206
+ for (let wf of workflows) {
207
+ if (!wf) {
208
+ throw new SuprsendError("null/empty element found in bulk instance");
209
+ }
210
+ if (!(wf instanceof Workflow)) {
211
+ throw new SuprsendError(
212
+ "element must be an instance of suprsend.Workflow"
213
+ );
214
+ }
215
+ const wf_copy = cloneDeep(wf);
216
+ this.__workflows.push(wf_copy);
217
+ }
218
+ }
219
+
220
+ async trigger() {
221
+ this.__validate_workflows();
222
+ this.__chunkify();
223
+ for (const [c_idx, ch] of this.chunks.entries()) {
224
+ if (this.config.req_log_level > 0) {
225
+ console.log(`DEBUG: triggering api call for chunk: ${c_idx}`);
226
+ }
227
+ // do api call
228
+ await ch.trigger();
229
+ // merge response
230
+ this.response.merge_chunk_response(ch.response);
231
+ }
232
+ return this.response;
233
+ }
234
+ }
package/dist/config.js DELETED
@@ -1,13 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports["default"] = void 0;
7
- var config = {
8
- staging: "https://collector-staging.suprsend.workers.dev/",
9
- prod: "https://hub.suprsend.com/"
10
- };
11
- var _default = config;
12
- exports["default"] = _default;
13
- module.exports = exports.default;
package/src/config.js DELETED
@@ -1,6 +0,0 @@
1
- const config = {
2
- staging: "https://collector-staging.suprsend.workers.dev/",
3
- prod: "https://hub.suprsend.com/",
4
- };
5
-
6
- export default config;