Просмотр исходного кода

docs: Add docs on uploading files

Michael Bromley 4 лет назад
Родитель
Сommit
199c3ee8c6

+ 217 - 0
docs/content/developer-guide/uploading-files.md

@@ -0,0 +1,217 @@
+---
+title: "Uploading Files"
+showtoc: true
+---
+
+# Uploading Files 
+
+Vendure handles file uploads with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). Internally, we use the [graphql-upload package](https://github.com/jaydenseric/graphql-upload). Once uploaded, a file is known as an [Asset]({{< relref "/docs/typescript-api/entities/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](https://github.com/jaydenseric/graphql-multipart-request-spec#client) which will allow you to upload files using the spec. If you are using Apollo Client, then you should install the [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) npm package.
+
+For testing, it is even possible to use a [plain curl request](https://github.com/jaydenseric/graphql-multipart-request-spec#single-file).
+
+## The `createAssets` mutation
+
+The [createAssets mutation]({{< relref "/docs/graphql-api/admin/mutations" >}}#createassets) 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:
+
+```TypeScript
+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: 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]({{< relref "customizing-models" >}}) 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:
+
+```TypeScript
+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:
+
+```TypeScript
+// 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]({{< relref "asset-service" >}}) to handle the processing of the uploaded file into an Asset.
+
+```TypeScript
+// 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:
+
+```TypeScript
+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:
+
+```TypeScript
+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} />;
+}
+```

+ 7 - 0
packages/dev-server/test-plugins/customer-avatar/api-extensions.ts

@@ -0,0 +1,7 @@
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    extend type Mutation {
+        setCustomerAvatar(file: Upload!): Asset
+    }
+`;

+ 22 - 0
packages/dev-server/test-plugins/customer-avatar/customer-avatar-plugin.ts

@@ -0,0 +1,22 @@
+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 {}

+ 55 - 0
packages/dev-server/test-plugins/customer-avatar/customer-avatar.resolver.ts

@@ -0,0 +1,55 @@
+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;
+    }
+}