A PostgreSQL-backed @imqueue RPC microservice that manages the washing schedule — reservations, the working-hours configuration, and the rule that keeps a car from being double-booked.
It stores reservations as time ranges (tstzrange) and exposes the station's options (opening
hours, number of boxes, washing types and their durations). It never speaks HTTP; every method
is exposed over the @imqueue/rpc Redis message queue and consumed by the gateways (api,
api-rest).
Persistence goes through @imqueue/pg-sequelize, the framework's Sequelize toolkit — see Data layer.
This repo is one piece of the imqueue-sandbox tutorial — a complete car-wash booking app built from independent RPC microservices that communicate over a Redis-backed message queue.
| Repo | Role | Store |
|---|---|---|
| user | Customer accounts & their garage | MongoDB |
| auth | Login, JWT issuing & revocation | Redis |
| car | Car catalog (makes / models / types) | in-memory |
| time-table | Washing reservations & schedule | PostgreSQL |
| api | GraphQL gateway orchestrating the fleet | — |
| api-rest | REST/OpenAPI gateway over the same fleet | — |
| web-app | React front-end on api (GraphQL/Relay) |
— |
| web-app-rest | React front-end on api-rest (REST) |
— |
The backend services are transport-agnostic: two interchangeable gateways and two independent front-ends prove the same fleet can be fronted by completely different API styles without changing a single service.
Exposed by the TimeTable service (src/TimeTable.ts) via @expose():
| Method | Signature | Description |
|---|---|---|
version |
() |
Running service name / version / repository. |
config |
() |
Time-table options: working hours, box count, washing types. |
list |
(date?, fields?) |
Reservations for the given day (defaults to today). |
fetch |
(id, fields?) |
A single reservation by id, or null. |
reserve |
(reservation, fields?) |
Create a reservation; returns that day's updated list. |
cancel |
(id, fields?) |
Soft-delete a reservation; returns that day's updated list. |
A reservation is { id, carId, userId, type, duration }, where type is fast / std /
full and duration is a [start, end) timestamp range. The default station options are
08:00–21:00, 4 boxes, and washing types fast (30 min), std (45 min),
full (60 min).
reserve is guarded at the database level by a unique index (car_duplicate_idx) on
(carId, day-of-duration, coalesced deletedAt): a car may hold at most one active reservation
per calendar day. A PostgreSQL unique-violation (23505) is translated into the friendly error
"Time for given car has been already reserved at this date!". Reservations are paranoid
(soft-deleted), so cancelling frees the slot again. The schema (including the index and a
range_date() helper function) is created automatically on start via orm.sync() + migration.
The ORM stack comes from @imqueue/pg-sequelize,
which is the single import surface for it — models import Table, Column, DataType and the
rest from there rather than from sequelize-typescript directly.
| Piece | Where | What it does |
|---|---|---|
dbConfig |
config.ts |
IMQORMOptions: connection string, pool, SQL logging, and the path the models are discovered under. |
connect() |
src/orm/index.ts |
database(dbConfig) — the process-wide connection singleton, with the models registered. |
migrate() |
src/orm/index.ts |
orm.sync(), then the range_date() function and the composite unique index. |
Reservation |
src/orm/models/Reservation.ts |
The model — and, via @classType()/@property(), the RPC wire type as well. |
Two details worth knowing:
- Models live in their own directory.
database()walksdbConfig.modelsPathfor compiled.jsfiles and takes from each the export named after the file, sosrc/orm/models/holds one model per file and nothing else (index.jsthere would hand Sequelizeundefined). - Indices are declared on the columns. The GiST index that makes range containment — the one
query every read runs — use an index is
@ColumnIndex({ method: IndexMethod.GIST })onduration. The double-booking guard is a key of one column plus two expressions, which is more than@ColumnIndexcan express, so that one is an explicit statement inmigrate().
Reads build their statement with query.autoQuery(), which narrows the SELECT to the columns
the caller asked for — and drops names that are not columns, falling back to the primary key — so
a gateway passing its GraphQL selection set through gets exactly those columns and nothing else.
Environment variables (loaded from an optional .env via process.loadEnvFile()):
| Variable | Default | Purpose |
|---|---|---|
DB_CONN_STR |
postgres://tutmq:tutmq@localhost:5432/tutmq |
PostgreSQL connection string. |
DB_POOL_MAX |
250 |
Maximum Sequelize connection-pool size. |
SQL_LOG |
0 |
Set to 1 to log every statement the service issues. |
SQL_PRETTIFY |
0 |
Set to 1 to break logged SQL across lines (read by @imqueue/pg-sequelize). |
SQL_COLORIZE |
0 |
Set to 1 to colourise logged SQL. |
IMQ_REDIS |
localhost:6379 |
Redis endpoint(s) for the RPC message queue. |
Development mode (rebuilds and restarts on change):
npm run devProduction mode:
npm startBoth start the service under the imqueue label time-table. Requires a reachable PostgreSQL at
DB_CONN_STR and Redis at IMQ_REDIS.