Milestone 0: import zappier billing, Verae middleware, and Zapier research

Compose-ready workspace: packages/zappier (rate card, portal, Stripe),
packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier
(CLI app), vendor/zapier-platform, and research/zapier vendor corpus.

Gate 0 structure checks pass. Product code and research are not yet wired.
This commit is contained in:
George Lambert 2026-09-09 02:37:36 -04:00
commit b4150c8250
1364 changed files with 6814366 additions and 0 deletions

View file

@ -0,0 +1 @@
coverage/*

View file

@ -0,0 +1,92 @@
{
"extends": "eslint:recommended",
"env": {
"es6": true,
"node": true
},
"rules": {
"arrow-parens": [2, "always"],
"camelcase": 0,
"comma-dangle": 0,
"comma-spacing": 2,
"consistent-return": 2,
"curly": [2, "all"],
"dot-notation": [2, { "allowKeywords": true }],
"eol-last": 2,
"eqeqeq": [2, "smart"],
"indent": [2, 2],
"jsx-quotes": [2, "prefer-double"],
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
"keyword-spacing": 2,
"new-cap": 0,
"new-parens": 2,
"no-alert": 2,
"no-array-constructor": 2,
"no-caller": 2,
"no-console": [2, { "allow": ["info", "warn", "error"] }],
"no-delete-var": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-fallthrough": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-label-var": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-mixed-spaces-and-tabs": [2, false],
"no-multi-spaces": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-wrappers": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-process-exit": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-sequences": 2,
"no-shadow": 2,
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-underscore-dangle": 0,
"no-unused-expressions": 2,
"no-unused-vars": [
2,
{ "vars": "all", "args": "after-used", "varsIgnorePattern": "should" }
],
"no-use-before-define": 2,
"no-with": 2,
"quotes": [
2,
"single",
{ "avoidEscape": true, "allowTemplateLiterals": true }
],
"semi": 2,
"semi-spacing": [2, { "before": false, "after": true }],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"strict": [0, "never"],
"yoda": [2, "never"]
},
"globals": {
"describe": true,
"before": true,
"after": true,
"beforeEach": true,
"afterEach": true,
"it": true
},
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
}
}

View file

@ -0,0 +1,6 @@
build
docs
node_modules
*.log
.environment
coverage

View file

@ -0,0 +1 @@
v6.10.2

View file

@ -0,0 +1,7 @@
language: node_js
node_js:
- "6.10.2"
before_script: bash ./travis-setup.sh && npm install -g zapier-platform-cli
script: zapier test
notifications:
email: false

View file

@ -0,0 +1,88 @@
# Example App - OneDrive
This is an example CLI App for the Zapier Developer Platform. It uses the OneDrive API as the target, showing what a
full-featured app looks like. It follows the best practices for building an app on Zapier.
What it demonstrates:
* OAuth2
* File Handling
* Hydration
* `beforeRequest` handler (and how to skip it for a specific call)
* Resources
* Breaking an app up into maintable pieces
Throughout the code we've added `CODE TIP` and `UX TIP` comments. These are good spots to find out some of the best
practices and gotchas you may run into.
## Getting Started
For a quicker primer on the OneDrive API see [here](https://dev.onedrive.com/getting-started.htm).
Also note, this app is not one you can download and start using immediately. Since it relies on the OneDrive API,
there is some out-of-band setup that you must complete if you want to be able to use the app on zapier.com. We walk you
through it all, but be prepared!
### 1. Install
To pull down the code and install the needed dependencies, run:
```bash
zapier-platform init --template=onedrive ./
npm install
```
> We recommend using the zapier-platform-cli and `zapier-platform init .`  to create an app - youll be presented with a list of currently available templates to start with.
### 2. Seeing Your App on zapier.com
The first step is to do `zapier-platform register 'My Example OneDrive App'`. This logs you in and tells Zapier about your new app.
Now try a `zapier-platform push`. At this point, you can go to zapier.com/app/editor and see your app in the Editor.
You won't be able to use the app yet (there are a couple steps required before auth works), but this gives you
a feel for some of the basic commands of the CLI tool.
### 3. Register a Microsoft App
The OneDrive API follows a typical OAuth2 flow that requires your app to have a `CLIENT_ID` and `CLIENT_SECRET`. For
security reasons, we cannot include those for you, so you need to head over to `https://apps.dev.microsoft.com/`
and register to obtain your own. When registering your app in MS, the minimal information you need to setup is:
1. App Name - Can be anything, like 'My Example Zapier App'
1. Application Secret - Generate a new password, copying that password somewhere for later
1. Platform - Choose web. For Redirect URL, run `zapier-platform describe --format=json | grep 'Redirect URI'` in your terminal and paste in the URI provided by Zapier
1. Click Save
You now need to tell Zapier what `CLIENT_ID` and `CLIENT_SECRET` to use when doing the OAuth2 flow. OneDrive uses
the Applicaiton ID of the app you just registered as the `CLIENT_ID`, and the Application Secret as the `CLIENT_SECRET`.
You can set those in Zapier like so:
```bash
zapier-platform env 1.0.0 CLIENT_ID <app_id>
zapier-platform env 1.0.0 CLIENT_SECRET <app_secret>
```
You should now be able to go back to zapier.com and connect your OneDrive account!
### 4. Exploring
At this point you have a fully working Zapier app you can build Zaps with. Feel free to make tweaks to your app
locally, do a `zapier-platform push`, and see the changes take effect in production right away.
## Testing
The tests are provided as an example of how to write good tests, along with stubs to give an idea of what scenarios to
cover. If you want to actually run the test suite, you need to do a couple things.
First is to manually walk through the OAuth2 flow and capture an access token and refresh token. You can then do:
```bash
export ACCESS_TOKEN='<access_token>'
export REFRESH_TOKEN='<refresh_token>'
```
The tests will pick those up and run with those credentials.
The second thing you need to do is update `TEST_RESOURCES` in `test/test-utils.js` with IDs and folder names in your
OneDrive account. Once that is updated, you should be able to do:
`zapier-platform test`

View file

@ -0,0 +1,127 @@
'use strict';
const baseOauthUrl = 'https://login.microsoftonline.com/common/oauth2';
// To get your OAuth2 redirect URI, run `zapier describe` and update this variable.
// Will looke like 'https://zapier.com/dashboard/auth/oauth/return/App123CLIAPI/'
const redirectUri = '';
const getAuthorizeURL = (z, bundle) => {
let url = `${baseOauthUrl}/v2.0/authorize`;
const urlParts = [
`client_id=${process.env.CLIENT_ID}`,
`redirect_uri=${encodeURIComponent(bundle.inputData.redirect_uri)}`,
'response_type=code',
];
if (bundle.inputData.accountType === 'business') {
url = `${baseOauthUrl}/authorize`;
} else {
urlParts.push(`state=${bundle.inputData.state}`);
}
const finalUrl = `${url}?${urlParts.join('&')}`;
return finalUrl;
};
const getAccessToken = (z, bundle) => {
let url = `${baseOauthUrl}/v2.0/token`;
const body = {
code: bundle.inputData.code,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
grant_type: 'authorization_code',
};
if (bundle.inputData.accountType === 'business') {
url = `${baseOauthUrl}/token`;
body.redirect_uri = redirectUri;
body.resource = 'https://graph.microsoft.com/';
}
const promise = z.request(url, {
method: 'POST',
body,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
return promise.then((response) => ({
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
id_token: response.data.id_token,
}));
};
const refreshAccessToken = (z, bundle) => {
let url = `${baseOauthUrl}/v2.0/token`;
const body = {
refresh_token: bundle.authData.refresh_token,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
grant_type: 'refresh_token',
};
if (bundle.authData.accountType === 'business') {
url = `${baseOauthUrl}/token`;
body.redirect_uri = redirectUri;
body.resource = 'https://graph.microsoft.com/';
}
const promise = z.request(url, {
method: 'POST',
body,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
return promise.then((response) => ({
access_token: response.data.access_token,
refresh_token: response.data.refresh_token,
id_token: response.data.id_token,
}));
};
// The test call Zapier makes to ensure an access token is valid
// UX TIP: Hit an endpoint that always returns data with valid credentials,
// like a /profile or /me endpoint. That way the success/failure is related to
// the token and not because the user didn't happen to have a recently created record.
const testAuth = (z) => {
const promise = z.request({
url: 'https://graph.microsoft.com/v1.0/me',
});
return promise.then((response) => response.data);
};
module.exports = {
type: 'oauth2',
connectionLabel: '{{bundle.inputData.userPrincipalName}}',
oauth2Config: {
authorizeUrl: getAuthorizeURL,
getAccessToken,
refreshAccessToken,
// Set so Zapier automatically checks for 401s and calls refreshAccessToken
autoRefresh: true,
// offline_access is necessary for the refresh_token
scope: 'User.Read Files.ReadWrite.All offline_access',
},
test: testAuth,
fields: [
{
key: 'accountType',
label: 'Account Type',
choices: {
personal: 'Personal - live.com/outlook.com',
business: 'Business - Work or School',
},
default: 'personal',
required: true,
},
],
};

View file

@ -0,0 +1,14 @@
'use strict';
// This adds the needed auth header to a request. By registering this on the
// app's `beforeRequest` section, every z.request() call will run this function.
const includeBearerToken = (request, z, bundle) => {
if (bundle.authData.access_token) {
request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
}
return request;
};
module.exports = {
includeBearerToken,
};

View file

@ -0,0 +1,12 @@
'use strict';
const BASE_ITEM_URL = 'https://graph.microsoft.com/v1.0';
const BIG_FILE_MSG = 'File contents too big.';
const BINARY_CONTENT_MSG = 'Binary contents unsupported.';
module.exports = {
BASE_ITEM_URL,
BIG_FILE_MSG,
BINARY_CONTENT_MSG,
};

View file

@ -0,0 +1,80 @@
'use strict';
const utils = require('../utils');
const getStringByteSize = utils.getStringByteSize;
const baseItem = require('../resources/base-item');
const fileResource = require('../resources/file');
const createTextFile = (z, bundle) => {
const folder = bundle.inputData.folder || '';
let name = `${bundle.inputData.name}.txt`;
// Remove potential duplicate extension
if (name.endsWith('.txt.txt')) {
name = name.slice(0, -4);
}
const fileSize = getStringByteSize(bundle.inputData.file);
const contentType = 'text/plain; charset=UTF-8';
return baseItem.handleCreateWithSession(
z,
bundle,
bundle.inputData.file,
fileSize,
contentType,
folder,
name,
);
};
module.exports = {
key: 'textFile',
noun: 'Text File',
display: {
label: 'Create New Text File',
description:
'Creates a brand new text file from plain text content you specify.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to place the file. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
{
key: 'file',
type: 'text',
label: 'File',
required: true,
helpText: 'Plain text content to put inside the new text file.',
},
{
key: 'name',
type: 'string',
label: 'Name of New File',
required: true,
helpText:
'Specify the name of this file. ".txt" will always be appended.',
},
],
perform: createTextFile,
// CODE TIP: If you define a resource and additional operations on that
// resource, you can simply import the resource and re-use parts of the
// definition in the other operations, like here. In this case, creating a
// text file and uploading a file are the same endpoint in the OneDrive API
// so the sample and output are the same for each.
sample: fileResource.sample,
outputFields: fileResource.outputFields,
},
};

View file

@ -0,0 +1,50 @@
'use strict';
const utils = require('./utils');
const handleError = utils.handleError;
const getStringByteSize = utils.getStringByteSize;
const {
BASE_ITEM_URL,
BIG_FILE_MSG,
BINARY_CONTENT_MSG,
} = require('./constants');
const getFileContents = (z, bundle) => {
const options = {
url: `${BASE_ITEM_URL}/me/drive/items/${bundle.inputData.id}/content`,
};
return z
.request(options)
.then((response) => {
const fileContents = response.content;
const fileSize = getStringByteSize(fileContents);
// UX TIP: It's good to be mindful of how users will consume the outputs
// of your app and how those outputs travel through Zapier.
// In this scenario, users asked for the full content of a file so they
// could map it into the body of an email vs. adding it as an attachment.
// Cool workflow, but there are limits to passing around large payloads.
// Here we compromise and hydrate the full content of the file for the
// user, but only allow it through if it's under a sane size limit. That
// way we don't send MBs of text through Zapier in every task. Note that
// this does not prevent the user from transfering large files in Zaps, as
// file fields are handled differently. This is specifically around our
// app providing a field that the user wants to map into text inputs in
// other Actions.
if (fileSize >= 100 * 1024) {
return BIG_FILE_MSG;
}
try {
return decodeURIComponent(fileContents);
} catch (e) {
return BINARY_CONTENT_MSG;
}
})
.catch(handleError);
};
module.exports = {
getFileContents,
};

View file

@ -0,0 +1,47 @@
const authentication = require('./authentication');
const { includeBearerToken } = require('./before-handlers');
const hydrators = require('./hydrators');
const folder = require('./resources/folder');
const file = require('./resources/file');
const createTextFile = require('./creates/text-file');
// We can roll up all our behaviors in an App.
const App = {
// This is just shorthand to reference the installed dependencies you have. Zapier will
// need to know these before we can upload
version: require('./package.json').version,
platformVersion: require('zapier-platform-core').version,
authentication,
hydrators,
// Ordered list of functions to pass the request object through before
// sending it. Here we register one function that will set the auth header.
beforeRequest: [includeBearerToken],
afterResponse: [],
/* Register the two resources for this app. Each resource defines the `list`,
* `search`, and `create` properties, allowing Zapier to automatically generate
* a trigger, search, and a create for each one.
*/
resources: {
[folder.key]: folder,
[file.key]: file,
},
triggers: {},
searches: {},
/* In addition to the default create from the file resource, we also want to
* allow users to create plain text files, so we register that create here.
*/
creates: {
[createTextFile.key]: createTextFile,
},
};
module.exports = App;

