Open
29

Docker・Nuxt・Typescript・Django REST Frameworkで開発

開発メモとして

何か作るときに、調べたことをメモしながら作っていきます。それが、ある程度まとまった内容になったら、ブログなり、Qiitaなり、Zennなりに書く(書こうという気持ちはある)というスタイルです。

けど、後から振り返ってまとめるのは、その時点では熱が冷めていたりすることもあります(それが大半だ)。

で、メモのスタイルは、こちらのスクラップブックと一致しているなと気づき、また、晒すことでホットな情報が得られるかもしれないし、ということで、これを書いていくことにしたいと思います。

タイトルすら更新していくと思います。

(気づき)
URLはそのまま貼り付けた方が良いですね。

作っているもの

構成の概要はこちらにも書きました。

https://zenn.dev/jqinglong/articles/8db55afdba0db8

そのうちGithubのソースも公開するつもりだし、加工するのも面倒なので、ソースのパス等もそのまま書いていきます。

技術選定

フロント:Nuxt

テンプレートが書きやすく、かつ、Vuetify が素晴らしいと思っているので、最近は Nuxt を使うようにしています。ただ、Typescript からは逃げていましたが、今回は Typescript でやります。

API

google spreadsheet api

直接使用は依存しすぎるのでダメ
GoogleSpread →HerokuDBは良いと思う
HerokuDB用APIを作ることにする

APIを作成するにあたっての参考情報

https://eng-blog.iij.ad.jp/archives/5415

Django REST Framework

  • Javaは捨て難いけど
  • 言語としてのトレンド
  • DB作るのはしんどいけど

python 環境

パッケージのインストールならpip,仮想環境の構築ならvirtualenv(venv)を使えば対応できますが,pipenvはそれらをまとめてより簡単に扱えるようにサポートしてくれます.
APIもherokuにデプロイしよう

DB

herokuは本来はPostgreSQL
チャレンジしてみる→断念してMySQL
マイグレーションもsqliteではなくpostgresにすることを考慮

参考情報

https://zenn.dev/dsonoda/articles/dbe14ca8af617ed85b1f
(必要なら)
https://qiita.com/penpenta/items/c993243c4ceee3840f30

Docker構成

参考情報

Django + REST framework + Swagger + JWT + docker-compose で開発環境構築

基本はこれに従うが、これはPythonだけ
DBも作成するので、大きな構成は下記

PoliLink
├── PolyLink.md
├── docker-compose.yml 
├── .git
├── .gitignore
├── README.md
├── poli-link-api
│   ├── config
│       ├── __init__.py
│       ├── __pycache__
│       ├── ...
├── poli-link-db
│       ├── (postgres)
│       ├── ...
├── poli-link-web
│       ├── (nuxt)
│       ├── ...

pipenv

参考情報

https://qiita.com/kapibara-3/items/c4acf09c8b61e43699ab
brew update

# エラー対処
sudo rm /usr/local/share/Library/Caches/Yarn/v6/npm-micromatch-3.1.10-70859bc95c9840952f359a068a3fc49f9ecfac23-integrity/node_modules/micromatch/lib/.DS_Store
sudo rm /usr/local/share/Library/Caches/Yarn/v6/npm-ws-3.3.3-f1cf84fe2d5e901ebce94efaece785f187a228f2-integrity/node_modules/ws/lib/.DS_Store

sudo find /usr/local/share/Library/Caches/Yarn/v6 -type f -name ".DS_Store" -print | xargs sudo rm -f

brew install pyenv
pyenv install --list

# 3.8.1が出てこないので
brew upgrade pyenv
pyenv install --list
sudo pyenv install 3.8.1

それでもエラー
error: implicit declaration of function 'sendfile' is invalid

参考

https://teratail.com/questions/309663
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install
brew reinstall zlib bzip2
==> Downloading https://homebrew.bintray.com/bottles/zlib-1.2.11.big_sur.bottle.tar.gz
(途中省略)

==> Summary
(途中省略)

==> bzip2
(途中省略)

For compilers to find bzip2 you may need to set:
  export LDFLAGS="-L/usr/local/opt/bzip2/lib"
  export CPPFLAGS="-I/usr/local/opt/bzip2/include"

メッセージに従って

export LDFLAGS="-L/usr/local/opt/zlib/lib"
export CPPFLAGS="-I/usr/local/opt/zlib/include"
export PKG_CONFIG_PATH="/usr/local/opt/zlib/lib/pkgconfig"
echo 'export PATH="/usr/local/opt/bzip2/bin:$PATH"' >> /Users/konnokiyotaka/.bash_profile
export LDFLAGS="-L/usr/local/opt/bzip2/lib"
export CPPFLAGS="-I/usr/local/opt/bzip2/include"

#さらに
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

#https://teratail.com/questions/309549
brew install pyenv zlib bzip2 readline
CPPFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix readline)/include -I$(brew --prefix zlib)/include" LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" pyenv install 3.8.6

これでようやくOKで、本題

$ pyenv versions
  system
