From 0a2742755da811feeaae7b18181173dd816f02da Mon Sep 17 00:00:00 2001 From: qpismont Date: Mon, 30 Dec 2024 19:48:29 +0000 Subject: [PATCH] init --- .devcontainer/Dockerfile | 5 + .devcontainer/devcontainer.json | 13 ++ .gitignore | 175 +++++++++++++++++++++ .vscode/settings.json | 8 + .woodpecker/.build.yml | 9 ++ .woodpecker/.lint.yml | 10 ++ .woodpecker/.test.yml | 30 ++++ README.md | 15 ++ biome.json | 16 ++ bun.lockb | Bin 0 -> 15625 bytes package.json | 22 +++ src/app.ts | 0 src/core/database/IDatabase.ts | 8 + src/core/database/PgDatabase.ts | 71 +++++++++ src/core/tests/database/PgDatabase.test.ts | 68 ++++++++ tsconfig.json | 27 ++++ 16 files changed, 477 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 .woodpecker/.build.yml create mode 100644 .woodpecker/.lint.yml create mode 100644 .woodpecker/.test.yml create mode 100644 README.md create mode 100644 biome.json create mode 100755 bun.lockb create mode 100644 package.json create mode 100644 src/app.ts create mode 100644 src/core/database/IDatabase.ts create mode 100644 src/core/database/PgDatabase.ts create mode 100644 src/core/tests/database/PgDatabase.test.ts create mode 100644 tsconfig.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..663ff8b --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:12 + +RUN apt update &&\ + apt install git curl unzip -y &&\ + curl -fsSL https://bun.sh/install | bash \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6ddbb39 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,13 @@ +{ + "workspaceFolder": "/workspace", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "extensions": ["oven.bun-vscode", "biomejs.biome"] + } + }, + "forwardPorts": [8080] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..23460e8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "biomejs.biome", + "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit" + } +} diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml new file mode 100644 index 0000000..5789581 --- /dev/null +++ b/.woodpecker/.build.yml @@ -0,0 +1,9 @@ +when: + event: [push] + +steps: + build: + image: oven/bun:1.1.42-slim + commands: + - bun install + - bun run build \ No newline at end of file diff --git a/.woodpecker/.lint.yml b/.woodpecker/.lint.yml new file mode 100644 index 0000000..a8496a9 --- /dev/null +++ b/.woodpecker/.lint.yml @@ -0,0 +1,10 @@ +when: + event: [push] + +steps: + lint: + image: oven/bun:1.1.42-slim + commands: + - bun install + - bun run ci + diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml new file mode 100644 index 0000000..07d50ac --- /dev/null +++ b/.woodpecker/.test.yml @@ -0,0 +1,30 @@ +when: + event: [push] + +steps: + test: + image: oven/bun:${BUN_VERSION}-slim + commands: + - sleep 15 + - bun install + - bun run test + environment: + - DB_HOST: pg + - DB_PORT: 5432 + - DB_USER: dev + - DB_PASSWORD: dev + - DB_NAME: trepa + +services: + - name: pg + image: postgres:16-alpine + environment: + - POSTGRES_USER: dev + - POSTGRES_PASSWORD: dev + - POSTGRES_DB: trepa + +matrix: + BUN_VERSION: + - 1.1.42 + - 1.1.41 + - 1.1.40 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cbbddf --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# trepa + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run app.ts +``` + +This project was created using `bun init` in bun v1.1.27. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..ca203a0 --- /dev/null +++ b/biome.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..44aaed9bbe77f7de6559a8c3480ac7b954e70f85 GIT binary patch literal 15625 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 G`~QD~8ByB+ literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..544af2d --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "trepa", + "module": "app.ts", + "type": "module", + "scripts": { + "build": "bun build ./src/app.ts --compile --outfile=./dist/app", + "ci": "biome ci . --error-on-warnings", + "test": "bun test" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/bun": "latest", + "@types/pg": "^8.11.10" + }, + "peerDependencies": { + "typescript": "^5.6.2" + }, + "dependencies": { + "hono": "^4.6.15", + "pg": "^8.13.1" + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/database/IDatabase.ts b/src/core/database/IDatabase.ts new file mode 100644 index 0000000..f8f9446 --- /dev/null +++ b/src/core/database/IDatabase.ts @@ -0,0 +1,8 @@ +export type SqlParams = Record; + +export default interface IDatabase { + fetch(sql: string, params: SqlParams): Promise; + fetchAll(sql: string, params: SqlParams): Promise; + insert(sql: string, params: SqlParams): Promise; + query(sql: string, params: SqlParams): Promise; +} diff --git a/src/core/database/PgDatabase.ts b/src/core/database/PgDatabase.ts new file mode 100644 index 0000000..b349339 --- /dev/null +++ b/src/core/database/PgDatabase.ts @@ -0,0 +1,71 @@ +import { Client, Pool, type PoolClient, type QueryResult } from "pg"; +import type { SqlParams } from "./IDatabase"; +import type IDatabase from "./IDatabase"; + +export interface PgConnectionOptions { + host: string; + port: number; + database: string; + user: string; + password: string; +} + +export default class PgDatabase implements IDatabase { + private pool: Pool; + + constructor(connectionOptions: PgConnectionOptions) { + this.pool = new Pool({ + user: connectionOptions.user, + password: connectionOptions.password, + host: connectionOptions.host, + port: connectionOptions.port, + database: connectionOptions.database, + }); + } + + async query(sql: string, params: SqlParams = {}): Promise { + await this.internalQuery(sql, params); + } + + async insert(sql: string, params: SqlParams): Promise { + const res = await this.internalQuery(sql, params); + + const id = res.rows[0]?.id; + if (!id) { + throw new Error("id not found in sql str"); + } + + return id; + } + + async fetch( + sql: string, + params: SqlParams = {}, + ): Promise { + const rows = await this.fetchAll(sql, params); + + return rows[0]; + } + + async fetchAll(sql: string, params: SqlParams = {}): Promise { + const res = await this.internalQuery(sql, params); + + return res.rows; + } + + private async internalQuery( + sql: string, + params: SqlParams, + ): Promise { + let client: PoolClient | null = null; + + try { + client = await this.pool.connect(); + const res = await client.query(sql, Object.values(params)); + + return res; + } finally { + client?.release(); + } + } +} diff --git a/src/core/tests/database/PgDatabase.test.ts b/src/core/tests/database/PgDatabase.test.ts new file mode 100644 index 0000000..98ceec0 --- /dev/null +++ b/src/core/tests/database/PgDatabase.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import PgDatabase from "../../database/PgDatabase"; + +let db: PgDatabase; +const host = Bun.env.DB_HOST; +const port = Bun.env.DB_PORT; +const database = Bun.env.DB_NAME; +const user = Bun.env.DB_USER; +const password = Bun.env.DB_PASSWORD; + +beforeEach(async () => { + db = new PgDatabase({ + host: host || "", + port: Number.parseInt(port || "0"), + database: database || "", + user: user || "", + password: password || "", + }); + + await db.query(` + CREATE TABLE public.test_table + ( + id integer NOT NULL GENERATED ALWAYS AS IDENTITY, + name text NOT NULL, + PRIMARY KEY (id) + ); + + INSERT INTO test_table(name) VALUES('Quentin'); + INSERT INTO test_table(name) VALUES('Thomas'); + `); +}); + +test("test insert", async () => { + const id = await db.insert( + "INSERT INTO test_table(name) VALUES($1) RETURNING id", + { + name: "Francis", + }, + ); + + expect(id).toBe(3); +}); + +test("test error insert no id", async () => { + expect( + db.insert("INSERT INTO test_table(name) VALUES($1)", { + name: "Francis", + }), + ).rejects.toBeInstanceOf(Error); +}); + +test("test fetch", async () => { + const row = (await db.fetch("SELECT * FROM test_table WHERE id = 1")) as { + id: number; + }; + + expect(row.id).toEqual(1); +}); + +test("test fetch all", async () => { + const rows = await db.fetchAll("SELECT * FROM test_table"); + + expect(rows.length).toEqual(2); +}); + +afterEach(async () => { + await db.query("DROP TABLE test_table"); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ffc08ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}