View file

@ -0,0 +1,34 @@
{
"name": "zapier-platform-example-app-onedrive",
"version": "1.0.0",
"description": "An example app for the Zapier platform. Implements part of the OneDrive API",
"repository": "zapier/zapier-platform-example-app-onedrive",
"homepage": "https://zapier.com/",
"author": "Zapier <partners@zapier.com>",
"license": "BSD-3-Clause",
"main": "index.js",
"scripts": {
"lint": "node_modules/.bin/eslint .",
"posttest": "npm run lint",
"test": "mocha --recursive --timeout 25000",
"coverage": "istanbul cover _mocha -- --recursive --timeout 25000"
},
"engines": {
"node": ">=6.10.2",
"npm": ">=3.0.0"
},
"dependencies": {
"async": "2.4.1",
"content-disposition": "0.5.2",
"lodash": "4.17.23",
"node-fetch": "1.7.1",
"zapier-platform-core": "19.1.0"
},
"devDependencies": {
"eslint": "3.19.0",
"istanbul": "0.4.5",
"mocha": "3.2.0",
"should": "11.2.1"
},
"private": true
}

View file

@ -0,0 +1,154 @@
'use strict';
//
// This is what file and folder use as a "base"
//
const _ = require('lodash');
const fetch = require('node-fetch');
const utils = require('../utils');
const hydrators = require('../hydrators');
const parseResponse = utils.parseResponse;
const handleError = utils.handleError;
const cleanupPaths = utils.cleanupPaths;
const { BASE_ITEM_URL } = require('../constants');
const getItem = (itemType, z, bundle) => {
const options = {
url: `${BASE_ITEM_URL}/me/drive/items/${bundle.inputData.id}`,
};
return z
.request(options)
.then(_.partial(parseResponse, z, itemType))
.then((item) => cleanupPaths([item])[0])
.then((item) => {
if (item.file) {
item.fileContents = z.dehydrate(hydrators.getFileContents, {
id: item.id,
});
}
return item;
})
.catch(handleError);
};
const listItems = (itemType, z, bundle) => {
let folder = bundle.inputData.folder || '';
if (folder) {
folder = `:${encodeURIComponent(folder)}:`; // OneDrive's URL format
}
const options = {
url: `${BASE_ITEM_URL}/me/drive/root${folder}/children`,
};
return z
.request(options)
.then(_.partial(parseResponse, z, itemType))
.then(cleanupPaths)
.then((items) => {
items.forEach((item) => {
if (item.file) {
item.fileContents = z.dehydrate(hydrators.getFileContents, {
id: item.id,
});
}
});
return items;
})
.catch(handleError);
};
const searchItem = (itemType, z, bundle) => {
let folder = bundle.inputData.folder || '';
if (folder) {
folder = `:${encodeURIComponent(folder)}:`; // OneDrive's URL format
}
const options = {
url: `${BASE_ITEM_URL}/me/drive/root${folder}/search(q='${encodeURIComponent(
bundle.inputData.name,
)}')`,
};
return z
.request(options)
.then(_.partial(parseResponse, z, itemType))
.then(cleanupPaths)
.catch(handleError);
};
// Note this only works for files, but is here so creates/text-file can reuse it
const handleCreateWithSession = (
z,
bundle,
fileContents,
fileSize,
fileContentType,
folder,
name,
) => {
if (folder) {
folder = encodeURIComponent(folder);
}
name = encodeURIComponent(name);
return z
.request({
url: `${BASE_ITEM_URL}/me/drive/root:${folder}/${name}:/createUploadSession`,
method: 'POST',
body: {
item: {
'@microsoft.graph.conflictBehavior': 'rename',
},
},
headers: {
'content-type': 'application/json',
},
})
.then((response) => {
const uploadUrl = response.data.uploadUrl;
// This should work fine for files up to 60MB (https://dev.onedrive.com/items/upload_large_files.htm#upload-fragments)
if (!fileContentType.includes('charset')) {
fileContentType += '; charset=UTF-8';
}
// CODE TIP: If you define beforeRequest handlers, then end up in a
// situation where you don't want that pre-processing, you can drop to a
// raw `fetch` call instead of the z.request() to avoid those handlers
return fetch(uploadUrl, {
method: 'PUT',
body: fileContents,
headers: {
'content-type': fileContentType,
'content-length': fileSize,
'content-range': `bytes 0-${fileSize - 1}/${fileSize}`,
},
});
})
.then((response) => response.text())
.then((content) => {
const resourceId = z.JSON.parse(content).id;
bundle.inputData = {
id: resourceId,
};
return _.partial(getItem, 'file')(z, bundle);
})
.catch(handleError);
};
module.exports = {
getItem,
listItems,
searchItem,
handleCreateWithSession,
};

