commit 0a2742755da811feeaae7b18181173dd816f02da Author: qpismont Date: Mon Dec 30 19:48:29 2024 +0000 init 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 0000000..44aaed9 Binary files /dev/null and b/bun.lockb differ 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 + } +}