@sqlitecloud/drivers 1.0.193 → 1.0.255

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/README.md CHANGED
@@ -12,6 +12,38 @@
12
12
  npm install @sqlitecloud/drivers
13
13
  ```
14
14
 
15
+ ## React Native / Expo Install
16
+
17
+ You also have to install Peer Dependencies
18
+
19
+ ```bash
20
+ npm install @sqlitecloud/drivers react-native-tcp-socket react-native-fast-base64
21
+ ```
22
+
23
+ React Native run IOS
24
+
25
+ ```bash
26
+ cd ios && pod install && cd .. && npm run ios
27
+ ```
28
+
29
+ React Native run Android (without ./ in Windows)
30
+
31
+ ```bash
32
+ cd android && ./gradlew clean build && cd .. && npm run android
33
+ ```
34
+
35
+ Expo run IOS
36
+
37
+ ```bash
38
+ npx expo prebuild && npx expo run:ios
39
+ ```
40
+
41
+ Expo run Android
42
+
43
+ ```bash
44
+ npx expo prebuild && npx expo run:android
45
+ ```
46
+
15
47
  ## Usage
16
48
 
17
49
  ```ts
@@ -60,6 +92,10 @@ Pub/Sub is a messaging pattern that allows multiple applications to communicate
60
92
 