View file

@ -0,0 +1,165 @@
'use strict';
const _ = require('lodash');
const utils = require('../utils');
const handleError = utils.handleError;
const getFileDetailsFromRequest = utils.getFileDetailsFromRequest;
const baseItem = require('./base-item');
const getFile = _.partial(baseItem.getItem, 'file');
const listFiles = _.partial(baseItem.listItems, 'file');
const createFile = (z, bundle) => {
const folder = bundle.inputData.folder || '';
// NOTE: `file` is really a URL like "https://zapier.com/engine/hydrate/<unique_id>/.blahblahblah:blah:blah/"
const file = bundle.inputData.file;
return getFileDetailsFromRequest(file)
.then((fileDetails) => {
const name =
bundle.inputData.name || fileDetails.filename || 'unnamedfile.unknown';
return baseItem.handleCreateWithSession(
z,
bundle,
fileDetails.content,
fileDetails.size,
fileDetails.contentType,
folder,
name,
);
})
.catch(handleError);
};
const searchFile = _.partial(baseItem.searchItem, 'file');
module.exports = {
key: 'file',
noun: 'File',
get: {
display: {
label: 'Get File',
description: 'Gets a file.',
},
operation: {
inputFields: [
{
key: 'id',
required: true,
},
],
perform: getFile,
},
},
// Will become a trigger on the app. Key will be `fileList`
list: {
display: {
label: 'New File',
description: 'Triggers when a new file is added in a folder.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to look for the file. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
],
perform: listFiles,
},
},
// Will become a create on the app. Key will be `fileCreate`
create: {
display: {
label: 'Upload File',
description: 'Upload an existing file or attachment.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to place the file. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
{
key: 'file',
type: 'file',
label: 'File',
required: true,
helpText:
'Must be a file object from another service (or some text or URL).',
},
{
key: 'name',
type: 'string',
label: 'File Name',
required: false,
helpText:
'By default, we use the same name and extension as the original file.',
},
],
perform: createFile,
},
},
// Will become a search on the app. Key will be `fileSearch`
search: {
display: {
label: 'Find File',
description: 'Finds a file by name.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to look for the file. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
{
key: 'name',
required: true,
type: 'string',
},
],
perform: searchFile,
},
},
sample: {
id: '1',
name: 'Example.jpg',
_path: '/Something/Example.jpg',
_parent: '/Something',
webUrl: 'https://example.com',
'@microsoft.graph.downloadUrl': 'https://example.com',
createdDateTime: '2016-09-16T03:37:04.72Z',
lastModifiedDateTime: '2016-09-16T03:37:04.72Z',
},
outputFields: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'File Name' },
{ key: '_path', label: 'File Path' },
{ key: '_parent', label: 'Folder' },
{ key: 'webUrl', label: 'URL' },
{ key: '@microsoft.graph.downloadUrl', label: 'Download URL' },
],
};

