@suprsend/node-sdk 1.7.1 → 1.8.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/event.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  epoch_milliseconds,
7
7
  validate_track_event_schema,
8
8
  get_apparent_event_size,
9
+ InputValueError,
9
10
  } from "./utils";
10
11
  import get_request_signature from "./signature";
11
12
  import axios from "axios";
@@ -32,38 +33,36 @@ export default class Event {
32
33
  this.properties = properties;
33
34
  this.idempotency_key = kwargs?.idempotency_key;
34
35
  this.brand_id = kwargs?.brand_id;
35
- // --- validate
36
- this.__validate_distinct_id();
37
- this.__validate_event_name();
38
- this.__validate_properties();
36
+
37
+ // default values
38
+ if (!this.properties) {
39
+ this.properties = {};
40
+ }
39
41
  }
40
42
 
41
43
  __validate_distinct_id() {
42
- if (this.distinct_id instanceof String) {
43
- throw new SuprsendError(
44
+ if (typeof this.distinct_id !== "string") {
45
+ throw new InputValueError(
44
46
  "distinct_id must be a string. an Id which uniquely identify a user in your app"
45
47
  );
46
48
  }
47
49
  const distinct_id = this.distinct_id.trim();
48
50
  if (!distinct_id) {
49
- throw new SuprsendError("distinct_id missing");
51
+ throw new InputValueError("distinct_id missing");
50
52
  }
51
53
  this.distinct_id = distinct_id;
52
54
  }
53
55
 
54
56
  __validate_properties() {
55
- if (!this.properties) {
56
- this.properties = {};
57
- }
58
57
  if (!(this.properties instanceof Object)) {
59
- throw new SuprsendError("properties must be a dictionary");
58
+ throw new InputValueError("properties must be a dictionary");
60
59
  }
61
60
  }
62
61
 
63
62
  __check_event_prefix(event_name) {
64
63
  if (!RESERVED_EVENT_NAMES.includes(event_name)) {
65
64
  if (has_special_char(event_name)) {
66
- throw new SuprsendError(
65
+ throw new InputValueError(
67
66
  "event_names starting with [$,ss_] are reserved by SuprSend"
68
67
  );
69
68
  }
@@ -72,9 +71,12 @@ export default class Event {
72
71
 
73
72
  __validate_event_name() {
74
73
  if (!is_string(this.event_name)) {
75
- throw new SuprsendError("event_name must be a string");
74
+ throw new InputValueError("event_name must be a string");
76
75
  }
77
76
  const event_name = this.event_name.trim();
77
+ if (!event_name) {
78
+ throw new InputValueError("event_name missing");
79
+ }
78
80
  this.__check_event_prefix(event_name);
79
81
  this.event_name = event_name;
80
82
  }
@@ -82,11 +84,25 @@ export default class Event {
82
84
  add_attachment(file_path, kwargs = {}) {
83
85
  const file_name = kwargs?.file_name;
84
86
  const ignore_if_error = kwargs?.ignore_if_error ?? false;
87
+
88
+ // if properties is not a dict, not raising error while adding attachment.
89
+ if (!(this.properties instanceof Object)) {
90
+ console.log(
91
+ "WARNING: attachment cannot be added. please make sure properties is a dictionary. Event" +
92
+ JSON.stringify(this.as_json())
93
+ );
94
+ return;
95
+ }
96
+
85
97
  const attachment = get_attachment_json(
86
98
  file_path,
87
99
  file_name,
88
100
  ignore_if_error
89
101
  );
102
+
103
+ if (!attachment) {
104
+ return;
105
+ }
90
106
  // --- add the attachment to properties->$attachments
91
107
  if (!this.properties["$attachments"]) {
92
108
  this.properties["$attachments"] = [];
@@ -95,6 +111,11 @@ export default class Event {
95
111
  }
96
112
 
97
113
  get_final_json(config, is_part_of_bulk = false) {
114
+ // --- validate
115
+ this.__validate_distinct_id();
116
+ this.__validate_event_name();
117
+ this.__validate_properties();
118
+
98
119
  const super_props = { $ss_sdk_version: config.user_agent };
99
120
  let event_dict = {
100
121
  $insert_id: uuid(),
@@ -113,12 +134,27 @@ export default class Event {
113
134
  event_dict = validate_track_event_schema(event_dict);
114
135
  const apparent_size = get_apparent_event_size(event_dict, is_part_of_bulk);
115
136
  if (apparent_size > SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES) {
116
- throw new SuprsendError(
137
+ throw new InputValueError(
117
138
  `Event size too big - ${apparent_size} Bytes,must not cross ${SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
118
139
  );
119
140
  }
120
141
  return [event_dict, apparent_size];
121
142
  }
143
+
144
+ as_json() {
145
+ const event_dict = {
146
+ event: this.event_name,
147
+ distinct_id: this.distinct_id,
148
+ properties: this.properties,
149
+ };
150
+ if (this.idempotency_key) {
151
+ event_dict["$idempotency_key"] = this.idempotency_key;
152
+ }
153
+ if (this.brand_id) {
154
+ event_dict["brand_id"] = this.brand_id;
155
+ }
156
+ return event_dict;
157
+ }
122
158
  }
123
159
 
124
160
  export class EventCollector {
@@ -9,7 +9,7 @@ import {
9
9
  import get_request_signature from "./signature";
10
10
  import BulkResponse from "./bulk_response";
11
11
  import Event from "./event";
12
- import { SuprsendError } from "./utils";
12
+ import { InputValueError, SuprsendError, invalid_record_json } from "./utils";
13
13
  import { cloneDeep } from "lodash";
14
14
  import axios from "axios";
15
15
 
@@ -79,7 +79,7 @@ class _BulkEventsChunk {
79
79
  return false;
80
80
  }
81
81
  if (event_size > SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES) {
82
- throw new SuprsendError(
82
+ throw new InputValueError(
83
83
  `Event properties too big - ${event_size} Bytes, must not cross ${SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
84
84
  );
85
85
  }
@@ -159,19 +159,23 @@ class BulkEvents {
159
159
  this.__pending_records = [];
160
160
  this.chunks = [];
161
161
  this.response = new BulkResponse();
162
+ // invalid_record json: {"record": event-json, "error": error_str, "code": 500}
163
+ this.__invalid_records = [];
162
164
  }
163
165
 
164
166
  __validate_events() {
165
- if (!this.__events) {
166
- throw new SuprsendError("events list is empty in bulk request");
167
- }
168
167
  for (let ev of this.__events) {
169
- const is_part_of_bulk = true;
170
- const [ev_json, body_size] = ev.get_final_json(
171
- this.config,
172
- is_part_of_bulk
173
- );
174
- this.__pending_records.push([ev_json, body_size]);
168
+ try {
169
+ const is_part_of_bulk = true;
170
+ const [ev_json, body_size] = ev.get_final_json(
171
+ this.config,
172
+ is_part_of_bulk
173
+ );
174
+ this.__pending_records.push([ev_json, body_size]);
175
+ } catch (ex) {
176
+ const inv_rec = invalid_record_json(ev.as_json(), ex);
177
+ this.__invalid_records.push(inv_rec);
178
+ }
175
179
  }
176
180
  }
177
181
 
@@ -192,35 +196,44 @@ class BulkEvents {
192
196
 
193
197
  append(...events) {
194
198
  if (!events) {
195
- throw new SuprsendError(
196
- "events list empty. must pass one or more events"
197
- );
199
+ return;
198
200
  }
199
201
  for (let ev of events) {
200
- if (!ev) {
201
- throw new SuprsendError("null/empty element found in bulk instance");
202
- }
203
- if (!(ev instanceof Event)) {
204
- throw new SuprsendError(
205
- "element must be an instance of suprsend.Event"
206
- );
202
+ if (ev && ev instanceof Event) {
203
+ const ev_copy = cloneDeep(ev);
204
+ this.__events.push(ev_copy);
207
205
  }
208
- const ev_copy = cloneDeep(ev);
209
- this.__events.push(ev_copy);
210
206
  }
211
207
  }
212
208
 
213
209
  async trigger() {
214
210
  this.__validate_events();
215
- this.__chunkify();
216
- for (const [c_idx, ch] of this.chunks.entries()) {
217
- if (this.config.req_log_level > 0) {
218
- console.log(`DEBUG: triggering api call for chunk: ${c_idx}`);
211
+ if (this.__invalid_records.length > 0) {
212
+ const ch_response = BulkResponse.invalid_records_chunk_response(
213
+ this.__invalid_records
214
+ );
215
+ this.response.merge_chunk_response(ch_response);
216
+ }
217
+
218
+ if (this.__pending_records.length) {
219
+ this.__chunkify();
220
+ for (const [c_idx, ch] of this.chunks.entries()) {
221
+ if (this.config.req_log_level > 0) {
222
+ console.log(`DEBUG: triggering api call for chunk: ${c_idx}`);
223
+ }
224
+ // do api call
225
+ await ch.trigger();
226
+ // merge response
227
+ this.response.merge_chunk_response(ch.response);
228
+ }
229
+ } else {
230
+ // if no records. i.e. invalid_records.length and pending_records.length both are 0
231
+ // then add empty success response
232
+ if (this.__invalid_records.length === 0) {
233
+ this.response.merge_chunk_response(
234
+ BulkResponse.empty_chunk_success_response()
235
+ );
219
236
  }
220
- // do api call
221
- await ch.trigger();
222
- // merge response
223
- this.response.merge_chunk_response(ch.response);
224
237
  }
225
238
  return this.response;
226
239
  }
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SuprsendError, SuprsendConfigError } from "./utils";
1
+ import { SuprsendConfigError, InputValueError } from "./utils";
2
2
  import get_attachment_json from "./attachment";
3
3
  import Workflow, { _WorkflowTrigger } from "./workflow";
4
4
  import { BulkWorkflowsFactory } from "./workflows_bulk";
@@ -85,11 +85,11 @@ class Suprsend {
85
85
  add_attachment(body, file_path, kwargs = {}) {
86
86
  const file_name = kwargs?.file_name;
87
87
  const ignore_if_error = kwargs?.ignore_if_error ?? false;
88
- if (!body.data) {
88
+ if (!body?.data) {
89
89
  body.data = {};
90
90
  }
91
- if (!body.data instanceof Object) {
92
- throw new SuprsendError("data must be an object");
91
+ if (!(body.data instanceof Object)) {
92
+ throw new InputValueError("data must be an object");
93
93
  }
94
94
  const attachment = get_attachment_json(
95
95
  file_path,
@@ -120,7 +120,9 @@ class Suprsend {
120
120
 
121
121
  track_event(event) {
122
122
  if (!(event instanceof Event)) {
123
- throw new SuprsendError("argument must be an instance of suprsend.Event");
123
+ throw new InputValueError(
124
+ "argument must be an instance of suprsend.Event"
125
+ );
124
126
  }
125
127
  return this._eventcollector.collect(event);
126
128
  }
package/src/subscriber.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  is_empty,
7
7
  is_string,
8
8
  get_apparent_identity_event_size,
9
+ InputValueError,
9
10
  } from "./utils";
10
11
  import get_request_signature from "./signature";
11
12
  import axios from "axios";
@@ -27,13 +28,13 @@ export default class SubscriberFactory {
27
28
 
28
29
  get_instance(distinct_id) {
29
30
  if (!is_string(distinct_id)) {
30
- throw new SuprsendError(
31
+ throw new InputValueError(
31
32
  "distinct_id must be a string. an Id which uniquely identify a user in your app"
32
33
  );
33
34
  }
34
35
  distinct_id = distinct_id.trim();
35
36
  if (!distinct_id) {
36
- throw new SuprsendError("distinct_id must be passed");
37
+ throw new InputValueError("distinct_id must be passed");
37
38
  }
38
39
  return new Subscriber(this.config, distinct_id);
39
40
  }
@@ -50,6 +51,7 @@ export class Subscriber {
50
51
  this.__info = [];
51
52
  this.user_operations = [];
52
53
  this._helper = new _SubscriberInternalHelper();
54
+ this.__warnings_list = [];
53
55
  }
54
56
 
55
57
  __get_url() {
@@ -82,10 +84,20 @@ export class Subscriber {
82
84
  };
83
85
  }
84
86
 
87
+ as_json() {
88
+ const event_dict = {
89
+ distinct_id: this.distinct_id,
90
+ $user_operations: this.user_operations,
91
+ warnings: this.__warnings_list,
92
+ };
93
+
94
+ return event_dict;
95
+ }
96
+
85
97
  validate_event_size(event_dict) {
86
98
  const apparent_size = get_apparent_identity_event_size(event_dict);
87
99
  if (apparent_size > IDENTITY_SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES) {
88
- throw new SuprsendError(
100
+ throw new InputValueError(
89
101
  `User Event size too big - ${apparent_size} Bytes, must not cross ${IDENTITY_SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
90
102
  );
91
103
  }
@@ -93,25 +105,25 @@ export class Subscriber {
93
105
  }
94
106
 
95
107
  validate_body(is_part_of_bulk = false) {
96
- const warnings_list = [];
108
+ this.__warnings_list = [];
97
109
  if (!is_empty(this.__info)) {
98
110
  const msg = `[distinct_id: ${this.distinct_id}]${this.__info.join("\n")}`;
99
- warnings_list.push(msg);
111
+ this.__warnings_list.push(msg);
100
112
  console.log(`WARNING: ${msg}`);
101
113
  }
102
114
  if (!is_empty(this.__errors)) {
103
115
  const msg = `[distinct_id: ${this.distinct_id}] ${this.__errors.join(
104
116
  "\n"
105
117
  )}`;
106
- warnings_list.push(msg);
118
+ this.__warnings_list.push(msg);
107
119
  const err_msg = `ERROR: ${msg}`;
108
120
  if (is_part_of_bulk) {
109
121
  console.log(err_msg);
110
122
  } else {
111
- throw new SuprsendError(err_msg);
123
+ throw new InputValueError(err_msg);
112
124
  }
113
125
  }
114
- return warnings_list;
126
+ return this.__warnings_list;
115
127
  }
116
128
 
117
129
  async save() {
@@ -5,6 +5,7 @@ import {
5
5
  get_apparent_list_broadcast_body_size,
6
6
  uuid,
7
7
  epoch_milliseconds,
8
+ InputValueError,
8
9
  } from "./utils";
9
10
  import get_request_signature from "./signature";
10
11
  import axios from "axios";
@@ -12,17 +13,51 @@ import {
12
13
  SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES,
13
14
  SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE,
14
15
  } from "./constants";
16
+ import get_attachment_json from "./attachment";
15
17
 
16
18
  class SubscriberListBroadcast {
17
19
  constructor(body, kwargs = {}) {
18
20
  if (!(body instanceof Object)) {
19
- throw new SuprsendError("broadcast body must be a json/dictionary");
21
+ throw new InputValueError("broadcast body must be a json/dictionary");
20
22
  }
21
23
  this.body = body;
22
24
  this.idempotency_key = kwargs?.idempotency_key;
23
25
  this.brand_id = kwargs?.brand_id;
24
26
  }
25
27
 
28
+ add_attachment(file_path, kwargs = {}) {
29
+ const file_name = kwargs?.file_name;
30
+ const ignore_if_error = kwargs?.ignore_if_error ?? false;
31
+
32
+ if (!this.body?.["data"]) {
33
+ this.body["data"] = {};
34
+ }
35
+ // if body["data"] is not a dict, not raising error while adding attachment.
36
+ if (!(this.body["data"] instanceof Object)) {
37
+ console.log(
38
+ "WARNING: attachment cannot be added. please make sure body['data'] is a dictionary. " +
39
+ "SubscriberListBroadcast" +
40
+ JSON.stringify(this.as_json())
41
+ );
42
+ return;
43
+ }
44
+ const attachment = get_attachment_json(
45
+ file_path,
46
+ file_name,
47
+ ignore_if_error
48
+ );
49
+ if (!attachment) {
50
+ return;
51
+ }
52
+
53
+ // --- add the attachment to body->data->$attachments
54
+ if (!this.body["data"]?.["$attachments"]) {
55
+ this.body["data"]["$attachments"] = [];
56
+ }
57
+ // -----
58
+ this.body["data"]["$attachments"].push(attachment);
59
+ }
60
+
26
61
  get_final_json() {
27
62
  this.body["$insert_id"] = uuid();
28
63
  this.body["$time"] = epoch_milliseconds();
@@ -35,12 +70,24 @@ class SubscriberListBroadcast {
35
70
  this.body = validate_list_broadcast_body_schema(this.body);
36
71
  const apparent_size = get_apparent_list_broadcast_body_size(this.body);
37
72
  if (apparent_size > SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES) {
38
- throw new SuprsendError(
73
+ throw new InputValueError(
39
74
  `SubscriberListBroadcast body too big - ${apparent_size} Bytes, must not cross ${SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
40
75
  );
41
76
  }
42
77
  return [this.body, apparent_size];
43
78
  }
79
+
80
+ as_json() {
81
+ const body_dict = { ...this.body };
82
+ if (this.idempotency_key) {
83
+ body_dict["$idempotency_key"] = this.idempotency_key;
84
+ }
85
+ if (this.brand_id) {
86
+ body_dict["brand_id"] = this.brand_id;
87
+ }
88
+ // -----
89
+ return body_dict;
90
+ }
44
91
  }
45
92
 
46
93
  class SubscriberListsApi {
@@ -243,7 +290,7 @@ class SubscriberListsApi {
243
290
 
244
291
  async broadcast(broadcast_instance) {
245
292
  if (!(broadcast_instance instanceof SubscriberListBroadcast)) {
246
- throw new SuprsendError(
293
+ throw new InputValueError(
247
294
  "argument must be an instance of suprsend.SubscriberListBroadcast"
248
295
  );
249
296
  }
@@ -9,6 +9,7 @@ import BulkResponse from "./bulk_response";
9
9
  import { Subscriber } from "./subscriber";
10
10
  import { cloneDeep } from "lodash";
11
11
  import axios from "axios";
12
+ import { invalid_record_json, InputValueError } from "./utils";
12
13
 
13
14
  export default class BulkSubscribersFactory {
14
15
  constructor(config) {
@@ -75,7 +76,7 @@ class _BulkSubscribersChunk {
75
76
  return false;
76
77
  }
77
78
  if (event_size > IDENTITY_SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES) {
78
- throw new SuprsendError(
79
+ throw new InputValueError(
79
80
  `Event too big - ${event_size} Bytes, must not cross ${IDENTITY_SINGLE_EVENT_MAX_APPARENT_SIZE_IN_BYTES_READABLE}`
80
81
  );
81
82
  }
@@ -152,21 +153,29 @@ class BulkSubscribers {
152
153
  this.__pending_records = [];
153
154
  this.chunks = [];
154
155
  this.response = new BulkResponse();
156
+
157
+ // invalid_record json: {"record": event-json, "error": error_str, "code": 500}
158
+ this.__invalid_records = [];
155
159
  }
156
160
 
157
161
  __validate_subscriber_events() {
158
- if (!this.__subscribers) {
159
- throw new SuprsendError("users list is empty in bulk request");
160
- }
161
162
  for (let sub of this.__subscribers) {
162
- const is_part_of_bulk = true;
163
- const warnings_list = sub.validate_body(is_part_of_bulk);
164
- if (warnings_list) {
165
- this.response.warnings = [...this.response.warnings, ...warnings_list];
163
+ try {
164
+ const is_part_of_bulk = true;
165
+ const warnings_list = sub.validate_body(is_part_of_bulk);
166
+ if (warnings_list) {
167
+ this.response.warnings = [
168
+ ...this.response.warnings,
169
+ ...warnings_list,
170
+ ];
171
+ }
172
+ const ev = sub.get_events();
173
+ const [ev_json, body_size] = sub.validate_event_size(ev);
174
+ this.__pending_records.push([ev_json, body_size]);
175
+ } catch (ex) {
176
+ const inv_rec = invalid_record_json(sub.as_json(), ex);
177
+ this.__invalid_records.push(inv_rec);
166
178
  }
167
- const ev = sub.get_events();
168
- const [ev_json, body_size] = sub.validate_event_size(ev);
169
- this.__pending_records.push([ev_json, body_size]);
170
179
  }
171
180
  }
172
181
 
@@ -187,19 +196,13 @@ class BulkSubscribers {
187
196
 
188
197
  append(...subscribers) {
189
198
  if (!subscribers) {
190
- throw SuprsendError("users list empty. must pass one or more users");
199
+ return;
191
200
  }
192
201
  for (let sub of subscribers) {
193
- if (!sub) {
194
- continue;
202
+ if (sub && sub instanceof Subscriber) {
203
+ const sub_copy = cloneDeep(sub);
204
+ this.__subscribers.push(sub_copy);
195
205
  }
196
- if (!(sub instanceof Subscriber)) {
197
- throw new SuprsendError(
198
- "element must be an instance of suprsend.Subscriber"
199
- );
200
- }
201
- const sub_copy = cloneDeep(sub);
202
- this.__subscribers.push(sub_copy);
203
206
  }
204
207
  }
205
208
 
@@ -209,16 +212,33 @@ class BulkSubscribers {
209
212
 
210
213
  async save() {
211
214
  this.__validate_subscriber_events();
212
- this.__chunkify();
213
- for (const [c_idx, ch] of this.chunks.entries()) {
214
- if (this.config.req_log_level > 0) {
215
- console.log(`DEBUG: triggering api call for chunk: ${c_idx}`);
215
+ if (this.__invalid_records.length > 0) {
216
+ const ch_response = BulkResponse.invalid_records_chunk_response(
217
+ this.__invalid_records
218
+ );
219
+ this.response.merge_chunk_response(ch_response);
220
+ }
221
+ if (this.__pending_records.length) {
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
+ } else {
233
+ // if no records. i.e. invalid_records.length and pending_records.length both are 0
234
+ // then add empty success response
235
+ if (this.__invalid_records.length === 0) {
236
+ this.response.merge_chunk_response(
237
+ BulkResponse.empty_chunk_success_response()
238
+ );
216
239
  }
217
- // do api call
218
- await ch.trigger();
219
- // merge response
220
- this.response.merge_chunk_response(ch.response);
221
240
  }
241
+
222
242
  return this.response;
223
243
  }
224
244
  }
package/src/utils.js CHANGED
@@ -58,6 +58,13 @@ export class SuprsendApiError extends Error {
58
58
  }
59
59
  }
60
60
 
61
+ export class InputValueError extends Error {
62
+ constructor(message) {
63
+ super(message);
64
+ this.name = "InputValueError";
65
+ }
66
+ }
67
+
61
68
  export function is_string(value) {
62
69
  return typeof value === "string";
63
70
  }
@@ -93,7 +100,7 @@ export function validate_workflow_body_schema(body) {
93
100
  body.data = {};
94
101
  }
95
102
  if (!(body.data instanceof Object)) {
96
- throw new SuprsendError("data must be a object");
103
+ throw new InputValueError("data must be a object");
97
104
  }
98
105
  const schema = workflow_schema;
99
106
  var v = new Validator();
@@ -128,7 +135,7 @@ export function validate_list_broadcast_body_schema(body) {
128
135
  body.data = {};
129
136
  }
130
137
  if (!(body.data instanceof Object)) {
131
- throw new SuprsendError("data must be a object");
138
+ throw new InputValueError("data must be a object");
132
139
  }
133
140
  const schema = list_broadcast_schema;
134
141
  var v = new Validator();
@@ -227,3 +234,15 @@ export function get_apparent_list_broadcast_body_size(body) {
227
234
  const body_size = JSON.stringify(body).length;
228
235
  return body_size;
229
236
  }
237
+
238
+ export function invalid_record_json(failed_record, err) {
239
+ let err_str;
240
+ if (err instanceof InputValueError) {
241
+ err_str = err.message;
242
+ } else {
243
+ // includes SuprsendValidationError,
244
+ // OR any other error
245
+ err_str = `${err.message}\n${err.stack}`;
246
+ }
247
+ return { record: failed_record, error: err_str, code: 500 };
248
+ }