feat: checkpoint
This commit is contained in:
121
platform/database/client.ts
Normal file
121
platform/database/client.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
import postgres, { type Options, type Sql, type TransactionSql } from "postgres";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
import { takeAll, takeOne } from "./parser.ts";
|
||||
|
||||
const storage = new AsyncLocalStorage<TransactionSql>();
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Database
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export class Client {
|
||||
/**
|
||||
* Cached SQL instance.
|
||||
*/
|
||||
#db?: Sql;
|
||||
|
||||
/**
|
||||
* Instantiate a new Database accessor wrapper.
|
||||
*
|
||||
* @param db - Dependency container token to retrieve.
|
||||
*/
|
||||
constructor(readonly config: Options<{}>) {}
|
||||
|
||||
/**
|
||||
* SQL instance to perform queries against.
|
||||
*/
|
||||
get sql(): Sql {
|
||||
const tx = storage.getStore();
|
||||
if (tx !== undefined) {
|
||||
return tx;
|
||||
}
|
||||
return this.#getResolvedInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL instance which ignores any potential transaction established
|
||||
* in instance scope.
|
||||
*/
|
||||
get direct(): Sql {
|
||||
return this.#getResolvedInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves cached SQL instance or attempts to create and return
|
||||
* a new instance.
|
||||
*/
|
||||
#getResolvedInstance(): Sql {
|
||||
if (this.#db === undefined) {
|
||||
this.#db = postgres(this.config);
|
||||
}
|
||||
return this.#db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a SQL transaction by wrapping a new db instance with a
|
||||
* new transaction instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { db } from "@optio/database/client.ts";
|
||||
*
|
||||
* db.begin(async (tx) => {
|
||||
* tx`SELECT ...`
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
begin<TResponse>(cb: (tx: TransactionSql) => TResponse | Promise<TResponse>): Promise<UnwrapPromiseArray<TResponse>> {
|
||||
return this.direct.begin((tx) => storage.run(tx, () => cb(tx)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes SQL connection if it has been instantiated.
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.#db !== undefined) {
|
||||
await this.#db.end();
|
||||
this.#db = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a schema pepared querying object allowing for a one or many
|
||||
* response based on the query used.
|
||||
*
|
||||
* @param schema - Zod schema to parse.
|
||||
*/
|
||||
schema<TSchema extends ZodType>(schema: TSchema) {
|
||||
return {
|
||||
/**
|
||||
* Executes a sql query and parses the result with the given schema.
|
||||
*
|
||||
* @param sql - Template string SQL value.
|
||||
*/
|
||||
one: (strings: TemplateStringsArray, ...values: any[]) => this.sql(strings, ...values).then(takeOne(schema)),
|
||||
|
||||
/**
|
||||
* Executes a sql query and parses the resulting list with the given schema.
|
||||
*
|
||||
* @param sql - Template string SQL value.
|
||||
*/
|
||||
many: (strings: TemplateStringsArray, ...values: any[]) => this.sql(strings, ...values).then(takeAll(schema)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
type UnwrapPromiseArray<T> = T extends any[]
|
||||
? {
|
||||
[k in keyof T]: T[k] extends Promise<infer R> ? R : T[k];
|
||||
}
|
||||
: T;
|
||||
27
platform/database/config.ts
Normal file
27
platform/database/config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getEnvironmentVariable } from "@platform/config/environment.ts";
|
||||
import z from "zod";
|
||||
|
||||
export const config = {
|
||||
xtdb: {
|
||||
host: getEnvironmentVariable({
|
||||
key: "DB_XTDB_HOST",
|
||||
type: z.string(),
|
||||
fallback: "localhost",
|
||||
}),
|
||||
port: getEnvironmentVariable({
|
||||
key: "DB_XTDB_PORT",
|
||||
type: z.coerce.number(),
|
||||
fallback: "5432",
|
||||
}),
|
||||
user: getEnvironmentVariable({
|
||||
key: "DB_XTDB_USER",
|
||||
type: z.string(),
|
||||
fallback: "xtdb",
|
||||
}),
|
||||
pass: getEnvironmentVariable({
|
||||
key: "DB_XTDB_PASSWORD",
|
||||
type: z.string(),
|
||||
fallback: "xtdb",
|
||||
}),
|
||||
},
|
||||
};
|
||||
11
platform/database/package.json
Normal file
11
platform/database/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@platform/database",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@platform/config": "workspace:*",
|
||||
"postgres": "3.4.7",
|
||||
"zod": "4.1.12"
|
||||
}
|
||||
}
|
||||
29
platform/database/parser.ts
Normal file
29
platform/database/parser.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type z from "zod";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
/**
|
||||
* Takes a single record from a list of database rows.
|
||||
*
|
||||
* @param rows - List of rows to retrieve record from.
|
||||
*/
|
||||
export function takeOne<TSchema extends ZodType>(
|
||||
schema: TSchema,
|
||||
): (records: unknown[]) => z.output<TSchema> | undefined {
|
||||
return (records: unknown[]) => {
|
||||
if (records[0] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return schema.parse(records[0]);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes all records from a list of database rows and validates each one.
|
||||
*
|
||||
* @param schema - Zod schema to validate each record against.
|
||||
*/
|
||||
export function takeAll<TSchema extends ZodType>(schema: TSchema): (records: unknown[]) => z.output<TSchema>[] {
|
||||
return (records: unknown[]) => {
|
||||
return records.map((record) => schema.parse(record));
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user