61
93
  Pub/Sub Documentation: [https://docs.sqlitecloud.io/docs/pub-sub](https://docs.sqlitecloud.io/docs/pub-sub)
62
94
 
95
+ ## Examples
96
+
97
+ Check out all the supported platforms with related examples [here](https://github.com/sqlitecloud/sqlitecloud-js/tree/main/examples)!
98
+
63
99
  ## More
64
100
 
65
101
  How do I deploy SQLite in the cloud?
@@ -19,6 +19,7 @@ export declare class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
19
19
  private startedOn;
20
20
  private executingCommands?;
21
21
  private processCallback?;
22
+ private pendingChunks;
22
23
  /** Handles data received in response to an outbound command sent by processCommands */
23
24
  private processCommandsData;
24
25
  /** Completes a transaction initiated by processCommands */
@@ -31,6 +31,8 @@ const types_1 = require("./types");
31
31
  const connection_1 = require("./connection");
32
32
  const utilities_1 = require("./utilities");
33
33
  const protocol_1 = require("./protocol");
34
+ // explicitly importing buffer library to allow cross-platform support by replacing it
35
+ const buffer_1 = require("buffer");
34
36
  const tls = __importStar(require("tls"));
35
37
  /**
36
38
  * Implementation of SQLiteCloudConnection that connects to the database using specific tls APIs
@@ -43,8 +45,9 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
43
45
  // onData is called when data is received, it will process the data until all data is retrieved for a response
44
46
  // when response is complete or there's an error, finish is called to call the results callback set by processCommands...
45
47
  // buffer to accumulate incoming data until an whole command is received and can be parsed
46
- this.buffer = Buffer.alloc(0);
48
+ this.buffer = buffer_1.Buffer.alloc(0);
47
49
  this.startedOn = new Date();
50
+ this.pendingChunks = [];
48
51
  }
49
52
  /** True if connection is open */
50
53
  get connected() {
@@ -70,7 +73,14 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
70
73
  // https://r2.nodejs.org/docs/v6.11.4/api/tls.html#tls_class_tls_tlssocket
71
74
  servername: config.host
72
75
  };
73
- this.socket = tls.connect(connectionOptions, () => {
76
+ // tls.connect in the react-native-tcp-socket library is tls.connectTLS
77
+ let connector = tls.connect;
78
+ // @ts-ignore
79
+ if (typeof tls.connectTLS !== 'undefined') {
80
+ // @ts-ignore
81
+ connector = tls.connectTLS;
82
+ }
83
+ this.socket = connector(connectionOptions, () => {
74
84
  var _a;
75
85
  if (this.config.verbose) {
76
86
  console.debug(`SQLiteCloudTlsConnection - connected to ${this.config.host}, authorized: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.authorized}`);
@@ -109,7 +119,7 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
109
119
  return this;
110
120
  }
111
121
  // reset buffer and rowset chunks, define response callback
112
- this.buffer = Buffer.alloc(0);
122
+ this.buffer = buffer_1.Buffer.alloc(0);
113
123
  this.startedOn = new Date();
114
124
  this.processCallback = callback;
115
125
  this.executingCommands = commands;
@@ -137,11 +147,12 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
137
147
  }
138
148
  /** Handles data received in response to an outbound command sent by processCommands */
139
149
  processCommandsData(data) {
140
- var _a, _b, _c, _d, _e, _f;
150
+ var _a, _b, _c, _d, _e, _f, _g;
141
151
  try {
142
152
  // append data to buffer as it arrives
143
153
  if (data.length && data.length > 0) {
144
- this.buffer = Buffer.concat([this.buffer, data]);
154
+ // console.debug(`processCommandsData - received ${data.length} bytes`)
155
+ this.buffer = buffer_1.Buffer.concat([this.buffer, data]);
145
156
  }
146
157
  let dataType = (_a = this.buffer) === null || _a === void 0 ? void 0 : _a.subarray(0, 1).toString();
147
158
  if ((0, protocol_1.hasCommandLength)(dataType)) {
@@ -154,22 +165,33 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
154
165
  bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40);
155
166
  }
156
167
  const elapsedMs = new Date().getTime() - this.startedOn.getTime();
157
- console.debug(`<- ${bufferString} (${elapsedMs}ms)`);
168
+ console.debug(`<- ${bufferString} (${bufferString.length} bytes, ${elapsedMs}ms)`);
158
169
  }
159
170
  // need to decompress this buffer before decoding?
160
171
  if (dataType === protocol_1.CMD_COMPRESSED) {
161
- ;
162
- ({ buffer: this.buffer, dataType } = (0, protocol_1.decompressBuffer)(this.buffer));
163
- }
164
- if (dataType !== protocol_1.CMD_ROWSET_CHUNK) {
165
- const { data } = (0, protocol_1.popData)(this.buffer);
166
- (_c = this.processCommandsFinish) === null || _c === void 0 ? void 0 : _c.call(this, null, data);
172
+ const decompressResults = (0, protocol_1.decompressBuffer)(this.buffer);
173
+ if (decompressResults.dataType === protocol_1.CMD_ROWSET_CHUNK) {
174
+ this.pendingChunks.push(decompressResults.buffer);
175
+ this.buffer = decompressResults.remainingBuffer;
176
+ this.processCommandsData(buffer_1.Buffer.alloc(0));
177
+ return;
178
+ }
179
+ else {
180
+ const { data } = (0, protocol_1.popData)(decompressResults.buffer);
181
+ (_c = this.processCommandsFinish) === null || _c === void 0 ? void 0 : _c.call(this, null, data);
182
+ }
167
183
  }
168
184
  else {
169
- // check if rowset received the ending chunk in which case it can be unpacked
170
- if ((0, protocol_1.bufferEndsWith)(this.buffer, protocol_1.ROWSET_CHUNKS_END)) {
171
- const parsedData = (0, protocol_1.parseRowsetChunks)([this.buffer]);
172
- (_d = this.processCommandsFinish) === null || _d === void 0 ? void 0 : _d.call(this, null, parsedData);
185
+ if (dataType !== protocol_1.CMD_ROWSET_CHUNK) {
186
+ const { data } = (0, protocol_1.popData)(this.buffer);
187
+ (_d = this.processCommandsFinish) === null || _d === void 0 ? void 0 : _d.call(this, null, data);
188
+ }
189
+ else {
190
+ const completeChunk = (0, protocol_1.bufferEndsWith)(this.buffer, protocol_1.ROWSET_CHUNKS_END);
191
+ if (completeChunk) {
192
+ const parsedData = (0, protocol_1.parseRowsetChunks)([...this.pendingChunks, this.buffer]);
193
+ (_e = this.processCommandsFinish) === null || _e === void 0 ? void 0 : _e.call(this, null, parsedData);
194
+ }
173
195
  }
174
196
  }
175
197
  }
@@ -179,14 +201,15 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
179
201
  const lastChar = this.buffer.subarray(this.buffer.length - 1, this.buffer.length).toString('utf8');
180
202
  if (lastChar == ' ') {
181
203
  const { data } = (0, protocol_1.popData)(this.buffer);
182
- (_e = this.processCommandsFinish) === null || _e === void 0 ? void 0 : _e.call(this, null, data);
204
+ (_f = this.processCommandsFinish) === null || _f === void 0 ? void 0 : _f.call(this, null, data);
183
205
  }
184
206
  }
185
207
  }
186
208
  catch (error) {
187
- console.assert(error instanceof Error);
209
+ console.error(`processCommandsData - error: ${error}`);
210
+ console.assert(error instanceof Error, 'An error occoured while processing data');
188
211
  if (error instanceof Error) {
189
- (_f = this.processCommandsFinish) === null || _f === void 0 ? void 0 : _f.call(this, error);
212
+ (_g = this.processCommandsFinish) === null || _g === void 0 ? void 0 : _g.call(this, error);
190
213
  }
191
214
  }
192
215
  }
@@ -203,7 +226,8 @@ class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
203
226
  if (this.processCallback) {
204
227
  this.processCallback(error, result);
205
228
  }
206
- this.buffer = Buffer.alloc(0);
229
+ this.buffer = buffer_1.Buffer.alloc(0);
230
+ this.pendingChunks = [];
207
231
  }
208
232
  /** Disconnect immediately, release connection, no events. */
209
233
  close() {
@@ -24,6 +24,7 @@ export declare function parseCommandLength(data: Buffer): number;
24
24
  export declare function decompressBuffer(buffer: Buffer): {
25
25
  buffer: Buffer;
26
26
  dataType: string;
27
+ remainingBuffer: Buffer;
27
28
  };
28
29
  /** Parse error message or extended error message */
29
30
  export declare function parseError(buffer: Buffer, spaceIndex: number): never;
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.formatCommand = exports.popData = exports.parseRowsetChunks = exports.bufferEndsWith = exports.bufferStartsWith = exports.parseRowsetHeader = exports.parseArray = exports.parseError = exports.decompressBuffer = exports.parseCommandLength = exports.hasCommandLength = exports.ROWSET_CHUNKS_END = exports.CMD_PUBSUB = exports.CMD_ARRAY = exports.CMD_COMMAND = exports.CMD_COMPRESSED = exports.CMD_BLOB = exports.CMD_NULL = exports.CMD_JSON = exports.CMD_ROWSET_CHUNK = exports.CMD_ROWSET = exports.CMD_FLOAT = exports.CMD_INT = exports.CMD_ERROR = exports.CMD_ZEROSTRING = exports.CMD_STRING = void 0;
7
7
  const types_1 = require("./types");
8
8
  const rowset_1 = require("./rowset");
9
+ // explicitly importing buffer library to allow cross-platform support by replacing it
10
+ const buffer_1 = require("buffer");
11
+ // https://www.npmjs.com/package/lz4js
9
12
  const lz4 = require('lz4js');
10
13
  // The server communicates with clients via commands defined in
11
14
  // SQLiteCloud Server Protocol (SCSP), see more at:
@@ -44,26 +47,31 @@ function parseCommandLength(data) {
44
47
  exports.parseCommandLength = parseCommandLength;
45
48
  /** Receive a compressed buffer, decompress with lz4, return buffer and datatype */
46
49
  function decompressBuffer(buffer) {
50
+ // https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md#scsp-compression
51
+ // jest test/database.test.ts -t "select large result set"
52
+ // starts with %<commandLength> <compressed> <uncompressed>
47
53
  const spaceIndex = buffer.indexOf(' ');
48
- buffer = buffer.subarray(spaceIndex + 1);
49
- // extract compressed size
50
- const compressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'));
51
- buffer = buffer.subarray(buffer.indexOf(' ') + 1);
52
- // extract decompressed size
53
- const decompressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'));
54
- buffer = buffer.subarray(buffer.indexOf(' ') + 1);
54
+ const commandLength = parseInt(buffer.subarray(1, spaceIndex).toString('utf8'));
55
+ let commandBuffer = buffer.subarray(spaceIndex + 1, spaceIndex + 1 + commandLength);
56
+ const remainingBuffer = buffer.subarray(spaceIndex + 1 + commandLength);
57
+ // extract compressed + decompressed size
58
+ const compressedSize = parseInt(commandBuffer.subarray(0, commandBuffer.indexOf(' ') + 1).toString('utf8'));
59
+ commandBuffer = commandBuffer.subarray(commandBuffer.indexOf(' ') + 1);
60
+ const decompressedSize = parseInt(commandBuffer.subarray(0, commandBuffer.indexOf(' ') + 1).toString('utf8'));
61
+ commandBuffer = commandBuffer.subarray(commandBuffer.indexOf(' ') + 1);
55
62
  // extract compressed dataType
56
- const dataType = buffer.subarray(0, 1).toString('utf8');
57
- const decompressedBuffer = Buffer.alloc(decompressedSize);
58
- const compressedBuffer = buffer.subarray(buffer.length - compressedSize);
63
+ const dataType = commandBuffer.subarray(0, 1).toString('utf8');
64
+ let decompressedBuffer = buffer_1.Buffer.alloc(decompressedSize);
65
+ const compressedBuffer = commandBuffer.subarray(commandBuffer.length - compressedSize);
59
66
  // lz4js library is javascript and doesn't have types so we silence the type check
60
67
  // eslint-disable-next-line
61
68
  const decompressionResult = lz4.decompressBlock(compressedBuffer, decompressedBuffer, 0, compressedSize, 0);
62
- buffer = Buffer.concat([buffer.subarray(0, buffer.length - compressedSize), decompressedBuffer]);
69
+ // the entire command is composed of the header (which is not compressed) + the decompressed block
70
+ decompressedBuffer = buffer_1.Buffer.concat([commandBuffer.subarray(0, commandBuffer.length - compressedSize), decompressedBuffer]);
63
71
  if (decompressionResult <= 0 || decompressionResult !== decompressedSize) {
64
72
  throw new Error(`lz4 decompression error at offset ${decompressionResult}`);
65
73
  }
66
- return { buffer, dataType };
74
+ return { buffer: decompressedBuffer, dataType, remainingBuffer };
67
75
  }
68
76
  exports.decompressBuffer = decompressBuffer;
69
77
  /** Parse error message or extended error message */
@@ -117,7 +125,7 @@ function parseRowsetHeader(buffer) {
117
125
  buffer = buffer.subarray(buffer.indexOf(':') + 1);
118
126
  // extract rowset header
119
127
  const { data, fwdBuffer } = popIntegers(buffer, 3);
120
- return {
128
+ const result = {
121
129
  index,
122
130
  metadata: {
123
131
  version: data[0],
@@ -127,6 +135,8 @@ function parseRowsetHeader(buffer) {
127
135
  },
128
136
  fwdBuffer
129
137
  };
138
+ // console.debug(`parseRowsetHeader`, result)
139
+ return result;
130
140
  }
131
141
  exports.parseRowsetHeader = parseRowsetHeader;
132
142
  /** Extract column names and, optionally, more metadata out of a rowset's header */
@@ -187,7 +197,7 @@ exports.bufferEndsWith = bufferEndsWith;
187
197
  * @see https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md#scsp-rowset-chunk
188
198
  */
189
199
  function parseRowsetChunks(buffers) {
190
- let buffer = Buffer.concat(buffers);
200
+ let buffer = buffer_1.Buffer.concat(buffers);
191
201
  if (!bufferStartsWith(buffer, exports.CMD_ROWSET_CHUNK) || !bufferEndsWith(buffer, exports.ROWSET_CHUNKS_END)) {
192
202
  throw new Error('SQLiteCloudConnection.parseRowsetChunks - invalid chunks buffer');
193
203
  }
@@ -195,7 +205,8 @@ function parseRowsetChunks(buffers) {
195
205
  const data = [];
196
206
  // validate and skip data type
197
207
  const dataType = buffer.subarray(0, 1).toString();
198
- console.assert(dataType === exports.CMD_ROWSET_CHUNK);
208
+ if (dataType !== exports.CMD_ROWSET_CHUNK)
209
+ throw new Error(`parseRowsetChunks - dataType: ${dataType} should be CMD_ROWSET_CHUNK`);
199
210
  buffer = buffer.subarray(buffer.indexOf(' ') + 1);
200
211
  while (buffer.length > 0 && !bufferStartsWith(buffer, exports.ROWSET_CHUNKS_END)) {
201
212
  // chunk header, eg: 0:VERS NROWS NCOLS
@@ -239,22 +250,25 @@ function popData(buffer) {
239
250
  return { data, fwdBuffer };
240
251
  }
241
252
  // first character is the data type
242
- console.assert(buffer && buffer instanceof Buffer);
243
- const dataType = buffer.subarray(0, 1).toString('utf8');
244
- console.assert(dataType !== exports.CMD_COMPRESSED, "Compressed data shouldn't be decompressed before parsing");
245
- console.assert(dataType !== exports.CMD_ROWSET_CHUNK, 'Chunked data should be parsed by parseRowsetChunks');
253
+ console.assert(buffer && buffer instanceof buffer_1.Buffer);
254
+ let dataType = buffer.subarray(0, 1).toString('utf8');
255
+ if (dataType == exports.CMD_COMPRESSED)
256
+ throw new Error('Compressed data should be decompressed before parsing');
257
+ if (dataType == exports.CMD_ROWSET_CHUNK)
258
+ throw new Error('Chunked data should be parsed by parseRowsetChunks');
246
259
  let spaceIndex = buffer.indexOf(' ');
247
260
  if (spaceIndex === -1) {
248
261
  spaceIndex = buffer.length - 1;
249
262
  }
250
- let commandEnd = -1;
263
+ let commandEnd = -1, commandLength = -1;
251
264
  if (dataType === exports.CMD_INT || dataType === exports.CMD_FLOAT || dataType === exports.CMD_NULL) {
252
265
  commandEnd = spaceIndex + 1;
253
266
  }
254
267
  else {
255
- const commandLength = parseInt(buffer.subarray(1, spaceIndex).toString());
268
+ commandLength = parseInt(buffer.subarray(1, spaceIndex).toString());
256
269
  commandEnd = spaceIndex + 1 + commandLength;
257
270
  }
271
+ // console.debug(`popData - dataType: ${dataType}, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`)
258
272
  switch (dataType) {
259
273
  case exports.CMD_INT:
260
274
  return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()));
@@ -282,12 +296,14 @@ function popData(buffer) {
282
296
  parseError(buffer, spaceIndex); // throws custom error
283
297
  break;
284
298
  }
285
- throw new TypeError(`Data type: ${dataType} is not defined in SCSP`);
299
+ const msg = `popData - Data type: ${Number(dataType)} '${dataType}' is not defined in SCSP, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`;
300
+ console.error(msg);
301
+ throw new TypeError(msg);
286
302
  }
287
303
  exports.popData = popData;
288
304
  /** Format a command to be sent via SCSP protocol */
289
305
  function formatCommand(command) {
290
- const commandLength = Buffer.byteLength(command, 'utf-8');
306
+ const commandLength = buffer_1.Buffer.byteLength(command, 'utf-8');
291
307
  return `+${commandLength} ${command}`;
292
308
  }
293
309
  exports.formatCommand = formatCommand;
@@ -7,7 +7,7 @@ import tls from 'tls';
7
7
  /** Default timeout value for queries */
8
8
  export declare const DEFAULT_TIMEOUT: number;
9
9
  /** Default tls connection port */
10
- export declare const DEFAULT_PORT = 9960;
10
+ export declare const DEFAULT_PORT = 8860;
11
11
  /**
12
12
  * Configuration for SQLite cloud connection
13
13
  * @note Options are all lowecase so they 1:1 compatible with C SDK
@@ -7,7 +7,7 @@ exports.SQLiteCloudArrayType = exports.SQLiteCloudError = exports.DEFAULT_PORT =
7
7
  /** Default timeout value for queries */
8
8
  exports.DEFAULT_TIMEOUT = 300 * 1000;
9
9
  /** Default tls connection port */
10
- exports.DEFAULT_PORT = 9960;
10
+ exports.DEFAULT_PORT = 8860;
11
11
  /** Custom error reported by SQLiteCloud drivers */
12
12
  class SQLiteCloudError extends Error {
13
13
  constructor(message, args) {
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseBooleanToZeroOne = exports.parseBoolean = exports.parseconnectionstring = exports.validateConfiguration = exports.popCallback = exports.getUpdateResults = exports.prepareSql = exports.escapeSqlParameter = exports.getInitializationCommands = exports.anonimizeError = exports.anonimizeCommand = exports.isNode = exports.isBrowser = void 0;
7
7
  const types_1 = require("./types");
8
8
  const types_2 = require("./types");
9
+ // explicitly importing these libraries to allow cross-platform support by replacing them
10
+ const whatwg_url_1 = require("whatwg-url");
11
+ const buffer_1 = require("buffer");
9
12
  //
10
13
  // determining running environment, thanks to browser-or-node
11
14
  // https://www.npmjs.com/package/browser-or-node
@@ -93,7 +96,7 @@ function escapeSqlParameter(param) {
93
96
  return param ? '1' : '0';
94
97
  }
95
98
  // serialize buffer as X'...' hex encoded string
96
- if (Buffer.isBuffer(param)) {
99
+ if (buffer_1.Buffer.isBuffer(param)) {
97
100
  return `X'${param.toString('hex')}'`;
98
101
  }
99
102
  if (typeof param === 'object') {
@@ -117,7 +120,7 @@ function prepareSql(sql, ...params) {
117
120
  const index = matchIndex ? parseInt(matchIndex) : parameterIndex;
118
121
  parameterIndex++;
119
122
  let sqlParameter;
120
- if (params[0] && typeof params[0] === 'object' && !(params[0] instanceof Buffer)) {
123
+ if (params[0] && typeof params[0] === 'object' && !(params[0] instanceof buffer_1.Buffer)) {
121
124
  sqlParameter = params[0][index];
122
125
  }
123
126
  if (!sqlParameter) {
@@ -200,7 +203,7 @@ function validateConfiguration(config) {
200
203
  config.clientid || (config.clientid = 'SQLiteCloud');
201
204
  config.verbose = parseBoolean(config.verbose);
202
205
  config.noblob = parseBoolean(config.noblob);
203
- config.compression = parseBoolean(config.compression);
206
+ config.compression = config.compression != undefined && config.compression != null ? parseBoolean(config.compression) : true; // default: true
204
207
  config.create = parseBoolean(config.create);
205
208
  config.non_linearizable = parseBoolean(config.non_linearizable);
206
209
  config.insecure = parseBoolean(config.insecure);
@@ -235,7 +238,7 @@ function parseconnectionstring(connectionstring) {
235
238
  // the sqlitecloud: protocol is not recognized by the URL constructor in browsers
236
239
  // so we need to replace it with https: to make it work
237
240
  const knownProtocolUrl = connectionstring.replace('sqlitecloud:', 'https:');
238
- const url = new URL(knownProtocolUrl);
241
+ const url = new whatwg_url_1.URL(knownProtocolUrl);
239
242
  // all lowecase options
240
243
  const options = {};
241
244
  url.searchParams.forEach((value, key) => {