4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

vue-apolloに付属するデモチャットアプリ「ApolloChat」のGraphQLバックエンドをPostGraphileに置き換えてみた

Last updated at Posted at 2019-06-19

現状、コードの説明が足りないので、後日追記します。

この記事で行うこと

vue-apolloに付属するデモチャットアプリ「ApolloChat」のGraphQLサーバーを、PostGraphileで実装して置き換え、PostgreSQLにデータ永続化するようにします。
以後、vue-apolloに付属するApolloChatを「オリジナルApolloChat」、PostGraphileに置き換えるApolloChatを「PostGraphile版ApolloChat」と呼びます。

PostGraphile版ApolloChatのソースコード

https://github.com/kanedaq/vue-apollo から入手できます。動かすには、以下を実行してWebサーバーを立ち上げ、ブラウザから「 http://localhost:8080/demo/ 」にアクセスします。

git clone https://github.com/kanedaq/vue-apollo
cd vue-apollo/tests/demo/
yarn install
yarn serve

実装の手抜き(デモにありがちな)

  • httpsで通信すべきところもhttpで通信しています。
  • 本記事ではJWTをローカルストレージに保存していますが、本番環境ではなさらないようお願いします。HTML5のLocal Storageを使ってはいけない(翻訳) (2019-10-10追記)
  • ログアウト処理はサーバー側は何もせずにtrueを返すだけにし、フロント側でブラウザのローカルストレージのJWTを削除するに留めました。きっちり実装したい場合は、以下のサイト等をご参照ください。
  • ログイン中に"jwt expired"のために動作が怪しくなったら、ログアウトボタンを押して強制的にJWTを削除してください。
  • ログイン画面で"jwt expired"のためにログインできなくなったら、ブラウザのローカルストレージに残っているJWT(キー:postgraphile-demo-token)を手動で削除してください。
  • その他、PostGraphileへの移植に際して、実装の抜け漏れがあるかと思います。
  • 私(C++おじさん)はまだJavaScriptもVue.jsも経験が浅いので、変な箇所がありましたら優しくご指摘ください。

参考ページ(感謝します)

GraphiQL で Header が指定できない対応策
Vue:Invalid Host Header
Vue.jsプロジェクトをサブディレクトリで配信するときの注意点
Vue CLI3のDevServerでWebSocketを使う
nginx(1.3.13)でWebSocketのプロキシを試した
Node.jsでlog4jsを使ってログを出力する
util.inspect 便利

vue-apolloを入手し、オリジナルApolloChatを動かしてみる

GraphQLバックエンドをPostGraphileに移植する前に、まずオリジナルApolloChatを動かしてみます。
作業は ~/work で行うことにします(私はMacを使用しました)。

vue-apolloを入手します。

cd ~/work
git clone https://github.com/Akryum/vue-apollo
cd vue-apollo

ここで「git log」を実行すると、以下のように表示されました。

