├── .gitignore
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── app.js
├── images
├── Matthew-Guay.jpg
└── oskar-yildiz-gy08FXeM2L4-unsplash.jpg
├── index.js
├── package-lock.json
├── package.json
├── routes
├── controllers
│ ├── deleteImage.js
│ ├── imageUpload.js
│ ├── persistImage.js
│ ├── retrieveImage.js
│ └── updateImage.js
└── routes.js
└── services
├── dbConnect.js
└── tutor.sql
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # use docker node 10
2 | FROM node:10
3 |
4 | # create a directory to run docker
5 | WORKDIR /app
6 |
7 | # copy package.json into the new directory
8 | COPY package.json /app
9 |
10 | # install the dependencies
11 | RUN npm install
12 |
13 | # copy all other files into the app directory
14 | COPY . /app
15 |
16 | # open port 5000
17 | EXPOSE 5000
18 |
19 | # run the server
20 | CMD node index.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 NJOKU SAMSON EBERE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Server-tutorial
2 | This is a tutorial was to teach how to create a simple, secure and robust nodejs server but we have expanded our scope to cloudinary and postgres
3 |
4 | **Full details on how to build out this server is found [here](https://dev.to/ebereplenty/setting-up-a-simple-secure-and-robust-node-js-server-10n0)**.
5 |
6 | **Full details on how to upload images to cloudinary using nodejs is found [here](https://dev.to/ebereplenty/image-upload-to-cloudinary-with-nodejs-and-dotenv-4fen)**.
7 |
8 | **Full details on how to persist and retrieve images to cloudinary using nodejs and postgres is found [here](https://dev.to/ebereplenty/cloudinary-and-postgresql-persisting-and-retrieving-images-using-nodejs-31b2)**.
9 |
10 | **Full details on how to delete and update images to cloudinary using nodejs and postgres is found [here](https://t.co/XDR1BtlHNI?amp=1)**.
11 |
12 | **Full details on Nodejs Code Structure Optimization With Express Routing is found [here](https://t.co/vJvTCd2KdF?amp=1)**.
13 |
14 | ## Dependences
15 | - [Express](https://www.npmjs.com/package/express)
16 | - [Cloudinary](https://cloudinary.com/)
17 | - [Node](http://nodejs.org/)
18 | - [NPM](https://www.npmjs.com/)
19 | - [DotENV](https://www.npmjs.com/package/dotenv)
20 | - [Nodemon](https://www.npmjs.com/package/nodemon)
21 | - [Node Postgres](https://node-postgres.com/)
22 |
23 |
24 | ## SETTING UP
25 | - Fork this repository
26 | - Clone the repositury to your machine
27 | - Open up a terminal
28 | - Navigate into the project directory
29 | - Run npm install
to install all needed dependencies
30 | - Run nodemon index
to spin up the server
31 | - The server runs on port 3000 http://localhost:3000/
32 |
33 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const app = express();
3 |
4 | // import the routes file
5 | const routes = require("./routes/routes")
6 |
7 | // body parser configuration
8 | const bodyParser = require("body-parser");
9 | app.use(bodyParser.json());
10 | app.use(bodyParser.urlencoded({ extended: true }));
11 |
12 | // register the routes
13 | app.use('/', routes);
14 |
15 | module.exports = app;
16 |
--------------------------------------------------------------------------------
/images/Matthew-Guay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EBEREGIT/server-tutorial/48498c187f98793154f89b3a332809c3a6554a19/images/Matthew-Guay.jpg
--------------------------------------------------------------------------------
/images/oskar-yildiz-gy08FXeM2L4-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EBEREGIT/server-tutorial/48498c187f98793154f89b3a332809c3a6554a19/images/oskar-yildiz-gy08FXeM2L4-unsplash.jpg
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const app = require('./app');
3 |
4 | const normalizePort = val => {
5 | const port = parseInt(val, 10);
6 |
7 | if (isNaN(port)) {
8 | return val;
9 | }
10 | if (port >= 0) {
11 | return port;
12 | }
13 | return false;
14 | };
15 | const port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | const errorHandler = error => {
19 | if (error.syscall !== 'listen') {
20 | throw error;
21 | }
22 | const address = server.address();
23 | const bind = typeof address === 'string' ? 'pipe ' + address : 'port: ' + port;
24 | switch (error.code) {
25 | case 'EACCES':
26 | console.error(bind + ' requires elevated privileges.');
27 | process.exit(1);
28 | break;
29 | case 'EADDRINUSE':
30 | console.error(bind + ' is already in use.');
31 | process.exit(1);
32 | break;
33 | default:
34 | throw error;
35 | }
36 | };
37 |
38 | const server = http.createServer(app);
39 |
40 | server.on('error', errorHandler);
41 | server.on('listening', () => {
42 | const address = server.address();
43 | const bind = typeof address === 'string' ? 'pipe ' + address : 'port ' + port;
44 | console.log('Listening on ' + bind);
45 | });
46 |
47 | server.listen(port);
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server-tutorial",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "create": "node ./services/dbConnect createTables"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "body-parser": "^1.19.0",
13 | "cloudinary": "^1.19.0",
14 | "dotenv": "^8.2.0",
15 | "express": "^4.17.1",
16 | "heroku": "^7.41.1",
17 | "make-runnable": "^1.3.6",
18 | "pg": "^8.0.3"
19 | },
20 | "devDependencies": {
21 | "nodemon": "^2.0.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/routes/controllers/deleteImage.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 | require("dotenv").config();
3 | const db = require("../../services/dbConnect");
4 |
5 | // cloudinary configuration
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUD_NAME,
8 | api_key: process.env.API_KEY,
9 | api_secret: process.env.API_SECRET,
10 | });
11 |
12 | exports.deleteImage = (request, response) => {
13 | // unique ID
14 | const { cloudinary_id } = request.params;
15 |
16 | // delete image from cloudinary first
17 | cloudinary.uploader
18 | .destroy(cloudinary_id)
19 |
20 | // delete image record from postgres also
21 | .then(() => {
22 | db.pool.connect((err, client) => {
23 | // delete query
24 | const deleteQuery = "DELETE FROM images WHERE cloudinary_id = $1";
25 | const deleteValue = [cloudinary_id];
26 |
27 | // execute delete query
28 | client
29 | .query(deleteQuery, deleteValue)
30 | .then((deleteResult) => {
31 | response.status(200).send({
32 | message: "Image Deleted Successfully!",
33 | deleteResult,
34 | });
35 | })
36 | .catch((e) => {
37 | response.status(500).send({
38 | message: "Image Couldn't be Deleted!",
39 | e,
40 | });
41 | });
42 | });
43 | })
44 | .catch((error) => {
45 | response.status(500).send({
46 | message: "Failure",
47 | error,
48 | });
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/routes/controllers/imageUpload.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 | require("dotenv").config();
3 |
4 | // cloudinary configuration
5 | cloudinary.config({
6 | cloud_name: process.env.CLOUD_NAME,
7 | api_key: process.env.API_KEY,
8 | api_secret: process.env.API_SECRET,
9 | });
10 |
11 | exports.imageUpload = (request, response) => {
12 | // collected image from a user
13 | const data = {
14 | image: request.body.image,
15 | };
16 |
17 | // upload image here
18 | cloudinary.uploader
19 | .upload(data.image)
20 | .then((result) => {
21 | response.status(200).send({
22 | message: "success",
23 | result,
24 | });
25 | })
26 | .catch((error) => {
27 | response.status(500).send({
28 | message: "failure",
29 | error,
30 | });
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/routes/controllers/persistImage.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 | require("dotenv").config();
3 | const db = require("../../services/dbConnect");
4 |
5 | // cloudinary configuration
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUD_NAME,
8 | api_key: process.env.API_KEY,
9 | api_secret: process.env.API_SECRET,
10 | });
11 |
12 | exports.persistImage = (request, response) => {
13 | // collected image from a user
14 | const data = {
15 | title: request.body.title,
16 | image: request.body.image,
17 | };
18 |
19 | // upload image here
20 | cloudinary.uploader
21 | .upload(data.image)
22 | .then((image) => {
23 | db.pool.connect((err, client) => {
24 | // inset query to run if the upload to cloudinary is successful
25 | const insertQuery =
26 | "INSERT INTO images (title, cloudinary_id, image_url) VALUES($1,$2,$3) RETURNING *";
27 | const values = [data.title, image.public_id, image.secure_url];
28 |
29 | // execute query
30 | client
31 | .query(insertQuery, values)
32 | .then((result) => {
33 | result = result.rows[0];
34 |
35 | // send success response
36 | response.status(201).send({
37 | status: "success",
38 | data: {
39 | message: "Image Uploaded Successfully",
40 | title: result.title,
41 | cloudinary_id: result.cloudinary_id,
42 | image_url: result.image_url,
43 | },
44 | });
45 | })
46 | .catch((e) => {
47 | response.status(500).send({
48 | message: "failure",
49 | e,
50 | });
51 | });
52 | });
53 | })
54 | .catch((error) => {
55 | response.status(500).send({
56 | message: "failure",
57 | error,
58 | });
59 | });
60 | }
--------------------------------------------------------------------------------
/routes/controllers/retrieveImage.js:
--------------------------------------------------------------------------------
1 | const db = require("../../services/dbConnect");
2 |
3 | exports.retrieveImage = (request, response) => {
4 | // data from user
5 | const { cloudinary_id } = request.params;
6 |
7 | db.pool.connect((err, client) => {
8 | // query to find image
9 | const retrieveQuery = "SELECT * FROM images WHERE cloudinary_id = $1";
10 | const value = [cloudinary_id];
11 |
12 | // execute query
13 | client
14 | .query(retrieveQuery, value)
15 | .then((output) => {
16 | response.status(200).send({
17 | status: "success",
18 | data: {
19 | message: "Image Retrieved Successfully!",
20 | id: output.rows[0].cloudinary_id,
21 | title: output.rows[0].title,
22 | url: output.rows[0].image_url,
23 | },
24 | });
25 | })
26 | .catch((error) => {
27 | response.status(401).send({
28 | status: "failure",
29 | data: {
30 | message: "could not retrieve record!",
31 | error,
32 | },
33 | });
34 | });
35 | });
36 | }
--------------------------------------------------------------------------------
/routes/controllers/updateImage.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 | require("dotenv").config();
3 | const db = require("../../services/dbConnect");
4 |
5 | // cloudinary configuration
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUD_NAME,
8 | api_key: process.env.API_KEY,
9 | api_secret: process.env.API_SECRET,
10 | });
11 |
12 | exports.updateImage = (request, response) => {
13 | // unique ID
14 | const { cloudinary_id } = request.params;
15 |
16 | // collected image from a user
17 | const data = {
18 | title: request.body.title,
19 | image: request.body.image,
20 | };
21 |
22 | // delete image from cloudinary first
23 | cloudinary.uploader
24 | .destroy(cloudinary_id)
25 |
26 | // upload image here
27 | .then(() => {
28 | cloudinary.uploader
29 | .upload(data.image)
30 |
31 | // update the database here
32 | .then((result) => {
33 | db.pool.connect((err, client) => {
34 | // update query
35 | const updateQuery =
36 | "UPDATE images SET title = $1, cloudinary_id = $2, image_url = $3 WHERE cloudinary_id = $4";
37 | const value = [
38 | data.title,
39 | result.public_id,
40 | result.secure_url,
41 | cloudinary_id,
42 | ];
43 |
44 | // execute query
45 | client
46 | .query(updateQuery, value)
47 | .then(() => {
48 | // send success response
49 | response.status(201).send({
50 | status: "success",
51 | data: {
52 | message: "Image Updated Successfully",
53 | },
54 | });
55 | })
56 | .catch((e) => {
57 | response.status(500).send({
58 | message: "Update Failed",
59 | e,
60 | });
61 | });
62 | });
63 | })
64 | .catch((err) => {
65 | response.status(500).send({
66 | message: "failed",
67 | err,
68 | });
69 | });
70 | })
71 | .catch((error) => {
72 | response.status(500).send({
73 | message: "failed",
74 | error,
75 | });
76 | });
77 | }
--------------------------------------------------------------------------------
/routes/routes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const imageUpload = require("./controllers/imageUpload");
4 | const persistImage = require("./controllers/persistImage");
5 | const retrieveImage = require("./controllers/retrieveImage");
6 | const updateImage = require("./controllers/updateImage");
7 | const deleteImage = require("./controllers/deleteImage");
8 |
9 | router.get("/", (request, response, next) => {
10 | response.json({ message: "Hey! This is your server response!" });
11 | next();
12 | });
13 |
14 | // image upload API
15 | router.post("/image-upload", imageUpload.imageUpload);
16 |
17 | // persist image
18 | router.post("/persist-image", persistImage.persistImage);
19 |
20 | // retrieve image
21 | router.get("/retrieve-image/:cloudinary_id", retrieveImage.retrieveImage);
22 |
23 | // delete image
24 | router.delete("/delete-image/:cloudinary_id", deleteImage.deleteImage);
25 |
26 | // update image
27 | router.put("/update-image/:cloudinary_id", updateImage.updateImage);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/services/dbConnect.js:
--------------------------------------------------------------------------------
1 | const pg = require('pg');
2 | require('dotenv').config();
3 |
4 | // set production variable. This will be called when deployed to a live host
5 | const isProduction = process.env.NODE_ENV === 'production';
6 |
7 | // configuration details
8 | const connectionString = `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_DATABASE}`;
9 |
10 | // if project has been deployed, connect with the host's DATABASE_URL
11 | // else connect with the local DATABASE_URL
12 | const pool = new pg.Pool({
13 | connectionString: isProduction ? process.env.DATABASE_URL : connectionString,
14 | ssl: isProduction,
15 | });
16 |
17 | // display message on success message if successful
18 | pool.on('connect', () => {
19 | console.log('Teamwork Database connected successfully!');
20 | });
21 |
22 |
23 | const createTables = () => {
24 | const imageTable = `CREATE TABLE IF NOT EXISTS
25 | images(
26 | id SERIAL PRIMARY KEY,
27 | title VARCHAR(128) NOT NULL,
28 | cloudinary_id VARCHAR(128) NOT NULL,
29 | image_url VARCHAR(128) NOT NULL
30 | )`;
31 | pool
32 | .query(imageTable)
33 | .then((res) => {
34 | console.log(res);
35 | pool.end();
36 | })
37 | .catch((err) => {
38 | console.log(err);
39 | pool.end();
40 | });
41 | };
42 |
43 | pool.on("remove", () => {
44 | console.log("client removed");
45 | process.exit(0);
46 | });
47 |
48 | //export pool and createTables to be accessible from an where within the application
49 | module.exports = {
50 | createTables,
51 | pool,
52 | };
53 |
54 | require("make-runnable");
55 |
--------------------------------------------------------------------------------
/services/tutor.sql:
--------------------------------------------------------------------------------
1 | --
2 | -- PostgreSQL database dump
3 | --
4 |
5 | -- Dumped from database version 12.1
6 | -- Dumped by pg_dump version 12.1
7 |
8 | -- Started on 2020-05-26 11:02:31
9 |
10 | SET statement_timeout = 0;
11 | SET lock_timeout = 0;
12 | SET idle_in_transaction_session_timeout = 0;
13 | SET client_encoding = 'UTF8';
14 | SET standard_conforming_strings = on;
15 | SELECT pg_catalog.set_config('search_path', '', false);
16 | SET check_function_bodies = false;
17 | SET xmloption = content;
18 | SET client_min_messages = warning;
19 | SET row_security = off;
20 |
21 | SET default_tablespace = '';
22 |
23 | SET default_table_access_method = heap;
24 |
25 | --
26 | -- TOC entry 203 (class 1259 OID 49321)
27 | -- Name: images; Type: TABLE; Schema: public; Owner: tutorial
28 | --
29 |
30 | CREATE TABLE public.images (
31 | id integer NOT NULL,
32 | title character varying(128) NOT NULL,
33 | cloudinary_id character varying(128) NOT NULL,
34 | image_url character varying(128) NOT NULL
35 | );
36 |
37 |
38 | ALTER TABLE public.images OWNER TO tutorial;
39 |
40 | --
41 | -- TOC entry 202 (class 1259 OID 49319)
42 | -- Name: images_id_seq; Type: SEQUENCE; Schema: public; Owner: tutorial
43 | --
44 |
45 | CREATE SEQUENCE public.images_id_seq
46 | AS integer
47 | START WITH 1
48 | INCREMENT BY 1
49 | NO MINVALUE
50 | NO MAXVALUE
51 | CACHE 1;
52 |
53 |
54 | ALTER TABLE public.images_id_seq OWNER TO tutorial;
55 |
56 | --
57 | -- TOC entry 2823 (class 0 OID 0)
58 | -- Dependencies: 202
59 | -- Name: images_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: tutorial
60 | --
61 |
62 | ALTER SEQUENCE public.images_id_seq OWNED BY public.images.id;
63 |
64 |
65 | --
66 | -- TOC entry 2687 (class 2604 OID 49324)
67 | -- Name: images id; Type: DEFAULT; Schema: public; Owner: tutorial
68 | --
69 |
70 | ALTER TABLE ONLY public.images ALTER COLUMN id SET DEFAULT nextval('public.images_id_seq'::regclass);
71 |
72 |
73 | --
74 | -- TOC entry 2817 (class 0 OID 49321)
75 | -- Dependencies: 203
76 | -- Data for Name: images; Type: TABLE DATA; Schema: public; Owner: tutorial
77 | --
78 |
79 | COPY public.images (id, title, cloudinary_id, image_url) FROM stdin;
80 | 8 Heroku Image ywdrgacv79cg18ap0w7l https://res.cloudinary.com/dunksyqjj/image/upload/v1590431624/ywdrgacv79cg18ap0w7l.jpg
81 | \.
82 |
83 |
84 | --
85 | -- TOC entry 2824 (class 0 OID 0)
86 | -- Dependencies: 202
87 | -- Name: images_id_seq; Type: SEQUENCE SET; Schema: public; Owner: tutorial
88 | --
89 |
90 | SELECT pg_catalog.setval('public.images_id_seq', 8, true);
91 |
92 |
93 | --
94 | -- TOC entry 2689 (class 2606 OID 49326)
95 | -- Name: images images_pkey; Type: CONSTRAINT; Schema: public; Owner: tutorial
96 | --
97 |
98 | ALTER TABLE ONLY public.images
99 | ADD CONSTRAINT images_pkey PRIMARY KEY (id);
100 |
101 |
102 | -- Completed on 2020-05-26 11:02:33
103 |
104 | --
105 | -- PostgreSQL database dump complete
106 | --
107 |
108 |
--------------------------------------------------------------------------------