├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build_and_test.yml │ ├── build_and_test_sample_ts.yml │ ├── deploy-docs.yml │ └── publish.yml ├── .gitignore ├── .nvmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── samples └── simple-ts │ ├── .dockerignore │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .nvmrc │ ├── .prettierignore │ ├── .prettierrc.json │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json │ ├── README.md │ ├── cockroachdb │ └── haproxy │ │ ├── Dockerfile │ │ └── haproxy.cfg │ ├── docker-compose-cockroachdb.yml │ ├── docker-compose-yugabytedb.yml │ ├── docker-compose.yml │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── pongo.config.ts │ ├── shim.ts │ └── typedClient.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── setup.ps1 ├── setup.sh └── src ├── .editorconfig ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── Node.js.code-profile ├── launch.json ├── settings.json └── tasks.json ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── custom.css │ │ └── index.mts ├── getting-started.md ├── index.md └── public │ ├── logo-square.png │ ├── logo.png │ └── social.png ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── packages ├── dumbo │ ├── package.json │ ├── src │ │ ├── benchmarks │ │ │ ├── index.ts │ │ │ └── ox.ts │ │ ├── core │ │ │ ├── connections │ │ │ │ ├── connection.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pool.ts │ │ │ │ └── transaction.ts │ │ │ ├── connectors │ │ │ │ └── index.ts │ │ │ ├── execute │ │ │ │ ├── execute.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── locks │ │ │ │ ├── databaseLock.ts │ │ │ │ └── index.ts │ │ │ ├── query │ │ │ │ ├── index.ts │ │ │ │ ├── mappers.ts │ │ │ │ ├── query.ts │ │ │ │ └── selectors.ts │ │ │ ├── schema │ │ │ │ ├── index.ts │ │ │ │ ├── migrations.ts │ │ │ │ └── schemaComponent.ts │ │ │ ├── serializer │ │ │ │ ├── index.ts │ │ │ │ └── json │ │ │ │ │ └── index.ts │ │ │ ├── sql │ │ │ │ ├── index.ts │ │ │ │ ├── pg-format │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── index.unit.spec.ts │ │ │ │ │ ├── pgFormat.ts │ │ │ │ │ ├── reserved.ts │ │ │ │ │ └── sqlFormatter.unit.spec.ts │ │ │ │ ├── sql.ts │ │ │ │ ├── sqlFormatter.ts │ │ │ │ └── sqlite-format │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sqlFormatter.unit.spec.ts │ │ │ │ │ └── sqliteFormat.ts │ │ │ └── tracing │ │ │ │ ├── index.ts │ │ │ │ └── printing │ │ │ │ ├── color.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pretty.ts │ │ │ │ └── pretty.unit.spec.ts │ │ ├── index.ts │ │ ├── pg.ts │ │ ├── sqlite3.ts │ │ └── storage │ │ │ ├── postgresql │ │ │ ├── core │ │ │ │ ├── connections │ │ │ │ │ ├── connectionString.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── locks │ │ │ │ │ ├── advisoryLocks.ts │ │ │ │ │ └── index.ts │ │ │ │ └── schema │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── migrations.int.spec.ts │ │ │ │ │ ├── migrations.ts │ │ │ │ │ └── schema.ts │ │ │ └── pg │ │ │ │ ├── connections │ │ │ │ ├── connection.int.spec.ts │ │ │ │ ├── connection.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pool.ts │ │ │ │ └── transaction.ts │ │ │ │ ├── execute │ │ │ │ ├── execute.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── serialization │ │ │ │ └── index.ts │ │ │ └── sqlite │ │ │ ├── core │ │ │ ├── connections │ │ │ │ └── index.ts │ │ │ ├── execute │ │ │ │ ├── execute.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── pool │ │ │ │ ├── index.ts │ │ │ │ └── pool.ts │ │ │ └── transactions │ │ │ │ └── index.ts │ │ │ └── sqlite3 │ │ │ ├── connections │ │ │ ├── connection.int.spec.ts │ │ │ ├── connection.ts │ │ │ └── index.ts │ │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── tsup.config.ts └── pongo │ ├── package.json │ ├── src │ ├── cli.ts │ ├── commandLine │ │ ├── configFile.ts │ │ ├── index.ts │ │ ├── migrate.ts │ │ └── shell.ts │ ├── core │ │ ├── collection │ │ │ ├── index.ts │ │ │ ├── pongoCollection.ts │ │ │ └── query.ts │ │ ├── errors │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── pongoClient.connections.int.spec.ts │ │ ├── pongoClient.ts │ │ ├── pongoDb.ts │ │ ├── pongoSession.ts │ │ ├── pongoTransaction.ts │ │ ├── schema │ │ │ └── index.ts │ │ ├── typing │ │ │ ├── entries.ts │ │ │ ├── index.ts │ │ │ └── operations.ts │ │ └── utils │ │ │ ├── deepEquals.ts │ │ │ └── index.ts │ ├── e2e │ │ ├── cli-config.ts │ │ ├── compatibilityTest.e2e.spec.ts │ │ ├── postgres.e2e.spec.ts │ │ └── postgres.optimistic-concurrency.spec.ts │ ├── index.ts │ ├── mongo │ │ ├── findCursor.ts │ │ ├── index.ts │ │ ├── mongoClient.ts │ │ ├── mongoCollection.ts │ │ └── mongoDb.ts │ ├── pg.ts │ ├── shim.ts │ ├── sqlite3.ts │ └── storage │ │ ├── postgresql │ │ ├── dbClient.ts │ │ ├── index.ts │ │ ├── migrations │ │ │ └── migrations.int.spec.ts │ │ └── sqlBuilder │ │ │ ├── filter │ │ │ ├── index.ts │ │ │ └── queryOperators.ts │ │ │ ├── index.ts │ │ │ └── update │ │ │ └── index.ts │ │ └── sqlite │ │ ├── core │ │ └── index.ts │ │ └── sqlite3 │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── tsup.config.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.shared.json └── tsup.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle line endings automatically for files detected as text 2 | # and leave all files detected as binary untouched. 3 | * text=auto 4 | 5 | # Force the following filetypes to have unix eols, so Windows does not break them 6 | *.* text eol=lf 7 | 8 | *.png binary 9 | *.jpg binary 10 | *.jpeg binary 11 | *.gif binary 12 | *.ico binary 13 | *.mov binary 14 | *.mp4 binary 15 | *.mp3 binary 16 | *.flv binary 17 | *.fla binary 18 | *.swf binary 19 | *.gz binary 20 | *.zip binary 21 | *.7z binary 22 | *.ttf binary 23 | *.eot binary 24 | *.woff binary 25 | *.pyc binary 26 | *.pdf binary 27 | *.ez binary 28 | *.bz2 binary 29 | *.swp binary -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [event-driven-io] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | # run it on push to the default repository branch 5 | push: 6 | branches: [main] 7 | paths: 8 | - "src/**" 9 | - "./.github/workflows/build_and_test.yml" 10 | 11 | # run it during pull request 12 | pull_request: 13 | paths: 14 | - "src/**" 15 | - "./.github/workflows/build_and_test.yml" 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | defaults: 21 | run: 22 | working-directory: src 23 | 24 | jobs: 25 | build-and-test-code: 26 | name: Build application code 27 | # use system defined below in the tests matrix 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Check Out Repo 32 | uses: actions/checkout@v4 33 | 34 | - name: Use Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version-file: ./src/.nvmrc 38 | cache: "npm" 39 | cache-dependency-path: "./src/package-lock.json" 40 | 41 | - name: Install dependencies 42 | run: npm ci 43 | 44 | - name: Build TS 45 | run: npm run build:ts 46 | 47 | - name: Run linting (ESlint and Prettier) 48 | run: npm run lint 49 | 50 | - name: Build 51 | run: npm run build 52 | 53 | - name: Test 54 | run: npm run test 55 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test_sample_ts.yml: -------------------------------------------------------------------------------- 1 | name: Build and test Simple Sample TypeScript 2 | 3 | on: 4 | # run it on push to the default repository branch 5 | push: 6 | branches: [main] 7 | paths: 8 | - "samples/simple-ts/**" 9 | - "./.github/workflows/build_and_test_sample_ts.yml" 10 | 11 | # run it during pull request 12 | pull_request: 13 | paths: 14 | - "samples/simple-ts/**" 15 | - "./.github/workflows/build_and_test_sample_ts.yml" 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | defaults: 21 | run: 22 | working-directory: samples/simple-ts 23 | 24 | jobs: 25 | build-and-test-code: 26 | name: Build sample code 27 | # use system defined below in the tests matrix 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Check Out Repo 32 | uses: actions/checkout@v4 33 | 34 | - name: Use Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version-file: ./samples/simple-ts/.nvmrc 38 | cache: "npm" 39 | cache-dependency-path: "./samples/simple-ts/package-lock.json" 40 | 41 | - name: Install dependencies 42 | run: npm ci 43 | 44 | - name: Build TS 45 | run: npm run build:ts 46 | 47 | - name: Run linting (ESlint and Prettier) 48 | run: npm run lint 49 | 50 | - name: Build 51 | run: npm run build 52 | 53 | - name: Test 54 | run: npm run test 55 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Deploy VitePress site to Pages 4 | 5 | on: 6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 7 | # using the `master` branch as the default branch. 8 | push: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | defaults: 15 | run: 16 | working-directory: src 17 | 18 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 19 | permissions: 20 | contents: read 21 | pages: write 22 | id-token: write 23 | 24 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 25 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 26 | concurrency: 27 | group: pages 28 | cancel-in-progress: false 29 | 30 | jobs: 31 | # Build job 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 39 | # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm 40 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun 41 | - name: Setup Node 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version-file: ./src/.nvmrc 45 | cache: "npm" 46 | cache-dependency-path: "./src/package-lock.json" 47 | 48 | - name: Setup Pages 49 | uses: actions/configure-pages@v4 50 | 51 | - name: Install dependencies 52 | run: npm ci # or pnpm install / yarn install / bun install 53 | 54 | - name: Build with VitePress 55 | run: | 56 | npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build 57 | touch docs/.vitepress/dist/.nojekyll 58 | 59 | - name: Upload artifact 60 | uses: actions/upload-pages-artifact@v3 61 | with: 62 | path: ./src/docs/.vitepress/dist 63 | 64 | # Deployment job 65 | deploy: 66 | environment: 67 | name: github-pages 68 | url: ${{ steps.deployment.outputs.page_url }} 69 | needs: build 70 | runs-on: ubuntu-latest 71 | name: Deploy 72 | steps: 73 | - name: Deploy to GitHub Pages 74 | id: deployment 75 | uses: actions/deploy-pages@v4 76 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: [workflow_dispatch] 3 | 4 | defaults: 5 | run: 6 | working-directory: src 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check Out Repo 13 | uses: actions/checkout@v4 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | registry-url: "https://registry.npmjs.org" 19 | node-version-file: ./src/.nvmrc 20 | cache: "npm" 21 | cache-dependency-path: "./src/package-lock.json" 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Copy README.md 27 | run: npm run copy:readme 28 | 29 | - name: Build 30 | run: npm run build 31 | env: 32 | NODE_ENV: production 33 | 34 | - name: Set public publishing 35 | run: npm config set access public 36 | 37 | - name: Publish Dumbo package on NPM 📦 38 | run: npm publish --w @event-driven-io/dumbo 39 | continue-on-error: true 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | NODE_ENV: production 43 | 44 | - name: Publish Pongo package on NPM 📦 45 | run: npm publish --w @event-driven-io/pongo 46 | continue-on-error: true 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | NODE_ENV: production 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | */node_modules/ 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | # vitepress 120 | src/docs/.vitepress/dist 121 | src/docs/.vitepress/cache 122 | lib 123 | 124 | .DS_Store 125 | */.output 126 | e2e/esmCompatibility/.output 127 | src/e2e/esmCompatibility/.output 128 | **/0x -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "connor4312.nodejs-testing" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug All Tests (Node)", 6 | "type": "node", 7 | "request": "launch", 8 | "skipFiles": ["/**"], 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run-script", "test", "--inspect-brk=9229"], // Use --inspect-brk for debugging 11 | "console": "integratedTerminal", 12 | "internalConsoleOptions": "neverOpen", 13 | "cwd": "${workspaceFolder}/src/", 14 | "sourceMaps": true 15 | }, 16 | { 17 | "name": "Debug Current Test File", 18 | "type": "node", 19 | "request": "launch", 20 | "env": { "DUMBO_LOG_LEVEL": "DEBUG" }, 21 | "skipFiles": ["/**"], 22 | "runtimeExecutable": "npm", 23 | "runtimeArgs": [ 24 | "run-script", 25 | "test:file", 26 | "--", 27 | "${file}", 28 | "--inspect-brk=9229" 29 | ], // Use --inspect-brk for debugging 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "cwd": "${workspaceFolder}/src/", 33 | "sourceMaps": true 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnSave": true, 5 | 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit", 8 | "source.fixAll.eslint": "explicit", 9 | "source.addMissingImports": "always" 10 | }, 11 | 12 | "[typescript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.formatOnPaste": false, 15 | "editor.formatOnSave": true, 16 | 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": "explicit", 19 | "source.fixAll.eslint": "explicit", 20 | "source.addMissingImports": "always" 21 | } 22 | }, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode", 25 | "editor.formatOnPaste": false, 26 | "editor.formatOnSave": true, 27 | 28 | "editor.codeActionsOnSave": { 29 | "source.organizeImports": "explicit", 30 | "source.fixAll.eslint": "explicit", 31 | "source.addMissingImports": "always" 32 | } 33 | }, 34 | 35 | "editor.tabSize": 2, 36 | 37 | "files.exclude": { 38 | "node_modules/": true, 39 | "**/node_modules/": true, 40 | "**/dist/": true, 41 | "**/*.tsbuildinfo": true 42 | }, 43 | "files.eol": "\n", 44 | 45 | "typescript.preferences.importModuleSpecifier": "relative", 46 | "eslint.workingDirectories": [{ "mode": "auto" }], 47 | "debug.javascript.terminalOptions": { 48 | "nodeVersionHint": 20 49 | }, 50 | "nodejs-testing.include": ["./**/**"], 51 | "nodejs-testing.extensions": [ 52 | { 53 | "extensions": ["ts", "mts"], 54 | "filePatterns": ["**/*.spec.ts", "**/*.spec.mts"], 55 | "parameters": ["--import", "tsx"] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build:ts:watch", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build:ts:watch", 13 | "detail": "tsc -b --watch", 14 | "options": { 15 | "cwd": "${workspaceFolder}/src/" 16 | } 17 | }, 18 | { 19 | "label": "Install All Recommended Extensions", 20 | "type": "shell", 21 | "windows": { 22 | "command": "powershell ((node -e \"console.log(JSON.parse(require('fs').readFileSync('./.vscode/extensions.json')).recommendations.join('\\\\n'))\") -split '\\r?\\n') | ForEach-Object { code --install-extension $_ }" 23 | }, 24 | "command": "node -e \"console.log(JSON.parse(require('fs').readFileSync('./.vscode/extensions.json')).recommendations.join('\\n'))\" | xargs -L 1 code --install-extension" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /samples/simple-ts/.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | **/dist/ 3 | **/*.d.ts 4 | /src/types/ -------------------------------------------------------------------------------- /samples/simple-ts/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | -------------------------------------------------------------------------------- /samples/simple-ts/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/* 2 | # build artifacts 3 | dist/*coverage/* 4 | 5 | # data definition files 6 | **/*.d.ts 7 | 8 | # custom definition files 9 | /src/types/ 10 | 11 | !.eslintrc.js -------------------------------------------------------------------------------- /samples/simple-ts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2023": true, 4 | "node": true 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "plugin:prettier/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2023, 16 | "sourceType": "module", 17 | "project": "./tsconfig.json" 18 | }, 19 | "rules": { 20 | "no-unused-vars": "off", 21 | "@typescript-eslint/no-unused-vars": [ 22 | "error", 23 | { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" } 24 | ], 25 | "@typescript-eslint/no-misused-promises": ["off"], 26 | "@typescript-eslint/prefer-namespace-keyword": "off" 27 | }, 28 | "settings": { 29 | "import/resolver": { 30 | "typescript": {} 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/simple-ts/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 -------------------------------------------------------------------------------- /samples/simple-ts/.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | **/lib/ -------------------------------------------------------------------------------- /samples/simple-ts/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /samples/simple-ts/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "connor4312.nodejs-testing" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /samples/simple-ts/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug current file", 6 | "type": "node", 7 | "request": "launch", 8 | 9 | // Debug current file in VSCode 10 | "program": "${file}", 11 | 12 | /* 13 | Path to tsx binary 14 | Assuming locally installed 15 | */ 16 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", 17 | 18 | /* 19 | Open terminal when debugging starts (Optional) 20 | Useful to see console.logs 21 | */ 22 | "console": "integratedTerminal", 23 | "internalConsoleOptions": "neverOpen", 24 | 25 | // Files to exclude from debugger (e.g. call stack) 26 | "skipFiles": [ 27 | // Node.js internal core modules 28 | "/**", 29 | 30 | // Ignore all dependencies (optional) 31 | "${workspaceFolder}/node_modules/**" 32 | ] 33 | }, 34 | { 35 | "name": "Debug All Tests (Node)", 36 | "type": "node", 37 | "request": "launch", 38 | "skipFiles": ["/**"], 39 | "runtimeExecutable": "npm", 40 | "runtimeArgs": [ 41 | "run-script", 42 | "test", 43 | "--inspect-brk=9229", 44 | "--preserve-symlinks" 45 | ], // Use --inspect-brk for debugging 46 | "console": "integratedTerminal", 47 | "internalConsoleOptions": "neverOpen", 48 | "cwd": "${workspaceFolder}", 49 | "sourceMaps": true, 50 | "smartStep": true, 51 | "resolveSourceMapLocations": [ 52 | "${workspaceFolder}/**", 53 | "!**/node_modules/**", 54 | "node_modules/@event-driven.io/emmett-expressjs/**/*.*" 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /samples/simple-ts/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnSave": true, 5 | 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit", 8 | "source.fixAll.eslint": "explicit", 9 | "source.addMissingImports": "always" 10 | }, 11 | 12 | "[typescript]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "editor.formatOnPaste": false, 15 | "editor.formatOnSave": true, 16 | 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": "explicit", 19 | "source.fixAll.eslint": "explicit", 20 | "source.addMissingImports": "always" 21 | } 22 | }, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "esbenp.prettier-vscode", 25 | "editor.formatOnPaste": false, 26 | "editor.formatOnSave": true, 27 | 28 | "editor.codeActionsOnSave": { 29 | "source.organizeImports": "explicit", 30 | "source.fixAll.eslint": "explicit", 31 | "source.addMissingImports": "always" 32 | } 33 | }, 34 | 35 | "editor.tabSize": 2, 36 | 37 | "files.exclude": { 38 | "**/*.tsbuildinfo": true, 39 | "**/node_modules": true, 40 | "**/dist": true 41 | }, 42 | "files.eol": "\n", 43 | 44 | "typescript.preferences.importModuleSpecifier": "relative", 45 | "eslint.workingDirectories": [{ "mode": "auto" }], 46 | "debug.javascript.terminalOptions": { 47 | "nodeVersionHint": 20 48 | }, 49 | "nodejs-testing.include": ["./**/**"], 50 | "nodejs-testing.extensions": [ 51 | { 52 | "extensions": ["ts", "mts"], 53 | "filePatterns": ["**/*.spec.ts", "**/*.spec.mts"], 54 | "parameters": ["--import", "tsx"] 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /samples/simple-ts/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build:ts:watch", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "problemMatcher": [], 12 | "label": "npm: build:ts:watch", 13 | "detail": "tsc -b --watch" 14 | }, 15 | { 16 | "label": "Install All Recommended Extensions", 17 | "type": "shell", 18 | "windows": { 19 | "command": "powershell ((node -e \"console.log(JSON.parse(require('fs').readFileSync('./.vscode/extensions.json')).recommendations.join('\\\\n'))\") -split '\\r?\\n') | ForEach-Object { code --install-extension $_ }" 20 | }, 21 | "command": "node -e \"console.log(JSON.parse(require('fs').readFileSync('./.vscode/extensions.json')).recommendations.join('\\n'))\" | xargs -L 1 code --install-extension" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /samples/simple-ts/README.md: -------------------------------------------------------------------------------- 1 | [![](https://dcbadge.vercel.app/api/server/fTpqUTMmVa?style=flat)](https://discord.gg/VzvxR5Rb38) [](https://www.linkedin.com/in/oskardudycz/) [![Github Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/oskardudycz/)](https://github.com/sponsors/oskardudycz/) [![blog](https://img.shields.io/badge/blog-event--driven.io-brightgreen)](https://event-driven.io/?utm_source=event_sourcing_nodejs) [![blog](https://img.shields.io/badge/%F0%9F%9A%80-Architecture%20Weekly-important)](https://www.architecture-weekly.com/?utm_source=event_sourcing_nodejs) 2 | 3 | ![](./docs/public/logo.png) 4 | 5 | # Emmett - Sample showing event-sourced WebApi with Express.js and EventStoreDB 6 | 7 | Read more in [Emmett getting started guide](https://event-driven-io.github.io/emmett/getting-started.html). 8 | 9 | ## Prerequisities 10 | 11 | Sample require EventStoreDB, you can start it by running 12 | 13 | with PostgreSQL: 14 | 15 | ```bash 16 | docker-compose up 17 | ``` 18 | 19 | Alternatively, with YugabyteDB (PostgreSQL-compatible distributed SQL) starting 3 nodes: 20 | 21 | ```bash 22 | docker-compose -f docker-compose-yugabytedb.yml up -d --scale dist=0 23 | docker-compose -f docker-compose-yugabytedb.yml up -d --scale dist=1 24 | docker-compose -f docker-compose-yugabytedb.yml up -d --scale dist=2 25 | ``` 26 | 27 | You need to install packages with 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | ## Running 34 | 35 | Just run 36 | 37 | ```bash 38 | npm run start 39 | ``` 40 | 41 | ## Running inside Docker 42 | 43 | To build application: 44 | 45 | ```bash 46 | docker-compose --profile app build 47 | ``` 48 | 49 | To run application: 50 | 51 | ```bash 52 | docker-compose --profile app up 53 | ``` 54 | 55 | ### Testing 56 | 57 | You can either run tests with 58 | 59 | ``` 60 | npm run test 61 | ``` 62 | 63 | Or manually with prepared [.http](.http) file 64 | -------------------------------------------------------------------------------- /samples/simple-ts/cockroachdb/haproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM haproxy:lts-bullseye 2 | 3 | COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg 4 | 5 | EXPOSE 26257 6 | EXPOSE 8080 7 | EXPOSE 8081 -------------------------------------------------------------------------------- /samples/simple-ts/cockroachdb/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 info 3 | maxconn 20000 4 | 5 | defaults 6 | log global 7 | timeout connect 10m 8 | timeout client 30m 9 | timeout server 30m 10 | option clitcpka 11 | option tcplog 12 | 13 | listen cockroach-jdbc 14 | bind :26000 15 | mode tcp 16 | balance leastconn 17 | option httpchk GET /health?ready=1 18 | server roach-0 roach-0:26257 check port 8080 19 | server roach-1 roach-1:26257 check port 8080 20 | server roach-2 roach-2:26257 check port 8080 21 | 22 | listen cockroach-ui 23 | bind :8080 24 | mode tcp 25 | balance leastconn 26 | option httpchk GET /health 27 | server roach-0 roach-0:8080 check port 8080 28 | server roach-1 roach-1:8080 check port 8080 29 | server roach-2 roach-2:8080 check port 8080 30 | 31 | listen stats 32 | bind :8081 33 | mode http 34 | stats enable 35 | stats hide-version 36 | stats realm Haproxy\ Statistics 37 | stats uri / 38 | -------------------------------------------------------------------------------- /samples/simple-ts/docker-compose-cockroachdb.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | # Took from: https://github.com/dbist/cockroach-docker 4 | # Connect to db console with: docker exec -it roach-0 ./cockroach sql --host=roach-0:26257 --insecure 5 | 6 | services: 7 | roach-0: 8 | container_name: roach-0 9 | hostname: roach-0 10 | image: cockroachdb/cockroach-unstable:v23.2.0-beta.1 11 | command: start --insecure --join=roach-0,roach-1,roach-2 --listen-addr=roach-0:26257 --advertise-addr=roach-0:26257 --max-sql-memory=.25 --cache=.25 12 | environment: 13 | - 'ALLOW_EMPTY_PASSWORD=yes' 14 | - 'COCKROACH_USER=postgres' 15 | - 'COCKROACH_PASSWORD=postgres' 16 | - 'COCKROACH_DATABASE=postgres' 17 | 18 | roach-1: 19 | container_name: roach-1 20 | hostname: roach-1 21 | image: cockroachdb/cockroach-unstable:v23.2.0-beta.1 22 | command: start --insecure --join=roach-0,roach-1,roach-2 --listen-addr=roach-1:26257 --advertise-addr=roach-1:26257 --max-sql-memory=.25 --cache=.25 23 | environment: 24 | - 'ALLOW_EMPTY_PASSWORD=yes' 25 | - 'COCKROACH_USER=postgres' 26 | - 'COCKROACH_PASSWORD=postgres' 27 | - 'COCKROACH_DATABASE=postgres' 28 | 29 | roach-2: 30 | container_name: roach-2 31 | hostname: roach-2 32 | image: cockroachdb/cockroach-unstable:v23.2.0-beta.1 33 | command: start --insecure --join=roach-0,roach-1,roach-2 --listen-addr=roach-2:26257 --advertise-addr=roach-2:26257 --max-sql-memory=.25 --cache=.25 34 | environment: 35 | - 'ALLOW_EMPTY_PASSWORD=yes' 36 | - 'COCKROACH_USER=postgres' 37 | - 'COCKROACH_PASSWORD=postgres' 38 | - 'COCKROACH_DATABASE=postgres' 39 | 40 | init: 41 | container_name: init 42 | image: cockroachdb/cockroach-unstable:v23.2.0-beta.1 43 | command: init --host=roach-0 --insecure 44 | environment: 45 | - 'ALLOW_EMPTY_PASSWORD=yes' 46 | - 'COCKROACH_USER=postgres' 47 | - 'COCKROACH_PASSWORD=postgres' 48 | - 'COCKROACH_DATABASE=postgres' 49 | depends_on: 50 | - roach-0 51 | 52 | lb: 53 | container_name: lb 54 | hostname: lb 55 | build: cockroachdb/haproxy 56 | ports: 57 | - '5432:26000' 58 | - '8080:8080' 59 | - '8081:8081' 60 | depends_on: 61 | - roach-0 62 | - roach-1 63 | - roach-2 64 | 65 | client: 66 | container_name: client 67 | hostname: client 68 | image: cockroachdb/cockroach-unstable:v23.2.0-beta.1 69 | entrypoint: ['/usr/bin/tail', '-f', '/dev/null'] 70 | -------------------------------------------------------------------------------- /samples/simple-ts/docker-compose-yugabytedb.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" # Specify Docker Compose version 2 | 3 | services: 4 | yugabytedb: 5 | image: yugabytedb/yugabyte:latest 6 | command: yugabyted start --background=false --ysql_port=5432 7 | ports: 8 | - "5432:5432" 9 | - "15433:15433" 10 | healthcheck: 11 | interval: 15s 12 | timeout: 3s 13 | test: postgres/bin/pg_isready -h yugabytedb -p 5432 14 | 15 | dist: 16 | image: yugabytedb/yugabyte:latest 17 | command: yugabyted start --join yugabytedb --background=false --ysql_port=5432 18 | deploy: 19 | replicas: 0 20 | restart_policy: 21 | condition: on-failure 22 | depends_on: 23 | yugabytedb: 24 | condition: service_healthy 25 | 26 | pgadmin: 27 | container_name: pgadmin_container 28 | image: dpage/pgadmin4 29 | environment: 30 | - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 31 | - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-postgres} 32 | - PGADMIN_CONFIG_SERVER_MODE=False 33 | - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False 34 | ports: 35 | - "${PGADMIN_PORT:-5050}:80" 36 | entrypoint: /bin/sh -c "chmod 600 /pgpass; /entrypoint.sh;" 37 | user: root 38 | volumes: 39 | - ./docker/pgAdmin/pgpass:/pgpass 40 | - ./docker/pgAdmin/servers.json:/pgadmin4/servers.json 41 | depends_on: 42 | yugabytedb: 43 | condition: service_healthy 44 | restart: unless-stopped 45 | -------------------------------------------------------------------------------- /samples/simple-ts/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" # Specify Docker Compose version 2 | 3 | services: 4 | postgres: 5 | image: postgres:15.1-alpine 6 | ports: 7 | - "5432:5432" 8 | environment: 9 | - POSTGRES_DB=postgres 10 | - POSTGRES_PASSWORD=postgres 11 | - POSTGRES_USER=postgres 12 | 13 | pgadmin: 14 | container_name: pgadmin_container 15 | image: dpage/pgadmin4 16 | environment: 17 | - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.org} 18 | - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-postgres} 19 | - PGADMIN_CONFIG_SERVER_MODE=False 20 | - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False 21 | ports: 22 | - "${PGADMIN_PORT:-5050}:80" 23 | entrypoint: /bin/sh -c "chmod 600 /pgpass; /entrypoint.sh;" 24 | user: root 25 | volumes: 26 | - ./docker/pgAdmin/pgpass:/pgpass 27 | - ./docker/pgAdmin/servers.json:/pgadmin4/servers.json 28 | depends_on: 29 | - postgres 30 | restart: unless-stopped 31 | -------------------------------------------------------------------------------- /samples/simple-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pongo-sample-simple", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Simple sample using Pongo ", 6 | "scripts": { 7 | "setup": "cat .nvmrc | nvm install; nvm use", 8 | "build": "tsup", 9 | "build:ts": "tsc", 10 | "build:ts:watch": "tsc -b --watch", 11 | "start": "tsx ./src/index.ts", 12 | "start:shim": "tsx ./src/shim.ts", 13 | "start:typed": "tsx ./src/typedClient.ts", 14 | "lint": "npm run lint:eslint && npm run lint:prettier", 15 | "lint:prettier": "prettier --check \"**/**/!(*.d).{ts,json,md}\"", 16 | "lint:eslint": "eslint '**/*.ts'", 17 | "fix": "run-s fix:eslint fix:prettier", 18 | "fix:prettier": "prettier --write \"**/**/!(*.d).{ts,json,md}\"", 19 | "fix:eslint": "eslint '**/*.ts' --fix", 20 | "test": "run-s test:unit test:int test:e2e", 21 | "test:unit": "glob -d -c \"node --import tsx --test\" **/*.unit.spec.ts", 22 | "test:int": "glob -d -c \"node --import tsx --test\" **/*.int.spec.ts", 23 | "test:e2e": "glob -d -c \"node --import tsx --test\" **/*.e2e.spec.ts", 24 | "test:watch": "run-p test:unit:watch test:int:watch test:e2e:watch", 25 | "test:unit:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.unit.spec.ts", 26 | "test:int:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.int.spec.ts", 27 | "test:e2e:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.e2e.spec.ts", 28 | "migrate": "pongo migrate run --config ./dist/pongo.config.js --connectionString postgresql://postgres:postgres@localhost:5432/postgres", 29 | "shell": "pongo shell" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/event-driven-io/Pongo.git" 34 | }, 35 | "keywords": [ 36 | "Event Sourcing" 37 | ], 38 | "author": "Oskar Dudycz", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/event-driven-io/Pongo/issues" 42 | }, 43 | "homepage": "https://github.com/event-driven-io/Pongo#readme", 44 | "devDependencies": { 45 | "@types/node": "^20.11.30", 46 | "@typescript-eslint/eslint-plugin": "6.21.0", 47 | "@typescript-eslint/parser": "6.21.0", 48 | "eslint": "8.56.0", 49 | "eslint-config-prettier": "9.1.0", 50 | "eslint-plugin-prettier": "5.1.3", 51 | "glob": "10.3.10", 52 | "npm-run-all2": "6.1.2", 53 | "prettier": "3.2.5", 54 | "tsconfig-paths": "4.2.0", 55 | "tsup": "8.0.2", 56 | "tsx": "4.7.1", 57 | "typescript": "5.3.3" 58 | }, 59 | "main": "./dist/index.js", 60 | "module": "./dist/index.mjs", 61 | "types": "./dist/index.d.ts", 62 | "files": [ 63 | "dist" 64 | ], 65 | "dependencies": { 66 | "@event-driven-io/pongo": "0.17.0-alpha.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/simple-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import { prettyJson } from '@event-driven-io/dumbo'; 2 | import { ObjectId, pongoClient } from '@event-driven-io/pongo'; 3 | 4 | type User = { _id?: string; name: string; age: number }; 5 | 6 | const connectionString = 7 | 'postgresql://postgres:postgres@localhost:5432/postgres'; 8 | // 'postgresql://root@localhost:26000/defaultdb?sslmode=disable'; // cockroachdb 9 | 10 | const pongo = pongoClient(connectionString); 11 | const pongoDb = pongo.db(); 12 | 13 | const users = pongoDb.collection('users'); 14 | const roger = { name: 'Roger', age: 30 }; 15 | const anita = { name: 'Anita', age: 25 }; 16 | const cruella = { _id: ObjectId(), name: 'Cruella', age: 40 }; 17 | 18 | // Inserting 19 | await users.insertOne(roger); 20 | await users.insertOne(cruella); 21 | 22 | const { insertedId } = await users.insertOne(anita); 23 | const anitaId = insertedId!; 24 | 25 | // Updating 26 | await users.updateOne({ _id: anitaId }, { $set: { age: 31 } }); 27 | 28 | // Deleting 29 | await users.deleteOne({ _id: cruella._id }); 30 | 31 | // Finding by Id 32 | const anitaFromDb = await users.findOne({ _id: anitaId }); 33 | console.log(prettyJson(anitaFromDb)); 34 | 35 | // Finding more 36 | const usersFromDB = await users.find({ age: { $lt: 40 } }); 37 | console.log(prettyJson(usersFromDB)); 38 | 39 | await pongo.close(); 40 | -------------------------------------------------------------------------------- /samples/simple-ts/src/pongo.config.ts: -------------------------------------------------------------------------------- 1 | import { pongoSchema } from '@event-driven-io/pongo'; 2 | 3 | export type User = { _id?: string; name: string; age: number }; 4 | 5 | export default { 6 | schema: pongoSchema.client({ 7 | database: pongoSchema.db({ 8 | users: pongoSchema.collection('users'), 9 | }), 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /samples/simple-ts/src/shim.ts: -------------------------------------------------------------------------------- 1 | import { prettyJson } from '@event-driven-io/dumbo'; 2 | import { MongoClient } from '@event-driven-io/pongo/shim'; 3 | 4 | type User = { name: string; age: number }; 5 | 6 | const connectionString = 7 | 'postgresql://postgres:postgres@localhost:5432/postgres'; 8 | // 'postgresql://root@localhost:26000/defaultdb?sslmode=disable'; // cockroachdb 9 | 10 | const pongoClient = new MongoClient(connectionString); 11 | const pongoDb = pongoClient.db('postgres'); 12 | 13 | const users = pongoDb.collection('users'); 14 | const roger = { name: 'Roger', age: 30 }; 15 | const anita = { name: 'Anita', age: 25 }; 16 | //const cruella = { _id: ObjectId(), name: 'Cruella', age: 40 }; 17 | 18 | // Inserting 19 | await users.insertOne(roger); 20 | //await users.insertOne(cruella); 21 | 22 | const { insertedId } = await users.insertOne(anita); 23 | const anitaId = insertedId; 24 | 25 | // Updating 26 | await users.updateOne({ _id: anitaId }, { $set: { age: 31 } }); 27 | 28 | // Deleting 29 | //await users.deleteOne({ _id: cruella._id }); 30 | 31 | // Finding by Id 32 | const anitaFromDb = await users.findOne({ _id: anitaId }); 33 | console.log(prettyJson(anitaFromDb)); 34 | 35 | // Finding more 36 | const usersFromDB = await users.find({ age: { $lt: 40 } }).toArray(); 37 | console.log(prettyJson(usersFromDB)); 38 | 39 | await pongoClient.close(); 40 | -------------------------------------------------------------------------------- /samples/simple-ts/src/typedClient.ts: -------------------------------------------------------------------------------- 1 | import { prettyJson } from '@event-driven-io/dumbo'; 2 | import { ObjectId, pongoClient } from '@event-driven-io/pongo'; 3 | import config from './pongo.config'; 4 | 5 | const connectionString = 6 | 'postgresql://postgres:postgres@localhost:5432/postgres'; 7 | // 'postgresql://root@localhost:26000/defaultdb?sslmode=disable'; // cockroachdb 8 | 9 | const pongo = pongoClient(connectionString, { 10 | schema: { definition: config.schema, autoMigration: 'None' }, 11 | }); 12 | const pongoDb = pongo.database; 13 | 14 | const users = pongoDb.users; 15 | const roger = { name: 'Roger', age: 30 }; 16 | const anita = { name: 'Anita', age: 25 }; 17 | const cruella = { _id: ObjectId(), name: 'Cruella', age: 40 }; 18 | 19 | // Inserting 20 | await users.insertOne(roger); 21 | await users.insertOne(cruella); 22 | 23 | const { insertedId } = await users.insertOne(anita); 24 | const anitaId = insertedId!; 25 | 26 | // Updating 27 | await users.updateOne({ _id: anitaId }, { $set: { age: 31 } }); 28 | 29 | // Deleting 30 | await users.deleteOne({ _id: cruella._id }); 31 | 32 | // Finding by Id 33 | const anitaFromDb = await users.findOne({ _id: anitaId }); 34 | console.log(prettyJson(anitaFromDb)); 35 | 36 | // Finding more 37 | const usersFromDB = await users.find({ age: { $lt: 40 } }); 38 | console.log(prettyJson(usersFromDB)); 39 | 40 | await pongo.close(); 41 | -------------------------------------------------------------------------------- /samples/simple-ts/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | splitting: true, 5 | clean: true, // clean up the dist folder 6 | dts: true, // generate dts files 7 | format: ['cjs', 'esm'], // generate cjs and esm files 8 | minify: true, //env === 'production', 9 | bundle: true, //env === 'production', 10 | skipNodeModulesBundle: true, 11 | target: 'esnext', 12 | outDir: 'dist', //env === 'production' ? 'dist' : 'lib', 13 | entry: [ 14 | 'src/index.ts', 15 | 'src/pongo.config.ts', 16 | 'src/typedClient.ts', 17 | 'src/shim.ts', 18 | ], 19 | sourcemap: true, 20 | tsconfig: 'tsconfig.json', // workaround for https://github.com/egoist/tsup/issues/571#issuecomment-1760052931 21 | }); 22 | -------------------------------------------------------------------------------- /setup.ps1: -------------------------------------------------------------------------------- 1 | Set-Location -Path src 2 | npm install 3 | npm run build 4 | npm run test 5 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd src 3 | npm install 4 | npm run build 5 | npm run test -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | -------------------------------------------------------------------------------- /src/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /src/.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/ 2 | **/lib/ 3 | **/cache/ -------------------------------------------------------------------------------- /src/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug All Tests (Node)", 6 | "type": "node", 7 | "request": "launch", 8 | "skipFiles": ["/**"], 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run-script", "test", "--inspect-brk=9229"], // Use --inspect-brk for debugging 11 | "console": "integratedTerminal", 12 | "internalConsoleOptions": "neverOpen", 13 | "cwd": "${workspaceFolder}", 14 | "sourceMaps": true 15 | }, 16 | { 17 | "name": "Debug Current Test File", 18 | "type": "node", 19 | "request": "launch", 20 | "skipFiles": ["/**"], 21 | "runtimeExecutable": "npm", 22 | "runtimeArgs": [ 23 | "run-script", 24 | "test:file", 25 | "--", 26 | "${file}", 27 | "--inspect-brk=9229" 28 | ], // Use --inspect-brk for debugging 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen", 31 | "cwd": "${workspaceFolder}/", 32 | "sourceMaps": true 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnPaste": false, 4 | "editor.formatOnSave": true, 5 | 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": "explicit", 8 | "source.fixAll.eslint": "explicit", 9 | "source.addMissingImports": "always" 10 | }, 11 | 12 | "editor.tabSize": 2, 13 | 14 | "files.exclude": { 15 | "node_modules/": true, 16 | "**/node_modules/": true, 17 | "**/dist/": true, 18 | "**/*.tsbuildinfo": true 19 | }, 20 | "files.eol": "\n", 21 | 22 | "typescript.preferences.importModuleSpecifier": "relative", 23 | "eslint.workingDirectories": [{ "mode": "auto" }], 24 | "debug.javascript.terminalOptions": { 25 | "nodeVersionHint": 20 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build:ts:watch", 7 | "group": "build", 8 | "problemMatcher": [], 9 | "label": "npm: build:ts:watch", 10 | "detail": "tsc --watch" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | const env = process.env.NODE_ENV; 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | base: env === 'production' ? '/Pongo/' : '/', 8 | lang: 'en-GB', 9 | title: 'Pongo', 10 | description: 'Event Sourcing made simple', 11 | themeConfig: { 12 | logo: '/logo.png', 13 | // https://vitepress.dev/reference/default-theme-config 14 | nav: [ 15 | { text: 'Getting Started', link: '/getting-started' }, 16 | { text: '🧑‍💻 Join Discord Server', link: 'https://discord.gg/VzvxR5Rb38' }, 17 | { 18 | text: 'Release Notes', 19 | link: 'https://github.com/event-driven-io/Pongo/releases', 20 | }, 21 | { 22 | text: 'Support', 23 | link: 'https://github.com/sponsors/event-driven-io', 24 | }, 25 | ], 26 | 27 | sidebar: [ 28 | { 29 | text: 'Documentation', 30 | items: [{ text: 'Getting Started', link: '/getting-started' }], 31 | }, 32 | ], 33 | 34 | search: { 35 | provider: 'local', 36 | }, 37 | 38 | editLink: { 39 | pattern: 40 | 'https://github.com/event-driven-io/Pongo/edit/master/docs/:path', 41 | text: 'Suggest changes to this page', 42 | }, 43 | 44 | socialLinks: [ 45 | { icon: 'github', link: 'https://github.com/event-driven-io/pongo' }, 46 | { icon: 'discord', link: 'https://discord.gg/VzvxR5Rb38' }, 47 | ], 48 | footer: { 49 | copyright: 'Copyright © Oskar Dudycz and contributors.', 50 | }, 51 | }, 52 | head: [ 53 | // ['link', { rel: 'apple-touch-icon', type: 'image/png', size: "180x180", href: '/apple-touch-icon.png' }], 54 | // ['link', { rel: 'icon', type: 'image/png', size: "32x32", href: '/favicon-32x32.png' }], 55 | // ['link', { rel: 'icon', type: 'image/png', size: "16x16", href: '/favicon-16x16.png' }], 56 | // ['link', { rel: 'manifest', manifest: '/manifest.json' }], 57 | ['meta', { property: 'og:title', content: 'Pongo' }], 58 | ['meta', { property: 'og:type', content: 'website' }], 59 | [ 60 | 'meta', 61 | { 62 | property: 'og:description', 63 | content: 'Event Sourcing development made simple', 64 | }, 65 | ], 66 | [ 67 | 'meta', 68 | { 69 | property: 'og:image', 70 | content: 'https://event-driven-io.github.io/Pongo/social.png', 71 | }, 72 | ], 73 | [ 74 | 'meta', 75 | { 76 | property: 'og:url', 77 | content: 'https://event-driven-io.github.io/pongo', 78 | }, 79 | ], 80 | ['meta', { property: 'twitter:card', content: 'summary_large_image' }], 81 | ['meta', { property: 'twitter:site', content: 'marten_lib' }], 82 | ['meta', { property: 'twitter:creator', content: 'marten_lib' }], 83 | [ 84 | 'meta', 85 | { 86 | property: 'twitter:image', 87 | content: 'https://event-driven-io.github.io/Pongo/social.png', 88 | }, 89 | ], 90 | ], 91 | }); 92 | -------------------------------------------------------------------------------- /src/docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | --vp-c-brand: #2b0f54; 2 | --vp-c-brand-light: #ab1f65; 3 | --vp-c-brand-dark: #2b0f54; 4 | --vp-home-hero-name-color: var(--vp-c-brand); 5 | --vp-button-brand-bg: var(--vp-c-brand); 6 | --vp-button-brand-border: #c9b8a9; 7 | --vp-button-brand-hover-border: var(--vp-button-brand-border); 8 | --vp-button-brand-hover-bg: var(--vp-c-brand-light); 9 | -------------------------------------------------------------------------------- /src/docs/.vitepress/theme/index.mts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import './custom.css'; 3 | 4 | export default { 5 | ...DefaultTheme, 6 | }; 7 | -------------------------------------------------------------------------------- /src/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: 'Pongo' 7 | text: 'Like Mongo
But on Postgres
And With
Strong Consistency' 8 | tagline: 'Flexibility or Consistency? Why not both!' 9 | image: 10 | src: /logo.png 11 | alt: Pongo logo 12 | actions: 13 | - theme: brand 14 | text: Getting Started 15 | link: /getting-started 16 | - theme: alt 17 | text: 🧑‍💻 Join Discord Server 18 | link: https://discord.gg/VzvxR5Rb38 19 | 20 | features: 21 | - title: Keep your data consistent 22 | details: Don't be afraid of getting inconsistent state 23 | - title: DevExperience as prime goal 24 | details: Reduce the boilerplate, and focus on delivery with accessible tooling 25 | - title: Known experience, new capabilities 26 | details: Keep your muscle memory, but get new tricks and TypeScript superpowers 27 | --- 28 | -------------------------------------------------------------------------------- /src/docs/public/logo-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/docs/public/logo-square.png -------------------------------------------------------------------------------- /src/docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/docs/public/logo.png -------------------------------------------------------------------------------- /src/docs/public/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/docs/public/social.png -------------------------------------------------------------------------------- /src/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import js from '@eslint/js'; 3 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import eslintConfigPrettier from 'eslint-config-prettier'; 6 | import globals from 'globals'; 7 | import path from 'node:path'; 8 | import { fileURLToPath } from 'node:url'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all, 16 | }); 17 | 18 | export default [ 19 | { 20 | ignores: [ 21 | '**/dist/', 22 | '**/lib/', 23 | '**/cache/', 24 | 'node_modules/*', 25 | '**/node_modules', 26 | 'dist/*coverage/*', 27 | 'dist/*', 28 | 'lib/*', 29 | '**/dist/*', 30 | '**/dist', 31 | '**/*.d.ts', 32 | 'src/types/', 33 | 'eslint.config.mjs', 34 | 'e2e/*', 35 | ], 36 | }, 37 | ...compat.extends( 38 | 'eslint:recommended', 39 | 'plugin:@typescript-eslint/recommended', 40 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 41 | 'plugin:prettier/recommended', 42 | ), 43 | { 44 | plugins: { 45 | '@typescript-eslint': typescriptEslint, 46 | }, 47 | 48 | languageOptions: { 49 | globals: { 50 | ...globals.node, 51 | }, 52 | 53 | parser: tsParser, 54 | ecmaVersion: 2023, 55 | sourceType: 'module', 56 | 57 | parserOptions: { 58 | project: './tsconfig.eslint.json', 59 | }, 60 | }, 61 | 62 | settings: { 63 | 'import/resolver': { 64 | typescript: {}, 65 | }, 66 | }, 67 | 68 | rules: { 69 | 'no-unused-vars': 'off', 70 | 71 | '@typescript-eslint/no-unused-vars': [ 72 | 'error', 73 | { 74 | varsIgnorePattern: '^_', 75 | argsIgnorePattern: '^_', 76 | }, 77 | ], 78 | 79 | '@typescript-eslint/no-misused-promises': ['off'], 80 | '@typescript-eslint/prefer-namespace-keyword': 'off', 81 | }, 82 | }, 83 | eslintConfigPrettier, 84 | ]; 85 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-driven-io/pongo-core", 3 | "version": "0.17.0-alpha.3", 4 | "description": "Pongo - Mongo with strong consistency on top of Postgres", 5 | "type": "module", 6 | "engines": { 7 | "node": ">=20.11.1" 8 | }, 9 | "private": true, 10 | "scripts": { 11 | "setup": "cat .nvmrc | nvm install; nvm use", 12 | "build": "run-s build:ts build:dumbo build:pongo", 13 | "build:ts": "tsc -b", 14 | "build:ts:clean": "tsc --build --clean", 15 | "build:ts:watch": "tsc -b --watch", 16 | "build:dumbo": "npm run build -w packages/dumbo", 17 | "build:pongo": "npm run build -w packages/pongo", 18 | "lint": "npm run lint:eslint && npm run lint:prettier", 19 | "lint:prettier": "prettier --check \"**/**/!(*.d).{ts,json,md}\"", 20 | "lint:eslint": "eslint '**/*.ts'", 21 | "fix": "run-s fix:eslint fix:prettier", 22 | "fix:prettier": "prettier --write \"**/**/!(*.d).{ts,json,md}\"", 23 | "fix:eslint": "eslint '**/*.ts' --fix", 24 | "test": "run-s test:unit test:int test:e2e", 25 | "test:unit": "glob -d -c \"node --import tsx --test\" **/*.unit.spec.ts", 26 | "test:int": "glob -d -c \"node --import tsx --test\" **/*.int.spec.ts", 27 | "test:e2e": "glob -d -c \"node --import tsx --test\" **/*.e2e.spec.ts", 28 | "test:watch": "run-p test:unit:watch test:int:watch test:e2e:watch", 29 | "test:unit:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.unit.spec.ts", 30 | "test:int:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.int.spec.ts", 31 | "test:e2e:watch": "glob -d -c \"node --import tsx --test --watch\" **/*.e2e.spec.ts", 32 | "test:file": "node --import tsx --test", 33 | "docs:dev": "vitepress dev docs", 34 | "docs:build": "vitepress build docs", 35 | "docs:preview": "vitepress preview docs", 36 | "copy:readme": "cpy '../README.md' 'packages/pongo/src'" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/event-driven-io/Pongo.git" 41 | }, 42 | "keywords": [ 43 | "Event Sourcing" 44 | ], 45 | "author": "Oskar Dudycz", 46 | "bugs": { 47 | "url": "https://github.com/event-driven-io/Pongo/issues" 48 | }, 49 | "homepage": "https://event-driven-io.github.io/Pongo/", 50 | "exports": { 51 | ".": { 52 | "import": { 53 | "types": "./dist/index.d.ts", 54 | "default": "./dist/index.js" 55 | }, 56 | "require": { 57 | "types": "./dist/index.d.cts", 58 | "default": "./dist/index.cjs" 59 | } 60 | } 61 | }, 62 | "main": "./dist/index.cjs", 63 | "module": "./dist/index.js", 64 | "types": "./dist/index.d.ts", 65 | "files": [ 66 | "dist" 67 | ], 68 | "devDependencies": { 69 | "@faker-js/faker": "^9.6.0", 70 | "@types/benchmark": "^2.1.5", 71 | "@testcontainers/mongodb": "^10.18.0", 72 | "@testcontainers/postgresql": "^10.18.0", 73 | "@types/mongodb": "^4.0.7", 74 | "@types/node": "^22.13.10", 75 | "@types/pg": "^8.11.11", 76 | "@types/sqlite3": "^5.1.0", 77 | "@types/uuid": "^10.0.0", 78 | "@typescript-eslint/eslint-plugin": "8.26.0", 79 | "@typescript-eslint/parser": "8.26.0", 80 | "0x": "^5.8.0", 81 | "benchmark": "^2.1.4", 82 | "cpy-cli": "^5.0.0", 83 | "dotenv": "^16.4.7", 84 | "eslint": "^9.22.0", 85 | "eslint-config-prettier": "^9.1.0", 86 | "eslint-plugin-prettier": "^5.2.1", 87 | "glob": "^11.0.1", 88 | "npm-run-all2": "^7.0.2", 89 | "prettier": "^3.5.3", 90 | "testcontainers": "^10.18.0", 91 | "ts-node": "^10.9.2", 92 | "tsconfig-paths": "^4.2.0", 93 | "tsup": "^8.4.0", 94 | "tsx": "^4.19.3", 95 | "typescript": "^5.8.2", 96 | "uuid": "^11.1.0", 97 | "vitepress": "^1.6.3" 98 | }, 99 | "peerDependencies": { 100 | "cli-table3": "^0.6.5", 101 | "commander": "^13.1.0", 102 | "pg": "^8.13.3", 103 | "pg-connection-string": "^2.7.0", 104 | "ansis": "^3.17.0", 105 | "sqlite3": "^5.1.7" 106 | }, 107 | "dependencies": {}, 108 | "workspaces": [ 109 | "packages/dumbo", 110 | "packages/pongo" 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /src/packages/dumbo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-driven-io/dumbo", 3 | "version": "0.13.0-alpha.3", 4 | "description": "Dumbo - tools for dealing with PostgreSQL", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup", 8 | "build:ts": "tsc -b", 9 | "build:ts:watch": "tsc -b --watch", 10 | "test": "run-s test:unit test:int test:e2e", 11 | "test:unit": "glob -c \"node --import tsx --test\" **/*.unit.spec.ts", 12 | "test:int": "glob -c \"node --import tsx --test\" **/*.int.spec.ts", 13 | "test:e2e": "glob -c \"node --import tsx --test\" **/*.e2e.spec.ts", 14 | "test:watch": "node --import tsx --test --watch", 15 | "test:unit:watch": "glob -c \"node --import tsx --test --watch\" **/*.unit.spec.ts", 16 | "test:int:watch": "glob -c \"node --import tsx --test --watch\" **/*.int.spec.ts", 17 | "test:e2e:watch": "glob -c \"node --import tsx --test --watch\" **/*.e2e.spec.ts", 18 | "benchmark": "node --import tsx src/benchmarks/index.ts", 19 | "flamegraph": "0x -D \"0x/{pid}\" -- node --import tsx src/benchmarks/ox.ts" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/event-driven-io/Pongo.git" 24 | }, 25 | "keywords": [ 26 | "Event Sourcing" 27 | ], 28 | "author": "Oskar Dudycz", 29 | "bugs": { 30 | "url": "https://github.com/event-driven-io/Pongo/issues" 31 | }, 32 | "homepage": "https://event-driven-io.github.io/Pongo/", 33 | "exports": { 34 | ".": { 35 | "import": { 36 | "types": "./dist/index.d.ts", 37 | "default": "./dist/index.js" 38 | }, 39 | "require": { 40 | "types": "./dist/index.d.cts", 41 | "default": "./dist/index.cjs" 42 | } 43 | }, 44 | "./pg": { 45 | "import": { 46 | "types": "./dist/pg.d.ts", 47 | "default": "./dist/pg.js" 48 | }, 49 | "require": { 50 | "types": "./dist/pg.d.cts", 51 | "default": "./dist/pg.cjs" 52 | } 53 | }, 54 | "./sqlite3": { 55 | "import": { 56 | "types": "./dist/sqlite3.d.ts", 57 | "default": "./dist/sqlite3.js" 58 | }, 59 | "require": { 60 | "types": "./dist/sqlite3.d.cts", 61 | "default": "./dist/sqlite3.cjs" 62 | } 63 | } 64 | }, 65 | "typesVersions": { 66 | "*": { 67 | ".": [ 68 | "./dist/index.d.ts" 69 | ], 70 | "pg": [ 71 | "./dist/pg.d.ts" 72 | ], 73 | "sqlite3": [ 74 | "./dist/sqlite3.d.ts" 75 | ] 76 | } 77 | }, 78 | "main": "./dist/index.cjs", 79 | "module": "./dist/index.js", 80 | "types": "./dist/index.d.ts", 81 | "files": [ 82 | "dist" 83 | ], 84 | "peerDependencies": { 85 | "@types/pg": "^8.11.11", 86 | "@types/uuid": "^10.0.0", 87 | "@types/sqlite3": "^5.1.0", 88 | "pg": "^8.13.3", 89 | "pg-connection-string": "^2.7.0", 90 | "uuid": "^11.1.0", 91 | "sqlite3": "^5.1.7" 92 | }, 93 | "devDependencies": { 94 | "@types/node": "^22.13.10" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import Benchmark from 'benchmark'; 4 | import pg from 'pg'; 5 | import { rawSql, single } from '..'; 6 | import { defaultPostgreSQLConenctionString, dumbo } from '../pg'; 7 | 8 | const connectionString = 9 | process.env.BENCHMARK_POSTGRESQL_CONNECTION_STRING ?? 10 | defaultPostgreSQLConenctionString; 11 | 12 | const pooled = process.env.BENCHMARK_CONNECTION_POOLED === 'true'; 13 | 14 | const pool = dumbo({ 15 | connectionString, 16 | pooled, 17 | }); 18 | 19 | const rawPgPool = new pg.Pool({ connectionString }); 20 | 21 | const setup = () => 22 | pool.execute.command( 23 | rawSql(` 24 | CREATE TABLE IF NOT EXISTS cars ( 25 | id SERIAL PRIMARY KEY, 26 | brand VARCHAR(255) 27 | );`), 28 | ); 29 | 30 | const openAndCloseRawConnection = async () => { 31 | if (pooled) { 32 | const client = await rawPgPool.connect(); 33 | client.release(); 34 | } else { 35 | const client = new pg.Client(connectionString); 36 | await client.connect(); 37 | await client.end(); 38 | } 39 | }; 40 | 41 | const openAndCloseDumboConnection = async () => { 42 | const connection = await pool.connection(); 43 | try { 44 | await connection.open(); 45 | } finally { 46 | await connection.close(); 47 | } 48 | }; 49 | 50 | const getRecord = () => 51 | single(pool.execute.query(rawSql(`SELECT * FROM cars LIMIT 1;`))); 52 | 53 | // Function to update a record by ID 54 | const insertRecord = () => 55 | pool.withTransaction((transaction) => 56 | transaction.execute.command( 57 | rawSql(`INSERT INTO cars (brand) VALUES ('bmw')`), 58 | ), 59 | ); 60 | 61 | // Setup Benchmark.js 62 | async function runBenchmark() { 63 | await setup(); 64 | 65 | const suite = new Benchmark.Suite(); 66 | 67 | suite 68 | .add('Opening and closing raw connection', { 69 | defer: true, 70 | fn: async function (deferred: Benchmark.Deferred) { 71 | await openAndCloseRawConnection(); 72 | deferred.resolve(); 73 | }, 74 | }) 75 | .add('Opening and closing connection', { 76 | defer: true, 77 | fn: async function (deferred: Benchmark.Deferred) { 78 | await openAndCloseDumboConnection(); 79 | deferred.resolve(); 80 | }, 81 | }) 82 | .add('INSERTING records in transaction', { 83 | defer: true, 84 | fn: async function (deferred: Benchmark.Deferred) { 85 | await insertRecord(); 86 | deferred.resolve(); 87 | }, 88 | }) 89 | .add('READING records', { 90 | defer: true, 91 | fn: async function (deferred: Benchmark.Deferred) { 92 | await getRecord(); 93 | deferred.resolve(); 94 | }, 95 | }) 96 | .on('cycle', function (event: Benchmark.Event) { 97 | console.log(String(event.target as unknown)); 98 | }) 99 | .on('complete', async function (this: Benchmark.Suite) { 100 | this.forEach((bench: Benchmark.Target) => { 101 | const stats = bench.stats; 102 | console.log(`\nBenchmark: ${bench.name}`); 103 | console.log(` Operations per second: ${bench.hz!.toFixed(2)} ops/sec`); 104 | console.log( 105 | ` Mean execution time: ${(stats!.mean * 1000).toFixed(2)} ms`, 106 | ); 107 | console.log( 108 | ` Standard deviation: ${(stats!.deviation * 1000).toFixed(2)} ms`, 109 | ); 110 | console.log(` Margin of error: ±${stats!.rme.toFixed(2)}%`); 111 | console.log(` Sample size: ${stats!.sample.length} runs`); 112 | console.log(); 113 | }); 114 | 115 | console.log('Benchmarking complete.'); 116 | await rawPgPool.end(); 117 | return pool.close(); // Close the database connection 118 | }) 119 | // Run the benchmarks 120 | .run({ async: true }); 121 | } 122 | 123 | runBenchmark().catch(console.error); 124 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/benchmarks/ox.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/packages/dumbo/src/benchmarks/ox.ts -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/connections/connection.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectorType } from '../connectors'; 2 | import { 3 | sqlExecutor, 4 | type DbSQLExecutor, 5 | type WithSQLExecutor, 6 | } from '../execute'; 7 | import { 8 | transactionFactoryWithDbClient, 9 | type DatabaseTransaction, 10 | type DatabaseTransactionFactory, 11 | } from './transaction'; 12 | 13 | export interface Connection< 14 | Connector extends ConnectorType = ConnectorType, 15 | DbClient = unknown, 16 | > extends WithSQLExecutor, 17 | DatabaseTransactionFactory { 18 | connector: Connector; 19 | open: () => Promise; 20 | close: () => Promise; 21 | } 22 | 23 | export interface ConnectionFactory< 24 | ConnectionType extends Connection = Connection, 25 | > { 26 | connection: () => Promise; 27 | 28 | withConnection: ( 29 | handle: (connection: ConnectionType) => Promise, 30 | ) => Promise; 31 | } 32 | 33 | export type CreateConnectionOptions< 34 | Connector extends ConnectorType = ConnectorType, 35 | DbClient = unknown, 36 | ConnectionType extends Connection = Connection< 37 | Connector, 38 | DbClient 39 | >, 40 | Executor extends DbSQLExecutor = DbSQLExecutor, 41 | > = { 42 | connector: Connector; 43 | connect: Promise; 44 | close: (client: DbClient) => Promise; 45 | initTransaction: ( 46 | connection: () => ConnectionType, 47 | ) => (client: Promise) => DatabaseTransaction; 48 | executor: () => Executor; 49 | }; 50 | 51 | export const createConnection = < 52 | Connector extends ConnectorType = ConnectorType, 53 | DbClient = unknown, 54 | ConnectionType extends Connection = Connection< 55 | Connector, 56 | DbClient 57 | >, 58 | Executor extends DbSQLExecutor = DbSQLExecutor, 59 | >( 60 | options: CreateConnectionOptions< 61 | Connector, 62 | DbClient, 63 | ConnectionType, 64 | Executor 65 | >, 66 | ): ConnectionType => { 67 | const { connector, connect, close, initTransaction, executor } = options; 68 | 69 | let client: DbClient | null = null; 70 | 71 | const getClient = async () => client ?? (client = await connect); 72 | 73 | const connection: Connection = { 74 | connector, 75 | open: getClient, 76 | close: () => (client ? close(client) : Promise.resolve()), 77 | ...transactionFactoryWithDbClient( 78 | getClient, 79 | initTransaction(() => typedConnection), 80 | ), 81 | execute: sqlExecutor(executor(), { connect: getClient }), 82 | }; 83 | 84 | const typedConnection = connection as ConnectionType; 85 | 86 | return typedConnection; 87 | }; 88 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/connections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | export * from './pool'; 3 | export * from './transaction'; 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/connections/pool.ts: -------------------------------------------------------------------------------- 1 | import { 2 | executeInNewConnection, 3 | sqlExecutorInNewConnection, 4 | type WithSQLExecutor, 5 | } from '../execute'; 6 | import { type Connection, type ConnectionFactory } from './connection'; 7 | import { 8 | transactionFactoryWithNewConnection, 9 | type DatabaseTransactionFactory, 10 | } from './transaction'; 11 | 12 | export interface ConnectionPool 13 | extends WithSQLExecutor, 14 | ConnectionFactory, 15 | DatabaseTransactionFactory { 16 | connector: ConnectionType['connector']; 17 | close: () => Promise; 18 | } 19 | 20 | export type ConnectionPoolFactory< 21 | ConnectionPoolType extends ConnectionPool = ConnectionPool, 22 | ConnectionPoolOptions = unknown, 23 | > = (options: ConnectionPoolOptions) => ConnectionPoolType; 24 | 25 | export const createConnectionPool = < 26 | ConnectionType extends Connection, 27 | ConnectionPoolType extends ConnectionPool, 28 | >( 29 | pool: Pick, 'connector'> & 30 | Partial> & { 31 | getConnection: () => ConnectionType; 32 | }, 33 | ): ConnectionPoolType => { 34 | const { connector, getConnection } = pool; 35 | 36 | const connection = 37 | 'connection' in pool 38 | ? pool.connection 39 | : () => Promise.resolve(getConnection()); 40 | 41 | const withConnection = 42 | 'withConnection' in pool 43 | ? pool.withConnection 44 | : (handle: (connection: ConnectionType) => Promise) => 45 | executeInNewConnection(handle, { 46 | connection, 47 | }); 48 | 49 | const close = 'close' in pool ? pool.close : () => Promise.resolve(); 50 | 51 | const execute = 52 | 'execute' in pool 53 | ? pool.execute 54 | : sqlExecutorInNewConnection({ connection }); 55 | 56 | const transaction = 57 | 'transaction' in pool && 'withTransaction' in pool 58 | ? { 59 | transaction: pool.transaction, 60 | withTransaction: pool.withTransaction, 61 | } 62 | : transactionFactoryWithNewConnection(getConnection); 63 | 64 | const result: ConnectionPool = { 65 | connector, 66 | connection, 67 | withConnection, 68 | close, 69 | execute, 70 | ...transaction, 71 | }; 72 | 73 | return result as ConnectionPoolType; 74 | }; 75 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/connections/transaction.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectorType } from '../connectors'; 2 | import type { WithSQLExecutor } from '../execute'; 3 | import { type Connection } from './connection'; 4 | 5 | export interface DatabaseTransaction< 6 | Connector extends ConnectorType = ConnectorType, 7 | DbClient = unknown, 8 | > extends WithSQLExecutor { 9 | connector: Connector; 10 | connection: Connection; 11 | begin: () => Promise; 12 | commit: () => Promise; 13 | rollback: (error?: unknown) => Promise; 14 | } 15 | 16 | export interface DatabaseTransactionFactory< 17 | Connector extends ConnectorType = ConnectorType, 18 | DbClient = unknown, 19 | > { 20 | transaction: () => DatabaseTransaction; 21 | 22 | withTransaction: ( 23 | handle: ( 24 | transaction: DatabaseTransaction, 25 | ) => Promise | Result>, 26 | ) => Promise; 27 | } 28 | 29 | export type TransactionResult = { success: boolean; result: Result }; 30 | 31 | const toTransactionResult = ( 32 | transactionResult: TransactionResult | Result, 33 | ): TransactionResult => 34 | transactionResult !== undefined && 35 | transactionResult !== null && 36 | typeof transactionResult === 'object' && 37 | 'success' in transactionResult 38 | ? transactionResult 39 | : { success: true, result: transactionResult }; 40 | 41 | export const executeInTransaction = async < 42 | Connector extends ConnectorType = ConnectorType, 43 | DbClient = unknown, 44 | Result = void, 45 | >( 46 | transaction: DatabaseTransaction, 47 | handle: ( 48 | transaction: DatabaseTransaction, 49 | ) => Promise | Result>, 50 | ): Promise => { 51 | await transaction.begin(); 52 | 53 | try { 54 | const { success, result } = toTransactionResult(await handle(transaction)); 55 | 56 | if (success) await transaction.commit(); 57 | else await transaction.rollback(); 58 | 59 | return result; 60 | } catch (e) { 61 | await transaction.rollback(); 62 | throw e; 63 | } 64 | }; 65 | 66 | export const transactionFactoryWithDbClient = < 67 | Connector extends ConnectorType = ConnectorType, 68 | DbClient = unknown, 69 | >( 70 | connect: () => Promise, 71 | initTransaction: ( 72 | client: Promise, 73 | ) => DatabaseTransaction, 74 | ): DatabaseTransactionFactory => ({ 75 | transaction: () => initTransaction(connect()), 76 | withTransaction: (handle) => 77 | executeInTransaction(initTransaction(connect()), handle), 78 | }); 79 | 80 | const wrapInConnectionClosure = async < 81 | ConnectionType extends Connection = Connection, 82 | Result = unknown, 83 | >( 84 | connection: ConnectionType, 85 | handle: () => Promise, 86 | ) => { 87 | try { 88 | return await handle(); 89 | } finally { 90 | await connection.close(); 91 | } 92 | }; 93 | 94 | export const transactionFactoryWithNewConnection = < 95 | ConnectionType extends Connection = Connection, 96 | >( 97 | connect: () => ConnectionType, 98 | ): DatabaseTransactionFactory => ({ 99 | transaction: () => { 100 | const connection = connect(); 101 | const transaction = connection.transaction(); 102 | 103 | return { 104 | ...transaction, 105 | commit: () => 106 | wrapInConnectionClosure(connection, () => transaction.commit()), 107 | rollback: () => 108 | wrapInConnectionClosure(connection, () => transaction.rollback()), 109 | }; 110 | }, 111 | withTransaction: (handle) => { 112 | const connection = connect(); 113 | return wrapInConnectionClosure(connection, () => 114 | connection.withTransaction(handle), 115 | ); 116 | }, 117 | }); 118 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/connectors/index.ts: -------------------------------------------------------------------------------- 1 | export type DatabaseType = string; 2 | export type DatabaseDriverName = string; 3 | 4 | export type ConnectorType< 5 | DatabaseTypeName extends DatabaseType = DatabaseType, 6 | DriverName extends DatabaseDriverName = DatabaseDriverName, 7 | > = `${DatabaseTypeName}:${DriverName}`; 8 | 9 | export type ConnectorTypeParts = { 10 | databaseType: T; 11 | driverName: string; 12 | }; 13 | 14 | /** 15 | * Accepts a `databaseType` (e.g. PostgreSQL, SQLite) and a `driverName` 16 | * (the library name, e.g. pg, sqlite3) and combines them to a singular 17 | * `connectorType` which can be used in database handling. 18 | */ 19 | export function toConnectorType( 20 | databaseType: T, 21 | driverName: string, 22 | ): ConnectorType { 23 | return `${databaseType}:${driverName}`; 24 | } 25 | 26 | /** 27 | * Accepts a fully formatted `connectorType` and returns the broken down 28 | * `databaseType` and `driverName`. 29 | */ 30 | export function fromConnectorType( 31 | connectorType: ConnectorType, 32 | ): ConnectorTypeParts { 33 | const parts = connectorType.split(':') as [T, string]; 34 | return { 35 | databaseType: parts[0], 36 | driverName: parts[1], 37 | }; 38 | } 39 | 40 | /** 41 | * Accepts a fully formatted `connectorType` and returns the `driverName`. 42 | */ 43 | export function getDriverName( 44 | connectorType: ConnectorType, 45 | ): DatabaseDriverName { 46 | const { driverName } = fromConnectorType(connectorType); 47 | return driverName; 48 | } 49 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/execute/execute.ts: -------------------------------------------------------------------------------- 1 | import type { Connection } from '../connections'; 2 | import type { ConnectorType } from '../connectors'; 3 | import type { QueryResult, QueryResultRow } from '../query'; 4 | import { type SQL } from '../sql'; 5 | 6 | export type SQLQueryOptions = { timeoutMs?: number }; 7 | export type SQLCommandOptions = { timeoutMs?: number }; 8 | 9 | export interface DbSQLExecutor< 10 | Connector extends ConnectorType = ConnectorType, 11 | DbClient = unknown, 12 | > { 13 | connector: Connector; 14 | query( 15 | client: DbClient, 16 | sql: SQL, 17 | options?: SQLQueryOptions, 18 | ): Promise>; 19 | batchQuery( 20 | client: DbClient, 21 | sqls: SQL[], 22 | options?: SQLQueryOptions, 23 | ): Promise[]>; 24 | command( 25 | client: DbClient, 26 | sql: SQL, 27 | options?: SQLCommandOptions, 28 | ): Promise>; 29 | batchCommand( 30 | client: DbClient, 31 | sqls: SQL[], 32 | options?: SQLCommandOptions, 33 | ): Promise[]>; 34 | } 35 | 36 | export interface SQLExecutor { 37 | query( 38 | sql: SQL, 39 | options?: SQLQueryOptions, 40 | ): Promise>; 41 | batchQuery( 42 | sqls: SQL[], 43 | options?: SQLQueryOptions, 44 | ): Promise[]>; 45 | command( 46 | sql: SQL, 47 | options?: SQLCommandOptions, 48 | ): Promise>; 49 | batchCommand( 50 | sqls: SQL[], 51 | options?: SQLCommandOptions, 52 | ): Promise[]>; 53 | } 54 | 55 | export interface WithSQLExecutor { 56 | execute: SQLExecutor; 57 | } 58 | 59 | export const sqlExecutor = < 60 | DbClient = unknown, 61 | DbExecutor extends DbSQLExecutor = DbSQLExecutor, 62 | >( 63 | sqlExecutor: DbExecutor, 64 | // TODO: In the longer term we should have different options for query and command 65 | options: { 66 | connect: () => Promise; 67 | close?: (client: DbClient, error?: unknown) => Promise; 68 | }, 69 | ): SQLExecutor => ({ 70 | query: (sql, queryOptions) => 71 | executeInNewDbClient( 72 | (client) => sqlExecutor.query(client, sql, queryOptions), 73 | options, 74 | ), 75 | batchQuery: (sqls, queryOptions) => 76 | executeInNewDbClient( 77 | (client) => sqlExecutor.batchQuery(client, sqls, queryOptions), 78 | options, 79 | ), 80 | command: (sql, commandOptions) => 81 | executeInNewDbClient( 82 | (client) => sqlExecutor.command(client, sql, commandOptions), 83 | options, 84 | ), 85 | batchCommand: (sqls, commandOptions) => 86 | executeInNewDbClient( 87 | (client) => sqlExecutor.batchQuery(client, sqls, commandOptions), 88 | options, 89 | ), 90 | }); 91 | 92 | export const sqlExecutorInNewConnection = < 93 | ConnectionType extends Connection, 94 | >(options: { 95 | connection: () => Promise; 96 | }): SQLExecutor => ({ 97 | query: (sql) => 98 | executeInNewConnection( 99 | (connection) => connection.execute.query(sql), 100 | options, 101 | ), 102 | batchQuery: (sqls) => 103 | executeInNewConnection( 104 | (connection) => connection.execute.batchQuery(sqls), 105 | options, 106 | ), 107 | command: (sql) => 108 | executeInNewConnection( 109 | (connection) => connection.execute.command(sql), 110 | options, 111 | ), 112 | batchCommand: (sqls) => 113 | executeInNewConnection( 114 | (connection) => connection.execute.batchCommand(sqls), 115 | options, 116 | ), 117 | }); 118 | 119 | export const executeInNewDbClient = async < 120 | DbClient = unknown, 121 | Result = unknown, 122 | >( 123 | handle: (client: DbClient) => Promise, 124 | options: { 125 | connect: () => Promise; 126 | close?: (client: DbClient, error?: unknown) => Promise; 127 | }, 128 | ): Promise => { 129 | const { connect, close } = options; 130 | const client = await connect(); 131 | try { 132 | return await handle(client); 133 | } catch (error) { 134 | if (close) await close(client, error); 135 | 136 | throw error; 137 | } 138 | }; 139 | 140 | export const executeInNewConnection = async < 141 | ConnectionType extends Connection, 142 | Result, 143 | >( 144 | handle: (connection: ConnectionType) => Promise, 145 | options: { 146 | connection: () => Promise; 147 | }, 148 | ) => { 149 | const connection = await options.connection(); 150 | 151 | try { 152 | return await handle(connection); 153 | } finally { 154 | await connection.close(); 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/execute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './execute'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/index.ts: -------------------------------------------------------------------------------- 1 | import type { Connection, ConnectionPool } from './connections'; 2 | import type { ConnectorType } from './connectors'; 3 | 4 | export * from './connections'; 5 | export * from './connectors'; 6 | export * from './execute'; 7 | export * from './locks'; 8 | export * from './query'; 9 | export * from './schema'; 10 | export * from './serializer'; 11 | export * from './sql'; 12 | export * from './tracing'; 13 | 14 | export type DumboOptions = { 15 | connector?: Connector; 16 | }; 17 | 18 | export type Dumbo< 19 | Connector extends ConnectorType = ConnectorType, 20 | ConnectionType extends Connection = Connection, 21 | > = ConnectionPool; 22 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/locks/databaseLock.ts: -------------------------------------------------------------------------------- 1 | import { type SQLExecutor } from '..'; 2 | 3 | export type DatabaseLockOptions = { lockId: number; timeoutMs?: number }; 4 | 5 | export type AcquireDatabaseLockMode = 'Permanent' | 'Session'; 6 | 7 | export type AcquireDatabaseLockOptions = DatabaseLockOptions & { 8 | mode?: AcquireDatabaseLockMode; 9 | }; 10 | export type ReleaseDatabaseLockOptions = DatabaseLockOptions; 11 | 12 | export const defaultDatabaseLockOptions: Required< 13 | Omit 14 | > = { 15 | timeoutMs: 10000, 16 | }; 17 | 18 | export type DatabaseLock = { 19 | acquire( 20 | execute: SQLExecutor, 21 | options: AcquireDatabaseLockOptions, 22 | ): Promise; 23 | tryAcquire( 24 | execute: SQLExecutor, 25 | options: AcquireDatabaseLockOptions, 26 | ): Promise; 27 | release( 28 | execute: SQLExecutor, 29 | options: ReleaseDatabaseLockOptions, 30 | ): Promise; 31 | withAcquire: ( 32 | execute: SQLExecutor, 33 | handle: () => Promise, 34 | options: AcquireDatabaseLockOptions, 35 | ) => Promise; 36 | }; 37 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/locks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './databaseLock'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mappers'; 2 | export * from './query'; 3 | export * from './selectors'; 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/query/mappers.ts: -------------------------------------------------------------------------------- 1 | import type { QueryResult, QueryResultRow } from './query'; 2 | 3 | export const mapRows = async < 4 | Result extends QueryResultRow = QueryResultRow, 5 | Mapped = unknown, 6 | >( 7 | getResult: Promise>, 8 | map: (row: Result) => Mapped, 9 | ): Promise => { 10 | const result = await getResult; 11 | 12 | return result.rows.map(map); 13 | }; 14 | 15 | export const toCamelCase = (snakeStr: string): string => 16 | snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? ''); 17 | 18 | export const mapToCamelCase = >( 19 | obj: Record, 20 | ): T => { 21 | const newObj: Record = {}; 22 | for (const key in obj) { 23 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 24 | newObj[toCamelCase(key)] = obj[key]; 25 | } 26 | } 27 | return newObj as T; 28 | }; 29 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/query/query.ts: -------------------------------------------------------------------------------- 1 | export interface QueryResultRow { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | [column: string]: any; 4 | } 5 | 6 | export type QueryResult = { 7 | rowCount: number | null; 8 | rows: Result[]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/query/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { QueryResult, QueryResultRow } from './query'; 2 | 3 | export const firstOrNull = async < 4 | Result extends QueryResultRow = QueryResultRow, 5 | >( 6 | getResult: Promise>, 7 | ): Promise => { 8 | const result = await getResult; 9 | 10 | return result.rows.length > 0 ? (result.rows[0] ?? null) : null; 11 | }; 12 | 13 | export const first = async ( 14 | getResult: Promise>, 15 | ): Promise => { 16 | const result = await getResult; 17 | 18 | if (result.rows.length === 0) 19 | throw new Error("Query didn't return any result"); 20 | 21 | return result.rows[0]!; 22 | }; 23 | 24 | export const singleOrNull = async < 25 | Result extends QueryResultRow = QueryResultRow, 26 | >( 27 | getResult: Promise>, 28 | ): Promise => { 29 | const result = await getResult; 30 | 31 | if (result.rows.length > 1) throw new Error('Query had more than one result'); 32 | 33 | return result.rows.length > 0 ? (result.rows[0] ?? null) : null; 34 | }; 35 | 36 | export const single = async ( 37 | getResult: Promise>, 38 | ): Promise => { 39 | const result = await getResult; 40 | 41 | if (result.rows.length === 0) 42 | throw new Error("Query didn't return any result"); 43 | 44 | if (result.rows.length > 1) throw new Error('Query had more than one result'); 45 | 46 | return result.rows[0]!; 47 | }; 48 | 49 | export type CountSQLQueryResult = { count: number }; 50 | 51 | export const count = async ( 52 | getResult: Promise>, 53 | ): Promise => { 54 | const result = await single(getResult); 55 | 56 | return result.count; 57 | }; 58 | 59 | export type ExistsSQLQueryResult = { exists: boolean }; 60 | 61 | export const exists = async ( 62 | getResult: Promise>, 63 | ): Promise => { 64 | const result = await single(getResult); 65 | 66 | return result.exists === true; 67 | }; 68 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './migrations'; 2 | export * from './schemaComponent'; 3 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/schema/migrations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mapToCamelCase, 3 | rawSql, 4 | singleOrNull, 5 | sql, 6 | tracer, 7 | type SchemaComponent, 8 | type SQLExecutor, 9 | } from '..'; 10 | import { type DatabaseLock, type DatabaseLockOptions, type Dumbo } from '../..'; 11 | 12 | export type MigrationStyle = 'None' | 'CreateOrUpdate'; 13 | 14 | export type SQLMigration = { 15 | name: string; 16 | sqls: string[]; 17 | }; 18 | 19 | export const sqlMigration = (name: string, sqls: string[]): SQLMigration => ({ 20 | name, 21 | sqls, 22 | }); 23 | 24 | export type MigrationRecord = { 25 | id: number; 26 | name: string; 27 | application: string; 28 | sqlHash: string; 29 | timestamp: Date; 30 | }; 31 | export const MIGRATIONS_LOCK_ID = 999956789; 32 | 33 | export type MigratorOptions = { 34 | schema: { 35 | migrationTable: SchemaComponent; 36 | }; 37 | lock: { 38 | databaseLock: DatabaseLock; 39 | options?: Omit & 40 | Partial>; 41 | }; 42 | dryRun?: boolean | undefined; 43 | }; 44 | 45 | export const runSQLMigrations = ( 46 | pool: Dumbo, 47 | migrations: ReadonlyArray, 48 | options: MigratorOptions, 49 | ): Promise => 50 | pool.withTransaction(async ({ execute }) => { 51 | const { databaseLock, ...rest } = options.lock; 52 | 53 | const lockOptions: DatabaseLockOptions = { 54 | lockId: MIGRATIONS_LOCK_ID, 55 | ...rest, 56 | }; 57 | 58 | const coreMigrations = options.schema.migrationTable.migrations({ 59 | connector: 'PostgreSQL:pg', // TODO: This will need to change to support more connectors 60 | }); 61 | 62 | await databaseLock.withAcquire( 63 | execute, 64 | async () => { 65 | for (const migration of coreMigrations) { 66 | const sql = combineMigrations(migration); 67 | await execute.command(rawSql(sql)); 68 | } 69 | 70 | for (const migration of migrations) { 71 | await runSQLMigration(execute, migration); 72 | } 73 | }, 74 | lockOptions, 75 | ); 76 | 77 | return { success: options.dryRun ? false : true, result: undefined }; 78 | }); 79 | 80 | const runSQLMigration = async ( 81 | execute: SQLExecutor, 82 | migration: SQLMigration, 83 | ): Promise => { 84 | const sql = combineMigrations(migration); 85 | const sqlHash = await getMigrationHash(sql); 86 | 87 | try { 88 | const newMigration = { 89 | name: migration.name, 90 | sqlHash, 91 | }; 92 | 93 | const wasMigrationApplied = await ensureMigrationWasNotAppliedYet( 94 | execute, 95 | newMigration, 96 | ); 97 | 98 | if (wasMigrationApplied) return; 99 | 100 | await execute.command(rawSql(sql)); 101 | 102 | await recordMigration(execute, newMigration); 103 | // console.log(`Migration "${newMigration.name}" applied successfully.`); 104 | } catch (error) { 105 | tracer.error('migration-error', { 106 | migationName: migration.name, 107 | error: error, 108 | }); 109 | throw error; 110 | } 111 | }; 112 | 113 | const getMigrationHash = async (content: string): Promise => { 114 | const encoder = new TextEncoder(); 115 | const data = encoder.encode(content); 116 | const hashBuffer = await crypto.subtle.digest('SHA-256', data); 117 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 118 | return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); 119 | }; 120 | 121 | export const combineMigrations = (...migration: Pick[]) => 122 | migration.flatMap((m) => m.sqls).join('\n'); 123 | 124 | const ensureMigrationWasNotAppliedYet = async ( 125 | execute: SQLExecutor, 126 | migration: { name: string; sqlHash: string }, 127 | ): Promise => { 128 | const result = await singleOrNull( 129 | execute.query<{ sql_hash: string }>( 130 | sql(`SELECT sql_hash FROM migrations WHERE name = %L`, migration.name), 131 | ), 132 | ); 133 | 134 | if (result === null) return false; 135 | 136 | const { sqlHash } = mapToCamelCase>(result); 137 | 138 | if (sqlHash !== migration.sqlHash) { 139 | throw new Error( 140 | `Migration hash mismatch for "${migration.name}". Aborting migration.`, 141 | ); 142 | } 143 | 144 | //console.log(`Migration "${migration.name}" already applied. Skipping.`); 145 | return true; 146 | }; 147 | 148 | const recordMigration = async ( 149 | execute: SQLExecutor, 150 | migration: { name: string; sqlHash: string }, 151 | ): Promise => { 152 | await execute.command( 153 | sql( 154 | ` 155 | INSERT INTO migrations (name, sql_hash) 156 | VALUES (%L, %L) 157 | `, 158 | migration.name, 159 | migration.sqlHash, 160 | ), 161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/schema/schemaComponent.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectorType } from '../..'; 2 | import { type SQLMigration } from './migrations'; 3 | 4 | export type SchemaComponentMigrationsOptions = { 5 | connector: ConnectorType; 6 | }; 7 | 8 | export type SchemaComponent = { 9 | schemaComponentType: string; 10 | components?: ReadonlyArray | undefined; 11 | migrations( 12 | options: SchemaComponentMigrationsOptions, 13 | ): ReadonlyArray; 14 | }; 15 | 16 | export const schemaComponent = ( 17 | type: string, 18 | migrationsOrComponents: 19 | | { 20 | migrations( 21 | options: SchemaComponentMigrationsOptions, 22 | ): ReadonlyArray; 23 | } 24 | | { 25 | migrations( 26 | options: SchemaComponentMigrationsOptions, 27 | ): ReadonlyArray; 28 | components: ReadonlyArray; 29 | } 30 | | { 31 | components: ReadonlyArray; 32 | }, 33 | ): SchemaComponent => { 34 | const components = 35 | 'components' in migrationsOrComponents 36 | ? migrationsOrComponents.components 37 | : undefined; 38 | 39 | const migrations = 40 | 'migrations' in migrationsOrComponents 41 | ? migrationsOrComponents.migrations 42 | : undefined; 43 | 44 | return { 45 | schemaComponentType: type, 46 | components, 47 | migrations: (options) => [ 48 | ...(migrations ? migrations(options) : []), 49 | ...(components 50 | ? components.flatMap((component) => component.migrations(options)) 51 | : []), 52 | ], 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/serializer/index.ts: -------------------------------------------------------------------------------- 1 | export interface Serializer< 2 | Payload, 3 | SerializeOptions = never, 4 | DeserializeOptions = SerializeOptions, 5 | > { 6 | serialize(object: T, options?: SerializeOptions): Payload; 7 | deserialize(payload: Payload, options?: DeserializeOptions): T; 8 | } 9 | 10 | export interface ObjectCodec { 11 | encode(object: T): Payload; 12 | decode(payload: Payload): T; 13 | } 14 | 15 | export * from './json'; 16 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/serializer/json/index.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectCodec, Serializer } from '..'; 2 | 3 | interface JSONSerializer< 4 | SerializeOptions = JSONSerializeOptions, 5 | DeserializeOptions = JSONDeserializeOptions, 6 | > extends Serializer { 7 | serialize(object: T, options?: SerializeOptions): string; 8 | deserialize(payload: string, options?: DeserializeOptions): T; 9 | } 10 | 11 | type JSONSerializerOptions = { 12 | disableBigIntSerialization?: boolean; 13 | }; 14 | 15 | type JSONSerializeOptions = { 16 | replacer?: JSONReplacer; 17 | } & JSONSerializerOptions; 18 | 19 | type JSONDeserializeOptions = { 20 | reviver?: JSONReviver; 21 | } & JSONSerializerOptions; 22 | 23 | interface JSONObjectCodec< 24 | T, 25 | SerializeOptions = JSONSerializeOptions, 26 | DeserializeOptions = JSONDeserializeOptions, 27 | > extends ObjectCodec { 28 | encode(object: T, options?: SerializeOptions): string; 29 | decode(payload: string, options?: DeserializeOptions): T; 30 | } 31 | 32 | type JSONObjectCodecOptions< 33 | SerializeOptions = JSONSerializeOptions, 34 | DeserializeOptions = JSONDeserializeOptions, 35 | > = 36 | | { serializer?: JSONSerializer } 37 | | { serializerOptions?: JSONSerializerOptions }; 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | type JSONReplacer = (this: any, key: string, value: any) => any; 41 | 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | type JSONReviver = (this: any, key: string, value: any) => any; 44 | 45 | const bigIntReplacer: JSONReplacer = (_key, value) => { 46 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 47 | return typeof value === 'bigint' ? value.toString() : value; 48 | }; 49 | 50 | const bigIntReviver: JSONReviver = (_key, value) => { 51 | if (typeof value === 'string' && /^[+-]?\d+n?$/.test(value)) { 52 | return BigInt(value); 53 | } 54 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 55 | return value; 56 | }; 57 | 58 | const composeJSONReplacers = 59 | (...replacers: JSONReplacer[]): JSONReplacer => 60 | (key, value) => 61 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 62 | replacers.reduce((accValue, replacer) => replacer(key, accValue), value); 63 | 64 | const composeJSONRevivers = 65 | (...revivers: JSONReviver[]): JSONReviver => 66 | (key, value) => 67 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 68 | revivers.reduce((accValue, reviver) => reviver(key, accValue), value); 69 | 70 | const JSONReplacer = (opts?: JSONSerializeOptions) => 71 | opts?.disableBigIntSerialization == true 72 | ? opts.replacer 73 | ? opts.replacer 74 | : undefined 75 | : opts?.replacer 76 | ? composeJSONReplacers(JSONReplacers.bigInt, opts.replacer) 77 | : JSONReplacers.bigInt; 78 | 79 | const JSONReviver = (opts?: JSONDeserializeOptions) => 80 | opts?.disableBigIntSerialization == true 81 | ? opts.reviver 82 | ? opts.reviver 83 | : undefined 84 | : opts?.reviver 85 | ? composeJSONRevivers(JSONRevivers.bigInt, opts.reviver) 86 | : JSONRevivers.bigInt; 87 | 88 | const JSONReplacers = { 89 | bigInt: bigIntReplacer, 90 | }; 91 | 92 | const JSONRevivers = { 93 | bigInt: bigIntReviver, 94 | }; 95 | 96 | const jsonSerializer = (options?: JSONSerializerOptions): JSONSerializer => { 97 | const defaultReplacer = JSONReplacer(options); 98 | const defaultReviver = JSONReviver(options); 99 | 100 | return { 101 | serialize: (object: T, options?: JSONSerializeOptions): string => 102 | JSON.stringify(object, options ? JSONReplacer(options) : defaultReplacer), 103 | deserialize: (payload: string, options?: JSONDeserializeOptions): T => 104 | JSON.parse(payload, options ? JSONReviver(options) : defaultReviver) as T, 105 | }; 106 | }; 107 | 108 | const JSONSerializer = jsonSerializer({ disableBigIntSerialization: false }); 109 | 110 | const RawJSONSerializer = jsonSerializer({ disableBigIntSerialization: true }); 111 | 112 | const JSONObjectCodec = < 113 | T, 114 | SerializeOptions = JSONSerializeOptions, 115 | DeserializeOptions = JSONDeserializeOptions, 116 | >( 117 | options: JSONObjectCodecOptions, 118 | ): JSONObjectCodec => { 119 | const serializer = 120 | 'serializer' in options 121 | ? options.serializer 122 | : jsonSerializer( 123 | 'serializerOptions' in options 124 | ? options.serializerOptions 125 | : undefined, 126 | ); 127 | 128 | return { 129 | decode: (payload: string, options?: DeserializeOptions) => 130 | options 131 | ? serializer.deserialize(payload, options) 132 | : serializer.deserialize(payload), 133 | encode: (object: T, options?: SerializeOptions) => 134 | options 135 | ? serializer.serialize(object, options) 136 | : serializer.serialize(object), 137 | }; 138 | }; 139 | 140 | export { 141 | composeJSONReplacers, 142 | composeJSONRevivers, 143 | JSONReplacer, 144 | JSONReplacers, 145 | JSONReviver, 146 | JSONRevivers, 147 | JSONSerializer, 148 | jsonSerializer, 149 | RawJSONSerializer, 150 | type JSONObjectCodec, 151 | type JSONObjectCodecOptions, 152 | }; 153 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sql'; 2 | export * from './sqlFormatter'; 3 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/pg-format/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer } from '../../serializer'; 2 | import { type SQLFormatter, registerFormatter } from '../sql'; 3 | import format from './pgFormat'; 4 | 5 | const pgFormatter: SQLFormatter = { 6 | formatIdentifier: format.ident, 7 | formatLiteral: format.literal, 8 | formatString: format.string, 9 | formatArray: ( 10 | array: unknown[], 11 | itemFormatter: (item: unknown) => string, 12 | ): string => { 13 | if (array.length === 0) { 14 | return '()'; 15 | } 16 | 17 | // Check if it's a nested array 18 | const isNestedArray = array.some((item) => Array.isArray(item)); 19 | 20 | if (isNestedArray) { 21 | // For nested arrays, format with double parentheses 22 | const formattedItems = array.map((item) => { 23 | if (Array.isArray(item)) { 24 | // Use parentheses around each subarray item 25 | return ( 26 | '(' + item.map((subItem) => itemFormatter(subItem)).join(', ') + ')' 27 | ); 28 | } 29 | return itemFormatter(item); 30 | }); 31 | 32 | // Wrap the entire result in additional parentheses 33 | return '(' + formattedItems.join(', ') + ')'; 34 | } else { 35 | // For regular arrays, use PostgreSQL's tuple syntax: (item1, item2, ...) 36 | const formattedItems = array.map((item) => itemFormatter(item)); 37 | return '(' + formattedItems.join(', ') + ')'; 38 | } 39 | }, 40 | 41 | formatDate: (value: Date): string => { 42 | // Format date for PostgreSQL with proper timezone 43 | let isoStr = value.toISOString(); 44 | // Replace 'T' with space and keep timezone info (Z becomes +00) 45 | isoStr = isoStr.replace('T', ' ').replace('Z', '+00'); 46 | return `'${isoStr}'`; 47 | }, 48 | 49 | formatObject: (value: object): string => { 50 | return `'${JSONSerializer.serialize(value).replace(/'/g, "''")}'`; 51 | }, 52 | }; 53 | 54 | registerFormatter('PostgreSQL', pgFormatter); 55 | 56 | // Export the original functions if needed 57 | export { format, pgFormatter }; 58 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/pg-format/reserved.ts: -------------------------------------------------------------------------------- 1 | // Ported from: https://github.com/datalanche/node-pg-format/blob/master/lib/reserved.js 2 | // 3 | // PostgreSQL reserved words 4 | // 5 | const reservedMap: { [key: string]: boolean } = { 6 | AES128: true, 7 | AES256: true, 8 | ALL: true, 9 | ALLOWOVERWRITE: true, 10 | ANALYSE: true, 11 | ANALYZE: true, 12 | AND: true, 13 | ANY: true, 14 | ARRAY: true, 15 | AS: true, 16 | ASC: true, 17 | AUTHORIZATION: true, 18 | BACKUP: true, 19 | BETWEEN: true, 20 | BINARY: true, 21 | BLANKSASNULL: true, 22 | BOTH: true, 23 | BYTEDICT: true, 24 | CASE: true, 25 | CAST: true, 26 | CHECK: true, 27 | COLLATE: true, 28 | COLUMN: true, 29 | CONSTRAINT: true, 30 | CREATE: true, 31 | CREDENTIALS: true, 32 | CROSS: true, 33 | CURRENT_DATE: true, 34 | CURRENT_TIME: true, 35 | CURRENT_TIMESTAMP: true, 36 | CURRENT_USER: true, 37 | CURRENT_USER_ID: true, 38 | DEFAULT: true, 39 | DEFERRABLE: true, 40 | DEFLATE: true, 41 | DEFRAG: true, 42 | DELTA: true, 43 | DELTA32K: true, 44 | DESC: true, 45 | DISABLE: true, 46 | DISTINCT: true, 47 | DO: true, 48 | ELSE: true, 49 | EMPTYASNULL: true, 50 | ENABLE: true, 51 | ENCODE: true, 52 | ENCRYPT: true, 53 | ENCRYPTION: true, 54 | END: true, 55 | EXCEPT: true, 56 | EXPLICIT: true, 57 | FALSE: true, 58 | FOR: true, 59 | FOREIGN: true, 60 | FREEZE: true, 61 | FROM: true, 62 | FULL: true, 63 | GLOBALDICT256: true, 64 | GLOBALDICT64K: true, 65 | GRANT: true, 66 | GROUP: true, 67 | GZIP: true, 68 | HAVING: true, 69 | IDENTITY: true, 70 | IGNORE: true, 71 | ILIKE: true, 72 | IN: true, 73 | INITIALLY: true, 74 | INNER: true, 75 | INTERSECT: true, 76 | INTO: true, 77 | IS: true, 78 | ISNULL: true, 79 | JOIN: true, 80 | LEADING: true, 81 | LEFT: true, 82 | LIKE: true, 83 | LIMIT: true, 84 | LOCALTIME: true, 85 | LOCALTIMESTAMP: true, 86 | LUN: true, 87 | LUNS: true, 88 | LZO: true, 89 | LZOP: true, 90 | MINUS: true, 91 | MOSTLY13: true, 92 | MOSTLY32: true, 93 | MOSTLY8: true, 94 | NATURAL: true, 95 | NEW: true, 96 | NOT: true, 97 | NOTNULL: true, 98 | NULL: true, 99 | NULLS: true, 100 | OFF: true, 101 | OFFLINE: true, 102 | OFFSET: true, 103 | OLD: true, 104 | ON: true, 105 | ONLY: true, 106 | OPEN: true, 107 | OR: true, 108 | ORDER: true, 109 | OUTER: true, 110 | OVERLAPS: true, 111 | PARALLEL: true, 112 | PARTITION: true, 113 | PERCENT: true, 114 | PLACING: true, 115 | PRIMARY: true, 116 | RAW: true, 117 | READRATIO: true, 118 | RECOVER: true, 119 | REFERENCES: true, 120 | REJECTLOG: true, 121 | RESORT: true, 122 | RESTORE: true, 123 | RIGHT: true, 124 | SELECT: true, 125 | SESSION_USER: true, 126 | SIMILAR: true, 127 | SOME: true, 128 | SYSDATE: true, 129 | SYSTEM: true, 130 | TABLE: true, 131 | TAG: true, 132 | TDES: true, 133 | TEXT255: true, 134 | TEXT32K: true, 135 | THEN: true, 136 | TO: true, 137 | TOP: true, 138 | TRAILING: true, 139 | TRUE: true, 140 | TRUNCATECOLUMNS: true, 141 | UNION: true, 142 | UNIQUE: true, 143 | USER: true, 144 | USING: true, 145 | VERBOSE: true, 146 | WALLET: true, 147 | WHEN: true, 148 | WHERE: true, 149 | WITH: true, 150 | WITHOUT: true, 151 | }; 152 | 153 | export default reservedMap; 154 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/sql.ts: -------------------------------------------------------------------------------- 1 | import format from './pg-format/pgFormat'; 2 | 3 | export type SQL = string & { __brand: 'sql' }; 4 | 5 | export interface SQLFormatter { 6 | formatIdentifier: (value: unknown) => string; 7 | formatLiteral: (value: unknown) => string; 8 | formatString: (value: unknown) => string; 9 | formatArray?: ( 10 | array: unknown[], 11 | itemFormatter: (item: unknown) => string, 12 | ) => string; 13 | formatDate?: (value: Date) => string; 14 | formatObject?: (value: object) => string; 15 | formatBigInt?: (value: bigint) => string; 16 | } 17 | 18 | const formatters: Record = {}; 19 | 20 | const ID = Symbol('SQL_IDENTIFIER'); 21 | const RAW = Symbol('SQL_RAW'); 22 | const LITERAL = Symbol('SQL_LITERAL'); 23 | 24 | type SQLIdentifier = { [ID]: true; value: string }; 25 | type SQLRaw = { [RAW]: true; value: string }; 26 | type SQLLiteral = { [LITERAL]: true; value: unknown }; 27 | 28 | export function identifier(value: string): SQLIdentifier { 29 | return { [ID]: true, value }; 30 | } 31 | 32 | export function raw(value: string): SQLRaw { 33 | return { [RAW]: true, value }; 34 | } 35 | 36 | //TODO: remove it 37 | export const plainString = raw; 38 | 39 | export function literal(value: unknown): SQLLiteral { 40 | return { [LITERAL]: true, value }; 41 | } 42 | 43 | // Type guards 44 | export const isIdentifier = (value: unknown): value is SQLIdentifier => { 45 | return value !== null && typeof value === 'object' && ID in value; 46 | }; 47 | 48 | export const isRaw = (value: unknown): value is SQLRaw => { 49 | return value !== null && typeof value === 'object' && RAW in value; 50 | }; 51 | 52 | export const isLiteral = (value: unknown): value is SQLLiteral => { 53 | return value !== null && typeof value === 'object' && LITERAL in value; 54 | }; 55 | 56 | export interface DeferredSQL { 57 | __brand: 'deferred-sql'; 58 | strings: TemplateStringsArray; 59 | values: unknown[]; 60 | } 61 | 62 | export const isDeferredSQL = (value: unknown): value is DeferredSQL => { 63 | return ( 64 | value !== null && 65 | typeof value === 'object' && 66 | '__brand' in value && 67 | value.__brand === 'deferred-sql' 68 | ); 69 | }; 70 | 71 | export function SQL(strings: TemplateStringsArray, ...values: unknown[]): SQL { 72 | const deferredSql: DeferredSQL = { 73 | __brand: 'deferred-sql', 74 | strings, 75 | values, 76 | }; 77 | 78 | return deferredSql as unknown as SQL; 79 | } 80 | 81 | export const isReserved = ( 82 | value: string, 83 | reservedWords: Record, 84 | ): boolean => !!reservedWords[value.toUpperCase()]; 85 | 86 | // Helper to format arrays as lists 87 | export function arrayToList( 88 | useSpace: boolean, 89 | array: unknown[], 90 | formatter: (value: unknown) => string, 91 | ): string { 92 | let sql = ''; 93 | sql += useSpace ? ' (' : '('; 94 | for (let i = 0; i < array.length; i++) { 95 | sql += (i === 0 ? '' : ', ') + formatter(array[i]); 96 | } 97 | sql += ')'; 98 | return sql; 99 | } 100 | 101 | // Register a formatter for a specific dialect 102 | export const registerFormatter = ( 103 | dialect: string, 104 | formatter: SQLFormatter, 105 | ): void => { 106 | formatters[dialect] = formatter; 107 | }; 108 | 109 | export const getFormatter = (dialect: string): SQLFormatter => { 110 | const formatterKey = dialect.toLowerCase(); 111 | if (!formatters[formatterKey]) { 112 | throw new Error(`No SQL formatter registered for dialect: ${dialect}`); 113 | } 114 | return formatters[formatterKey]; 115 | }; 116 | 117 | export const sql = (sqlQuery: string, ...params: unknown[]): SQL => { 118 | return format(sqlQuery, ...params) as SQL; 119 | }; 120 | 121 | export const rawSql = (sqlQuery: string): SQL => { 122 | return sqlQuery as SQL; 123 | }; 124 | 125 | export const isSQL = (value: unknown): value is SQL => { 126 | if (value === undefined || value === null) { 127 | return false; 128 | } 129 | 130 | if (isDeferredSQL(value)) { 131 | return true; 132 | } 133 | 134 | if (typeof value === 'object') { 135 | return '__brand' in value && value.__brand === 'sql'; 136 | } 137 | 138 | return typeof value === 'string'; 139 | }; 140 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/sqlFormatter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isDeferredSQL, 3 | isIdentifier, 4 | isLiteral, 5 | isRaw, 6 | SQL, 7 | type DeferredSQL, 8 | type SQLFormatter, 9 | } from './sql'; 10 | 11 | function formatValue(value: unknown, formatter: SQLFormatter): string { 12 | // Handle SQL wrapper types first 13 | if (isIdentifier(value)) { 14 | return formatter.formatIdentifier(value.value); 15 | } else if (isRaw(value)) { 16 | return value.value; 17 | } else if (isLiteral(value)) { 18 | return formatter.formatLiteral(value.value); 19 | } else if (isDeferredSQL(value)) { 20 | return processDeferredSQL( 21 | value as unknown as SQL, 22 | formatter, 23 | ) as unknown as string; 24 | } 25 | 26 | // Handle specific types directly 27 | if (value === null || value === undefined) { 28 | return 'NULL'; 29 | } else if (typeof value === 'number') { 30 | return value.toString(); 31 | } else if (Array.isArray(value)) { 32 | return formatter.formatArray 33 | ? formatter.formatArray(value, (item) => formatValue(item, formatter)) 34 | : formatter.formatLiteral(value); 35 | } else if (typeof value === 'bigint') { 36 | // Format BigInt as a quoted string to match test expectations 37 | 38 | return formatter.formatBigInt 39 | ? formatter.formatBigInt(value) 40 | : formatter.formatLiteral(value); 41 | } else if (value instanceof Date) { 42 | // Let the formatter handle dates consistently 43 | return formatter.formatDate 44 | ? formatter.formatDate(value) 45 | : formatter.formatLiteral(value); 46 | } else if (typeof value === 'object') { 47 | // Let the formatter handle objects (excluding null which is handled above) 48 | return formatter.formatObject 49 | ? formatter.formatObject(value) 50 | : formatter.formatLiteral(value); 51 | } 52 | 53 | // For all other types, use the formatter's literal formatting 54 | return formatter.formatLiteral(value); 55 | } 56 | 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | export function processDeferredSQL(sql: SQL, formatter: any): SQL { 59 | // If it's not a DeferredSQL, return as is 60 | if (!isDeferredSQL(sql)) { 61 | return sql; 62 | } 63 | 64 | const { strings, values } = sql as DeferredSQL; 65 | 66 | // Process the template 67 | let result = ''; 68 | strings.forEach((string, i) => { 69 | result += string; 70 | 71 | if (i < values.length) { 72 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 73 | result += formatValue(values[i], formatter); 74 | } 75 | }); 76 | 77 | return result as SQL; 78 | } 79 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/sqlite-format/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer } from '../../serializer'; 2 | import { type SQLFormatter, registerFormatter } from '../sql'; 3 | import format from './sqliteFormat'; 4 | 5 | const sqliteFormatter: SQLFormatter = { 6 | formatIdentifier: format.ident, 7 | formatLiteral: format.literal, 8 | formatString: format.string, 9 | formatArray: (array, itemFormatter) => { 10 | if (array.length === 0) return '()'; 11 | return '(' + array.map(itemFormatter).join(', ') + ')'; 12 | }, 13 | formatDate: (value) => format.literal(value.toISOString()), 14 | formatObject: (value) => 15 | `'${JSONSerializer.serialize(value).replace(/'/g, "''")}'`, 16 | }; 17 | 18 | registerFormatter('SQLite', sqliteFormatter); 19 | 20 | export { format, sqliteFormatter }; 21 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/sql/sqlite-format/sqliteFormat.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer } from '../../serializer'; 2 | 3 | const arrayToList = ( 4 | useSpace: boolean, 5 | array: unknown[], 6 | formatter: (val: unknown) => string, 7 | ): string => { 8 | let sql = ''; 9 | sql += useSpace ? ' (' : '('; 10 | for (let i = 0; i < array.length; i++) { 11 | sql += (i === 0 ? '' : ', ') + formatter(array[i]); 12 | } 13 | sql += ')'; 14 | return sql; 15 | }; 16 | 17 | const quoteIdent = (value: unknown): string => { 18 | if (value === undefined || value === null) { 19 | throw new Error('SQL identifier cannot be null or undefined'); 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 23 | const ident = value.toString(); 24 | 25 | // Only leave unquoted if it's lowercase snake_case 26 | if (/^[a-z_][a-z0-9_]*$/.test(ident)) { 27 | return ident; 28 | } 29 | 30 | return `"${ident.replace(/"/g, '""')}"`; 31 | }; 32 | 33 | const quoteLiteral = (value: unknown): string => { 34 | if (value === undefined || value === null) return 'NULL'; 35 | 36 | if (typeof value === 'boolean') return value ? '1' : '0'; 37 | if (typeof value === 'number') return value.toString(); 38 | if (value instanceof Date) return `'${value.toISOString()}'`; 39 | if (typeof value === 'bigint') { 40 | const minSQLiteInt = -9223372036854775808n; 41 | const maxSQLiteInt = 9223372036854775807n; 42 | 43 | // If it's in SQLite's INTEGER range, treat it as a number 44 | if (value >= minSQLiteInt && value <= maxSQLiteInt) { 45 | return value.toString(); 46 | } 47 | 48 | // Out of range — fallback to quoted TEXT 49 | return `'${value.toString()}'`; 50 | } 51 | 52 | if (Array.isArray(value)) { 53 | return value 54 | .map((v, i) => { 55 | if (Array.isArray(v)) { 56 | return arrayToList(i !== 0, v, quoteLiteral); 57 | } 58 | return quoteLiteral(v); 59 | }) 60 | .toString(); 61 | } 62 | 63 | if (typeof value === 'object') { 64 | const json = JSONSerializer.serialize(value); 65 | return `'${json.replace(/'/g, "''")}'`; 66 | } 67 | 68 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 69 | const str = value.toString(); 70 | return `'${str.replace(/'/g, "''")}'`; 71 | }; 72 | 73 | const quoteString = (value: unknown): string => { 74 | if (value === undefined || value === null) return ''; 75 | if (typeof value === 'boolean') return value ? '1' : '0'; 76 | if (value instanceof Date) return value.toISOString(); 77 | if (typeof value === 'bigint') { 78 | const minSQLiteInt = -9223372036854775808n; 79 | const maxSQLiteInt = 9223372036854775807n; 80 | 81 | // If it's in SQLite's INTEGER range, treat it as a number 82 | if (value >= minSQLiteInt && value <= maxSQLiteInt) { 83 | return value.toString(); 84 | } 85 | 86 | // Out of range — fallback to quoted TEXT 87 | return `'${value.toString()}'`; 88 | } 89 | 90 | if (Array.isArray(value)) { 91 | return value 92 | .map((v, i) => { 93 | if (v !== null && v !== undefined) { 94 | if (Array.isArray(v)) return arrayToList(i !== 0, v, quoteString); 95 | return quoteString(v); 96 | } 97 | return ''; 98 | }) 99 | .filter((v) => v !== '') 100 | .toString(); 101 | } 102 | 103 | if (typeof value === 'object') { 104 | return JSONSerializer.serialize(value); 105 | } 106 | 107 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 108 | return value.toString(); 109 | }; 110 | 111 | const formatWithArray = (fmt: string, parameters: unknown[]): string => { 112 | let index = 0; 113 | const re = /%(%|(\\d+\\$)?[ILs])/g; 114 | 115 | return fmt.replace(re, (_, type) => { 116 | if (type === '%') return '%'; 117 | 118 | let position = index; 119 | 120 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access 121 | const tokens = type.split('$') as string[]; 122 | if (tokens.length > 1) { 123 | position = parseInt(tokens[0]!, 10) - 1; 124 | type = tokens[1]; 125 | } 126 | 127 | if (position < 0 || position >= parameters.length) { 128 | throw new Error('Invalid parameter index'); 129 | } 130 | 131 | index = position + 1; 132 | 133 | if (type === 'I') return quoteIdent(parameters[position]); 134 | if (type === 'L') return quoteLiteral(parameters[position]); 135 | if (type === 's') return quoteString(parameters[position]); 136 | 137 | return ''; 138 | }); 139 | }; 140 | 141 | const format = (fmt: string, ...args: unknown[]): string => { 142 | return formatWithArray(fmt, args); 143 | }; 144 | 145 | format.ident = quoteIdent; 146 | format.literal = quoteLiteral; 147 | format.string = quoteString; 148 | format.withArray = formatWithArray; 149 | format.config = (_cfg: unknown) => {}; // No-op for SQLite 150 | 151 | export default format; 152 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/tracing/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer } from '../serializer'; 2 | import { prettyJson } from './printing'; 3 | 4 | export const tracer = () => {}; 5 | 6 | export type LogLevel = 'DISABLED' | 'INFO' | 'LOG' | 'WARN' | 'ERROR'; 7 | 8 | export const LogLevel = { 9 | DISABLED: 'DISABLED' as LogLevel, 10 | INFO: 'INFO' as LogLevel, 11 | LOG: 'LOG' as LogLevel, 12 | WARN: 'WARN' as LogLevel, 13 | ERROR: 'ERROR' as LogLevel, 14 | }; 15 | 16 | export type LogType = 'CONSOLE'; 17 | 18 | export type LogStyle = 'RAW' | 'PRETTY'; 19 | 20 | export const LogStyle = { 21 | RAW: 'RAW' as LogStyle, 22 | PRETTY: 'PRETTY' as LogStyle, 23 | }; 24 | 25 | const shouldLog = (logLevel: LogLevel): boolean => { 26 | const definedLogLevel = process.env.DUMBO_LOG_LEVEL ?? LogLevel.DISABLED; 27 | 28 | if (definedLogLevel === LogLevel.ERROR && logLevel === LogLevel.ERROR) 29 | return true; 30 | 31 | if ( 32 | definedLogLevel === LogLevel.WARN && 33 | [LogLevel.ERROR, LogLevel.WARN].includes(logLevel) 34 | ) 35 | return true; 36 | 37 | if ( 38 | definedLogLevel === LogLevel.LOG && 39 | [LogLevel.ERROR, LogLevel.WARN, LogLevel.LOG].includes(logLevel) 40 | ) 41 | return true; 42 | 43 | if ( 44 | definedLogLevel === LogLevel.INFO && 45 | [LogLevel.ERROR, LogLevel.WARN, LogLevel.LOG, LogLevel.INFO].includes( 46 | logLevel, 47 | ) 48 | ) 49 | return true; 50 | 51 | return false; 52 | }; 53 | 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | type TraceEventRecorder = (message?: any, ...optionalParams: any[]) => void; 56 | 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | type TraceEventFormatter = (event: any) => string; 59 | 60 | const nulloTraceEventRecorder: TraceEventRecorder = () => {}; 61 | 62 | const getTraceEventFormatter = 63 | (logStyle: LogStyle): TraceEventFormatter => 64 | (event) => { 65 | switch (logStyle) { 66 | case 'RAW': 67 | return JSONSerializer.serialize(event); 68 | case 'PRETTY': 69 | return prettyJson(event, { handleMultiline: true }); 70 | } 71 | }; 72 | 73 | const getTraceEventRecorder = ( 74 | logLevel: LogLevel, 75 | logStyle: LogStyle, 76 | ): TraceEventRecorder => { 77 | const format = getTraceEventFormatter(logStyle); 78 | switch (logLevel) { 79 | case 'DISABLED': 80 | return nulloTraceEventRecorder; 81 | case 'INFO': 82 | return (event) => console.info(format(event)); 83 | case 'LOG': 84 | return (event) => console.log(format(event)); 85 | case 'WARN': 86 | return (event) => console.warn(format(event)); 87 | case 'ERROR': 88 | return (event) => console.error(format(event)); 89 | } 90 | }; 91 | 92 | const recordTraceEvent = ( 93 | logLevel: LogLevel, 94 | eventName: string, 95 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 96 | attributes?: Record, 97 | ) => { 98 | if (!shouldLog(LogLevel.LOG)) return; 99 | 100 | const event = { 101 | name: eventName, 102 | timestamp: new Date().getTime(), 103 | ...attributes, 104 | }; 105 | 106 | const record = getTraceEventRecorder( 107 | logLevel, 108 | (process.env.DUMBO_LOG_STYLE as LogStyle | undefined) ?? 'RAW', 109 | ); 110 | 111 | record(event); 112 | }; 113 | 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 115 | tracer.info = (eventName: string, attributes?: Record) => 116 | recordTraceEvent(LogLevel.INFO, eventName, attributes); 117 | 118 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 119 | tracer.warn = (eventName: string, attributes?: Record) => 120 | recordTraceEvent(LogLevel.WARN, eventName, attributes); 121 | 122 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 123 | tracer.log = (eventName: string, attributes?: Record) => 124 | recordTraceEvent(LogLevel.LOG, eventName, attributes); 125 | 126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 127 | tracer.error = (eventName: string, attributes?: Record) => 128 | recordTraceEvent(LogLevel.ERROR, eventName, attributes); 129 | 130 | export * from './printing'; 131 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/tracing/printing/color.ts: -------------------------------------------------------------------------------- 1 | import ansis from 'ansis'; 2 | 3 | let enableColors = true; 4 | 5 | export const color = { 6 | set level(value: 0 | 1) { 7 | enableColors = value === 1; 8 | }, 9 | hex: 10 | (value: string) => 11 | (text: string): string => 12 | enableColors ? ansis.hex(value)(text) : text, 13 | red: (value: string): string => (enableColors ? ansis.red(value) : value), 14 | green: (value: string): string => (enableColors ? ansis.green(value) : value), 15 | blue: (value: string): string => (enableColors ? ansis.blue(value) : value), 16 | cyan: (value: string): string => (enableColors ? ansis.cyan(value) : value), 17 | yellow: (value: string): string => 18 | enableColors ? ansis.yellow(value) : value, 19 | }; 20 | 21 | export default color; 22 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/tracing/printing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './color'; 2 | export * from './pretty'; 3 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/tracing/printing/pretty.ts: -------------------------------------------------------------------------------- 1 | import chalk from './color'; 2 | 3 | const TWO_SPACES = ' '; 4 | 5 | const COLOR_STRING = chalk.hex('#98c379'); // Soft green for strings 6 | const COLOR_KEY = chalk.hex('#61afef'); // Muted cyan for keys 7 | const COLOR_NUMBER_OR_DATE = chalk.hex('#d19a66'); // Light orange for numbers 8 | const COLOR_BOOLEAN = chalk.hex('#c678dd'); // Light purple for booleans 9 | const COLOR_NULL_OR_UNDEFINED = chalk.hex('#c678dd'); // Light purple for null 10 | const COLOR_BRACKETS = chalk.hex('#abb2bf'); // Soft white for object and array brackets 11 | 12 | const processString = ( 13 | str: string, 14 | indent: string, 15 | handleMultiline: boolean, 16 | ): string => { 17 | if (handleMultiline && str.includes('\n')) { 18 | const lines = str.split('\n'); 19 | const indentedLines = lines.map( 20 | (line) => indent + TWO_SPACES + COLOR_STRING(line), 21 | ); 22 | return ( 23 | COLOR_STRING('"') + 24 | '\n' + 25 | indentedLines.join('\n') + 26 | '\n' + 27 | indent + 28 | COLOR_STRING('"') 29 | ); 30 | } 31 | return COLOR_STRING(`"${str}"`); 32 | }; 33 | 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | const shouldPrint = (obj: any): boolean => 36 | typeof obj !== 'function' && typeof obj !== 'symbol'; 37 | 38 | const formatJson = ( 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | obj: any, 41 | indentLevel: number = 0, 42 | handleMultiline: boolean = false, 43 | ): string => { 44 | const indent = TWO_SPACES.repeat(indentLevel); 45 | 46 | if (obj === null) return COLOR_NULL_OR_UNDEFINED('null'); 47 | 48 | if (obj === undefined) return COLOR_NULL_OR_UNDEFINED('undefined'); 49 | 50 | if (typeof obj === 'string') 51 | return processString(obj, indent, handleMultiline); 52 | if (typeof obj === 'number' || typeof obj === 'bigint' || obj instanceof Date) 53 | return COLOR_NUMBER_OR_DATE(String(obj)); 54 | if (typeof obj === 'boolean') return COLOR_BOOLEAN(String(obj)); 55 | 56 | if (obj instanceof Error) { 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | const errorObj: Record = {}; 59 | 60 | const propNames = Object.getOwnPropertyNames(obj); 61 | 62 | propNames.forEach((key) => { 63 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 64 | errorObj[key] = (obj as any)[key]; 65 | }); 66 | 67 | return formatJson(errorObj, indentLevel, handleMultiline); 68 | } 69 | 70 | if (obj instanceof Promise) { 71 | return COLOR_STRING('Promise {pending}'); 72 | } 73 | 74 | if (Array.isArray(obj)) { 75 | const arrayItems = obj.map((item) => 76 | formatJson(item, indentLevel + 1, handleMultiline), 77 | ); 78 | return `${COLOR_BRACKETS('[')}\n${indent} ${arrayItems.join( 79 | `,\n${indent} `, 80 | )}\n${indent}${COLOR_BRACKETS(']')}`; 81 | } 82 | 83 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 84 | const entries = Object.entries(obj) 85 | .filter(([_, value]) => shouldPrint(value)) 86 | .map( 87 | ([key, value]) => 88 | `${COLOR_KEY(`"${key}"`)}: ${formatJson( 89 | value, 90 | indentLevel + 1, 91 | handleMultiline, 92 | )}`, 93 | ); 94 | return `${COLOR_BRACKETS('{')}\n${indent} ${entries.join( 95 | `,\n${indent} `, 96 | )}\n${indent}${COLOR_BRACKETS('}')}`; 97 | }; 98 | 99 | export const prettyJson = ( 100 | obj: unknown, 101 | options?: { handleMultiline?: boolean }, 102 | ): string => formatJson(obj, 0, options?.handleMultiline); 103 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { describe, it } from 'node:test'; 3 | import chalk from './color'; 4 | import { prettyJson } from './pretty'; 5 | 6 | void describe('prettyPrintJson', () => { 7 | // Turn off chalk colorization during tests for easy comparison 8 | chalk.level = 0; 9 | 10 | void it('formats a simple object correctly without multiline strings', () => { 11 | const input = { 12 | name: 'John Doe', 13 | age: 30, 14 | }; 15 | 16 | const expectedOutput = `{ 17 | "name": "John Doe", 18 | "age": 30 19 | }`; 20 | 21 | const output = prettyJson(input, { handleMultiline: false }); 22 | assert.strictEqual(output, expectedOutput); 23 | }); 24 | 25 | void it('formats a simple object with multiline string handling', () => { 26 | const input = { 27 | name: 'John Doe', 28 | bio: 'This is line one.\nThis is line two.', 29 | }; 30 | 31 | const expectedOutput = `{ 32 | "name": "John Doe", 33 | "bio": " 34 | This is line one. 35 | This is line two. 36 | " 37 | }`; 38 | 39 | const output = prettyJson(input, { handleMultiline: true }); 40 | assert.strictEqual(output, expectedOutput); 41 | }); 42 | 43 | void it('formats nested objects correctly', () => { 44 | const input = { 45 | user: { 46 | name: 'Alice', 47 | age: 25, 48 | location: { 49 | city: 'Wonderland', 50 | country: 'Fiction', 51 | }, 52 | }, 53 | }; 54 | 55 | const expectedOutput = `{ 56 | "user": { 57 | "name": "Alice", 58 | "age": 25, 59 | "location": { 60 | "city": "Wonderland", 61 | "country": "Fiction" 62 | } 63 | } 64 | }`; 65 | 66 | const output = prettyJson(input, { handleMultiline: false }); 67 | assert.strictEqual(output, expectedOutput); 68 | }); 69 | 70 | void it('handles arrays and numbers correctly', () => { 71 | const input = { 72 | numbers: [1, 2, 3, 4, 5], 73 | active: true, 74 | }; 75 | 76 | const expectedOutput = `{ 77 | "numbers": [ 78 | 1, 79 | 2, 80 | 3, 81 | 4, 82 | 5 83 | ], 84 | "active": true 85 | }`; 86 | 87 | const output = prettyJson(input, { handleMultiline: false }); 88 | assert.strictEqual(output, expectedOutput); 89 | }); 90 | 91 | void it('formats an object with null values and booleans correctly', () => { 92 | const input = { 93 | name: 'Test', 94 | isActive: false, 95 | tags: null, 96 | }; 97 | 98 | const expectedOutput = `{ 99 | "name": "Test", 100 | "isActive": false, 101 | "tags": null 102 | }`; 103 | 104 | const output = prettyJson(input, { handleMultiline: false }); 105 | assert.strictEqual(output, expectedOutput); 106 | }); 107 | 108 | void it('handles multiline SQL-like queries in strings', () => { 109 | const input = { 110 | query: 111 | 'CREATE TABLE users (\n id INT PRIMARY KEY,\n name TEXT NOT NULL\n)', 112 | }; 113 | 114 | const expectedOutput = `{ 115 | "query": " 116 | CREATE TABLE users ( 117 | id INT PRIMARY KEY, 118 | name TEXT NOT NULL 119 | ) 120 | " 121 | }`; 122 | 123 | const output = prettyJson(input, { handleMultiline: true }); 124 | assert.strictEqual(output, expectedOutput); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/pg.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './storage/postgresql/core'; 3 | export * from './storage/postgresql/pg'; 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/sqlite3.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './storage/sqlite/core'; 3 | export * from './storage/sqlite/sqlite3'; 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/connections/connectionString.ts: -------------------------------------------------------------------------------- 1 | import pgcs from 'pg-connection-string'; 2 | import { defaultPostgreSqlDatabase } from '../schema'; 3 | 4 | export const defaultPostgreSQLConenctionString = 5 | 'postgresql://postgres@localhost:5432/postgres'; 6 | 7 | export const getDatabaseNameOrDefault = (connectionString: string) => 8 | pgcs.parse(connectionString).database ?? defaultPostgreSqlDatabase; 9 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/connections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connectionString'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/index.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectorType } from '../../../core'; 2 | 3 | export * from './connections'; 4 | export * from './locks'; 5 | export * from './schema'; 6 | 7 | export type PostgreSQLConnector = 'PostgreSQL'; 8 | export const PostgreSQLConnector = 'PostgreSQL'; 9 | 10 | export type PostgreSQLConnectorType = 11 | ConnectorType; 12 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/locks/advisoryLocks.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultDatabaseLockOptions, 3 | single, 4 | sql, 5 | type AcquireDatabaseLockMode, 6 | type AcquireDatabaseLockOptions, 7 | type DatabaseLock, 8 | type DatabaseLockOptions, 9 | type ReleaseDatabaseLockOptions, 10 | type SQLExecutor, 11 | } from '../../../../core'; 12 | 13 | export const tryAcquireAdvisoryLock = async ( 14 | execute: SQLExecutor, 15 | options: AcquireDatabaseLockOptions, 16 | ): Promise => { 17 | const timeoutMs = options.timeoutMs ?? defaultDatabaseLockOptions.timeoutMs; 18 | 19 | const advisoryLock = 20 | options.mode === 'Permanent' ? 'pg_advisory_lock' : 'pg_advisory_xact_lock'; 21 | 22 | try { 23 | await single( 24 | execute.query<{ locked: boolean }>( 25 | sql('SELECT %s(%s) AS locked', advisoryLock, options.lockId), 26 | { timeoutMs }, 27 | ), 28 | ); 29 | return true; 30 | } catch (error) { 31 | if (error instanceof Error && 'code' in error && error.code === '57014') 32 | return false; 33 | 34 | throw error; 35 | } 36 | }; 37 | 38 | export const releaseAdvisoryLock = async ( 39 | execute: SQLExecutor, 40 | options: ReleaseDatabaseLockOptions, 41 | ): Promise => { 42 | const timeoutMs = options.timeoutMs ?? defaultDatabaseLockOptions.timeoutMs; 43 | 44 | try { 45 | await single( 46 | execute.query<{ locked: boolean }>( 47 | sql('SELECT pg_advisory_unlock(%s) AS locked', options.lockId), 48 | { timeoutMs }, 49 | ), 50 | ); 51 | return true; 52 | } catch (error) { 53 | if (error instanceof Error && 'code' in error && error.code === '57014') 54 | return false; 55 | 56 | throw error; 57 | } 58 | }; 59 | 60 | export const acquireAdvisoryLock = async ( 61 | execute: SQLExecutor, 62 | options: AcquireDatabaseLockOptions, 63 | ) => { 64 | const lockAcquired = await tryAcquireAdvisoryLock(execute, options); 65 | if (!lockAcquired) { 66 | throw new Error( 67 | 'Failed to acquire advisory lock within the specified timeout. Migration aborted.', 68 | ); 69 | } 70 | }; 71 | 72 | export const AdvisoryLock: DatabaseLock = { 73 | acquire: acquireAdvisoryLock, 74 | tryAcquire: tryAcquireAdvisoryLock, 75 | release: releaseAdvisoryLock, 76 | withAcquire: async ( 77 | execute: SQLExecutor, 78 | handle: () => Promise, 79 | options: AcquireDatabaseLockOptions, 80 | ) => { 81 | await acquireAdvisoryLock(execute, options); 82 | try { 83 | return await handle(); 84 | } finally { 85 | if (options.mode === 'Permanent') 86 | await releaseAdvisoryLock(execute, options); 87 | } 88 | }, 89 | }; 90 | 91 | export const advisoryLock = ( 92 | execute: SQLExecutor, 93 | options: DatabaseLockOptions, 94 | ) => ({ 95 | acquire: (acquireOptions?: { mode: AcquireDatabaseLockMode }) => 96 | acquireAdvisoryLock(execute, { 97 | ...options, 98 | ...(acquireOptions ?? {}), 99 | }), 100 | tryAcquire: (acquireOptions?: { mode: AcquireDatabaseLockMode }) => 101 | tryAcquireAdvisoryLock(execute, { 102 | ...options, 103 | ...(acquireOptions ?? {}), 104 | }), 105 | release: () => releaseAdvisoryLock(execute, options), 106 | withAcquire: async ( 107 | handle: () => Promise, 108 | acquireOptions?: { mode: AcquireDatabaseLockMode }, 109 | ) => { 110 | await acquireAdvisoryLock(execute, { 111 | ...options, 112 | ...(acquireOptions ?? {}), 113 | }); 114 | try { 115 | return await handle(); 116 | } finally { 117 | await releaseAdvisoryLock(execute, options); 118 | } 119 | }, 120 | }); 121 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/locks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './advisoryLocks'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './migrations'; 2 | export * from './schema'; 3 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/schema/migrations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MIGRATIONS_LOCK_ID, 3 | rawSql, 4 | runSQLMigrations, 5 | schemaComponent, 6 | sqlMigration, 7 | type DatabaseLockOptions, 8 | type Dumbo, 9 | type SQLMigration, 10 | } from '../../../../core'; 11 | import { AdvisoryLock } from '../locks'; 12 | 13 | export type PostgreSQLMigratorOptions = { 14 | lock?: { 15 | options?: Omit & 16 | Partial>; 17 | }; 18 | dryRun?: boolean | undefined; 19 | }; 20 | 21 | const migrationTableSQL = rawSql(` 22 | CREATE TABLE IF NOT EXISTS migrations ( 23 | id SERIAL PRIMARY KEY, 24 | name VARCHAR(255) NOT NULL UNIQUE, 25 | application VARCHAR(255) NOT NULL DEFAULT 'default', 26 | sql_hash VARCHAR(64) NOT NULL, 27 | timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 28 | ); 29 | `); 30 | 31 | export const migrationTableSchemaComponent = schemaComponent( 32 | 'dumbo:schema-component:migrations-table', 33 | { 34 | migrations: () => [ 35 | sqlMigration('dumbo:migrationTable:001', [migrationTableSQL]), 36 | ], 37 | }, 38 | ); 39 | 40 | export const runPostgreSQLMigrations = ( 41 | pool: Dumbo, 42 | migrations: SQLMigration[], 43 | options?: PostgreSQLMigratorOptions, 44 | ): Promise => 45 | runSQLMigrations(pool, migrations, { 46 | schema: { 47 | migrationTable: migrationTableSchemaComponent, 48 | }, 49 | lock: { 50 | databaseLock: AdvisoryLock, 51 | options: { 52 | ...(options ?? {}), 53 | lockId: MIGRATIONS_LOCK_ID, 54 | }, 55 | }, 56 | dryRun: options?.dryRun, 57 | }); 58 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/core/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import { exists, sql, type ConnectionPool, type SQL } from '../../../../core'; 2 | export * from './schema'; 3 | 4 | export const defaultPostgreSqlDatabase = 'postgres'; 5 | 6 | export const tableExistsSQL = (tableName: string): SQL => 7 | sql( 8 | ` 9 | SELECT EXISTS ( 10 | SELECT FROM pg_tables 11 | WHERE tablename = %L 12 | ) AS exists;`, 13 | tableName, 14 | ); 15 | 16 | export const tableExists = async ( 17 | pool: ConnectionPool, 18 | tableName: string, 19 | ): Promise => exists(pool.execute.query(tableExistsSQL(tableName))); 20 | 21 | export const functionExistsSQL = (functionName: string): SQL => 22 | sql( 23 | ` 24 | SELECT EXISTS ( 25 | SELECT FROM pg_proc 26 | WHERE 27 | proname = %L 28 | ) AS exists; 29 | `, 30 | functionName, 31 | ); 32 | 33 | export const functionExists = async ( 34 | pool: ConnectionPool, 35 | tableName: string, 36 | ): Promise => exists(pool.execute.query(functionExistsSQL(tableName))); 37 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/connections/connection.ts: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | import { createConnection, type Connection } from '../../../../core'; 3 | import type { PostgreSQLConnectorType } from '../../core'; 4 | import { nodePostgresSQLExecutor } from '../execute'; 5 | import { nodePostgresTransaction } from './transaction'; 6 | 7 | export type NodePostgresConnector = PostgreSQLConnectorType<'pg'>; 8 | export const NodePostgresConnectorType: NodePostgresConnector = 'PostgreSQL:pg'; 9 | 10 | export type NodePostgresPoolClient = pg.PoolClient; 11 | export type NodePostgresClient = pg.Client; 12 | 13 | export type NodePostgresClientOrPoolClient = 14 | | NodePostgresPoolClient 15 | | NodePostgresClient; 16 | 17 | export type NodePostgresPoolOrClient = 18 | | pg.Pool 19 | | NodePostgresPoolClient 20 | | NodePostgresClient; 21 | 22 | export type NodePostgresClientConnection = Connection< 23 | NodePostgresConnector, 24 | NodePostgresClient 25 | >; 26 | 27 | export type NodePostgresPoolClientConnection = Connection< 28 | NodePostgresConnector, 29 | NodePostgresPoolClient 30 | >; 31 | 32 | export type NodePostgresConnection = 33 | | NodePostgresPoolClientConnection 34 | | NodePostgresClientConnection; 35 | 36 | export type NodePostgresPoolClientOptions = { 37 | type: 'PoolClient'; 38 | connect: Promise; 39 | close: (client: NodePostgresPoolClient) => Promise; 40 | }; 41 | 42 | export type NodePostgresClientOptions = { 43 | type: 'Client'; 44 | connect: Promise; 45 | close: (client: NodePostgresClient) => Promise; 46 | }; 47 | 48 | export const nodePostgresClientConnection = ( 49 | options: NodePostgresClientOptions, 50 | ): NodePostgresClientConnection => { 51 | const { connect, close } = options; 52 | 53 | return createConnection({ 54 | connector: NodePostgresConnectorType, 55 | connect, 56 | close, 57 | initTransaction: (connection) => nodePostgresTransaction(connection), 58 | executor: nodePostgresSQLExecutor, 59 | }); 60 | }; 61 | 62 | export const nodePostgresPoolClientConnection = ( 63 | options: NodePostgresPoolClientOptions, 64 | ): NodePostgresPoolClientConnection => { 65 | const { connect, close } = options; 66 | 67 | return createConnection({ 68 | connector: NodePostgresConnectorType, 69 | connect, 70 | close, 71 | initTransaction: (connection) => nodePostgresTransaction(connection), 72 | executor: nodePostgresSQLExecutor, 73 | }); 74 | }; 75 | 76 | export function nodePostgresConnection( 77 | options: NodePostgresPoolClientOptions, 78 | ): NodePostgresPoolClientConnection; 79 | export function nodePostgresConnection( 80 | options: NodePostgresClientOptions, 81 | ): NodePostgresClientConnection; 82 | export function nodePostgresConnection( 83 | options: NodePostgresPoolClientOptions | NodePostgresClientOptions, 84 | ): NodePostgresPoolClientConnection | NodePostgresClientConnection { 85 | return options.type === 'Client' 86 | ? nodePostgresClientConnection(options) 87 | : nodePostgresPoolClientConnection(options); 88 | } 89 | 90 | export type ConnectionCheckResult = 91 | | { successful: true } 92 | | { 93 | successful: false; 94 | code: string | undefined; 95 | errorType: 'ConnectionRefused' | 'Authentication' | 'Unknown'; 96 | error: unknown; 97 | }; 98 | 99 | export const checkConnection = async ( 100 | connectionString: string, 101 | ): Promise => { 102 | const client = new pg.Client({ 103 | connectionString, 104 | }); 105 | 106 | try { 107 | await client.connect(); 108 | return { successful: true }; 109 | } catch (error) { 110 | const code = 111 | error instanceof Error && 112 | 'code' in error && 113 | typeof error.code === 'string' 114 | ? error.code 115 | : undefined; 116 | 117 | return { 118 | successful: false, 119 | errorType: 120 | code === 'ECONNREFUSED' 121 | ? 'ConnectionRefused' 122 | : code === '28P01' 123 | ? 'Authentication' 124 | : 'Unknown', 125 | code, 126 | error, 127 | }; 128 | } finally { 129 | // Ensure the client is closed properly if connected 130 | await client.end(); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/connections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | export * from './pool'; 3 | export * from './transaction'; 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/connections/transaction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sqlExecutor, 3 | type Connection, 4 | type DatabaseTransaction, 5 | } from '../../../../core'; 6 | import { nodePostgresSQLExecutor } from '../execute'; 7 | import { 8 | NodePostgresConnectorType, 9 | type NodePostgresConnector, 10 | type NodePostgresPoolOrClient, 11 | } from './connection'; 12 | 13 | export type NodePostgresTransaction = 14 | DatabaseTransaction; 15 | 16 | export const nodePostgresTransaction = 17 | ( 18 | connection: () => Connection, 19 | ) => 20 | ( 21 | getClient: Promise, 22 | options?: { close: (client: DbClient, error?: unknown) => Promise }, 23 | ): DatabaseTransaction => ({ 24 | connection: connection(), 25 | connector: NodePostgresConnectorType, 26 | begin: async () => { 27 | const client = await getClient; 28 | await client.query('BEGIN'); 29 | }, 30 | commit: async () => { 31 | const client = await getClient; 32 | 33 | await client.query('COMMIT'); 34 | 35 | if (options?.close) await options?.close(client); 36 | }, 37 | rollback: async (error?: unknown) => { 38 | const client = await getClient; 39 | await client.query('ROLLBACK'); 40 | 41 | if (options?.close) await options?.close(client, error); 42 | }, 43 | execute: sqlExecutor(nodePostgresSQLExecutor(), { 44 | connect: () => getClient, 45 | }), 46 | }); 47 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/execute/execute.ts: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | import { 3 | tracer, 4 | type DbSQLExecutor, 5 | type QueryResult, 6 | type QueryResultRow, 7 | type SQL, 8 | } from '../../../../core'; 9 | import { 10 | NodePostgresConnectorType, 11 | type NodePostgresClientOrPoolClient, 12 | type NodePostgresConnector, 13 | } from '../connections'; 14 | 15 | export const isNodePostgresNativePool = ( 16 | poolOrClient: pg.Pool | pg.PoolClient | pg.Client, 17 | ): poolOrClient is pg.Pool => { 18 | return poolOrClient instanceof pg.Pool; 19 | }; 20 | 21 | export const isNodePostgresClient = ( 22 | poolOrClient: pg.Pool | pg.PoolClient | pg.Client, 23 | ): poolOrClient is pg.Client => poolOrClient instanceof pg.Client; 24 | 25 | export const isNodePostgresPoolClient = ( 26 | poolOrClient: pg.Pool | pg.PoolClient | pg.Client, 27 | ): poolOrClient is pg.PoolClient => 28 | 'release' in poolOrClient && typeof poolOrClient.release === 'function'; 29 | 30 | export const nodePostgresExecute = async ( 31 | poolOrClient: pg.Pool | pg.PoolClient | pg.Client, 32 | handle: (client: pg.PoolClient | pg.Client) => Promise, 33 | ) => { 34 | const client = isNodePostgresNativePool(poolOrClient) 35 | ? await poolOrClient.connect() 36 | : poolOrClient; 37 | 38 | try { 39 | return await handle(client); 40 | } finally { 41 | // release only if client wasn't injected externally 42 | if ( 43 | isNodePostgresNativePool(poolOrClient) && 44 | isNodePostgresPoolClient(client) 45 | ) 46 | client.release(); 47 | } 48 | }; 49 | 50 | export type NodePostgresSQLExecutor = DbSQLExecutor< 51 | NodePostgresConnector, 52 | NodePostgresClientOrPoolClient 53 | >; 54 | 55 | export const nodePostgresSQLExecutor = (): NodePostgresSQLExecutor => ({ 56 | connector: NodePostgresConnectorType, 57 | query: batch, 58 | batchQuery: batch, 59 | command: batch, 60 | batchCommand: batch, 61 | }); 62 | 63 | export type BatchQueryOptions = { timeoutMs?: number }; 64 | 65 | function batch( 66 | client: NodePostgresClientOrPoolClient, 67 | sqlOrSqls: SQL, 68 | options?: BatchQueryOptions, 69 | ): Promise>; 70 | function batch( 71 | client: NodePostgresClientOrPoolClient, 72 | sqlOrSqls: SQL[], 73 | options?: BatchQueryOptions, 74 | ): Promise[]>; 75 | async function batch( 76 | client: NodePostgresClientOrPoolClient, 77 | sqlOrSqls: SQL | SQL[], 78 | options?: BatchQueryOptions, 79 | ): Promise | QueryResult[]> { 80 | const sqls = Array.isArray(sqlOrSqls) ? sqlOrSqls : [sqlOrSqls]; 81 | const results: QueryResult[] = Array>( 82 | sqls.length, 83 | ); 84 | 85 | if (options?.timeoutMs) { 86 | await client.query(`SET statement_timeout = ${options?.timeoutMs}`); 87 | } 88 | 89 | //TODO: make it smarter at some point 90 | for (let i = 0; i < sqls.length; i++) { 91 | tracer.info('db:sql:query', { sql: sqls[i]! }); 92 | const result = await client.query(sqls[i]!); 93 | results[i] = { rowCount: result.rowCount, rows: result.rows }; 94 | } 95 | return Array.isArray(sqlOrSqls) ? results : results[0]!; 96 | } 97 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/execute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './execute'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/index.ts: -------------------------------------------------------------------------------- 1 | import type { Dumbo, DumboOptions } from '../../../core'; 2 | import { 3 | type NodePostgresConnection, 4 | type NodePostgresConnector, 5 | type NodePostgresPool, 6 | nodePostgresPool, 7 | type NodePostgresPoolOptions, 8 | } from './connections'; 9 | 10 | export type PostgresConnector = NodePostgresConnector; 11 | export type PostgresPool = NodePostgresPool; 12 | export type PostgresConnection = NodePostgresConnection; 13 | 14 | export type PostgresPoolOptions = DumboOptions & 15 | NodePostgresPoolOptions; 16 | export const postgresPool = nodePostgresPool; 17 | 18 | export const connectionPool = postgresPool; 19 | 20 | export const dumbo = < 21 | DumboOptionsType extends PostgresPoolOptions = PostgresPoolOptions, 22 | >( 23 | options: DumboOptionsType, 24 | ): Dumbo => connectionPool(options); 25 | 26 | export * from './connections'; 27 | export * from './execute'; 28 | export * from './serialization'; 29 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/postgresql/pg/serialization/index.ts: -------------------------------------------------------------------------------- 1 | import pg from 'pg'; 2 | import { JSONSerializer } from '../../../../core/serializer'; 3 | 4 | export const setNodePostgresTypeParser = (jsonSerializer: JSONSerializer) => { 5 | // BigInt 6 | pg.types.setTypeParser(20, (val) => BigInt(val)); 7 | 8 | // JSONB 9 | pg.types.setTypeParser(3802, (val) => jsonSerializer.deserialize(val)); 10 | 11 | // JSON 12 | pg.types.setTypeParser(114, (val) => jsonSerializer.deserialize(val)); 13 | }; 14 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/connections/index.ts: -------------------------------------------------------------------------------- 1 | import { sqliteSQLExecutor, type SQLiteConnectorType } from '..'; 2 | import { createConnection, type Connection } from '../../../../core'; 3 | import { sqliteTransaction } from '../transactions'; 4 | 5 | export type Parameters = object | string | bigint | number | boolean | null; 6 | 7 | export type SQLiteClient = { 8 | close: () => Promise; 9 | command: (sql: string, values?: Parameters[]) => Promise; 10 | query: (sql: string, values?: Parameters[]) => Promise; 11 | querySingle: (sql: string, values?: Parameters[]) => Promise; 12 | }; 13 | 14 | export type SQLitePoolClient = { 15 | release: () => void; 16 | command: (sql: string, values?: Parameters[]) => Promise; 17 | query: (sql: string, values?: Parameters[]) => Promise; 18 | querySingle: (sql: string, values?: Parameters[]) => Promise; 19 | }; 20 | 21 | export type SQLiteClientFactory = ( 22 | options: SQLiteClientOptions, 23 | ) => SQLiteClient; 24 | 25 | export type SQLiteClientOrPoolClient = SQLitePoolClient | SQLiteClient; 26 | 27 | export interface SQLiteError extends Error { 28 | errno: number; 29 | } 30 | 31 | export const isSQLiteError = (error: unknown): error is SQLiteError => { 32 | if (error instanceof Error && 'code' in error) { 33 | return true; 34 | } 35 | 36 | return false; 37 | }; 38 | 39 | export type SQLitePoolConnectionOptions< 40 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 41 | > = { 42 | connector: ConnectorType; 43 | type: 'PoolClient'; 44 | connect: Promise; 45 | close: (client: SQLitePoolClient) => Promise; 46 | }; 47 | 48 | export type SQLiteClientConnectionOptions< 49 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 50 | > = { 51 | connector: ConnectorType; 52 | type: 'Client'; 53 | connect: Promise; 54 | close: (client: SQLiteClient) => Promise; 55 | }; 56 | 57 | export type SQLiteClientConnection< 58 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 59 | > = Connection; 60 | 61 | export type SQLitePoolClientConnection< 62 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 63 | > = Connection; 64 | 65 | export type SQLiteConnection< 66 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 67 | > = 68 | | SQLiteClientConnection 69 | | SQLitePoolClientConnection; 70 | 71 | export const sqliteClientConnection = < 72 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 73 | >( 74 | options: SQLiteClientConnectionOptions, 75 | ): SQLiteClientConnection => { 76 | const { connect, close } = options; 77 | 78 | return createConnection({ 79 | connector: options.connector, 80 | connect, 81 | close, 82 | initTransaction: (connection) => 83 | sqliteTransaction(options.connector, connection), 84 | executor: () => sqliteSQLExecutor(options.connector), 85 | }); 86 | }; 87 | 88 | export const sqlitePoolClientConnection = < 89 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 90 | >( 91 | options: SQLitePoolConnectionOptions, 92 | ): SQLitePoolClientConnection => { 93 | const { connect, close } = options; 94 | 95 | return createConnection({ 96 | connector: options.connector, 97 | connect, 98 | close, 99 | initTransaction: (connection) => 100 | sqliteTransaction(options.connector, connection), 101 | executor: () => sqliteSQLExecutor(options.connector), 102 | }); 103 | }; 104 | 105 | export function sqliteConnection< 106 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 107 | >( 108 | options: SQLitePoolConnectionOptions, 109 | ): SQLitePoolClientConnection; 110 | export function sqliteConnection< 111 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 112 | >( 113 | options: SQLiteClientConnectionOptions, 114 | ): SQLiteClientConnection; 115 | export function sqliteConnection< 116 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 117 | >( 118 | options: 119 | | SQLitePoolConnectionOptions 120 | | SQLiteClientConnectionOptions, 121 | ): 122 | | SQLitePoolClientConnection 123 | | SQLiteClientConnection { 124 | return options.type === 'Client' 125 | ? sqliteClientConnection(options) 126 | : sqlitePoolClientConnection(options); 127 | } 128 | 129 | export type InMemorySQLiteDatabase = ':memory:'; 130 | export const InMemorySQLiteDatabase = ':memory:'; 131 | 132 | export type SQLiteClientOptions = { 133 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents 134 | fileName: InMemorySQLiteDatabase | string | undefined; 135 | }; 136 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/execute/execute.ts: -------------------------------------------------------------------------------- 1 | import type { SQLiteConnectorType } from '..'; 2 | import { 3 | SQL, 4 | tracer, 5 | type DbSQLExecutor, 6 | type QueryResult, 7 | type QueryResultRow, 8 | } from '../../../../core'; 9 | import type { SQLiteClient } from '../connections'; 10 | 11 | export const sqliteExecute = async ( 12 | database: SQLiteClient, 13 | handle: (client: SQLiteClient) => Promise, 14 | ) => { 15 | try { 16 | return await handle(database); 17 | } finally { 18 | await database.close(); 19 | } 20 | }; 21 | 22 | export type SQLiteSQLExecutor< 23 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 24 | > = DbSQLExecutor; 25 | 26 | export const sqliteSQLExecutor = < 27 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 28 | >( 29 | connector: ConnectorType, 30 | ): SQLiteSQLExecutor => ({ 31 | connector, 32 | query: batch, 33 | batchQuery: batch, 34 | command: batch, 35 | batchCommand: batch, 36 | }); 37 | 38 | export type BatchQueryOptions = { timeoutMs?: number }; 39 | 40 | function batch( 41 | client: SQLiteClient, 42 | sqlOrSqls: SQL, 43 | options?: BatchQueryOptions, 44 | ): Promise>; 45 | function batch( 46 | client: SQLiteClient, 47 | sqlOrSqls: SQL[], 48 | options?: BatchQueryOptions, 49 | ): Promise[]>; 50 | async function batch( 51 | client: SQLiteClient, 52 | sqlOrSqls: SQL | SQL[], 53 | options?: BatchQueryOptions, 54 | ): Promise | QueryResult[]> { 55 | const sqls = Array.isArray(sqlOrSqls) ? sqlOrSqls : [sqlOrSqls]; 56 | const results: QueryResult[] = Array>( 57 | sqls.length, 58 | ); 59 | 60 | if (options?.timeoutMs) { 61 | // TODO: This is not precisely timeout 62 | // SQLite's busy_timeout determines how long SQLite will wait 63 | // when the database is locked before returning 64 | // a "database is locked" error 65 | await client.query(`PRAGMA busy_timeout = ${options?.timeoutMs}`); 66 | } 67 | 68 | //TODO: make it smarter at some point 69 | for (let i = 0; i < sqls.length; i++) { 70 | tracer.info('db:sql:query', { sql: sqls[i]! }); 71 | 72 | const result = await client.query(sqls[i]!); 73 | 74 | results[i] = { rowCount: result.length, rows: result }; 75 | } 76 | return Array.isArray(sqlOrSqls) ? results : results[0]!; 77 | } 78 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/execute/index.ts: -------------------------------------------------------------------------------- 1 | export * from './execute'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/index.ts: -------------------------------------------------------------------------------- 1 | import { getDriverName, type ConnectorType } from '../../..'; 2 | import type { SQLiteClientFactory } from './connections'; 3 | 4 | export * from './connections'; 5 | export * from './execute'; 6 | export * from './pool'; 7 | export * from './transactions'; 8 | 9 | export type SQLiteConnector = 'SQLite'; 10 | export const SQLiteConnector = 'SQLite'; 11 | 12 | export type SQLiteConnectorType = 13 | ConnectorType; 14 | 15 | export type SQLiteDatabaseType = 'SQLite'; 16 | 17 | export const sqliteClientProvider = async < 18 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 19 | >( 20 | connector: ConnectorType, 21 | ): Promise => { 22 | const driverName = getDriverName(connector); 23 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 24 | const driverModule = await import(`../${driverName.toLowerCase()}`); 25 | 26 | if (!('sqliteClient' in driverModule)) 27 | throw new Error( 28 | `The connector module "${connector}" does not export a sqliteClient`, 29 | ); 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 32 | return driverModule.sqliteClient as SQLiteClientFactory; 33 | }; 34 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/pool/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pool'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/core/transactions/index.ts: -------------------------------------------------------------------------------- 1 | import type { SQLiteConnectorType } from '..'; 2 | import { 3 | sqlExecutor, 4 | type Connection, 5 | type DatabaseTransaction, 6 | } from '../../../../core'; 7 | import { sqliteSQLExecutor } from '../../core/execute'; 8 | import type { SQLiteClientOrPoolClient } from '../connections'; 9 | 10 | export type SQLiteTransaction< 11 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 12 | > = DatabaseTransaction; 13 | 14 | export const sqliteTransaction = 15 | < 16 | ConnectorType extends SQLiteConnectorType = SQLiteConnectorType, 17 | DbClient extends SQLiteClientOrPoolClient = SQLiteClientOrPoolClient, 18 | >( 19 | connector: ConnectorType, 20 | connection: () => Connection, 21 | ) => 22 | ( 23 | getClient: Promise, 24 | options?: { close: (client: DbClient, error?: unknown) => Promise }, 25 | ): DatabaseTransaction => ({ 26 | connection: connection(), 27 | connector, 28 | begin: async () => { 29 | const client = await getClient; 30 | await client.query('BEGIN TRANSACTION'); 31 | }, 32 | commit: async () => { 33 | const client = await getClient; 34 | 35 | await client.query('COMMIT'); 36 | 37 | if (options?.close) await options?.close(client); 38 | }, 39 | rollback: async (error?: unknown) => { 40 | const client = await getClient; 41 | await client.query('ROLLBACK'); 42 | 43 | if (options?.close) await options?.close(client, error); 44 | }, 45 | execute: sqlExecutor(sqliteSQLExecutor(connector), { 46 | connect: () => getClient, 47 | }), 48 | }); 49 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/sqlite3/connections/connection.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import type { SQLiteConnectorType } from '../../core'; 3 | import { 4 | InMemorySQLiteDatabase, 5 | type Parameters, 6 | type SQLiteClient, 7 | type SQLiteClientOptions, 8 | } from '../../core/connections'; 9 | 10 | export type SQLite3Connector = SQLiteConnectorType<'sqlite3'>; 11 | export const SQLite3ConnectorType: SQLite3Connector = 'SQLite:sqlite3'; 12 | 13 | export type ConnectionCheckResult = 14 | | { successful: true } 15 | | { 16 | successful: false; 17 | code: string | undefined; 18 | errorType: 'ConnectionRefused' | 'Authentication' | 'Unknown'; 19 | error: unknown; 20 | }; 21 | 22 | export const sqlite3Client = (options: SQLiteClientOptions): SQLiteClient => { 23 | const db = new sqlite3.Database(options.fileName ?? InMemorySQLiteDatabase); 24 | 25 | return { 26 | close: (): Promise => { 27 | db.close(); 28 | return Promise.resolve(); 29 | }, 30 | command: (sql: string, params?: Parameters[]) => 31 | new Promise((resolve, reject) => { 32 | db.run(sql, params ?? [], (err: Error | null) => { 33 | if (err) { 34 | reject(err); 35 | return; 36 | } 37 | 38 | resolve(); 39 | }); 40 | }), 41 | query: (sql: string, params?: Parameters[]): Promise => 42 | new Promise((resolve, reject) => { 43 | db.all(sql, params ?? [], (err: Error | null, result: T[]) => { 44 | if (err) { 45 | reject(err); 46 | return; 47 | } 48 | 49 | resolve(result); 50 | }); 51 | }), 52 | querySingle: (sql: string, params?: Parameters[]): Promise => 53 | new Promise((resolve, reject) => { 54 | db.get(sql, params ?? [], (err: Error | null, result: T | null) => { 55 | if (err) { 56 | reject(err); 57 | return; 58 | } 59 | 60 | resolve(result); 61 | }); 62 | }), 63 | }; 64 | }; 65 | 66 | export const checkConnection = async ( 67 | fileName: string, 68 | ): Promise => { 69 | const client = sqlite3Client({ 70 | fileName, 71 | }); 72 | 73 | try { 74 | await client.querySingle('SELECT 1'); 75 | return { successful: true }; 76 | } catch (error) { 77 | const code = 78 | error instanceof Error && 79 | 'code' in error && 80 | typeof error.code === 'string' 81 | ? error.code 82 | : undefined; 83 | 84 | return { 85 | successful: false, 86 | errorType: 87 | code === 'SQLITE_CANTOPEN' 88 | ? 'ConnectionRefused' 89 | : code === 'SQLITE_AUTH' 90 | ? 'Authentication' 91 | : 'Unknown', 92 | code, 93 | error, 94 | }; 95 | } finally { 96 | await client.close(); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/sqlite3/connections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | -------------------------------------------------------------------------------- /src/packages/dumbo/src/storage/sqlite/sqlite3/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connections'; 2 | import type { Dumbo } from '../../../core'; 3 | import { 4 | sqlitePool, 5 | type SQLiteConnection, 6 | type SQLitePoolOptions, 7 | } from '../core'; 8 | import { 9 | sqlite3Client as sqliteClient, 10 | type SQLite3Connector, 11 | } from './connections'; 12 | 13 | export { sqliteClient }; 14 | 15 | export const connectionPool = sqlitePool; 16 | 17 | export const dumbo = < 18 | DumboOptionsType extends 19 | SQLitePoolOptions = SQLitePoolOptions, 20 | >( 21 | options: DumboOptionsType, 22 | ): Dumbo> => 23 | sqlitePool(options); 24 | -------------------------------------------------------------------------------- /src/packages/dumbo/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/packages/dumbo/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/packages/dumbo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared.json", 3 | "include": ["./src/**/**/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "outDir": "./dist" /* Redirect output structure to the directory. */, 7 | "rootDir": "./src", 8 | "paths": {} 9 | }, 10 | "references": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/packages/dumbo/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const env = process.env.NODE_ENV; 4 | 5 | export default defineConfig({ 6 | splitting: true, 7 | clean: true, // clean up the dist folder 8 | dts: true, // generate dts files 9 | format: ['esm', 'cjs'], // generate cjs and esm files 10 | minify: false, //env === 'production', 11 | bundle: true, //env === 'production', 12 | skipNodeModulesBundle: true, 13 | watch: env === 'development', 14 | target: 'esnext', 15 | outDir: 'dist', //env === 'production' ? 'dist' : 'lib', 16 | entry: ['src/index.ts', 'src/pg.ts', 'src/sqlite3.ts'], 17 | //entry: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/*.internal.ts'], //include all files under src but not specs 18 | sourcemap: true, 19 | tsconfig: 'tsconfig.build.json', // workaround for https://github.com/egoist/tsup/issues/571#issuecomment-1760052931 20 | }); 21 | -------------------------------------------------------------------------------- /src/packages/pongo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@event-driven-io/pongo", 3 | "version": "0.17.0-alpha.3", 4 | "description": "Pongo - Mongo with strong consistency on top of Postgres", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup", 8 | "build:ts": "tsc -b", 9 | "build:ts:watch": "tsc -b --watch", 10 | "test": "run-s test:unit test:int test:e2e", 11 | "test:unit": "glob -c \"node --import tsx --test\" **/*.unit.spec.ts", 12 | "test:int": "glob -c \"node --import tsx --test\" **/*.int.spec.ts", 13 | "test:e2e": "glob -c \"node --import tsx --test\" **/*.e2e.spec.ts", 14 | "test:watch": "node --import tsx --test --watch", 15 | "test:unit:watch": "glob -c \"node --import tsx --test --watch\" **/*.unit.spec.ts", 16 | "test:int:watch": "glob -c \"node --import tsx --test --watch\" **/*.int.spec.ts", 17 | "test:e2e:watch": "glob -c \"node --import tsx --test --watch\" **/*.e2e.spec.ts", 18 | "cli:sql:print": "tsx src/cli.ts migrate sql --collection users", 19 | "cli:migrate:dryRun": "tsx src/cli.ts migrate run --config src/e2e/cli-config.ts -cs postgresql://postgres:postgres@localhost:5432/postgres", 20 | "cli:config:print": "tsx src/cli.ts config sample --print", 21 | "cli:config:generate": "tsx src/cli.ts config sample --generate --file ./pongoConfig.ts " 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/event-driven-io/Pongo.git" 26 | }, 27 | "keywords": [ 28 | "Event Sourcing" 29 | ], 30 | "author": "Oskar Dudycz", 31 | "bugs": { 32 | "url": "https://github.com/event-driven-io/Pongo/issues" 33 | }, 34 | "homepage": "https://event-driven-io.github.io/Pongo/", 35 | "exports": { 36 | ".": { 37 | "import": { 38 | "types": "./dist/index.d.ts", 39 | "default": "./dist/index.js" 40 | }, 41 | "require": { 42 | "types": "./dist/index.d.cts", 43 | "default": "./dist/index.cjs" 44 | } 45 | }, 46 | "./shim": { 47 | "import": { 48 | "types": "./dist/shim.d.ts", 49 | "default": "./dist/shim.js" 50 | }, 51 | "require": { 52 | "types": "./dist/shim.d.cts", 53 | "default": "./dist/shim.cjs" 54 | } 55 | }, 56 | "./cli": { 57 | "import": { 58 | "types": "./dist/cli.d.ts", 59 | "default": "./dist/cli.js" 60 | }, 61 | "require": { 62 | "types": "./dist/cli.d.cts", 63 | "default": "./dist/cli.cjs" 64 | } 65 | }, 66 | "./pg": { 67 | "import": { 68 | "types": "./dist/pg.d.ts", 69 | "default": "./dist/pg.js" 70 | }, 71 | "require": { 72 | "types": "./dist/pg.d.cts", 73 | "default": "./dist/pg.cjs" 74 | } 75 | }, 76 | "./sqlite3": { 77 | "import": { 78 | "types": "./dist/sqlite3.d.ts", 79 | "default": "./dist/sqlite3.js" 80 | }, 81 | "require": { 82 | "types": "./dist/sqlite3.d.cts", 83 | "default": "./dist/sqlite3.cjs" 84 | } 85 | } 86 | }, 87 | "typesVersions": { 88 | "*": { 89 | ".": [ 90 | "./dist/index.d.ts" 91 | ], 92 | "shim": [ 93 | "./dist/shim.d.ts" 94 | ], 95 | "cli": [ 96 | "./dist/cli.d.ts" 97 | ], 98 | "pg": [ 99 | "./dist/pg.d.ts" 100 | ], 101 | "sqlite3": [ 102 | "./dist/sqlite3.d.ts" 103 | ] 104 | } 105 | }, 106 | "main": "./dist/index.cjs", 107 | "module": "./dist/index.js", 108 | "types": "./dist/index.d.ts", 109 | "files": [ 110 | "dist" 111 | ], 112 | "bin": { 113 | "pongo": "./dist/cli.js" 114 | }, 115 | "peerDependencies": { 116 | "@event-driven-io/dumbo": "0.13.0-alpha.3", 117 | "@types/mongodb": "^4.0.7", 118 | "@types/pg": "^8.11.11", 119 | "@types/sqlite3": "^5.1.0", 120 | "@types/uuid": "^10.0.0", 121 | "pg": "^8.13.3", 122 | "uuid": "^11.1.0", 123 | "ansis": "^3.17.0", 124 | "cli-table3": "^0.6.5", 125 | "commander": "^13.1.0", 126 | "pg-connection-string": "^2.7.0", 127 | "sqlite3": "^5.1.7" 128 | }, 129 | "devDependencies": { 130 | "@types/node": "^22.13.10" 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/packages/pongo/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander'; 3 | import { configCommand, migrateCommand, shellCommand } from './commandLine'; 4 | 5 | const program = new Command(); 6 | 7 | program.name('pongo').description('CLI tool for Pongo'); 8 | 9 | program.addCommand(configCommand); 10 | program.addCommand(migrateCommand); 11 | program.addCommand(shellCommand); 12 | 13 | program.parse(process.argv); 14 | 15 | export default program; 16 | -------------------------------------------------------------------------------- /src/packages/pongo/src/commandLine/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configFile'; 2 | export * from './migrate'; 3 | export * from './shell'; 4 | -------------------------------------------------------------------------------- /src/packages/pongo/src/commandLine/migrate.ts: -------------------------------------------------------------------------------- 1 | import { combineMigrations } from '@event-driven-io/dumbo'; 2 | import { 3 | dumbo, 4 | migrationTableSchemaComponent, 5 | runPostgreSQLMigrations, 6 | } from '@event-driven-io/dumbo/pg'; 7 | import { Command } from 'commander'; 8 | import { pongoCollectionSchemaComponent } from '../core'; 9 | import { loadConfigFile } from './configFile'; 10 | 11 | interface MigrateRunOptions { 12 | collection: string[]; 13 | connectionString: string; 14 | config?: string; 15 | dryRun?: boolean; 16 | } 17 | 18 | interface MigrateSqlOptions { 19 | print?: boolean; 20 | write?: string; 21 | config?: string; 22 | collection: string[]; 23 | } 24 | 25 | export const migrateCommand = new Command('migrate').description( 26 | 'Manage database migrations', 27 | ); 28 | 29 | migrateCommand 30 | .command('run') 31 | .description('Run database migrations') 32 | .option( 33 | '-cs, --connection-string ', 34 | 'Connection string for the database', 35 | ) 36 | .option( 37 | '-col, --collection ', 38 | 'Specify the collection name', 39 | (value: string, previous: string[]) => { 40 | // Accumulate collection names into an array (explicitly typing `previous` as `string[]`) 41 | return previous.concat([value]); 42 | }, 43 | [] as string[], 44 | ) 45 | .option('-f, --config ', 'Path to configuration file with Pongo config') 46 | .option('-dr, --dryRun', 'Perform dry run without commiting changes', false) 47 | .action(async (options: MigrateRunOptions) => { 48 | const { collection, dryRun } = options; 49 | const connectionString = 50 | options.connectionString ?? process.env.DB_CONNECTION_STRING; 51 | let collectionNames: string[]; 52 | 53 | if (!connectionString) { 54 | console.error( 55 | 'Error: Connection string is required. Provide it either as a "--connection-string" parameter or through the DB_CONNECTION_STRING environment variable.' + 56 | '\nFor instance: --connection-string postgresql://postgres:postgres@localhost:5432/postgres', 57 | ); 58 | process.exit(1); 59 | } 60 | 61 | if (options.config) { 62 | const config = await loadConfigFile(options.config); 63 | 64 | collectionNames = config.collections.map((c) => c.name); 65 | } else if (collection) { 66 | collectionNames = collection; 67 | } else { 68 | console.error( 69 | 'Error: You need to provide at least one collection name. Provide it either through "--config" file or as a "--collection" parameter.', 70 | ); 71 | process.exit(1); 72 | } 73 | 74 | const pool = dumbo({ connectionString }); 75 | 76 | const migrations = collectionNames.flatMap((collectionsName) => 77 | pongoCollectionSchemaComponent(collectionsName).migrations({ 78 | connector: 'PostgreSQL:pg', // TODO: Provide connector here 79 | }), 80 | ); 81 | 82 | await runPostgreSQLMigrations(pool, migrations, { 83 | dryRun, 84 | }); 85 | }); 86 | 87 | migrateCommand 88 | .command('sql') 89 | .description('Generate SQL for database migration') 90 | .option( 91 | '-col, --collection ', 92 | 'Specify the collection name', 93 | (value: string, previous: string[]) => { 94 | // Accumulate collection names into an array (explicitly typing `previous` as `string[]`) 95 | return previous.concat([value]); 96 | }, 97 | [] as string[], 98 | ) 99 | .option('-f, --config ', 'Path to configuration file with Pongo config') 100 | .option('--print', 'Print the SQL to the console (default)', true) 101 | //.option('--write ', 'Write the SQL to a specified file') 102 | .action(async (options: MigrateSqlOptions) => { 103 | const { collection } = options; 104 | 105 | let collectionNames: string[]; 106 | 107 | if (options.config) { 108 | const config = await loadConfigFile(options.config); 109 | 110 | collectionNames = config.collections.map((c) => c.name); 111 | } else if (collection) { 112 | collectionNames = collection; 113 | } else { 114 | console.error( 115 | 'Error: You need to provide at least one collection name. Provide it either through "--config" file or as a "--collection" parameter.', 116 | ); 117 | process.exit(1); 118 | } 119 | 120 | const coreMigrations = migrationTableSchemaComponent.migrations({ 121 | connector: 'PostgreSQL:pg', 122 | }); 123 | const migrations = [ 124 | ...coreMigrations, 125 | ...collectionNames.flatMap((collectionName) => 126 | pongoCollectionSchemaComponent(collectionName).migrations({ 127 | connector: 'PostgreSQL:pg', // TODO: Provide connector here 128 | }), 129 | ), 130 | ]; 131 | 132 | console.log('Printing SQL:'); 133 | console.log(combineMigrations(...migrations)); 134 | }); 135 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/collection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pongoCollection'; 2 | export * from './query'; 3 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/collection/query.ts: -------------------------------------------------------------------------------- 1 | export const QueryOperators = { 2 | $eq: '$eq', 3 | $gt: '$gt', 4 | $gte: '$gte', 5 | $lt: '$lt', 6 | $lte: '$lte', 7 | $ne: '$ne', 8 | $in: '$in', 9 | $nin: '$nin', 10 | $elemMatch: '$elemMatch', 11 | $all: '$all', 12 | $size: '$size', 13 | }; 14 | 15 | export const OperatorMap = { 16 | $gt: '>', 17 | $gte: '>=', 18 | $lt: '<', 19 | $lte: '<=', 20 | $ne: '!=', 21 | }; 22 | 23 | export const isOperator = (key: string) => key.startsWith('$'); 24 | 25 | export const hasOperators = (value: Record) => 26 | Object.keys(value).some(isOperator); 27 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/errors/index.ts: -------------------------------------------------------------------------------- 1 | export const isNumber = (val: unknown): val is number => 2 | typeof val === 'number' && val === val; 3 | 4 | export const isString = (val: unknown): val is string => 5 | typeof val === 'string'; 6 | 7 | export class PongoError extends Error { 8 | public errorCode: number; 9 | 10 | constructor( 11 | options?: { errorCode: number; message?: string } | string | number, 12 | ) { 13 | const errorCode = 14 | options && typeof options === 'object' && 'errorCode' in options 15 | ? options.errorCode 16 | : isNumber(options) 17 | ? options 18 | : 500; 19 | const message = 20 | options && typeof options === 'object' && 'message' in options 21 | ? options.message 22 | : isString(options) 23 | ? options 24 | : `Error with status code '${errorCode}' ocurred during Pongo processing`; 25 | 26 | super(message); 27 | this.errorCode = errorCode; 28 | 29 | // 👇️ because we are extending a built-in class 30 | Object.setPrototypeOf(this, PongoError.prototype); 31 | } 32 | } 33 | 34 | export class ConcurrencyError extends PongoError { 35 | constructor(message?: string) { 36 | super({ 37 | errorCode: 412, 38 | message: message ?? `Expected document state does not match current one!`, 39 | }); 40 | 41 | // 👇️ because we are extending a built-in class 42 | Object.setPrototypeOf(this, ConcurrencyError.prototype); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './collection'; 2 | export * from './errors'; 3 | export * from './pongoClient'; 4 | export * from './pongoDb'; 5 | export * from './pongoSession'; 6 | export * from './pongoTransaction'; 7 | export * from './schema'; 8 | export * from './typing'; 9 | export * from './utils'; 10 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/pongoClient.connections.int.spec.ts: -------------------------------------------------------------------------------- 1 | import { dumbo, isNodePostgresNativePool } from '@event-driven-io/dumbo/pg'; 2 | import { 3 | PostgreSqlContainer, 4 | type StartedPostgreSqlContainer, 5 | } from '@testcontainers/postgresql'; 6 | import { randomUUID } from 'node:crypto'; 7 | import { after, before, describe, it } from 'node:test'; 8 | import pg from 'pg'; 9 | import { pongoClient } from './pongoClient'; 10 | 11 | type User = { 12 | _id?: string; 13 | name: string; 14 | }; 15 | 16 | void describe('Pongo collection', () => { 17 | let postgres: StartedPostgreSqlContainer; 18 | let connectionString: string; 19 | 20 | before(async () => { 21 | postgres = await new PostgreSqlContainer().start(); 22 | connectionString = postgres.getConnectionUri(); 23 | }); 24 | 25 | after(async () => { 26 | await postgres.stop(); 27 | }); 28 | 29 | const insertDocumentUsingPongo = async ( 30 | poolOrClient: pg.Pool | pg.PoolClient | pg.Client, 31 | ) => { 32 | const pongo = pongoClient( 33 | connectionString, 34 | isNodePostgresNativePool(poolOrClient) 35 | ? undefined 36 | : { 37 | connectionOptions: { 38 | client: poolOrClient, 39 | }, 40 | }, 41 | ); 42 | 43 | try { 44 | const pongoCollection = pongo.db().collection('connections'); 45 | await pongoCollection.insertOne({ name: randomUUID() }); 46 | } finally { 47 | await pongo.close(); 48 | } 49 | }; 50 | 51 | void describe('Pool', () => { 52 | void it('connects using pool', async () => { 53 | const pool = new pg.Pool({ connectionString }); 54 | 55 | try { 56 | await insertDocumentUsingPongo(pool); 57 | } catch (error) { 58 | console.log(error); 59 | } finally { 60 | await pool.end(); 61 | } 62 | }); 63 | 64 | void it('connects using connected pool client', async () => { 65 | const pool = new pg.Pool({ connectionString }); 66 | const poolClient = await pool.connect(); 67 | 68 | try { 69 | await insertDocumentUsingPongo(poolClient); 70 | } finally { 71 | poolClient.release(); 72 | await pool.end(); 73 | } 74 | }); 75 | 76 | void it('connects using connected client', async () => { 77 | const client = new pg.Client({ connectionString }); 78 | await client.connect(); 79 | 80 | try { 81 | await insertDocumentUsingPongo(client); 82 | } finally { 83 | await client.end(); 84 | } 85 | }); 86 | 87 | void it('connects using existing connection', async () => { 88 | const pool = dumbo({ connectionString }); 89 | 90 | try { 91 | await pool.withConnection(async (connection) => { 92 | const pongo = pongoClient(connectionString, { 93 | connectionOptions: { 94 | connection, 95 | pooled: false, 96 | }, 97 | }); 98 | 99 | const users = pongo.db().collection('connections'); 100 | await users.insertOne({ name: randomUUID() }); 101 | await users.insertOne({ name: randomUUID() }); 102 | }); 103 | } finally { 104 | await pool.close(); 105 | } 106 | }); 107 | 108 | void it('connects using existing connection from transaction', async () => { 109 | const pool = dumbo({ connectionString }); 110 | 111 | try { 112 | await pool.withTransaction(async ({ connection }) => { 113 | const pongo = pongoClient(connectionString, { 114 | connectionOptions: { connection }, 115 | }); 116 | 117 | const users = pongo.db().collection('connections'); 118 | await users.insertOne({ name: randomUUID() }); 119 | await users.insertOne({ name: randomUUID() }); 120 | }); 121 | } finally { 122 | await pool.close(); 123 | } 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/pongoClient.ts: -------------------------------------------------------------------------------- 1 | import { type MigrationStyle } from '@event-driven-io/dumbo'; 2 | import { 3 | NodePostgresConnectorType, 4 | type NodePostgresConnection, 5 | } from '@event-driven-io/dumbo/pg'; 6 | import pg from 'pg'; 7 | import type { PostgresDbClientOptions } from '../storage/postgresql'; 8 | import { getPongoDb, type AllowedDbClientOptions } from './pongoDb'; 9 | import { pongoSession } from './pongoSession'; 10 | import { 11 | proxyClientWithSchema, 12 | type PongoClientSchema, 13 | type PongoClientWithSchema, 14 | } from './schema'; 15 | import type { PongoClient, PongoDb, PongoSession } from './typing'; 16 | 17 | export type PooledPongoClientOptions = 18 | | { 19 | pool: pg.Pool; 20 | } 21 | | { 22 | pooled: true; 23 | } 24 | | { 25 | pool: pg.Pool; 26 | pooled: true; 27 | } 28 | | object; 29 | 30 | export type NotPooledPongoOptions = 31 | | { 32 | client: pg.Client; 33 | } 34 | | { 35 | pooled: false; 36 | } 37 | | { 38 | client: pg.Client; 39 | pooled: false; 40 | } 41 | | { 42 | connection: NodePostgresConnection; 43 | pooled?: false; 44 | }; 45 | 46 | export type PongoClientOptions< 47 | TypedClientSchema extends PongoClientSchema = PongoClientSchema, 48 | > = { 49 | schema?: { autoMigration?: MigrationStyle; definition?: TypedClientSchema }; 50 | errors?: { throwOnOperationFailures?: boolean }; 51 | connectionOptions?: PooledPongoClientOptions | NotPooledPongoOptions; 52 | }; 53 | 54 | export const pongoClient = < 55 | TypedClientSchema extends PongoClientSchema = PongoClientSchema, 56 | DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, 57 | >( 58 | connectionString: string, 59 | options: PongoClientOptions = {}, 60 | ): PongoClient & PongoClientWithSchema => { 61 | const dbClients = new Map(); 62 | 63 | const dbClient = getPongoDb( 64 | clientToDbOptions({ 65 | connectionString, 66 | clientOptions: options, 67 | }), 68 | ); 69 | dbClients.set(dbClient.databaseName, dbClient); 70 | 71 | const pongoClient: PongoClient = { 72 | connect: async () => { 73 | await dbClient.connect(); 74 | return pongoClient; 75 | }, 76 | close: async () => { 77 | for (const db of dbClients.values()) { 78 | await db.close(); 79 | } 80 | }, 81 | db: (dbName?: string): PongoDb => { 82 | if (!dbName) return dbClient; 83 | 84 | return ( 85 | dbClients.get(dbName) ?? 86 | dbClients 87 | .set( 88 | dbName, 89 | getPongoDb( 90 | clientToDbOptions({ 91 | connectionString, 92 | dbName, 93 | clientOptions: options, 94 | }), 95 | ), 96 | ) 97 | .get(dbName)! 98 | ); 99 | }, 100 | startSession: pongoSession, 101 | withSession: async ( 102 | callback: (session: PongoSession) => Promise, 103 | ): Promise => { 104 | const session = pongoSession(); 105 | 106 | try { 107 | return await callback(session); 108 | } finally { 109 | await session.endSession(); 110 | } 111 | }, 112 | }; 113 | 114 | return proxyClientWithSchema(pongoClient, options?.schema?.definition); 115 | }; 116 | 117 | export const clientToDbOptions = < 118 | DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, 119 | >(options: { 120 | connectionString: string; 121 | dbName?: string; 122 | clientOptions: PongoClientOptions; 123 | }): DbClientOptions => { 124 | const postgreSQLOptions: PostgresDbClientOptions = { 125 | connector: NodePostgresConnectorType, 126 | connectionString: options.connectionString, 127 | dbName: options.dbName, 128 | ...options.clientOptions, 129 | }; 130 | 131 | return postgreSQLOptions as DbClientOptions; 132 | }; 133 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/pongoDb.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectorType } from '@event-driven-io/dumbo/src'; 2 | import { 3 | isPostgresClientOptions, 4 | postgresDb, 5 | type PostgresDbClientOptions, 6 | } from '../storage/postgresql'; 7 | import type { PongoClientOptions } from './pongoClient'; 8 | import type { PongoDb } from './typing'; 9 | 10 | export type PongoDbClientOptions< 11 | Connector extends ConnectorType = ConnectorType, 12 | > = { 13 | connector: Connector; 14 | connectionString: string; 15 | dbName: string | undefined; 16 | } & PongoClientOptions; 17 | 18 | export type AllowedDbClientOptions = PostgresDbClientOptions; 19 | 20 | export const getPongoDb = < 21 | DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, 22 | >( 23 | options: DbClientOptions, 24 | ): PongoDb => { 25 | const { connector } = options; 26 | // This is the place where in the future could come resolution of other database types 27 | if (!isPostgresClientOptions(options)) 28 | throw new Error(`Unsupported db type: ${connector}`); 29 | 30 | return postgresDb(options); 31 | }; 32 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/pongoSession.ts: -------------------------------------------------------------------------------- 1 | import { pongoTransaction } from './pongoTransaction'; 2 | import type { 3 | PongoDbTransaction, 4 | PongoSession, 5 | PongoTransactionOptions, 6 | } from './typing'; 7 | 8 | export type PongoSessionOptions = { 9 | explicit?: boolean; 10 | defaultTransactionOptions: PongoTransactionOptions; 11 | }; 12 | 13 | const isActive = ( 14 | transaction: PongoDbTransaction | null, 15 | ): transaction is PongoDbTransaction => transaction?.isActive === true; 16 | 17 | function assertInActiveTransaction( 18 | transaction: PongoDbTransaction | null, 19 | ): asserts transaction is PongoDbTransaction { 20 | if (!isActive(transaction)) throw new Error('No active transaction exists!'); 21 | } 22 | 23 | function assertNotInActiveTransaction( 24 | transaction: PongoDbTransaction | null, 25 | ): asserts transaction is null { 26 | if (isActive(transaction)) 27 | throw new Error('Active transaction already exists!'); 28 | } 29 | 30 | export const pongoSession = (options?: PongoSessionOptions): PongoSession => { 31 | const explicit = options?.explicit === true; 32 | const defaultTransactionOptions: PongoTransactionOptions = 33 | options?.defaultTransactionOptions ?? { 34 | get snapshotEnabled() { 35 | return false; 36 | }, 37 | }; 38 | 39 | let transaction: PongoDbTransaction | null = null; 40 | let hasEnded = false; 41 | 42 | const startTransaction = (options?: PongoTransactionOptions) => { 43 | assertNotInActiveTransaction(transaction); 44 | 45 | transaction = pongoTransaction(options ?? defaultTransactionOptions); 46 | }; 47 | const commitTransaction = async () => { 48 | assertInActiveTransaction(transaction); 49 | 50 | await transaction.commit(); 51 | }; 52 | const abortTransaction = async () => { 53 | assertInActiveTransaction(transaction); 54 | 55 | await transaction.rollback(); 56 | }; 57 | 58 | const endSession = async (): Promise => { 59 | if (hasEnded) return; 60 | hasEnded = true; 61 | 62 | if (isActive(transaction)) await transaction.rollback(); 63 | }; 64 | 65 | const session = { 66 | get hasEnded() { 67 | return hasEnded; 68 | }, 69 | explicit, 70 | defaultTransactionOptions: defaultTransactionOptions ?? { 71 | get snapshotEnabled() { 72 | return false; 73 | }, 74 | }, 75 | get transaction() { 76 | return transaction; 77 | }, 78 | get snapshotEnabled() { 79 | return defaultTransactionOptions.snapshotEnabled; 80 | }, 81 | endSession, 82 | incrementTransactionNumber: () => {}, 83 | inTransaction: () => isActive(transaction), 84 | startTransaction, 85 | commitTransaction, 86 | abortTransaction, 87 | withTransaction: async ( 88 | fn: (session: PongoSession) => Promise, 89 | options?: PongoTransactionOptions, 90 | ): Promise => { 91 | startTransaction(options); 92 | 93 | try { 94 | const result = await fn(session); 95 | await commitTransaction(); 96 | return result; 97 | } catch (error) { 98 | await abortTransaction(); 99 | throw error; 100 | } 101 | }, 102 | }; 103 | 104 | return session; 105 | }; 106 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/pongoTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseTransaction } from '@event-driven-io/dumbo'; 2 | import type { 3 | PongoDb, 4 | PongoDbTransaction, 5 | PongoTransactionOptions, 6 | } from './typing'; 7 | 8 | export const pongoTransaction = ( 9 | options: PongoTransactionOptions, 10 | ): PongoDbTransaction => { 11 | let isCommitted = false; 12 | let isRolledBack = false; 13 | let databaseName: string | null = null; 14 | let transaction: DatabaseTransaction | null = null; 15 | 16 | return { 17 | enlistDatabase: async (db: PongoDb): Promise => { 18 | if (transaction && databaseName !== db.databaseName) 19 | throw new Error( 20 | "There's already other database assigned to transaction", 21 | ); 22 | 23 | if (transaction && databaseName === db.databaseName) return transaction; 24 | 25 | databaseName = db.databaseName; 26 | transaction = db.transaction(); 27 | await transaction.begin(); 28 | 29 | return transaction; 30 | }, 31 | commit: async () => { 32 | if (!transaction) throw new Error('No database transaction started!'); 33 | if (isCommitted) return; 34 | if (isRolledBack) throw new Error('Transaction is not active!'); 35 | 36 | isCommitted = true; 37 | 38 | await transaction.commit(); 39 | 40 | transaction = null; 41 | }, 42 | rollback: async (error?: unknown) => { 43 | if (!transaction) throw new Error('No database transaction started!'); 44 | if (isCommitted) throw new Error('Cannot rollback commited transaction!'); 45 | if (isRolledBack) return; 46 | 47 | isRolledBack = true; 48 | 49 | await transaction.rollback(error); 50 | 51 | transaction = null; 52 | }, 53 | databaseName, 54 | isStarting: false, 55 | isCommitted, 56 | get isActive() { 57 | return !isCommitted && !isRolledBack; 58 | }, 59 | get sqlExecutor() { 60 | if (transaction === null) 61 | throw new Error('No database transaction was started'); 62 | 63 | return transaction.execute; 64 | }, 65 | options, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/typing/entries.ts: -------------------------------------------------------------------------------- 1 | type Entry = { 2 | [K in keyof Required]: [K, Required[K]]; 3 | }[keyof Required]; 4 | 5 | type IterableEntry = Entry & { 6 | [Symbol.iterator](): Iterator>; 7 | }; 8 | 9 | export const objectEntries = (obj: T): IterableEntry[] => 10 | Object.entries(obj).map(([key, value]) => [key as keyof T, value]); 11 | 12 | export type NonPartial = { [K in keyof Required]: T[K] }; 13 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/typing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entries'; 2 | export * from './operations'; 3 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/utils/deepEquals.ts: -------------------------------------------------------------------------------- 1 | export const deepEquals = (left: T, right: T): boolean => { 2 | if (isEquatable(left)) { 3 | return left.equals(right); 4 | } 5 | 6 | if (Array.isArray(left)) { 7 | return ( 8 | Array.isArray(right) && 9 | left.length === right.length && 10 | left.every((val, index) => deepEquals(val, right[index])) 11 | ); 12 | } 13 | 14 | if ( 15 | typeof left !== 'object' || 16 | typeof right !== 'object' || 17 | left === null || 18 | right === null 19 | ) { 20 | return left === right; 21 | } 22 | 23 | if (Array.isArray(right)) return false; 24 | 25 | const keys1 = Object.keys(left); 26 | const keys2 = Object.keys(right); 27 | 28 | if ( 29 | keys1.length !== keys2.length || 30 | !keys1.every((key) => keys2.includes(key)) 31 | ) 32 | return false; 33 | 34 | for (const key in left) { 35 | if (left[key] instanceof Function && right[key] instanceof Function) 36 | continue; 37 | 38 | const isEqual = deepEquals(left[key], right[key]); 39 | if (!isEqual) { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | }; 46 | 47 | export type Equatable = { equals: (right: T) => boolean } & T; 48 | 49 | export const isEquatable = (left: T): left is Equatable => { 50 | return ( 51 | left && 52 | typeof left === 'object' && 53 | 'equals' in left && 54 | typeof left['equals'] === 'function' 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/packages/pongo/src/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deepEquals'; 2 | -------------------------------------------------------------------------------- /src/packages/pongo/src/e2e/cli-config.ts: -------------------------------------------------------------------------------- 1 | import { pongoSchema } from '../core'; 2 | 3 | type User = { name: string }; 4 | 5 | export default { 6 | schema: pongoSchema.client({ 7 | database: pongoSchema.db({ 8 | users: pongoSchema.collection('users'), 9 | }), 10 | }), 11 | }; 12 | -------------------------------------------------------------------------------- /src/packages/pongo/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './storage/postgresql'; 3 | -------------------------------------------------------------------------------- /src/packages/pongo/src/mongo/findCursor.ts: -------------------------------------------------------------------------------- 1 | export class FindCursor { 2 | private findDocumentsPromise: Promise; 3 | private documents: T[] | null = null; 4 | private index: number = 0; 5 | 6 | constructor(documents: Promise) { 7 | this.findDocumentsPromise = documents; 8 | } 9 | 10 | async toArray(): Promise { 11 | return this.findDocuments(); 12 | } 13 | 14 | async forEach(callback: (doc: T) => void): Promise { 15 | const docs = await this.findDocuments(); 16 | 17 | for (const doc of docs) { 18 | callback(doc); 19 | } 20 | return Promise.resolve(); 21 | } 22 | 23 | hasNext(): boolean { 24 | if (this.documents === null) throw Error('Error while fetching documents'); 25 | return this.index < this.documents.length; 26 | } 27 | 28 | async next(): Promise { 29 | const docs = await this.findDocuments(); 30 | return this.hasNext() ? (docs[this.index++] ?? null) : null; 31 | } 32 | 33 | private async findDocuments(): Promise { 34 | this.documents = await this.findDocumentsPromise; 35 | return this.documents; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/packages/pongo/src/mongo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './findCursor'; 2 | export * from './mongoClient'; 3 | export * from './mongoCollection'; 4 | export * from './mongoDb'; 5 | -------------------------------------------------------------------------------- /src/packages/pongo/src/mongo/mongoClient.ts: -------------------------------------------------------------------------------- 1 | import type { ClientSessionOptions } from 'http2'; 2 | import type { ClientSession, WithSessionCallback } from 'mongodb'; 3 | import { 4 | pongoClient, 5 | pongoSession, 6 | type PongoClient, 7 | type PongoClientOptions, 8 | } from '../core'; 9 | import { Db } from './mongoDb'; 10 | 11 | export class MongoClient { 12 | private pongoClient: PongoClient; 13 | 14 | constructor(connectionString: string, options: PongoClientOptions = {}) { 15 | this.pongoClient = pongoClient(connectionString, options); 16 | } 17 | 18 | async connect() { 19 | await this.pongoClient.connect(); 20 | return this; 21 | } 22 | 23 | async close() { 24 | await this.pongoClient.close(); 25 | } 26 | 27 | db(dbName?: string): Db { 28 | return new Db(this.pongoClient.db(dbName)); 29 | } 30 | startSession(_options?: ClientSessionOptions): ClientSession { 31 | return pongoSession() as unknown as ClientSession; 32 | } 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | withSession(_executor: WithSessionCallback): Promise; 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | withSession( 37 | _options: ClientSessionOptions, 38 | _executor: WithSessionCallback, 39 | ): Promise; 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | async withSession( 42 | optionsOrExecutor: ClientSessionOptions | WithSessionCallback, 43 | executor?: WithSessionCallback, 44 | ): Promise { 45 | const callback = 46 | typeof optionsOrExecutor === 'function' ? optionsOrExecutor : executor!; 47 | 48 | const session = pongoSession() as unknown as ClientSession; 49 | 50 | try { 51 | return await callback(session); 52 | } finally { 53 | await session.endSession(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/packages/pongo/src/mongo/mongoDb.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Collection as MongoCollection, 3 | ObjectId, 4 | type Document, 5 | } from 'mongodb'; 6 | import type { 7 | DocumentHandler, 8 | HandleOptions, 9 | PongoDb, 10 | PongoHandleResult, 11 | } from '../core'; 12 | import { Collection } from './mongoCollection'; 13 | 14 | export class Db { 15 | private pongoDb: PongoDb; 16 | constructor(pongoDb: PongoDb) { 17 | this.pongoDb = pongoDb; 18 | } 19 | 20 | get databaseName(): string { 21 | return this.pongoDb.databaseName; 22 | } 23 | 24 | collection( 25 | collectionName: string, 26 | ): MongoCollection & { 27 | handle( 28 | id: ObjectId, 29 | handle: DocumentHandler, 30 | options?: HandleOptions, 31 | ): Promise>; 32 | } { 33 | return new Collection(this.pongoDb.collection(collectionName)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/packages/pongo/src/pg.ts: -------------------------------------------------------------------------------- 1 | export * from './storage/postgresql'; 2 | -------------------------------------------------------------------------------- /src/packages/pongo/src/shim.ts: -------------------------------------------------------------------------------- 1 | export * from './mongo'; 2 | -------------------------------------------------------------------------------- /src/packages/pongo/src/sqlite3.ts: -------------------------------------------------------------------------------- 1 | export * from './storage/sqlite/core'; 2 | export * from './storage/sqlite/sqlite3'; 3 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/dbClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | schemaComponent, 3 | SQL, 4 | type QueryResult, 5 | type QueryResultRow, 6 | type SchemaComponent, 7 | } from '@event-driven-io/dumbo'; 8 | import { 9 | dumbo, 10 | getDatabaseNameOrDefault, 11 | NodePostgresConnectorType, 12 | runPostgreSQLMigrations, 13 | type PostgresConnector, 14 | type PostgresPoolOptions, 15 | } from '@event-driven-io/dumbo/pg'; 16 | import type { Document } from 'mongodb'; 17 | import { 18 | objectEntries, 19 | pongoCollection, 20 | pongoCollectionSchemaComponent, 21 | proxyPongoDbWithSchema, 22 | transactionExecutorOrDefault, 23 | type CollectionOperationOptions, 24 | type PongoCollection, 25 | type PongoDb, 26 | type PongoDbClientOptions, 27 | } from '../../core'; 28 | import { postgresSQLBuilder } from './sqlBuilder'; 29 | 30 | export type PostgresDbClientOptions = PongoDbClientOptions; 31 | 32 | export const isPostgresClientOptions = ( 33 | options: PongoDbClientOptions, 34 | ): options is PostgresDbClientOptions => 35 | options.connector === NodePostgresConnectorType; 36 | 37 | export const postgresDb = ( 38 | options: PostgresDbClientOptions, 39 | ): PongoDb => { 40 | const { connectionString, dbName } = options; 41 | const databaseName = dbName ?? getDatabaseNameOrDefault(connectionString); 42 | 43 | const pool = dumbo({ 44 | connectionString, 45 | ...options.connectionOptions, 46 | }); 47 | 48 | const collections = new Map>(); 49 | 50 | const command = async ( 51 | sql: SQL, 52 | options?: CollectionOperationOptions, 53 | ) => 54 | ( 55 | await transactionExecutorOrDefault(db, options, pool.execute) 56 | ).command(sql); 57 | 58 | const query = async ( 59 | sql: SQL, 60 | options?: CollectionOperationOptions, 61 | ) => 62 | (await transactionExecutorOrDefault(db, options, pool.execute)).query( 63 | sql, 64 | ); 65 | 66 | const db: PongoDb = { 67 | connector: options.connector, 68 | databaseName, 69 | connect: () => Promise.resolve(), 70 | close: () => pool.close(), 71 | 72 | collections: () => [...collections.values()], 73 | collection: (collectionName) => 74 | pongoCollection({ 75 | collectionName, 76 | db, 77 | pool, 78 | sqlBuilder: postgresSQLBuilder(collectionName), 79 | schema: options.schema ? options.schema : {}, 80 | errors: options.errors ? options.errors : {}, 81 | }), 82 | transaction: () => pool.transaction(), 83 | withTransaction: (handle) => pool.withTransaction(handle), 84 | 85 | schema: { 86 | get component(): SchemaComponent { 87 | return schemaComponent('pongoDb', { 88 | components: [...collections.values()].map((c) => c.schema.component), 89 | }); 90 | }, 91 | migrate: () => 92 | runPostgreSQLMigrations( 93 | pool, 94 | [...collections.values()].flatMap((c) => 95 | // TODO: This needs to change to support more connectors 96 | c.schema.component.migrations({ connector: 'PostgreSQL:pg' }), 97 | ), 98 | ), 99 | }, 100 | 101 | sql: { 102 | async query( 103 | sql: SQL, 104 | options?: CollectionOperationOptions, 105 | ): Promise { 106 | const result = await query(sql, options); 107 | return result.rows; 108 | }, 109 | async command( 110 | sql: SQL, 111 | options?: CollectionOperationOptions, 112 | ): Promise> { 113 | return command(sql, options); 114 | }, 115 | }, 116 | }; 117 | 118 | const dbsSchema = options?.schema?.definition?.dbs; 119 | 120 | if (dbsSchema) { 121 | const dbSchema = objectEntries(dbsSchema) 122 | .map((e) => e[1]) 123 | .find((db) => db.name === dbName || db.name === databaseName); 124 | 125 | if (dbSchema) return proxyPongoDbWithSchema(db, dbSchema, collections); 126 | } 127 | 128 | return db; 129 | }; 130 | 131 | export const pongoDbSchemaComponent = ( 132 | collections: string[] | SchemaComponent[], 133 | ) => { 134 | const components = 135 | collections.length > 0 && typeof collections[0] === 'string' 136 | ? collections.map((collectionName) => 137 | pongoCollectionSchemaComponent(collectionName as string), 138 | ) 139 | : (collections as SchemaComponent[]); 140 | 141 | return schemaComponent('pongo:schema_component:db', { 142 | components, 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dbClient'; 2 | export * from './sqlBuilder'; 3 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/migrations/migrations.int.spec.ts: -------------------------------------------------------------------------------- 1 | import { type Dumbo, rawSql } from '@event-driven-io/dumbo'; 2 | import { dumbo, tableExists } from '@event-driven-io/dumbo/pg'; 3 | import { 4 | PostgreSqlContainer, 5 | StartedPostgreSqlContainer, 6 | } from '@testcontainers/postgresql'; 7 | import assert from 'assert'; 8 | import { after, before, beforeEach, describe, it } from 'node:test'; 9 | import { pongoClient, type PongoClient, pongoSchema } from '../../../core'; 10 | 11 | void describe('Migration Integration Tests', () => { 12 | let pool: Dumbo; 13 | let postgres: StartedPostgreSqlContainer; 14 | let connectionString: string; 15 | let client: PongoClient; 16 | 17 | const schema = pongoSchema.client({ 18 | database: pongoSchema.db({ 19 | users: pongoSchema.collection('users'), 20 | roles: pongoSchema.collection('roles'), 21 | }), 22 | }); 23 | 24 | before(async () => { 25 | postgres = await new PostgreSqlContainer().start(); 26 | connectionString = postgres.getConnectionUri(); 27 | pool = dumbo({ connectionString }); 28 | client = pongoClient(connectionString, { 29 | schema: { autoMigration: 'CreateOrUpdate', definition: schema }, 30 | }); 31 | }); 32 | 33 | after(async () => { 34 | await pool.close(); 35 | await postgres.stop(); 36 | }); 37 | 38 | beforeEach(async () => { 39 | await pool.execute.query( 40 | rawSql('DROP SCHEMA public CASCADE; CREATE SCHEMA public;'), 41 | ); 42 | }); 43 | 44 | void it('should apply multiple migrations sequentially', async () => { 45 | await client.db().schema.migrate(); 46 | 47 | const usersTableExists = await tableExists(pool, 'users'); 48 | const rolesTableExists = await tableExists(pool, 'roles'); 49 | 50 | assert.ok(usersTableExists, 'The users table should exist.'); 51 | assert.ok(rolesTableExists, 'The roles table should exist.'); 52 | }); 53 | 54 | void it('should correctly apply a migration if the hash matches the previous migration with the same name', async () => { 55 | await client.db().schema.migrate(); 56 | 57 | // Attempt to run the same migration again with the same content 58 | await client.db().schema.migrate(); 59 | 60 | const migrationNames = await pool.execute.query<{ name: number }>( 61 | rawSql('SELECT name FROM migrations'), 62 | ); 63 | assert.strictEqual( 64 | migrationNames.rowCount, 65 | 2, 66 | 'The migration should only be applied once.', 67 | ); 68 | assert.deepEqual( 69 | migrationNames.rows.map((r) => r.name), 70 | [ 71 | 'pongoCollection:users:001:createtable', 72 | 'pongoCollection:roles:001:createtable', 73 | ], 74 | ); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/sqlBuilder/filter/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | hasOperators, 3 | objectEntries, 4 | QueryOperators, 5 | type PongoFilter, 6 | } from '../../../../core'; 7 | import { handleOperator } from './queryOperators'; 8 | 9 | export * from './queryOperators'; 10 | 11 | const AND = 'AND'; 12 | 13 | export const constructFilterQuery = (filter: PongoFilter): string => 14 | Object.entries(filter) 15 | .map(([key, value]) => 16 | isRecord(value) 17 | ? constructComplexFilterQuery(key, value) 18 | : handleOperator(key, '$eq', value), 19 | ) 20 | .join(` ${AND} `); 21 | 22 | const constructComplexFilterQuery = ( 23 | key: string, 24 | value: Record, 25 | ): string => { 26 | const isEquality = !hasOperators(value); 27 | 28 | return objectEntries(value) 29 | .map( 30 | ([nestedKey, val]) => 31 | isEquality 32 | ? handleOperator(`${key}.${nestedKey}`, QueryOperators.$eq, val) // regular value 33 | : handleOperator(key, nestedKey, val), // operator 34 | ) 35 | .join(` ${AND} `); 36 | }; 37 | 38 | const isRecord = (value: unknown): value is Record => 39 | value !== null && typeof value === 'object' && !Array.isArray(value); 40 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/sqlBuilder/filter/queryOperators.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer, sql } from '@event-driven-io/dumbo'; 2 | import { objectEntries, OperatorMap } from '../../../../core'; 3 | 4 | export const handleOperator = ( 5 | path: string, 6 | operator: string, 7 | value: unknown, 8 | ): string => { 9 | if (path === '_id' || path === '_version') { 10 | return handleMetadataOperator(path, operator, value); 11 | } 12 | 13 | switch (operator) { 14 | case '$eq': 15 | return sql( 16 | `(data @> %L::jsonb OR jsonb_path_exists(data, '$.%s[*] ? (@ == %s)'))`, 17 | JSONSerializer.serialize(buildNestedObject(path, value)), 18 | path, 19 | JSONSerializer.serialize(value), 20 | ); 21 | case '$gt': 22 | case '$gte': 23 | case '$lt': 24 | case '$lte': 25 | case '$ne': 26 | return sql( 27 | `data #>> %L ${OperatorMap[operator]} %L`, 28 | `{${path.split('.').join(',')}}`, 29 | value, 30 | ); 31 | case '$in': 32 | return sql( 33 | 'data #>> %L IN (%s)', 34 | `{${path.split('.').join(',')}}`, 35 | (value as unknown[]).map((v) => sql('%L', v)).join(', '), 36 | ); 37 | case '$nin': 38 | return sql( 39 | 'data #>> %L NOT IN (%s)', 40 | `{${path.split('.').join(',')}}`, 41 | (value as unknown[]).map((v) => sql('%L', v)).join(', '), 42 | ); 43 | case '$elemMatch': { 44 | const subQuery = objectEntries(value as Record) 45 | .map(([subKey, subValue]) => 46 | sql(`@."%s" == %s`, subKey, JSONSerializer.serialize(subValue)), 47 | ) 48 | .join(' && '); 49 | return sql(`jsonb_path_exists(data, '$.%s[*] ? (%s)')`, path, subQuery); 50 | } 51 | case '$all': 52 | return sql( 53 | 'data @> %L::jsonb', 54 | JSONSerializer.serialize(buildNestedObject(path, value)), 55 | ); 56 | case '$size': 57 | return sql( 58 | 'jsonb_array_length(data #> %L) = %L', 59 | `{${path.split('.').join(',')}}`, 60 | value, 61 | ); 62 | default: 63 | throw new Error(`Unsupported operator: ${operator}`); 64 | } 65 | }; 66 | 67 | const handleMetadataOperator = ( 68 | fieldName: string, 69 | operator: string, 70 | value: unknown, 71 | ): string => { 72 | switch (operator) { 73 | case '$eq': 74 | return sql(`${fieldName} = %L`, value); 75 | case '$gt': 76 | case '$gte': 77 | case '$lt': 78 | case '$lte': 79 | case '$ne': 80 | return sql(`${fieldName} ${OperatorMap[operator]} %L`, value); 81 | case '$in': 82 | return sql( 83 | `${fieldName} IN (%s)`, 84 | (value as unknown[]).map((v) => sql('%L', v)).join(', '), 85 | ); 86 | case '$nin': 87 | return sql( 88 | `${fieldName} NOT IN (%s)`, 89 | (value as unknown[]).map((v) => sql('%L', v)).join(', '), 90 | ); 91 | default: 92 | throw new Error(`Unsupported operator: ${operator}`); 93 | } 94 | }; 95 | 96 | const buildNestedObject = ( 97 | path: string, 98 | value: unknown, 99 | ): Record => 100 | path 101 | .split('.') 102 | .reverse() 103 | .reduce((acc, key) => ({ [key]: acc }), value as Record); 104 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/postgresql/sqlBuilder/update/index.ts: -------------------------------------------------------------------------------- 1 | import { JSONSerializer, sql, type SQL } from '@event-driven-io/dumbo'; 2 | import { 3 | objectEntries, 4 | type $inc, 5 | type $push, 6 | type $set, 7 | type $unset, 8 | type PongoUpdate, 9 | } from '../../../../core'; 10 | 11 | export const buildUpdateQuery = (update: PongoUpdate): SQL => 12 | objectEntries(update).reduce((currentUpdateQuery, [op, value]) => { 13 | switch (op) { 14 | case '$set': 15 | return buildSetQuery(value, currentUpdateQuery); 16 | case '$unset': 17 | return buildUnsetQuery(value, currentUpdateQuery); 18 | case '$inc': 19 | return buildIncQuery(value, currentUpdateQuery); 20 | case '$push': 21 | return buildPushQuery(value, currentUpdateQuery); 22 | default: 23 | return currentUpdateQuery; 24 | } 25 | }, sql('data')); 26 | 27 | export const buildSetQuery = (set: $set, currentUpdateQuery: SQL): SQL => 28 | sql('%s || %L::jsonb', currentUpdateQuery, JSONSerializer.serialize(set)); 29 | 30 | export const buildUnsetQuery = ( 31 | unset: $unset, 32 | currentUpdateQuery: SQL, 33 | ): SQL => 34 | sql( 35 | '%s - %L', 36 | currentUpdateQuery, 37 | Object.keys(unset) 38 | .map((k) => `{${k}}`) 39 | .join(', '), 40 | ); 41 | 42 | export const buildIncQuery = ( 43 | inc: $inc, 44 | currentUpdateQuery: SQL, 45 | ): SQL => { 46 | for (const [key, value] of Object.entries(inc)) { 47 | currentUpdateQuery = sql( 48 | typeof value === 'bigint' 49 | ? "jsonb_set(%s, '{%s}', to_jsonb((COALESCE((data->>'%s')::BIGINT, 0) + %L)::TEXT), true)" 50 | : "jsonb_set(%s, '{%s}', to_jsonb(COALESCE((data->>'%s')::NUMERIC, 0) + %L), true)", 51 | currentUpdateQuery, 52 | key, 53 | key, 54 | value, 55 | ); 56 | } 57 | return currentUpdateQuery; 58 | }; 59 | 60 | export const buildPushQuery = ( 61 | push: $push, 62 | currentUpdateQuery: SQL, 63 | ): SQL => { 64 | for (const [key, value] of Object.entries(push)) { 65 | currentUpdateQuery = sql( 66 | "jsonb_set(%s, '{%s}', (coalesce(data->'%s', '[]'::jsonb) || %L::jsonb), true)", 67 | currentUpdateQuery, 68 | key, 69 | key, 70 | JSONSerializer.serialize([value]), 71 | ); 72 | } 73 | return currentUpdateQuery; 74 | }; 75 | -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/sqlite/core/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/packages/pongo/src/storage/sqlite/core/index.ts -------------------------------------------------------------------------------- /src/packages/pongo/src/storage/sqlite/sqlite3/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/event-driven-io/Pongo/bf2b5a28064984ce85f9753d9278b88e25413d0a/src/packages/pongo/src/storage/sqlite/sqlite3/index.ts -------------------------------------------------------------------------------- /src/packages/pongo/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/packages/pongo/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /src/packages/pongo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.shared.json", 3 | "include": ["./src/**/*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "outDir": "./dist" /* Redirect output structure to the directory. */, 7 | "rootDir": "./src", 8 | "paths": { 9 | "@event-driven-io/emmett": ["../packages/dumbo"] 10 | } 11 | }, 12 | "references": [ 13 | { 14 | "path": "../dumbo/" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/packages/pongo/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const env = process.env.NODE_ENV; 4 | 5 | export default defineConfig({ 6 | splitting: true, 7 | clean: true, // clean up the dist folder 8 | dts: true, // generate dts files 9 | format: ['esm', 'cjs'], // generate cjs and esm files 10 | minify: false, //env === 'production', 11 | bundle: true, //env === 'production', 12 | skipNodeModulesBundle: true, 13 | watch: env === 'development', 14 | target: 'esnext', 15 | outDir: 'dist', //env === 'production' ? 'dist' : 'lib', 16 | entry: [ 17 | 'src/index.ts', 18 | 'src/shim.ts', 19 | 'src/cli.ts', 20 | 'src/pg.ts', 21 | 'src/sqlite3.ts', 22 | ], 23 | //entry: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/**/*.internal.ts'], //include all files under src but not specs 24 | sourcemap: true, 25 | tsconfig: 'tsconfig.build.json', // workaround for https://github.com/egoist/tsup/issues/571#issuecomment-1760052931 26 | }); 27 | -------------------------------------------------------------------------------- /src/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.shared.json", 3 | "include": ["./packages/**/*.ts", "./docs/**/*.ts", "./tsup.config.ts"], 4 | "exclude": ["node_modules", "tmp"], 5 | "files": [], 6 | "compilerOptions": { 7 | "noEmit": true /* Do not emit outputs. */ 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.shared.json", 3 | "include": ["docs/**/*.ts", "./tsup.config.ts"], 4 | "exclude": ["node_modules", "tmp"], 5 | "files": [], 6 | "compilerOptions": { 7 | "noEmit": true /* Do not emit outputs. */, 8 | "paths": { 9 | "@event-driven-io/dumbo": ["./packages/dumbo/src"], 10 | "@event-driven-io/pongo": ["./packages/pongo/src"] 11 | } 12 | }, 13 | "references": [ 14 | { 15 | "path": "./packages/dumbo/" 16 | }, 17 | { 18 | "path": "./packages/pongo/" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const env = process.env.NODE_ENV; 4 | 5 | export default defineConfig({ 6 | splitting: true, 7 | clean: true, // clean up the dist folder 8 | dts: true, // generate dts files 9 | format: ['cjs', 'esm'], // generate cjs and esm files 10 | minify: false, //env === 'production', 11 | bundle: true, //env === 'production', 12 | skipNodeModulesBundle: true, 13 | watch: env === 'development', 14 | target: 'esnext', 15 | outDir: 'dist', //env === 'production' ? 'dist' : 'lib', 16 | entry: ['src/index.ts'], 17 | sourcemap: true, 18 | }); 19 | --------------------------------------------------------------------------------