As we've seen in the previous article, there's a small inconvenient with Prisma ORM that we'll solve by rewriting it entirely (cause who doesn't want another npm package) We'll go step by step, explaining how things work thus this series of post might be a bit long.
In our process of creating an ORM, we will create StORM, which stands for Storm Object Relational Mapping or Storm ORM. It will be open source and located in this repository.
That being said, let's start ๐ช๏ธ
PostgreSQL Database
First, we need a database to query. I'll use Docker to run one locally with one command. I set simple credentials since it is for local development but feel free to override them.
docker run --name storm-test-db \
-e POSTGRES_PASSWORD=storm-pass \
-e POSTGRES_USER=storm-user \
-e POSTGRES_DB=storm-database \
-p 5432:5432 -d postgres:15.2-alpine
Once this command is successful, you should have a database running on your port 5432. The connection URL should be something like this :
postgres://storm-user:storm-pass@localhost:5432/storm-database
Database Tables
The database is running, now let's add a few tables so we can build the library before having to create a migration system.
Suppose we were working on a simple CMS, we would need to create 3 tables: Users, Posts and CoverImages. Tables are linked together so that users have several posts, and each post has a single cover image.
We can write SQL queries to create each table with a bunch of properties like this:
Users
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
);
Posts
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
user_id INT,
publication_date timestamp NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
CoverImages
CREATE TABLE IF NOT EXISTS cover_images (
id SERIAL PRIMARY KEY,
url VARCHAR(255) NOT NULL,
post_id INT UNIQUE,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts (id)
);
Running the scripts
We need to send these queries to our database and we'll again use Docker to connect to PostgreSQL inside the container.
docker exec -it storm-test-db psql -U storm-user -W storm-database
This should ask you for the password we used to create the database, then you can paste the 3 SQL queries in order and execute them. Finally, type exit
to close the terminal.
Node.Js Typescript Project
Now that our database is set up we can init the typescript project for our lib. Pretty simple and straight-forward instructions with pnpm
, notice that you can use npm
or yarn
as well.
# init nodejs typescript project
pnpm init
pnpm i -D tsc typescript @types/node
pnpm tsc --init --outDir dist
touch index.ts
Next, we need to add a few libraries to be used in our project. We will also create a .env
file that will hold our environment variables, specifically our database URL.
pnpm i pg dotenv
pnpm i -D @types/pg
# file : .env
DATABASE_URL=postgres://storm-user:storm-pass@localhost:5432/storm-database
We will also add vitest
for Test-Driven-Development and create a vitest.config.ts
file.
pnpm i -D vite vitest
mkdir tests
touch vitest.config.ts tests/index.ts
// file : vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
dir: "tests",
include: ["**/*.spec.ts"],
environment: "node",
passWithNoTests: true,
},
});
The last step of our setup is updating our package.json with these lines:
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "npm run build && npm run start",
"test": "vitest"
},
Our first test
To finish our setup, let's create a simple test. Communicating with our database requires us to have its URL so our first test will make sure that we have it in our environment.
First, we create a src
directory and an index.ts
file inside it.
We need a class that checks our process.env
for the DARABASE_URL
variable. We will use dotenv
to populate the process.env
.
// file: src/index.ts
import Dotenv from "dotenv";
import pg from "pg";
Dotenv.config();
class StormClient {
private connectionString: string;
constructor() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("No connection string provided");
}
this.connectionString = connectionString;
}
}
export default StormClient;
For the tests, the stubEnv
helper provided by vitest
will allow us to mock the content of the process.env
.
// file: tests/index.spec.ts
import { describe, it, expect, afterEach, vi } from "vitest";
import StormClient from "../src/index";
describe("Check connection strihng", () => {
it("should use the connection string provided in the environment", () => {
// Make sure the environment variable is set to some value
const url = "postgres://dummy_url:5432/storm";
vi.stubEnv("DATABASE_URL", url);
// Create a new instance of the client and check the connection string
const client = new StormClient();
expect(client["connectionString"]).toBe(url);
});
it("should throw an error if no connection string is provided", () => {
// Delete the environment variable
vi.stubEnv("DATABASE_URL", "");
// Ensure that an error is thrown when creating a new instance
expect(() => new StormClient()).toThrow("No connection string provided");
});
afterEach(() => {
vi.unstubAllEnvs();
});
});
Let's run our tests
pnpm test
If the tests run successfully you should have a result like this:
> storm@1.0.0 test /[...]/storm
> vitest
RUN v0.31.4 /[...]/storm
โ tests/index.spec.ts (2)
Test Files 1 passed (1)
Tests 2 passed (2)
...
What's next
The project setup is done. You can find the whole code for this article here. In the next article, we'll connect to our database and start implementing a few methods on the client. See you then ๐