@trycourier/cli 2.4.0 → 2.5.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.
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare const _default: () => React.JSX.Element;
3
+ export default _default;
@@ -0,0 +1,215 @@
1
+ import { ProgressBar } from '@inkjs/ui';
2
+ import { Box, Text } from 'ink';
3
+ import _ from 'lodash';
4
+ import fs from 'fs/promises';
5
+ import React, { useEffect, useState } from 'react';
6
+ import { useBoolean, useCounter } from 'usehooks-ts';
7
+ import getDb, { getChunk } from '../bulk.js';
8
+ import { useCliContext } from '../components/Context.js';
9
+ import Spinner from '../components/Spinner.js';
10
+ import UhOh from '../components/UhOh.js';
11
+ import delay from '../lib/delay.js';
12
+ const DEFAULT_DELAY = 5000;
13
+ const DEFAULT_CHUNK_SIZE = 500;
14
+ const DEFAULT_TIMEOUT = 10;
15
+ const DEFAULT_ERROR_FILENAME = 'errors.json';
16
+ export default () => {
17
+ const { parsedParams, courier } = useCliContext();
18
+ const [error, setError] = useState();
19
+ const processing = useBoolean(true);
20
+ const running = useBoolean(true);
21
+ const [data, setData] = useState();
22
+ const [data_errors, setDataErrors] = useState([]);
23
+ const counter = useCounter(0);
24
+ const [row_errors, setRowErrors] = useState([]);
25
+ const filename = String(_.get(parsedParams, ['_', 0], ''));
26
+ const { db, filetype, sql } = getDb(filename);
27
+ const delay_between_chunks = Number(parsedParams['delay']) ?? DEFAULT_DELAY;
28
+ const chunk_size = parsedParams['chunk_size']
29
+ ? Number(parsedParams['chunk_size'])
30
+ : DEFAULT_CHUNK_SIZE;
31
+ const log_errors = true;
32
+ useEffect(() => {
33
+ if (filetype) {
34
+ getData();
35
+ }
36
+ else {
37
+ setError('File type not supported.');
38
+ }
39
+ }, []);
40
+ useEffect(() => {
41
+ if (data) {
42
+ processData();
43
+ }
44
+ }, [data]);
45
+ useEffect(() => {
46
+ if (!processing.value) {
47
+ handleErrors();
48
+ }
49
+ }, [processing.value]);
50
+ const getData = () => {
51
+ db.all(sql, (err, result) => {
52
+ if (err) {
53
+ setError(err.message);
54
+ }
55
+ else {
56
+ setData(result);
57
+ }
58
+ });
59
+ };
60
+ const processChunkRows = (data, start_index) => {
61
+ return data.map((row, i) => {
62
+ const curr_index = start_index + i;
63
+ let { user_id, token, provider_key, device, tracking, expiry_date, ...properties } = row || {};
64
+ if (!device)
65
+ device = {};
66
+ if (!tracking)
67
+ tracking = {};
68
+ if (!properties)
69
+ properties = {};
70
+ if (!user_id) {
71
+ return Promise.resolve({
72
+ success: false,
73
+ userId: '__unknown__',
74
+ error: `user_id not found in index ${curr_index}`,
75
+ index: curr_index,
76
+ });
77
+ }
78
+ else if (!provider_key) {
79
+ return Promise.resolve({
80
+ success: false,
81
+ userId: user_id,
82
+ error: `provider_key not found in index ${curr_index}`,
83
+ index: curr_index,
84
+ });
85
+ }
86
+ else if (!token) {
87
+ return Promise.resolve({
88
+ success: false,
89
+ userId: user_id,
90
+ error: `token not found in index ${curr_index}`,
91
+ index: curr_index,
92
+ });
93
+ }
94
+ else {
95
+ Object.entries(properties).forEach(([key, value]) => {
96
+ if (key.startsWith('device.')) {
97
+ _.unset(properties, key);
98
+ _.set(device, key.replace('device.', ''), value);
99
+ }
100
+ else if (key.startsWith('tracking.')) {
101
+ _.unset(properties, key);
102
+ _.set(tracking, key.replace('tracking.', ''), value);
103
+ }
104
+ else {
105
+ _.unset(properties, key);
106
+ _.set(properties, key.replace('properties.', ''), value);
107
+ }
108
+ });
109
+ return processRow({
110
+ user_id: String(user_id),
111
+ token: token,
112
+ provider_key,
113
+ device,
114
+ tracking,
115
+ expiry_date,
116
+ properties,
117
+ index: curr_index,
118
+ });
119
+ }
120
+ });
121
+ };
122
+ const processRow = async ({ user_id, token, index, ...body }) => {
123
+ return new Promise(async (resolve) => {
124
+ let promises = [];
125
+ try {
126
+ promises.push(courier.users.tokens.add(user_id, token, body, {
127
+ maxRetries: 5,
128
+ timeoutInSeconds: DEFAULT_TIMEOUT,
129
+ }));
130
+ await Promise.all(promises);
131
+ counter.increment();
132
+ return resolve({ userId: user_id, success: true, index });
133
+ }
134
+ catch (error) {
135
+ counter.increment();
136
+ return resolve({
137
+ userId: user_id,
138
+ success: false,
139
+ index,
140
+ error: (String(error) ??
141
+ error?.message ??
142
+ error.message ??
143
+ 'Unknown Error') + `+ ${user_id}`,
144
+ });
145
+ }
146
+ });
147
+ };
148
+ const processData = async () => {
149
+ if (data?.length) {
150
+ let data_copy = [...data];
151
+ let counter = 0;
152
+ let { rows, data: rest } = getChunk(data_copy, chunk_size);
153
+ while (rows?.length) {
154
+ const chunk = processChunkRows(rows, counter);
155
+ const processed_chunks = await Promise.all(chunk);
156
+ const errors = processed_chunks.filter(r => !r.success);
157
+ if (errors.length) {
158
+ setDataErrors(p => [
159
+ ...p,
160
+ ...errors.map(r => {
161
+ return `user_id (${r.userId}) failed to update in index ${r.index}: ${String(r.error)}`;
162
+ }),
163
+ ]);
164
+ setRowErrors(r => [
165
+ ...r,
166
+ ...errors.map(e => data[e.index]),
167
+ ]);
168
+ }
169
+ if (rest.length > 0) {
170
+ await delay(delay_between_chunks);
171
+ counter += rows.length;
172
+ const next = getChunk(rest, chunk_size);
173
+ rows = next.rows;
174
+ rest = next.data;
175
+ }
176
+ else {
177
+ processing.setFalse();
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ };
183
+ const handleErrors = async () => {
184
+ if (row_errors.length && log_errors) {
185
+ await fs.writeFile(DEFAULT_ERROR_FILENAME, JSON.stringify(row_errors, null, 2), {
186
+ encoding: 'utf-8',
187
+ });
188
+ running.setFalse();
189
+ }
190
+ else {
191
+ running.setFalse();
192
+ }
193
+ };
194
+ if (!filename?.length) {
195
+ return React.createElement(UhOh, { text: "You must specify a filename." });
196
+ }
197
+ else if (error?.length) {
198
+ return React.createElement(UhOh, { text: error });
199
+ }
200
+ else if (data && running.value) {
201
+ return (React.createElement(React.Fragment, null,
202
+ React.createElement(ProgressBar, { value: Math.floor((counter.count / data.length) * 100) }),
203
+ React.createElement(Spinner, { text: `Completed Rows: ${counter.count} / ${data.length}` })));
204
+ }
205
+ else {
206
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
207
+ React.createElement(Text, { color: 'green' }, `Completed Rows: ${counter.count} / ${data?.length || 0}`),
208
+ data_errors.map((err, i) => {
209
+ return React.createElement(UhOh, { key: i, text: err });
210
+ }),
211
+ log_errors && data_errors.length ? (React.createElement(Text, null,
212
+ "Errors output to ",
213
+ DEFAULT_ERROR_FILENAME)) : (React.createElement(React.Fragment, null))));
214
+ }
215
+ };
package/dist/mappings.js CHANGED
@@ -21,6 +21,7 @@ import AutomationInvokeBulk from './commands/AutomationInvokeBulk.js';
21
21
  import MessagesSearch from './commands/MessagesSearch.js';
22
22
  import TenantsMembershipBulk from './commands/TenantsMembershipBulk.js';
23
23
  import AudienceSearch from './commands/AudienceSearch.js';
24
+ import UsersTokensBulk from './commands/UsersTokensBulk.js';
24
25
  const mappings = new Map();
25
26
  export const COMMON_OPTIONS = [
26
27
  {
@@ -281,6 +282,18 @@ mappings.set('users:bulk', {
281
282
  ],
282
283
  component: () => React.createElement(UsersBulk, null),
283
284
  });
285
+ mappings.set('users:tokens:bulk', {
286
+ params: '<filename>',
287
+ instructions: 'Bulk import user tokens from a file (csv, json, jsonl, xls, xlsx, .parquet)." For CSVs, we will unpack nested objects into based on the header. E.g., "address.city" becomes {"address": {"city": "value"}}. Lodash path syntax is used for created the nested object. Supports wildcard syntax for multiple files, must surround with quotes (see examples).',
288
+ options: [
289
+ {
290
+ option: '--replace',
291
+ value: 'Replace existing users with the same ID, if not set, will do a merge based on key',
292
+ },
293
+ ],
294
+ example: ['courier users:tokens:bulk examples/users-tokens.csv'],
295
+ component: () => React.createElement(UsersTokensBulk, null),
296
+ });
284
297
  mappings.set('users:preferences', {
285
298
  params: '<user>',
286
299
  instructions: 'Fetch the preferences for a given user ID',
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- let VERSION = '2.4.0';
1
+ let VERSION = '2.5.0';
2
2
  export default VERSION;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycourier/cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "courier": "dist/cli.js"