Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs(examples): add TODO example with Postgres and Node.js cluster
- Loading branch information
1 parent
d12aab2
commit be3d7f0
Showing
10 changed files
with
403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
examples/basic-crud-application/server-postgres-cluster/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
|
||
A basic TODO project. | ||
|
||
| Characteristic | | | ||
|----------------|-------------------------------------------------------------------------------------------| | ||
| Language | plain JavaScript | | ||
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | | ||
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) | | ||
|
||
## Usage | ||
|
||
``` | ||
$ docker-compose up -d | ||
$ npm install | ||
$ npm start | ||
``` |
9 changes: 9 additions & 0 deletions
9
examples/basic-crud-application/server-postgres-cluster/docker-compose.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
version: "3" | ||
|
||
services: | ||
postgres: | ||
image: postgres:12 | ||
ports: | ||
- "5432:5432" | ||
environment: | ||
POSTGRES_PASSWORD: "changeit" |
26 changes: 26 additions & 0 deletions
26
examples/basic-crud-application/server-postgres-cluster/lib/app.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Server } from "socket.io"; | ||
import createTodoHandlers from "./todo-management/todo.handlers.js"; | ||
import { setupWorker } from "@socket.io/sticky"; | ||
import { createAdapter } from "@socket.io/postgres-adapter"; | ||
|
||
export function createApplication(httpServer, components, serverOptions = {}) { | ||
const io = new Server(httpServer, serverOptions); | ||
|
||
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } = | ||
createTodoHandlers(components); | ||
|
||
io.on("connection", (socket) => { | ||
socket.on("todo:create", createTodo); | ||
socket.on("todo:read", readTodo); | ||
socket.on("todo:update", updateTodo); | ||
socket.on("todo:delete", deleteTodo); | ||
socket.on("todo:list", listTodo); | ||
}); | ||
|
||
// enable sticky session in the cluster (to remove in standalone mode) | ||
setupWorker(io); | ||
|
||
io.adapter(createAdapter(components.connectionPool)); | ||
|
||
return io; | ||
} |
28 changes: 28 additions & 0 deletions
28
examples/basic-crud-application/server-postgres-cluster/lib/cluster.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import cluster from "cluster"; | ||
import { createServer } from "http"; | ||
import { setupMaster } from "@socket.io/sticky"; | ||
import { cpus } from "os"; | ||
|
||
if (cluster.isMaster) { | ||
console.log(`Master ${process.pid} is running`); | ||
const httpServer = createServer(); | ||
|
||
setupMaster(httpServer, { | ||
loadBalancingMethod: "least-connection", | ||
}); | ||
|
||
httpServer.listen(3000); | ||
|
||
for (let i = 0; i < cpus().length; i++) { | ||
cluster.fork(); | ||
} | ||
|
||
cluster.on("exit", (worker) => { | ||
console.log(`Worker ${worker.process.pid} died`); | ||
cluster.fork(); | ||
}); | ||
} else { | ||
console.log(`Worker ${process.pid} started`); | ||
|
||
import("./index.js"); | ||
} |
51 changes: 51 additions & 0 deletions
51
examples/basic-crud-application/server-postgres-cluster/lib/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { createServer } from "http"; | ||
import { createApplication } from "./app.js"; | ||
import { Sequelize } from "sequelize"; | ||
import pg from "pg"; | ||
import { PostgresTodoRepository } from "./todo-management/todo.repository.js"; | ||
|
||
const httpServer = createServer(); | ||
|
||
const sequelize = new Sequelize("postgres", "postgres", "changeit", { | ||
dialect: "postgres", | ||
}); | ||
|
||
const connectionPool = new pg.Pool({ | ||
user: "postgres", | ||
host: "localhost", | ||
database: "postgres", | ||
password: "changeit", | ||
port: 5432, | ||
}); | ||
|
||
createApplication( | ||
httpServer, | ||
{ | ||
connectionPool, | ||
todoRepository: new PostgresTodoRepository(sequelize), | ||
}, | ||
{ | ||
cors: { | ||
origin: ["http://localhost:4200"], | ||
}, | ||
} | ||
); | ||
|
||
const main = async () => { | ||
// create the tables if they do not exist already | ||
await sequelize.sync(); | ||
|
||
// create the table needed by the postgres adapter | ||
await connectionPool.query(` | ||
CREATE TABLE IF NOT EXISTS socket_io_attachments ( | ||
id bigserial UNIQUE, | ||
created_at timestamptz DEFAULT NOW(), | ||
payload bytea | ||
); | ||
`); | ||
|
||
// uncomment when running in standalone mode | ||
// httpServer.listen(3000); | ||
}; | ||
|
||
main(); |
140 changes: 140 additions & 0 deletions
140
examples/basic-crud-application/server-postgres-cluster/lib/todo-management/todo.handlers.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js"; | ||
import { v4 as uuid } from "uuid"; | ||
import Joi from "joi"; | ||
|
||
const idSchema = Joi.string().guid({ | ||
version: "uuidv4", | ||
}); | ||
|
||
const todoSchema = Joi.object({ | ||
id: idSchema.alter({ | ||
create: (schema) => schema.forbidden(), | ||
update: (schema) => schema.required(), | ||
}), | ||
title: Joi.string().max(256).required(), | ||
completed: Joi.boolean().required(), | ||
}); | ||
|
||
export default function (components) { | ||
const { todoRepository } = components; | ||
return { | ||
createTodo: async function (payload, callback) { | ||
const socket = this; | ||
|
||
// validate the payload | ||
const { error, value } = todoSchema.tailor("create").validate(payload, { | ||
abortEarly: false, | ||
stripUnknown: true, | ||
}); | ||
|
||
if (error) { | ||
return callback({ | ||
error: Errors.INVALID_PAYLOAD, | ||
errorDetails: mapErrorDetails(error.details), | ||
}); | ||
} | ||
|
||
value.id = uuid(); | ||
|
||
// persist the entity | ||
try { | ||
await todoRepository.save(value); | ||
} catch (e) { | ||
return callback({ | ||
error: sanitizeErrorMessage(e), | ||
}); | ||
} | ||
|
||
// acknowledge the creation | ||
callback({ | ||
data: value.id, | ||
}); | ||
|
||
// notify the other users | ||
socket.broadcast.emit("todo:created", value); | ||
}, | ||
|
||
readTodo: async function (id, callback) { | ||
const { error } = idSchema.validate(id); | ||
|
||
if (error) { | ||
return callback({ | ||
error: Errors.ENTITY_NOT_FOUND, | ||
}); | ||
} | ||
|
||
try { | ||
const todo = await todoRepository.findById(id); | ||
callback({ | ||
data: todo, | ||
}); | ||
} catch (e) { | ||
callback({ | ||
error: sanitizeErrorMessage(e), | ||
}); | ||
} | ||
}, | ||
|
||
updateTodo: async function (payload, callback) { | ||
const socket = this; | ||
|
||
const { error, value } = todoSchema.tailor("update").validate(payload, { | ||
abortEarly: false, | ||
stripUnknown: true, | ||
}); | ||
|
||
if (error) { | ||
return callback({ | ||
error: Errors.INVALID_PAYLOAD, | ||
errorDetails: mapErrorDetails(error.details), | ||
}); | ||
} | ||
|
||
try { | ||
await todoRepository.save(value); | ||
} catch (e) { | ||
return callback({ | ||
error: sanitizeErrorMessage(e), | ||
}); | ||
} | ||
|
||
callback(); | ||
socket.broadcast.emit("todo:updated", value); | ||
}, | ||
|
||
deleteTodo: async function (id, callback) { | ||
const socket = this; | ||
|
||
const { error } = idSchema.validate(id); | ||
|
||
if (error) { | ||
return callback({ | ||
error: Errors.ENTITY_NOT_FOUND, | ||
}); | ||
} | ||
|
||
try { | ||
await todoRepository.deleteById(id); | ||
} catch (e) { | ||
return callback({ | ||
error: sanitizeErrorMessage(e), | ||
}); | ||
} | ||
|
||
callback(); | ||
socket.broadcast.emit("todo:deleted", id); | ||
}, | ||
|
||
listTodo: async function (callback) { | ||
try { | ||
callback({ | ||
data: await todoRepository.findAll(), | ||
}); | ||
} catch (e) { | ||
callback({ | ||
error: sanitizeErrorMessage(e), | ||
}); | ||
} | ||
}, | ||
}; | ||
} |
74 changes: 74 additions & 0 deletions
74
...les/basic-crud-application/server-postgres-cluster/lib/todo-management/todo.repository.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { Errors } from "../util.js"; | ||
import { Model, DataTypes } from "sequelize"; | ||
|
||
class CrudRepository { | ||
findAll() {} | ||
findById(id) {} | ||
save(entity) {} | ||
deleteById(id) {} | ||
} | ||
|
||
export class TodoRepository extends CrudRepository {} | ||
|
||
class Todo extends Model {} | ||
|
||
export class PostgresTodoRepository extends TodoRepository { | ||
constructor(sequelize) { | ||
super(); | ||
this.sequelize = sequelize; | ||
|
||
Todo.init( | ||
{ | ||
id: { | ||
type: DataTypes.STRING, | ||
primaryKey: true, | ||
allowNull: false, | ||
}, | ||
title: { | ||
type: DataTypes.STRING, | ||
}, | ||
completed: { | ||
type: DataTypes.BOOLEAN, | ||
}, | ||
}, | ||
{ | ||
sequelize, | ||
tableName: "todos", | ||
} | ||
); | ||
} | ||
|
||
findAll() { | ||
return this.sequelize.transaction((transaction) => { | ||
return Todo.findAll({ transaction }); | ||
}); | ||
} | ||
|
||
async findById(id) { | ||
return this.sequelize.transaction(async (transaction) => { | ||
const todo = await Todo.findByPk(id, { transaction }); | ||
|
||
if (!todo) { | ||
throw Errors.ENTITY_NOT_FOUND; | ||
} | ||
|
||
return todo; | ||
}); | ||
} | ||
|
||
save(entity) { | ||
return this.sequelize.transaction((transaction) => { | ||
return Todo.upsert(entity, { transaction }); | ||
}); | ||
} | ||
|
||
async deleteById(id) { | ||
return this.sequelize.transaction(async (transaction) => { | ||
const count = await Todo.destroy({ where: { id }, transaction }); | ||
|
||
if (count === 0) { | ||
throw Errors.ENTITY_NOT_FOUND; | ||
} | ||
}); | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
examples/basic-crud-application/server-postgres-cluster/lib/util.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export const Errors = { | ||
ENTITY_NOT_FOUND: "entity not found", | ||
INVALID_PAYLOAD: "invalid payload", | ||
}; | ||
|
||
const errorValues = Object.values(Errors); | ||
|
||
export function sanitizeErrorMessage(message) { | ||
if (typeof message === "string" && errorValues.includes(message)) { | ||
return message; | ||
} else { | ||
return "an unknown error has occurred"; | ||
} | ||
} | ||
|
||
export function mapErrorDetails(details) { | ||
return details.map((item) => ({ | ||
message: item.message, | ||
path: item.path, | ||
type: item.type, | ||
})); | ||
} |
Oops, something went wrong.