View file

@ -0,0 +1,170 @@
'use strict';
const _ = require('lodash');
const utils = require('../utils');
const parseResponse = utils.parseResponse;
const handleError = utils.handleError;
const cleanupPaths = utils.cleanupPaths;
const extractParentsFromPath = utils.extractParentsFromPath;
const { BASE_ITEM_URL } = require('../constants');
const baseItem = require('./base-item');
const getFolder = _.partial(baseItem.getItem, 'folder');
const listFolders = (z, bundle) => {
return baseItem.listItems('folder', z, bundle).then((results) => {
// Add parents when being called in the context of populating a dynamic dropdown (prefill).
// This allows users to "navigate back" to previous dirs in the Zap Editor
if (bundle.meta.prefill && bundle.inputData.folder) {
const parents = extractParentsFromPath(bundle.inputData.folder);
parents.forEach((result) => results.unshift(result));
}
return results;
});
};
const createFolder = (z, bundle) => {
let folder = bundle.inputData.folder || '';
if (folder) {
folder = `:${encodeURIComponent(folder)}:`; // OneDrive URI format
}
return z
.request({
url: `${BASE_ITEM_URL}/me/drive/root${folder}/children`,
method: 'POST',
body: {
name: bundle.inputData.name,
folder: {}, // This tells OneDrive it's a folder (https://dev.onedrive.com/items/create.htm#example)
'@microsoft.graph.conflictBehavior': 'rename',
},
headers: {
'content-type': 'application/json',
},
})
.then(_.partial(parseResponse, z, 'folder'))
.then(cleanupPaths)
.catch(handleError);
};
const searchFolder = _.partial(baseItem.searchItem, 'folder');
module.exports = {
key: 'folder',
noun: 'Folder',
get: {
display: {
label: 'Get Folder',
description: 'Gets a folder.',
},
operation: {
inputFields: [
{
key: 'id',
required: true,
},
],
perform: getFolder,
},
},
// Will become a trigger on the app. Key will be `folderList`
list: {
display: {
label: 'New Folder',
description: 'Triggers when a new folder is added.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
// As mentioned above, triggers genereated from resources follow a
// format of `<resource.key>List`, so that is what we use in all the dynamic dropdowns
dynamic: 'folderList._path.name',
helpText:
'Folder where to look for the folder. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
],
perform: listFolders,
},
},
// Will become a create on the app. Key will be `folderCreate`
create: {
display: {
label: 'Create Folder',
description: 'Creates a new folder.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to create the folder. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
{
key: 'name',
required: true,
type: 'string',
},
],
perform: createFolder,
},
},
// Will become a search on the app. Key will be `folderSearch`
search: {
display: {
label: 'Find Folder',
description: 'Finds a folder by name.',
},
operation: {
inputFields: [
{
key: 'folder',
type: 'string',
label: 'Folder',
required: false,
dynamic: 'folderList._path.name',
helpText:
'Folder where to look for the folder. Keep clicking the dropdown to go inside folders. Defaults to the top-level folder if left blank.',
},
{
key: 'name',
required: true,
type: 'string',
},
],
perform: searchFolder,
},
},
sample: {
id: '1',
name: 'Example',
_path: '/Something/Example',
_parent: '/Something',
webUrl: 'https://example.com',
createdDateTime: '2016-09-16T03:37:04.72Z',
lastModifiedDateTime: '2016-09-16T03:37:04.72Z',
},
outputFields: [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Folder Name' },
{ key: '_path', label: 'Folder Path' },
{ key: '_parent', label: 'Parent Folder' },
{ key: 'webUrl', label: 'URL' },
],
};

