Skip to content

Commit 4bca9da

Browse files
committed
feat: initial graphql and migrations
1 parent 99ace50 commit 4bca9da

22 files changed

+9103
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
*.log
33

44
node_modules
5+
generated

README.md

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,54 @@
11
# Postgres GraphQL
22

3-
This is an example project using the following technologies to provide a GraphQL server.
3+
This is an example project using the following technologies to provide a GraphQL
4+
server.
45

5-
- Postgres
6-
- Postgraphql
7-
- Graphile Migrate
6+
- [Postgres](https://www.postgresql.org)
7+
- [PostGraphile](https://www.graphile.org)
8+
- [graphile-migrate](https://github.com/graphile/migrate)
89

9-
The main benefit here is our GraphQL API types come directly from the Database, furthermore in our frontend applications
10-
we can automatically generate Typescript Types and Typed requests to interact with our API greatly improving development
11-
agility and our application's integrity.
10+
The main benefit here is our GraphQL API types come directly from the Database,
11+
furthermore in our frontend applications we can automatically generate
12+
Typescript Types and Typed requests to interact with our API improving
13+
development agility and our application's integrity.
1214

1315
## Getting Started
1416

15-
The only hard requirement is that you have `docker` and `docker-compose` installed which, although naturally you could
16-
install everything locally.
17+
The only hard requirement is that you have `docker` and `docker-compose`
18+
installed, although naturally, you could install everything locally.
1719

1820
### .env
1921

20-
First lets copy the `.env.example` to `.env`, this file will be used by `docker-compose` which will pass these environment
21-
variables into designated containers.
22+
First, let's copy the `.env.example` to `.env`. This file used by
23+
`docker-compose` will pass these environment variables into designated
24+
containers.
2225

2326
```bash
2427
cp .env.example .env
2528
```
2629

27-
### Local Docker Development
30+
Pull the images
31+
32+
```bash
33+
docker-compose pull
34+
```
35+
36+
And then we can build our services.
2837

2938
```bash
3039
docker-compose build
3140
```
3241

33-
Lets initialize and insure the database is ready to go.
42+
### Database
43+
44+
Initialize and ensure the database is ready to go.
3445

3546
```bash
3647
docker-compose up database
3748
```
3849

50+
#### Schema and Migrations
51+
3952
Install the dependencies
4053

4154
```bash
@@ -48,8 +61,60 @@ Initialize graphile-migrate
4861
docker-compose run migrations yarn graphile-migrate init
4962
```
5063

51-
Write some migrations in `current.sql`, once your ready to commit.
64+
At this point, we are ready to start the migrations watch service. Kill our
65+
existing docker-compose process with `control + c`. Then start up the migration
66+
watcher.
5267

5368
```bash
54-
docker-compose exec migrations yarn graphile-migrate commit
69+
docker-compose up database migrations
70+
```
71+
72+
Write some migrations in `current.sql`, once you're ready to commit.
73+
74+
Note I like to keep the minimum amount of logic in a specific commit to make
75+
your schema easier to read in the future, this also keeps your agility up in
76+
regards to developing new features.
77+
78+
```bash
79+
docker-compose exec migrations yarn graphile-migrate commit -m "my commit message"
80+
```
81+
82+
If you need to reset the database and rerun committed migrations.
83+
84+
```bash
85+
docker-compose exec migrations yarn graphile-migrate reset --erase
86+
```
87+
88+
### GraphQL
89+
90+
Once we have our database up and running with a schema we can go ahead and start
91+
the GraphQL server.
92+
93+
```bash
94+
docker-compose up database migrations gql
95+
```
96+
97+
Or using up without and other parameters would start all available
98+
docker-compose services
99+
100+
```bash
101+
docker-compose up database migrations gql
102+
```
103+
104+
Note that because GraphQL specifies migrations as a dependency and migrations
105+
specify database as a dependency we could also start everything up with the
106+
following command, although this would only output logs from the gql service
107+
hiding other crucial logs.
108+
109+
```bash
110+
docker-compose up database migrations gql
111+
```
112+
113+
#### GraphiQL
114+
115+
Postgraphile comes with a nifty interface to aid in development called GraphiQL,
116+
this allows us to inspect our current GraphQL schema at the following URL.
117+
118+
```
119+
http://localhost:5000/graphiql
55120
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--! Previous: -
2+
--! Hash: sha1:417a071b3828db454e480480b1f48ba490dcf60e
3+
--! Message: schemas and roles
4+
5+
-- Enter migration here
6+
7+
CREATE SCHEMA IF NOT EXISTS public;
8+
CREATE SCHEMA IF NOT EXISTS private;
9+
10+
DO
11+
$$
12+
BEGIN
13+
CREATE ROLE myapp_anonymous;
14+
EXCEPTION
15+
WHEN DUPLICATE_OBJECT THEN
16+
RAISE NOTICE 'not creating anonymous role -- it already exists';
17+
END
18+
$$;
19+
20+
DO
21+
$$
22+
BEGIN
23+
CREATE ROLE myapp_viewer;
24+
EXCEPTION
25+
WHEN DUPLICATE_OBJECT THEN
26+
RAISE NOTICE 'not creating viewer role -- it already exists';
27+
END
28+
$$;
29+
30+
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
31+
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM myapp_anonymous;
32+
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM myapp_viewer;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--! Previous: sha1:417a071b3828db454e480480b1f48ba490dcf60e
2+
--! Hash: sha1:2d6741b787f0894efd5bbb849d19df6eed3d1839
3+
--! Message: updated at trigger
4+
5+
-- Enter migration here
6+
7+
CREATE OR REPLACE FUNCTION private.set_updated_at() RETURNS trigger AS
8+
$$
9+
BEGIN
10+
new.updated_at := current_timestamp;
11+
RETURN new;
12+
END;
13+
$$ LANGUAGE plpgsql;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
--! Previous: sha1:2d6741b787f0894efd5bbb849d19df6eed3d1839
2+
--! Hash: sha1:2874c961801a9d6832bd9442d92795af2ee6048b
3+
--! Message: user tables
4+
5+
-- Enter migration here
6+
7+
DROP TABLE IF EXISTS public.user;
8+
9+
CREATE TABLE public.user
10+
(
11+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
12+
username text CHECK (char_length(username) < 32) UNIQUE,
13+
created_at timestamptz NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'),
14+
updated_at timestamptz NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
15+
);
16+
17+
COMMENT ON TABLE public.user IS 'Public information about a user';
18+
COMMENT ON COLUMN public.user.username IS 'This publicly viewable username';
19+
20+
CREATE TRIGGER user_updated_at
21+
BEFORE UPDATE
22+
ON public.user
23+
FOR EACH ROW
24+
EXECUTE PROCEDURE private.set_updated_at();
25+
26+
CREATE POLICY select_user ON public.user FOR SELECT
27+
USING (id = current_setting('jwt.claims.user_id', TRUE)::uuid);
28+
29+
CREATE POLICY update_user ON public.user FOR UPDATE TO myapp_viewer
30+
USING (id = current_setting('jwt.claims.person_id', TRUE)::uuid);
31+
32+
COMMENT ON TABLE public.user IS E'@omit delete';
33+
34+
DROP TABLE IF EXISTS private.user_account;
35+
36+
CREATE TABLE private.user_account
37+
(
38+
user_id uuid PRIMARY KEY REFERENCES public.user (id) ON DELETE CASCADE,
39+
email text NOT NULL UNIQUE CHECK (email ~* '^.+@.+\..+$'),
40+
active bool DEFAULT FALSE,
41+
password_hash text,
42+
created_at timestamptz NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc'),
43+
updated_at timestamptz NOT NULL DEFAULT (NOW() AT TIME ZONE 'utc')
44+
);
45+
46+
COMMENT ON TABLE private.user_account IS 'Private information about a person’s account.';
47+
COMMENT ON COLUMN private.user_account.user_id IS 'The id of the person associated with this account.';
48+
COMMENT ON COLUMN private.user_account.email IS 'The email address of the person.';
49+
COMMENT ON COLUMN private.user_account.password_hash IS 'An opaque hash of the person’s password.';
50+
51+
CREATE TRIGGER user_account_updated_at
52+
BEFORE UPDATE
53+
ON private.user_account
54+
FOR EACH ROW
55+
EXECUTE PROCEDURE private.set_updated_at();
56+
57+
CREATE POLICY select_private_user ON private.user_account FOR SELECT
58+
USING (user_id = current_setting('jwt.claims.user_id', TRUE)::uuid);
59+
60+
CREATE POLICY update_private_user ON private.user_account FOR UPDATE TO myapp_viewer
61+
USING (user_id = current_setting('jwt.claims.person_id', TRUE)::uuid);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--! Previous: sha1:2874c961801a9d6832bd9442d92795af2ee6048b
2+
--! Hash: sha1:ab7772c2fb1a26725a0f3802bdb273a470e77191
3+
--! Message: user registration
4+
5+
-- Enter migration here
6+
7+
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
8+
9+
DROP TYPE IF EXISTS public.jwt_token CASCADE;
10+
11+
CREATE TYPE public.jwt_token AS
12+
(
13+
role text,
14+
user_id uuid,
15+
username text,
16+
email text,
17+
exp bigint
18+
);
19+
20+
CREATE OR REPLACE FUNCTION public.register_user(email text,
21+
password text) RETURNS public.jwt_token AS
22+
$$
23+
DECLARE "user" public.user;
24+
BEGIN
25+
INSERT INTO public.user (username)
26+
VALUES (email)
27+
RETURNING * INTO "user";
28+
29+
INSERT INTO private.user_account (user_id, email, password_hash)
30+
VALUES ("user".id, email, crypt(password, gen_salt('bf')));
31+
32+
RETURN public.authenticate(email, password);
33+
END;
34+
$$ LANGUAGE plpgsql STRICT
35+
SECURITY DEFINER;
36+
37+
COMMENT ON FUNCTION public.register_user(text, text) IS 'Registers a single user and creates an account in our forum.';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
--! Previous: sha1:ab7772c2fb1a26725a0f3802bdb273a470e77191
2+
--! Hash: sha1:f74433c99ebe55a2175d99c75f8a77cad78222c4
3+
--! Message: user authentication
4+
5+
-- Enter migration here
6+
7+
CREATE OR REPLACE FUNCTION public.authenticate(email text,
8+
password text) RETURNS public.jwt_token AS
9+
$$
10+
DECLARE account private.user_account;
11+
username text;
12+
BEGIN
13+
SELECT a.*
14+
INTO account
15+
FROM private.user_account AS a
16+
WHERE a.email = authenticate.email;
17+
18+
SELECT u.username INTO username FROM public.user u WHERE id = account.user_id;
19+
20+
IF account.password_hash = crypt(password, account.password_hash)
21+
THEN
22+
RETURN ('jtx_viewer', account.user_id, username, account.email,
23+
extract(EPOCH FROM (now() + INTERVAL '2 days')))::public.jwt_token;
24+
ELSE
25+
RETURN NULL;
26+
END IF;
27+
END;
28+
$$ LANGUAGE plpgsql STRICT
29+
SECURITY DEFINER;
30+
31+
COMMENT ON FUNCTION public.authenticate(text, text) IS 'Creates a JWT token that will securely identify a user and give them certain permissions. This token expires in 2 days.';

apps/database/migrations/current.sql

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1 @@
11
-- Enter migration here
2-
3-
CREATE SCHEMA IF NOT EXISTS myapp;
4-
CREATE SCHEMA IF NOT EXISTS myapp_private;
5-
6-
DO
7-
$$
8-
BEGIN
9-
CREATE ROLE myapp_anonymous;
10-
EXCEPTION
11-
WHEN DUPLICATE_OBJECT THEN
12-
RAISE NOTICE 'not creating anonymous role -- it already exists';
13-
END
14-
$$;
15-
16-
DO
17-
$$
18-
BEGIN
19-
CREATE ROLE myapp_viewer;
20-
EXCEPTION
21-
WHEN DUPLICATE_OBJECT THEN
22-
RAISE NOTICE 'not creating viewer role -- it already exists';
23-
END
24-
$$;
25-
26-
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
27-
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM myapp_anonymous;
28-
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM myapp_viewer;

apps/graphql/Dockerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
ARG BASE_IMAGE=node:12.18.2-alpine3.11
2+
3+
FROM $BASE_IMAGE as base
4+
5+
WORKDIR /app
6+
7+
CMD yarn start
8+
9+
EXPOSE 5000
10+
11+
RUN apk add postgresql-client
12+
13+
COPY apps/graphql/package.json apps/graphql/yarn.lock /app/
14+
15+
RUN yarn install --production=true
16+
17+
COPY apps/graphql/src /app/src
18+
19+
FROM base as dev
20+
21+
RUN yarn install

apps/graphql/codegen.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
overwrite: true
2+
schema: "schema.graphql"
3+
documents: "../**/*.graphql"
4+
generates:
5+
generated/graphql.ts:
6+
plugins:
7+
- typescript
8+
- typescript-operations
9+
- typescript-graphql-request
10+
11+
config:
12+
scalars:
13+
DateTime: "string"
14+
JSON: "{ [key: string]: any }"

0 commit comments

Comments
 (0)