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 => {
config.customFields.Customer.push({
name: 'avatar',
type: 'relation',
label: [{languageCode: LanguageCode.en, value: 'Customer avatar'}],
entity: Asset,
nullable: true,
});
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 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} />;
}