View file

@ -0,0 +1,75 @@
require('should');
const zapier = require('zapier-platform-core');
const testUtils = require('./test-utils');
const App = require('../index');
const appTester = zapier.createAppTester(App);
describe('Authentication', () => {
before(testUtils.globalBeforeSetup);
it('should refresh auth', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
};
appTester(App.authentication.oauth2Config.refreshAccessToken, bundle)
.then((result) => {
result.should.have.property('access_token');
result.should.have.property('refresh_token');
done();
})
.catch(done);
});
it('should test auth', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
accountType: 'personal',
},
};
appTester(App.authentication.test, bundle)
.then((result) => {
result.should.have.property('userPrincipalName');
done();
})
.catch(done);
});
it('should generate an authorize URL', (done) => {
const bundle = {
// When an app runs on zapier.com, these will be generated by Zapier and
// passed to your app in the bundle. For the purpose of this test, we
// create a bundle with dummy values to ensure our function works.
inputData: {
state: '4444',
redirect_uri: 'https://zapier.com/',
accountType: 'personal',
},
environment: {
CLIENT_ID: '1234',
CLIENT_SECRET: 'asdf',
},
};
appTester(App.authentication.oauth2Config.authorizeUrl, bundle)
.then((authorizeUrl) => {
authorizeUrl.should.eql(
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=1234&redirect_uri=http%3A%2F%2Fzapier.com%2F&response_type=code&state=4444',
);
done();
})
.catch(done);
});
});

View file

