@simplysf/simply-data 2.3.25 → 2.3.26
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 +3 -3
- package/lib/common/contentVersionUtils.d.ts +2 -0
- package/lib/common/contentVersionUtils.js +53 -13
- package/lib/common/contentVersionUtils.js.map +1 -1
- package/lib/common/multipart.d.ts +58 -0
- package/lib/common/multipart.js +92 -0
- package/lib/common/multipart.js.map +1 -0
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ EXAMPLES
|
|
|
56
56
|
$ sf simply data file upload --file-path fileToUpload.txt --first-publish-location-id 0019000000DmehK --target-org myTargetOrg
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
_See code: [lib/commands/simply/data/file/upload.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.
|
|
59
|
+
_See code: [lib/commands/simply/data/file/upload.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.26/packages/simply-data/lib/commands/simply/data/file/upload.js)_
|
|
60
60
|
|
|
61
61
|
## `sf simply data files download`
|
|
62
62
|
|
|
@@ -101,7 +101,7 @@ FLAG DESCRIPTIONS
|
|
|
101
101
|
Provide a WHERE clause to allow the plugin to specify which ContentVersion records should be downloaded.
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
_See code: [lib/commands/simply/data/files/download.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.
|
|
104
|
+
_See code: [lib/commands/simply/data/files/download.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.26/packages/simply-data/lib/commands/simply/data/files/download.js)_
|
|
105
105
|
|
|
106
106
|
## `sf simply data files upload`
|
|
107
107
|
|
|
@@ -146,7 +146,7 @@ FLAG DESCRIPTIONS
|
|
|
146
146
|
quasi concurrent uploads. Please note that setting this value too high can cause performance issues.
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
_See code: [lib/commands/simply/data/files/upload.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.
|
|
149
|
+
_See code: [lib/commands/simply/data/files/upload.js](https://github.com/SimplySF/simply-node/blob/@simplysf/simply-data@2.3.26/packages/simply-data/lib/commands/simply/data/files/upload.js)_
|
|
150
150
|
<!-- commandsstop -->
|
|
151
151
|
|
|
152
152
|
## License
|
|
@@ -8,6 +8,7 @@ import { ContentVersion, ContentVersionDownload } from './contentVersionTypes.js
|
|
|
8
8
|
* `ContentDocumentId`, and `FileExtension` are used to build the output file path.
|
|
9
9
|
* @param downloadDirectory - The local directory to write the file into.
|
|
10
10
|
* @returns The local path the file was written to.
|
|
11
|
+
* @throws {SfError} If the org returns a non-2xx response, before any file is created.
|
|
11
12
|
*/
|
|
12
13
|
export declare function downloadContentVersion(targetOrgConnection: Connection, contentVersionDownload: ContentVersionDownload, downloadDirectory: string): Promise<string>;
|
|
13
14
|
/**
|
|
@@ -19,5 +20,6 @@ export declare function downloadContentVersion(targetOrgConnection: Connection,
|
|
|
19
20
|
* @param firstPublishLocationId - The ID of the record/library to attach the resulting
|
|
20
21
|
* `ContentDocument` to, if any.
|
|
21
22
|
* @returns The created `ContentVersion`, re-queried to include `ContentDocumentId`.
|
|
23
|
+
* @throws {SfError} If the org returns a non-2xx response.
|
|
22
24
|
*/
|
|
23
25
|
export declare function uploadContentVersion(targetOrgConnection: Connection, pathOnClient: string, title?: string, firstPublishLocationId?: string): Promise<ContentVersion>;
|
|
@@ -15,9 +15,32 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import stream from 'node:stream';
|
|
19
|
-
import
|
|
20
|
-
import
|
|
18
|
+
import stream, { Readable } from 'node:stream';
|
|
19
|
+
import { SfError } from '@salesforce/core';
|
|
20
|
+
import { contentVersionMultipart } from './multipart.js';
|
|
21
|
+
/** How much of an error response body to include in a thrown error before truncating. */
|
|
22
|
+
const MAX_ERROR_BODY_LENGTH = 2000;
|
|
23
|
+
/**
|
|
24
|
+
* Turn a non-2xx response into an `SfError`.
|
|
25
|
+
*
|
|
26
|
+
* `fetch` — unlike the `got` client this replaced — resolves successfully for 4xx and 5xx, so every
|
|
27
|
+
* call site has to check `response.ok` itself. Missing one is how an HTML error page ends up
|
|
28
|
+
* written to disk with a `.pdf` extension.
|
|
29
|
+
*
|
|
30
|
+
* @param response - The failed response. Its body is consumed.
|
|
31
|
+
* @param context - What was being attempted, used as the error message prefix.
|
|
32
|
+
* @returns An `SfError` carrying the status and as much of the body as is useful.
|
|
33
|
+
*/
|
|
34
|
+
async function responseError(response, context) {
|
|
35
|
+
let body = '';
|
|
36
|
+
try {
|
|
37
|
+
body = (await response.text()).slice(0, MAX_ERROR_BODY_LENGTH);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// A body we can't read is not worth failing differently over; the status still tells the story.
|
|
41
|
+
}
|
|
42
|
+
return new SfError(`${context} failed with HTTP ${response.status} ${response.statusText}${body ? `: ${body}` : ''}`, 'ContentVersionRequestError');
|
|
43
|
+
}
|
|
21
44
|
/**
|
|
22
45
|
* Download a `ContentVersion`'s file data to a local directory.
|
|
23
46
|
*
|
|
@@ -26,14 +49,23 @@ import got from 'got';
|
|
|
26
49
|
* `ContentDocumentId`, and `FileExtension` are used to build the output file path.
|
|
27
50
|
* @param downloadDirectory - The local directory to write the file into.
|
|
28
51
|
* @returns The local path the file was written to.
|
|
52
|
+
* @throws {SfError} If the org returns a non-2xx response, before any file is created.
|
|
29
53
|
*/
|
|
30
54
|
export async function downloadContentVersion(targetOrgConnection, contentVersionDownload, downloadDirectory) {
|
|
31
55
|
const filePath = `${downloadDirectory}/${contentVersionDownload.ContentDocumentId}_${contentVersionDownload.Title.replaceAll(' ', '_')}.${contentVersionDownload.FileExtension}`;
|
|
32
|
-
await
|
|
56
|
+
const response = await fetch(`${targetOrgConnection.baseUrl()}/sobjects/ContentVersion/${contentVersionDownload.Id}/VersionData`, {
|
|
33
57
|
headers: {
|
|
34
58
|
Authorization: `Bearer ${targetOrgConnection.accessToken}`,
|
|
35
59
|
},
|
|
36
|
-
})
|
|
60
|
+
});
|
|
61
|
+
// Checked before the write stream is opened, so a failed download leaves nothing behind.
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw await responseError(response, `Download of ContentVersion ${contentVersionDownload.Id}`);
|
|
64
|
+
}
|
|
65
|
+
if (!response.body) {
|
|
66
|
+
throw new SfError(`Download of ContentVersion ${contentVersionDownload.Id} returned an empty response body.`, 'ContentVersionRequestError');
|
|
67
|
+
}
|
|
68
|
+
await stream.promises.pipeline(Readable.fromWeb(response.body), fs.createWriteStream(filePath));
|
|
37
69
|
return filePath;
|
|
38
70
|
}
|
|
39
71
|
/**
|
|
@@ -45,6 +77,7 @@ export async function downloadContentVersion(targetOrgConnection, contentVersion
|
|
|
45
77
|
* @param firstPublishLocationId - The ID of the record/library to attach the resulting
|
|
46
78
|
* `ContentDocument` to, if any.
|
|
47
79
|
* @returns The created `ContentVersion`, re-queried to include `ContentDocumentId`.
|
|
80
|
+
* @throws {SfError} If the org returns a non-2xx response.
|
|
48
81
|
*/
|
|
49
82
|
export async function uploadContentVersion(targetOrgConnection, pathOnClient, title, firstPublishLocationId) {
|
|
50
83
|
// Check that we have access to the file
|
|
@@ -54,18 +87,25 @@ export async function uploadContentVersion(targetOrgConnection, pathOnClient, ti
|
|
|
54
87
|
PathOnClient: pathOnClient,
|
|
55
88
|
Title: title ?? path.basename(pathOnClient),
|
|
56
89
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
const { contentType, createBody } = contentVersionMultipart({
|
|
91
|
+
entity: contentVersionCreateRequest,
|
|
92
|
+
filePath: pathOnClient,
|
|
93
|
+
filename: path.basename(pathOnClient),
|
|
94
|
+
});
|
|
95
|
+
const response = await fetch(`${targetOrgConnection.baseUrl()}/sobjects/ContentVersion`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
// `duplex: 'half'` is required by undici whenever the body is a stream.
|
|
98
|
+
body: Readable.toWeb(createBody()),
|
|
99
|
+
duplex: 'half',
|
|
62
100
|
headers: {
|
|
63
101
|
Authorization: `Bearer ${targetOrgConnection.accessToken}`,
|
|
64
|
-
'Content-Type':
|
|
102
|
+
'Content-Type': contentType,
|
|
65
103
|
},
|
|
66
|
-
resolveBodyOnly: true,
|
|
67
|
-
responseType: 'json',
|
|
68
104
|
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw await responseError(response, `Upload of "${pathOnClient}"`);
|
|
107
|
+
}
|
|
108
|
+
const data = (await response.json());
|
|
69
109
|
const queryResult = await targetOrgConnection.singleRecordQuery(`SELECT ContentDocumentId, FileExtension, Id, Title FROM ContentVersion WHERE Id='${data.id}'`);
|
|
70
110
|
return queryResult;
|
|
71
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contentVersionUtils.js","sourceRoot":"","sources":["../../src/common/contentVersionUtils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"contentVersionUtils.js","sourceRoot":"","sources":["../../src/common/contentVersionUtils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a multipart boundary.
|
|
4
|
+
*
|
|
5
|
+
* The boundary must not appear anywhere in the encoded body. Rather than scanning the file to
|
|
6
|
+
* prove that, we use 128 bits of randomness, which makes an accidental collision less likely than
|
|
7
|
+
* a disk error corrupting the upload anyway.
|
|
8
|
+
*
|
|
9
|
+
* @returns A boundary string, comfortably under the 70-character limit.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createBoundary(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Escape a filename for inclusion in a `Content-Disposition` header.
|
|
14
|
+
*
|
|
15
|
+
* Applies the same normalization the WHATWG `FormData` serializer does. Without it, a file named
|
|
16
|
+
* `my"file.pdf` would close the quoted string early, and one containing a newline could inject a
|
|
17
|
+
* header. The `form-data` package this replaced did neither.
|
|
18
|
+
*
|
|
19
|
+
* @param filename - The raw filename.
|
|
20
|
+
* @returns The filename with `"`, CR, and LF percent-encoded.
|
|
21
|
+
*/
|
|
22
|
+
export declare function escapeHeaderFilename(filename: string): string;
|
|
23
|
+
/** A multipart request body, plus the `Content-Type` header value that describes it. */
|
|
24
|
+
export type ContentVersionMultipart = {
|
|
25
|
+
/** The generated (or supplied) boundary. */
|
|
26
|
+
boundary: string;
|
|
27
|
+
/** The value for the request's `Content-Type` header, including the boundary. */
|
|
28
|
+
contentType: string;
|
|
29
|
+
/**
|
|
30
|
+
* Build the request body.
|
|
31
|
+
*
|
|
32
|
+
* This is a factory rather than a stream because a stream can only be consumed once, and a
|
|
33
|
+
* retried upload needs a fresh body. Each call re-opens the file.
|
|
34
|
+
*/
|
|
35
|
+
createBody: () => Readable;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Build the `multipart/form-data` body for a `ContentVersion` create request.
|
|
39
|
+
*
|
|
40
|
+
* The shape here matches Salesforce's documented example exactly, including the trailing semicolon
|
|
41
|
+
* after `name="entity_content"` and the absence of a `filename` on that part. That absence is the
|
|
42
|
+
* reason this is hand-built rather than delegated to the platform's `FormData`: a `FormData` part
|
|
43
|
+
* carrying `Content-Type: application/json` is necessarily a `Blob`, and a `Blob` part always
|
|
44
|
+
* serializes with a filename.
|
|
45
|
+
*
|
|
46
|
+
* @param options - The entity metadata, the file to attach, and an optional fixed boundary.
|
|
47
|
+
* @param options.entity - Serialized as the JSON `entity_content` part.
|
|
48
|
+
* @param options.filePath - Path of the file streamed as the `VersionData` part.
|
|
49
|
+
* @param options.filename - Filename advertised for the `VersionData` part.
|
|
50
|
+
* @param options.boundary - Boundary to use. Defaults to a random one; pass a fixed value in tests.
|
|
51
|
+
* @returns The boundary, the `Content-Type` header value, and a body factory.
|
|
52
|
+
*/
|
|
53
|
+
export declare function contentVersionMultipart(options: {
|
|
54
|
+
entity: unknown;
|
|
55
|
+
filePath: string;
|
|
56
|
+
filename: string;
|
|
57
|
+
boundary?: string;
|
|
58
|
+
}): ContentVersionMultipart;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026, Clay Chipps.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import { Readable } from 'node:stream';
|
|
19
|
+
/**
|
|
20
|
+
* Prefix for generated boundaries. Salesforce caps a boundary at 70 characters, so this plus the
|
|
21
|
+
* 32 hex characters appended to it has to stay under that.
|
|
22
|
+
*/
|
|
23
|
+
const BOUNDARY_PREFIX = 'simplyBoundary';
|
|
24
|
+
/** CRLF, which is what RFC 2046 requires between multipart headers and parts — not a bare `\n`. */
|
|
25
|
+
const CRLF = '\r\n';
|
|
26
|
+
/**
|
|
27
|
+
* Generate a multipart boundary.
|
|
28
|
+
*
|
|
29
|
+
* The boundary must not appear anywhere in the encoded body. Rather than scanning the file to
|
|
30
|
+
* prove that, we use 128 bits of randomness, which makes an accidental collision less likely than
|
|
31
|
+
* a disk error corrupting the upload anyway.
|
|
32
|
+
*
|
|
33
|
+
* @returns A boundary string, comfortably under the 70-character limit.
|
|
34
|
+
*/
|
|
35
|
+
export function createBoundary() {
|
|
36
|
+
return `${BOUNDARY_PREFIX}${crypto.randomBytes(16).toString('hex')}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Escape a filename for inclusion in a `Content-Disposition` header.
|
|
40
|
+
*
|
|
41
|
+
* Applies the same normalization the WHATWG `FormData` serializer does. Without it, a file named
|
|
42
|
+
* `my"file.pdf` would close the quoted string early, and one containing a newline could inject a
|
|
43
|
+
* header. The `form-data` package this replaced did neither.
|
|
44
|
+
*
|
|
45
|
+
* @param filename - The raw filename.
|
|
46
|
+
* @returns The filename with `"`, CR, and LF percent-encoded.
|
|
47
|
+
*/
|
|
48
|
+
export function escapeHeaderFilename(filename) {
|
|
49
|
+
return filename.replaceAll('\r', '%0D').replaceAll('\n', '%0A').replaceAll('"', '%22');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the `multipart/form-data` body for a `ContentVersion` create request.
|
|
53
|
+
*
|
|
54
|
+
* The shape here matches Salesforce's documented example exactly, including the trailing semicolon
|
|
55
|
+
* after `name="entity_content"` and the absence of a `filename` on that part. That absence is the
|
|
56
|
+
* reason this is hand-built rather than delegated to the platform's `FormData`: a `FormData` part
|
|
57
|
+
* carrying `Content-Type: application/json` is necessarily a `Blob`, and a `Blob` part always
|
|
58
|
+
* serializes with a filename.
|
|
59
|
+
*
|
|
60
|
+
* @param options - The entity metadata, the file to attach, and an optional fixed boundary.
|
|
61
|
+
* @param options.entity - Serialized as the JSON `entity_content` part.
|
|
62
|
+
* @param options.filePath - Path of the file streamed as the `VersionData` part.
|
|
63
|
+
* @param options.filename - Filename advertised for the `VersionData` part.
|
|
64
|
+
* @param options.boundary - Boundary to use. Defaults to a random one; pass a fixed value in tests.
|
|
65
|
+
* @returns The boundary, the `Content-Type` header value, and a body factory.
|
|
66
|
+
*/
|
|
67
|
+
export function contentVersionMultipart(options) {
|
|
68
|
+
const boundary = options.boundary ?? createBoundary();
|
|
69
|
+
const entityJson = JSON.stringify(options.entity);
|
|
70
|
+
const filename = escapeHeaderFilename(options.filename);
|
|
71
|
+
const header = Buffer.from(`--${boundary}${CRLF}` +
|
|
72
|
+
`Content-Disposition: form-data; name="entity_content";${CRLF}` +
|
|
73
|
+
`Content-Type: application/json${CRLF}${CRLF}` +
|
|
74
|
+
`${entityJson}${CRLF}` +
|
|
75
|
+
`--${boundary}${CRLF}` +
|
|
76
|
+
`Content-Type: application/octet-stream${CRLF}` +
|
|
77
|
+
`Content-Disposition: form-data; name="VersionData"; filename="${filename}"${CRLF}${CRLF}`, 'utf8');
|
|
78
|
+
const footer = Buffer.from(`${CRLF}--${boundary}--${CRLF}`, 'utf8');
|
|
79
|
+
const createBody = () => Readable.from((async function* body() {
|
|
80
|
+
yield header;
|
|
81
|
+
yield* fs.createReadStream(options.filePath);
|
|
82
|
+
yield footer;
|
|
83
|
+
})());
|
|
84
|
+
return {
|
|
85
|
+
boundary,
|
|
86
|
+
// Unquoted, matching what `form-data` and jsforce emit. RFC 2046 permits quoting, but the
|
|
87
|
+
// unquoted form is the one known to work against this endpoint.
|
|
88
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
89
|
+
createBody,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=multipart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.js","sourceRoot":"","sources":["../../src/common/multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;GAGG;AACH,MAAM,eAAe,GAAG,gBAAgB,CAAC;AAEzC,mGAAmG;AACnG,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,GAAG,eAAe,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACvE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,OAAO,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;AACzF,CAAC;AAiBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAKvC;IACC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,EAAE,CAAC;IACtD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAExD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CACxB,KAAK,QAAQ,GAAG,IAAI,EAAE;QACpB,yDAAyD,IAAI,EAAE;QAC/D,iCAAiC,IAAI,GAAG,IAAI,EAAE;QAC9C,GAAG,UAAU,GAAG,IAAI,EAAE;QACtB,KAAK,QAAQ,GAAG,IAAI,EAAE;QACtB,yCAAyC,IAAI,EAAE;QAC/C,iEAAiE,QAAQ,IAAI,IAAI,GAAG,IAAI,EAAE,EAC5F,MAAM,CACP,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,QAAQ,KAAK,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;IAEpE,MAAM,UAAU,GAAG,GAAa,EAAE,CAChC,QAAQ,CAAC,IAAI,CACX,CAAC,KAAK,SAAS,CAAC,CAAC,IAAI;QACnB,MAAM,MAAM,CAAC;QACb,KAAK,CAAC,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,MAAM,CAAC;IACf,CAAC,CAAC,EAAE,CACL,CAAC;IAEJ,OAAO;QACL,QAAQ;QACR,0FAA0F;QAC1F,gEAAgE;QAChE,WAAW,EAAE,iCAAiC,QAAQ,EAAE;QACxD,UAAU;KACX,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysf/simply-data",
|
|
3
3
|
"description": "Utilities for working with data",
|
|
4
|
-
"version": "2.3.
|
|
4
|
+
"version": "2.3.26",
|
|
5
5
|
"author": "@ClayChipps",
|
|
6
6
|
"homepage": "https://github.com/SimplySF/simply-node",
|
|
7
7
|
"bugs": "https://github.com/SimplySF/simply-node/issues",
|
|
@@ -68,8 +68,6 @@
|
|
|
68
68
|
"@simplysf/simply-core": "^1.3.0",
|
|
69
69
|
"@simplysf/simply-plugin-kit": "^1.0.4",
|
|
70
70
|
"csv-parse": "^7.0.2",
|
|
71
|
-
"form-data": "^4.0.5",
|
|
72
|
-
"got": "14.6.6",
|
|
73
71
|
"p-queue": "^9.2.0"
|
|
74
72
|
},
|
|
75
73
|
"devDependencies": {
|
|
@@ -216,5 +214,5 @@
|
|
|
216
214
|
"output": []
|
|
217
215
|
}
|
|
218
216
|
},
|
|
219
|
-
"gitHead": "
|
|
217
|
+
"gitHead": "b347839b65111c6691ebf541d89dd7308fc51152"
|
|
220
218
|
}
|