jaydenseric / graphql-upload

Middleware and an Upload scalar to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
https://npm.im/graphql-upload
MIT License
1.43k stars 132 forks source link

Any example of graphql-upload, Nest.js + codegen? #358

Closed Kasheftin closed 1 year ago

Kasheftin commented 1 year ago

Sorry, that's so freaking frustrating to use. This all starts with that you need file upload in your nest.js graphql app. You google "nestjs graphql file upload", and all the links are about graphql-upload. The first one, https://stephen-knutter.github.io/2020-02-07-nestjs-graphql-file-upload/, shows just few lines of code, so simple, and you are so happy! But then, it's from Feb 2020, meaning it's for ~v10. And there're no types. Then, https://www.elbarryamine.com/blog/how-to-upload-files-with-nest-js-and-graphql says v15 has some issues and you have to use v14. But the @types/graphql-upload is available for v15 or v8 only. Is v8 the way to go? Not anymore since it's not compatible with graphql@16. Maybe you should just try the lastest version, graphql-upload@16? No, it's ESM module, and that is something that nest.js currently does not support.

Is there any combination of graphql-upload and @types/graphql-upload working with nest.js?

jaydenseric commented 1 year ago

The reason you are frustrated is ultimately because Nest.js doesn't support modern ESM packages like this one; accordingly your questions are better answered by the Nest.js team/community. I personally don't consider Nest.js viable to use for reasons like this and I'm not keen to do their legwork to figure out workarounds which would only encourage its use. It's not wise to surrender your tooling to a framework like that to make a Node.js API.

If anyone is able to answer some of the specific questions raised by this issue, feel free to add comments.

tugascript commented 1 year ago

Nest does support dynamic imports, but you need to enable experimental features, you can use version 15 or downgrade to version 13 with types for version 8 (in the end the types are the same from version 8 till 13), use version 14 with version 15 types.

https://dev.to/tugascript/nestjs-graphql-image-upload-to-a-s3-bucket-1njg

In this article I explain how to use version 13 and 16 for Apollo, though I personally use Mercurius with Mercurius-upload and graphql-upload version 15 and everything seems to work fine.

uncleDimasik commented 1 year ago

Here is my setup package.json

  "dependencies": {
    "@apollo/server": "^4.5.0",
    "@apollo/subgraph": "^2.4.1",
    "@gideo-llc/backblaze-b2-upload-any": "^0.1.4",
    "@nestjs/apollo": "^11.0.4",
    "@nestjs/common": "^9.0.0",
    "@nestjs/config": "^2.3.1",
    "@nestjs/core": "^9.0.0",
    "@nestjs/graphql": "^11.0.5",
    "@nestjs/jwt": "^10.0.3",
    "@nestjs/mapped-types": "*",
    "@nestjs/passport": "^9.0.3",
    "@nestjs/platform-express": "^9.0.0",
    "@prisma/client": "^4.13.0",
    "backblaze-b2": "^1.7.0",
    "bcrypt": "^5.1.0",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0",
    "cookie": "^0.5.0",
    "cookie-parser": "^1.4.6",
    "cross-env": "^7.0.3",
    "graphql": "^16.6.0",
    "graphql-tools": "^8.3.20",
    "graphql-upload": "14.0.0",  //!!!IMPORTANT VERSION 14
    "helmet": "^6.0.1",
    "passport": "^0.6.0",
    "passport-jwt": "^4.0.1",
    "passport-local": "^1.0.0",
    "prisma-graphql-type-decimal": "^3.0.0",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.2.0"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/backblaze-b2": "^1.5.2",
    "@types/bcrypt": "^5.0.0",
    "@types/cookie": "^0.5.1",
    "@types/cookie-parser": "^1.4.3",
    "@types/express": "^4.17.13",
    "@types/graphql-upload": "8.0.12", //!!!IMPORTANT 
    "@types/jest": "29.2.4",
    "@types/node": "18.11.18",
    "@types/passport-jwt": "^3.0.8",
    "@types/passport-local": "^1.0.35",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "dotenv-cli": "^7.2.1",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "29.3.1",
    "prettier": "^2.3.2",
    "prisma": "^4.13.0",
    "prisma-nestjs-graphql": "^17.1.0",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "29.0.3",
    "ts-loader": "^9.2.3",
    "ts-morph": "^17.0.1",
    "ts-node": "^10.0.0",
    "tsconfig-paths": "4.1.1",
    "typescript": "^4.7.4"
  },

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as process from 'process';
import helmet from 'helmet';
import * as cookieParser from 'cookie-parser';
import { ValidationPipe } from '@nestjs/common';
import * as graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const isDevelopment = process.env.NODE_ENV === 'development';
  app.use(
    '/graphql',
    graphqlUploadExpress({ maxFileSize: 50000000, maxFiles: 10 }),
  );
  app.useGlobalPipes(
    new ValidationPipe({
      enableDebugMessages: isDevelopment,
      // whitelist: true,
      skipUndefinedProperties: true,
    }),
  );
  await app.use(cookieParser(process.env.COOKIE_SECRET));
  app.enableCors({
    origin: true,
    credentials: true,
  });

  app.use(
    helmet({
      crossOriginEmbedderPolicy: !isDevelopment,
      contentSecurityPolicy: !isDevelopment,
    }),
  );
  await app.listen(process.env.PORT, () =>
    console.log(`Server started on port = ${process.env.PORT}`),
  );
}