@ -0,0 +1,112 @@
'use strict';
require('should');
const zapier = require('zapier-platform-core');
const testUtils = require('../test-utils');
const App = require('../../index');
const appTester = zapier.createAppTester(App);
const TEST_RESOURCES = testUtils.TEST_RESOURCES;
describe('Create Text File', () => {
before(testUtils.globalBeforeSetup);
it('should create a new text file in the root dir', (done) => {
const file = 'Sample content';
const name = `Test-${new Date().getTime()}`;
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
file,
name,
},
};
appTester(App.creates.textFile.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
result.name.should.containEql(name);
result.name.should.containEql('.txt');
result.name.should.not.containEql('.txt.txt');
result._parent.should.eql('');
result._path.should.containEql(`/${name}`);
done();
})
.catch(done);
});
it('should create a text file with a unicode name and contents', (done) => {
const file = 'Ohweeeeee®™¥¶‰÷!!!!!!!!!!!';
const name = `Test-ç€is-æ-${new Date().getTime()}.txt`;
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: TEST_RESOURCES.folder.path,
name,
file,
},
};
appTester(App.creates.textFile.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
result.name.should.containEql(name);
result.name.should.containEql('.txt');
result.name.should.not.containEql('.txt.txt');
result._parent.should.eql(TEST_RESOURCES.folder.parent);
result._path.should.containEql(`${TEST_RESOURCES.folder.path}/${name}`);
done();
})
.catch(done);
});
it('should create a new text file in the root dir, renaming it, because another one exists', (done) => {
const file = 'Sample content';
const name = 'boom';
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
file,
name,
},
};
appTester(App.creates.textFile.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
result.name.should.containEql(`${name} `);
result.name.should.containEql('.txt');
result.name.should.not.containEql('.txt.txt');
result.name.should.not.containEql(`${name}.txt`);
result._parent.should.eql('');
result._path.should.containEql(`/${name} `);
done();
})
.catch(done);
});
});

View file

@ -0,0 +1,47 @@
require('should');
const zapier = require('zapier-platform-core');
const testUtils = require('./test-utils');
const App = require('../index');
const appTester = zapier.createAppTester(App);
const TEST_RESOURCES = testUtils.TEST_RESOURCES;
describe('Hydrators', () => {
before(testUtils.globalBeforeSetup);
it('should get file contents', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
},
inputData: {
id: TEST_RESOURCES.root.id,
},
};
appTester(App.hydrators.getFileContents, bundle)
.then((fileContents) => {
fileContents.length.should.above(0);
done();
})
.catch(done);
});
it('should get unicode file contents', (done) => {
// CODE TIP: Hydrating files often involves touching the raw bytes (and
// handling the file names), so it's a good idea to test different edge cases
done();
});
it('should not get file contents for big files', (done) => {
// An example edge case that would be worth testing
done();
});
it('should not get file contents for binary files', (done) => {
// An example edge case that would be worth testing
done();
});
});

View file

@ -0,0 +1,186 @@
'use strict';
require('should');
const zapier = require('zapier-platform-core');
const testUtils = require('../test-utils');
const App = require('../../index');
const appTester = zapier.createAppTester(App);
const TEST_RESOURCES = testUtils.TEST_RESOURCES;
describe('File Resource', () => {
before(testUtils.globalBeforeSetup);
it('should get an existing file in the root folder', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
id: TEST_RESOURCES.root.id,
},
};
appTester(App.resources.file.get.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
result.name.should.eql(TEST_RESOURCES.root.name);
result._path.should.eql(TEST_RESOURCES.root.path);
result._parent.should.eql(TEST_RESOURCES.root.parent);
done();
})
.catch(done);
});
it('should get an existing file in a child folder', (done) => {
// An example scenario that could be worth testing if the HTTP request for
// a child folder is sufficiently different from a root folder
done();
});
it('should list files in root dir', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
},
};
appTester(App.resources.file.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
});
done();
})
.catch(done);
});
it('should not miss any fields from the sample', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
},
};
appTester(App.resources.file.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
});
const file = results[0];
const sampleKeys = Object.keys(App.resources.file.sample);
sampleKeys.forEach((sampleKey) => file[sampleKey].should.eql(true));
done();
})
.catch(done);
});
it('should upload a new file in the root dir', (done) => {
const file =
'https://cdn.zapier.com/storage/files/f6679cf77afeaf6b8426de8d7b9642fc.pdf';
const name = `Test-${new Date().getTime()}-ç€is-æ.pdf`;
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
file,
name,
},
};
appTester(App.resources.file.create.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('file');
result.should.not.have.property('folder');
result.name.should.containEql(name);
result._parent.should.eql('');
result._path.should.containEql(`/${name}`);
done();
})
.catch(done);
});
it('should upload an utf8-named file', (done) => {
// CODE TIP: It's a good idea to write multiple tests cases around creates
// to verify some of the common edges:
// * Non-ascii data
// * Records associated with the right relationships (i.e. a file in a folder, an owner of a lead)
// * Critical defaults work as expected
// * Dates & times are set correctly
done();
});
it('should upload a new file in the root dir, renaming it, because another one exists', (done) => {
// An example of an edge case where a naming collision in OneDrive could
// cause an overwrite or an auto-renaming of the file, so we want to test
// and make sure we get the behavior we want (auto-renaming)
done();
});
it('should find a file in the root dir', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
name: 'sample',
},
};
appTester(App.resources.file.search.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results[0].should.have.property('id');
results[0].should.have.property('file');
results[0].should.not.have.property('folder');
results[0].name.should.containEql('sample');
results[0]._parent.should.eql('');
results[0]._path.should.containEql('/sample');
done();
})
.catch(done);
});
it('should find a file in a child dir', (done) => {
// An example scenario that could be worth testing if the HTTP request for
// a child folder is sufficiently different from a root folder
done();
});
it('should find a unicode file in a child dir', (done) => {
// CODE TIP: Like with creates, adding multiple tests around the common
// edge cases of searching is a good idea
done();
});
});

