@api-client/core 0.6.2 → 0.6.3

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.
@@ -1,35 +1,52 @@
1
- import { blobToDataUrl } from './Utils.js';
2
-
3
1
  export type PayloadTypes = 'string' | 'file' | 'blob' | 'buffer' | 'arraybuffer' | 'formdata' | 'x-www-form-urlencoded';
4
2
  export type DeserializedPayload = string | Blob | File | FormData | Buffer | ArrayBuffer | undefined;
5
3
  export const SupportedPayloadTypes: PayloadTypes[] = ['string', 'file', 'blob', 'buffer', 'arraybuffer', 'formdata', 'x-www-form-urlencoded'];
6
4
 
7
5
  export interface IMultipartBody {
8
6
  /**
9
- * When true a this entry represent a file part
7
+ * Whether the parameter is enabled. Default to true.
10
8
  */
11
- isFile: boolean;
9
+ enabled?: boolean;
12
10
  /**
13
11
  * The name of the filed
14
12
  */
15
13
  name: string;
16
14
  /**
17
- * Converted value
15
+ * Converted value.
16
+ * When the part value was a string this is a string.
17
+ * When the previous value was a Blob or a Buffer, this will be a serialized payload.
18
18
  */
19
- value: string;
19
+ value: string | ISafePayload;
20
+ /**
21
+ * When `true` this entry represent a file part
22
+ * @deprecated This is only used for the compatibility with ARC. This information is encoded in the `value`.
23
+ */
24
+ isFile?: boolean;
20
25
  /**
21
26
  * A content type entered by the user to the text part of the text part input.
22
27
  * This can only be set when `isFile` is false.
28
+ * @deprecated This is only used for the compatibility with ARC. This information is encoded in the `value`.
23
29
  */
24
30
  type?: string;
25
31
  /**
26
32
  * The original file name used with the part
33
+ * @deprecated This is only used for the compatibility with ARC. This information is encoded in the `value`.
27
34
  */
28
35
  fileName?: string;
36
+ }
37
+
38
+ export interface IBlobMeta {
29
39
  /**
30
- * Whether the parameter is enabled. Default to true.
40
+ * The blob's mime type.
31
41
  */
32
- enabled?: boolean;
42
+ mime: string;
43
+ }
44
+
45
+ export interface IFileMeta extends IBlobMeta {
46
+ /**
47
+ * The file name.
48
+ */
49
+ name: string;
33
50
  }
34
51
 
35
52
  /**
@@ -43,13 +60,17 @@ export interface ISafePayload {
43
60
  * The type od the originating payload object.
44
61
  */
45
62
  type: PayloadTypes;
63
+ /**
64
+ * The payload contents. The data type depends on the `type`.
65
+ */
46
66
  data: string | number[] | IMultipartBody[];
47
67
  /**
48
68
  * Optionally the original mime type of the payload.
49
69
  * This is used with files.
50
70
  */
51
- mime?: string;
71
+ meta?: IBlobMeta | IFileMeta;
52
72
  }
73
+
53
74
  /**
54
75
  * The request payload. When not a string then it has to go through a
55
76
  * transformation from a store safe object to the original data object.
@@ -84,6 +105,17 @@ export class PayloadSerializer {
84
105
  return false;
85
106
  }
86
107
 
108
+ /**
109
+ * Tests whether the given input should be processed by the `serialize()`.
110
+ */
111
+ static needsSerialization(input: unknown): boolean {
112
+ const typedSerialized = input as ISafePayload;
113
+ if (typedSerialized.type && SupportedPayloadTypes.includes(typedSerialized.type)) {
114
+ return false;
115
+ }
116
+ return true;
117
+ }
118
+
87
119
  /**
88
120
  * Transforms the payload into a data store safe object.
89
121
  */