bootstrap();

app.module.ts

import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PrismaModule } from './prisma/prisma.module';
import {
  ApolloServerPluginLandingPageLocalDefault,
  ApolloServerPluginLandingPageProductionDefault,
} from '@apollo/server/plugin/landingPage/default';
import { UserModule } from './user/user.module';
import { join } from 'path';
import { ConfigModule } from '@nestjs/config';
import { AuthenticationModule } from './authentication/authentication.module';
import { DishModule } from './dish/dish.module';
import { CategoryModule } from './category/category.module';
import { GoodModule } from './good/good.module';
import { RestaurantModule } from './restaurant/restaurant.module';
import { OrderModule } from './order/order.module';
import { B2Module } from './b2/b2.module';
import * as process from 'process';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.${process.env.NODE_ENV}.env`,
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      playground: false,
      context: ({ req, res }) => ({ req, res }),
      plugins: [
        // Install a landing page plugin based on NODE_ENV
        process.env.NODE_ENV === 'production'
          ? ApolloServerPluginLandingPageProductionDefault({
              graphRef: 'my-graph-id@my-graph-variant',
              footer: false,
            })
          : ApolloServerPluginLandingPageLocalDefault({
              footer: true,
              includeCookies: true,
            }),
      ],
      autoSchemaFile: join(
        process.cwd(),
        'src/@generated/schema.gql',
      ),
    }),
    PrismaModule,
    UserModule,
    AuthenticationModule,
    DishModule,
    CategoryModule,
    GoodModule,
    RestaurantModule,
    OrderModule,
    B2Module,
  ],
})
export class AppModule {}

file.entity.ts

import { Stream } from 'stream';

export interface FileUpload {
  filename: string;
  mimetype: string;
  encoding: string;
  createReadStream: () => Stream;
}

i used b2 bucket b2.service.ts

import { Injectable } from '@nestjs/common';
import { FileUpload } from './entities/file.entity'; // cuz "module": "commonjs"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as B2 from 'backblaze-b2'; // cuz "module": "commonjs"
import * as uploadAny from '@gideo-llc/backblaze-b2-upload-any';
import { slug } from '../utils/slug';

@Injectable()
export class B2Service {
  private b2: any;
  private bucketUrl: any;
  private readonly bucketId = process.env.B2_BUCKET_ID;
  private recommendedPartSize: string;

  constructor() {
    const applicationKeyId = process.env.B2_KEY_ID;
    const applicationKey = process.env.B2_KEY;
    const b2 = new B2({
      applicationKeyId,
      applicationKey,
    });

    b2.authorize().then((r) => {
      this.recommendedPartSize = r.data.recommendedPartSize;
      this.b2 = b2;
    });
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    B2.prototype.uploadAny = uploadAny;
  }

  async uploadFile(
    file: FileUpload,
    fileName: string,
  ): Promise<string> {
    const newFileName = this.concatExtension(
      file.filename,
      slug(fileName),
    );
    await this.b2.uploadAny({
      bucketId: this.bucketId,
      fileName: newFileName,
      partSize: this.recommendedPartSize,
      data: file.createReadStream(),
    });
    return `${process.env.B2_ENDPOINT}${newFileName}`;
  }

  private concatExtension(filename, str) {
    // Get the file extension by splitting the filename string at the last period and getting the last element of the resulting array
    const extension = filename.split('.').pop();
    // Concatenate the specified string with the file extension and return the result
    return `${str}.${extension}`;
  }

  async downloadFile(fileId: string): Promise<Buffer> {
    const response = await this.b2.downloadFileById({
      responseType: 'document',
      fileId,
    });

    return response.data;
  }

  async deleteFile(fileId: string): Promise<void> {
    await this.b2.deleteFileVersion({
      fileName: fileId,
      fileId,
    });
  }
}

usage

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CategoryService } from './category.service';
import {
  Category,
  CreateOneCategoryArgs,
  DeleteOneCategoryArgs,
  FindUniqueCategoryArgs,
  UpdateOneCategoryArgs,
} from '../@generated';
import { UseGuards } from '@nestjs/common';
import { CheckAuthGuard } from '../guards/auth-guards/check-auth.guard';
import { Role } from '../user/entities/role.enum';
import { Roles } from '../guards/role.decorator';
import { B2Service } from '../b2/b2.service';
import { FileUpload } from '../b2/entities/file.entity';
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js';

@Resolver(() => Category)
export class CategoryResolver {
  constructor(
    private readonly categoryService: CategoryService,
    private readonly b2Service: B2Service,
  ) {}

  @Mutation(() => String)
  async uploadFile(
    @Args({ name: 'file', type: () => GraphQLUpload })
    file: FileUpload,
    @Args('fileName') fileName: string,
  ) {
    return await this.b2Service.uploadFile(file, fileName);
  }
}

image image

caferyukseloglu commented 1 year ago

This good but it is no longer a typescript we can't use this package with typescript

Mouhib-Rabbeg commented 8 months ago

@uncleDimasik i got this error when i make a request "TypeError: request.is is not a function\n' + ' at graphqlUploadExpressMiddleware'