View file

@ -0,0 +1,247 @@
'use strict';
require('should');
const zapier = require('zapier-platform-core');
const testUtils = require('../test-utils');
const App = require('../../index');
const appTester = zapier.createAppTester(App);
const TEST_RESOURCES = testUtils.TEST_RESOURCES;
describe('Folder Resource', () => {
before(testUtils.globalBeforeSetup);
it('should get an existing root folder', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
id: TEST_RESOURCES.folder.id,
},
};
appTester(App.resources.folder.get.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
result.name.should.eql(TEST_RESOURCES.root.name);
result._path.should.eql(TEST_RESOURCES.root.path);
result._parent.should.eql(TEST_RESOURCES.root.parent);
done();
})
.catch(done);
});
it('should get an existing child folder', (done) => {
// An example scenario that could be worth testing if the HTTP request for
// a child folder is sufficiently different from a root folder
done();
});
it('should list folders in root dir', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
},
meta: {
prefill: false,
},
};
appTester(App.resources.folder.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
});
done();
})
.catch(done);
});
it('should list folders without parents on no prefill', (done) => {
// CODE TIP: When reusing a list operation to power a trigger and a
// dynmaic dropdown (prefill), it's a good idea to test both scenarios if the
// output from the operation varies between the two contexts
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: TEST_RESOURCES.childFolder.path,
},
meta: {
prefill: false,
},
};
appTester(App.resources.folder.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
let foundParents = false;
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
if (result.id < 0) {
foundParents = true;
}
});
foundParents.should.eql(false);
done();
})
.catch(done);
});
it('should list folders with parents on prefill', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: TEST_RESOURCES.childFolder.path,
},
meta: {
prefill: true,
},
};
appTester(App.resources.folder.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
let foundParents = false;
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
if (result.id < 0) {
foundParents = true;
}
});
foundParents.should.eql(true);
done();
})
.catch(done);
});
it('should not miss any fields from the sample', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
},
meta: {
prefill: false,
},
};
appTester(App.resources.folder.list.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results.forEach((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
});
const folder = results[0];
const sampleKeys = Object.keys(App.resources.folder.sample);
sampleKeys.forEach((sampleKey) => folder[sampleKey].should.eql(true));
done();
})
.catch(done);
});
it('should create a new folder in the root dir', (done) => {
const name = `Test-${new Date().getTime()}`;
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
name,
},
};
appTester(App.resources.folder.create.operation.perform, bundle)
.then((result) => {
result.should.have.property('id');
result.should.have.property('folder');
result.should.not.have.property('file');
result.name.should.containEql(name);
result._parent.should.eql('');
result._path.should.containEql(`/${name}`);
done();
})
.catch(done);
});
it('should create a new folder in a child dir', (done) => {
// An example scenario that could be worth testing if the HTTP request for
// a child folder is sufficiently different from a root folder
done();
});
it('should find a folder in the root dir', (done) => {
const bundle = {
authData: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
inputData: {
folder: '',
name: 'Documents',
},
};
appTester(App.resources.folder.search.operation.perform, bundle)
.then((results) => {
results.length.should.above(0);
results[0].should.have.property('id');
results[0].should.have.property('folder');
results[0].should.not.have.property('file');
results[0].name.should.containEql('Documents');
results[0]._parent.should.eql('');
results[0]._path.should.containEql('/Documents');
done();
})
.catch(done);
});
it('should find a folder in a child dir', (done) => {
// An example scenario that could be worth testing if the HTTP request for
// a child folder is sufficiently different from a root folder
done();
});
});

View file

@ -0,0 +1,83 @@
'use strict';
require('should');
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
const globalBeforeSetup = (done) => {
zapier.tools.env.inject();
if (
!process.env.CLIENT_ID ||
!process.env.CLIENT_SECRET ||
!process.env.REFRESH_TOKEN
) {
throw new Error(
'Setup your .environment file (or use `export`) according to the README.',
);
}
// ACCESS_TOKEN expires very quickly, so we get a new one per test run from REFRESH_TOKEN
if (!process.env.ACCESS_TOKEN) {
const bundle = {
authData: {
access_token: '',
refresh_token: process.env.REFRESH_TOKEN,
accountType: 'personal',
},
};
appTester(App.authentication.oauth2Config.refreshAccessToken, bundle)
.then((result) => {
result.should.have.property('access_token');
result.should.have.property('refresh_token');
process.env.ACCESS_TOKEN = result.access_token;
done();
})
.catch(done);
} else {
done();
}
};
// Below is a set of files the app can use to test different scenarios.
// To make the test suite run, add files to your account in the given folders,
// then fill in the IDs of files from your account. The `id` is visible in the
// URL when accessing the file in the browser, for example:
// URL: https://onedrive.live.com/?cid=697LD7B578EB1372&id=697LD7B578EB1372%21716
// ID: 697LD7B578EB1372!716
const TEST_RESOURCES = {
root: {
id: '',
name: 'sample.pdf',
path: '/sample.pdf',
parent: '',
},
nested: {
id: '',
name: 'test.txt',
path: '/Documents/Testing/test.txt',
parent: '/Documents/Testing',
},
folder: {
id: '',
name: 'Documents',
path: '/Documents',
parent: '',
},
childFolder: {
id: '',
name: 'Testing',
path: '/Documents/Testing',
parent: '/Documents',
},
};
module.exports = {
globalBeforeSetup,
TEST_RESOURCES,
};

