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:
import { Asset } from '@vendure/core';
const config = {
// ...
customFields: {
Customer: [
{
name: 'avatar',
type: 'relation',
entity: Asset,
nullable: true,
},
],
},
}
In a later step, we will refactor this config to encapsulate it in a plugin.
Schema definition
Next, we will define the schema for the mutation:
// api-extensions.ts
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 to handle the processing of the uploaded file into an Asset.
// customer-avatar.resolver.ts
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
We can group all of this together into a plugin:
import { Asset, PluginCommonModule, VendurePlugin } from '@vendure/core';
import { shopApiExtensions } from './api-extensions';
import { CustomerAvatarResolver } from './customer-avatar.resolver';
@VendurePlugin({
imports: [PluginCommonModule],
shopApiExtensions: {
schema: shopApiExtensions,
resolvers: [CustomerAvatarResolver],
},
configuration: config => {
config.customFields.Customer.push({
name: 'avatar',
type: 'relation',
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} />;
}