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