View file

@ -0,0 +1,68 @@
'use strict';
require('should');
const utils = require('../utils');
const extractParentsFromPath = utils.extractParentsFromPath;
const getStringByteSize = utils.getStringByteSize;
describe('Utils', () => {
it('should extract parents from a folder path', () => {
let parents = extractParentsFromPath('/Some/Kind of /Test');
parents.length.should.eql(3);
parents[0].id.should.eql(-1);
parents[0].name.should.eql('/');
parents[0]._path.should.eql('');
parents[0].should.have.property('folder');
parents[0].should.not.have.property('file');
parents[1].id.should.eql(-2);
parents[1].name.should.eql('Some');
parents[1]._path.should.eql('/Some');
parents[1].should.have.property('folder');
parents[1].should.not.have.property('file');
parents[2].id.should.eql(-3);
parents[2].name.should.eql('Kind of ');
parents[2]._path.should.eql('/Some/Kind of ');
parents[2].should.have.property('folder');
parents[2].should.not.have.property('file');
parents = extractParentsFromPath('/Zapier');
parents.length.should.eql(1);
parents[0].id.should.eql(-1);
parents[0].name.should.eql('/');
parents[0]._path.should.eql('');
parents[0].should.have.property('folder');
parents[0].should.not.have.property('file');
});
it('should correctly compute the byte size of strings', () => {
const tests = [
{
string: 'asd',
expected: 3,
},
{
string: '@€£¶‰dfg',
expected: 14,
},
{
string: '÷¶[]÷d()ß-°ú',
expected: 18,
},
{
string: 'ç€is-æ',
expected: 10,
},
];
tests.forEach((test) =>
getStringByteSize(test.string).should.eql(test.expected),
);
});
});

View file

@ -0,0 +1,138 @@
'use strict';
const _ = require('lodash');
const fetch = require('node-fetch');
const contentDisposition = require('content-disposition');
// CODE TIP: This function is used to process the response of several endpoints
// on the OneDrive API. We intentionally don't set it up as an `afterResposne`
// handler because not *all* calls need it (i.e. the auth test and file create),
// so we break it out and share the code this way instead.
const parseResponse = (z, type, response) => {
let results = [];
if (response.status >= 200 && response.status < 300) {
results = response.data;
// OneDrive puts the contents of lists inside .value property
if (!_.isArray(results) && _.isArray(results.value)) {
results = results.value;
}
} else {
throw new z.errors.Error(response.content, null, response.status);
}
// Only return files or folders, according to type
if (_.isArray(results)) {
results = results.filter((result) => result[type]);
} else {
if (!results[type]) {
results = {};
}
}
return results;
};
const handleError = (error) => {
if (typeof error === 'string') {
throw new Error(error);
}
throw error;
};
const extractParentsFromPath = (path) => {
const parts = path.split('/');
const results = [];
parts.splice(parts.length - 1); // Last is the current directory, so we remove it
let i = parts.length - 1;
while (parts.length > 0) {
const name = parts.splice(i)[0];
const result = {
id: (i + 1) * -1,
name: name === '' ? '/' : name,
_path: parts.join('/') + (name === '' ? name : `/${name}`),
folder: {},
};
results.push(result);
i -= 1;
}
results.reverse();
return results;
};
// UX TIP: Sometimes it can be helpful to translate raw values from an API into
// terms that end users are familiar with. In this example, OneDrive returns
// parent and the path of a file with a '/drive/root:' that end users of OneDrive
// never see. To help it read better in a dynamic dropdown, we clean that off
const cleanupPaths = (results) => {
// We can get a single object here as well
if (!_.isArray(results)) {
results._parent = _.get(results, 'parentReference.path', '').replace(
'/drive/root:',
'',
);
results._path = `${results._parent}/${results.name}`;
return results;
}
// Adds easier to reference paths, cleaning up the "/drive/root:" clutter
return results.map((result) => {
result._parent = _.get(result, 'parentReference.path', '').replace(
'/drive/root:',
'',
);
result._path = `${result._parent}/${result.name}`;
return result;
});
};
const getFileDetailsFromRequest = (url) =>
new Promise((resolve, reject) => {
const fileDetails = {
filename: '',
size: 0,
content: '',
contentType: '',
};
fetch(url)
.then((response) => {
fileDetails.size = response.headers.get('content-length');
fileDetails.contentType = response.headers.get('content-type');
const disposition = response.headers.get('content-disposition');
if (disposition) {
fileDetails.filename =
contentDisposition.parse(disposition).parameters.filename;
}
return response.buffer();
})
.then((content) => {
fileDetails.content = content;
return resolve(fileDetails);
})
.catch(reject);
});
const getStringByteSize = (string) => Buffer.byteLength(string, 'utf8');
module.exports = {
parseResponse,
handleError,
extractParentsFromPath,
cleanupPaths,
getFileDetailsFromRequest,
getStringByteSize,
};