diff --git a/index.d.ts b/index.d.ts index 111b234..3c5136e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,7 +5,6 @@ declare module "moleculer-apollo-server" { import { SchemaDirectiveVisitor, IResolvers } from "graphql-tools"; export { - GraphQLUpload, GraphQLExtension, gql, ApolloError, @@ -18,6 +17,8 @@ declare module "moleculer-apollo-server" { defaultPlaygroundOptions, } from "apollo-server-core"; + export { GraphQLUpload } from 'graphql-upload'; + export * from "graphql-tools"; export interface ApolloServerOptions { diff --git a/index.js b/index.js index b5e2c23..315390a 100644 --- a/index.js +++ b/index.js @@ -15,13 +15,14 @@ "use strict"; const core = require("apollo-server-core"); +const { GraphQLUpload } = require("graphql-upload"); const { ApolloServer } = require("./src/ApolloServer"); const ApolloService = require("./src/service"); const gql = require("./src/gql"); module.exports = { // Core - GraphQLUpload: core.GraphQLUpload, + GraphQLUpload: GraphQLUpload, GraphQLExtension: core.GraphQLExtension, gql: core.gql, ApolloError: core.ApolloError, diff --git a/package.json b/package.json index 1c339b6..e0fb114 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "dependencies": { "@apollographql/graphql-playground-html": "^1.6.24", "@hapi/accept": "^3.2.4", + "@types/graphql-upload": "^8.0.0", "apollo-server-core": "^2.10.0", "dataloader": "^2.0.0", "graphql-subscriptions": "^1.1.0", diff --git a/src/service.js b/src/service.js index b69952a..32b07d5 100644 --- a/src/service.js +++ b/src/service.js @@ -133,6 +133,7 @@ module.exports = function(mixinOptions) { nullIfError = false, params: staticParams = {}, rootParams = {}, + fileUploadArg = null, } = def; const rootKeys = Object.keys(rootParams); @@ -189,6 +190,27 @@ module.exports = function(mixinOptions) { return Array.isArray(dataLoaderKey) ? await dataLoader.loadMany(dataLoaderKey) : await dataLoader.load(dataLoaderKey); + } else if (fileUploadArg != null && args[fileUploadArg] != null) { + if (Array.isArray(args[fileUploadArg])) { + return await Promise.all( + args[fileUploadArg].map(async uploadPromise => { + const { + createReadStream, + ...$fileInfo + } = await uploadPromise; + const stream = createReadStream(); + return context.ctx.call(actionName, stream, { + meta: { $fileInfo }, + }); + }) + ); + } + + const { createReadStream, ...$fileInfo } = await args[fileUploadArg]; + const stream = createReadStream(); + return await context.ctx.call(actionName, stream, { + meta: { $fileInfo }, + }); } else { const params = {}; if (root && rootKeys) { @@ -411,7 +433,10 @@ module.exports = function(mixinOptions) { const name = this.getFieldName(mutation); mutations.push(mutation); resolver.Mutation[name] = this.createActionResolver( - action.name + action.name, + { + fileUploadArg: def.fileUploadArg, + } ); }); } diff --git a/test/unit/service.spec.js b/test/unit/service.spec.js index ccf4399..e915129 100644 --- a/test/unit/service.spec.js +++ b/test/unit/service.spec.js @@ -436,7 +436,7 @@ describe("Test Service", () => { }); }); - describe("Test 'createActionResolver' without DataLoader", () => { + describe("Test 'createActionResolver' without DataLoader or Upload", () => { let broker, svc, stop; beforeAll(async () => { @@ -534,6 +534,105 @@ describe("Test Service", () => { }); }); + describe("Test 'createActionResolver' with File Upload", () => { + let broker, svc, stop; + + beforeAll(async () => { + const res = await startService(); + broker = res.broker; + svc = res.svc; + stop = res.stop; + }); + + afterAll(async () => await stop()); + + it("should create a stream and pass to call", async () => { + const resolver = svc.createActionResolver("posts.uploadSingle", { + fileUploadArg: "file", + }); + + const ctx = new Context(broker); + ctx.call = jest.fn(() => "response from action"); + + const fakeRoot = {}; + + const file = { + filename: "filename.txt", + encoding: "7bit", + mimetype: "text/plain", + createReadStream: () => "fake read stream", + }; + + const res = await resolver(fakeRoot, { file }, { ctx }); + + expect(res).toBe("response from action"); + + expect(ctx.call).toBeCalledTimes(1); + expect(ctx.call).toBeCalledWith("posts.uploadSingle", "fake read stream", { + meta: { + $fileInfo: { + filename: "filename.txt", + encoding: "7bit", + mimetype: "text/plain", + }, + }, + }); + }); + + it("should invoke call once per file when handling an array of file uploads", async () => { + const resolver = svc.createActionResolver("posts.uploadMulti", { + fileUploadArg: "files", + }); + + const ctx = new Context(broker); + ctx.call = jest.fn((_, stream) => `response for ${stream}`); + + const fakeRoot = {}; + + const files = [ + { + filename: "filename1.txt", + encoding: "7bit", + mimetype: "text/plain", + createReadStream: () => "fake read stream 1", + }, + { + filename: "filename2.txt", + encoding: "7bit", + mimetype: "text/plain", + createReadStream: () => "fake read stream 2", + }, + ]; + + const res = await resolver(fakeRoot, { files }, { ctx }); + + expect(res).toEqual([ + "response for fake read stream 1", + "response for fake read stream 2", + ]); + + expect(ctx.call).toBeCalledTimes(2); + expect(ctx.call).toBeCalledWith("posts.uploadMulti", "fake read stream 1", { + meta: { + $fileInfo: { + filename: "filename1.txt", + encoding: "7bit", + mimetype: "text/plain", + }, + }, + }); + expect(ctx.call).toBeCalledWith("posts.uploadMulti", "fake read stream 2", { + meta: { + $fileInfo: { + filename: "filename2.txt", + encoding: "7bit", + mimetype: "text/plain", + }, + }, + }); + }); + }); + describe("Test 'createActionResolver' with DataLoader", () => { let broker, svc, stop;