├── .gitignore
├── README.md
├── docker-compose.yml
├── frontend
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .stylelintrc
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── Apollo.js
│ ├── App.js
│ ├── Contacts.js
│ ├── ServiceWorker.js
│ └── index.js
└── yarn.lock
├── scripts
├── enter-in-pg.sh
├── load-fixtures.sh
├── load-seed.sh
├── up.sh
└── wait-service.sh
└── sqls
├── README.md
├── fixtures.sql
└── seed.sql
/.gitignore:
--------------------------------------------------------------------------------
1 | volumes/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Admin and Postgraphile playground
2 |
3 | This project is based on:
4 |
5 | - [postgraphile](https://www.graphile.org/postgraphile/)
6 | - [React-Admin](https://github.com/marmelab/react-admin)
7 | - [`BowlingX/ra-postgraphile`](https://github.com/BowlingX/ra-postgraphile) (see https://github.com/graphile/graphile.github.io/issues/224)
8 |
9 | ## Prerequisites
10 |
11 | - [Docker Engine](https://docs.docker.com/engine/) (version `18.06.1-ce`)
12 | - [Docker Compose](https://docs.docker.com/compose/) (version `1.22.0`)
13 | - [nodejs](https://nodejs.org/en/) (version `v12.10.0`)
14 |
15 | [Homebrew](https://brew.sh/index_fr) instructions:
16 |
17 | ```sh
18 | $ brew cask install docker
19 | $ brew install git node yarn jq
20 | ```
21 |
22 | ## Getting started
23 |
24 | Git clone this project a working directory, next:
25 |
26 | ```
27 | $ ./scripts/up.sh
28 | ```
29 |
30 | You can browse in database with [graphiql](https://github.com/graphql/graphiql) on this page: http://127.0.0.1:5000/graphiql
31 |
32 | Now that the backend has been started, go to [`frontend/`](frontend/) to start the « Contact Web Frontend ».
33 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 | services:
3 | postgres:
4 | image: postgres:11.2-alpine
5 | environment:
6 | POSTGRES_USER: postgres
7 | POSTGRES_DB: postgres
8 | POSTGRES_PASSWORD: password
9 | ports:
10 | - "5432:5432"
11 | volumes:
12 | - ./volumes/postgres/:/var/lib/postgresql/data/
13 |
14 | postgraphile:
15 | image: stephaneklein/poc-postgraphile-react-upload-to-s3:latest
16 | ports:
17 | - 5000:5000
18 | environment:
19 | GRAPHQL_API_DATABASE_STRING_URL: postgres://postgres:password@postgres:5432/postgres
20 |
--------------------------------------------------------------------------------
/frontend/.eslintignore:
--------------------------------------------------------------------------------
1 | /public
2 |
--------------------------------------------------------------------------------
/frontend/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "standard",
9 | "eslint:recommended",
10 | "plugin:react/recommended",
11 | "plugin:import/errors",
12 | "plugin:import/warnings",
13 | "plugin:jest/recommended"
14 | ],
15 | "globals": {
16 | "Atomics": "readonly",
17 | "SharedArrayBuffer": "readonly",
18 | "process": true,
19 | "page": true,
20 | "browser": true
21 | },
22 | "parser": "babel-eslint",
23 | "parserOptions": {
24 | "ecmaFeatures": {
25 | "jsx": true
26 | },
27 | "ecmaVersion": 2018,
28 | "sourceType": "module"
29 | },
30 | "plugins": [
31 | "react",
32 | "react-hooks",
33 | "unicorn",
34 | "jest"
35 | ],
36 | "rules": {
37 | "indent": [
38 | "error",
39 | 4
40 | ],
41 | "linebreak-style": [
42 | "error",
43 | "unix"
44 | ],
45 | "react/no-unescaped-entities": 0,
46 | "no-console": [
47 | "error",
48 | {
49 | "allow": [
50 | "info",
51 | "error"
52 | ]
53 | }
54 | ],
55 | "semi": [
56 | "error",
57 | "always"
58 | ],
59 | "array-callback-return": [
60 | "error"
61 | ],
62 | "no-useless-concat": [
63 | "error"
64 | ],
65 | "react/prop-types": 0,
66 | "react/jsx-max-props-per-line": [
67 | 1
68 | ],
69 | "react/jsx-indent": [
70 | 2,
71 | 4,
72 | {
73 | "checkAttributes": true
74 | }
75 | ],
76 | "react/jsx-closing-bracket-location": 2,
77 | "react/jsx-closing-tag-location": 2,
78 | "react/jsx-curly-spacing": [
79 | 2,
80 | {
81 | "when": "never",
82 | "children": true
83 | }
84 | ],
85 | "space-before-function-paren": [
86 | "error",
87 | "never"
88 | ],
89 | "import/order": [
90 | "error",
91 | {
92 | "groups": [
93 | "builtin",
94 | "external",
95 | "internal",
96 | "parent",
97 | "sibling",
98 | "index"
99 | ],
100 | "newlines-between": "always"
101 | }
102 | ],
103 | "unicorn/filename-case": [
104 | "error",
105 | {
106 | "case": "pascalCase"
107 | }
108 | ],
109 | "jsx-quotes": [
110 | "error",
111 | "prefer-single"
112 | ],
113 | "react-hooks/rules-of-hooks": "error",
114 | "react-hooks/exhaustive-deps": "warn"
115 | },
116 | "overrides": [
117 | {
118 | "files": [
119 | "src/ServiceWorker.js"
120 | ],
121 | "rules": {
122 | "no-console": [
123 | "off",
124 | ]
125 | }
126 | },
127 | {
128 | "files": [
129 | ".backstop/**",
130 | "I18nextScanner.Config.js",
131 | "e2e/jest-puppeteer-env.js",
132 | "src/ServiceWorker.js",
133 | "stories/**"
134 | ],
135 | "rules": {
136 | "no-console": [
137 | "off"
138 | ]
139 | }
140 | }
141 | ],
142 | "settings": {
143 | "react": {
144 | "pragma": "React",
145 | "version": "dectect"
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /build
--------------------------------------------------------------------------------
/frontend/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "processors": [
3 | "stylelint-processor-styled-components"
4 | ],
5 | "extends": [
6 | "stylelint-config-recommended",
7 | "stylelint-config-styled-components"
8 | ],
9 | "rules": {
10 | "function-name-case": "lower",
11 | "selector-pseudo-class-case": "lower",
12 | "selector-pseudo-element-case": "lower",
13 | "selector-type-case": "lower",
14 | "no-descending-specificity": null,
15 | "indentation": [
16 | 4
17 | ],
18 | "declaration-block-semicolon-newline-after": "always",
19 | "block-closing-brace-newline-after": "always",
20 | "block-opening-brace-newline-after": "always",
21 | "no-empty-first-line": true,
22 | "max-empty-lines": 1
23 | },
24 | "ignoreFiles": [
25 | "node_modules/**",
26 | "public/**"
27 | ]
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Dummy Contact Web Frontend based on React-Admin
2 |
3 | ## Getting started
4 |
5 | ```
6 | $ yarn install
7 | $ yarn run start
8 | ```
9 |
10 | Open your browser on http://localhost:3000
11 |
12 | ## Linter
13 |
14 | You can launch linter:
15 |
16 | ```
17 | $ yarn run lint
18 | $ yarn run lint:css
19 | ```
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dummy-contact",
3 | "private": true,
4 | "dependencies": {
5 | "@apollo/react-hooks": "3.1.5",
6 | "apollo-boost": "0.4.9",
7 | "apollo-upload-client": "13.0.0",
8 | "graphql": "15.3.0",
9 | "graphql-tag": "2.10.4",
10 | "prop-types": "15.7.2",
11 | "ra-data-graphql": "3.6.1",
12 | "ra-data-json-server": "3.7.1",
13 | "ra-postgraphile": "3.1.1",
14 | "react": "16.13.1",
15 | "react-admin": "3.7.1",
16 | "react-dom": "16.13.1",
17 | "react-scripts": "3.4.1"
18 | },
19 | "scripts": {
20 | "start": "BROWSER=none react-scripts start",
21 | "build": "react-scripts build",
22 | "lint": "eslint --fix .",
23 | "lint:css": "stylelint './**/*.js'"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "eslint": "6.6.0",
42 | "eslint-config-standard": "14.1.1",
43 | "eslint-plugin-import": "2.22.0",
44 | "eslint-plugin-jest": "23.18.0",
45 | "eslint-plugin-node": "11.1.0",
46 | "eslint-plugin-promise": "4.2.1",
47 | "eslint-plugin-react": "7.20.3",
48 | "eslint-plugin-react-hooks": "4.0.8",
49 | "eslint-plugin-standard": "4.0.1",
50 | "eslint-plugin-unicorn": "21.0.0",
51 | "stylelint": "13.6.1",
52 | "stylelint-config-recommended": "3.0.0",
53 | "stylelint-config-styled-components": "0.1.1",
54 | "stylelint-processor-styled-components": "1.10.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephane-klein/react-admin-and-postgraphile-playground/e53954370c58f63a5584ee663d331f5bb7c5d28d/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephane-klein/react-admin-and-postgraphile-playground/e53954370c58f63a5584ee663d331f5bb7c5d28d/frontend/public/logo192.png
--------------------------------------------------------------------------------
/frontend/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephane-klein/react-admin-and-postgraphile-playground/e53954370c58f63a5584ee663d331f5bb7c5d28d/frontend/public/logo512.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/Apollo.js:
--------------------------------------------------------------------------------
1 | import {
2 | ApolloClient as _ApolloClient,
3 | InMemoryCache
4 | } from 'apollo-boost';
5 | import { createUploadLink } from 'apollo-upload-client'
6 |
7 | const ApolloClient = new _ApolloClient({
8 | link: createUploadLink({
9 | uri: 'http://127.0.0.1:5000/graphql'
10 | }),
11 | cache: new InMemoryCache(),
12 | defaultOptions: {
13 | watchQuery: {
14 | fetchPolicy: 'no-cache',
15 | errorPolicy: 'ignore'
16 | },
17 | query: {
18 | fetchPolicy: 'no-cache',
19 | errorPolicy: 'all'
20 | }
21 | }
22 | });
23 |
24 | export default ApolloClient;
25 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { ApolloProvider } from '@apollo/react-hooks';
3 | import { Admin, Resource } from 'react-admin'
4 | import { useApolloClient } from '@apollo/react-hooks'
5 | import pgDataProvider from 'ra-postgraphile'
6 | import { ContactList, ContactEdit, ContactCreate } from './Contacts'
7 | import ApolloClient from './Apollo';
8 |
9 | const ReactAdminWrapper = () => {
10 | const [dataProvider, setDataProvider] = useState(null);
11 | const client = useApolloClient();
12 |
13 | useEffect(() => {
14 | (async () => {
15 | const dataProvider = await pgDataProvider(client);
16 | setDataProvider(() => dataProvider);
17 | })()
18 | }, [client]);
19 |
20 | return (
21 | dataProvider && (
22 |
23 |
29 |
30 | )
31 | );
32 | }
33 |
34 | const App = () => {
35 | return (
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | export default App;
--------------------------------------------------------------------------------
/frontend/src/Contacts.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List, Datagrid, Edit, Create, SimpleForm, DateField, TextField, EditButton, TextInput } from 'react-admin';
3 |
4 | export const ContactList = (props) => (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | const ContactTitle = ({ record }) => {
16 | return Contact {record ? `"${record.firstname}"` : ''};
17 | };
18 |
19 | export const ContactEdit = (props) => (
20 | } {...props}>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | export const ContactCreate = (props) => (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
--------------------------------------------------------------------------------
/frontend/src/ServiceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import App from './App';
5 | import * as serviceWorker from './ServiceWorker';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/scripts/enter-in-pg.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | cd "$(dirname "$0")/../"
5 |
6 | docker-compose exec postgres sh -c "psql -U \$POSTGRES_USER \$POSTGRES_DB"
--------------------------------------------------------------------------------
/scripts/load-fixtures.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | cd "$(dirname "$0")/../"
5 |
6 | docker exec $(docker-compose ps -q postgres) sh -c "rm -rf /sqls/"
7 | docker cp sqls/ $(docker-compose ps -q postgres):/sqls/
8 | docker-compose exec postgres sh -c "cd /sqls/ && psql --quiet -U \$POSTGRES_USER \$POSTGRES_DB -f /sqls/fixtures.sql"
--------------------------------------------------------------------------------
/scripts/load-seed.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | cd "$(dirname "$0")/../"
5 |
6 | docker exec $(docker-compose ps -q postgres) sh -c "rm -rf /sqls/"
7 | docker cp sqls/ $(docker-compose ps -q postgres):/sqls/
8 | docker-compose exec postgres sh -c "cd /sqls/ && psql --quiet -U \$POSTGRES_USER \$POSTGRES_DB -f /sqls/seed.sql"
--------------------------------------------------------------------------------
/scripts/up.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | cd "$(dirname "$0")/../"
5 |
6 | docker-compose up -d postgres
7 | ./scripts/wait-service.sh postgres 5432
8 |
9 | ./scripts/load-seed.sh
10 | ./scripts/load-fixtures.sh
11 |
12 | docker-compose up -d
13 |
--------------------------------------------------------------------------------
/scripts/wait-service.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # How to use:
4 | #
5 | # $ ./scripts/wait-service.sh postgres1 5432
6 | #
7 | set -e
8 |
9 | cd "$(dirname "$0")/../"
10 |
11 | NetworkID=$(docker inspect `docker-compose ps -q $1` | jq '.[0].NetworkSettings.Networks|map(select(true))[0].NetworkID' -r)
12 |
13 | docker run \
14 | -e TARGETS=$1:$2 \
15 | --network $NetworkID \
16 | --rm \
17 | waisbrot/wait
--------------------------------------------------------------------------------
/sqls/README.md:
--------------------------------------------------------------------------------
1 | # SQL guidelines
2 |
3 | Try to follow this SQL coding style: https://www.sqlstyle.guide/
4 |
5 | Try to use explicit field and table names.
6 |
--------------------------------------------------------------------------------
/sqls/fixtures.sql:
--------------------------------------------------------------------------------
1 | TRUNCATE public.contacts CASCADE;
2 |
3 | INSERT INTO public.contacts
4 | (
5 | id,
6 | email,
7 | firstname,
8 | lastname
9 | )
10 | SELECT
11 | ('1de9c987-08ab-32fe-e218-89c124cd' || to_char(seqnum, 'FM0000'))::uuid,
12 | 'firstname' || to_char(seqnum, 'FM0000') || '@example.com',
13 | 'firstname' || to_char(seqnum, 'FM0000'),
14 | 'lastname' || to_char(seqnum, 'FM0000')
15 | FROM
16 | GENERATE_SERIES(1, 50) seqnum;
--------------------------------------------------------------------------------
/sqls/seed.sql:
--------------------------------------------------------------------------------
1 | SET client_min_messages = error;
2 |
3 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public;
4 |
5 | DROP TABLE IF EXISTS public.contacts CASCADE;
6 |
7 | CREATE TABLE public.contacts (
8 | id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL,
9 | email VARCHAR(255) NOT NULL,
10 | firstname VARCHAR(255),
11 | lastname VARCHAR(255),
12 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
13 | );
14 |
15 | CREATE INDEX contacts_email_index ON public.contacts (email);
16 |
17 | SET client_min_messages = INFO;
18 |
--------------------------------------------------------------------------------