$ git log
commit 9419ffd03d5c0942ac5954572920ffa2281f1851 (HEAD -> master, origin/master, origin/HEAD)
Author: Darryl Hein <dhein@xmmedia.com>
Date:   Tue May 28 11:38:41 2019 -0600

    docs: Minor spelling correction (#635)
(後略)

オリジナルApolloChatのディレクトリに移動し、必要なパッケージをインストールします。

cd ~/work/vue-apollo/tests/demo
yarn install

早速オリジナルApolloChatを動かしてみます。起動方法を調べるには、

more package.json

以下のように表示されました。

{
  "name": "demo",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "apollo:dev": "vue-cli-service apollo:watch",
    "apollo:run": "vue-cli-service apollo:run",
    "test:e2e:dev": "start-server-and-test apollo:dev http://localhost:4000/.well-known/apollo/server-health test:e2e:dev:client",
    "test:e2e:dev:client": "vue-cli-service test:e2e --mode development",
    "test:e2e": "start-server-and-test apollo:run http://localhost:4000/.well-known/apollo/server-health test:e2e:client",
    "test:e2e:client": "vue-cli-service test:e2e --mode production --headless",
    "test": "yarn run lint --no-fix && yarn run test:e2e"
  },

オリジナルApolloChatの起動コマンドは、
・GraphQLサーバー:yarn apollo:dev または yarn apollo:run
・Webサーバー(Vue.jsアプリ配信):yarn serve
で良さそうです。

まず「yarn apollo:dev」を実行してGraphQLサーバーを起動すると、以下のように表示されました。

warning ../../../package.json: No license field
$ vue-cli-service apollo:watch
warning ../../../package.json: No license field
$ vue-cli-service apollo:run --delay
Using default PubSub implementation for subscriptions.
You should provide a different implementation in production (for example with Redis) by exporting it in 'apollo-server/pubsub.js'.
✔️  GraphQL Server is running on http://localhost:4000/graphql
✔️  Type rs to restart the server

表示に従い、ブラウザで「 http://localhost:4000/graphql 」にアクセスすると、公式GraphiQLと似た画面が立ち上がります。本記事では省略しますが、「 ~/work/vue-apollo/tests/demo/src/graphql/ 」配下のgqlファイルに書かれているGraphQL問い合わせを試すことができます。画面の左下で「HTTP HEADERS」を入力できるので、ログイン処理で返ってきたJWTをHEADERSに設定すれば、ログインが必要な問い合わせを試すこともできますね。

引き続き、GraphQLサーバーを立ち上げたままの状態で、別コンソールでWebサーバーも起動します。

cd ~/work/vue-apollo/tests/demo
yarn serve

フロントエンドがコンパイルされた後、以下のように表示されました。

 DONE  Compiled successfully in 7780ms                                  17:51:01

 
  App running at:
  - Local:   http://localhost:8080/ 
  - Network: unavailable

  Note that the development build is not optimized.
  To create a production build, run yarn build.

表示に従い、ブラウザで「 http://localhost:8080/ 」にアクセスすると、オリジナルApolloChatが立ち上がります。
まずログイン画面が現れます。

スクリーンショット 2019-06-13 9.55.48.png

アカウント作成画面。

スクリーンショット 2019-06-13 9.56.01.png

アカウントを作成し、ログインした直後のウェルカム画面。

スクリーンショット 2019-06-13 9.56.23.png

generalチャンネルに入って、「こんにちは世界」と送信しようとしている画面。

スクリーンショット 2019-06-13 9.58.08.png

「こんにちは世界」と送信した直後の画面。

スクリーンショット 2019-06-13 9.58.16.png

いろいろ動かしてみると、オリジナルApolloChatのGraphQLサーバーはデータを永続化せずにメモリ上に保存している様子で、GraphQLサーバーを落とすと作成したアカウントやチャット内容が失われることがわかります。

バックエンド実装

それでは、オリジナルApolloChatのGraphQLサーバーをPostGraphileに移植する作業を開始します。データはPostgreSQLに永続化されます。

環境構築&バックエンド実装(その1):PostgreSQL関連まで

以前書いた記事「PostgreSQLを操作するAPIを提供する、PostgRESTとPostGraphileを両方試してみた」の「環境構築その1」と全く同じなので、記事は割愛します。
この環境設定では、Laradockのworkspaceの「/var/www」がホスト側の「~/work」にマウントされます。

環境構築&バックエンド実装(その2):DB作成

ER図

オリジナルApolloChatを動かしたり、~/work/vue-apollo/tests/demo/apollo-server/schema.graphql を見ながら、ER図を描いてみました。

er.png

4つのテーブルのうち、ログインユーザーに見せたい以下の3つは、PostgreSQLでapollo_demoスキーマに格納することにします。

  • channels
  • messages
  • users

ユーザに見せたくない以下のテーブルは、PostgreSQLでapollo_demo_privateスキーマに格納することにします。

  • user_accounts

PostgreSQLの機能であるスキーマは、名前空間のようなもので、見せたいオブジェクトと見せたくないオブジェクトを分けるセキュリティー用途に使用することができます。

実装作業

混乱を避けるため、オリジナルのGraphQLサーバーは削除し、代わりにこれから開発するバックエンドのファイルの置き場所を作成します。

cd ~/work/vue-apollo/tests/demo/
rm -rf apollo-server
mkdir postgraphile-server

Laradockのworkspaceに入リます。

cd ~/work/laradock
docker-compose exec --user=laradock workspace bash

更にpsqlに入リます。defaultユーザのパスワード: secret

psql -h postgres -U default

PostGraphile版ApolloChat用DBを作成し、接続できることを確認します。
DB名をdemodbとしましたが、名前は何でも良いです。

create database demodb;
\connect demodb

「 \connect demodb 」し忘れると、別のデータベース内にスキーマを作成することになりますのでご注意ください。

引き続き以下のSQLを実行して、スキーマとデータを作成します(内容は後ほどご説明します)。
このSQLは「 ~/work/vue-apollo/tests/demo/postgraphile-server/schema_and_data.sql 」に保存しておきます。
なお、JWTの期限を発行後60分にしてあります。変更したい場合は、「60 minutes」の部分を書き換えてください。

~/work/vue-apollo/tests/demo/postgraphile-server/schema_and_data.sql
begin;

create role apollo_demo_postgraphile login password 'xyz';

create role apollo_demo_anonymous;
grant apollo_demo_anonymous to apollo_demo_postgraphile;

create role apollo_demo_login_user;
grant apollo_demo_login_user to apollo_demo_postgraphile;

create schema apollo_demo;
create schema apollo_demo_private;

create table apollo_demo.users (
  id               integer primary key generated by default as identity,
  nickname         text not null,
  email            text not null unique check (email ~* '^.+@.+\..+$')
);

comment on table apollo_demo.users is 'A user of the chat.';
comment on column apollo_demo.users.id is 'The primary unique identifier for the user.';
comment on column apollo_demo.users.nickname is 'The user’s nickname.';
comment on column apollo_demo.users.email is 'The email address of the user.';

create table apollo_demo.channels (
  id               text primary key,
  name             text not null
);

comment on table apollo_demo.channels is 'A channel of the chat.';
comment on column apollo_demo.channels.id is 'The primary key for the channel.';
comment on column apollo_demo.channels.name is 'The channel’s name.';

create table apollo_demo.messages (
  id               integer primary key generated by default as identity,
  channel_id       text not null references apollo_demo.channels(id),
  user_id          integer not null references apollo_demo.users(id),
  content          text not null,
  date_added       timestamp not null default current_timestamp,
  date_updated     timestamp
) ;

comment on table apollo_demo.messages is 'A message written by a user.';
comment on column apollo_demo.messages.id is 'The primary key for the message.'; 
comment on column apollo_demo.messages.channel_id is 'The id of the channel.';
comment on column apollo_demo.messages.user_id is 'The id of the user.';
comment on column apollo_demo.messages.content is 'The content this has been posted in.';
comment on column apollo_demo.messages.date_added is 'The time this message was added.';
comment on column apollo_demo.messages.date_updated is 'The time this message was updated';

alter default privileges revoke execute on functions from public;

create function apollo_demo_private.set_date_updated() returns trigger as $$
begin
  new.date_updated := current_timestamp;
  return new;
end;
$$ language plpgsql;

create trigger messages_date_updated before update
  on apollo_demo.messages
  for each row
  execute procedure apollo_demo_private.set_date_updated();

create table apollo_demo_private.user_accounts (
  user_id          integer primary key references apollo_demo.users(id) on delete cascade,
  password_hash    text not null
) ;

comment on table apollo_demo_private.user_accounts is 'Private information about a user’s account.';
comment on column apollo_demo_private.user_accounts.user_id is 'The id of the User associated with this account.';
comment on column apollo_demo_private.user_accounts.password_hash is 'An opaque hash of the user password.';

create extension if not exists "pgcrypto";

create function apollo_demo.user_register(
  nickname text,
  email text,
  password text
) returns apollo_demo.users as $$
declare
  usr apollo_demo.users;
begin
  insert into apollo_demo.users (nickname, email) values
    (user_register.nickname, user_register.email)
    returning * into usr;

  insert into apollo_demo_private.user_accounts (user_id, password_hash) values
    (usr.id, crypt(user_register.password, gen_salt('bf')));

  return usr;
end;
$$ language plpgsql strict security definer;

comment on function apollo_demo.user_register(text, text, text) is 'Registers a single user and creates an account in our chat.';

create type apollo_demo.jwt_token as (
  role text,
  user_id integer,
  exp integer
);

create type apollo_demo.usr_and_token as (
  usr apollo_demo.users,
  token apollo_demo.jwt_token
);

create function apollo_demo.user_login(
  email text,
  password text
) returns apollo_demo.usr_and_token as $$
  select ((users.*)::apollo_demo.users, ('apollo_demo_login_user', users.id, extract(epoch from (now() + interval '60 minutes')))::apollo_demo.jwt_token)::apollo_demo.usr_and_token
    from apollo_demo.users
      inner join apollo_demo_private.user_accounts
      on users.id = user_accounts.user_id
    where 
      users.email = user_login.email
      and user_accounts.password_hash = crypt(user_login.password, user_accounts.password_hash);
$$ language sql strict security definer;

comment on function apollo_demo.user_login(text, text) is 'Creates a JWT token that will securely identify a user and give them certain permissions.';

create function apollo_demo.user_current() returns apollo_demo.users as $$
  select *
  from apollo_demo.users
  where id = current_setting('jwt.claims.user_id', true)::integer
$$ language sql stable;

comment on function apollo_demo.user_current() is 'Gets the user who was identified by our JWT.';

create function apollo_demo.change_password(
  current_password text,
  new_password text
) 
returns boolean as $$
declare
  usr_current apollo_demo.users;
begin
  usr_current := apollo_demo.user_current();
  if exists (select 1 from apollo_demo_private.user_accounts where user_accounts.user_id = usr_current.id and user_accounts.password_hash = crypt(change_password.current_password, usr_current.password_hash)) 
  then
    update apollo_demo_private.user_accounts set password_hash = crypt(change_password.new_password, gen_salt('bf'))
      where user_accounts.user_id = usr_current.id; 
    return true;
  else 
    return false;
  end if;
end;
$$ language plpgsql strict security definer;

create function apollo_demo.user_logout(
) returns boolean as $$
begin
  return true;
end;
$$ language plpgsql strict security definer;

create function apollo_demo.message_add(
  channel_id text,
  content text
) returns apollo_demo.messages as $$
declare
  message apollo_demo.messages;
begin
  INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
    (message_add.channel_id, current_setting('jwt.claims.user_id', true)::integer, message_add.content)
    RETURNING messages.* into message;
  return message;
end;
$$ language plpgsql strict security definer;

CREATE SEQUENCE mock_message_seq
    INCREMENT BY 1
    START WITH 1
;

create function apollo_demo.mock_message_send(
) returns boolean as $$
begin
  INSERT INTO apollo_demo.messages (channel_id, user_id, content)
    SELECT 'general', 0, 'How are you doing? ' || nextval('mock_message_seq');
  return true;
end;
$$ language plpgsql strict security definer;

grant usage on schema apollo_demo to apollo_demo_anonymous, apollo_demo_login_user;

grant select on table apollo_demo.users to apollo_demo_login_user;
grant update, delete on table apollo_demo.users to apollo_demo_login_user;

grant select on table apollo_demo.channels to apollo_demo_login_user;
grant insert, update, delete on table apollo_demo.channels to apollo_demo_login_user;

grant select on table apollo_demo.messages to apollo_demo_login_user;
grant insert, update, delete on table apollo_demo.messages to apollo_demo_login_user;

grant execute on function apollo_demo.user_register(text, text, text) to apollo_demo_anonymous;
grant execute on function apollo_demo.user_login(text, text) to apollo_demo_anonymous;
grant execute on function apollo_demo.user_current() to apollo_demo_login_user;
grant execute on function apollo_demo.change_password(text, text) to apollo_demo_login_user;
grant execute on function apollo_demo.user_logout() to apollo_demo_anonymous, apollo_demo_login_user;
grant execute on function apollo_demo.message_add(text, text) to apollo_demo_login_user;
grant execute on function apollo_demo.mock_message_send() to apollo_demo_login_user;

alter table apollo_demo.users enable row level security;
alter table apollo_demo.messages enable row level security;

create policy select_users on apollo_demo.users for select
  using (true);

create policy select_messages on apollo_demo.messages for select
  using (true);

create policy update_users on apollo_demo.users for update to apollo_demo_login_user
  using (id = current_setting('jwt.claims.user_id', true)::integer);

create policy delete_users on apollo_demo.users for delete to apollo_demo_login_user
  using (id = current_setting('jwt.claims.user_id', true)::integer);

create policy insert_messages on apollo_demo.messages for insert to apollo_demo_login_user
  with check (user_id = current_setting('jwt.claims.user_id', true)::integer);

create policy update_messages on apollo_demo.messages for update to apollo_demo_login_user
  using (user_id = current_setting('jwt.claims.user_id', true)::integer);

create policy delete_messages on apollo_demo.messages for delete to apollo_demo_login_user
  using (user_id = current_setting('jwt.claims.user_id', true)::integer);


INSERT INTO apollo_demo.users (id, nickname, email) VALUES
  (0, 'The Bot', 'bot@bot.com');

INSERT INTO apollo_demo_private.user_accounts (user_id, password_hash) VALUES
  (0, crypt('bot', gen_salt('bf')));

INSERT INTO apollo_demo.channels (id, name) VALUES
  ('general', 'General discussion'),
  ('random', 'Have fun chatting!'),
  ('help', 'Ask for or give help');

INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
  ('general', 0, 'Welcome to the chat!');


create type apollo_demo.message_nullable as (
  id               integer,
  channel_id       text,
  user_id          integer,
  content          text,
  date_added       timestamp,
  date_updated     timestamp
) ;

create or replace function apollo_demo.graphql_subscription() returns trigger as $$
declare
  v_process_new bool = (TG_OP = 'INSERT' OR TG_OP = 'UPDATE');
  v_process_old bool = (TG_OP = 'UPDATE' OR TG_OP = 'DELETE');
  v_event text = TG_ARGV[0];
  v_topic_template text = TG_ARGV[1];
  v_attribute text = TG_ARGV[2];
  v_record record;
  v_sub text;
  v_topic text;
begin
  if v_process_new then
    v_record = new;
  else
    v_record = old;
  end if;

  if v_attribute is not null then
    execute 'select $1.' || quote_ident(v_attribute)
    using v_record
    into v_sub;
  end if;

  if v_sub is not null then
    v_topic = replace(v_topic_template, '$1', v_sub);
  else
    v_topic = v_topic_template;
  end if;

  perform pg_notify(v_topic, json_build_object(
      'event', v_event,
      'newrec', new.*,
      'oldrec', old.*,
      'type', TG_OP
    )::text);

  -- RAISE LOG     'v_topic = %', v_topic;
  -- RAISE LOG     'json_build_object = %', json_build_object('event', v_event, 'newrec', new.*, 'oldrec', old.*, 'type', TG_OP);

  return v_record;
end;
$$ language plpgsql volatile set search_path from current;

DROP TRIGGER IF EXISTS _500_gql_update_message ON apollo_demo.messages;
CREATE TRIGGER _500_gql_update_message
  AFTER INSERT OR UPDATE OR DELETE ON apollo_demo.messages
  FOR EACH ROW
  EXECUTE PROCEDURE apollo_demo.graphql_subscription(
    'messageChanged', -- the "event" string, useful for the client to know what happened
    'graphql:message:$1', -- the "topic" the event will be published to, as a template
    'channel_id' -- If specified, `$1` above will be replaced with NEW.channel_id or OLD.channel_id from the trigger.
  );

commit;

処理が成功したら、psqlと、Laradockのworkspaceからもexitしてホストに戻っておきます。

環境構築&バックエンド実装(その3):PostGraphile関連

PostGraphileは公式Dockerイメージを利用することにします。ただしGraphQLサブスクリプションを動かすために、イメージ内にJavaScriptパッケージを追加インストールする必要があります。そこで、公式イメージをベースイメージにして新たなDockerイメージを作成します。
公式イメージのDockerfileはこちら

新Dockerfileの置き場所を作成。

mkdir -p ~/work/laradock/postgraphile_plus

Dockerfileを新規作成。

FROM graphile/postgraphile

ENV NODE_PATH=/usr/local/lib/node_modules

# RUN yarn global add @graphile/pg-pubsub graphile-build graphql postgraphile graphile-utils log4js
RUN npm install -g @graphile/pg-pubsub graphile-build graphql postgraphile graphile-utils log4js

ENTRYPOINT ["./cli.js"]

認証にJWTを使用します。
以下を実行して、JWT用共通鍵(秘密鍵)を作成(Linuxコマンド)。この共通鍵は、トークン作成とトークン検証の両方で共通して使用されます。

LC_CTYPE=C < /dev/urandom tr -dc A-Za-z0-9 | head -c32

本ページでは、以下の共通鍵を使用します。秘密鍵なので、本番環境では取り扱い要注意です。

K3INqoxbNWCB4hYiDnX2zlbIpm8UA5zR

Laradockのdocker-compose.ymlファイルの最後に以下を追加。JWT用共通鍵も設定しています。

~/work/laradock/docker-compose.yml
### PostGraphile ################################################
    postgraphile_demo:
      build:
        context: ./postgraphile_plus
      environment:
        DATABASE_URL: "postgres://apollo_demo_postgraphile:xyz@postgres:${POSTGRES_PORT}/demodb"
      expose:
        - "16000"
      command: ["--plugins", "@graphile/pg-pubsub", "--append-plugins", "/root/www/vue-apollo/tests/demo/postgraphile-server/DemoSubscriptionPlugin.js", "--subscriptions", "--connection", "postgres://apollo_demo_postgraphile:xyz@postgres:${POSTGRES_PORT}/demodb", "--port", "16000", "--schema", "apollo_demo", "--default-role", "apollo_demo_anonymous", "--secret", "K3INqoxbNWCB4hYiDnX2zlbIpm8UA5zR", "--token", "apollo_demo.jwt_token", "--export-schema-graphql", "/root/www/vue-apollo/tests/demo/postgraphile-server/schema.graphql"]
      ports:
        - "16000:16000"
      depends_on:
        - postgres
      restart: on-failure:20
      networks:
        - backend
      volumes:
        - ${APP_CODE_PATH_HOST}:/root/www

上記docker-compose.ymlで、PostGraphileのプラグインとして「DemoSubscriptionPlugin.js」を使うように設定していますので、そのファイルも作成します。プラグインの内容は後ほどご説明します。

DemoSubscriptionPlugin.jsを以下の内容で新規作成。
ファイルの置き場所は「 ~/work/vue-apollo/tests/demo/postgraphile-server 」とします。

~/work/vue-apollo/tests/demo/postgraphile-server/DemoSubscriptionPlugin.js
const DEBUG_MODE = false

////////////////////////////////////////////////////////////////
// デバッグ用コード。デバッグ時に /root/www/debug.log にログを出力する。
////////////////////////////////////////////////////////////////
const log4js = require('log4js')
const util = require('util')

log4js.configure({
  appenders : {
    debug : {type : 'file', filename : '/root/www/debug.log'}
  },
  categories : {
    default : {appenders : ['debug'], level : 'debug'},
  }
});
var logger = log4js.getLogger('debug');

////////////////////////////////////////////////////////////////
// 以下、https://www.graphile.org/postgraphile/subscriptions/ の
// MySubscriptionPlugin.jsを改変
////////////////////////////////////////////////////////////////
const { makeExtendSchemaPlugin, gql, embed } = require("graphile-utils");

const makeTopic = (_args, context, _resolveInfo) => {
  if (DEBUG_MODE) {
    logger.debug('makeTopic() in');
    logger.debug('_args = ');
    logger.debug(util.inspect(_args));
    logger.debug('context = ');
    logger.debug(util.inspect(context));

    // デバッグ時に、JWTが渡されなくてもSubscriptionを有効にしたいなら、ここでreturn
    return `graphql:message:${_args.channelId}`;
  }

  if (context.jwtClaims.user_id) {
    return `graphql:message:${_args.channelId}`;
  } else {
    throw new Error("You're not logged in");
  }
};

module.exports = makeExtendSchemaPlugin(({ pgSql: sql }) => ({
  typeDefs: gql`
    type MessageChanged {
      # This is returned directly from the PostgreSQL subscription payload (JSON object)
      type: String!

      # This is populated by our resolver below
      message: Message

      # This is returned directly from the PostgreSQL subscription payload (JSON object)
      oldrec: MessageNullable
    }

    extend type Subscription {
      messageChanged (channelId: String!): MessageChanged! @pgSubscription(topic: ${embed(
        makeTopic
      )})
    }
  `,

  resolvers: {
    MessageChanged: {
      // This method finds the user from the database based on the event
      // published by PostgreSQL.
      //
      // In a future release, we hope to enable you to replace this entire
      // method with a small schema directive above, should you so desire. It's
      // mostly boilerplate.

      async message(
        event,
        _args,
        _context,
        {
          graphile: { selectGraphQLResultFromTable },
        }
      ) {

        if (DEBUG_MODE) {
          logger.debug('message() in');
          logger.debug('event = ');
          logger.debug(util.inspect(event));
        }

        if (event.type === 'DELETE') {
          return null;
        }

        const rows = await selectGraphQLResultFromTable(
          sql.fragment`apollo_demo.messages`,
          (tableAlias, sqlBuilder) => {

            if (DEBUG_MODE) {
              logger.debug('selectGraphQLResultFromTable() in');
            }

            sqlBuilder.where(
              sql.fragment`${sqlBuilder.getTableAlias()}.id = ${sql.value(
                event.newrec.id
              )}`
            );
          }
        );

        if (DEBUG_MODE) {
          logger.debug('rows = ');
          logger.debug(util.inspect(rows));
        }
        return rows[0];
      },

    },
  },
}));

Docker環境の構築

cd ~/work/laradock
docker-compose up -d postgres pgadmin workspace postgraphile_demo

しばらく時間がかかります。終了したら、

docker-compose ps

以下のように表示されました。StateがUpになっているのを確認します。

            Name                          Command              State                Ports            
-----------------------------------------------------------------------------------------------------
laradock_docker-in-docker_1    dockerd-entrypoint.sh           Up       2375/tcp                     
laradock_pgadmin_1             docker-entrypoint.sh pgadmin4   Up       0.0.0.0:5050->5050/tcp       
laradock_postgraphile_demo_1   ./cli.js --plugins @graphi      Up       0.0.0.0:16000->16000/tcp,    
                               ...                                      5000/tcp                     
laradock_postgres_1            docker-entrypoint.sh postgres   Up       0.0.0.0:5432->5432/tcp       
laradock_workspace_1           sudo bash -c [ -e /var/run      Up       0.0.0.0:2222->22/tcp,        
                               ...                                      0.0.0.0:3389->3389/tcp,      
                                                                        0.0.0.0:14000->4000/tcp,     
                                                                        0.0.0.0:18080->8080/tcp      

PostGraphile が postgres に接続できているか確認します。

docker logs laradock_postgraphile_demo_1

以下のように、最終的にエラーが表示されていなければ、PostGraphileが正常に立ち上がっています。

PostGraphile v4.4.1-alpha.4 server listening on port 16000 🚀

  ‣ GraphQL API:         http://0.0.0.0:16000/graphql (subscriptions enabled)
  ‣ GraphiQL GUI/IDE:    http://0.0.0.0:16000/graphiql
  ‣ Postgres connection: postgres://apollo_demo_postgraphile:[SECRET]@postgres/demodb
  ‣ Postgres schema(s):  apollo_demo
  ‣ Documentation:       https://graphile.org/postgraphile/introduction/

* * *

何も立ち上がっていない状態からdocker-composeで一斉にコンテナを立ち上げると、PostgreSQLの立ち上げを待たずにPostGraphileが立ち上がろうとして、何回かエラーが発生することがあります。その現象については、「PostGraphile(GraphQL)、Vue.js、Apollo、VuetifyでCRUD機能を実装してみた」に書きましたので、ご参照ください。

さて先程のdocker-compose.ymlでは、GraphQLスキーマを「schema.graphql」にエクスポートするように設定しています。PostGraphileが立ち上がる時に、PostgreSQLのスキーマ情報を自動で読み取ってGraphQLスキーマを生成するのですが、その内容がエクスポートされます。ファイルが存在するか確認してみます。

$ ls ~/work/vue-apollo/tests/demo/postgraphile-server
DemoSubscriptionPlugin.js	schema_and_data.sql
schema.graphql

バックエンド実装解説

以上で、オリジナルApolloChatのGraphQLサーバー相当の機能をPostGraphileで実装する作業は完了しています。後はPostGraphileを使うようにフロントエンドのソースコードを変更すれば、PostGraphile版ApolloChatが動くのですが、その前に今までの作業で行ったバックエンドの実装についてご説明します。

(解説は後日書きます)

フロントエンド変更

バックエンドの実装が完了しましたので、次はフロントエンドのVue.jsアプリに手を加えていきます。
オリジナルApolloChatの起動URLは「 http://localhost:8080/ 」でしたが、同じURLにすると混乱するため、PostGraphile版ApolloChatは「 http://localhost:8080/demo/ 」にすることにします。

追記(2019-06-19)

本記事で以後、Altair GraphQL Clientを使ってGraphQL問い合わせを試行しますが、queryとmutationはHEADERSに設定したJWTを認識してくれましたが、subscriptionだけ認識してくれませんでした。記事公開後にGraphql Playgroundの存在を知り、試したところsubscriptionもJWTを認識してくれましたので、こちらを使用した方が楽ですね。(追記終了)

GraphQL問い合わせ文を書き換え、Altair GraphQL Clientで試行する

オリジナルApolloChatが使用するGraphQL問い合わせ文は、以下のディレクトリに格納されています。

$ ls ~/work/vue-apollo/tests/demo/src/graphql
channel.gql		messageFragment.gql	userLogin.gql
channelFragment.gql	messageRemove.gql	userLogout.gql
channels.gql		messageUpdate.gql	userRegister.gql
messageAdd.gql		userCurrent.gql
messageChanged.gql	userFragment.gql

これらのgqlファイルを、ひとつひとつPostGraphile用に書き換えながら試行します。

問い合わせ文を試行するために、Altair GraphQL Clientをダウンロードしてインストールしてください。
Altair GraphQL Clientを立ち上げ、URL入力欄に「 http://localhost:16000/graphql 」と入力します。

PostGraphileが自動生成したファイル「schema.graphql」にGraphQLスキーマが記述されていますので、GraphQL問い合わせ文を作成するときの参考になります。
以下、変更前後のファイル内容を記載します。

Fragment

userFragment.gql

変更ありません。

~/work/vue-apollo/tests/demo/src/graphql/userFragment.gql
fragment user on User {
  id
  nickname
}

channelFragment.gql

変更ありません。

~/work/vue-apollo/tests/demo/src/graphql/channelFragment.gql
fragment channel on Channel {
  id
  name
}

messageFragment.gql

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/messageFragment.gql
# import "./userFragment.gql"

fragment message on Message {
  id
  content
  user {
    ...user
  }
  dateAdded
  dateUpdated
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/messageFragment.gql
# import "./userFragment.gql"

fragment message on Message {
  id
  content
  userByUserId {
    ...user
  }
  dateAdded
  dateUpdated
}

ログインが不要な問い合わせ

userRegister.gql

機能:新規ユーザー登録

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/userRegister.gql
mutation userRegister ($input: UserRegister!) {
  userRegister(input: $input)
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/userRegister.gql
mutation userRegister ($input: UserRegisterInput!) {
  userRegister(input: $input) {
    user {
      id
      nickname
      email
    }
  }
}

VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "input": {
    "nickname": "foo",
    "email": "foo@foo.com",
    "password": "p@ss"
  }
}

スクリーンショット 2019-06-18 16.21.29.png

レスポンス

{
  "data": {
    "userRegister": {
      "user": {
        "id": 1,
        "nickname": "foo",
        "email": "foo@foo.com"
      }
    }
  }
}

userLogout.gql

機能:ログアウト

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/userLogout.gql
mutation userLogout {
  userLogout
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/userLogout.gql
mutation userLogout ($input: UserLogoutInput!) {
  userLogout (input: $input) {
    boolean
  }
}

VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "input": {
  }
}

スクリーンショット 2019-06-18 16.24.26.png

レスポンス

{
  "data": {
    "userLogout": {
      "boolean": true
    }
  }
}

userLogin.gql

機能:ログイン

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/userLogin.gql
# import "./userFragment.gql"

mutation userLogin ($email: String!, $password: String!) {
  userLogin (email: $email, password: $password) {
    user {
      ...user
      email
    }
    token {
      id
      userId
      expiration
    }
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/userLogin.gql
mutation userLogin ($input: UserLoginInput!) {
  userLogin (input: $input) {
    usrAndToken {
      usr {
        id
        nickname
        email
      }
      token
    }
  }
}

VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "input": {
    "email": "foo@foo.com",
    "password": "p@ss"
  }
}

スクリーンショット 2019-06-18 16.27.16.png

レスポンス

{
  "data": {
    "userLogin": {
      "usrAndToken": {
        "usr": {
          "id": 1,
          "nickname": "foo",
          "email": "foo@foo.com"
        },
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBvbGxvX2RlbW9fbG9naW5fdXNlciIsInVzZXJfaWQiOjEsImV4cCI6MTU2MDg0NjM3NiwiaWF0IjoxNTYwODQyNzc2LCJhdWQiOiJwb3N0Z3JhcGhpbGUiLCJpc3MiOiJwb3N0Z3JhcGhpbGUifQ.uE6rXeIWDuFaLaAqE56OisICQKzntl5e4L4znSryoXU"
      }
    }
  }
}

ログインが必要な問い合わせ

先程のログイン処理のレスポンスでJWTトークンが返ってきましたので、それを以下のようにAltair GraphQL ClientのHEADERSに設定します。

Authorization
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYXBvbGxvX2RlbW9fbG9naW5fdXNlciIsInVzZXJfaWQiOjEsImV4cCI6MTU2MDg0NjM3NiwiaWF0IjoxNTYwODQyNzc2LCJhdWQiOiJwb3N0Z3JhcGhpbGUiLCJpc3MiOiJwb3N0Z3JhcGhpbGUifQ.uE6rXeIWDuFaLaAqE56OisICQKzntl5e4L4znSryoXU

スクリーンショット 2019-06-18 16.29.00.png

以後、問い合わせをして"jwt expired"というレスポンスが返ってきたら、Altair GraphQL ClientのHEADERSからJWTを削除して、再びuserLoginミューテーションを実行し、レスポンスで返ってきたトークンをHEADERSに設定し直してください。

userCurrent.gql

機能:ログイン中のユーザー情報の取得

変更ありません。

~/work/vue-apollo/tests/demo/src/graphql/userCurrent.gql
# import "./userFragment.gql"

query userCurrent {
  userCurrent {
    ...user
    email
  }
}

Altair GraphQL Clientで試行してみます。importしているfragmentは直接入力しました。

スクリーンショット 2019-06-18 16.34.30.png

レスポンス

{
  "data": {
    "userCurrent": {
      "id": 1,
      "nickname": "foo",
      "email": "foo@foo.com"
    }
  }
}

channels.gql

機能:全チャンネルを取得

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/channels.gql
# import "./channelFragment.gql"

query channels {
  channels {
    ...channel
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/channels.gql
# import "./channelFragment.gql"

query channels {
  allChannels {
    nodes {
      ...channel
    }
  }
}

Altair GraphQL Clientで試行してみます。

スクリーンショット 2019-06-18 16.38.59.png

レスポンス

{
  "data": {
    "allChannels": {
      "nodes": [
        {
          "id": "general",
          "name": "General discussion"
        },
        {
          "id": "help",
          "name": "Ask for or give help"
        },
        {
          "id": "random",
          "name": "Have fun chatting!"
        }
      ]
    }
  }
}

channel.gql

機能:チャンネルをidで検索し、そのチャンネルの全メッセージも取得

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/channel.gql
# import "./channelFragment.gql"
# import "./messageFragment.gql"

query channel ($id: ID!) {
  channel (id: $id) {
    ...channel
    messages {
      ...message
    }
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/channel.gql
# import "./channelFragment.gql"
# import "./messageFragment.gql"

query channel ($id: String!) {
  channelById (id: $id) {
    ...channel
    messagesByChannelId {
      nodes {
        ...message
      }
    }
  }
}

VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "id": "general"
}

スクリーンショット 2019-06-18 16.41.31.png

レスポンス

{
  "data": {
    "channelById": {
      "id": "general",
      "name": "General discussion",
      "messagesByChannelId": {
        "nodes": [
          {
            "id": 1,
            "content": "Welcome to the chat!",
            "userByUserId": {
              "id": 0,
              "nickname": "The Bot"
            },
            "dateAdded": "2019-06-18T07:10:27.671412",
            "dateUpdated": null
          }
        ]
      }
    }
  }
}

messageAdd.gql

機能:指定チャンネルでメッセージ追加

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/messageAdd.gql
# import "./messageFragment.gql"

mutation messageAdd ($input: MessageAdd!) {
  messageAdd (input: $input) {
    ...message
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/messageAdd.gql
# import "./messageFragment.gql"

mutation messageAdd ($input: MessageAddInput!) {
  messageAdd (input: $input) {
    message {
      ...message
    }
  }
}

VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "input": {
    "channelId": "general",
    "content": "ハローワールド"
  }
}

スクリーンショット 2019-06-18 16.44.20.png

レスポンス

{
  "data": {
    "messageAdd": {
      "message": {
        "id": 2,
        "content": "ハローワールド",
        "userByUserId": {
          "id": 1,
          "nickname": "foo"
        },
        "dateAdded": "2019-06-18T07:44:02.534676",
        "dateUpdated": null
      }
    }
  }
}

ここで再度channelクエリーを発行すると、「ハローワールド」メッセージが追加されているのが確認できます。

スクリーンショット 2019-06-18 16.45.42.png

messageUpdate.gql

機能:メッセージ変更(ApolloChatでは未使用)

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/messageUpdate.gql
# import "./messageFragment.gql"

mutation messageUpdate ($input: MessageUpdate!) {
  messageUpdate (input: $input) {
    ...message
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/messageUpdate.gql
# import "./messageFragment.gql"

mutation messageUpdate ($input: UpdateMessageByIdInput!) {
  updateMessageById (input: $input) {
    message {
      ...message
    }
  }
}

先程messageAddミューテーションで追加したメッセージに変更を加えてみます。VARIABLESは、

{
  "input": {
    "messagePatch": {
      "content": "こんにちは世界"
    },
    "id": 2
  }
}

スクリーンショット 2019-06-18 16.48.38.png

レスポンス

{
  "data": {
    "updateMessageById": {
      "message": {
        "id": 2,
        "content": "こんにちは世界",
        "userByUserId": {
          "id": 1,
          "nickname": "foo"
        },
        "dateAdded": "2019-06-18T07:44:02.534676",
        "dateUpdated": "2019-06-18T07:48:16.794758"
      }
    }
  }
}

ここでchannelクエリーを発行すると、「ハローワールド」メッセージが「こんにちは世界」に変更されているのが確認できます。

スクリーンショット 2019-06-18 16.50.14.png

messageRemove.gql

機能:メッセージ削除(ApolloChatでは未使用)

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/messageRemove.gql
# import "./messageFragment.gql"

mutation messageRemove ($id: ID!) {
  messageRemove (id: $id) {
    ...message
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/messageRemove.gql
# import "./messageFragment.gql"

mutation messageRemove ($input: DeleteMessageByIdInput!) {
  deleteMessageById (input: $input) {
    message {
      ...message
    }
  }
}

先程messageUpdateミューテーションで変更したメッセージを削除してみます。VARIABLESは、

{
  "input": {
    "id": 2
  }
}

スクリーンショット 2019-06-18 16.52.23.png

レスポンス

{
  "data": {
    "deleteMessageById": {
      "message": {
        "id": 2,
        "content": "こんにちは世界",
        "userByUserId": {
          "id": 1,
          "nickname": "foo"
        },
        "dateAdded": "2019-06-18T07:44:02.534676",
        "dateUpdated": "2019-06-18T07:48:16.794758"
      }
    }
  }
}

ここでchannelクエリーを発行すると、「こんにちは世界」メッセージが削除されたのが確認できます。

スクリーンショット 2019-06-18 16.53.14.png

messageChanged.gql

機能:サブスクリプションです。指定チャンネルで誰かがメッセージを追加・変更・削除したらサーバーから通知が来ます

オリジナル

~/work/vue-apollo/tests/demo/src/graphql/messageChanged.gql
# import "./messageFragment.gql"

subscription messageChanged ($channelId: ID!) {
  messageChanged (channelId: $channelId) {
    type
    message {
      ...message
    }
  }
}

変更後

~/work/vue-apollo/tests/demo/src/graphql/messageChanged.gql
# import "./messageFragment.gql"

fragment messageNullable on MessageNullable {
  id
  channelId
  userId
  content
  dateAdded
  dateUpdated
}
subscription messageChanged ($channelId: String!) {
  messageChanged (channelId: $channelId) {
    type
    message {
      ...message
    }
    oldrec {
      ...messageNullable
    }
  }
}
subscriptionの試行について

Altair GraphQL Clientでは、HEADERSにJWTトークンを設定すればログイン後のqueryやmutationを試行できますが、subscriptionだけはトークンを認識してくれませんでした。
この件についてのつぶやきはこちら:

こういう事情ですので、ログインしなくてもmessageChangedサブスクリプションを試行できるように、一旦バックエンド側のセキュリティーを緩めることにします。
PostGraphileのプラグイン「DemoSubscriptionPlugin.js」で、DEBUG_MODEをtrueにします。

~/work/vue-apollo/tests/demo/postgraphile-server/DemoSubscriptionPlugin.js
- const DEBUG_MODE = false
+ const DEBUG_MODE = true
(後略)

変更後のDemoSubscriptionPlugin.jsを有効にするために、PostGraphileコンテナを再起動します。

cd ~/work/laradock
docker stop laradock_postgraphile_demo_1
docker-compose up -d postgres pgadmin workspace postgraphile_demo

これでmessageChangedの受信待ちができるようになりました。
更に、messageChangedが送信データを作る時にPostgreSQLからデータ取得できるよう、一旦PostgreSQL側のセキュリティーも緩めます。

Laradockのworkspaceに入リ、更にpsqlに入ります。

docker-compose exec --user=laradock workspace bash
psql -h postgres -U default demodb

anonymousユーザーに各テーブルのselect権限を与えます。

grant select on table apollo_demo.messages to apollo_demo_anonymous;
grant select on table apollo_demo.users to apollo_demo_anonymous;
grant select on table apollo_demo.channels to apollo_demo_anonymous;

これでmessageChangedが受信できるようになりました。
psqlは引き続き使いますので、立ち上げたままにしておきます。

それでは、VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "channelId": "general"
}

受信待ちの画面:

スクリーンショット 2019-06-18 17.01.36.png

この状態で、psqlで以下のINSERT文を発行します。

INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
    ('general', 1, '美味しい!');

スクリーンショット 2019-06-18 17.03.57.png

レスポンス

{
  "data": {
    "messageChanged": {
      "type": "INSERT",
      "message": {
        "id": 3,
        "content": "美味しい!",
        "userByUserId": {
          "id": 1,
          "nickname": "foo"
        },
        "dateAdded": "2019-06-18T08:02:26.293796",
        "dateUpdated": null
      },
      "oldrec": null
    }
  }
}

psqlでINSERTされたデータがレスポンスで返ってきました。
このデータに対して、psqlでUPDATE文を発行します。

UPDATE apollo_demo.messages SET
    content = '美味しい!'
    WHERE id = 3;

スクリーンショット 2019-06-18 17.08.37.png

レスポンス

{
  "data": {
    "messageChanged": {
      "type": "UPDATE",
      "message": {
        "id": 3,
        "content": "美味しい!",
        "userByUserId": {
          "id": 1,
          "nickname": "foo"
        },
        "dateAdded": "2019-06-18T08:02:26.293796",
        "dateUpdated": "2019-06-18T08:07:13.937846"
      },
      "oldrec": {
        "id": 3,
        "channelId": null,
        "userId": null,
        "content": "美味しい!",
        "dateAdded": null,
        "dateUpdated": null
      }
    }
  }
}

更新後のデータがレスポンスで返ってきました。
oldrec(UPDATE前の行)の列の多くがnullになっているのが気になります。今回PostgreSQL側でスキーマ定義する時、列名をスネークケースにしました。PostGraphileはGraphQLスキーマを自動生成する時に、スネークケースをキャメルケースに自動変換します。この変換がなされた列について、値が渡されずにnullになってしまった様子です。AppolloChatではoldrec.idしか使いませんので、値をきちんと渡す実装は今後の宿題ということにして、先に進みます。

返ってきたデータに対して、psqlでDELETE文を発行します。

DELETE FROM apollo_demo.messages
    WHERE id = 3;

スクリーンショット 2019-06-18 17.09.44.png

レスポンス

{
  "data": {
    "messageChanged": {
      "type": "DELETE",
      "message": null,
      "oldrec": {
        "id": 3,
        "channelId": null,
        "userId": null,
        "content": "美味しい!",
        "dateAdded": null,
        "dateUpdated": null
      }
    }
  }
}

削除についても通知が来ました。

以上でmessageChangedサブスクリプションの試行を終わりにし、先程緩めたセキュリティーを復旧させます。
psqlで以下のREVOKE文を発行します。

revoke select on table apollo_demo.messages from apollo_demo_anonymous;
revoke select on table apollo_demo.users from apollo_demo_anonymous;
revoke select on table apollo_demo.channels from apollo_demo_anonymous;

この状態で、psqlから先程のINSERT文をもう一度発行すると、以下のようにエラーが返ってきて、PostgreSQL側のセキュリティーが復旧したのがわかります。

スクリーンショット 2019-06-18 17.14.12.png

レスポンス

{
  "errors": [
    {
      "message": "permission denied for table messages",
      "locations": [
        {
          "line": 25,
          "column": 5
        }
      ],
      "path": [
        "messageChanged",
        "message"
      ]
    }
  ],
  "data": {
    "messageChanged": {
      "type": "INSERT",
      "message": null,
      "oldrec": null
    }
  }
}

psqlと、Laradockのworkspaceからもexitしてホストに戻っておきます。
Altair GraphQL Clientは、Stopボタンを押してmessageChangedサブスクリプションを停止させます。

次はPostGraphileのプラグイン「DemoSubscriptionPlugin.js」のセキュリティーを復旧させます。
以下のように、DEBUG_MODEをfalseにします。

~/work/vue-apollo/tests/demo/postgraphile-server/DemoSubscriptionPlugin.js
- const DEBUG_MODE = true
+ const DEBUG_MODE = false
(後略)

変更後のDemoSubscriptionPlugin.jsを有効にするために、PostGraphileコンテナを再起動します。

cd ~/work/laradock
docker stop laradock_postgraphile_demo_1
docker-compose up -d postgres pgadmin workspace postgraphile_demo

この状態でAltair GraphQL ClientでmessageChangedを開始させようとしても、エラーメッセージが返ってきて受信待ちできなくなり、セキュリティーが復旧したのが確認できました。

スクリーンショット 2019-06-18 17.20.17.png

レスポンス

{
  "errors": [
    {
      "message": "Cannot read property 'user_id' of null",
      "locations": [
        {
          "line": 23,
          "column": 3
        }
      ],
      "path": [
        "messageChanged"
      ]
    }
  ]
}

~/work/vue-apollo/tests/demo/src/components/MockSendMessage.vue

これまでのgqlファイル以外に、Vueコンポーネントのソースコードに直接書かれた問い合わせがありますので、それも書き換えながら試行します。

問い合わせの機能:generalチャンネルに「"How are you doing? " + 連番」というメッセージを投稿

オリジナル

~/work/vue-apollo/tests/demo/src/components/MockSendMessage.vue
<script>
import gql from 'graphql-tag'

export default {
  created () {
    this.SEND = gql`
      mutation mockMessageSend {
        mockMessageSend
      }
    `
  },
}
</script>

<template>
  <ApolloMutation
    :mutation="SEND"
    class="mock-send-message"
  >
    <a
      slot-scope="{ mutate }"
      type="button"
      @click="mutate"
    >Send bot message</a>
  </ApolloMutation>
</template>

<style lang="stylus" scoped>
.mock-send-message
  margin-top 8px
  display flex
  align-items center
  justify-content center
</style>

変更後

~/work/vue-apollo/tests/demo/src/components/MockSendMessage.vue
<script>
import gql from 'graphql-tag'

export default {
  created () {
    this.SEND = gql`
      mutation mockMessageSend ($input: MockMessageSendInput!) {
        mockMessageSend (input: $input) {
          boolean
        }
      }
    `
  },
}
</script>

<template>
  <ApolloMutation
    :mutation="SEND"
    :variables="{
      input: {
      },
    }"
    class="mock-send-message"
  >
    <a
      slot-scope="{ mutate }"
      type="button"
      @click="mutate"
    >Send bot message</a>
  </ApolloMutation>
</template>

<style lang="stylus" scoped>
.mock-send-message
  margin-top 8px
  display flex
  align-items center
  justify-content center
</style>

GraphQL問い合わせ文を抜き出し、VARIABLESに以下を設定して、Altair GraphQL Clientで試行してみます。

{
  "input": {
  }
}

スクリーンショット 2019-06-18 17.26.20.png

レスポンス

{
  "data": {
    "mockMessageSend": {
      "boolean": true
    }
  }
}

この試行でgeneralチャンネルに「"How are you doing? " + 連番」が投稿されたかどうかは、後にPostGraphile版ApolloChatを起動した時に確認することにします。

以上で、GraphQL問い合わせ文の書き換えと試行が完了しました。

フロントエンドのGraphQL問い合わせ以外のソースコード書き換え

それでは、問い合わせ文以外の変更ということで、以下のようにファイルを作成・変更していきます。

~/work/vue-apollo/tests/demo/vue.config.js (変更)

CORSエラーを避けるために、devServerのproxyの部分でリバースプロキシを設定しました(WebSocketとhttpの両方)。

~/work/vue-apollo/tests/demo/vue.config.js
module.exports = {
  pluginOptions: {
    graphqlMock: false,
    apolloEngine: false,
  },
+   devServer: {
+     host: '0.0.0.0',
+     disableHostCheck: true,
+     proxy: {
+       "^/postgraphile/demo/ws": {
+         target: "http://localhost:16000",
+         ws: true,
+         changeOrigin: true,
+         pathRewrite: {
+           "^/postgraphile/demo/ws": "/"
+         }
+       },
+       "^/postgraphile/demo": {
+         target: "http://localhost:16000",
+         ws: false,
+         pathRewrite: {
+           "^/postgraphile/demo": "/"
+         }
+       },
+     }
+   },
+   publicPath: '/demo',

  /* Without vue-cli-plugin-apollo 0.20.0+ */
  // chainWebpack: config => {
  //   config.module
  //     .rule('vue')
  //     .use('vue-loader')
  //       .loader('vue-loader')
  //       .tap(options => {
  //         options.transpileOptions = {
  //           transforms: {
  //             dangerousTaggedTemplateString: true,
  //           },
  //         }
  //         return options
  //       })
  // }
}

参考までに、リバースプロキシをvue.config.jsでなくLaradockのnginxで設定する場合は、「 ~/work/laradock/nginx/site/default.conf 」に以下の設定を加えて、docker-compose立ち上げ時にnginxコンテナも立ち上げ、その後workspaceコンテナに入ってyarn serveでWebサーバーを立ち上げ、ホストのブラウザから「 http://localhost/demo/ 」にアクセスしてPostGraphile版ApolloChatを起動します。

~/work/laradock/nginx/site/default.conf
   location /demo/ {
       proxy_pass http://workspace:8080/demo/;
   }

   location /postgraphile/demo/ws/ {
       proxy_pass http://postgraphile_demo:16000/;
       proxy_http_version 1.1;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection "upgrade";
   }

   location /postgraphile/demo/ {
       proxy_pass http://postgraphile_demo:16000/;
   }

~/work/vue-apollo/tests/demo/.env (新規)

内容:接続するPostGraphileのエンドポイントの設定。

~/work/vue-apollo/tests/demo/.env
VUE_APP_GRAPHQL_HTTP=http://localhost:8080/postgraphile/demo/graphql
VUE_APP_GRAPHQL_WS=ws://localhost:8080/postgraphile/demo/ws/graphql

この内容は、~/work/vue-apollo/tests/demo/src/vue-apollo.jsから参照されます。
参考までに、リバースプロキシをvue.config.jsでなくLaradockのnginxで設定する場合は、.envを以下のように設定します。

~/work/vue-apollo/tests/demo/.env
VUE_APP_GRAPHQL_HTTP=http://localhost/postgraphile/demo/graphql
VUE_APP_GRAPHQL_WS=ws://localhost/postgraphile/demo/ws/graphql

~/work/vue-apollo/tests/demo/.graphqlconfig.yml (変更)

PostGraphileが自動生成したGraphQLスキーマファイルの場所をApolloChatに教えてあげます。

~/work/vue-apollo/tests/demo/.graphqlconfig.yml
projects:
  app:
-     schemaPath: apollo-server/schema.graphql
+     schemaPath: postgraphile-server/schema.graphql
    includes:
      - '**/*.gql'
    extensions:
      endpoints:
        default: 'http://localhost:4000/graphql'

~/work/vue-apollo/tests/demo/src/router.js (変更)

Vue.jsプロジェクトをdemoサブディレクトリで配信するために必要な1行を加えます。

~/work/vue-apollo/tests/demo/src/router.js
import Vue from 'vue'
import Router from 'vue-router'
import UserLogin from './components/UserLogin.vue'
import WelcomeView from './components/WelcomeView.vue'
import ChannelView from './components/ChannelView.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
+   base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: WelcomeView,
    },
    {
      path: '/login',
      name: 'login',
      component: UserLogin,
    },
    {
      path: '/chan/:id',
      name: 'channel',
      component: ChannelView,
      props: true,
    },
  ],
})

~/work/vue-apollo/tests/demo/src/vue-apollo.js (変更)

~/work/vue-apollo/tests/demo/src/vue-apollo.js
import Vue from 'vue'
import VueApollo from '../../../'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
+ import { InMemoryCache } from 'apollo-cache-inmemory'

// Install the vue plugin
Vue.use(VueApollo)

// Name of the localStorage item
- const AUTH_TOKEN = 'apollo-token'
+ const AUTH_TOKEN = 'postgraphile-demo-token'

// Config
const defaultOptions = {
  // You can use `https` for secure connection (recommended in production)
  httpEndpoint: process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql',
  // You can use `wss` for secure connection (recommended in production)
  // Use `null` to disable subscriptions
  wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
  // LocalStorage token
  tokenName: AUTH_TOKEN,
  // Enable Automatic Query persisting with Apollo Engine
  persisting: false,
  // Use websockets for everything (no HTTP)
  // You need to pass a `wsEndpoint` for this to work
  websocketsOnly: false,
  // Is being rendered on the server?
  ssr: false,
  // Override default http link
  // link: myLink,
  // Override default cache
  // cache: myCache,
+   cache: new InMemoryCache(),
  // Additional ApolloClient options
  // apollo: { ... }
  getAuth: tokenName => {
    // get the authentication token from local storage if it exists
    const token = localStorage.getItem(tokenName)
    // return the headers to the context so httpLink can read them
-     return token || ''
+     return token ? ('Bearer ' + token) : ''
  },
}

// Call this in the Vue app file
export function createProvider (options = {}, { router }) {
  // Create apollo client
  const { apolloClient, wsClient } = createApolloClient({
    ...defaultOptions,
    ...options,
  })
  apolloClient.wsClient = wsClient

  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        fetchPolicy: 'cache-and-network',
      },
    },
    errorHandler (error) {
      if (isUnauthorizedError(error)) {
        // Redirect to login page
        if (router.currentRoute.name !== 'login') {
          router.replace({
            name: 'login',
            params: {
              wantedRoute: router.currentRoute.fullPath,
            },
          })
        }
      } else {
        console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
      }
    },
  })

  return apolloProvider
}

// Manually call this when user log in
export async function onLogin (apolloClient, token) {
-   localStorage.setItem(AUTH_TOKEN, JSON.stringify(token))
+   localStorage.setItem(AUTH_TOKEN, token)
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    if (!isUnauthorizedError(e)) {
      console.log('%cError on cache reset (login)', 'color: orange;', e.message)
    }
  }
}

// Manually call this when user log out
export async function onLogout (apolloClient) {
  localStorage.removeItem(AUTH_TOKEN)
  if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
  try {
    await apolloClient.resetStore()
  } catch (e) {
    if (!isUnauthorizedError(e)) {
      console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
    }
  }
}

function isUnauthorizedError (error) {
-   const { graphQLErrors } = error
-   return (graphQLErrors && graphQLErrors.some(e => e.message === 'Unauthorized'))
+   if (error) {
+     if (error.message.indexOf('Unauthorized') >= 0 || error.message.indexOf('permission denied') >= 0 || error.message.indexOf('status code 401') >= 0) {
+       return true
+     }
+   }
+   return false
}

~/work/vue-apollo/tests/demo/src/components/UserLogin.vue (変更)

~/work/vue-apollo/tests/demo/src/components/UserLogin.vue
<script>
import UserCurrent from '../mixins/UserCurrent'
import USER_CURRENT from '../graphql/userCurrent.gql'
import { onLogin } from '../vue-apollo'

export default {
  name: 'UserLogin',

  mixins: [
    UserCurrent,
  ],

  data () {
    return {
      showRegister: false,
      email: '',
      password: '',
      nickname: '',
    }
  },

  watch: {
    // If already logged in redirect to other page
    userCurrent (value) {
      if (value) {
        this.redirect()
      }
    },
  },

  methods: {
    async onDone (result) {
      if (this.showRegister) {
        this.showRegister = false
      } else {
        if (!result.data.userLogin) return
        const apolloClient = this.$apollo.provider.defaultClient
        // Update token and reset cache
-         const { id, userId, expiration } = result.data.userLogin.token
-         await onLogin(apolloClient, { id, userId, expiration })
+         await onLogin(apolloClient, result.data.userLogin.usrAndToken.token)
        // Update cache
        apolloClient.writeQuery({
          query: USER_CURRENT,
          data: {
-             userCurrent: result.data.userLogin.user,
+             userCurrent: result.data.userLogin.usrAndToken.usr,
          },
        })
      }
    },

    redirect () {
      this.$router.replace(this.$route.params.wantedRoute || { name: 'home' })
    },
  },
}
</script>

<template>
  <div class="user-login">
    <div class="logo">
      <i class="material-icons icon">chat</i>
    </div>
    <div class="app-name">
      Apollo<b>Chat</b>
    </div>
    <ApolloMutation
      :mutation="showRegister
        ? require('../graphql/userRegister.gql')
        : require('../graphql/userLogin.gql')"
      :variables="showRegister
        ? {
          input: {
            email,
            password,
            nickname,
          },
        }
        : {
-           email,
-           password,
+           input: {
+             email,
+             password,
+           },
        }"
      class="wrapper"
      @done="onDone"
    >
      <form
        slot-scope="{ mutate, loading, gqlError: error }"
        :key="showRegister"
        class="form"
        @submit.prevent="mutate()"
      >
        <input
          v-model="email"
          class="form-input"
          type="email"
          name="email"
          placeholder="Email"
          required
        >
        <input
          v-model="password"
          class="form-input"
          type="password"
          name="password"
          placeholder="Password"
          required
        >
        <input
          v-if="showRegister"
          v-model="nickname"
          class="form-input"
          name="nickname"
          placeholder="Nickname"
          required
        >
        <div v-if="error" class="error">{{ error.message }}</div>
        <template v-if="!showRegister">
          <button
            type="submit"
            :disabled="loading"
            class="button"
            data-id="login"
          >Login</button>
          <div class="actions">
            <a
              data-id="create-account"
              @click="showRegister = true"
            >Create an account</a>
          </div>
        </template>
        <template v-else>
          <button
            type="submit"
            :disabled="loading"
            class="button"
            data-id="submit-new-account"
          >Create new account</button>
          <div class="actions">
            <a @click="showRegister = false">Go back</a>
          </div>
        </template>
      </form>
    </ApolloMutation>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.user-login
  height 100%
  display flex
  flex-direction column
  align-items center
  justify-content center

.logo
  .icon
    font-size 80px
    color $color

.app-name
  font-size 42px
  font-weight lighter
  margin-bottom 32px

.wrapper
  flex auto 0 0

.form
  width 100vw
  max-width 300px

.form-input,
.button
  display block
  width 100%
  box-sizing border-box

.form-input
  margin-bottom 12px

.actions
  margin-top 12px
  text-align center
  font-size 12px
</style>

~/work/vue-apollo/tests/demo/src/components/UserCurrent.vue (変更)

~/work/vue-apollo/tests/demo/src/components/UserCurrent.vue
<script>
import UserCurrent from '../mixins/UserCurrent'
import { onLogout } from '../vue-apollo'
import USER_LOGOUT from '../graphql/userLogout.gql'

export default {
  name: 'MessageForm',

  mixins: [
    UserCurrent,
  ],

  methods: {
    async logout () {
+       const apolloClient = this.$apollo.provider.defaultClient
+       onLogout(apolloClient)
      await this.$apollo.mutate({
        mutation: USER_LOGOUT,
+         variables: {
+           input: {
+           },
+         },
      })
-       const apolloClient = this.$apollo.provider.defaultClient
-       onLogout(apolloClient)
    },
  },
}
</script>

<template>
  <div class="user-current">
    <template v-if="userCurrent">
      <i class="material-icons user-icon">account_circle</i>
      <div class="info">
        <div class="nickname">{{ userCurrent.nickname }}</div>
        <div class="email">{{ userCurrent.email }}</div>
      </div>
      <button
        class="icon-button"
        data-id="logout"
        @click="logout()"
      >
        <i class="material-icons">power_settings_new</i>
      </button>
    </template>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.user-current
  color white
  display grid
  grid-template-columns auto 1fr auto
  grid-template-rows auto
  grid-gap 12px
  align-items center
  margin-bottom 20px
  padding 12px 0 12px 12px

.email
  font-size 12px

.icon-button
  &:not(:hover)
    background none
</style>

~/work/vue-apollo/tests/demo/src/components/MessageForm.vue (変更)

~/work/vue-apollo/tests/demo/src/components/MessageForm.vue
<script>
- import MESSAGE_FRAGMENT from '../graphql/messageFragment.gql'
- import USER_FRAGMENT from '../graphql/userFragment.gql'
- 
export default {
  props: {
    channelId: {
      type: String,
      required: true,
    },
  },

  data () {
    return {
      newMessage: '',
    }
  },

  methods: {
    onDone () {
      this.newMessage = ''
      this.$refs.input.focus()
    },
  },
- 
-   fragments: {
-     message: MESSAGE_FRAGMENT,
-     user: USER_FRAGMENT,
-   },
}
</script>

<template>
  <ApolloMutation
-     :mutation="gql => gql`
-       mutation messageAdd ($input: MessageAdd!) {
-         messageAdd (input: $input) {
-           ...message
-         }
-       }
-       ${$options.fragments.message}
-       ${$options.fragments.user}
-     `"
+     :mutation="require('../graphql/messageAdd.gql')"
    :variables="{
      input: {
        channelId,
        content: newMessage,
      },
    }"
    class="message-form"
    @done="onDone"
  >
    <input
      slot-scope="{ mutate, loading, error }"
      ref="input"
      v-model="newMessage"
      :disabled="loading"
      class="form-input"
      placeholder="Type a message"
      @keyup.enter="newMessage && mutate()"
    >
  </ApolloMutation>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.message-form
  padding 12px
  width 100%
  box-sizing border-box

  .form-input
    display block
    box-sizing border-box
    width 100%
</style>

~/work/vue-apollo/tests/demo/src/components/MessageItem.vue (変更)

~/work/vue-apollo/tests/demo/src/components/MessageItem.vue
<script>
import marked from 'marked'

// Open links in new tab
const renderer = new marked.Renderer()
const linkRenderer = renderer.link
renderer.link = (href, title, text) => {
  const html = linkRenderer.call(renderer, href, title, text)
  return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ')
}

export default {
  props: {
    message: {
      type: Object,
      required: true,
    },
  },

  computed: {
    html () {
      return marked(this.message.content, { renderer })
    },
  },
}
</script>

<template>
  <div class="message-item">
-     <div class="user">{{ message.user.nickname }}</div>
+     <div class="user">{{ message.userByUserId.nickname }}</div>
    <div class="content" v-html="html"/>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.message-item
  padding 12px 12px
  &:hover
    background #f8f8f8

.user
  color #777
  font-size 13px
  margin-bottom 2px

.content
  word-wrap break-word

  >>>
    p
      margin 0

    img
      max-width 500px
      max-height 500px
</style>

~/work/vue-apollo/tests/demo/src/components/ChannelList.vue (変更)

~/work/vue-apollo/tests/demo/src/components/ChannelList.vue
<script>
import UserCurrent from './UserCurrent.vue'
import MockSendMessage from './MockSendMessage.vue'
- import gql from 'graphql-tag'

export default {
  name: 'ChannelList',

  components: {
    UserCurrent,
    MockSendMessage,
  },
- 
-   fragments: {
-     channel: gql`
-       fragment channel on Channel {
-         id
-         name
-       }
-     `,
-   },
}
</script>

<template>
  <div class="channel-list">
    <UserCurrent />

-     <ApolloQuery :query="gql => gql`
-       query channels {
-         channels {
-           ...channel
-         }
-       }
-       ${$options.fragments.channel}
-     `">
+     <ApolloQuery :query="require('../graphql/channels.gql')">
      <template slot-scope="{ result: { data, loading } }">
        <div v-if="loading" class="loading">Loading...</div>
        <div v-else-if="data" class="channels">
          <router-link
-             v-for="channel of data.channels"
+             v-for="channel of data.allChannels.nodes"
            :key="channel.id"
            :to="{ name: 'channel', params: { id: channel.id } }"
            class="channel"
          >
            <div class="id">#{{ channel.id }}</div>
            <div class="name">{{ channel.name }}</div>
          </router-link>
        </div>
      </template>
    </ApolloQuery>

    <MockSendMessage/>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.channel-list
  background desaturate(darken($color, 60%), 95%)
  color white
  padding 12px

.channel
  display block
  padding 12px
  border-radius 4px
  &:hover
    background rgba($color, .3)
  &.router-link-active
    background $color
    color white
    font-weight bold

.id
  font-family monospace
  margin-bottom 4px
  font-size 14px

.name
  font-size 12px
  opacity .9
</style>

~/work/vue-apollo/tests/demo/src/components/ChannelView.vue (変更)

~/work/vue-apollo/tests/demo/src/components/ChannelView.vue
<script>
import MessageItem from './MessageItem.vue'
import MessageForm from './MessageForm.vue'

export default {
  name: 'ChannelView',

  components: {
    MessageItem,
    MessageForm,
  },

  props: {
    id: {
      type: String,
      required: true,
    },
  },

  watch: {
    id: {
      handler () {
        this.$_init = false
      },
      immediate: true,
    },
  },

  methods: {
    onMessageChanged (previousResult, { subscriptionData }) {
-       const { type, message } = subscriptionData.data.messageChanged
- 
-       // No list change
-       if (type === 'updated') return previousResult
- 
-       const messages = previousResult.channel.messages.slice()
-       // Add or remove item
-       if (type === 'added') {
-         messages.push(message)
-       } else if (type === 'removed') {
-         const index = messages.findIndex(m => m.id === message.id)
-         if (index !== -1) messages.splice(index, 1)
-       }
- 
-       // New query result
-       return {
-         channel: {
-           ...previousResult.channel,
-           messages,
-         },
-       }
+       const { message, oldrec, type } = subscriptionData.data.messageChanged
+ 
+       const nodes = previousResult.channelById.messagesByChannelId.nodes.slice()
+       if (type === 'INSERT') {
+         nodes.push(message)
+       } else {
+         const index = nodes.findIndex(m => m.id === oldrec.id)
+         if (index !== -1) {
+           if (type === 'DELETE') {
+             nodes.splice(index, 1)
+           } else if (type === 'UPDATE') {
+             nodes.splice(index, 1, message)
+           }
+         }
+       }
+ 
+       // New query result
+       return {
+         channelById: {
+           ...previousResult.channelById,
+           messagesByChannelId: {
+             ...previousResult.channelById.messagesByChannelId,
+             nodes,
+           },
+         },
+       }
    },

    async scrollToBottom (force = false) {
      let el = this.$refs.body

      // No body element yet
      if (!el) {
        setTimeout(() => this.scrollToBottom(force), 100)
        return
      }
      // User is scrolling up => no auto scroll
      if (!force && el.scrollTop + el.clientHeight < el.scrollHeight - 100) return

      // Scroll to bottom
      await this.$nextTick()
      el.scrollTop = el.scrollHeight
    },

    onResult (result) {
      // The first time we load a channel, we force scroll to bottom
      this.scrollToBottom(!this.$_init)
      this.$_init = true
    },
  },
}
</script>

<template>
  <div class="channel-view">
    <ApolloQuery
      :query="require('../graphql/channel.gql')"
      :variables="{
        id
      }"
      @result="onResult"
    >
      <template slot-scope="{ result: { data, loading } }">
        <div v-if="!data && loading" class="loading">Loading...</div>

        <div v-else-if="data">
          <!-- Websockets -->
          <ApolloSubscribeToMore
            :document="require('../graphql/messageChanged.gql')"
            :variables="{
              channelId: id,
            }"
            :updateQuery="onMessageChanged"
          />

          <div class="wrapper">
            <div class="header">
-               <div class="id">#{{ data.channel.id }}</div>
-               <div class="name">{{ data.channel.name }}</div>
+               <div class="id">#{{ data.channelById.id }}</div>
+               <div class="name">{{ data.channelById.name }}</div>
            </div>

            <div ref="body" class="body">
              <MessageItem
-                 v-for="message in data.channel.messages"
+                 v-for="message in data.channelById.messagesByChannelId.nodes"
                :key="message.id"
                :message="message"
              />
            </div>

            <div class="footer">
              <MessageForm :channel-id="id" />
            </div>
          </div>
        </div>
      </template>
    </ApolloQuery>
  </div>
</template>

<style lang="stylus" scoped>
@import '~@/style/imports'

.wrapper
  height 100vh
  display grid
  grid-template-columns 1fr
  grid-template-rows auto 1fr auto

.header
  padding 12px
  border-bottom $border

.id
  font-family monospace
  margin-bottom 4px

.name
  color #555

.body
  overflow-x hidden
  overflow-y auto

.footer
  border-top $border
</style>

以上でソースコードの変更は完了です。

PostGraphile版ApolloChatの実行

それでは動かしてみます。
以下を実行して、Webサーバー(Vue.jsアプリ配信)を立ち上げます。
PostGraphileがDockerコンテナで立ち上がっていますので、別途GraphQLサーバーを立ち上げる必要はありません。

cd ~/work/vue-apollo/tests/demo
yarn serve

正常にコンパイルされることを確認します。
コンパイル後、以下のように表示されました。

 DONE  Compiled successfully in 3593ms                                       10:31:39

 
  App running at:
  - Local:   http://localhost:8080/demo/ 
  - Network: unavailable

  Note that the development build is not optimized.
  To create a production build, run yarn build.

表示に従い、ブラウザで「 http://localhost:8080/demo/ 」にアクセスすると、PostGraphile版ApolloChatが立ち上がります。
まずログイン画面が現れます。

スクリーンショット 2019-06-19 10.33.31.png

Altair GraphQL Clientから作成したアカウントでログインしてみます。

Email:foo@foo.com
Password:p@ss

スクリーンショット 2019-06-19 10.35.41.png

generalチャンネルに入ってみます。

スクリーンショット 2019-06-19 10.36.12.png

GraphQL問い合わせを試行した時のメッセージが残っていました。

  • messageChangedサブスクリプションを試行した時に、psqlからINSERTした「美味しい!」メッセージ
  • mockMessageSendミューテーションをAltair GraphQL Clientで試行した時に投稿された「"How are you doing? " + 連番」

では、左下の「Send bot message」を押してみます。

スクリーンショット 2019-06-19 10.45.23.png

「"How are you doing? " + 連番」が1つ増えました。
右下のメッセージ入力欄から「こんにちは世界」と投稿してみます。

スクリーンショット 2019-06-19 10.46.54.png

スクリーンショット 2019-06-19 11.34.41.png

「こんにちは世界」が投稿されました。
ではサブスクリプションを試してみます。
コンソールからLaradockのworkspaceに入リ、更にpsqlに入ります。

cd ~/work/laradock
docker-compose exec --user=laradock workspace bash
psql -h postgres -U default demodb

以下のSQLを発行して、メッセージ一覧を表示してみます。

SELECT * FROM apollo_demo.messages
  ORDER BY id;

以下のように表示されました。

 id | channel_id | user_id |       content        |         date_added         | date_updated 
----+------------+---------+----------------------+----------------------------+--------------
  1 | general    |       0 | Welcome to the chat! | 2019-06-18 07:10:27.671412 | 
  4 | general    |       1 | 美味しい!           | 2019-06-18 08:13:57.466309 | 
  5 | general    |       0 | How are you doing? 1 | 2019-06-18 08:26:07.808588 | 
  6 | general    |       0 | How are you doing? 2 | 2019-06-19 01:44:42.855632 | 
  7 | general    |       1 | こんにちは世界       | 2019-06-19 02:33:33.951225 | 

id=5の行を更新して、ApolloChatのデータが更新されるか試してみます。以下のSQLを発行します。

UPDATE apollo_demo.messages SET
  content = 'Thank you.'
  WHERE id = 5;

ApolloChatの画面も更新されました。

スクリーンショット 2019-06-19 11.46.57.png

次はid=5の行を削除してみます。以下のSQLを発行します。

DELETE FROM apollo_demo.messages
  WHERE id = 5;

ApolloChatの画面からも削除されました。

スクリーンショット 2019-06-19 11.48.33.png

次は新しい行を挿入してみます。以下のSQLを発行します。

INSERT INTO apollo_demo.messages (channel_id, user_id, content) VALUES
    ('general', 1, 'Hello, World!');

ApolloChatの画面にも追加されました。

スクリーンショット 2019-06-19 11.51.20.png

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?