[Improving Javascript package] Rewriting Prisma ORM - part 2

Project Setup

ยท

5 min read

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 ๐Ÿ‘‹

ย