<file_sep>version: "3.7"
services:
  client:
    build:
      context: ./client
      dockerfile: ./Dockerfile
    image: post-client
    container_name: post-client
    ports:
      - "4444:3000"
    deploy:
      replicas: 3
  api:
    build:
      context: ./server
      dockerfile: ./Dockerfile
    image: post-api
    container_name: post-api
    environment:
      - PORT=3000
    ports:
      - "3333:3000"
    deploy:
      replicas: 6
  nginx:
    image: post-nginx
    container_name: nginx
    depends_on:
      - api
      - client
    build:
      context: ./nginx
      dockerfile: ./Dockerfile
    ports:
      - "80:80"
    deploy:
      replicas: 1
<file_sep>import Axios from "axios";

export const axios = Axios.create({
  baseURL: " https://jsonplaceholder.typicode.com",
});

export const imageApi = Axios.create({
  baseURL: "https://picsum.photos/v2",
});
<file_sep>import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { PostsModule } from './posts/posts.module';
import { CommentsModule } from './comments/comments.module';
import { PostsService } from './posts/posts.service';
import { CommentsService } from './comments/comments.service';

@Module({
  imports: [
    GraphQLModule.forRoot({
      dataSources: () => ({
        postsAPI: new PostsService(),
        commentsAPI: new CommentsService(),
      }),
      include: [PostsModule, CommentsModule],
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
    }),
    PostsModule,
    CommentsModule,
  ],
})
export class AppModule {}
<file_sep>import { ObjectType, Field, Int } from '@nestjs/graphql';
import { IPost } from '../../types';

@ObjectType()
export class Post implements IPost {
  @Field(() => Int, { description: 'Id of the User' })
  userId: number;
  @Field(() => Int, { description: 'Id of the Post' })
  id: number;
  @Field(() => String, { description: 'Title of the Post' })
  title: string;

  @Field(() => String, { description: 'Body of the Post' })
  body: string;
}

@ObjectType()
export class PostsConnection {
  @Field(() => Int)
  totlaCount: number;
  @Field(() => String, { nullable: true })
  cursor?: string;
  @Field(() => Boolean)
  hasMore: boolean;
  @Field(() => [Post])
  posts: Post[];
}
<file_sep>- display all posts
- display a post with its comments
  - a search box that filters the comments
  - the search should be able to handle :
    - name
    - email
    - body
<file_sep>## A fullstack Repository Implementing Docker Swarm
<file_sep>import { Injectable } from '@nestjs/common';
import { CommentSearch } from '../types';
import { Comment } from './entities/comment.entity';
import { RESTDataSource } from 'apollo-datasource-rest';
@Injectable()
export class CommentsService extends RESTDataSource {
  private _URL = 'https://jsonplaceholder.typicode.com/comments';

  constructor() {
    super();
    this.baseURL = this._URL;
  }
  async findAll() {
    const result = await this.get<Comment[]>('/');
    return result;
  }
  async findSearch(input: string, search: CommentSearch) {
    const result = await this.get<Comment[]>(`/`, {
      per_page: 20,
      [search]: input,
    });
    return result;
  }
}
<file_sep>## Instructions

There are several ways to run the api

### Run With Docker Compose Dev Mode

```
docker-compose -f docker-compose.dev.yml up -d
```

### Run With Docker Compose

```
docker-compose  up -d
```

### Run With Docker Build Only

```
docker build . -t graphapi

docker run --name api -p 3000:3000 \
graphapi
```

### Run Project Normally

```
yarn install OR npm install

yarn start:dev OR npm start
```
<file_sep>import { Resolver, Query, Args, Int, Context } from '@nestjs/graphql';
import { Post, PostsConnection } from './entities/post.entity';
import { IDatasource } from '../types';
import { paginateResults } from '../util';
@Resolver(() => Post)
export class PostsResolver {
  constructor() {}

  @Query(() => PostsConnection, { name: 'posts' })
  async findAll(
    @Args('after', { nullable: true }) after: number,
    @Context('dataSources') { postsAPI }: IDatasource,
  ) {
    const posts = await postsAPI.findAll();
    return this.paginateComments(posts, after);
  }

  @Query(() => Post, { name: 'post' })
  findOne(
    @Args('id', { type: () => Int }) id: number,
    @Context('dataSources') { postsAPI }: IDatasource,
  ) {
    return postsAPI.findOne(id);
  }