@@ -100,6 +132,10 @@ export class PayloadSerializer {
100
132
  // if (typeof payload === 'string') {
101
133
  // return payload;
102
134
  // }
135
+
136
+ if (hasBlob && payload instanceof File) {
137
+ return PayloadSerializer.stringifyFile(payload);
138
+ }
103
139
  if (hasBlob && payload instanceof Blob) {
104
140
  return PayloadSerializer.stringifyBlob(payload);
105
141
  }
@@ -111,7 +147,7 @@ export class PayloadSerializer {
111
147
  }
112
148
  if (hasFormData && payload instanceof FormData) {
113
149
  try {
114
- const result = await PayloadSerializer.stringifyFormData((payload as unknown) as Iterable<(string | File)[]>);
150
+ const result = await PayloadSerializer.stringifyFormData(payload);
115
151
  return result;
116
152
  } catch (e: unknown) {
117
153
  console.warn(`Unable to transform FormData: ${(e as Error).message}`);
@@ -121,18 +157,38 @@ export class PayloadSerializer {
121
157
  }
122
158
 
123
159
  /**
124
- * Converts blob data to base64 string.
160
+ * Stringifies a file object.
161
+ */
162
+ static async stringifyFile(file: File): Promise<ISafePayload> {
163
+ const buffer = await file.arrayBuffer();
164
+ const view = new Uint8Array(buffer);
165
+ const meta: IFileMeta = {
166
+ mime: file.type,
167
+ name: file.name,
168
+ };
169
+ const result: ISafePayload = {
170
+ type: 'file',
171
+ data: [...view],
172
+ meta,
173
+ };
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Stringifies a blob object.
125
179
  *
126
- * @param blob File or blob object to be translated to string
127
- * @return Promise resolved to a base64 string data from the file.
180
+ * @param blob Blob object to be translated to string
128
181
  */
129
182
  static async stringifyBlob(blob: Blob): Promise<ISafePayload> {
130
- const typedFile = blob as File;
131
- const data = await blobToDataUrl(blob);
183
+ const buffer = await blob.arrayBuffer();
184
+ const view = new Uint8Array(buffer);
185
+ const meta: IBlobMeta = {
186
+ mime: blob.type,
187
+ };
132
188
  const result: ISafePayload = {
133
189
  type: 'blob',
134
- data,
135
- mime: typedFile.type,
190
+ data: [...view],
191
+ meta,
136
192
  };
137
193
  return result;
138
194
  }
@@ -160,14 +216,11 @@ export class PayloadSerializer {
160
216
  * @returns The buffer metadata or undefined if the passed argument is not an ArrayBuffer.
161
217
  */
162
218
  static stringifyArrayBuffer(payload: ArrayBuffer): ISafePayload | undefined {
163
- if (payload.byteLength) {
164
- const view = new Uint8Array(payload);
165
- return {
166
- type: 'arraybuffer',
167
- data: Array.from(view),
168
- };
169
- }
170
- return undefined;
219
+ const view = new Uint8Array(payload);
220
+ return {
221
+ type: 'arraybuffer',
222
+ data: Array.from(view),
223
+ };
171
224
  }
172
225
 
173
226
  /**
@@ -176,9 +229,11 @@ export class PayloadSerializer {
176
229
  * @param payload A `FormData` object
177
230
  * @return A promise resolved to a datastore safe entries.
178
231
  */
179
- static async stringifyFormData(payload: Iterable<(string | File)[]>): Promise<ISafePayload> {
232
+ static async stringifyFormData(payload: FormData): Promise<ISafePayload> {
233
+ // TS apparently doesn't know that FormData is iterable.
234
+ const iterable = (payload as unknown) as Iterable<(string | File)[]>;
180
235
  const promises: Promise<IMultipartBody>[] = [];
181
- for (const part of payload) {
236
+ for (const part of iterable) {
182
237
  promises.push(PayloadSerializer.serializeFormDataEntry(part[0] as string, part[1]));
183
238
  }
184
239
  const items = await Promise.all(promises);
@@ -195,33 +250,27 @@ export class PayloadSerializer {
195
250
  * @param file The part value
196
251
  * @returns Transformed FormData part to a datastore safe entry.
197
252
  */
198
- static async serializeFormDataEntry(name: string, file: string | File): Promise<IMultipartBody> {
253
+ static async serializeFormDataEntry(name: string, file: string | File | Blob): Promise<IMultipartBody> {
199
254
  if (typeof file === 'string') {
200
- // when adding an item to the FormData object without 3rd parameter of the append function
201
- // then the value is a string.
202
255
  return {
203
- isFile: false,
204
256
  name,
205
257
  value: file,
206
258
  enabled: true,
207
259
  };
208
260
  }
209
-
210
- const value = await blobToDataUrl(file);
261
+ let value: ISafePayload;
262
+ // API Client adds the "blob" when adding a text value with a mime type.
263
+ // This is recognized by the UI to restore the entry as the text and not a file.
264
+ if (file instanceof File && file.name !== 'blob') {
265
+ value = await PayloadSerializer.stringifyFile(file);
266
+ } else {
267
+ value = await PayloadSerializer.stringifyBlob(file);
268
+ }
211
269
  const part: IMultipartBody = {
212
- isFile: false,
213
270
  name,
214
271
  value,
215
272
  enabled: true,
216
273
  };
217
- if (file.name === 'blob') {
218
- // API Client adds the "blob" filename when the content type is set on the editor.
219
- // otherwise it wouldn't be possible to set the content type value.
220
- part.type = file.type;
221
- } else {
222
- part.isFile = true;
223
- part.fileName = file.name;
224
- }
225
274
  return part;
226
275
  }
227
276
 
@@ -240,8 +289,8 @@ export class PayloadSerializer {
240
289
  // We mostly gonna return a Buffer here.
241
290
  switch (payload.type) {
242
291
  case 'string': return payload.data as string;
243
- case 'file':
244
- case 'blob': return PayloadSerializer.deserializeBlobBuffer(payload.data as string);
292
+ case 'file': return PayloadSerializer.deserializeFileBuffer(payload);
293
+ case 'blob': return PayloadSerializer.deserializeBlobBuffer(payload);
245
294
  case 'buffer': return PayloadSerializer.deserializeBuffer(payload.data as number[]);
246
295
  case 'arraybuffer': return PayloadSerializer.deserializeArrayBufferBuffer(payload.data as number[]);
247
296
  case 'formdata': return undefined;
@@ -250,9 +299,9 @@ export class PayloadSerializer {
250
299
  }
251
300
  switch (payload.type) {
252
301
  case 'string': return payload.data as string;
253
- case 'file':
254
- case 'blob': return PayloadSerializer.deserializeBlob(payload.data as string);
255
- case 'buffer': return PayloadSerializer.deserializeBuffer(payload.data as number[]);
302
+ case 'file': return PayloadSerializer.deserializeFile(payload);
303
+ case 'blob': return PayloadSerializer.deserializeBlob(payload);
304
+ case 'buffer': return PayloadSerializer.deserializeArrayBuffer(payload.data as number[]);
256
305
  case 'arraybuffer': return PayloadSerializer.deserializeArrayBuffer(payload.data as number[]);
257
306
  case 'formdata': return PayloadSerializer.deserializeFormData(payload.data as IMultipartBody[]);
258
307
  default: return undefined;
@@ -260,12 +309,47 @@ export class PayloadSerializer {
260
309
  }
261
310
 
262
311
  /**
263
- * Converts data-url string to blob
312
+ * Deserializes previously serialized file object.
313
+ *
314
+ * @param payload The serialized payload with a file.
315
+ */
316
+ static deserializeFile(payload: ISafePayload): File {
317
+ const data = payload.data as number[];
318
+ const meta = payload.meta as IFileMeta;
319
+ const { mime, name } = meta;
320
+ const { buffer } = new Uint8Array(data);
321
+ return new File([buffer], name, {
322
+ type: mime,
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Deserializes previously serialized blob object.
328
+ *
329
+ * In previous versions of ARC the data was a string as data URL. In API client this is a buffer.
264
330
  *
331
+ * @param payload The serialized payload.
332
+ * @return Restored blob value
333
+ */
334
+ static deserializeBlob(payload: ISafePayload): Blob | undefined {
335
+ if (typeof payload.data === 'string') {
336
+ return this.deserializeBlobLegacy(payload.data);
337
+ }
338
+ const data = payload.data as number[];
339
+ const meta = payload.meta as IBlobMeta;
340
+ const { mime } = meta;
341
+ const { buffer } = new Uint8Array(data);
342
+ return new Blob([buffer], { type: mime });
343
+ }
344
+
345
+ /**
346
+ * The old implementation of the blob deserializer.
347
+ *
348
+ * @deprecated
265
349
  * @param dataUrl Data url from blob value.
266
350
  * @return Restored blob value
267
351
  */
268
- static deserializeBlob(dataUrl: string): Blob | undefined {
352
+ static deserializeBlobLegacy(dataUrl: string): Blob | undefined {
269
353
  const arr = dataUrl.split(',');
270
354
  const matchedMime = arr[0].match(/:(.*?);/);
271
355
  if (!matchedMime) {
@@ -282,12 +366,40 @@ export class PayloadSerializer {
282
366
  }
283
367
 
284
368
  /**
285
- * Converts data-url string to blob
369
+ * Converts previously serialized File to a Buffer.
286
370
  *
371
+ * @param payload The serialized payload.
372
+ * @return Restored File value as Buffer
373
+ */
374
+ static deserializeFileBuffer(payload: ISafePayload): Buffer {
375
+ const data = payload.data as number[];
376
+ const ab = this.deserializeArrayBuffer(data);
377
+ return Buffer.from(ab);
378
+ }
379
+
380
+ /**
381
+ * Converts data-url string to buffer
382
+ *
383
+ * @param payload The serialized payload.
384
+ * @return Restored blob value
385
+ */
386
+ static deserializeBlobBuffer(payload: ISafePayload): Buffer {
387
+ if (typeof payload.data === 'string') {
388
+ return this.deserializeBlobBufferLegacy(payload.data);
389
+ }
390
+ const data = payload.data as number[];
391
+ const ab = this.deserializeArrayBuffer(data);
392
+ return Buffer.from(ab);
393
+ }
394
+
395
+ /**
396
+ * Converts data-url string to buffer
397
+ *
398
+ * @deprecated
287
399
  * @param dataUrl Data url from blob value.
288
400
  * @return Restored blob value
289
401
  */
290
- static deserializeBlobBuffer(dataUrl: string): Buffer {
402
+ static deserializeBlobBufferLegacy(dataUrl: string): Buffer {
291
403
  const arr = dataUrl.split(',');
292
404
  const value = arr[1];
293
405
  return Buffer.from(value, 'base64url');
@@ -334,26 +446,50 @@ export class PayloadSerializer {
334
446
  if (!Array.isArray(parts) || !parts.length) {
335
447
  return fd;
336
448
  }
337
- parts.forEach((part) => {
338
- const { isFile, name, value, type, fileName, enabled } = part;
339
- if (enabled === false) {
340
- return;
449
+ parts.forEach(part => this.deserializeFormDataPart(fd, part));
450
+ return fd;
451
+ }
452
+
453
+ private static deserializeFormDataPart(form: FormData, part: IMultipartBody): void {
454
+ if (part.enabled === false) {
455
+ return;
456
+ }
457
+ // the compatibility with old ARC.
458
+ if (typeof part.isFile === 'boolean') {
459
+ this.deserializeFormDataLegacy(form, part);
460
+ return;
461
+ }
462
+ const { name, value } = part;
463
+ if (typeof value === 'string') {
464
+ form.append(name, value);
465
+ return;
466
+ }
467
+ if (value.type === 'file') {
468
+ const file = this.deserializeFile(value);
469
+ form.append(name, file);
470
+ return;
471
+ }
472
+ const blob = this.deserializeBlob(value) as Blob;
473
+ form.append(name, blob);
474
+ }
475
+
476
+ /**
477
+ * @deprecated This is only for compatibility with ARC.
478
+ */
479
+ private static deserializeFormDataLegacy(form: FormData, part: IMultipartBody): void {
480
+ let blob;
481
+ if (part.isFile) {
482
+ blob = PayloadSerializer.deserializeBlobLegacy(part.value as string);
483
+ if (blob) {
484
+ form.append(part.name, blob, part.fileName);
341
485
  }
342
- let blob;
343
- if (isFile) {
344
- blob = PayloadSerializer.deserializeBlob(value);
345
- if (blob) {
346
- fd.append(name, blob, fileName);
347
- }
348
- } else if (type) {
349
- blob = PayloadSerializer.deserializeBlob(value);
350
- if (blob) {
351
- fd.append(name, blob, 'blob');
352
- }
353
- } else {
354
- fd.append(name, value);
486
+ } else if (part.type) {
487
+ blob = PayloadSerializer.deserializeBlobLegacy(part.value as string);
488
+ if (blob) {
489
+ form.append(part.name, blob, 'blob');
355
490
  }
356
- });
357
- return fd;
491
+ } else {
492
+ form.append(part.name, part.value as string);
493
+ }
358
494
  }
359
495
  }
@@ -134,12 +134,9 @@ export class CoreEngine extends HttpEngine {
134
134
  if (auth) {
135
135
  headers.set('proxy-authorization', auth);
136
136
  }
137
- let buffer: Buffer | undefined;
138
- if (payload) {
139
- buffer = await PayloadSupport.payloadToBuffer(payload, headers);
140
- if (buffer) {
141
- addContentLength(this.request.method || 'GET', buffer, headers);
142
- }
137
+ const buffer = PayloadSupport.payloadToBuffer(headers, payload);
138
+ if (buffer) {
139
+ addContentLength(this.request.method || 'GET', buffer, headers);
143
140
  }
144
141
 
145
142
  this._handleAuthorization(headers);