├── scripts ├── build.sh ├── run.sh ├── postgres.sh └── test.sh ├── docs └── pics │ └── showTables.png ├── .npmignore ├── .gitignore ├── src ├── test │ ├── conditions.ts │ ├── client.test.ts │ ├── real.test.ts │ ├── friendship.ts │ └── query.test.ts ├── fakeClient.ts ├── pg.ts └── index.ts ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── .github └── workflows │ ├── npm-publish.yml │ ├── auto-update.yml │ ├── first-interaction-greetings.yml │ └── pull-request.yml ├── tsconfig.json └── README.md /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | 5 | docker build -t al_sql_img . 6 | 7 | -------------------------------------------------------------------------------- /docs/pics/showTables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NobleMajo/al-sql/HEAD/docs/pics/showTables.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # global ignore 2 | * 3 | 4 | # exe scripts 5 | !bin 6 | 7 | # npm configs 8 | !package-lock.json 9 | !package.json 10 | 11 | # project infos 12 | !README.md 13 | !LICENCE 14 | 15 | # dist 16 | !dist/**/* 17 | dist/**/*.d.ts.map 18 | dist/**/*.js.map 19 | 20 | # tests 21 | dist/test/**/* -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd .. 4 | 5 | docker run -it --rm \ 6 | --network postgres_net \ 7 | -e POSTGRES_HOST="postgres-test" \ 8 | -e POSTGRES_PORT="5432" \ 9 | -e POSTGRES_USER="admin" \ 10 | -e POSTGRES_PASSWORD="adminPw" \ 11 | -e POSTGRES_DB="adminDb" \ 12 | --name al_sql \ 13 | al_sql_img -------------------------------------------------------------------------------- /scripts/postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | 5 | docker rm -f postgres-test 2> /dev/null 6 | 7 | docker network create postgres_net 2> /dev/null 8 | 9 | docker run -it --rm \ 10 | --network postgres_net \ 11 | --name postgres-test \ 12 | -e POSTGRES_USER="test" \ 13 | -e POSTGRES_PASSWORD="test" \ 14 | -e POSTGRES_DB="test" \ 15 | -p 127.0.0.1:5432:5432/tcp \ 16 | -p 127.0.0.1:5432:5432/udp \ 17 | postgres:14 -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd .. 4 | 5 | docker rm -f nodejs-test 2> /dev/null 6 | 7 | docker create -it --rm \ 8 | --network postgres_net \ 9 | -e POSTGRES_HOST="postgres-test" \ 10 | -e POSTGRES_PORT="5432" \ 11 | -e POSTGRES_USER="admin" \ 12 | -e POSTGRES_PASSWORD="adminPw" \ 13 | -e POSTGRES_DB="adminDb" \ 14 | --name nodejs-test \ 15 | -w /app \ 16 | node:14 \ 17 | npm run testi 18 | 19 | docker cp . nodejs-test:/app 20 | 21 | docker start -i nodejs-test 22 | 23 | docker rm -f nodejs-test 2> /dev/null -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # app data 2 | .store 3 | 4 | # mac 5 | .DS_Store 6 | 7 | # windows 8 | Thumbs.db 9 | 10 | # vscode 11 | **/.vscode 12 | 13 | # codec 14 | core 15 | 16 | # npm 17 | **/node_modules 18 | **/.npmrc 19 | **/.npm 20 | 21 | # Log files 22 | **/logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | 29 | # apps 30 | *.app 31 | *.exe 32 | *.war 33 | 34 | # media files 35 | *.mp4 36 | *.tiff 37 | *.avi 38 | *.flv 39 | *.mov 40 | *.wmv 41 | 42 | # typescript 43 | dist 44 | src 45 | !src/**/*.ts 46 | 47 | # git 48 | !.keep 49 | !.gitkeep 50 | -------------------------------------------------------------------------------- /src/test/conditions.ts: -------------------------------------------------------------------------------- 1 | import { SqlCondition } from "../index"; 2 | 3 | const some: SqlCondition = [ 4 | "OR", 5 | ["id", "==", 123], 6 | [ 7 | "AND", 8 | ["name", "==", "tester"], 9 | ["age", "<", 32], 10 | [ 11 | "OR", 12 | ["active", true], 13 | { 14 | query: "'user'.'name' != $1 AND ('user'.age >= $3) = $2", 15 | values: ["test", true, 123] 16 | } 17 | ] 18 | ] 19 | ] 20 | 21 | const test: SqlCondition = [ 22 | "OR", 23 | [ 24 | "AND", 25 | ["name", "tester"], 26 | ["age", ">", 34], 27 | ], 28 | ["", ">", 34], 29 | ] -------------------------------------------------------------------------------- /src/fakeClient.ts: -------------------------------------------------------------------------------- 1 | export interface QueryData { 2 | query: string, 3 | params: any[], 4 | } 5 | 6 | export function useFakeClient(): boolean { 7 | return (process.env.PG_FAKE_CLIENT ?? "").toLowerCase() == "true" 8 | } 9 | 10 | export class Client { 11 | constructor() { 12 | console.info("FakeClient created!") 13 | } 14 | 15 | querys: QueryData[] = [] 16 | 17 | shiftQuery(): QueryData { 18 | let data = this.querys.shift() 19 | if (!data) { 20 | data = { 21 | query: "No query found!", 22 | params: [] 23 | } 24 | } 25 | return data 26 | } 27 | 28 | async query(query: string, parameter: any[]): Promise { 29 | this.querys.push({ 30 | query: query, 31 | params: parameter, 32 | }) 33 | return { 34 | rows: [] 35 | } 36 | } 37 | 38 | async connect(): Promise { } 39 | 40 | async end(): Promise { } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Majo Richter (NobleMajo) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "al-sql", 3 | "description": "Abstraction_Layer-Structured_Query_Language is a sql abstraction layer that can be used for every sql database to perform DML and simple DLL querys.", 4 | "version": "1.2.9", 5 | "main": "dist/index.js", 6 | "author": "majo418", 7 | "license": "MIT", 8 | "private": false, 9 | "keywords": [ 10 | "typescript", 11 | "node", 12 | "lib", 13 | "sql", 14 | "abstract" 15 | ], 16 | "repository": { 17 | "url": "git@github.com:majo418/al-sql.git", 18 | "type": "git" 19 | }, 20 | "scripts": { 21 | "tsc": "tsc -p tsconfig.json", 22 | "start": "ts-node src/index.ts", 23 | "exec": "node dist/index.js", 24 | "test": "mocha --require ts-node/register src/test/**/*.test.ts", 25 | "build": "npm run tsc", 26 | "start:watch": "nodemon -w ./src -x \"npm run start\" --ext *.ts", 27 | "build:watch": "nodemon -w ./src -x \"npm run build\" --ext *.ts", 28 | "test:watch": "nodemon -w ./src -x \"npm run test\" --ext *.ts", 29 | "exec:watch": "nodemon -w ./dist -x \"npm run exec\"", 30 | "dev": "npm run start:watch" 31 | }, 32 | "devDependencies": { 33 | "@types/chai": "4", 34 | "@types/mocha": "9", 35 | "@types/node": "16", 36 | "@types/pg": "8", 37 | "chai": "4", 38 | "mocha": "9", 39 | "nodemon": "2", 40 | "pg": "8", 41 | "ts-node": "10", 42 | "typescript": "4" 43 | }, 44 | "dependencies": { 45 | "colors": "1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions from the community to help improve this project. 4 | Your input is valuable to us. 5 | 6 | You can contribute to the project in the following ways: 7 | 8 | ### **Create Issues** 9 | Feel free to create issues for: 10 | - feature requests, 11 | - bug fixes, 12 | - documentation improvements, 13 | - test cases, 14 | - general questions, 15 | - or any recommendations you may have. 16 | ### **Merge Requests** 17 | Please avoid multiple different changes in one merge request. 18 | 1. **Fork** the repository, 19 | 2. **commit and push** your changes, 20 | 3. and submit your **merge request**: 21 | - with a clear explanation of the changes you've made, 22 | - and a note with your thoughts about your tests. 23 | 24 | There are many reasons for submitting a merge request: 25 | - Fixing bugs, 26 | - implement new features, 27 | - improve documentation, 28 | - and adding tests. 29 | 30 | ## Rules for Contributions 31 | 32 | To ensure a smooth contribution process, please follow the rules below: 33 | 34 | - **License Awareness**: Be aware of and check the LICENSE file before contributing to understand the project's licence terms. 35 | - **Respect the Licence Terms**: Ensure that your contributions comply with the project's license terms. 36 | - **Avoid Plagiarism**: Do not plagiarise code or content from other sources. All contributions should be original work or properly attributed. 37 | - **Platform Rules** Also be sure to follow the rules of the provider and the platform. 38 | 39 | ## Thank you 40 | A big thank you, for considering a contribution. 41 | If anything is unclear, please contact us via a issue with your question. 42 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | run-name: NPM build, test and publish 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: ["main"] 8 | paths: 9 | - "package.json" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 24 23 | cache: "npm" 24 | 25 | - name: Update npm dependencies 26 | run: | 27 | npm ci 28 | npm run build 29 | npm run test --if-present 30 | 31 | - name: Commit and push changes 32 | uses: EndBug/add-and-commit@v9 33 | with: 34 | add: "package*.json" 35 | message: "Bot: npm deps update" 36 | 37 | - name: Store build artifacts 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: build-artifacts 41 | path: | 42 | dist 43 | package*.json 44 | 45 | publish-npmjs-com: 46 | needs: build 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | 52 | - name: Set up Node.js 53 | uses: actions/setup-node@v3 54 | with: 55 | node-version: 24 56 | registry-url: https://registry.npmjs.org/ 57 | 58 | - name: Download build artifacts 59 | uses: actions/download-artifact@v4 60 | with: 61 | name: build-artifacts 62 | 63 | - name: Publish to npm 64 | run: npm publish 65 | env: 66 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 67 | -------------------------------------------------------------------------------- /.github/workflows/auto-update.yml: -------------------------------------------------------------------------------- 1 | name: auto-update 2 | run-name: Update npm dependencies 3 | 4 | on: 5 | schedule: 6 | - cron: "30 12 4,18 * *" 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | update-deps: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 24 24 | cache: "npm" 25 | 26 | - name: Update npm dependencies 27 | run: | 28 | npm version patch --no-git-tag-version 29 | npm update 30 | npm run build 31 | npm run test --if-present 32 | 33 | - name: Commit and push changes 34 | uses: EndBug/add-and-commit@v9 35 | with: 36 | add: "package*.json" 37 | message: "Bot: npm deps update" 38 | 39 | - name: Store build artifacts 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: build-artifacts 43 | path: | 44 | dist 45 | package*.json 46 | 47 | publish-npm: 48 | needs: update-deps 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v4 53 | 54 | - name: Set up Node.js 55 | uses: actions/setup-node@v3 56 | with: 57 | node-version: 24 58 | registry-url: https://registry.npmjs.org/ 59 | 60 | - name: Download build artifacts 61 | uses: actions/download-artifact@v4 62 | with: 63 | name: build-artifacts 64 | 65 | - name: Publish to npm 66 | run: npm publish 67 | env: 68 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 69 | -------------------------------------------------------------------------------- /.github/workflows/first-interaction-greetings.yml: -------------------------------------------------------------------------------- 1 | name: first-interaction-greetings 2 | run-name: First interaction greetings 3 | 4 | on: [pull_request_target, issues] 5 | 6 | jobs: 7 | greeting: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - name: First interaction greetings 14 | uses: actions/first-interaction@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | issue-message: | 18 | 👋 Hii! 19 | 20 | Welcome to the repository, and thank you for opening an issue. 🎉 21 | We're excited to have you contribute! 22 | Please make sure to include all the relevant details to help us understand your report or suggestion. 23 | 24 | If you’re new here, take a moment to review our [contribution guidelines](../blob/main/CONTRIBUTING.md). 25 | These document will help you collaborate effectively and ensure a positive experience for everyone. 26 | 27 | We appreciate your effort and look forward to collaborating with you! 🚀 28 | When chatting, please don't forget that we are human beings and that we do this with our own dedication and out of joy. 29 | 30 | Cheers ❤️ 31 | pr-message: | 32 | 👋 Hii! 33 | 34 | Welcome, and thank you for opening your first pull request with us! 🎉 We're thrilled to see your contribution. 35 | 36 | Before we review, please ensure that: 37 | - You've followed the [contribution guidelines](../blob/main/CONTRIBUTING.md). 38 | - Your changes are thoroughly tested and documented (if applicable). 39 | - The PR description includes all changes and necessary details for the reviewers. 40 | 41 | Our team will review your submission as soon as possible. 42 | In the meantime, feel free to ask any questions or provide additional context to help with the review process. 43 | 44 | We appreciate your effort and look forward to collaborating with you! 🚀 45 | When chatting, please don't forget that we are human beings and that we do this with our own dedication and out of joy. 46 | 47 | Cheers ❤️ 48 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test on Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 24 20 | 21 | - name: Install dependencies 22 | id: deps 23 | run: npm ci 24 | continue-on-error: true 25 | 26 | - name: Build project 27 | id: build 28 | run: npm run build 29 | continue-on-error: true 30 | 31 | - name: Run tests 32 | id: test 33 | run: npm run test 34 | continue-on-error: true 35 | 36 | - name: Post comment on PR 37 | uses: peter-evans/create-or-update-comment@v3 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | repository: ${{ github.repository }} 41 | issue-number: 42 | ${{ github.event.pull_request.number }} 43 | body: | 44 | ${{ steps.outcome.outputs.message }} 45 | 46 | - id: outcome 47 | run: | 48 | status() { 49 | if [ "$1" == "success" ]; then 50 | echo "✅ Success" 51 | else 52 | echo "❌ Failed" 53 | fi 54 | } 55 | 56 | deps_status=$(status "${{ steps.deps.outcome }}") 57 | build_status=$(status "${{ steps.build.outcome }}") 58 | test_status=$(status "${{ steps.test.outcome }}") 59 | 60 | if [ "${{ steps.deps.outcome }}" == "success" ] && \ 61 | [ "${{ steps.build.outcome }}" == "success" ] && \ 62 | [ "${{ steps.test.outcome }}" == "success" ]; then 63 | echo "message=✅ Everything successful! Deps, build and tests passed!" >> $GITHUB_ENV 64 | else 65 | echo "message=❌ Failed tests! Your changes may have broken something:\n- install dependencies: $deps_status\n- build project: $build_status\n- run tests: $test_status\n\nPlease run the test locally and review and fix your changes." >> $GITHUB_ENV 66 | fi 67 | shell: bash 68 | -------------------------------------------------------------------------------- /src/test/client.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { expect } from 'chai' 3 | 4 | if ( 5 | typeof process.env.PG_FAKE_CLIENT != "string" || 6 | process.env.PG_FAKE_CLIENT.length == 0 7 | ) { 8 | process.env.PG_FAKE_CLIENT = "true" 9 | } 10 | 11 | import { PostgresConnection } from "../pg" 12 | import { SqlClient } from "../index" 13 | import { 14 | createFriendshipTables, FriendshipTables 15 | } from "./friendship" 16 | 17 | export const client: SqlClient = new SqlClient( 18 | new PostgresConnection("127.0.0.1", 5432, "test", "test", "test"), 19 | 1000 * 10, 20 | true, 21 | ) 22 | 23 | describe('client base test', () => { 24 | let tables: FriendshipTables 25 | before('test get tables query', async () => { 26 | tables = createFriendshipTables(client) 27 | 28 | await client.connect() 29 | }) 30 | 31 | beforeEach("clear query list", () => { 32 | client.clearQuerys() 33 | }) 34 | 35 | after('close fake connection', async () => { 36 | await client.close() 37 | }) 38 | 39 | it('create tables', async () => { 40 | await client.createAllTables() 41 | }) 42 | 43 | it('insert test data', async () => { 44 | await tables.accountTable.insert({ 45 | name: "tester1", 46 | email: "tester1@testermail.com", 47 | }) 48 | await tables.accountTable.insert({ 49 | name: "tester2", 50 | email: "tester2@testermail.com", 51 | }) 52 | await tables.accountTable.insert({ 53 | name: "tester3", 54 | email: "tester3@testermail.com", 55 | }) 56 | await tables.accountTable.insert({ 57 | name: "tester4", 58 | email: "tester4@testermail.com", 59 | }) 60 | await tables.accountTable.insert({ 61 | name: "tester5", 62 | email: "tester5@testermail.com", 63 | }) 64 | 65 | client?.shiftQuery() 66 | client?.shiftQuery() 67 | client?.shiftQuery() 68 | client?.shiftQuery() 69 | }) 70 | 71 | it('drop all tables', async () => { 72 | await client.dropAllTables() 73 | client?.shiftQuery() 74 | client?.shiftQuery() 75 | }) 76 | 77 | it('friendship example create account', async () => { 78 | await tables.accountTable.createTable() 79 | client?.shiftQuery() 80 | 81 | expect((await tables.accountTable.select( 82 | "id" 83 | )).length).is.equals(0) 84 | }) 85 | 86 | it('friendship example create friendship', async () => { 87 | await tables.friendshipTable.createTable() 88 | expect((await tables.friendshipTable.select( 89 | "id" 90 | )).length).is.equals(0) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /src/test/real.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { expect } from 'chai' 3 | 4 | if ( 5 | typeof process.env.PG_FAKE_CLIENT != "string" || 6 | process.env.PG_FAKE_CLIENT.length == 0 7 | ) { 8 | process.env.PG_FAKE_CLIENT = "true" 9 | } 10 | 11 | import { useFakeClient } from '../fakeClient'; 12 | import { PostgresConnection } from "../pg" 13 | import { SqlClient } from "../index" 14 | import { 15 | acceptFriendship, 16 | createAccount, 17 | getFriends, 18 | requestFriendship, 19 | FriendshipTables, 20 | createFriendshipTables, 21 | } from "./friendship" 22 | 23 | export const client: SqlClient = new SqlClient( 24 | new PostgresConnection("127.0.0.1", 5432, "test", "test", "test"), 25 | 1000 * 10, 26 | true, 27 | ) 28 | 29 | useFakeClient() && console.info( 30 | ` 31 | If you want to run test with a real postgres database set the 'PG_FAKE_CLIENT' environment variable to 'false'! 32 | The test database needs to be reachable over localhost:5432 with username, password and database 'test'. 33 | ` 34 | ) 35 | !useFakeClient() && console.info( 36 | `Try to run tests with real postgres database! 37 | The test database needs to be reachable over localhost:5432 with username, password and database 'test'. 38 | To disable this feature and use a fake-client for tests set 'PG_FAKE_CLIENT' environment variable to 'true'. 39 | ` 40 | ) 41 | 42 | !useFakeClient() && describe('real pg database test', () => { 43 | let tables: FriendshipTables 44 | 45 | before('test get tables query', async () => { 46 | tables = createFriendshipTables(client) 47 | 48 | await client.connect() 49 | }) 50 | 51 | beforeEach("clear query list", () => { 52 | client.clearQuerys() 53 | }) 54 | 55 | after('close fake connection', async () => { 56 | await client.close() 57 | }) 58 | 59 | let tester1Id: number 60 | let tester2Id: number 61 | let tester3Id: number 62 | let tester4Id: number 63 | 64 | it('friendship example fill account', async () => { 65 | tester1Id = await createAccount(tables.accountTable, "tester1", "tester1@testermail.com") 66 | tester2Id = await createAccount(tables.accountTable, "tester2", "tester2@testermail.com") 67 | tester3Id = await createAccount(tables.accountTable, "tester3", "tester3@testermail.com") 68 | tester4Id = await createAccount(tables.accountTable, "tester4", "tester4@testermail.com") 69 | 70 | expect((await tables.accountTable.select( 71 | "id" 72 | )).length).is.equals(4) 73 | 74 | expect(typeof tester1Id).is.equals("number") 75 | expect(typeof tester4Id).is.equals("number") 76 | }) 77 | 78 | it('friendship example fill friendship', async () => { 79 | await Promise.all([ 80 | requestFriendship(tables.friendshipTable, tester1Id, tester2Id), 81 | requestFriendship(tables.friendshipTable, tester1Id, tester3Id), 82 | requestFriendship(tables.friendshipTable, tester1Id, tester4Id), 83 | requestFriendship(tables.friendshipTable, tester3Id, tester2Id), 84 | requestFriendship(tables.friendshipTable, tester4Id, tester3Id), 85 | requestFriendship(tables.friendshipTable, tester4Id, tester2Id) 86 | ]) 87 | await Promise.all([ 88 | acceptFriendship(tables.friendshipTable, tester1Id, tester2Id), 89 | acceptFriendship(tables.friendshipTable, tester1Id, tester3Id), 90 | acceptFriendship(tables.friendshipTable, tester3Id, tester2Id), 91 | acceptFriendship(tables.friendshipTable, tester4Id, tester2Id) 92 | ]) 93 | 94 | expect((await tables.friendshipTable.select( 95 | "id" 96 | )).length).is.equals(6) 97 | }) 98 | 99 | it('friendship example check friendship', async () => { 100 | expect((await tables.friendshipTable.select( 101 | "id", 102 | ["accepted", true] 103 | )).length).is.equals(4) 104 | expect((await tables.friendshipTable.select( 105 | "id", 106 | ["accepted", false] 107 | )).length).is.equals(2) 108 | expect((await getFriends(tables.friendshipTable, tester1Id)).length).is.equals(2) 109 | expect((await getFriends(tables.friendshipTable, tester2Id)).length).is.equals(3) 110 | expect((await getFriends(tables.friendshipTable, tester3Id)).length).is.equals(2) 111 | expect((await getFriends(tables.friendshipTable, tester4Id)).length).is.equals(1) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | "lib": [ 8 | "ESNext", 9 | "ES6", 10 | "ES5" 11 | ], /* Specify library files to be included in the compilation. */ 12 | "allowJs": false, /* Allow javascript files to be compiled. */ 13 | // "skipLibCheck": true, 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "dist", /* Redirect output structure to the directory. */ 21 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "incremental": true, /* Enable incremental compilation */ 24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 25 | "removeComments": true, /* Do not emit comments to output. */ 26 | // "noEmit": true, /* Do not emit outputs. */ 27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | /* Strict Type-Checking Options */ 31 | // "strict": true, /* Enable all strict type-checking options. */ 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | //"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | /* Additional Checks */ 40 | //"noUnusedLocals": true, /* Report errors on unused locals. */ 41 | //"noUnusedParameters": false, /* Report errors on unused parameters. */ 42 | //"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | /* Module Resolution Options */ 45 | "resolveJsonModule": true, 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | "typeRoots": [ 51 | "./node_modules/@types", 52 | ], /* List of folders to include type definitions from. */ 53 | "types": [ 54 | "node" 55 | ], /* Type declaration files to be included in compilation. */ 56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 57 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | /* Source Map Options */ 60 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 63 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 64 | /* Experimental Options */ 65 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | }, 68 | "include": [ 69 | "src/**/*.ts", 70 | ], 71 | "exclude": [ 72 | "dist", 73 | "node_modules" 74 | ] 75 | } -------------------------------------------------------------------------------- /src/test/friendship.ts: -------------------------------------------------------------------------------- 1 | import { SqlClient, SqlQueryResult, SqlTable } from '../index'; 2 | 3 | export interface FriendshipTables { 4 | accountTable: SqlTable, 5 | friendshipTable: SqlTable, 6 | } 7 | 8 | export function createFriendshipTables( 9 | client: SqlClient 10 | ): FriendshipTables { 11 | // user table example: 12 | const accountTable = client.getTable( 13 | "account", 14 | [ // column example: 15 | { 16 | name: "id", 17 | type: "SERIAL", 18 | primaryKey: true, 19 | nullable: false, 20 | }, 21 | { 22 | name: "name", 23 | type: "VARCHAR", 24 | unique: true, 25 | nullable: false, 26 | size: 32, 27 | }, 28 | { 29 | name: "email", 30 | type: "VARCHAR", 31 | unique: true, 32 | nullable: false, 33 | size: 128, 34 | }, 35 | ] 36 | ) 37 | 38 | // friendship example: 39 | const friendshipTable = client.getTable( 40 | "friendship", 41 | [ // column example: 42 | { 43 | name: "id", 44 | type: "SERIAL", 45 | primaryKey: true, 46 | nullable: false, 47 | }, 48 | { 49 | name: "sender_id", 50 | type: "INT", 51 | nullable: false, 52 | }, 53 | { 54 | name: "receiver_id", 55 | type: "INT", 56 | nullable: false, 57 | }, 58 | { 59 | name: "accepted", 60 | type: "BOOL", 61 | nullable: false, 62 | default: false, 63 | }, 64 | ], 65 | [// foreign keys example: 66 | { 67 | columnName: "sender_id", 68 | foreignColumnName: "id", 69 | foreignTableName: "account" 70 | }, 71 | { 72 | columnName: "receiver_id", 73 | foreignColumnName: "id", 74 | foreignTableName: "account" 75 | } 76 | ] 77 | ) 78 | 79 | return { 80 | accountTable, 81 | friendshipTable, 82 | } 83 | } 84 | 85 | export async function getAccountByName( 86 | accountTable: SqlTable, 87 | name: string 88 | ): Promise { 89 | const result = await accountTable.selectOne( 90 | ["id"], // SELECT "id" FROM "account" LIMIT 1 91 | // WHERE name = $1 ("name" is a prepared statement) 92 | ["name", name] 93 | ) 94 | if (!result || typeof result.id != "number") { 95 | throw new Error("User with name '" + name + "' not exists!") 96 | } 97 | return result.id 98 | } 99 | 100 | export async function getAccountByEmail( 101 | accountTable: SqlTable, 102 | email: string 103 | ): Promise { 104 | const result = await accountTable.selectOne( 105 | ["id"], // SELECT "id" from "account" LIMIT 1 106 | // WHERE email = $1 ("email" is a prepared statement) 107 | ["email", email] 108 | ) 109 | if (!result || typeof result.id != "number") { 110 | throw new Error("User with email '" + email + "' not exists!") 111 | } 112 | return result.id 113 | } 114 | 115 | export async function createAccount( 116 | accountTable: SqlTable, 117 | name: string, 118 | email: string 119 | ): Promise { 120 | const result = await accountTable.insert( 121 | { // INSERT INTO "account" (name, email) VALUES ($1, $2) 122 | name: name, 123 | email: email 124 | }, 125 | ["id"] // RETURNING "ID" 126 | ) 127 | if (!result || typeof result.id != "number") { 128 | throw new Error("User with email '" + email + "' not exists!") 129 | } 130 | return result.id 131 | } 132 | 133 | export async function requestFriendship( 134 | friendshipTable: SqlTable, 135 | senderId: number, 136 | receiverId: number 137 | ): Promise { 138 | await removeFriendship(friendshipTable, senderId, receiverId) 139 | 140 | await friendshipTable.insert({ // INSERT INTO "friendship" (sender_id, receiver_id) VALUES ($1, $2) 141 | sender_id: senderId, 142 | receiver_id: receiverId 143 | }) 144 | } 145 | 146 | export async function acceptFriendship( 147 | friendshipTable: SqlTable, 148 | senderId: number, 149 | receiverId: number 150 | ): Promise { 151 | await friendshipTable.update( 152 | { // UPDATE SET accepted = $1 153 | accepted: true 154 | }, 155 | [ // WHERE sender_id = $1 AND receiver_id = $2 156 | "AND", 157 | ["sender_id", senderId], 158 | ["receiver_id", receiverId] 159 | ] 160 | ) 161 | } 162 | 163 | export async function getFriends( 164 | friendshipTable: SqlTable, 165 | user: number 166 | ): Promise { 167 | return await friendshipTable.select( 168 | [ // SELECT sender_id, receiver_id from "friendship" 169 | "sender_id", 170 | "receiver_id" 171 | ], 172 | [ // WHERE accepted = $1 AND (sender_id = $2 OR receiver_id = $3) 173 | "AND", 174 | ["accepted", true], 175 | [ 176 | "OR", 177 | ["sender_id", user], 178 | ["receiver_id", user] 179 | ] 180 | ] 181 | ) 182 | } 183 | 184 | export async function removeFriendship( 185 | friendshipTable: SqlTable, 186 | user1: number, 187 | user2: number 188 | ): Promise { 189 | await friendshipTable.delete( 190 | [ // WHERE sender_id = $1 OR receiver_id = $2 191 | "OR", 192 | ["sender_id", user1], 193 | ["receiver_id", user2] 194 | ] 195 | ) 196 | } 197 | -------------------------------------------------------------------------------- /src/test/query.test.ts: -------------------------------------------------------------------------------- 1 | import "mocha" 2 | import { expect } from 'chai' 3 | 4 | if ( 5 | typeof process.env.PG_FAKE_CLIENT != "string" || 6 | process.env.PG_FAKE_CLIENT.length == 0 7 | ) { 8 | process.env.PG_FAKE_CLIENT = "true" 9 | } 10 | 11 | import { PostgresConnection } from "../pg" 12 | import { SqlClient, SqlTable } from "../index" 13 | 14 | export const client: SqlClient = new SqlClient( 15 | new PostgresConnection("127.0.0.1", 5432, "test", "test", "test"), 16 | 1000 * 10, 17 | true, 18 | ) 19 | 20 | describe('query generation test', () => { 21 | before('test get tables query', async () => { 22 | await client.connect() 23 | }) 24 | 25 | beforeEach("clear query list", () => { 26 | client.clearQuerys() 27 | }) 28 | 29 | after('close fake connection', async () => { 30 | await client.close() 31 | }) 32 | 33 | it('test get tables query', async () => { 34 | const result = await client.getTables() 35 | if ( 36 | typeof result != "object" || 37 | !Array.isArray(result.rows) 38 | ) { 39 | throw new Error("Result is not a rows object!") 40 | } 41 | const query = client?.shiftQuery()?.shift() 42 | expect(query).is.equals( 43 | 'SELECT * FROM pg_catalog.pg_tables WHERE schemaname != \'pg_catalog\' AND schemaname != \'information_schema\'' 44 | ) 45 | }) 46 | 47 | let userTable: SqlTable 48 | it('create account test table', async () => { 49 | userTable = client.getTable( 50 | "user", 51 | [ 52 | { 53 | name: "id", 54 | type: "SERIAL", 55 | primaryKey: true, 56 | nullable: false, 57 | }, 58 | { 59 | name: "name", 60 | type: "VARCHAR", 61 | unique: true, 62 | nullable: false, 63 | size: 32, 64 | }, 65 | { 66 | name: "email", 67 | type: "VARCHAR", 68 | unique: true, 69 | nullable: false, 70 | size: 128, 71 | }, 72 | ] 73 | ) 74 | await userTable.createTable() 75 | const query = client?.shiftQuery()?.shift() 76 | expect(query).is.equals( 77 | 'CREATE TABLE IF NOT EXISTS "user"(id SERIAL PRIMARY KEY NOT NULL, name VARCHAR(32) UNIQUE NOT NULL, email VARCHAR(128) UNIQUE NOT NULL)' 78 | ) 79 | }) 80 | 81 | let friendstateTable: SqlTable 82 | it('create friendship test table', async () => { 83 | friendstateTable = client.getTable( 84 | "friendstate", 85 | [ // column example: 86 | { 87 | name: "id", 88 | type: "SERIAL", 89 | primaryKey: true, 90 | nullable: false, 91 | }, 92 | { 93 | name: "sender_id", 94 | type: "INT", 95 | nullable: false, 96 | }, 97 | { 98 | name: "receiver_id", 99 | type: "INT", 100 | nullable: false, 101 | }, 102 | { 103 | name: "accepted", 104 | type: "BOOL", 105 | nullable: false, 106 | default: false, 107 | }, 108 | ], 109 | [// foreign keys example: 110 | { 111 | columnName: "sender_id", 112 | foreignColumnName: "id", 113 | foreignTableName: "user" 114 | }, 115 | { 116 | columnName: "receiver_id", 117 | foreignColumnName: "id", 118 | foreignTableName: "user" 119 | } 120 | ] 121 | ) 122 | await friendstateTable.createTable() 123 | const query = client?.shiftQuery()?.shift() 124 | expect(query).is.equals( 125 | 'CREATE TABLE IF NOT EXISTS "friendstate"(id SERIAL PRIMARY KEY NOT NULL, sender_id INT NOT NULL, receiver_id INT NOT NULL, accepted BOOL NOT NULL DEFAULT FALSE, FOREIGN KEY(sender_id) REFERENCES "user" (id) ON DELETE CASCADE, FOREIGN KEY(receiver_id) REFERENCES "user" (id) ON DELETE CASCADE)' 126 | ) 127 | }) 128 | 129 | it('insert data test data', async () => { 130 | let query 131 | let result 132 | 133 | result = await userTable.insert({ 134 | name: "tester", 135 | email: "tester@tester.com" 136 | }) 137 | query = client?.shiftQuery()?.shift() 138 | expect(query).is.equals( 139 | 'INSERT INTO "user" (name, email) VALUES ($1, $2)' 140 | ) 141 | 142 | result = await userTable.insert({ 143 | name: "majo", 144 | email: "majo@coreunit.net" 145 | }) 146 | query = client?.shiftQuery()?.shift() 147 | expect(query).is.equals( 148 | 'INSERT INTO "user" (name, email) VALUES ($1, $2)' 149 | ) 150 | 151 | result = await friendstateTable.insert({ 152 | sender_id: 1, 153 | receiver_id: 2 154 | }) 155 | query = client?.shiftQuery()?.shift() 156 | expect(query).is.equals( 157 | 'INSERT INTO "friendstate" (sender_id, receiver_id) VALUES ($1, $2)' 158 | ) 159 | }) 160 | 161 | it('select with complex where tables', async () => { 162 | let query 163 | let result 164 | 165 | result = await userTable.select( 166 | ["name"] 167 | ) 168 | query = client?.shiftQuery()?.shift() 169 | expect(query).is.equals( 170 | 'SELECT "name" FROM "user"' 171 | ) 172 | 173 | result = await userTable.selectOne( 174 | ["id"], 175 | ["name", "tester"] 176 | ) 177 | query = client?.shiftQuery()?.shift() 178 | expect(query).is.equals( 179 | 'SELECT "id" FROM "user" WHERE "user".name = $1 LIMIT 1' 180 | ) 181 | 182 | result = await friendstateTable.select( 183 | [ 184 | ["ra", "name"], 185 | ["sa", "name"], 186 | ], 187 | [ 188 | "AND", 189 | [["accepted", "NOT"], true], 190 | [ 191 | "OR", 192 | ["receiver_id", 1], 193 | ["sender_id", 1], 194 | ], 195 | ], 196 | -1, 197 | { 198 | as: "ra", 199 | sourceKey: "receiver_id", 200 | targetTable: "user", 201 | targetKey: "id" 202 | }, 203 | { 204 | as: "sa", 205 | sourceKey: "sender_id", 206 | targetTable: "user", 207 | targetKey: "id" 208 | }, 209 | ) 210 | query = client?.shiftQuery()?.shift() 211 | expect(query).is.equals( 212 | 'SELECT "ra"."name", "sa"."name" FROM "friendstate" INNER JOIN "user" ra ON "ra".id = "friendstate".receiver_id INNER JOIN "user" sa ON "sa".id = "friendstate".sender_id WHERE ("friendstate".accepted != $1 AND ("friendstate".receiver_id = $2 OR "friendstate".sender_id = $3))' 213 | ) 214 | 215 | result = await friendstateTable.update( 216 | { 217 | "accepted": true 218 | }, 219 | [ 220 | "OR", 221 | ["receiver_id", 1], 222 | ["sender_id", 1] 223 | ] 224 | ) 225 | query = client?.shiftQuery()?.shift() 226 | expect(query).is.equals( 227 | 'UPDATE "friendstate" SET accepted=$1 WHERE ("friendstate".receiver_id = $2 OR "friendstate".sender_id = $3)' 228 | ) 229 | 230 | result = await friendstateTable.select( 231 | [ 232 | ["ra", "name"], 233 | ["sa", "name"], 234 | ], 235 | [["accepted", "NOT"], false], 236 | -1, 237 | { 238 | as: "ra", 239 | sourceKey: "receiver_id", 240 | targetTable: "user", 241 | targetKey: "id" 242 | }, 243 | { 244 | as: "sa", 245 | sourceKey: "sender_id", 246 | targetTable: "user", 247 | targetKey: "id" 248 | }, 249 | ) 250 | query = client?.shiftQuery()?.shift() 251 | expect(query).is.equals( 252 | 'SELECT "ra"."name", "sa"."name" FROM "friendstate" INNER JOIN "user" ra ON "ra".id = "friendstate".receiver_id INNER JOIN "user" sa ON "sa".id = "friendstate".sender_id WHERE "friendstate".accepted != $1' 253 | ) 254 | }) 255 | 256 | it('drop tables', async () => { 257 | await client.dropAllTables() 258 | 259 | let query 260 | query = client?.shiftQuery()?.shift() 261 | 262 | expect(query).is.equals( 263 | 'DROP TABLE IF EXISTS "friendstate" CASCADE' 264 | ) 265 | query = client?.shiftQuery()?.shift() 266 | 267 | expect(query).is.equals( 268 | 'DROP TABLE IF EXISTS "user" CASCADE' 269 | ) 270 | }) 271 | }) 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # al-sql 2 | 3 | ![CI/CD](https://github.com/noblemajo/al-sql/actions/workflows/npm-publish.yml/badge.svg) 4 | ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) 5 | ![typescript](https://img.shields.io/badge/dynamic/json?style=plastic&color=blue&label=Typescript&prefix=v&query=devDependencies.typescript&url=https%3A%2F%2Fraw.githubusercontent.com%2Fnoblemajo%2Fal-sql%2Fmain%2Fpackage.json) 6 | ![npm](https://img.shields.io/npm/v/al-sql.svg?style=plastic&logo=npm&color=red) 7 | 8 | 9 | ![](https://img.shields.io/badge/dynamic/json?color=green&label=watchers&query=watchers&suffix=x&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fnoblemajo%2Fal-sql) 10 | ![](https://img.shields.io/badge/dynamic/json?color=yellow&label=stars&query=stargazers_count&suffix=x&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fnoblemajo%2Fal-sql) 11 | ![](https://img.shields.io/badge/dynamic/json?color=navy&label=forks&query=forks&suffix=x&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fnoblemajo%2Fal-sql) 12 | 14 | 15 | "al-sql" is a Abstraction_Layer for sql databases to perform simple sql querys. 16 | 17 | You create or use a sql dialect interface and a sql connection interface for your sql database. 18 | With this you can create a SqlClient instance which provides full controll over a database and its table structure. 19 | 20 | There is already a working postgres abstraction implementation that you can use for a postgres databases or as base to create a own abstraction implementation (see [here](#getting-started-postgres)). 21 | 22 | --- 23 | 24 | - [al-sql](#al-sql) 25 | - [Getting started (postgres)](#getting-started-postgres) 26 | - [1. Install package](#1-install-package) 27 | - [2. Client cnnnections](#2-client-cnnnections) 28 | - [3. Table definition](#3-table-definition) 29 | - [4. Implement control functions](#4-implement-control-functions) 30 | - [6. Use the table](#6-use-the-table) 31 | - [Debugging help](#debugging-help) 32 | - [Layer Implementation](#layer-implementation) 33 | - [AbstractSqlDialect](#abstractsqldialect) 34 | - [AbstractSqlConnection](#abstractsqlconnection) 35 | - [Postgres connection via 'pg'](#postgres-connection-via-pg) 36 | - [NPM Scripts](#npm-scripts) 37 | - [use](#use) 38 | - [base scripts](#base-scripts) 39 | - [watch mode](#watch-mode) 40 | - [Contributing](#contributing) 41 | - [License](#license) 42 | - [Disclaimer](#disclaimer) 43 | 44 | # Getting started (postgres) 45 | ## 1. Install package 46 | ```sh 47 | npm i al-sql 48 | ``` 49 | 50 | ## 2. Client cnnnections 51 | Don't worry, much of it is or can be copied and pasted or distributed across multiple files. 52 | 53 | Implement the base client connection: 54 | ```ts 55 | import { SqlClient } from "al-sql" 56 | import { PostgresConnection } from "al-sql/dist/pg" 57 | 58 | export const client = new SqlClient( 59 | new PostgresConnection( 60 | env.POSTGRES_HOST, 61 | env.POSTGRES_PORT, 62 | env.POSTGRES_USER, 63 | env.POSTGRES_PASSWORD, 64 | env.POSTGRES_DB, 65 | ) 66 | ) 67 | ``` 68 | 69 | ## 3. Table definition 70 | Define your tables in the database, this tables can be created via al-sql: 71 | ```ts 72 | // user table example: 73 | export const accountTable = client.getTable( 74 | "account", 75 | [{ // column example: 76 | name: "id", 77 | type: "SERIAL", 78 | primaryKey: true, 79 | nullable: false, 80 | },{ 81 | name: "name", 82 | type: "VARCHAR", 83 | unique: true, 84 | nullable: false, 85 | size: 32, 86 | },{ 87 | name: "email", 88 | type: "VARCHAR", 89 | unique: true, 90 | nullable: false, 91 | size: 128, 92 | },] 93 | ) 94 | 95 | // friendship example: 96 | export const friendshipTable = client.getTable( 97 | "friendship", 98 | [{ // column example: 99 | name: "id", 100 | type: "SERIAL", 101 | primaryKey: true, 102 | nullable: false, 103 | },{ 104 | name: "sender_id", 105 | type: "INT", 106 | nullable: false, 107 | },{ 108 | name: "receiver_id", 109 | type: "INT", 110 | nullable: false, 111 | },{ 112 | name: "accepted", 113 | type: "BOOL", 114 | nullable: false, 115 | default: false, 116 | },], 117 | [{ // foreign keys example: 118 | columnName: "sender_id", 119 | foreignColumnName: "id", 120 | foreignTableName: "account", 121 | },{ 122 | columnName: "receiver_id", 123 | foreignColumnName: "id", 124 | foreignTableName: "account", 125 | },] 126 | ) 127 | ``` 128 | 129 | ## 4. Implement control functions 130 | This way database entities feel like local objects with control functions. 131 | This is just a example, there are better implementations depends on the codebase and coding style: 132 | ```ts 133 | export async function getAccountByName( 134 | name: string 135 | ): Promise { 136 | const result = await accountTable.selectOne( 137 | ["id"], // SELECT "id" FROM "account" LIMIT 1 138 | { // WHERE name = $1 ("name" is a prepared statement) 139 | name: name, 140 | } 141 | ) 142 | 143 | if (!result || typeof result.id != "number") { 144 | throw new Error("User with name '" + name + "' not exists!") 145 | } 146 | 147 | return result.id 148 | } 149 | 150 | export async function getAccountByEmail( 151 | email: string 152 | ): Promise { 153 | const result = await accountTable.selectOne( 154 | ["id"], // SELECT "id" from "account" LIMIT 1 155 | { // WHERE email = $1 ("email" is a prepared statement) 156 | email: email, 157 | } 158 | ) 159 | if (!result || typeof result.id != "number") { 160 | throw new Error("User with email '" + email + "' not exists!") 161 | } 162 | return result.id 163 | } 164 | 165 | export async function createAccount( 166 | name: string, 167 | email: string 168 | ): Promise { 169 | const result = await accountTable.insert( 170 | { // INSERT INTO "account" (name, email) VALUES ($1, $2) 171 | name: name, 172 | email: email, 173 | }, 174 | ["id"] // RETURNING "ID" 175 | ) 176 | if (!result || typeof result.id != "number") { 177 | throw new Error("User with email '" + email + "' not exists!") 178 | } 179 | return result.id 180 | } 181 | 182 | export async function requestFriendship( 183 | senderId: number, 184 | receiverId: number 185 | ): Promise { 186 | await removeFriendship(senderId, receiverId) 187 | // INSERT INTO "friendship" (sender_id, receiver_id) VALUES ($1, $2) 188 | await friendshipTable.insert({ 189 | sender_id: senderId, 190 | receiver_id: receiverId, 191 | }) 192 | } 193 | 194 | export async function acceptFriendship( 195 | senderId: number, 196 | receiverId: number 197 | ): Promise { 198 | await friendshipTable.update( 199 | { // UPDATE SET accepted = $1 200 | accepted: true, 201 | },{ // WHERE sender_id = $1 AND receiver_id = $2 202 | sender_id: senderId, 203 | receiver_id: receiverId, 204 | }, 205 | ) 206 | } 207 | 208 | export async function getFriends( 209 | user: number 210 | ): Promise { 211 | const result = await Promise.all([ 212 | friendshipTable.select( 213 | [ // SELECT "friendship".sender_id from "friendship" 214 | ["friendship", "sender_id"], 215 | ], 216 | { // WHERE receiver_id = $1 217 | receiver_id: user, 218 | }, 219 | ), 220 | friendshipTable.select( 221 | [ // SELECT "friendship".receiver_id from "friendship" 222 | ["friendship", "receiver_id"], 223 | ], 224 | { // WHERE sender_id = $1 225 | sender_id: user, 226 | } 227 | ) 228 | ]) 229 | // merge results together 230 | const friends: number[] = [] 231 | result[0].forEach((f) => friends.push(f.sender_id as number)) 232 | result[1].forEach((f) => friends.push(f.receiver_id as number)) 233 | 234 | return friends 235 | } 236 | 237 | export async function removeFriendship( 238 | user1: number, 239 | user2: number 240 | ): Promise { 241 | await Promise.all([ 242 | friendshipTable.delete( 243 | { // DELETE FROM "friendship" WHERE sender_id = $1 AND receiver_id = $2 244 | sender_id: user1, 245 | receiver_id: user2 246 | } 247 | ), 248 | friendshipTable.delete( 249 | { // DELETE FROM "friendship" WHERE sender_id = $1 AND receiver_id = $2 250 | sender_id: user2, 251 | receiver_id: user1 252 | } 253 | ) 254 | ]) 255 | } 256 | ``` 257 | 258 | ## 6. Use the table 259 | After defining the tables in code use "createTable()" on the client to create the tables if not exist: 260 | ```ts 261 | await client.createTables() 262 | ``` 263 | 264 | You can use the "dropAllTables()" function to drop all (defined) tables. 265 | This is handy for debug and tests: 266 | ```ts 267 | // drops all tables (cascaded) in reversed order 268 | await client.dropAllTables() 269 | 270 | // creates all tables in normal order 271 | await client.createAllTables() 272 | ``` 273 | 274 | From here on your can use the tables or control function to manipulate the database data. 275 | 276 | # Debugging help 277 | Example: 278 | showResult(object, ...options) / showTable(table, ...options) 279 | ![showTables output](https://raw.githubusercontent.com/noblemajo/al-sql/main/docs/pics/showTables.png) 280 | 281 | # Layer Implementation 282 | If you want to create a own abstraction layer implementation you need to implement this two interfaces: 283 | - AbstractSqlDialect 284 | - AbstractSqlConnection 285 | 286 | ## AbstractSqlDialect 287 | First you implement the sql querys for your sql dialect. 288 | You can checkout the postgres implementation for help: 289 | ```ts 290 | export interface AbstractSqlDialect { 291 | getDialectName(): string 292 | 293 | getTablesQuery( 294 | client: SqlClient 295 | ): ExecutableSqlQuery 296 | 297 | createTableQuery( 298 | table: SqlTable 299 | ): ExecutableSqlQuery 300 | dropTableQuery( 301 | table: SqlTable 302 | ): ExecutableSqlQuery 303 | 304 | insertQuery( 305 | table: SqlTable, 306 | set: SqlSetValueMap, 307 | returning?: SqlResultColumnSelector | undefined, 308 | ): ExecutableSqlQuery 309 | updateQuery( 310 | table: SqlTable, 311 | set: SqlSetValueMap, 312 | where?: SqlWhereSelector, 313 | returning?: SqlResultColumnSelector | undefined, 314 | ): ExecutableSqlQuery 315 | selectQuery( 316 | table: SqlTable, 317 | select?: SqlResultColumnSelector, 318 | where?: SqlJoinWhereSelector, 319 | join?: number | undefined, 320 | ...joins: SqlJoin[] 321 | ): ExecutableSqlQuery 322 | deleteQuery( 323 | table: SqlTable, 324 | where?: SqlWhereSelector, 325 | returning?: SqlResultColumnSelector | undefined, 326 | ): ExecutableSqlQuery 327 | } 328 | ``` 329 | 330 | ## AbstractSqlConnection 331 | Now you can implement the needed sql connection based on the sql driver/library. 332 | If two sql databases share the same sql dialect but need a other connection driver/library you can reuse the AbstractSqlDialect and just implement a other AbstractSqlConnection for that driver/library. 333 | ```ts 334 | export interface AbstractSqlConnection { 335 | getDialect(): AbstractSqlDialect // HERE YOU RETURN YOUR SQL DIALECT IMPLEMENTATION 336 | 337 | execute(query: ExecutableSqlQuery): Promise 338 | 339 | isConnected(): Promise 340 | connect(): Promise 341 | close(): Promise 342 | } 343 | ``` 344 | 345 | ## Postgres connection via 'pg' 346 | The postgres connection implementation looks like this: 347 | ```ts 348 | export class PostgresConnection implements AbstractSqlConnection { 349 | public readonly client: Client 350 | public readonly dialect: PostgresSqlDialect 351 | public connected: boolean = false 352 | 353 | constructor( 354 | public readonly host: string, 355 | public readonly port: number, 356 | public readonly username: string, 357 | public readonly password: string, 358 | public readonly database: string 359 | ) { 360 | this.client = new Client({ // <- "Client" is a export of the "pg"-package (postgres-client) 361 | host: host, 362 | port: port, 363 | user: username, 364 | password: password, 365 | database: database 366 | }) 367 | this.dialect = new PostgresSqlDialect() 368 | } 369 | 370 | getDialect(): AbstractSqlDialect { 371 | return this.dialect 372 | } 373 | 374 | async execute(query: ExecutableSqlQuery): Promise { 375 | try{ 376 | return this.client.query( 377 | query[0], 378 | query.slice(1) 379 | ) 380 | }catch(err: Error | any){ 381 | await this.client.end().catch(() => {}) 382 | this.connected = false 383 | throw err 384 | } 385 | } 386 | 387 | async isConnected(): Promise { 388 | return this.connected 389 | } 390 | 391 | async connect(): Promise { 392 | await this.client.connect() 393 | this.connected = true 394 | } 395 | 396 | async close(): Promise { 397 | await this.client.end() 398 | this.connected = false 399 | } 400 | } 401 | ``` 402 | 403 | # NPM Scripts 404 | The npm scripts are made for linux. 405 | But your welcome to test them on macos and windows and create feedback. 406 | 407 | ## use 408 | You can run npm scripts in the project folder like this: 409 | ```sh 410 | npm run 411 | ``` 412 | Here is an example: 413 | ```sh 414 | npm run test 415 | ``` 416 | 417 | ## base scripts 418 | You can find all npm scripts in the `package.json` file. 419 | This is a list of the most important npm scripts: 420 | - test // test the app 421 | - build // build the app 422 | - exec // run the app 423 | - start // build and run the app 424 | 425 | ## watch mode 426 | Like this example you can run all npm scripts in watch mode: 427 | ```sh 428 | npm run start:watch 429 | ``` 430 | 431 | # Contributing 432 | Contributions to this project are welcome! 433 | Interested users can refer to the guidelines provided in the [CONTRIBUTING.md](CONTRIBUTING.md) file to contribute to the project and help improve its functionality and features. 434 | 435 | # License 436 | This project is licensed under the [MIT license](LICENSE), providing users with flexibility and freedom to use and modify the software according to their needs. 437 | 438 | # Disclaimer 439 | This project is provided without warranties. 440 | Users are advised to review the accompanying license for more information on the terms of use and limitations of liability. 441 | 442 | -------------------------------------------------------------------------------- /src/pg.ts: -------------------------------------------------------------------------------- 1 | import { useFakeClient } from "./fakeClient" 2 | 3 | let pg: any 4 | if (useFakeClient()) { 5 | pg = require("./fakeClient") 6 | } else { 7 | try { 8 | pg = require("pg") 9 | } catch (error) { 10 | throw new Error( 11 | "You need to install the 'pg' module to use this postgres implementation!\n" + 12 | "You can also set the 'PG_FAKE_CLIENT' environment variable to 'true' for tests." 13 | ) 14 | } 15 | } 16 | 17 | import { Client } from "pg" 18 | let PgClient = pg.Client as typeof Client 19 | 20 | import { 21 | AbstractSqlConnection, 22 | AbstractSqlDialect, 23 | Column, 24 | ExecutableSqlQuery, 25 | SqlFieldCondition, 26 | SqlJoin, 27 | SqlQueryExecuteResult, 28 | SqlRawCondition, 29 | SqlResultColumnSelector, 30 | SqlSetValueMap, SqlTable, 31 | SqlValue, 32 | SqlCondition, 33 | toPrettyString, 34 | SqlConditionMerge 35 | } from "./index" 36 | 37 | export class PostgresSqlDialect implements AbstractSqlDialect { 38 | getDialectName(): string { 39 | return "postgres" 40 | } 41 | 42 | getDatabasesQuery(): ExecutableSqlQuery { 43 | return [ 44 | `SELECT * FROM pg_database` 45 | ] 46 | } 47 | 48 | getTablesQuery( 49 | ): ExecutableSqlQuery { 50 | return [ 51 | `SELECT *` + 52 | ` FROM pg_catalog.pg_tables` + 53 | ` WHERE` + 54 | ` schemaname != 'pg_catalog' AND schemaname != 'information_schema'` 55 | ] 56 | } 57 | 58 | getTableStructure( 59 | table: SqlTable 60 | ): ExecutableSqlQuery { 61 | return [ 62 | `select column_name, data_type, character_maximum_length ` + 63 | `from INFORMATION_SCHEMA.COLUMNS where table_name = $1`, 64 | table.name 65 | ] 66 | } 67 | 68 | createSqlFieldCondition( 69 | currentTable: string, 70 | condition: SqlFieldCondition, 71 | valueCounter: [number] 72 | ): ExecutableSqlQuery { 73 | if (condition.length < 2) { 74 | throw new Error("A sql field condition needs minimum 2 value!") 75 | } 76 | 77 | let selectedTable: string = currentTable 78 | let selectedField: string 79 | let is: boolean = true 80 | let values: SqlValue[] = condition.slice(1) as SqlValue[] 81 | 82 | if (values.length == 0) { 83 | throw new Error("A sql condition needs minimum 1 value!") 84 | } 85 | 86 | if (typeof condition[0] == "string") { 87 | selectedField = condition[0] 88 | } else if (Array.isArray(condition[0])) { 89 | switch (condition[0].length as number) { 90 | case 2: 91 | if (condition[0][1].toUpperCase() == "NOT") { 92 | is = false 93 | selectedField = condition[0][0] 94 | } else { 95 | selectedTable = condition[0][0] 96 | selectedField = condition[0][1] 97 | } 98 | break; 99 | case 0: 100 | case 1: 101 | throw new Error("The first value of a condition needs to be a array with 2-3 values!") 102 | default: 103 | selectedTable = condition[0][0] 104 | selectedField = condition[0][1] 105 | if (("" + condition[0][2]).toUpperCase() == "NOT") { 106 | is = false 107 | } 108 | break; 109 | } 110 | } else { 111 | throw new Error("The first value of a condition needs to be a array with 2-3 values or a string!") 112 | } 113 | 114 | const query: ExecutableSqlQuery = [ 115 | `"${selectedTable}".${selectedField}` 116 | ] 117 | if (values.length > 1) { 118 | values.forEach((value) => query.push(value)) 119 | let i = valueCounter 120 | if (is) { 121 | query[0] += " IN (" 122 | } else { 123 | query[0] += " NOT IN (" 124 | } 125 | query[0] += values 126 | .map(() => "$" + (valueCounter[0]++)) 127 | .join(", ") + ")" 128 | } else { 129 | if (values[0] == null) { 130 | if (is) { 131 | query[0] += " IS NULL" 132 | } else { 133 | query[0] += " IS NOT NULL" 134 | } 135 | } else { 136 | query.push(values[0]) 137 | if (is) { 138 | query[0] += " = $" + (valueCounter[0]++) 139 | } else { 140 | query[0] += " != $" + (valueCounter[0]++) 141 | } 142 | } 143 | } 144 | return query 145 | } 146 | 147 | createSqlRawCondition( 148 | currentTable: string, 149 | condition: SqlRawCondition, 150 | valueCounter: [number] 151 | ): ExecutableSqlQuery { 152 | if (typeof condition.query != "string") { 153 | throw new Error("The 'query' value of a raw condition is not a string!") 154 | } else if (!Array.isArray(condition.values)) { 155 | throw new Error("The 'values' vakue of a raw condition is not an array!") 156 | } 157 | let i: number = 1 158 | while (condition.query.includes("$" + i)) { 159 | condition.query = condition.query 160 | .split("$" + (i++)) 161 | .join("$" + (valueCounter[0]++)) 162 | } 163 | return [condition.query, ...condition.values] 164 | } 165 | 166 | createSqlConditionMerge( 167 | currentTable: string, 168 | condition: SqlConditionMerge, 169 | valueCounter: [number] 170 | ): ExecutableSqlQuery { 171 | if (condition.length < 3) { 172 | throw new Error("A sql condition merge needs minimum 3 value!") 173 | } 174 | 175 | const querys: ExecutableSqlQuery[] = [] 176 | const and: boolean = condition[0].toUpperCase() != "OR" 177 | const condition2: SqlCondition[] = condition.slice(1) as SqlCondition[] 178 | if (condition.length <= 0) { 179 | throw new Error("A sql join where selector needs minimum 1 condition!") 180 | } 181 | condition2.forEach((condition3) => querys.push(this.createSqlCondition( 182 | currentTable, 183 | condition3, 184 | valueCounter 185 | ))) 186 | 187 | const conditions: string[] = [] 188 | const values: SqlValue[] = [] 189 | querys.forEach((query) => { 190 | query.slice(1).forEach((value: SqlValue) => values.push(value)) 191 | conditions.push(query[0]) 192 | }) 193 | 194 | return [ 195 | "(" + conditions.join(and ? " AND " : " OR ") + ")", 196 | ...values 197 | ] 198 | } 199 | 200 | createSqlCondition( 201 | currentTable: string, 202 | condition: SqlCondition, 203 | valueCounter: [number] 204 | ): ExecutableSqlQuery { 205 | if (Array.isArray(condition)) { 206 | if (condition.length < 1) { 207 | throw new Error("A sql condition array needs minimum 1 value!") 208 | } 209 | const first = condition[0] 210 | if ( 211 | typeof first == "string" && 212 | ( 213 | first == "AND" || 214 | first == "OR" 215 | ) 216 | ) { 217 | return this.createSqlConditionMerge( 218 | currentTable, 219 | condition as SqlConditionMerge, 220 | valueCounter 221 | ) 222 | } else { 223 | return this.createSqlFieldCondition( 224 | currentTable, 225 | condition as SqlFieldCondition, 226 | valueCounter 227 | ) 228 | } 229 | } else if (typeof condition == "object" || condition != null) { 230 | return this.createSqlRawCondition( 231 | currentTable, 232 | condition as SqlRawCondition, 233 | valueCounter 234 | ) 235 | } 236 | throw new Error("Unknown where type!") 237 | } 238 | 239 | createSqlWhereCondition( 240 | currentTable: string, 241 | condition: SqlCondition, 242 | valueCounter: [number] 243 | ): ExecutableSqlQuery { 244 | try { 245 | return this.createSqlCondition( 246 | currentTable, 247 | condition, 248 | valueCounter 249 | ) 250 | } catch (err: Error | any) { 251 | const msgSuffix: string = "\nType of condition is: " + 252 | typeof condition + 253 | "\nvalue:\n" + 254 | toPrettyString(condition, { maxLevel: 2 }) 255 | if (typeof err == "string") { 256 | err += msgSuffix 257 | } else if (typeof err.msg == "string") { 258 | err.msg += msgSuffix 259 | } else if (typeof err.message == "string") { 260 | err.message += msgSuffix 261 | } 262 | throw err 263 | } 264 | 265 | } 266 | 267 | createSelectQuery( 268 | select: SqlResultColumnSelector, 269 | ): string { 270 | if (select == null) { 271 | return "*" 272 | } else if (typeof select == "string") { 273 | select = [select] 274 | } 275 | if (Array.isArray(select)) { 276 | return select.map( 277 | (s) => { 278 | if (typeof s == "string") { 279 | return '"' + s + '"' 280 | } 281 | return `"${s[0]}"."${s[1]}"` 282 | } 283 | ).join(", ") 284 | } else { 285 | return "*" 286 | } 287 | } 288 | 289 | createColumnQuery( 290 | column: Column 291 | ): string { 292 | let line: string = column.name + " " + column.type.toUpperCase() 293 | if (column.size) { 294 | line += "(" + column.size + ")" 295 | } 296 | if (column.unique) { 297 | line += " UNIQUE" 298 | } else if (column.primaryKey) { 299 | line += " PRIMARY KEY" 300 | } 301 | if (column.nullable) { 302 | line += " NULL" 303 | } else { 304 | line += " NOT NULL" 305 | } 306 | const type = typeof column.default 307 | if (type != "undefined") { 308 | line += " DEFAULT " 309 | if (column.default == null) { 310 | line += "NULL" 311 | } else if (type == "boolean") { 312 | line += column.default == true ? "TRUE" : "FALSE" 313 | } else if (type == "number") { 314 | line += Number(column.default) 315 | } else if (type == "string") { 316 | line += "'" + (column.default as string).split("'").join("\\'") + "'" 317 | } else { 318 | throw new Error("Type of default value is not string, number, boolean or null!") 319 | } 320 | } 321 | return line 322 | } 323 | 324 | createTableQuery( 325 | table: SqlTable 326 | ): ExecutableSqlQuery { 327 | let line: string = table.columns.map( 328 | (c) => this.createColumnQuery(c) 329 | ).join(", ") 330 | 331 | if (table.foreignKey && table.foreignKey.length > 0) { 332 | line += "," + table.foreignKey.map( 333 | (fKey) => 334 | ` FOREIGN KEY(${fKey.columnName}) REFERENCES "${fKey.foreignTableName}" (${fKey.foreignColumnName}) ON DELETE CASCADE` 335 | ).join(",") 336 | } 337 | 338 | line = `CREATE TABLE IF NOT EXISTS "${table.name}"(${line})` 339 | 340 | return [ 341 | line 342 | ] 343 | } 344 | 345 | dropTableQuery( 346 | table: SqlTable 347 | ): ExecutableSqlQuery { 348 | return [ 349 | `DROP TABLE IF EXISTS "${table.name}" CASCADE` 350 | ] 351 | } 352 | 353 | insertQuery( 354 | table: SqlTable, 355 | set: SqlSetValueMap, 356 | returning?: SqlResultColumnSelector | undefined, 357 | ): ExecutableSqlQuery { 358 | let i: [number] = [1] 359 | let line = `INSERT INTO "${table.name}"` 360 | line += ` (${Object.keys(set).join(", ")})` 361 | line += ` VALUES (${Object.keys(set).map(() => "$" + (i[0]++)).join(", ")})` 362 | if (typeof returning != "undefined") { 363 | line += ` RETURNING ${this.createSelectQuery(returning)}` 364 | } 365 | return [ 366 | line, 367 | ...Object.values(set) 368 | ] 369 | } 370 | 371 | updateQuery( 372 | table: SqlTable, 373 | set: SqlSetValueMap, 374 | where?: SqlCondition, 375 | returning?: SqlResultColumnSelector | undefined, 376 | ): ExecutableSqlQuery { 377 | let i: [number] = [1] 378 | let line = `UPDATE "${table.name}"` 379 | line += ` SET ${Object.keys(set).map((k) => k + "=$" + (i[0]++)).join(", ")}` 380 | let values: any[] = [] 381 | if (where && Object.keys(where).length > 0) { 382 | const whereData = this.createSqlWhereCondition(table.name, where, i) 383 | line += ` WHERE ${whereData[0]}` 384 | values = whereData.slice(1) 385 | } 386 | if (typeof returning != "undefined") { 387 | line += ` RETURNING ${this.createSelectQuery(returning)}` 388 | } 389 | return [ 390 | line, 391 | ...Object.values(set), 392 | ...values 393 | ] 394 | } 395 | 396 | selectQuery( 397 | table: SqlTable, 398 | select: SqlResultColumnSelector = null, 399 | where?: SqlCondition, 400 | limit?: number | undefined, 401 | ...joins: SqlJoin[] 402 | ): ExecutableSqlQuery { 403 | let line = `SELECT ${this.createSelectQuery(select)}` 404 | line += ` FROM "${table.name}"` 405 | if (joins && joins.length > 0) { 406 | joins.forEach((join) => { 407 | const tableName: string = join.as ? join.as : join.targetTable 408 | line += ` ${join.join ?? "INNER"} JOIN "${join.targetTable}"${join.as ? " " + join.as : ""}` 409 | line += ` ON "${tableName}".${join.targetKey} = "${join.sourceTable ?? table.name}".${join.sourceKey}` 410 | }) 411 | } 412 | let values: any[] = [] 413 | if (where && Object.keys(where).length > 0) { 414 | let i: [number] = [1] 415 | const whereData = this.createSqlWhereCondition(table.name, where, i) 416 | line += ` WHERE ${whereData[0]}` 417 | values = whereData.slice(1) 418 | } 419 | if (limit && limit > 0) { 420 | line += " LIMIT " + limit 421 | } 422 | return [ 423 | line, 424 | ...values 425 | ] 426 | } 427 | 428 | deleteQuery( 429 | table: SqlTable, 430 | where?: SqlCondition, 431 | returning?: SqlResultColumnSelector | undefined, 432 | ): ExecutableSqlQuery { 433 | let line = `DELETE FROM "${table.name}"` 434 | 435 | let values: any[] = [] 436 | if (where && Object.keys(where).length > 0) { 437 | let i: [number] = [1] 438 | const whereData = this.createSqlWhereCondition(table.name, where, i) 439 | line += ` WHERE ${whereData[0]}` 440 | values = whereData.slice(1) 441 | } 442 | 443 | if (typeof returning != "undefined") { 444 | line += ` RETURNING ${this.createSelectQuery(returning)}` 445 | } 446 | return [ 447 | line, 448 | ...values 449 | ] 450 | } 451 | } 452 | 453 | export class PostgresConnection implements AbstractSqlConnection { 454 | public client: Client 455 | public readonly dialect: PostgresSqlDialect 456 | public connected: boolean = false 457 | 458 | constructor( 459 | public readonly host: string, 460 | public readonly port: number, 461 | public readonly username: string, 462 | public readonly password: string, 463 | public readonly database: string 464 | ) { 465 | this.dialect = new PostgresSqlDialect() 466 | } 467 | 468 | getDialect(): AbstractSqlDialect { 469 | return this.dialect 470 | } 471 | 472 | execute(query: ExecutableSqlQuery): Promise { 473 | return this.client.query( 474 | query[0], 475 | query.slice(1) 476 | ) 477 | } 478 | 479 | async isConnected(): Promise { 480 | return this.connected 481 | } 482 | 483 | connect(): Promise { 484 | this.client = new PgClient({ 485 | host: this.host, 486 | port: this.port, 487 | user: this.username, 488 | password: this.password, 489 | database: this.database, 490 | }) 491 | return this.client.connect().then(() => { 492 | this.connected = true 493 | }) 494 | } 495 | 496 | close(): Promise { 497 | return this.client.end().then(() => { 498 | this.connected = false 499 | }) 500 | } 501 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "colors" 2 | import * as crypto from "crypto" 3 | 4 | export type ColumnType = "SERIAL" | "VARCHAR" | "TEXT" | "LONG" | "INT" | "BOOL" | string 5 | 6 | export interface Column { 7 | name: string, 8 | type: ColumnType, 9 | nullable: boolean | undefined, 10 | size?: number | undefined, 11 | unique?: boolean | undefined, 12 | primaryKey?: boolean | undefined, 13 | default?: SqlValue 14 | } 15 | 16 | export interface ForeignKey { 17 | columnName: string, 18 | foreignTableName: string, 19 | foreignColumnName: string, 20 | } 21 | 22 | export type SqlValue = string | number | boolean | null | undefined 23 | 24 | export interface SqlQueryResultRow { 25 | [key: string]: SqlValue 26 | } 27 | 28 | export type SqlQueryResult = SqlQueryResultRow[] 29 | 30 | export interface SqlQueryExecuteResult { 31 | rows: SqlQueryResult 32 | } 33 | 34 | export type ExecutableSqlQuery = [string, ...SqlValue[]] 35 | 36 | export interface SqlSetValueMap { 37 | [key: string]: SqlValue 38 | } 39 | 40 | export type OrOperator = "OR" 41 | export type AndOperator = "AND" 42 | export type AndOrOrOperator = OrOperator | AndOperator 43 | 44 | export type IsOperator = "IS" 45 | export type NotOperator = "NOT" 46 | export type IsOrNotOperator = IsOperator | NotOperator 47 | 48 | export type SqlFieldCondition = [ 49 | string | [string, string] | [string, IsOrNotOperator] | [string, string, IsOrNotOperator], 50 | SqlValue, 51 | ...SqlValue[] 52 | ] 53 | export type SqlRawCondition = { 54 | query: string, 55 | values: SqlValue[] 56 | } 57 | export type SqlConditionMerge = [ 58 | AndOrOrOperator, 59 | SqlCondition, 60 | SqlCondition, 61 | ...SqlCondition[] 62 | ] 63 | 64 | export type SqlCondition = SqlConditionMerge | SqlRawCondition | SqlFieldCondition 65 | 66 | export type SqlResultColumnSelector = ( 67 | ( 68 | string 69 | | 70 | [string, string] 71 | )[] 72 | | 73 | string 74 | | 75 | null 76 | ) 77 | 78 | export interface SqlJoin { 79 | join?: undefined | "LEFT" | "RIGHT" | "FULL", 80 | as?: string, 81 | sourceTable?: string, 82 | sourceKey: string 83 | targetTable: string, 84 | targetKey: string, 85 | } 86 | 87 | export interface AbstractSqlDialect { 88 | getDialectName(): string 89 | 90 | getTablesQuery( 91 | client: SqlClient 92 | ): ExecutableSqlQuery 93 | 94 | getTableStructure( 95 | table: SqlTable 96 | ): ExecutableSqlQuery 97 | 98 | createTableQuery( 99 | table: SqlTable 100 | ): ExecutableSqlQuery 101 | dropTableQuery( 102 | table: SqlTable 103 | ): ExecutableSqlQuery 104 | 105 | insertQuery( 106 | table: SqlTable, 107 | set: SqlSetValueMap, 108 | returning?: SqlResultColumnSelector | undefined, 109 | ): ExecutableSqlQuery 110 | updateQuery( 111 | table: SqlTable, 112 | set: SqlSetValueMap, 113 | where?: SqlCondition, 114 | returning?: SqlResultColumnSelector | undefined, 115 | ): ExecutableSqlQuery 116 | selectQuery( 117 | table: SqlTable, 118 | select?: SqlResultColumnSelector, 119 | where?: SqlCondition, 120 | join?: number | undefined, 121 | ...joins: SqlJoin[] 122 | ): ExecutableSqlQuery 123 | deleteQuery( 124 | table: SqlTable, 125 | where?: SqlCondition, 126 | returning?: SqlResultColumnSelector | undefined, 127 | ): ExecutableSqlQuery 128 | } 129 | 130 | export interface AbstractSqlConnection { 131 | getDialect(): AbstractSqlDialect 132 | 133 | execute(query: ExecutableSqlQuery): Promise 134 | 135 | isConnected(): Promise 136 | connect(): Promise 137 | close(): Promise 138 | } 139 | 140 | export class SqlClient { 141 | public readonly dialect: AbstractSqlDialect 142 | public closeTimeout: NodeJS.Timeout | undefined = undefined 143 | 144 | public connectPromise: Promise | undefined 145 | public closePromise: Promise | undefined 146 | 147 | private readonly querys: ExecutableSqlQuery[] = [] 148 | 149 | shiftQuery(): ExecutableSqlQuery | undefined { 150 | return this.querys.shift() 151 | } 152 | 153 | clearQuerys(): ExecutableSqlQuery[] { 154 | let querys: ExecutableSqlQuery[] = [] 155 | while (this.querys.length > 0) { 156 | querys.push(this.querys.shift()) 157 | } 158 | return querys 159 | } 160 | 161 | constructor( 162 | public readonly connection: AbstractSqlConnection, 163 | public connectionTime: number = 1000 * 45, 164 | public listQuery: boolean = false, 165 | public queryCallback?: (query: ExecutableSqlQuery, client: SqlClient) => void 166 | ) { 167 | this.dialect = connection.getDialect() 168 | } 169 | 170 | async execute(query: ExecutableSqlQuery): Promise { 171 | if (!await this.connection.isConnected()) { 172 | await this.connect() 173 | } 174 | if (this.queryCallback) { 175 | this.queryCallback(query, this) 176 | } 177 | if (this.listQuery) { 178 | this.querys.push(query) 179 | } 180 | return await this.connection.execute(query).catch((err) => { 181 | err.message = "Error while execute following query:\n```sql\n" + query[0] + "\n```\n" + 182 | 183 | err.message 184 | 185 | throw err 186 | }) 187 | } 188 | 189 | async connect(): Promise { 190 | if (this.closePromise) { 191 | await this.closePromise 192 | } 193 | if (this.connectPromise) { 194 | await this.connectPromise 195 | } 196 | if (this.closeTimeout) { 197 | clearTimeout(this.closeTimeout) 198 | } 199 | this.closeTimeout = setTimeout( 200 | async () => { 201 | await this.close().catch(() => { }) 202 | this.closeTimeout = undefined 203 | }, 204 | this.connectionTime 205 | ) 206 | if (await this.connection.isConnected()) { 207 | return 208 | } 209 | 210 | return this.connectPromise = this.connection.connect().then(() => { 211 | this.connectPromise = undefined 212 | }) 213 | } 214 | 215 | async close(): Promise { 216 | if (this.connectPromise) { 217 | await this.connectPromise 218 | } 219 | if (this.closePromise) { 220 | await this.closePromise 221 | } 222 | if (this.closeTimeout) { 223 | clearTimeout(this.closeTimeout) 224 | } 225 | if (!(await this.connection.isConnected())) { 226 | return 227 | } 228 | return this.closePromise = this.connection.close().then(() => { 229 | this.closePromise = undefined 230 | }) 231 | } 232 | 233 | private tables: SqlTable[] = [] 234 | 235 | async createAllTables(): Promise { 236 | for (let index = 0; index < this.tables.length; index++) { 237 | const table = this.tables[index] 238 | await table.createTable() 239 | } 240 | } 241 | 242 | async dropAllTables(): Promise { 243 | for (let index = this.tables.length - 1; index >= 0; index--) { 244 | const table = this.tables[index] 245 | await table.dropTable() 246 | } 247 | } 248 | 249 | getSqlTables(): SqlTable[] { 250 | return this.tables 251 | } 252 | 253 | resetSqlTables(): void { 254 | this.tables = [] 255 | } 256 | 257 | removeTableByName(table: SqlTable): void { 258 | this.tables = this.tables.filter((t) => t.name == table.name) 259 | } 260 | 261 | getTable( 262 | name: string, 263 | columns?: Column[], 264 | foreignKey?: ForeignKey[], 265 | ): SqlTable { 266 | const table = new SqlTable( 267 | this, 268 | name, 269 | columns ?? [], 270 | foreignKey ?? [] 271 | ) 272 | this.tables.push(table) 273 | return table 274 | } 275 | 276 | async getTables(): Promise { 277 | return await this.execute( 278 | this.dialect.getTablesQuery(this) 279 | ) 280 | } 281 | } 282 | 283 | export class SqlTable { 284 | constructor( 285 | public readonly database: SqlClient, 286 | public readonly name: string, 287 | public readonly columns: Column[], 288 | public readonly foreignKey: ForeignKey[], 289 | ) { 290 | } 291 | 292 | async exist(): Promise { 293 | return (await this.getStructure()) == undefined 294 | } 295 | 296 | async getStructure(): Promise { 297 | return ( 298 | await this.database.execute( 299 | this.database.dialect.getTableStructure( 300 | this 301 | ) 302 | ) 303 | ).rows.shift() 304 | } 305 | 306 | async getStructureHash(): Promise { 307 | const struct = await this.getStructure() 308 | if (!struct) { 309 | return undefined 310 | } 311 | return crypto 312 | .createHash('sha256') 313 | .update(JSON.stringify(struct)) 314 | .digest('hex') 315 | } 316 | 317 | async createTable(): Promise { 318 | await this.database.execute( 319 | this.database.dialect.createTableQuery( 320 | this 321 | ) 322 | ) 323 | } 324 | 325 | async dropTable(): Promise { 326 | await this.database.execute( 327 | this.database.dialect.dropTableQuery( 328 | this 329 | ) 330 | ) 331 | } 332 | 333 | async insert( 334 | set: SqlSetValueMap, 335 | returning?: SqlResultColumnSelector | undefined, 336 | ): Promise { 337 | return ( 338 | await this.database.execute( 339 | this.database.dialect.insertQuery( 340 | this, 341 | set, 342 | returning 343 | ) 344 | ) 345 | ).rows[0] 346 | } 347 | 348 | async update( 349 | set: SqlSetValueMap, 350 | where?: SqlCondition, 351 | returning?: SqlResultColumnSelector | undefined, 352 | ): Promise { 353 | return ( 354 | await this.database.execute( 355 | this.database.dialect.updateQuery( 356 | this, 357 | set, 358 | where, 359 | returning 360 | ) 361 | ) 362 | ).rows 363 | } 364 | 365 | async select( 366 | select?: SqlResultColumnSelector, 367 | where?: SqlCondition, 368 | limit?: number | undefined, 369 | ...joins: SqlJoin[] 370 | ): Promise { 371 | return ( 372 | await this.database.execute( 373 | this.database.dialect.selectQuery( 374 | this, 375 | select, 376 | where, 377 | limit, 378 | ...joins 379 | ) 380 | ) 381 | ).rows 382 | } 383 | 384 | 385 | async selectOne( 386 | select?: SqlResultColumnSelector | undefined, 387 | where?: SqlCondition, 388 | ...joins: SqlJoin[] 389 | ): Promise { 390 | const rows = await this.select(select, where, 1, ...joins) 391 | if (rows.length < 1) { 392 | return undefined 393 | } 394 | return rows[0] 395 | } 396 | 397 | async delete( 398 | where?: SqlCondition, 399 | returning?: SqlResultColumnSelector | undefined, 400 | ): Promise { 401 | return ( 402 | await this.database.execute( 403 | this.database.dialect.deleteQuery( 404 | this, 405 | where, 406 | returning 407 | ) 408 | ) 409 | ).rows 410 | } 411 | } 412 | 413 | export function removeSpaces(query: string): string { 414 | while (query.startsWith(" ") || query.startsWith("\n")) { 415 | query = query.substring(1) 416 | } 417 | while (query.endsWith(" ") || query.endsWith("\n")) { 418 | query = query.slice(0, -1) 419 | } 420 | return query 421 | } 422 | 423 | export function toPrettyQuery( 424 | query: string, 425 | queryKeywords: string[] = [ 426 | "SELECT", 427 | "DELETE", 428 | "UPDATE", 429 | "INSERT", 430 | "FROM", 431 | "INTO", 432 | "WHERE", 433 | "INNER JOIN", 434 | "LEFT JOIN", 435 | "RIGHT JOIN", 436 | "FULL JOIN", 437 | "ON", 438 | ] 439 | ): string { 440 | query = query 441 | .split("\n").map((q) => removeSpaces(q)).join(" ") 442 | .split(";").map((q) => removeSpaces(q)).join("; ") 443 | while (query.includes(" ")) { 444 | query = query 445 | .split(" ").join(" ") 446 | } 447 | query = (" " + query + " ") 448 | queryKeywords.forEach((keyword) => { 449 | query = query 450 | .split(" " + keyword + " ") 451 | .map((q) => removeSpaces(q)) 452 | .join("\n" + keyword + "\n ") 453 | }) 454 | query = removeSpaces(query) 455 | return query 456 | .split("=").map((q) => removeSpaces(q)).join("=") 457 | .split("=").map((q) => removeSpaces(q)).join("=") 458 | .split(",").map((q) => removeSpaces(q)).join(",\n ") 459 | .split("(").map((q) => removeSpaces(q)).join(" (\n ") 460 | .split(")").map((q) => removeSpaces(q)).join("\n) ") 461 | } 462 | 463 | export function createFillerString(value: string, size: number): string { 464 | let i = 0 465 | let filler: string = "" 466 | while (i < size) { 467 | filler += value 468 | i++ 469 | } 470 | return filler 471 | } 472 | 473 | export function isSqlValue(value: any): boolean { 474 | const type = typeof value 475 | return value == null || type == "string" || type == "number" || type == "boolean" 476 | } 477 | 478 | export interface PrettyStringOptions { 479 | stringFont?: Font, 480 | numberFont?: Font, 481 | booleanFont?: Font, 482 | objectFont?: Font, 483 | objectKeyFont?: Font, 484 | arrayFont?: Font, 485 | nullFont?: Font, 486 | undefinedFont?: Font, 487 | functionFont?: Font, 488 | tabSpaces?: number, 489 | level?: number, 490 | maxLevel?: number, 491 | } 492 | 493 | export interface PrettyStringSettings { 494 | stringFont?: Font, 495 | numberFont?: Font, 496 | booleanFont?: Font, 497 | objectFont?: Font, 498 | objectKeyFont?: Font, 499 | arrayFont?: Font, 500 | nullFont?: Font, 501 | undefinedFont?: Font, 502 | functionFont?: Font, 503 | tabSpaces: number, 504 | level: number, 505 | maxLevel: number, 506 | } 507 | 508 | export function toPrettyString( 509 | obj: any, 510 | options?: PrettyStringOptions, 511 | ): string { 512 | const settings: PrettyStringSettings = { 513 | stringFont: ["green", null], 514 | numberFont: ["yellow", null], 515 | booleanFont: ["blue", null], 516 | objectFont: ["gray", null], 517 | objectKeyFont: ["white", null], 518 | arrayFont: ["gray", null], 519 | nullFont: ["magenta", null], 520 | undefinedFont: ["magenta", null], 521 | functionFont: ["blue", null], 522 | tabSpaces: 4, 523 | level: 0, 524 | maxLevel: -1, 525 | ...options 526 | } 527 | const type = typeof obj 528 | if (type == "string") { 529 | return styleText('"' + obj + '"', settings.stringFont) 530 | } else if (type == "number") { 531 | return styleText("" + obj, settings.numberFont) 532 | } else if (type == "boolean") { 533 | return styleText(obj == true ? "TRUE" : "FALSE", settings.booleanFont) 534 | } else if (type == "undefined") { 535 | return styleText("UNDEFINED", settings.undefinedFont) 536 | } else if (type == "object") { 537 | if (obj == null) { 538 | return styleText("NULL", settings.nullFont) 539 | } else if (Array.isArray(obj)) { 540 | let line: string = "" 541 | if (obj.length == 0) { 542 | line += styleText("[]", settings.arrayFont) 543 | } else if (settings.maxLevel == settings.level) { 544 | line += styleText("[...]", settings.arrayFont) 545 | } else { 546 | line += styleText("[\n" + createFillerString(" ", settings.level * settings.tabSpaces + settings.tabSpaces) + "1. ", settings.arrayFont) + 547 | obj 548 | .map( 549 | (value: any, index: number) => { 550 | let line = toPrettyString(value, { 551 | ...settings, 552 | level: settings.level + 1 553 | }) 554 | if (index < obj.length - 1) { 555 | line += styleText(",\n" + createFillerString(" ", settings.level * settings.tabSpaces + settings.tabSpaces) + (index + 2) + ". ", settings.arrayFont) 556 | } 557 | return line 558 | } 559 | ).join("") + 560 | styleText("\n" + createFillerString(" ", settings.level * settings.tabSpaces) + "]", settings.arrayFont) 561 | } 562 | return line 563 | } else { 564 | let line: string = "" 565 | if ( 566 | obj instanceof Object && 567 | obj.constructor && 568 | typeof obj.constructor.name == "string" && 569 | obj.constructor.name != "Object" 570 | ) { 571 | line += 572 | styleText("[", settings.objectFont) + 573 | styleText(obj.constructor.name, settings.objectKeyFont) + 574 | styleText("] ", settings.objectFont) 575 | } 576 | if (Object.keys(obj).length == 0) { 577 | line += styleText("{}", settings.arrayFont) 578 | } else { 579 | line += styleText("{\n" + createFillerString(" ", settings.level * settings.tabSpaces + settings.tabSpaces), settings.objectFont) + 580 | Object.keys(obj).map( 581 | (key: string) => 582 | styleText(key, settings.objectKeyFont) + 583 | styleText(": ", settings.objectFont) + 584 | toPrettyString(obj[key], { 585 | ...settings, 586 | level: settings.level + 1 587 | }) 588 | ) 589 | .join( 590 | styleText(",\n" + createFillerString(" ", settings.level * settings.tabSpaces + settings.tabSpaces), settings.objectFont) 591 | ) + 592 | styleText("\n" + createFillerString(" ", settings.level * settings.tabSpaces) + "}", settings.objectFont) 593 | } 594 | return line 595 | } 596 | } else if (type == "function") { 597 | return styleText("FUNCTION", settings.booleanFont) 598 | } 599 | return "" + obj 600 | } 601 | 602 | export interface Colume { 603 | name: string, 604 | type: string, 605 | primary: boolean, 606 | nullable: boolean, 607 | } 608 | 609 | export interface ForeignKeys { 610 | [keyName: string]: ForeignKey 611 | } 612 | 613 | export interface Table { 614 | name: string, 615 | createQuery: string, 616 | colums: Colume[], 617 | foreignKeys: ForeignKeys 618 | } 619 | 620 | export type Tables = Table[] 621 | 622 | export interface TableMap { 623 | [tableName: string]: Table 624 | } 625 | 626 | export async function asyncMap( 627 | arr: I[], 628 | cb: (value: I) => undefined | Promise 629 | ): Promise { 630 | const promises: Promise[] = [] 631 | for (let index = 0; index < arr.length; index++) { 632 | const input = arr[index] 633 | const promise = cb(input) 634 | if (promise) { 635 | promises.push(promise) 636 | } 637 | } 638 | const ret: T[] = [] 639 | for (let index = 0; index < promises.length; index++) { 640 | const promise = promises[index]; 641 | const out = await promise 642 | if (out) { 643 | ret.push(out) 644 | } 645 | } 646 | return ret 647 | } 648 | 649 | export function jsTypeToPostgresType(type: string, length: number = -1): [string, number] { 650 | type = type.toLowerCase() 651 | switch (type) { 652 | case "string": 653 | if (length > 128) { 654 | return ["text", length] 655 | } 656 | return ["varchar", length] 657 | case "number": 658 | return ["int", length] 659 | case "boolean": 660 | return ["bool", length] 661 | default: 662 | return [type, length] 663 | } 664 | } 665 | 666 | export function postgresTypeToJsType(type: string, length: number = -1): [string, number] { 667 | type = type.toLowerCase() 668 | switch (type) { 669 | case "text": 670 | case "varchar": 671 | return ["string", length] 672 | case "long": 673 | case "int": 674 | return ["number", length] 675 | case "bool": 676 | return ["boolean", length] 677 | default: 678 | return [type, length] 679 | } 680 | } 681 | 682 | export function getPostgresType(type: string): [string, number] { 683 | type = type.toLowerCase() 684 | 685 | while (type.startsWith(" ") || type.startsWith("\n")) { 686 | type = type.substring(1) 687 | } 688 | while (type.endsWith(" ") || type.endsWith("\n")) { 689 | type = type.slice(0, -1) 690 | } 691 | if (type.endsWith(")") && type.includes("(")) { 692 | const index = type.indexOf("(") 693 | const type2 = type.substring(0, index) 694 | type = type.slice(0, -1) 695 | type = type.substring(index + 1) 696 | let length = Number(type) 697 | if (isNaN(length)) { 698 | length = -1 699 | } 700 | return [type2, length] 701 | } 702 | return [type, -1] 703 | } 704 | 705 | export function mapToObject( 706 | arr: T[], 707 | cb: (value: T, index: number) => string | undefined 708 | ): { 709 | [key: string]: T 710 | } { 711 | const obj: { 712 | [key: string]: T 713 | } = {} 714 | for (let index = 0; index < arr.length; index++) { 715 | const value = arr[index]; 716 | const key = cb(value, index) 717 | if (key) { 718 | obj[key] = value 719 | } 720 | } 721 | return obj 722 | } 723 | 724 | export function dropTable(tableName: string): string { 725 | return 'DROP TABLE IF EXISTS "' + tableName + '";' 726 | } 727 | 728 | export type FontColorStyle = "rainbow" | "zebra" | "america" | "trap" | "random" | "zalgo" 729 | export type FontNativeColor = "red" | "black" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey" 730 | export type FontColor = FontColorStyle | FontNativeColor 731 | export type FontBgColor = "bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" 732 | export type FontStyle = "reset" | "bold" | "dim" | "italic" | "underline" | "inverse" | "hidden" | "strikethrough" 733 | 734 | export const fontColorStyles: FontColorStyle[] = ["rainbow", "zebra", "america", "trap", "random", "zalgo"] 735 | export const fontNativeColor: FontNativeColor[] = ["black", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", "grey"] 736 | export const fontColor: FontColor[] = [...fontNativeColor, ...fontColorStyles] 737 | export const fontBgColor: FontBgColor[] = ["bgBlack", "bgRed", "bgGreen", "bgYellow", "bgBlue", "bgMagenta", "bgCyan", "bgWhite"] 738 | export const fontStyle: FontStyle[] = ["reset", "bold", "dim", "italic", "underline", "inverse", "hidden", "strikethrough"] 739 | 740 | export type Font = [FontColor | null, FontBgColor | null, ...FontStyle[]] 741 | 742 | export function styleText(text: string, font: Font | undefined): string { 743 | if (font) { 744 | font.forEach((fontStyle: any) => { 745 | if (!fontStyle) { 746 | return 747 | } 748 | const tmp = text[fontStyle] 749 | if (!tmp) { 750 | return 751 | } 752 | if (typeof tmp == "function") { 753 | text = (text as any)[fontStyle]() 754 | } else { 755 | text = tmp 756 | } 757 | }) 758 | } 759 | return text 760 | } 761 | 762 | export function mixFonts(main: Font, seconds: Font, mixStyles: boolean = true): Font { 763 | if (mixStyles) { 764 | return [ 765 | main[0] ?? seconds[0] ?? null, 766 | main[1] ?? seconds[1] ?? null, 767 | ...[ 768 | ...main.slice(2) as FontStyle[], 769 | ...seconds.slice(2) as FontStyle[] 770 | ] 771 | ] 772 | } else { 773 | return [ 774 | main[0] ?? seconds[0] ?? null, 775 | main[1] ?? seconds[1] ?? null, 776 | ...( 777 | main.length > 2 ? 778 | main.slice(2) as FontStyle[] : 779 | seconds.slice(2) as FontStyle[] 780 | ) 781 | ] 782 | } 783 | } 784 | 785 | export async function showTable( 786 | table: SqlTable, 787 | maxValueSize: number = 16, 788 | defaultFont: Font = ["white", "bgBlack"], 789 | titleFont: Font = ["yellow", null, "bold"], 790 | columeFont: Font = [null, null, "underline"], 791 | fontOrder: Font[] = [ 792 | ["yellow", null], 793 | ["red", null], 794 | ["magenta", null], 795 | ["blue", null], 796 | ["green", null] 797 | ] 798 | ): Promise { 799 | const rows = await table.select() 800 | console.info(generateResultString( 801 | table.name, 802 | rows, 803 | maxValueSize, 804 | defaultFont, 805 | titleFont, 806 | columeFont, 807 | fontOrder 808 | )) 809 | } 810 | 811 | export interface RowResult { 812 | [key: string]: string | number | boolean | null 813 | } 814 | 815 | export type SelectResult = RowResult[] 816 | 817 | export function generateResultString( 818 | title: string, 819 | result: SqlQueryResultRow[], 820 | maxValueSize: number = 16, 821 | defaultFont: Font = ["white", "bgBlack"], 822 | titleFont: Font = ["yellow", null, "bold"], 823 | columeFont: Font = [null, null, "underline"], 824 | fontOrder: Font[] = [ 825 | ["yellow", null], 826 | ["red", null], 827 | ["magenta", null], 828 | ["blue", null], 829 | ["green", null] 830 | ] 831 | ): string { 832 | const paint = (text: string, font?: Font): string => { 833 | if (font) { 834 | font = mixFonts(font, defaultFont) 835 | } else { 836 | font = defaultFont 837 | } 838 | return styleText(text, font) 839 | } 840 | 841 | if (fontOrder.length == 0) { 842 | fontOrder.push(["green", null]) 843 | } 844 | 845 | let preSpaceSize = title.length + 2 846 | let msg = "" 847 | msg += paint("| ") 848 | msg += paint( 849 | title, 850 | titleFont 851 | ) 852 | try { 853 | if (result.length == 0) { 854 | msg += paint("\n| EMPTY!") 855 | return 856 | } 857 | let targetFontIndex = 0 858 | msg += paint("\n| ") 859 | let columeLineSize: number = 4 860 | const tableColums = Object.keys(result[0]) 861 | const coloredColums = tableColums.map((value: string) => { 862 | const currentFont = fontOrder[targetFontIndex] 863 | targetFontIndex++ 864 | if (targetFontIndex >= fontOrder.length) { 865 | targetFontIndex = 0 866 | } 867 | const text = "" + value 868 | columeLineSize += text.length 869 | return paint(text, mixFonts(currentFont, columeFont)) 870 | }) 871 | columeLineSize += (coloredColums.length - 1) * 3 872 | if (columeLineSize > preSpaceSize) { 873 | preSpaceSize = columeLineSize 874 | } 875 | msg += coloredColums.join(paint(" | ")) 876 | msg += paint(" |") 877 | 878 | result.forEach((row: SqlQueryResultRow) => { 879 | targetFontIndex = 0 880 | msg += paint("\n| ") 881 | const coloredColums = Object.values(row).map((value: any) => { 882 | const currentFont = fontOrder[targetFontIndex] 883 | targetFontIndex++ 884 | if (targetFontIndex >= fontOrder.length) { 885 | targetFontIndex = 0 886 | } 887 | let text = "" + value 888 | if (text.length > maxValueSize) { 889 | text = text.substring(0, maxValueSize) + "..." 890 | } 891 | return paint(text, currentFont) 892 | }) 893 | msg += coloredColums.join(paint(" | ")) 894 | msg += paint(" |") 895 | }) 896 | } finally { 897 | let preSpace: string = "" 898 | let i = 0 899 | while (i < preSpaceSize) { 900 | preSpace += " " 901 | i++ 902 | } 903 | return paint(preSpace + "\n", [null, null, "underline"]) + msg 904 | } 905 | } --------------------------------------------------------------------------------