  paginateComments(posts: Post[], after: number) {
    const paginateComments = paginateResults({
      after: after,
      pageSize: 20,
      results: posts,
    });
    const postsConnected = {
      totlaCount: posts.length,
      posts: paginateComments,
      cursor: paginateComments.length
        ? paginateComments[paginateComments.length - 1].id
        : null,
      hasMore: paginateComments.length
        ? paginateComments[paginateComments.length - 1].id !==
          posts[paginateComments.length]?.id
        : false,
    } as PostsConnection;
    return postsConnected;
  }
}
<file_sep>import { Args, Context, Query, Resolver } from '@nestjs/graphql';
import { paginateResults } from '../util';
import { CommentSearch, IDatasource } from '../types';
import { Comment, CommentsConnection } from './entities/comment.entity';
@Resolver(() => Comment)
export class CommentsResolver {
  constructor() {}

  @Query(() => CommentsConnection, { name: 'comments' })
  async findAll(
    @Context('dataSources') { commentsAPI }: IDatasource,
    @Args('after', { nullable: true }) after: number,
  ) {
    const comments = await commentsAPI.findAll();
    return this.paginateComments(comments, after);
  }

  @Query(() => CommentsConnection, { name: 'commentsBySearch' })
  async findBySearch(
    @Args('CommentSearch', { type: () => CommentSearch }) search: CommentSearch,
    @Args('input') input: string,
    @Args('after', { nullable: true }) after: number,
    @Context('dataSources') { commentsAPI }: IDatasource,
  ) {
    const comments = await commentsAPI.findSearch(input, search);
    return this.paginateComments(comments, after);
  }

  paginateComments(comments: Comment[], after: number) {
    const paginateComments = paginateResults({
      after: after,
      pageSize: 20,
      results: comments,
    });
    const commentsConnected = {
      totlaCount: comments.length,
      comments: paginateComments,
      cursor: paginateComments.length
        ? paginateComments[paginateComments.length - 1].id
        : null,
      hasMore: paginateComments.length
        ? paginateComments[paginateComments.length - 1].id !==
          comments[paginateComments.length]?.id
        : false,
    } as CommentsConnection;
    return commentsConnected;
  }
}
<file_sep>export const containerVariants = {
  hidden: {
    opacity: 0,
  },
  visible: {
    opacity: 1,
    transition: { delay: 0.4, duration: 0.8 },
  },
  exit: {
    x: "-100vh",
    transition: { ease: "easeInOut" },
  },
};
<file_sep>import { Field, Int, ObjectType } from '@nestjs/graphql';
import { IComment } from '../../types';

@ObjectType()
export class Comment implements IComment {
  @Field(() => Int, { description: 'Comment Id' })
  id: number;
  @Field(() => Int, { description: ' Post Id' })
  postId: number;
  @Field(() => String, { description: 'User Name of the Commenter' })
  name: string;
  @Field(() => String, { description: 'Email of the Commenter' })
  email: string;
  @Field(() => String, { description: 'Text of the Comment' })
  body: string;
}

@ObjectType()
export class CommentsConnection {
  @Field(() => Int)
  totlaCount: number;
  @Field(() => String, { nullable: true })
  cursor?: string;
  @Field(() => Boolean)
  hasMore: boolean;
  @Field(() => [Comment])
  comments: Comment[];
}
<file_sep>import { registerEnumType } from '@nestjs/graphql';
import { CommentsService } from '../comments/comments.service';
import { PostsService } from '../posts/posts.service';

export interface IPost {
  userId: number;
  id: number;

  title: string;
  body: string;
}
export interface IComment {
  id: number;
  postId: number;
  name: string;
  email: string;
  body: string;
}

export enum CommentSearch {
  id = 'id',
  postId = 'postId',
  name = 'name',
  email = 'email',
  body = 'body',
}

registerEnumType(CommentSearch, {
  name: 'CommentSearch',
  description: 'Property Type of the comment to searched ',
});
export interface IDatasource {
  postsAPI: PostsService;
  commentsAPI: CommentsService;
}

export type pageItem = IComment | IPost;
export type pageItems = IComment[] | IPost[];
<file_sep>import { Injectable } from '@nestjs/common';
import { RESTDataSource } from 'apollo-datasource-rest';
import { Post } from './entities/post.entity';
@Injectable()
export class PostsService extends RESTDataSource {
  private _URL = 'https://jsonplaceholder.typicode.com/posts';
  constructor() {
    super();
    this.baseURL = this._URL;
  }
  async findAll() {
    const result = await this.get<Post[]>('/');
    return result;
  }

  async findOne(id: number) {
    const result = await this.get<Post>(`/${id}`);
    return result;
  }
}
<file_sep>export interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
  image?: string;
}

export interface Comment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}

export enum CommentSearch {
  name = "name",
  email = "email",
  body = "body",
}
