├── .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 | --------------------------------------------------------------------------------