├── store ├── book │ ├── main.ts │ ├── type.ts │ └── index.ts └── README.md ├── test └── unit │ ├── store │ └── book.spec.ts │ └── application │ ├── books.spec.ts │ └── users.spec.ts ├── .prettierrc ├── static └── favicon.ico ├── infrastructure ├── Path.ts ├── mock │ ├── index.ts │ └── data │ │ ├── books │ │ └── index.ts │ │ └── users │ │ └── index.ts ├── HttpClientFactory.ts └── provider │ ├── IClient.ts │ ├── axiosBase.ts │ ├── prodClient.ts │ └── mockClient.ts ├── application ├── users │ ├── useCase │ │ ├── index.ts │ │ ├── IFetchAll.ts │ │ └── IFetch.ts │ ├── interactors │ │ ├── index.ts │ │ ├── fetch.ts │ │ └── fetchAll.ts │ └── index.ts └── books │ ├── usecase │ ├── IFetchAll.ts │ ├── IDelete.ts │ ├── IFetch.ts │ ├── IPut.ts │ ├── IPost.ts │ └── index.ts │ ├── interactors │ ├── index.ts │ ├── fetch.ts │ ├── delete.ts │ ├── fetchAll.ts │ ├── put.ts │ └── post.ts │ └── index.ts ├── interfaces └── repository │ ├── user │ ├── IUserRepository.ts │ └── index.ts │ └── book │ ├── IBookRepository.ts │ └── index.ts ├── jsconfig.json ├── .editorconfig ├── .babelrc ├── pages ├── index.vue ├── user │ ├── detail │ │ └── _id.vue │ └── index.vue └── book │ ├── detail │ └── _id.vue │ └── index.vue ├── domain └── models │ ├── users │ └── userResponse.ts │ └── books │ └── bookResponse.ts ├── utils └── changeCase.js ├── jest.config.js ├── .eslintrc.js ├── tsconfig.json ├── README.md ├── di ├── user │ └── index.ts └── book │ └── index.ts ├── .gitignore ├── .prettierignore ├── nuxt.config.js └── package.json /store/book/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/book/type.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/book/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/store/book.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/application/books.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tak001/nuxt-practice-ca-like/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /infrastructure/Path.ts: -------------------------------------------------------------------------------- 1 | export const API = '/api/'; 2 | export const USERS = 'users'; 3 | export const BOOKS = 'books'; 4 | -------------------------------------------------------------------------------- /application/users/useCase/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/application/users/useCase/IFetchAll'; 2 | export * from '@/application/users/useCase/IFetch'; 3 | -------------------------------------------------------------------------------- /application/users/interactors/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/application/users/interactors/fetch'; 2 | export * from '@/application/users/interactors/fetchAll'; 3 | -------------------------------------------------------------------------------- /application/books/usecase/IFetchAll.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export interface IFetchAll { 4 | execute(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /application/users/useCase/IFetchAll.ts: -------------------------------------------------------------------------------- 1 | import User from '@/domain/models/users/UserResponse'; 2 | 3 | export interface IFetchAll { 4 | execute(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /application/books/usecase/IDelete.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export interface IDelete { 4 | execute: (id: Book['id']) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /application/books/usecase/IFetch.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export interface IFetch { 4 | execute: (id: Book['id']) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /application/users/useCase/IFetch.ts: -------------------------------------------------------------------------------- 1 | import User from '@/domain/models/users/UserResponse'; 2 | 3 | export interface IFetch { 4 | execute: (id: User['id']) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /application/books/usecase/IPut.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export interface IPut { 4 | execute: (args: { id: Book['id']; book: Book }) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /interfaces/repository/user/IUserRepository.ts: -------------------------------------------------------------------------------- 1 | import User from '@/domain/models/users/UserResponse'; 2 | 3 | export default interface IUserRepository { 4 | fetchAll(): Promise; 5 | fetch(id: User['id']): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /application/books/usecase/IPost.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export interface IPost { 4 | execute: (args: { 5 | title: Book['title']; 6 | author: Book['author']; 7 | }) => Promise; 8 | } 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /infrastructure/mock/index.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | 4 | const server = express(); 5 | server.use(cors()); 6 | 7 | server.listen(8881, () => { 8 | // eslint-disable-next-line no-console 9 | console.log('Listening on port 8881'); 10 | }); 11 | -------------------------------------------------------------------------------- /application/books/usecase/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/application/books/useCase/IDelete'; 2 | export * from '@/application/books/useCase/IFetchAll'; 3 | export * from '@/application/books/useCase/IFetch'; 4 | export * from '@/application/books/useCase/IPost'; 5 | export * from '@/application/books/useCase/IPut'; 6 | -------------------------------------------------------------------------------- /application/books/interactors/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/application/books/interactors/delete'; 2 | export * from '@/application/books/interactors/fetch'; 3 | export * from '@/application/books/interactors/fetchAll'; 4 | export * from '@/application/books/interactors/post'; 5 | export * from '@/application/books/interactors/put'; 6 | -------------------------------------------------------------------------------- /infrastructure/mock/data/books/index.ts: -------------------------------------------------------------------------------- 1 | const books = { 2 | message: 'ok', 3 | data: [ 4 | { 5 | id: 1, 6 | title: 'title', 7 | author: 'author', 8 | }, 9 | { 10 | id: 2, 11 | title: 'title2', 12 | author: 'author2', 13 | }, 14 | ], 15 | }; 16 | 17 | export default books; 18 | -------------------------------------------------------------------------------- /infrastructure/mock/data/users/index.ts: -------------------------------------------------------------------------------- 1 | const users = { 2 | message: 'ok', 3 | data: [ 4 | { 5 | id: 1, 6 | name: 'name', 7 | mail_address: 'hoge@gmail.com', 8 | }, 9 | { 10 | id: 2, 11 | name: 'name2', 12 | mail_address: 'hoge2@gmail.com', 13 | }, 14 | ], 15 | }; 16 | 17 | export default users; 18 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /interfaces/repository/book/IBookRepository.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | 3 | export default interface IBookRepository { 4 | delete(id: Book['id']): Promise; 5 | fetchAll(): Promise; 6 | fetch(id: Book['id']): Promise; 7 | post(args: { title: Book['title']; author: Book['author'] }): Promise; 8 | put(args: { id: Book['id']; book: Book }): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /application/books/interactors/fetch.ts: -------------------------------------------------------------------------------- 1 | import { IFetch } from '@/application/books/usecase'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 4 | 5 | export class Fetch implements IFetch { 6 | constructor(private readonly bookRepository: IBookRepository) {} 7 | 8 | execute(id: Book['id']): Promise { 9 | return this.bookRepository.fetch(id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /application/users/interactors/fetch.ts: -------------------------------------------------------------------------------- 1 | import { IFetch } from '@/application/users/useCase'; 2 | import User from '@/domain/models/users/UserResponse'; 3 | import IUserRepository from '@/interfaces/repository/user/IUserRepository'; 4 | 5 | export class Fetch implements IFetch { 6 | constructor(private readonly userRepository: IUserRepository) {} 7 | 8 | execute(id: User['id']): Promise { 9 | return this.userRepository.fetch(id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /application/books/interactors/delete.ts: -------------------------------------------------------------------------------- 1 | import { IDelete } from '@/application/books/usecase'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 4 | 5 | export class Delete implements IDelete { 6 | constructor(private readonly bookRepository: IBookRepository) {} 7 | 8 | async execute(id: Book['id']): Promise { 9 | await this.bookRepository.delete(id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /application/books/interactors/fetchAll.ts: -------------------------------------------------------------------------------- 1 | import { IFetchAll } from '@/application/books/useCase/IFetchAll'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 4 | 5 | export class FetchAll implements IFetchAll { 6 | constructor(private readonly bookRepository: IBookRepository) {} 7 | 8 | execute(): Promise { 9 | return this.bookRepository.fetchAll(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /application/users/interactors/fetchAll.ts: -------------------------------------------------------------------------------- 1 | import { IFetchAll } from '@/application/users/useCase/IFetchAll'; 2 | import User from '@/domain/models/users/UserResponse'; 3 | import IUserRepository from '@/interfaces/repository/user/IUserRepository'; 4 | 5 | export class FetchAll implements IFetchAll { 6 | constructor(private readonly userRepository: IUserRepository) {} 7 | 8 | execute(): Promise { 9 | return this.userRepository.fetchAll(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /infrastructure/HttpClientFactory.ts: -------------------------------------------------------------------------------- 1 | import { prodClient } from '@/infrastructure/provider/prodClient'; 2 | import { mockClient } from '@/infrastructure/provider/mockClient'; 3 | 4 | class HttpClientFactory { 5 | getClient() { 6 | if (process.env.NUXT_ENV_DEPLOYMENT === 'local') { 7 | return mockClient; 8 | } 9 | return prodClient; 10 | } 11 | } 12 | 13 | const httpClientFactory = new HttpClientFactory(); 14 | 15 | export default httpClientFactory; 16 | -------------------------------------------------------------------------------- /application/books/interactors/put.ts: -------------------------------------------------------------------------------- 1 | import { IPut } from '@/application/books/usecase'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 4 | 5 | export class Put implements IPut { 6 | constructor(private readonly bookRepository: IBookRepository) {} 7 | 8 | async execute(args: { id: Book['id']; book: Book }): Promise { 9 | await this.bookRepository.put(args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /domain/models/users/userResponse.ts: -------------------------------------------------------------------------------- 1 | export default class UserResponse { 2 | public id: number = 0; 3 | public name: string = ''; 4 | public mailAddress: string = ''; 5 | } 6 | 7 | // TODO: 以下の形に修正 8 | // export type Id = number; 9 | // export type Name = string; 10 | // export type MailAddress = string; 11 | 12 | // export class Book { 13 | // private id: Id 14 | // } 15 | 16 | // export const hasTitle = (book: Book): boolean { 17 | // return !!book.title 18 | // } 19 | -------------------------------------------------------------------------------- /utils/changeCase.js: -------------------------------------------------------------------------------- 1 | import changeCase from 'change-object-case'; 2 | 3 | changeCase.options = { recursive: true, arrayRecursive: true }; 4 | 5 | export const objectKeysToCamel = (item) => changeCase.toCamel(item); 6 | 7 | export const stringToCamel = (str) => changeCase.camelCase(str); 8 | 9 | export const objectKeysToKebab = (item) => changeCase.toParam(item); 10 | 11 | export const stringToKebab = (str) => changeCase.paramCase(str); 12 | 13 | export const stringToSnake = (str) => changeCase.snakeCase(str); 14 | -------------------------------------------------------------------------------- /application/books/interactors/post.ts: -------------------------------------------------------------------------------- 1 | import { IPost } from '@/application/books/usecase'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 4 | 5 | export class Post implements IPost { 6 | constructor(private readonly bookRepository: IBookRepository) {} 7 | 8 | async execute(args: { 9 | title: Book['title']; 10 | author: Book['author']; 11 | }): Promise { 12 | await this.bookRepository.post(args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^~/(.*)$': '/$1', 5 | '^vue$': 'vue/dist/vue.common.js', 6 | }, 7 | moduleFileExtensions: ['ts', 'js', 'vue', 'json'], 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest', 10 | '^.+\\.js$': 'babel-jest', 11 | '.*\\.(vue)$': 'vue-jest', 12 | }, 13 | collectCoverage: true, 14 | collectCoverageFrom: [ 15 | '/components/**/*.vue', 16 | '/pages/**/*.vue', 17 | ], 18 | testEnvironment: 'jsdom', 19 | } 20 | -------------------------------------------------------------------------------- /application/users/index.ts: -------------------------------------------------------------------------------- 1 | import di from '@/di/user'; 2 | import User from '@/domain/models/users/UserResponse'; 3 | 4 | class UserUseCase { 5 | async findAll(): Promise { 6 | const useCase = di.getInstance('fetchAll'); 7 | const result = await useCase.execute(); 8 | return result; 9 | } 10 | 11 | async find(id: User['id']): Promise { 12 | const useCase = di.getInstance('fetch'); 13 | const result = await useCase.execute(id); 14 | return result; 15 | } 16 | } 17 | 18 | const userUseCase = new UserUseCase(); 19 | 20 | export default userUseCase; 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | '@nuxtjs/eslint-config-typescript', 9 | 'plugin:nuxt/recommended', 10 | 'prettier', 11 | ], 12 | plugins: [], 13 | // add your custom rules here 14 | rules: { 15 | 'no-useless-constructor': 'off', 16 | 'vue/singleline-html-element-content-newline': 'off', 17 | 'no-use-before-define': 'off', 18 | 'max-len': ['error', { code: 80, ignoreUrls: true }], 19 | quotes: [ 20 | 'error', 21 | 'single', 22 | { avoidEscape: true, allowTemplateLiterals: false }, 23 | ], 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"], 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "experimentalDecorators": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "~/*": ["./*"], 16 | "@/*": ["./*"] 17 | }, 18 | "types": [ 19 | "@nuxt/types", 20 | "@nuxtjs/axios", 21 | "@types/node", 22 | "@types/lodash", 23 | "@types/jest" 24 | ] 25 | }, 26 | "exclude": ["node_modules", ".nuxt", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /infrastructure/provider/IClient.ts: -------------------------------------------------------------------------------- 1 | import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | 3 | export default interface IClient { 4 | post>( 5 | url: string, 6 | data?: any, 7 | config?: AxiosRequestConfig, 8 | ): AxiosPromise; 9 | 10 | put>( 11 | url: string, 12 | data?: any, 13 | config?: AxiosRequestConfig, 14 | ): AxiosPromise; 15 | 16 | get>( 17 | url: string, 18 | config?: AxiosRequestConfig, 19 | ): AxiosPromise; 20 | 21 | delete>( 22 | url: string, 23 | config?: AxiosRequestConfig, 24 | ): AxiosPromise; 25 | } 26 | -------------------------------------------------------------------------------- /interfaces/repository/user/index.ts: -------------------------------------------------------------------------------- 1 | import User from '@/domain/models/users/UserResponse'; 2 | import { API, USERS } from '@/infrastructure/Path'; 3 | import IClient from '@/infrastructure/provider/IClient'; 4 | import IUserRepository from '@/interfaces/repository/user/IUserRepository'; 5 | 6 | export class UserRepository implements IUserRepository { 7 | constructor(private readonly _client: IClient) {} 8 | 9 | async fetchAll(): Promise { 10 | const { data } = await this._client.get(API + USERS); 11 | return data.data; 12 | } 13 | 14 | async fetch(id: User['id']): Promise { 15 | const { data } = await this._client.get(API + `${USERS}/${id}`); 16 | return data.data; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /domain/models/books/bookResponse.ts: -------------------------------------------------------------------------------- 1 | export default class BookResponse { 2 | public id: number = 0; 3 | public title: string = ''; 4 | public author: string = ''; 5 | } 6 | 7 | // TODO: 以下の形に修正 8 | // export type Id = number; 9 | // export type Title = string; 10 | // export type Author = string; 11 | 12 | // export default class Book { 13 | // constructor( 14 | // private _id: Id, 15 | // private _title: string, 16 | // private _author: string, 17 | // ) {} 18 | 19 | // get id() { 20 | // return this._id; 21 | // } 22 | 23 | // get title() { 24 | // return this._title; 25 | // } 26 | 27 | // get author() { 28 | // return this._author; 29 | // } 30 | 31 | // hasTitle(book: Book): boolean { 32 | // return !!book.title; 33 | // } 34 | // } 35 | -------------------------------------------------------------------------------- /pages/user/detail/_id.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /infrastructure/provider/axiosBase.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import { objectKeysToCamel, objectKeysToKebab } from '@/utils/changeCase'; 3 | 4 | const client = axios.create({ 5 | // ブラウザのSessionCookieなどをaxiosでそのままサーバーに送信する 6 | withCredentials: true, 7 | headers: { 8 | 'X-Origin': location.hostname, 9 | 'X-Protocol': location.protocol, 10 | }, 11 | }); 12 | 13 | client.interceptors.request.use( 14 | (config: AxiosRequestConfig) => { 15 | config.data = objectKeysToKebab(config.data); 16 | return config; 17 | }, 18 | (error) => { 19 | return Promise.reject(error); 20 | }, 21 | ); 22 | 23 | client.interceptors.response.use((response: AxiosResponse) => { 24 | // キャメルケースに変換 25 | response.data = objectKeysToCamel(response.data); 26 | 27 | return response; 28 | }); 29 | 30 | export default client; 31 | -------------------------------------------------------------------------------- /infrastructure/provider/prodClient.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import axiosBase from '@/infrastructure/provider/axiosBase'; 3 | import IClient from '@/infrastructure/provider/IClient'; 4 | 5 | export class ProdClient implements IClient { 6 | post(url: string, data: any, config?: AxiosRequestConfig): Promise { 7 | return axiosBase.post(url, data, config); 8 | } 9 | 10 | // TODO: 整備 11 | put(url: string, data: any, config?: AxiosRequestConfig): Promise { 12 | return axiosBase.put(url, data, config); 13 | } 14 | 15 | // TODO: 整備 16 | get(url: string, config?: AxiosRequestConfig): Promise { 17 | return axiosBase.get(url, config); 18 | } 19 | 20 | // TODO: 整備 21 | delete(url: string, config?: AxiosRequestConfig): Promise { 22 | return axiosBase.delete(url, config); 23 | } 24 | } 25 | 26 | export const prodClient = new ProdClient(); 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-practice-ca-like 2 | 3 | ## 手順 4 | 5 | ### install dependencies 6 | 7 | ```bash 8 | % npm i 9 | ``` 10 | 11 | ### mockを使用した開発 12 | 13 | ```markdown 14 | 1. .env.local.js を作成 15 | 2. 以下のように API_URL を指定する 16 | 17 | module.exports = { 18 | API_URL: 'http://localhost:8881', 19 | }; 20 | 21 | 3. `npm run local` を叩く 22 | 4. http://localhost:3000 にアクセス 23 | ``` 24 | 25 | ### ローカルサーバーを立てた開発 26 | 27 | ```markdown 28 | 1. .env.development.js を作成 29 | 2. API_URL を指定 30 | 3. `npm run dev` を叩く 31 | 4. ローカルサーバーを立てる 32 | 5. http://localhost:3000 にアクセス 33 | ``` 34 | 35 | ### 本番 36 | 37 | ```markdown 38 | 1. .env.production.js を作成 39 | 2. API_URL を指定 40 | 3. `npm run prod` を叩く 41 | ``` 42 | 43 | ### jest 44 | 45 | ```markdown 46 | % npm run test 47 | ``` 48 | 49 | ## TODO 50 | 51 | - Domainの見直し 52 | - DIコンテナの作り直し 53 | - Request(args)型の持たせ方と置き場所 54 | - storeへ移行 55 | - 集約ルートの作成 56 | - Domainに必要な値を抽出 57 | - フロントで使用する値に加工 58 | - unit test 59 | - e2e test 60 | -------------------------------------------------------------------------------- /test/unit/application/users.spec.ts: -------------------------------------------------------------------------------- 1 | import userUseCase from '@/application/users'; 2 | import { objectKeysToCamel } from '@/utils/changeCase'; 3 | import mockData from '@/infrastructure/mock/data/users'; 4 | 5 | const mock: typeof mockData = objectKeysToCamel(mockData); 6 | 7 | // 第一引数は、テストしたいclassをpath指定 8 | jest.mock('@/application/users', () => ({ 9 | // UserUseCaseのclass内のメソッドが呼ばれた時にmockを返すようにする 10 | findAll: () => mock, 11 | find: (id: number) => mock.data.find((item) => item.id === id), 12 | })); 13 | 14 | describe('application: User', () => { 15 | test('fetchAll', async () => { 16 | const res = await userUseCase.findAll(); 17 | expect(res).toEqual(mock); 18 | }); 19 | 20 | const testData = [1, 2]; 21 | 22 | test.each(testData)('fetch: id = %#', async (testData) => { 23 | const res = await userUseCase.find(testData); 24 | const expected = mock.data.find((item) => item.id === testData); 25 | expect(res).toEqual(expected); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /di/user/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { Fetch, FetchAll } from '@/application/users/interactors'; 3 | import httpClientFactory from '@/infrastructure/HttpClientFactory'; 4 | import IClient from '@/infrastructure/provider/IClient'; 5 | import { UserRepository } from '@/interfaces/repository/user'; 6 | 7 | type KeyType = 'fetchAll' | 'fetch'; 8 | 9 | // TODO: 実務に耐えれないので、ちゃんとしたDIコンテナを作成する 10 | class DI { 11 | constructor(private readonly _client: IClient) {} 12 | 13 | // Function Overloading 14 | getInstance(type: 'fetchAll'): FetchAll; 15 | getInstance(type: 'fetch'): Fetch; 16 | getInstance(type: KeyType) { 17 | if (type === 'fetchAll') { 18 | const repository = new UserRepository(this._client); 19 | return new FetchAll(repository); 20 | } 21 | if (type === 'fetch') { 22 | const repository = new UserRepository(this._client); 23 | return new Fetch(repository); 24 | } 25 | } 26 | } 27 | 28 | const di = new DI(httpClientFactory.getClient()); 29 | 30 | export default di; 31 | -------------------------------------------------------------------------------- /pages/user/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /application/books/index.ts: -------------------------------------------------------------------------------- 1 | import di from '@/di/book'; 2 | import Book from '@/domain/models/books/bookResponse'; 3 | 4 | class BookUseCase { 5 | async delete(id: Book['id']): Promise { 6 | const useCase = di.getInstance('delete'); 7 | await useCase.execute(id); 8 | } 9 | 10 | async findAll(): Promise { 11 | const useCase = di.getInstance('fetchAll'); 12 | const result = await useCase.execute(); 13 | return result; 14 | } 15 | 16 | async find(id: Book['id']): Promise { 17 | const useCase = di.getInstance('fetch'); 18 | const result = await useCase.execute(id); 19 | return result; 20 | } 21 | 22 | async post(args: { 23 | title: Book['title']; 24 | author: Book['author']; 25 | }): Promise { 26 | const useCase = di.getInstance('post'); 27 | await useCase.execute(args); 28 | } 29 | 30 | async put(args: { id: Book['id']; book: Book }): Promise { 31 | const useCase = di.getInstance('put'); 32 | await useCase.execute(args); 33 | } 34 | } 35 | 36 | const bookUseCase = new BookUseCase(); 37 | 38 | export default bookUseCase; 39 | -------------------------------------------------------------------------------- /interfaces/repository/book/index.ts: -------------------------------------------------------------------------------- 1 | import Book from '@/domain/models/books/bookResponse'; 2 | import { API, BOOKS } from '@/infrastructure/Path'; 3 | import IClient from '@/infrastructure/provider/IClient'; 4 | import IBookRepository from '@/interfaces/repository/book/IBookRepository'; 5 | 6 | export class BookRepository implements IBookRepository { 7 | constructor(private readonly _client: IClient) {} 8 | 9 | async delete(id: Book['id']): Promise { 10 | await this._client.delete(API + `${BOOKS}/${id}`); 11 | } 12 | 13 | async fetchAll(): Promise { 14 | const { data } = await this._client.get(API + BOOKS); 15 | return data.data; 16 | } 17 | 18 | async fetch(id: Book['id']): Promise { 19 | const { data } = await this._client.get(API + `${BOOKS}/${id}`); 20 | return data.data; 21 | } 22 | 23 | async post(args: { 24 | title: Book['title']; 25 | author: Book['author']; 26 | }): Promise { 27 | await this._client.post(API + BOOKS, args); 28 | } 29 | 30 | async put(args: { id: Book['id']; book: Book }): Promise { 31 | await this._client.put(API + `${BOOKS}/${args.id}`, args.book); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pages/book/detail/_id.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | .env.* 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # Nuxt generate 73 | dist 74 | 75 | # vuepress build output 76 | .vuepress/dist 77 | 78 | # Serverless directories 79 | .serverless 80 | 81 | # IDE / Editor 82 | .idea 83 | 84 | # Service worker 85 | sw.* 86 | 87 | # macOS 88 | .DS_Store 89 | 90 | # Vim swap files 91 | *.swp 92 | -------------------------------------------------------------------------------- /di/book/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-dupe-class-members */ 2 | import { 3 | Delete, 4 | Fetch, 5 | FetchAll, 6 | Post, 7 | Put, 8 | } from '@/application/books/interactors'; 9 | import httpClientFactory from '@/infrastructure/HttpClientFactory'; 10 | import IClient from '@/infrastructure/provider/IClient'; 11 | import { BookRepository } from '@/interfaces/repository/book'; 12 | 13 | type KeyType = 'delete' | 'fetchAll' | 'fetch' | 'post' | 'put'; 14 | 15 | // TODO: いけてないので、マジでちゃんとしたDIコンテナを作成する 16 | class DI { 17 | constructor(private readonly _client: IClient) {} 18 | 19 | // Function Overloading 20 | getInstance(type: 'delete'): Delete; 21 | getInstance(type: 'fetchAll'): FetchAll; 22 | getInstance(type: 'fetch'): Fetch; 23 | getInstance(type: 'post'): Post; 24 | getInstance(type: 'put'): Put; 25 | getInstance(type: KeyType) { 26 | if (type === 'delete') { 27 | const repository = new BookRepository(this._client); 28 | return new Delete(repository); 29 | } 30 | if (type === 'fetchAll') { 31 | const repository = new BookRepository(this._client); 32 | return new FetchAll(repository); 33 | } 34 | if (type === 'fetch') { 35 | const repository = new BookRepository(this._client); 36 | return new Fetch(repository); 37 | } 38 | if (type === 'post') { 39 | const repository = new BookRepository(this._client); 40 | return new Post(repository); 41 | } 42 | if (type === 'put') { 43 | const repository = new BookRepository(this._client); 44 | return new Put(repository); 45 | } 46 | } 47 | } 48 | 49 | const di = new DI(httpClientFactory.getClient()); 50 | 51 | export default di; 52 | -------------------------------------------------------------------------------- /pages/book/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 61 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ### 2 | # Place your Prettier ignore content here 3 | 4 | ### 5 | # .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | ### Node template 9 | # Logs 10 | /logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js build output 72 | .next 73 | 74 | # nuxt.js build output 75 | .nuxt 76 | 77 | # Nuxt generate 78 | dist 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # IDE / Editor 87 | .idea 88 | 89 | # Service worker 90 | sw.* 91 | 92 | # macOS 93 | .DS_Store 94 | 95 | # Vim swap files 96 | *.swp 97 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const deploymentEnv = process.env.NUXT_ENV_DEPLOYMENT || 'development'; 2 | const environment = require(`./.env.${deploymentEnv}.js`); 3 | 4 | export default { 5 | // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode 6 | ssr: false, 7 | env: environment, 8 | 9 | // Target: https://go.nuxtjs.dev/config-target 10 | target: 'static', 11 | 12 | // Global page headers: https://go.nuxtjs.dev/config-head 13 | head: { 14 | title: 'nuxt-practice-ca-like', 15 | htmlAttrs: { 16 | lang: 'en', 17 | }, 18 | meta: [ 19 | { charset: 'utf-8' }, 20 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 21 | { hid: 'description', name: 'description', content: '' }, 22 | { name: 'format-detection', content: 'telephone=no' }, 23 | ], 24 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 25 | }, 26 | 27 | // Global CSS: https://go.nuxtjs.dev/config-css 28 | css: [], 29 | 30 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 31 | plugins: [], 32 | 33 | // Auto import components: https://go.nuxtjs.dev/config-components 34 | components: true, 35 | 36 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 37 | buildModules: [ 38 | // https://go.nuxtjs.dev/typescript 39 | '@nuxt/typescript-build', 40 | ], 41 | 42 | // Modules: https://go.nuxtjs.dev/config-modules 43 | modules: [ 44 | // https://go.nuxtjs.dev/axios 45 | '@nuxtjs/axios', 46 | '@nuxtjs/proxy', 47 | ], 48 | 49 | // Axios module configuration: https://go.nuxtjs.dev/config-axios 50 | axios: { 51 | // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308 52 | baseURL: '/', 53 | proxy: true, 54 | }, 55 | 56 | proxy: { 57 | '/api': `${environment.API_URL}`, 58 | }, 59 | 60 | // Build Configuration: https://go.nuxtjs.dev/config-build 61 | build: {}, 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-practice-ca-like", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "local": "cross-env NUXT_ENV_DEPLOYMENT=local nuxt & npm run mock:api", 7 | "dev": "cross-env NUXT_ENV_DEPLOYMENT=development nuxt", 8 | "prod": "cross-env NUXT_ENV_DEPLOYMENT=production nuxt", 9 | "mock:api": "node ./infrastructure/mock/index.ts", 10 | "build": "nuxt build", 11 | "start": "nuxt start", 12 | "generate": "nuxt generate", 13 | "lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .", 14 | "lint:prettier": "prettier --check .", 15 | "lint": "npm run lint:js && npm run lint:prettier", 16 | "lintfix": "prettier --write --list-different . && npm run lint:js -- --fix", 17 | "test": "jest" 18 | }, 19 | "dependencies": { 20 | "@nuxtjs/axios": "^5.13.6", 21 | "change-object-case": "^0.2.1", 22 | "core-js": "^3.19.3", 23 | "lodash": "^4.17.21", 24 | "nuxt": "^2.15.8", 25 | "proxy": "^1.0.2", 26 | "vue": "^2.6.14", 27 | "vue-server-renderer": "^2.6.14", 28 | "vue-template-compiler": "^2.6.14", 29 | "webpack": "^4.46.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/eslint-parser": "^7.16.3", 33 | "@nuxt/types": "^2.15.8", 34 | "@nuxt/typescript-build": "^2.0.0", 35 | "@nuxt/typescript-runtime": "^2.0.0", 36 | "@nuxtjs/eslint-config-typescript": "^8.0.0", 37 | "@nuxtjs/eslint-module": "^3.0.2", 38 | "@types/jest": "^27.4.1", 39 | "@types/lodash": "^4.14.179", 40 | "@vue/test-utils": "^1.3.0", 41 | "babel-core": "7.0.0-bridge.0", 42 | "babel-jest": "^27.4.4", 43 | "cors": "^2.8.5", 44 | "cross-env": "^7.0.3", 45 | "eslint": "^8.4.1", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-nuxt": "^3.1.0", 48 | "eslint-plugin-vue": "^8.2.0", 49 | "express": "^4.17.3", 50 | "jest": "^27.4.4", 51 | "nuxt-property-decorator": "^2.9.1", 52 | "prettier": "^2.5.1", 53 | "ts-jest": "^27.1.1", 54 | "vue-jest": "^3.0.4", 55 | "vuex-type-helper": "^1.3.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /infrastructure/provider/mockClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-absolute-path */ 2 | import { AxiosRequestConfig } from 'axios'; 3 | import { BOOKS, USERS } from '@/infrastructure/Path'; 4 | import axiosBase from '@/infrastructure/provider/axiosBase'; 5 | import IClient from '@/infrastructure/provider/IClient'; 6 | import { objectKeysToCamel } from '@/utils/changeCase'; 7 | 8 | const result = (data: any) => ({ data }); 9 | 10 | const wrapPromise = (item: any) => 11 | new Promise((resolve) => 12 | setTimeout(() => { 13 | resolve(result(objectKeysToCamel(item))); 14 | }, 500), 15 | ); 16 | 17 | const mockPaths = [ 18 | { 19 | path: USERS, 20 | value: require('@/infrastructure/mock/data/users/index.ts').default, 21 | }, 22 | { 23 | path: BOOKS, 24 | value: require('@/infrastructure/mock/data/books/index.ts').default, 25 | }, 26 | ]; 27 | 28 | const getTarget = (path: string | undefined) => { 29 | return mockPaths.filter((item) => item.path === path); 30 | }; 31 | 32 | class MockClient implements IClient { 33 | /** 34 | * post 35 | */ 36 | post(path: string, data?: any, config?: AxiosRequestConfig): Promise { 37 | const lastPath = path.split('/').pop(); 38 | 39 | const target = getTarget(lastPath); 40 | 41 | if (target.length !== 0) { 42 | return wrapPromise(target[0].value); 43 | } 44 | 45 | return axiosBase.post(path, data, config); 46 | } 47 | 48 | // TODO: 整備 49 | put(path: string, data: any, config?: AxiosRequestConfig): Promise { 50 | const lastPath = path.split('/').pop(); 51 | const target = getTarget(lastPath); 52 | 53 | if (target.length !== 0) { 54 | return wrapPromise(target[0].value); 55 | } 56 | 57 | return axiosBase.put(path, data, config); 58 | } 59 | 60 | // TODO: 整備 61 | get(path: string, config?: AxiosRequestConfig): Promise { 62 | const matchedId = path.match(/\d+$/); 63 | const lastPath = matchedId 64 | ? path.split('/').splice(2, 1)[0] 65 | : path.split('/').pop(); 66 | const target = getTarget(lastPath); 67 | 68 | if (target.length === 0) { 69 | return axiosBase.get(path, config); 70 | } 71 | 72 | if (matchedId) { 73 | const id = matchedId ? Number(matchedId[0]) : 0; 74 | const obj = target[0].value.data.find((item: any) => item.id === id); 75 | return wrapPromise({ data: obj }); 76 | } 77 | return wrapPromise(target[0].value); 78 | } 79 | 80 | // TODO: 整備 81 | delete(path: string, config?: AxiosRequestConfig): Promise { 82 | const lastPath = path.split('/').pop(); 83 | const target = getTarget(lastPath); 84 | 85 | if (target.length !== 0) { 86 | return wrapPromise(target[0].value); 87 | } 88 | 89 | return axiosBase.delete(path, config); 90 | } 91 | } 92 | 93 | export const mockClient = new MockClient(); 94 | --------------------------------------------------------------------------------