Skip to content

Latest commit

 

History

History
220 lines (179 loc) · 6.52 KB

File metadata and controls

220 lines (179 loc) · 6.52 KB
title showtoc
Uploading Files
true

Uploading Files

Vendure handles file uploads with the GraphQL multipart request specification. Internally, we use the graphql-upload package. Once uploaded, a file is known as an Asset. Assets are typically used for images, but can represent any kind of binary data such as PDF files or videos.

Upload clients

Here is a list of client implementations that will allow you to upload files using the spec. If you are using Apollo Client, then you should install the apollo-upload-client npm package.

For testing, it is even possible to use a plain curl request.

The createAssets mutation

The createAssets mutation in the Admin API is the only means of uploading files by default.

Here's an example of how a file upload would look using the apollo-upload-client package:

import { gql, useMutation } from "@apollo/client";

const MUTATION = gql`
  mutation CreateAssets($input: [CreateAssetInput!]!) {
    createAssets(input: $input) {
      ... on Asset {
        id
        name
        fileSize
      }
      ... on ErrorResult {
        message
      }
    }
  }
`;

function UploadFile() {
    const [mutate] = useMutation(MUTATION);

    function onChange(event) {
        const {target} = event;
        if (target.validity.valid) {
            mutate({
                variables: {
                    input: Array.from(target.files).map((file) => ({file}));
                }
            });
        }
    }

    return <input type="file" required onChange={onChange}/>;
}

Custom upload mutations

How about if you want to implement a custom mutation for file uploads? Let's take an example where we want to allow customers to set an avatar image. To do this, we'll add a custom field to the Customer entity and then define a new mutation in the Shop API.

Configuration

Let's define a custom field to associate the avatar Asset with the Customer entity. To keep everything encapsulated, we'll do all of this in a plugin

import { Asset, LanguageCode, PluginCommonModule, VendurePlugin } from '@vendure/core';

@VendurePlugin({
    imports: [PluginCommonModule],
    configure: config => {
        // highlight-start
        config.customFields.Customer.push({
            name: 'avatar',
            type: 'relation',
            label: [{languageCode: LanguageCode.en, value: 'Customer avatar'}],
            entity: Asset,
            nullable: true,
        });
        // highlight-end
        return config;
    },
})
export class CustomerAvatarPlugin {}

Schema definition

Next, we will define the schema for the mutation:

import gql from 'graphql-tag';

export const shopApiExtensions = gql`
extend type Mutation {
  setCustomerAvatar(file: Upload!): Asset
}`

Resolver

The resolver will make use of the built-in [AssetService]({{< relref "asset-service" >}}) to handle the processing of the uploaded file into an Asset.

import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Asset } from '@vendure/common/lib/generated-types';
import {
    Allow, AssetService, Ctx, CustomerService, isGraphQlErrorResult,
    Permission, RequestContext, Transaction
} from '@vendure/core';

@Resolver()
export class CustomerAvatarResolver {
    constructor(private assetService: AssetService, private customerService: CustomerService) {}

    @Transaction()
    @Mutation()
    @Allow(Permission.Authenticated)
    async setCustomerAvatar(
        @Ctx() ctx: RequestContext,
        @Args() args: { file: any },
    ): Promise<Asset | undefined> {
        const userId = ctx.activeUserId;
        if (!userId) {
            return;
        }
        const customer = await this.customerService.findOneByUserId(ctx, userId);
        if (!customer) {
            return;
        }
        // Create an Asset from the uploaded file
        const asset = await this.assetService.create(ctx, {
            file: args.file,
            tags: ['avatar'],
        });
        // Check to make sure there was no error when
        // creating the Asset
        if (isGraphQlErrorResult(asset)) {
            // MimeTypeError
            throw asset;
        }
        // Asset created correctly, so assign it as the
        // avatar of the current Customer
        await this.customerService.update(ctx, {
            id: customer.id,
            customFields: {
                avatarId: asset.id,
            },
        });

        return asset;
    }
}

Complete Customer Avatar Plugin

Let's put all these parts together into the plugin:

import { Asset, PluginCommonModule, VendurePlugin } from '@vendure/core';

import { shopApiExtensions } from './api/api-extensions';
import { CustomerAvatarResolver } from './api/customer-avatar.resolver';

@VendurePlugin({
    imports: [PluginCommonModule],
    shopApiExtensions: {
        schema: shopApiExtensions,
        resolvers: [CustomerAvatarResolver],
    },
    configuration: config => {
        config.customFields.Customer.push({
            name: 'avatar',
            type: 'relation',
            label: [{languageCode: LanguageCode.en, value: 'Customer avatar'}],
            entity: Asset,
            nullable: true,
        });
        return config;
    },
})
export class CustomerAvatarPlugin {
}

Uploading a Customer Avatar

In our storefront, we would then upload a Customer's avatar like this:

import { gql, useMutation } from "@apollo/client";

const MUTATION = gql`
  mutation SetCustomerAvatar($file: Upload!) {
    setCustomerAvatar(file: $file) {
      id
      name
      fileSize
    }
  }
`;

function UploadAvatar() {
  const [mutate] = useMutation(MUTATION);

  function onChange(event) {
    const { target } = event;  
    if (target.validity.valid && target.files.length === 1) {
      mutate({ 
        variables: {
          file: target.files[0],
        }  
      });
    }
  }

  return <input type="file" required onChange={onChange} />;
}