* 3.7.2 (set by ...
  3.8.6
$ pyenv global 3.8.6
$ pyenv versions
  system
  3.7.2
* 3.8.6 (set by ...
$ python -V
Python 3.8.6
$ pip install --upgrade pip
$ pip install pipenv
$ pipenv --version
pipenv, version 2020.11.15
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

Django環境・プロジェクト作成

参考情報

https://zenn.dev/dsonoda/articles/dbe14ca8af617ed85b1f#プロジェクトのセットアップ
$ mkdir poli-link-api && cd poli-link-api
$ pipenv --python 3.8

gunicornは使わない
psycopg2-binaryもはPostgresように必要

$ pipenv shell
(poli-link-api)$ pipenv install django psycopg2-binary

✘ Locking Failed! 
ERROR:pip.subprocessor:Command errored out with exit status 1:
(途中省略)

結局このエラーは解消できなかったので、mysqlにした

version: '3'

services:
  db:
    container_name: poli-link-db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: polilink_database
      MYSQL_USER: root
      MYSQL_PASSWORD: password
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - ./poli-link-db/mysql:/var/lib/mysql
    ports:
      - 33306:3306
  api:
    container_name: poli-link-api
    build: ./poli-link-api
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./poli-link-api/:/code
    ports:
      - "8000:8000"
    depends_on:
      - db

Django プロジェクトの生成

docker-compose run api django-admin.py startproject config .

Djangoアプリ作成

先ほどはdjango-admin.py startprojectでconfigというプロジェクトを作った
次は、python manage.py startapp sample_appでアプリを作る

$ docker exec -it poli-link-api /bin/bash
code# python manage.py startapp polilink_api

作成された

以下、先程のサイトの手順に従って、

テストビューの作成
アプリ用URLConfの新規作成
ルート用URLConfの編集

ここまでで、http://localhost:8000/ が「Hello, world.」に切り替わった
さらに

$ docker exec -it poli-link-api /bin/bash
code# pip install djangorestframework django-filter

Djangoモデル

poli-link-api/polilink_api/models.py
でモデル定義して、

$ docker exec -it poli-link-api /bin/bash
code# python manage.py makemigrations
code# python manage.py migrate

接続できない
Access denied for user 'root'@'192.168.32.1' (using password: YES)
しかし、もう少し続けてみると
エラーになるけど管理画面は表示される
Memosテーブルもできている

なので、続けていく
Djagno REST Framework のアプリケーション登録
Serializerの定義
ViewSetの定義
REST frameworkの設定をアプリ用URLConfに組み込む
REST frameworkの設定をルート用URLConfに

これにより、Api Root の画面が表示された

Django REST Swagger のインストール

$ docker exec -it poli-link-api /bin/bash
code# pip install django-rest-swagger

swaggerの有効化
swaggerの設定をルート用URLConfに組み込む

$ docker exec -it poli-link-api /bin/bash
code# python manage.py runserver

これでOK

djangorestframework-jwt のインストール

code# pip install djangorestframework-jwt

JWTの有効化
JWTの設定をルート用URLConfに組み込む

nuxt-typescript用のdocker

poli-link-web/Dockerfile
FROM node:14.16.0-alpine
ENV LANG=C.UTF-8 TZ=Asia/Tokyo
WORKDIR /app
RUN apk update
COPY ./package*.json ./
RUN npm install
COPY ./ .
ENV HOST 0.0.0.0
EXPOSE 3000
docker-compose.yml
version: '3'

services:
  db:
    container_name: poli-link-db
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: polilink_database
      MYSQL_USER: root
      MYSQL_PASSWORD: password
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - ./poli-link-db/mysql:/var/lib/mysql
    ports:
      - 3306:3306
  api:
    container_name: poli-link-api
    build: ./poli-link-api
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./poli-link-api/:/code
    ports:
      - "8000:8000"
    depends_on:
      - db
  web:
    container_name: poli-link-web
    build: ./poli-link-web
    ports:
      - 3000:3000
    volumes:
      - ./poli-link-web:/app
      - /app/node_modules
    tty: true
    working_dir: /app
    # command: sh -c "cd poli-link/ && yarn dev"

最初は
COPY ./package*.json ./
でエラーになったが、一度外してビルドして、2回目からはOK
最後のコマンドはコメントアウトしておいて、構築

$ docker exec -it poli-link-web sh
/app# yarn create nuxt-app poli-link

? Project name: poli-link
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git

/app # cd poli-link/
/app/poli-link # yarn dev

起動したけど、エラー

webpack-internal:///./.nuxt/client.js:230 TypeError: Cannot read property 'call' of undefined
    at __webpack_require__ (runtime.js:854)
    at fn (runtime.js:151)
    at eval (webpack-internal:///./node_modules/babel-loader/lib/index.js?!./node_modules/ts-loader/index.js?!./node_modules/vue-loader/lib/index.js?!./components/Keyboard.vue?vue&type=script&lang=ts&:29)
    at Module../node_modules/babel-loader/lib/index.js?!./node_modules/ts-loader/index.js?!./node_modules/vue-loader/lib/index.js?!./components/Keyboard.vue?vue&type=script&lang=ts& (:3000/_nuxt/0.js:59)
    at __webpack_require__ (runtime.js:854)
    at fn (runtime.js:151)
    at eval (webpack-internal:///./components/Keyboard.vue?vue&type=script&lang=ts&:2)
    at Module../components/Keyboard.vue?vue&type=script&lang=ts& (:3000/_nuxt/0.js:23)
    at __webpack_require__ (runtime.js:854)
    at fn (runtime.js:151)

再起動したらOK

一旦ストップし、docker-composeもdown

docker-compose.yml
最後の行のコメントを戻して再度up

それぞれ参照可能

http://localhost:8000/swagger/
http://localhost:8000/api/memo/
http://localhost:8000/admin/polilink_api/memo/
http://localhost:3000/

RepositoryFactory

ここで改めて、API呼び出しの構成を検討する

https://zenn.dev/hassan/articles/b7f76a56f59375
RepositoryFactory
serviceではなくapi/repostry
そこでcrudを定義するという意味で、やろうとしていることは似ていると思われる
https://qiita.com/lucaspoppy/items/a081beb8fdd7746a8c05

こんな構成か


│  ├ repositories
│  │  ├ repository.ts
              (対象ごとに作るのではなく) ※baseUrlの考え方は欲しい
│  ├ store
│  │  └ xxx.ts 
│  └ plugins
│     └ epository-factory.ts
│     
└ nuxt.config.js
    ($repositories: Repositories // 追加)

nuxt.config.js から nuxt.config.ts への変更

参考情報

https://toragramming.com/web/nuxtjs/nuxt-typescript-vscode-env-2019-09/

ただし、package.json の scriptsで、nuxt から nuxt-ts にするというのはエラーになるのでやらない
→エラーになるというよりも、

poli-link-web |  WARN  You're using Nuxt 2.15.3, which includes built-in TypeScript runtime support
poli-link-web | 
poli-link-web | 
poli-link-web |  WARN  You can safely use nuxt instead of nuxt-ts and remove @nuxt/typescript-runtime package

と出てる。これは不要と考えて戻す。

下記を参考に、layoutをtypescriptに変更

https://qiita.com/Aruneko/items/552fcd3ae5da4eb1b218#composition-api-による実装の例

ただし、createComponent は現状は defineComponent
ちょっと古いか

下記追加
poli-link-web/poli-link/repositories/repository.ts
poli-link-web/poli-link/plugins/repository-factory.ts

そして、nuxt.config.ts に

import { Repositories } from "@/plugins/repository-factory";

を追加しようとするところで、

{
	"resource": "/・・・/workspace/PoliLink/poli-link-web/poli-link/nuxt.config.ts",
	"owner": "typescript",
	"code": "2307",
	"severity": 8,
	"message": "Cannot find module '@/plugins/repository-factory' or its corresponding type declarations.",
	"source": "ts",
	"startLineNumber": 4,
	"startColumn": 30,
	"endLineNumber": 4,
	"endColumn": 60
}

となっている。

json:tsconfig.json

  "include": [
    "*.ts",
    "components/**/*.ts",
    "components/**/*.vue",
    "layouts/**/*.ts",
    "layouts/**/*.vue",
    "store/**/*.ts",
    "plugins/**/*.ts",
    "utils/**/*.ts",
    "pages/**/*.ts",
    "pages/**/*.vue"
  ],

この1行目を追加することで解消するが、今度は、

Could not find a declaration file for module 'vuetify/es5/util/colors'. '/・・・/workspace/PoliLink/poli-link-web/poli-link/node_modules/vuetify/es5/util/colors.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/vuetify` if it exists or add a new declaration (.d.ts) file containing `declare module 'vuetify/es5/util/colors';`

こちらを参考に

https://qiita.com/Nossa/items/3860e55551697bb46e38

tsconfig.jsonのcompilerOptions.typesに"vuetify"を追加

store を追加し、 pages から使用する

ところが、この部分のソースがちょっと理解できない

import { ClientResponse } from '@/types/model/client'

この、@/types/model がどこから来ているかわわからない・・・

こちらがよいか

https://qiita.com/lucaspoppy/items/a081beb8fdd7746a8c05
tsではない

こちらがより近い

https://tech.smartshopping.co.jp/nuxt_typescript_repository_pattern

そのものズバリの情報がないので自力で頑張る
公式のいつもの図

https://vuex.vuejs.org/ja/

ComponentからはActionを呼び出し、ActionがMutationをコミットする。
Actionは非同期処理ができる

しかし、このやり方

https://zenn.dev/hassan/articles/b7f76a56f59375
は素晴らしいと思うけど、ちょっと難しい・・・
もう少しべたに、個別のrepositoryを作る方法で試してみる
https://tech.smartshopping.co.jp/nuxt_typescript_repository_pattern

https://qiita.com/07JP27/items/0923cbe3b6435c19d761
を見ながら

vueのinterfaceを拡張
というのはどこでやるのだろう・・・

まずは、下の方をそのままやってみる

いい感じっぽいが、

Access to XMLHttpRequest at 'http://localhost:8000/api/memo' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

となるので、先にAPI側のCORS対応

こちらの通り

https://qiita.com/HIJIKI/items/ed4badee7ef37e30b957
$ docker exec -it poli-link-api /bin/bash
code# pip install django-cors-headers

でインストールして、poli-link-api/config/settings.py に追記
すると下記エラー

ModuleNotFoundError: No module named 'corsheaders'

pip install するだけではダメで、

poli-link-api/requirements.txt
django-cors-headers==3.7.0

を追加

今度は次のエラー

(corsheaders.E013) Origin 'localhost:3000' in CORS_ORIGIN_WHITELIST is missing scheme or netloc

正しくは下記

poli-link-api/config/settings.py
CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
)

※最後に/を付けるとエラーになるのでこれだけ

ところが、やはりCORSエラーになる

poli-link-api/config/settings.py
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',

という感じで、CorsMiddleware は、CommonMiddleware より前に入れる。
すでに、CommonMiddleware は入っていたので、注意
これでCORSエラーは消えた
意外と大変だった

ところが今度は、

GET http://localhost:8000/api/ 401 (Unauthorized)

解決は

poli-link-api/config/settings.py
REST_FRAMEWORK = {
        'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
        # 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
        'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),

これで、無事(?)Vue側のエラーになったので、API側は一旦終了

vueのエラーも、一旦使っていないファイルだったので、コメントアウトして、無事データ取得はできました。

https://qiita.com/07JP27/items/0923cbe3b6435c19d761
こちらの、最後の部分、componentを作っているようですが、pagesで書いてしまいました。
さらに、別のリスト用componentを作っているようですが、そこも省略。
最低限の動くもの、ということで、こんな感じです。
poli-link-web/poli-link/pages/memo2.vue
<template>
  <div class="container">
    <p>{{ memos }}</p>
  </div>
</template>

<script>
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
const MemoRepository = RepositoryFactory.get('memo')

export default {
  name: "name",
  // components: { }
  data() {
    return {
      isLoading: false,
      memos: [],
    }
  },
  created () {
    this.fetch()
  },
  methods: {
    async fetch () {
      this.isLoading = true
      const { data } = await MemoRepository.get()
      this.isLoading = false
      this.memos = data;
    }
  }
}
</script>

<style>

</style>

これをTypescriptに変更

参考情報

https://v3.vuejs.org/guide/composition-api-introduction.html#lifecycle-hook-registration-inside-setup
poli-link-web/poli-link/pages/memo2.vue
<template>
  <div class="container">
      <input v-model="state.memo" placeholder="メモを入力してください">
      <br>
      <li v-for="(memo, index) in state.memos" :key="index">
        {{ memo }}
      </li>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, onMounted } from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
const MemoRepository = RepositoryFactory.get('memo')
interface StateData {
  id: number
  title: string
  memo: string
  isLoading: boolean,
  memos: []
}

export default defineComponent({
  head: {
    title: "Memo"
  },
  setup(_props, context) {
    const state = reactive<StateData>({
      id: 0,
      title: 'title0',
      memo: 'memo0',
      isLoading: false,
      memos: []
    })

    const fetch = async () => {
      state.isLoading = true
      console.log('state.isLoading', state.isLoading)
      const { data } = await MemoRepository.get()
      state.isLoading = false
      console.log('state.isLoading', state.isLoading)
      console.log('data', data)
      state.memos = data
    }

    onMounted(fetch) 

    return {state}

  }
})


</script>

<style>

</style>

その後、jsになっていた部分をtsにしようとしていましたが、悪戦苦闘の結果断念。

悩みながら調べていたら、これに出会いました。

https://github.com/devJang/nuxt-realworld

store使わないのか、と思いましたが、こちら

https://zenn.dev/koudaiishigame/articles/810ce2d0ee8ade
の「Vuexとの使い分けについて」も参考にさせていただき、nuxt-realworld 方式で行くことにしました。

動くサンプルがあるのはやはり助かります。

とはいえ、もうしばらく悪戦苦闘は続きました。

poli-link-web/poli-link/nuxt.config.ts
  axios: {
    baseURL: 'http://localhost:8000/api/',
  },

このbaseURLがないと、「Failed to execute 'fetch' on 'Window': 1 argument required, but only 0 present.」エラー。

その後、apiからデータは取れてきているのに、戻り値として返せない、というエラーにずっとハマっていましたが、そもそも、APIの返し方が違うというオチでした。

Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object
articles: (20) [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}]
articlesCount: 500
__proto__: Object

というAPIからの戻りを前提に、

type ArticleListResponse = ResponseTypes<{
  articles: Article[]
  articlesCount: number
}>

という型定義がされていたわけですが、
今回のAPIの戻りは

Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Array(2)
0: {id: 1, title: "a", memo: "a"}
1: {id: 2, title: "b", memo: "bmemo"}
length: 2
__proto__: Array(0)

なので、

type MemoListResponse = ResponseTypes<Memo[]>

としました。
これにより、Repositoryパターン的な形で、APIからデータを取得できるようになりました。

スクラップにも目次がついて欲しいですね・・・

まだ、色々挫折の残骸が残っていますが、とりあえず、データ取得ができたということで、今度は、データの作成、削除をやりたいと思います。
その記録を残していきます。

その前に、

データ取得の処理の流れを整理

components を使うとして、今回参考にしている「devJang/nuxt-realworld」では、データ表示側はデータをpropsで受け取るようにしていますが、データもcomponents で取得するとして、

poli-link-web/poli-link/components/memo/MemoEdit.vue
    const {
      state: memoListState,
      getMemoList
    } = useMemoList()

    const fetchData = async (offset = 0) => {
      await getMemoList({ offset })
      console.log('fetchData')
    }
poli-link-web/poli-link/compositions/useMemoList.ts
  const getMemoList = async(payload: MemoListRequest = {}) => {
    const memos= await $repository.memo.getMemoList(payload)
poli-link-web/poli-link/api/memoRepository.ts
  getMemoList({
    limit = LIMIT_LIST_ITEM,
    offset = 0,
  }: MemoListRequest = {}): MemoListResponse {
    const defaultParam = {
    }

    return axios.$get('/memo/', {
      params: {...defaultParam, limit, offset},
    })
  },

という遡りになります。

データ更新処理の作成手順

先程の逆に行くことになるので、まずは、memoRepository.ts に、更新系API呼び出しを追加。
Request用の型定義が追加と合わせて。

※その際に、type Pick というものを使いますが、これは、
型 T の中から K に当てはまるプロパティのみを抜き取った新しい型を返します。
とのこと。なるほどねぇ

そして、最初に書いたものからちょっと訂正

RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to localhost:8000/api/memo/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.

というエラーが出るので、スラッシュを忘れないように。

poli-link-web/poli-link/api/memoRepository.ts
export type CreateMemoRequest = Pick<
  Memo,
  'title' | 'memo'
>
export type UpdateMemoPayload = Partial<CreateMemoRequest>
export type UpdateMemoRequest = {
  payload: UpdateMemoPayload
  slug: Memoid
}
・・・
export const memoRepository = (axios: NuxtAxiosInstance) => ({
・・・
  createMemo(payload: CreateMemoRequest): MemoResponse | CustomErrors {
    console.log('createMemo', payload)
    return axios.$post('/memo/', payload)
  },
  updateMemo(request: UpdateMemoRequest): MemoResponse | CustomErrors {
    return axios.$put(`/memo/${request.id}`, { article: request.payload })
  },
  deleteMemo(memoid: Memoid) {
    return axios.$delete(`/memo/${memoid}`)
  },

useMemoを追加
追加したら、

poli-link-web/poli-link/compositions/index.ts
import useMemoList from "./useMemoList";
import useMemo from "./useMemo";

export {
  useMemoList,
  useMemo,
}

vueから呼び出し

vuetifyの話が挟まるので一旦終了

画面は、こんな感じを作ります。

まずは、データ表示部分。

Vuetify の v-data-table を使います。

poli-link-web/poli-link/components/memo/MemoEdit.vue
      <v-data-table
        :headers="headers"
        :items="memoList"
        dense
      >
        <template v-slot:[`item.actions`]="{ item }">
          <v-icon
            small
            class="mr-2"
            @click="editItem(item)"
          >
            mdi-pencil
          </v-icon>
          <v-icon
            small
            @click="deleteItem(item)"
          >
            mdi-delete
          </v-icon>
        </template>
      </v-data-table>

上に書いたように、useMemoListからstate型のmemoListStateを取得して、
...toRefs(memoListState),
として、returnすることにより、stateに入っている、memoListが使える、という感じですね。
headersも必要なので、こんな感じで足しておきます。

  setup() {
    const headers = [
      {text: 'タイトル', value: 'title'},
      {text: 'メモ', value: 'memo'},
      {text: '', value: 'actions', sortable: false},
    ]

ここまでくるのも苦労しましたけど、データ追加部分はもっと大変でした。

途中の悪戦苦闘はかなり省略

      <v-form lazy-validation>
        <v-row>
          <v-col
            cols="12"
            sm="4"
          >
            <v-text-field
              label="タイトル"
              dense
              outlined
              clearable
              v-model="form.title"
              >
            </v-text-field>
          </v-col>
          <v-col
            cols="12"
            sm="4"
          >
          <v-text-field
            label="メモ内容"
            dense
            outlined
            clearable
            v-model="form.memo"
            >
          </v-text-field>
          </v-col>
          <v-col
            cols="12"
            sm="4"
          >
            <v-btn color="primary" @click="handleCreateMemo">新規</v-btn>
          </v-col>
        </v-row>
      </v-form>

refの参照のしかたとか色々調べましたが、一旦省略。
v-model="form.title"
の部分に集中します。

クリックのイベントの部分は、こうならないといけない。

    const handleCreateMemo = async() => {
      try {
        const newMemo = await createMemo(createMemoState) //こことformを結びつける

        if (!newMemo) {
          return
        }

        console.log('Success')
      } catch (error) {
        console.log('error')
        // setError(error)
      }
    }

createMemo はuseMemoにあるので

    const { createMemo } = useMemo()

この引数は、CreateMemoRequest型
それと一致するのが、useMemo で定義している、CreateState型
そして、そちらで

  const createState = reactive<CreateState>(initCreateState)

がreturnされている。ここがreactiveになっているので、scriptと、templateが連動する。

なので、

    const {
      createState: createMemoState,
    } = useMemo()

として、createMemoStateをapi呼び出し処理への引数とする。
これをreturnするのも忘れず。

最終的にはこんな感じ。

  setup() {

    const headers = [
      {text: 'タイトル', value: 'title'},
      {text: 'メモ', value: 'memo'},
      {text: '', value: 'actions', sortable: false},
    ]

    const {
      state: memoListState,
      getMemoList,
    } = useMemoList()

    const {
      createState: createMemoState,
    } = useMemo()


    const { createMemo } = useMemo()

    const handleCreateMemo = async() => {
      try {
        console.log('createMemoState', createMemoState)
        const newMemo = await createMemo(createMemoState) //こことformを結びつける

        if (!newMemo) {
          return
        }

        console.log('Success')
      } catch (error) {
        console.log('error')
        // setError(error)
      }
    }

    const fetchData = async (offset = 0) => {
      await getMemoList({ offset })
      console.log('fetchData')
    }

    const { fetchState } = useFetch(() => fetchData())
    // console.log('fetchState', fetchState)

    return {
      headers,
      // state,
      fetchState,
      fetchData,
      ...toRefs(memoListState),
      form: createMemoState,
      handleCreateMemo,
    }
  }

まずは、削除ダイアログを出す。

      <v-dialog v-model="dialogDelete" max-width="500px">
        <v-card>
          <v-card-title class="headline">削除しますか?</v-card-title>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-spacer></v-spacer>
          </v-card-actions>
        </v-card>
      </v-dialog>

dialogDelete は元々Dataとして公開していた部分なので、reactiveなstateの中に定義して、return する。
削除アイコンから呼び出しているdeleteItemも。

  setup() {
    ・・・
    const state = reactive<Data>({
      dialogDelete: false,
      editedIndex: -1,
      editedItem: {
        title: '',
        meeo: '',
      },
    })
    ・・・

    const deleteItem = (item: Memo) =>{
      state.editedIndex = item.id
      state.editedItem = item
      console.log(item)
      state.dialogDelete = true
    }
    ・・・

    return {
      headers,
      ...toRefs(state),
      ・・・
      deleteItem,
    }

閉じるボタン

            <v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>

このcloseDelete

      closeDelete () {
        this.dialogDelete = false
        this.$nextTick(() => {
          this.editedItem = Object.assign({}, this.defaultItem)
          this.editedIndex = -1
        })
      },

を、Composition API でどう書くか。
参考

https://qiita.com/engineerYodaka/items/48ba7b7a560e75dcc447
import { nextTick } from 'vue'


TypeError: Object(...) is not a function
となった。
方法1はOK。

  setup(_, { root })  {
    ・・・
    const closeDelete = () =>{
      state.dialogDelete = false
      root.$nextTick(() => {
        state.editedItem = Object.assign({}, state.defaultItem)
        state.editedIndex = -1
      })
    }

削除のAPI呼び出し

      <v-dialog v-model="dialogDelete" max-width="500px">
        <v-card>
          <v-card-title class="headline">削除しますか?</v-card-title>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
            <v-btn color="blue darken-1" text @click="deleteItemConfirm">OK</v-btn>
            <v-spacer></v-spacer>
          </v-card-actions>
        </v-card>
      </v-dialog>
    const state = reactive<Data>({
      dialogDelete: false,
      editedIndex: -1,
      editedItem: {
        id: -1,
        title: '',
        meeo: '',
      },
      defaultItem: {
        id: -1,
        title: '',
        meeo: '',
      },
    })
    ・・・
    const {
      createState: createMemoState,
      createMemo,
      deleteMemo,
    } = useMemo()
    ・・・
    const deleteItemConfirm = async() =>{
      console.log('deleteItemConfirm', state.editedItem)
      const memoid:number = state.editedItem.id
      deleteMemo(memoid)
      closeDelete()
    }

これでreturnに追加することで、削除はできた。

ただ、画面描画が更新されない。
別のページに行って戻れば更新されてる。

お手本である、nuxt-realworld 方式で、compositions/useArticleList.ts と compositions/useArticleSlug.ts が分かれているのでそれを真似して、poli-link-web/poli-link/compositions/useMemo.ts と、poli-link-web/poli-link/compositions/useMemoList.ts を作っていたのですが、情報はuseMemoListの中で定義されている memoList: Memo[] を参照しているわけなので、useMemoで更新した結果をuseMemoListのmemoListに伝えないといけないということになる。
nuxt-realworldでも、コメントについてはcompositions/useComment.ts一つで、

  const deleteComment = async ({ slug, id }: DeleteCommentRequest) => {
    await $repository.comment.deleteComment({ slug, id })
    await getCommentList(slug)
  }

としているので、それを参考に、useMemo.ts にまとめて、useMemoList.ts は廃止しました。
現時点の全体像

poli-link-web/poli-link/compositions/useMemo.ts
import { reactive, useContext } from '@nuxtjs/composition-api'
import { MemoListRequest, CreateMemoRequest, UpdateMemoRequest } from "@/api/memoRepository";
import { Memo } from "@/types";
import MemoKey from '~/store/memo-key';

type MemoPayload = Required<CreateMemoRequest>
type CreateState = MemoPayload
type State = {
  memo: Memo
  memoList: Memo[]
  memoCount: number
}

const initCreateState = {
    title: '',
    memo: '',
}
const initState = {
  memo: {
    id: 0,
    title: '',
    memo: '',
  },
}

export default function useMemo() {
  const { $repository } = useContext()

  const createState = reactive<CreateState>(initCreateState)
  // const state = reactive<State>(initState)
  const state = reactive<State>({
    memo: {
      id: 0,
      title: '',
      memo: '',
    },
    memoList: [],
    memoCount: 0,
  })
  const getMemo = async (memoid: Memo['id']) => {
    const { memo } = await $repository.memo.getMemo(memoid)

    state.memo = memo
  }

  const getMemoList = async(payload: MemoListRequest = {}) => {
    const memos= await $repository.memo.getMemoList(payload)

    console.log('memos', memos)
    
    state.memoList = memos
    state.memoCount = memos.length
  }

  const createMemo = async (payload: CreateMemoRequest) => {
    const response = await $repository.memo.createMemo(payload)

    if ('memo' in response) {
      await getMemoList()
      return response.memo
    }

    return false
  }

  const updateMemo = async (payload: UpdateMemoRequest) => {
    const response = await $repository.memo.updateMemo(payload)

    if ('memo' in response) {
      return response.memo
    }

    return false
  }

  const deleteMemo = async (memoid: Memo['id']) => {
    await $repository.memo.deleteMemo(memoid)
    await getMemoList()
  }
  return {
    createState,
    state,
    getMemo,
    getMemoList,
    createMemo,
    updateMemo,
    deleteMemo,
  }
}
poli-link-web/poli-link/components/memo/MemoEdit.vue
<template>
  <v-card>
    <v-card-title> メモ の管理 </v-card-title>
    <v-card-subtitle>
      <v-form lazy-validation>
        <v-row>
          <v-col
            cols="12"
            sm="4"
          >
            <v-text-field
              label="タイトル"
              dense
              outlined
              clearable
              v-model="form.title"
              >
            </v-text-field>
          </v-col>
          <v-col
            cols="12"
            sm="4"
          >
          <v-text-field
            label="メモ内容"
            dense
            outlined
            clearable
            v-model="form.memo"
            >
          </v-text-field>
          </v-col>
          <v-col
            cols="12"
            sm="4"
          >
            <v-btn color="primary" @click="handleCreateMemo">新規</v-btn>
          </v-col>
        </v-row>
      </v-form>
    </v-card-subtitle>

    <v-card-text>
        <v-data-table
          :headers="headers"
          :items="memoList"
          dense
        >
          <template v-slot:[`item.actions`]="{ item }">
            <v-icon
              small
              class="mr-2"
              @click="editItem(item)"
            >
              mdi-pencil
            </v-icon>
            <v-icon
              small
              @click="deleteItem(item)"
            >
              mdi-delete
            </v-icon>
          </template>
        </v-data-table>

        <v-dialog v-model="dialogDelete" max-width="500px">
          <v-card>
            <v-card-title class="headline">削除しますか?</v-card-title>
            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
              <v-btn color="blue darken-1" text @click="deleteItemConfirm">OK</v-btn>
              <v-spacer></v-spacer>
            </v-card-actions>
          </v-card>
        </v-dialog>

    </v-card-text>

  </v-card>
</template>

<script lang="ts">
import { PropType } from 'vue'
import {
  Data,
  defineComponent,
  reactive,
  ref,
  toRef,
  toRefs,
  useFetch,
  onMounted,
} from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
import { useMemo } from "@/compositions";

const MemoRepository = RepositoryFactory.get('memo')
interface StateData {
  id: number
  title: string
  memo: string
  isLoading: boolean,
  memos: []
}


import { Memo } from '@/types'

export default defineComponent({
  name: 'MemoEdit',
  setup(_, { root })  {

    const headers = [
      {text: 'タイトル', value: 'title'},
      {text: 'メモ', value: 'memo'},
      {text: '', value: 'actions', sortable: false},
    ]

    const state = reactive<Data>({
      dialogDelete: false,
      editedIndex: -1,
      editedItem: {
        id: -1,
        title: '',
        meeo: '',
      },
      defaultItem: {
        id: -1,
        title: '',
        meeo: '',
      },
    })

    // const {
    //   state: memoListState,
    //   getMemoList,
    // } = useMemoList()

    const {
      state: memoState,
      createState: createMemoState,
      createMemo,
      deleteMemo,
      getMemoList,
    } = useMemo()

    const handleCreateMemo = async() => {
      try {
        console.log('createMemoState', createMemoState)
        const newMemo = await createMemo(createMemoState) //こことformを結びつける

        if (!newMemo) {
          return
        }

        console.log('Success')
      } catch (error) {
        console.log('error')
        // setError(error)
      }
    }

    const deleteItem = (item: Memo) =>{
      state.editedIndex = item.id
      state.editedItem = item
      console.log('item', state.editedItem)
      state.dialogDelete = true
    }

    const closeDelete = () =>{
      state.dialogDelete = false
      root.$nextTick(() => {
        state.editedItem = Object.assign({}, state.defaultItem)
        state.editedIndex = -1
      })
    }

    const deleteItemConfirm = async() =>{
      console.log('deleteItemConfirm', state.editedItem)
      const memoid:number = state.editedItem.id
      deleteMemo(memoid)
      // fetchData()
      closeDelete()
    }

    const fetchData = async (offset = 0) => {
      await getMemoList({ offset })
      console.log('fetchData')
    }

    const { fetchState } = useFetch(() => fetchData())
    // console.log('fetchState', fetchState)

    return {
      headers,
      ...toRefs(state),
      fetchState,
      fetchData,
      ...toRefs(memoState),
      form: createMemoState,
      handleCreateMemo,
      deleteItem,
      closeDelete,
      deleteItemConfirm,
    }
  }
  

  // props: {
  //   memoList: {
  //     type: Array as () => PropType<Memo[]>,
  //     required: true,
  //   },
  // },


})
</script>

<style>

</style>

では、最後Update

今参考にしているのは、Vuetifyのデータテーブルのサンプル

https://vuetifyjs.com/en/components/data-tables/#crud-actions

これだと、Update処理も、ダイアログを出してその中でやることにしていますが、より汎用的なのは、更新画面に遷移する形だと思うので、そちらにチャレンジ。

それに伴い、pages構成も変更。

git mv poli-link-web/poli-link/pages/memo.vue poli-link-web/poli-link/pages/memo/index.vue

編集用ページ作成
MemoEditFormというcomponentsを使うことにしておく

poli-link-web/poli-link/pages/memo/_slug/edit.vue
<template>
  <div>
    memo edit
    <memo-edit-form />

  </div>
</template>

<script>
import MemoList from '~/components/memo/MemoList.vue';
import MemoEditForm from '~/components/memo/MemoEditForm.vue';

export default {
  components: { MemoList, MemoEditForm },

}
</script>

<style>

</style>

componentsはまずは、vuetifyのサンプルをそのまま貼り付けておく

poli-link-web/poli-link/components/memo/MemoEditForm.vue
<template>
  <v-form
    ref="form"
    v-model="valid"
    lazy-validation
  >
    <v-text-field
      v-model="name"
      :counter="10"
      :rules="nameRules"
      label="Name"
      required
    ></v-text-field>

    <v-text-field
      v-model="email"
      :rules="emailRules"
      label="E-mail"
      required
    ></v-text-field>

    <v-select
      v-model="select"
      :items="items"
      :rules="[v => !!v || 'Item is required']"
      label="Item"
      required
    ></v-select>

    <v-checkbox
      v-model="checkbox"
      :rules="[v => !!v || 'You must agree to continue!']"
      label="Do you agree?"
      required
    ></v-checkbox>

    <v-btn
      :disabled="!valid"
      color="success"
      class="mr-4"
      @click="validate"
    >
      Validate
    </v-btn>

    <v-btn
      color="error"
      class="mr-4"
      @click="reset"
    >
      Reset Form
    </v-btn>

    <v-btn
      color="warning"
      @click="resetValidation"
    >
      Reset Validation
    </v-btn>
  </v-form>
</template>

<script>
  export default {
    data: () => ({
      valid: true,
      name: '',
      nameRules: [
        v => !!v || 'Name is required',
        v => (v && v.length <= 10) || 'Name must be less than 10 characters',
      ],
      email: '',
      emailRules: [
        v => !!v || 'E-mail is required',
        v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
      ],
      select: null,
      items: [
        'Item 1',
        'Item 2',
        'Item 3',
        'Item 4',
      ],
      checkbox: false,
    }),

    methods: {
      validate () {
        this.$refs.form.validate()
      },
      reset () {
        this.$refs.form.reset()
      },
      resetValidation () {
        this.$refs.form.resetValidation()
      },
    },
  }
</script>

<style>

</style>

そして、編集ボタンを押したときに、遷移するようにする

poli-link-web/poli-link/components/memo/MemoEdit.vue
            <v-icon
              small
              @click="$router.push(`/memo/${item.id}/edit`)"
            >
              mdi-pencil
            </v-icon>

これでとりあえず遷移の動きができたので、poli-link-web/poli-link/components/memo/MemoEditForm.vue を作っていく

slugの取得は、useContextのparams

poli-link-web/poli-link/pages/memo/_slug/edit.vue
import {
  defineComponent,
  useContext,
} from '@nuxtjs/composition-api'
import MemoList from '~/components/memo/MemoList.vue';
import MemoEditForm from '~/components/memo/MemoEditForm.vue';

export default defineComponent({
  setup() {
    const { app, params, query } = useContext()
    const { slug } = params.value

    console.log('slug', slug)

    return {

    }
  },
})

データ参照して更新は同じようにできるだろうと思ったら甘かった

データ参照はまあ良いです

    const {
      state: memoState,
      updateMemo,
      getMemo,
    } = useMemo()

    const fetchData = async () => {
      await getMemo(props.memoId)
    }

    const { fetchState } = useFetch(() => fetchData())

    return {
      form: memoState,
    }

ただ、ここで取得されている型が混乱していて、更新の処理を書く際も大変でした。
ちゃんと解消できていないので、動作はするけど、エラーが表示されている状態です。

poli-link-web/poli-link/compositions/useMemo.ts
  const state = reactive<State>({
    memoData: {
      id: 0,
      title: '',
      memo: '',
    },
    memoList: [],
    memoCount: 0,
  })

  const getMemo = async (memoid: Memo['id']) => {
    const response = await $repository.memo.getMemo(memoid)
    console.log('getMemo', response)

    state.memoData = response //型が合っていないと言われてしまうがこれで返る
  }

ここでresponseに返ってきているのが

{id: 24, title: "title", memo: "memoa"}

これでいいじゃんと思うのですが、state.memoData に入れるところで、型が合っていないと言われています。

ちょっとこれはそのままにして、更新処理へ

template

poli-link-web/poli-link/components/memo/MemoEditForm.vue
<template>
  <v-form
    lazy-validation
  >
    {{ form.memoData.id }}
    <v-text-field
      v-model="form.memoData.title"
      label="タイトル"
      required
    ></v-text-field>

    <v-text-field
      v-model="form.memoData.memo"
      label="メモ"
      required
    ></v-text-field>
    <v-btn
      color="success"
      class="mr-4"
      @click="handleUpdateMemo"
    >
      更新
    </v-btn>
    <v-btn @click="$router.push('/memo/')">戻る</v-btn>
  </v-form>
</template>

これに向けて、formを返す

import {
  Data,
  defineComponent,
  reactive,
  ref,
  toRef,
  toRefs,
  useFetch,
  onMounted,
} from "@nuxtjs/composition-api"
import { RepositoryFactory } from "~/repositories/RepositoryFactory";
import { useMemo } from "@/compositions";
import { Memo } from '@/types'

const MemoRepository = RepositoryFactory.get('memo')

export default defineComponent({
  name: 'MemoEditForm',
  props: {
    memoId: {
      type: Number,
      required: true,
    },
  },
  setup(props, { root })  {
    const {
      state: memoState,
      updateMemo,
      getMemo,
    } = useMemo()

    const handleUpdateMemo = async() => {
      try {
        console.log('createMemoState', memoState)
        await updateMemo({id: memoState.memoData.id, payload: {"title": memoState.memoData.title, "memo": memoState.memoData.memo}}) 

        return

      } catch (error) {
        console.log('error')
        // setError(error)
      }
    }

    const fetchData = async () => {
      await getMemo(props.memoId)
    }

    const { fetchState } = useFetch(() => fetchData())

    return {
      form: memoState,
      handleUpdateMemo,
    }
  }

})

ちょっと時間開けたら忘れてしまったので駆け足
updateMemoのところ

poli-link-web/poli-link/compositions/useMemo.ts
  const updateMemo = async (payload: UpdateMemoRequest) => {
    const response = await $repository.memo.updateMemo(payload)

    if ('memoData' in response) {
      return response.memoData
    }

    return false
  }

ここからmemoRepositoryのupdateMemoが呼ばれる

poli-link-web/poli-link/api/memoRepository.ts
  updateMemo(request: UpdateMemoRequest): MemoResponse | CustomErrors {
    return axios.$put(`/memo/${request.id}/`, request.payload )
  },

最初、APIから

{
  "detail": "JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"
}

というエラーが返ってきていて、そうか、pythonは厳しいのか・・・、createは大丈夫なのに・・・
とか思いながら、JSON.stringify(request.payload) とかやってみましたが、そういう問題ではなく
やはり、APIの構造が違う

    return axios.$put(`/memo/${request.id}/`, { data: request.payload })

から、上記のように、dataに入れない形にしたらOK。

これで、無事CRUDができました!

ここから、本来作りたい部分の画面デザインを考えようか、テストを書こうか、と悩みましたが、さすがに長すぎるので、一旦終了。

次の編に進む、ということにします。

ログインするとコメントできます