Răsfoiți Sursa

test(server): Add Assets to populate script & e2e test config

Michael Bromley 7 ani în urmă
părinte
comite
55872a5d56

+ 2 - 2
admin-ui/src/app/data/definitions/product-definitions.ts

@@ -239,8 +239,8 @@ export const GET_ASSET_LIST = gql`
     ${ASSET_FRAGMENT}
 `;
 
-export const CREATE_ASSET = gql`
-    mutation CreateAsset($input: [CreateAssetInput!]!) {
+export const CREATE_ASSETS = gql`
+    mutation CreateAssets($input: [CreateAssetInput!]!) {
         createAssets(input: $input) {
             ...Asset
         }

+ 5 - 6
admin-ui/src/app/data/providers/product-data.service.ts

@@ -4,9 +4,8 @@ import {
     AddOptionGroupToProductVariables,
     ApplyFacetValuesToProductVariants,
     ApplyFacetValuesToProductVariantsVariables,
-    CreateAsset,
-    CreateAssetInput,
-    CreateAssetVariables,
+    CreateAssets,
+    CreateAssetsVariables,
     CreateProduct,
     CreateProductInput,
     CreateProductOptionGroup,
@@ -39,7 +38,7 @@ import { addCustomFields } from '../add-custom-fields';
 import {
     ADD_OPTION_GROUP_TO_PRODUCT,
     APPLY_FACET_VALUE_TO_PRODUCT_VARIANTS,
-    CREATE_ASSET,
+    CREATE_ASSETS,
     CREATE_PRODUCT,
     CREATE_PRODUCT_OPTION_GROUP,
     GENERATE_PRODUCT_VARIANTS,
@@ -183,8 +182,8 @@ export class ProductDataService {
         });
     }
 
-    createAssets(files: File[]): Observable<CreateAsset> {
-        return this.baseDataService.mutate<CreateAsset, CreateAssetVariables>(CREATE_ASSET, {
+    createAssets(files: File[]): Observable<CreateAssets> {
+        return this.baseDataService.mutate<CreateAssets, CreateAssetsVariables>(CREATE_ASSETS, {
             input: files.map(file => ({ file })),
         });
     }

+ 5 - 0
server/e2e/config/test-config.ts

@@ -2,6 +2,9 @@ import { API_PATH } from 'shared/shared-constants';
 
 import { VendureConfig } from '../../src/config/vendure-config';
 
+import { TestingAssetPreviewStrategy } from './testing-asset-preview-strategy';
+import { TestingAssetStorageStrategy } from './testing-asset-storage-strategy';
+
 export const TEST_CONNECTION_NAME = undefined;
 
 /**
@@ -20,4 +23,6 @@ export const testConfig: VendureConfig = {
         logging: false,
     },
     customFields: {},
+    assetStorageStrategy: new TestingAssetStorageStrategy(),
+    assetPreviewStrategy: new TestingAssetPreviewStrategy(),
 };

+ 10 - 0
server/e2e/config/testing-asset-preview-strategy.ts

@@ -0,0 +1,10 @@
+import { AssetPreviewStrategy } from '../../src/config/asset-preview-strategy/asset-preview-strategy';
+
+/**
+ * A mock preview strategy which returns a new Buffer without doing any actual work.
+ */
+export class TestingAssetPreviewStrategy implements AssetPreviewStrategy {
+    generatePreviewImage(mimeType: string, data: Buffer): Promise<Buffer> {
+        return Promise.resolve(Buffer.from('test'));
+    }
+}

+ 40 - 0
server/e2e/config/testing-asset-storage-strategy.ts

@@ -0,0 +1,40 @@
+import { Request } from 'express';
+import { Readable, Stream, Writable } from 'stream';
+
+import { AssetStorageStrategy } from '../../src/config/asset-storage-strategy/asset-storage-strategy';
+
+/**
+ * A mock storage strategy which does not actually persist the assets anywhere.
+ */
+export class TestingAssetStorageStrategy implements AssetStorageStrategy {
+    readFileToBuffer(identifier: string): Promise<Buffer> {
+        return Promise.resolve(Buffer.from('test'));
+    }
+
+    readFileToStream(identifier: string): Promise<Stream> {
+        const s = new Readable();
+        s.push(identifier);
+        s.push(null);
+        return Promise.resolve(s);
+    }
+
+    toAbsoluteUrl(reqest: Request, identifier: string): string {
+        return `test-url/${identifier}`;
+    }
+
+    writeFileFromBuffer(fileName: string, data: Buffer): Promise<string> {
+        return Promise.resolve(`test-assets/${fileName}`);
+    }
+
+    writeFileFromStream(fileName: string, data: Stream): Promise<string> {
+        const writable = new Writable();
+        writable._write = (chunk, encoding, done) => {
+            done();
+        };
+        return new Promise<string>((resolve, reject) => {
+            data.pipe(writable);
+            writable.on('finish', () => resolve(`test-assets/${fileName}`));
+            writable.on('error', reject);
+        });
+    }
+}

BIN
server/mock-data/assets/charles-deluvio-695736-unsplash.jpg


BIN
server/mock-data/assets/chuttersnap-584518-unsplash.jpg


BIN
server/mock-data/assets/derick-david-409858-unsplash.jpg


BIN
server/mock-data/assets/eniko-kis-663725-unsplash.jpg


BIN
server/mock-data/assets/mikkel-bech-748940-unsplash.jpg


+ 12 - 0
server/mock-data/mock-data.service.ts

@@ -1,8 +1,11 @@
 import * as faker from 'faker/locale/en_GB';
+import * as fs from 'fs-extra';
 import gql from 'graphql-tag';
+import * as path from 'path';
 import {
     AddOptionGroupToProduct,
     AddOptionGroupToProductVariables,
+    Asset,
     CreateFacet,
     CreateFacetValueWithFacetInput,
     CreateFacetVariables,
@@ -157,6 +160,15 @@ export class MockDataService {
         }
     }
 
+    async populateAssets(): Promise<Asset[]> {
+        const fileNames = await fs.readdir(path.join(__dirname, 'assets'));
+        const filePaths = fileNames.map(fileName => path.join(__dirname, 'assets', fileName));
+        return this.client.uploadAssets(filePaths).then(response => {
+            console.log(`Created ${response.createAssets.length} Assets`);
+            return response.createAssets;
+        });
+    }
+
     async populateProducts(count: number = 5, optionGroupId: string): Promise<any> {
         for (let i = 0; i < count; i++) {
             const query = CREATE_PRODUCT;

+ 7 - 1
server/mock-data/populate-cli.ts

@@ -21,5 +21,11 @@ if (require.main === module) {
         channels: ['mobile-app'],
     })
         .then(app => app.close())
-        .then(() => process.exit(0));
+        .then(
+            () => process.exit(0),
+            err => {
+                console.log(err);
+                process.exit(1);
+            },
+        );
 }

+ 1 - 0
server/mock-data/populate.ts

@@ -39,6 +39,7 @@ export async function populate(
     if (options.channels) {
         channels = await mockDataService.populateChannels(options.channels);
     }
+    const assets = await mockDataService.populateAssets();
     const optionGroupId = await mockDataService.populateOptions();
     await mockDataService.populateProducts(options.productCount, optionGroupId);
     await mockDataService.populateCustomers(options.customerCount);

+ 64 - 17
server/mock-data/simple-graphql-client.ts

@@ -1,11 +1,12 @@
 import { DocumentNode } from 'graphql';
 import { GraphQLClient } from 'graphql-request';
-import { GraphQLError } from 'graphql-request/dist/src/types';
 import { print } from 'graphql/language/printer';
-import { AttemptLogin, AttemptLoginVariables } from 'shared/generated-types';
+import { Curl } from 'node-libcurl';
+import { AttemptLogin, AttemptLoginVariables, CreateAssets } from 'shared/generated-types';
 import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from 'shared/shared-constants';
 
 import { ATTEMPT_LOGIN } from '../../admin-ui/src/app/data/definitions/auth-definitions';
+import { CREATE_ASSETS } from '../../admin-ui/src/app/data/definitions/product-definitions';
 import { getConfig } from '../src/config/vendure-config';
 
 // tslint:disable:no-console
@@ -17,7 +18,7 @@ export class SimpleGraphQLClient {
     private authToken: string;
     private channelToken: string;
 
-    constructor(apiUrl: string = '') {
+    constructor(private apiUrl: string = '') {
         this.client = new GraphQLClient(apiUrl);
     }
 
@@ -31,31 +32,77 @@ export class SimpleGraphQLClient {
         this.setHeaders();
     }
 
+    /**
+     * Performs both query and mutation operations.
+     */
     query<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<T> {
         const queryString = print(query);
         return this.client.request(queryString, variables);
     }
 
-    queryRaw<T = any, V = Record<string, any>>(
-        query: DocumentNode,
-        variables?: V,
-    ): Promise<{
-        data?: T;
-        extensions?: any;
-        headers: Record<string, string>;
-        status: number;
-        errors?: GraphQLError[];
-    }> {
-        const queryString = print(query);
-        return this.client.rawRequest<T>(queryString, variables);
-    }
-
     async queryStatus<T = any, V = Record<string, any>>(query: DocumentNode, variables?: V): Promise<number> {
         const queryString = print(query);
         const result = await this.client.rawRequest<T>(queryString, variables);
         return result.status;
     }
 
+    /**
+     * Uses curl to post a multipart/form-data request to create new assets. Due to differences between the Node and browser
+     * environments, we cannot just use an existing library like apollo-upload-client.
+     *
+     * Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
+     * Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
+     */
+    uploadAssets(filePaths: string[]): Promise<CreateAssets> {
+        return new Promise((resolve, reject) => {
+            const curl = new Curl();
+
+            const postData: any[] = [
+                {
+                    name: 'operations',
+                    contents: JSON.stringify({
+                        operationName: 'CreateAssets',
+                        variables: {
+                            input: filePaths.map(() => ({ file: null })),
+                        },
+                        query: print(CREATE_ASSETS),
+                    }),
+                },
+                {
+                    name: 'map',
+                    contents:
+                        '{' +
+                        filePaths.map((filePath, i) => `"${i}":["variables.input.${i}.file"]`).join(',') +
+                        '}',
+                },
+                ...filePaths.map((filePath, i) => ({
+                    name: i.toString(),
+                    file: filePath,
+                })),
+            ];
+            curl.setOpt(Curl.option.URL, this.apiUrl);
+            curl.setOpt(Curl.option.VERBOSE, true);
+            curl.setOpt(Curl.option.TIMEOUT_MS, 30000);
+            curl.setOpt(Curl.option.HTTPPOST, postData);
+            curl.setOpt(Curl.option.HTTPHEADER, [
+                `Authorization: Bearer ${this.authToken}`,
+                `${getConfig().channelTokenKey}: ${this.channelToken}`,
+            ]);
+            curl.perform();
+            curl.on('end', (statusCode, body) => {
+                curl.close();
+                console.log(JSON.parse(body));
+                resolve(JSON.parse(body).data);
+            });
+
+            curl.on('error', err => {
+                curl.close();
+                console.log(err);
+                reject(err);
+            });
+        });
+    }
+
     async asUserWithCredentials(username: string, password: string) {
         const result = await this.query<AttemptLogin, AttemptLoginVariables>(ATTEMPT_LOGIN, {
             username,

+ 1 - 0
server/package.json

@@ -66,6 +66,7 @@
     "graphql-request": "^1.8.2",
     "gulp": "^4.0.0",
     "jest": "^23.5.0",
+    "node-libcurl": "^1.3.3",
     "nodemon": "^1.14.1",
     "rimraf": "^2.6.2",
     "sql.js": "^0.5.0",

+ 2 - 2
server/src/api/asset/asset.resolver.ts

@@ -1,5 +1,5 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { CreateAssetVariables, Permission } from 'shared/generated-types';
+import { CreateAssetsVariables, Permission } from 'shared/generated-types';
 import { PaginatedList } from 'shared/shared-types';
 
 import { Asset } from '../../entity/asset/asset.entity';
@@ -34,7 +34,7 @@ export class AssetResolver {
      */
     @Mutation()
     @Allow(Permission.CreateCatalog)
-    async createAssets(@Args() args: CreateAssetVariables): Promise<Asset[]> {
+    async createAssets(@Args() args: CreateAssetsVariables): Promise<Asset[]> {
         return Promise.all(args.input.map(asset => this.assetService.create(asset)));
     }
 }

+ 156 - 12
server/yarn.lock

@@ -311,7 +311,7 @@ acorn@^5.0.0, acorn@^5.3.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
 
-ajv@^5.1.0:
+ajv@^5.1.0, ajv@^5.3.0:
   version "5.5.2"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
   dependencies:
@@ -700,6 +700,10 @@ aws4@^1.6.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
 
+aws4@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+
 axios@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
@@ -918,6 +922,12 @@ bl@^1.0.0:
     readable-stream "^2.3.5"
     safe-buffer "^5.1.1"
 
+block-stream@*:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+  dependencies:
+    inherits "~2.0.0"
+
 body-parser@1.18.2:
   version "1.18.2"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
@@ -1316,6 +1326,12 @@ combined-stream@1.0.6, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
+combined-stream@~1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+  dependencies:
+    delayed-stream "~1.0.0"
+
 compare-versions@^3.1.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.3.0.tgz#af93ea705a96943f622ab309578b9b90586f39c3"
@@ -1475,7 +1491,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.
   dependencies:
     ms "2.0.0"
 
-debug@=3.1.0, debug@^3.1.0:
+debug@3.1.0, debug@=3.1.0, debug@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
   dependencies:
@@ -1920,6 +1936,10 @@ extend@^3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+
 extglob@^0.3.1:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@@ -2119,7 +2139,7 @@ forever-agent@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
 
-form-data@~2.3.1:
+form-data@~2.3.1, form-data@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
   dependencies:
@@ -2153,6 +2173,14 @@ fs-copy-file-sync@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918"
 
+fs-extra@5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
 fs-extra@6.0.1, fs-extra@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b"
@@ -2193,6 +2221,15 @@ fsevents@^1.2.2, fsevents@^1.2.3:
     nan "^2.9.2"
     node-pre-gyp "^0.10.0"
 
+fstream@^1.0.0, fstream@^1.0.2:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2466,6 +2503,13 @@ har-validator@~5.0.3:
     ajv "^5.1.0"
     har-schema "^2.0.0"
 
+har-validator@~5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
+  dependencies:
+    ajv "^5.3.0"
+    har-schema "^2.0.0"
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -2671,7 +2715,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
@@ -3814,12 +3858,22 @@ mime-db@~1.33.0:
   version "1.33.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
 
+mime-db@~1.36.0:
+  version "1.36.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
+
 mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18:
   version "2.1.18"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
   dependencies:
     mime-db "~1.33.0"
 
+mime-types@~2.1.19:
+  version "2.1.20"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
+  dependencies:
+    mime-db "~1.36.0"
+
 mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@@ -3874,7 +3928,7 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@^0.5.0, mkdirp@^0.5.1:
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -3989,10 +4043,38 @@ node-fetch@^2.1.2, node-fetch@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5"
 
+node-gyp@3.8.0:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+  dependencies:
+    fstream "^1.0.0"
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3 || 4"
+    osenv "0"
+    request "^2.87.0"
+    rimraf "2"
+    semver "~5.3.0"
+    tar "^2.0.0"
+    which "1"
+
 node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
 
+node-libcurl@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/node-libcurl/-/node-libcurl-1.3.3.tgz#0a2545813e4cfe288313335f94e8559700109eec"
+  dependencies:
+    debug "3.1.0"
+    fs-extra "5.0.0"
+    nan "2.10.0"
+    node-gyp "3.8.0"
+    node-pre-gyp "0.11.0"
+    npmlog "4.1.2"
+
 node-notifier@^5.2.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
@@ -4017,6 +4099,21 @@ node-pre-gyp@0.10.2:
     semver "^5.3.0"
     tar "^4"
 
+node-pre-gyp@0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
+  dependencies:
+    detect-libc "^1.0.2"
+    mkdirp "^0.5.1"
+    needle "^2.2.1"
+    nopt "^4.0.1"
+    npm-packlist "^1.1.6"
+    npmlog "^4.0.2"
+    rc "^1.2.7"
+    rimraf "^2.6.1"
+    semver "^5.3.0"
+    tar "^4"
+
 node-pre-gyp@^0.10.0:
   version "0.10.3"
   resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
@@ -4051,6 +4148,12 @@ noop-logger@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
 
+"nopt@2 || 3":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+  dependencies:
+    abbrev "1"
+
 nopt@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -4102,7 +4205,7 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
-npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@4.1.2, npmlog@^4.0.1, npmlog@^4.0.2, npmlog@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   dependencies:
@@ -4123,6 +4226,10 @@ oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+
 object-assign@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
@@ -4271,7 +4378,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@^0.1.4:
+osenv@0, osenv@^0.1.4:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
   dependencies:
@@ -4623,7 +4730,7 @@ qs@6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
-qs@6.5.2, qs@~6.5.1:
+qs@6.5.2, qs@~6.5.1, qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 
@@ -4845,6 +4952,31 @@ request@^2.83.0:
     tunnel-agent "^0.6.0"
     uuid "^3.1.0"
 
+request@^2.87.0:
+  version "2.88.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.0"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.4.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -4908,7 +5040,7 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -4981,6 +5113,10 @@ semver@^5.5.1:
   version "5.5.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
 
+semver@~5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
 send@0.16.2:
   version "0.16.2"
   resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
@@ -5409,6 +5545,14 @@ tar-stream@^1.1.2:
     to-buffer "^1.1.0"
     xtend "^4.0.0"
 
+tar@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+  dependencies:
+    block-stream "*"
+    fstream "^1.0.2"
+    inherits "2"
+
 tar@^4:
   version "4.4.4"
   resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd"
@@ -5551,7 +5695,7 @@ touch@^3.1.0:
   dependencies:
     nopt "~1.0.10"
 
-tough-cookie@>=2.3.3, tough-cookie@^2.3.3:
+tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.4.3:
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
   dependencies:
@@ -5797,7 +5941,7 @@ utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
-uuid@3.3.2, uuid@^3.1.0:
+uuid@3.3.2, uuid@^3.1.0, uuid@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
 
@@ -5932,7 +6076,7 @@ which-pm-runs@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
 
-which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0:
+which@1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   dependencies:

+ 5 - 5
shared/generated-types.ts

@@ -1540,10 +1540,10 @@ export interface GetAssetListVariables {
 // This file was automatically generated and should not be edited.
 
 // ====================================================
-// GraphQL mutation operation: CreateAsset
+// GraphQL mutation operation: CreateAssets
 // ====================================================
 
-export interface CreateAsset_createAssets {
+export interface CreateAssets_createAssets {
   __typename: "Asset";
   id: string;
   name: string;
@@ -1554,14 +1554,14 @@ export interface CreateAsset_createAssets {
   source: string;
 }
 
-export interface CreateAsset {
+export interface CreateAssets {
   /**
    * Create a new Asset
    */
-  createAssets: CreateAsset_createAssets[];
+  createAssets: CreateAssets_createAssets[];
 }
 
-export interface CreateAssetVariables {
+export interface CreateAssetsVariables {
   input: CreateAssetInput[];
 }
 

+ 12 - 1
shared/omit.ts

@@ -8,7 +8,7 @@ declare const File: any;
 export function omit<T extends object, K extends keyof T>(obj: T, keysToOmit: K[]): Omit<T, K>;
 export function omit<T extends object | any[], K extends keyof T>(obj: T, keysToOmit: string[], recursive: boolean): T;
 export function omit<T extends any, K extends keyof T>(obj: T, keysToOmit: string[], recursive: boolean = false): T {
-    if ((recursive && !isObject(obj)) || obj instanceof File) {
+    if ((recursive && !isObject(obj)) || isFileObject(obj)) {
         return obj;
     }
 
@@ -30,3 +30,14 @@ export function omit<T extends any, K extends keyof T>(obj: T, keysToOmit: strin
 function isObject(input: any): input is object {
     return typeof input === 'object' && input !== null;
 }
+
+/**
+ * When running in the Node environment, there is no native File object.
+ */
+function isFileObject(input): boolean {
+    if (typeof File === 'undefined') {
+        return false;
+    } else {
+        return input instanceof File;
+    }
+}