├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── conventional-pr.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── bun.lock ├── lefthook.yml ├── package.json ├── packages ├── wabe-build │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bunVersion.ts │ │ ├── index.ts │ │ └── tscVersion.ts │ └── tsconfig.json ├── wabe-buns3 │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── wabe-documentation │ ├── .gitignore │ ├── README.md │ ├── components │ │ ├── blog │ │ │ ├── footerArticle.tsx │ │ │ ├── header.tsx │ │ │ └── listBlogArticles.tsx │ │ ├── index.tsx │ │ ├── landing │ │ │ ├── callToAction.tsx │ │ │ ├── example.tsx │ │ │ ├── features.tsx │ │ │ ├── footer.tsx │ │ │ ├── hero.tsx │ │ │ └── presentation.tsx │ │ └── utils.ts │ ├── docs │ │ ├── documentation │ │ │ ├── ai │ │ │ │ └── index.md │ │ │ ├── authentication │ │ │ │ ├── customMethods.md │ │ │ │ ├── defaultMethods.md │ │ │ │ ├── emailPasswordSRP.md │ │ │ │ ├── interact.md │ │ │ │ ├── oauth.md │ │ │ │ ├── resetPassword.md │ │ │ │ ├── roles.md │ │ │ │ ├── sessions.md │ │ │ │ └── twoFactor.md │ │ │ ├── codegen.md │ │ │ ├── cron │ │ │ │ └── index.md │ │ │ ├── database │ │ │ │ └── index.md │ │ │ ├── email │ │ │ │ └── index.md │ │ │ ├── file │ │ │ │ └── index.md │ │ │ ├── graphql │ │ │ │ └── api.md │ │ │ ├── hooks.md │ │ │ ├── index.md │ │ │ ├── payment │ │ │ │ └── index.md │ │ │ ├── public │ │ │ │ ├── auth.webp │ │ │ │ ├── copy.webp │ │ │ │ ├── cover2.webp │ │ │ │ ├── database.webp │ │ │ │ ├── email.webp │ │ │ │ ├── favicon.ico │ │ │ │ ├── github.webp │ │ │ │ ├── graphql.webp │ │ │ │ ├── graphqlPlayground.webp │ │ │ │ ├── graphqlPlayground2.webp │ │ │ │ ├── hooks.webp │ │ │ │ ├── logo.webp │ │ │ │ ├── payment.webp │ │ │ │ ├── permissions.webp │ │ │ │ ├── robots.txt │ │ │ │ └── sitemap.xml │ │ │ ├── rootKey.md │ │ │ ├── routes.md │ │ │ ├── schema │ │ │ │ ├── classes.md │ │ │ │ ├── enums.md │ │ │ │ ├── resolvers.md │ │ │ │ └── scalars.md │ │ │ ├── security │ │ │ │ └── index.md │ │ │ └── wabe │ │ │ │ ├── concepts.md │ │ │ │ ├── motivations.md │ │ │ │ └── start.md │ │ ├── index.mdx │ │ └── public │ │ │ ├── assets │ │ │ ├── appleTouchIcon.png │ │ │ ├── auth.webp │ │ │ ├── bun.svg │ │ │ ├── code.webp │ │ │ ├── copy.webp │ │ │ ├── cover.png │ │ │ ├── database.webp │ │ │ ├── email.webp │ │ │ ├── favicon.ico │ │ │ ├── github.svg │ │ │ ├── google.svg │ │ │ ├── graphql.svg │ │ │ ├── graphql.webp │ │ │ ├── graphqlPlayground.webp │ │ │ ├── graphqlPlayground2.webp │ │ │ ├── hooks.webp │ │ │ ├── logo.png │ │ │ ├── logoWithoutBackground.webp │ │ │ ├── logoXWhite.png │ │ │ ├── mongodb.svg │ │ │ ├── payment.webp │ │ │ ├── permissions.webp │ │ │ ├── resend.svg │ │ │ ├── robots.txt │ │ │ ├── schema.webp │ │ │ ├── sitemap.xml │ │ │ └── stripe.svg │ │ │ ├── robots.txt │ │ │ ├── scripts │ │ │ └── data.js │ │ │ └── sitemap.xml │ ├── package.json │ ├── postcss.config.js │ ├── rspress.config.ts │ ├── styles │ │ └── index.css │ ├── tailwind.config.js │ └── tsconfig.json ├── wabe-mistralai │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json ├── wabe-mongodb-launcher │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── wabe-mongodb │ ├── .gitignore │ ├── README.md │ ├── bunfig.toml │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ ├── tsconfig.json │ └── utils │ │ ├── preload.ts │ │ └── testHelper.ts ├── wabe-openai │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json ├── wabe-pluralize │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json ├── wabe-postgres-launcher │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── wabe-postgres │ ├── .gitignore │ ├── README.md │ ├── bunfig.toml │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ ├── tsconfig.json │ └── utils │ │ ├── preload.ts │ │ └── testHelper.ts ├── wabe-resend │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json ├── wabe-stripe │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json └── wabe │ ├── README.md │ ├── bunfig.toml │ ├── dev │ └── index.ts │ ├── generated │ ├── schema.graphql │ └── wabe.ts │ ├── package.json │ ├── src │ ├── ai │ │ ├── AIController.test.ts │ │ ├── AIController.ts │ │ ├── index.ts │ │ └── interface.ts │ ├── authentication │ │ ├── OTP.test.ts │ │ ├── OTP.ts │ │ ├── Session.test.ts │ │ ├── Session.ts │ │ ├── defaultAuthentication.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ ├── oauth │ │ │ ├── GitHub.test.ts │ │ │ ├── GitHub.ts │ │ │ ├── Google.test.ts │ │ │ ├── Google.ts │ │ │ ├── Oauth2Client.test.ts │ │ │ ├── Oauth2Client.ts │ │ │ ├── index.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── providers │ │ │ ├── EmailOTP.test.ts │ │ │ ├── EmailOTP.ts │ │ │ ├── EmailPassword.test.ts │ │ │ ├── EmailPassword.ts │ │ │ ├── EmailPasswordSRP.test.ts │ │ │ ├── EmailPasswordSRP.ts │ │ │ ├── GitHub.ts │ │ │ ├── Google.ts │ │ │ ├── OAuth.test.ts │ │ │ ├── OAuth.ts │ │ │ ├── PhonePassword.test.ts │ │ │ ├── PhonePassword.ts │ │ │ └── index.ts │ │ ├── resolvers │ │ │ ├── refreshResolver.test.ts │ │ │ ├── refreshResolver.ts │ │ │ ├── signInWithResolver.test.ts │ │ │ ├── signInWithResolver.ts │ │ │ ├── signOutResolver.test.ts │ │ │ ├── signOutResolver.ts │ │ │ ├── signUpWithResolver.test.ts │ │ │ ├── signUpWithResolver.ts │ │ │ ├── verifyChallenge.test.ts │ │ │ └── verifyChallenge.ts │ │ ├── roles.test.ts │ │ ├── roles.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── cache │ │ ├── InMemoryCache.test.ts │ │ └── InMemoryCache.ts │ ├── cron │ │ ├── index.test.ts │ │ └── index.ts │ ├── database │ │ ├── DatabaseController.test.ts │ │ ├── DatabaseController.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── interface.ts │ ├── email │ │ ├── DevAdapter.ts │ │ ├── EmailController.test.ts │ │ ├── EmailController.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── templates │ │ │ └── sendOtpCode.ts │ ├── files │ │ ├── FileController.ts │ │ ├── FileDevAdapter.ts │ │ ├── hookDeleteFile.ts │ │ ├── hookReadFile.ts │ │ ├── hookUploadFile.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── interface.ts │ ├── graphql │ │ ├── GraphQLSchema.test.ts │ │ ├── GraphQLSchema.ts │ │ ├── index.ts │ │ ├── parseGraphqlSchema.ts │ │ ├── parser.test.ts │ │ ├── parser.ts │ │ ├── pointerAndRelationFunction.ts │ │ ├── resolvers.ts │ │ ├── tests │ │ │ ├── aggregation.test.ts │ │ │ ├── e2e.test.ts │ │ │ └── scalars.test.ts │ │ └── types.ts │ ├── hooks │ │ ├── HookObject.test.ts │ │ ├── HookObject.ts │ │ ├── authentication.test.ts │ │ ├── authentication.ts │ │ ├── defaultFields.test.ts │ │ ├── defaultFields.ts │ │ ├── deleteSession.test.ts │ │ ├── deleteSession.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── permissions.test.ts │ │ ├── permissions.ts │ │ ├── protected.test.ts │ │ ├── protected.ts │ │ ├── searchableFields.test.ts │ │ ├── searchableFields.ts │ │ ├── session.test.ts │ │ ├── session.ts │ │ ├── setEmail.test.ts │ │ ├── setEmail.ts │ │ ├── setupAcl.test.ts │ │ └── setupAcl.ts │ ├── index.ts │ ├── payment │ │ ├── DevAdapter.ts │ │ ├── PaymentController.test.ts │ │ ├── PaymentController.ts │ │ ├── index.ts │ │ └── interface.ts │ ├── schema │ │ ├── Schema.test.ts │ │ ├── Schema.ts │ │ ├── defaultResolvers.ts │ │ ├── index.ts │ │ └── resolvers │ │ │ ├── meResolver.test.ts │ │ │ ├── meResolver.ts │ │ │ ├── newFile.ts │ │ │ ├── resetPassword.test.ts │ │ │ ├── resetPassword.ts │ │ │ ├── sendEmail.test.ts │ │ │ ├── sendEmail.ts │ │ │ ├── sendOtpCode.test.ts │ │ │ └── sendOtpCode.ts │ ├── security.test.ts │ ├── server │ │ ├── defaultHandlers.ts │ │ ├── generateCodegen.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── routes │ │ │ ├── authHandler.ts │ │ │ └── index.ts │ └── utils │ │ ├── export.ts │ │ ├── helper.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── preload.ts │ │ └── testHelper.ts │ └── tsconfig.json └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | ## **Additional context** 17 | Add any other context or screenshots about the feature request here (code example). 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci checks 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - '**' 8 | types: [opened, synchronize, reopened, unlabeled] 9 | paths: 10 | - '**' 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | test: 18 | timeout-minutes: 10 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Install Docker 26 | run: sudo apt-get update && sudo apt-get install -y docker-ce-cli 27 | 28 | - uses: oven-sh/setup-bun@v2 29 | with: 30 | bun-version: latest 31 | 32 | - run: bun install 33 | - run: bun run build 34 | - run: bun ci 35 | -------------------------------------------------------------------------------- /.github/workflows/conventional-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Conventional PR' 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: read 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5 19 | with: 20 | requireScope: true 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | /.next/ 4 | /out/ 5 | /build 6 | .DS_Store 7 | *.pem 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | .vercel 16 | **/*.trace 17 | **/*.zip 18 | **/*.tar.gz 19 | **/*.tgz 20 | **/*.log 21 | package-lock.json 22 | **/*.bun 23 | .zed 24 | 25 | 26 | cache 27 | bucket 28 | .nuxt 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | logs 2 | .cache 3 | node_modules/ 4 | .npm 5 | .env 6 | .env.development.local 7 | .env.test.local 8 | .env.production.local 9 | .env.local 10 | 11 | cache 12 | build 13 | lib 14 | src 15 | tests 16 | test 17 | CONTRIBUTING.md 18 | CODE_OF_CONDUCT.md 19 | CHANGELOG.md 20 | tsconfig.json 21 | .git 22 | bun.lockb 23 | dev 24 | fixtures 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Contributions are welcome! Here's how you can help: 4 | 5 | - **Report a bug**: If you find a bug, please open an issue. 6 | - **Request a feature**: If you have an idea for a feature, please open an issue. 7 | - **Create a pull request**: If you can fix a bug or implement a feature, please create a pull request (I promise a quick review). 8 | - **Use Wabe**: The best way to contribute is to use Wabe for your backend. 9 | 10 | Note: Each code contribution must be tested either by an existing test or a new one. 11 | 12 | ## Note about new PR 13 | 14 | First of all, thank you for taking the time to improve Wabe. Wabe has a great test coverage, to keep this code quality, please add tests that cover your changes 🙂. 15 | 16 | ## Requirement 17 | 18 | Wabe supports two types of databases: MongoDB and PostgreSQL. Since PostgreSQL is faster, it is the one we use in our tests. To run the tests, you need to have Docker installed and running on your machine. The launch scripts will take care of downloading the PostgreSQL image and creating a container before running the tests. More information can be found in the `wabe-postgres-launcher` or `wabe-mongodb-launcher` packages. 19 | 20 | ## Install 21 | 22 | Wabe uses Bun, so you need the latest version of Bun. You can see [here](https://bun.sh/docs/installation) if Bun is not installed on your machine. 23 | 24 | Wabe uses a monorepo organization, all the packages are under the `packages` directory. 25 | 26 | Once you have cloned the repository you can run the following command at the root of the project. 27 | 28 | ```sh 29 | bun install 30 | ``` 31 | 32 | You can run the tests in all packages by running the following commands at the root repository: 33 | 34 | ```sh 35 | cd packages/wabe 36 | bun dev # Run server and create all codegen 37 | 38 | bun test # Run test on wabe package 39 | # or 40 | bun ci # Run lint + test on package 41 | ``` 42 | 43 | ## Pre-commit 44 | 45 | Before any commit a pre-commit command that will run on your machine to ensure that the code is correctly formatted and the lint is respected. If you have any error of formatting during the pre-commit you can simply run the following command (at the root of the repository): 46 | 47 | Wabe repository also uses the conventional commits to ensure consistence and facilitate the release. Your PRs and your commits need to follow this convention. You can see here to see more information about [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). 48 | 49 | ```sh 50 | bun format 51 | ``` 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, payment, emails, and more for you. 12 | 13 | ## Install 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | ``` 20 | 21 | ## Basic example 22 | 23 | ```ts 24 | import { Wabe } from "wabe"; 25 | import { MongoAdapter } from "wabe-mongodb" 26 | 27 | const run = async () => { 28 | // Ensure your database is running before run the file 29 | 30 | const wabe = new Wabe({ 31 | isProduction: process.env.NODE_ENV === "production", 32 | // Root key example (must be long minimal 64 characters, you can generate it online) 33 | rootKey: 34 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 35 | database: { 36 | adapter: new MongoAdapter({ 37 | databaseName: "WabeApp", 38 | url: "mongodb://127.0.0.1:27045", 39 | }) 40 | }, 41 | port: 3000, 42 | }); 43 | 44 | await wabe.start(); 45 | }; 46 | 47 | await run(); 48 | ``` 49 | 50 | ## Features 51 | 52 | - **Authentication**: Secure and scalable authentication for your applications. 53 | - **Permissions**: Granular permissions control to secure your resources. 54 | - **Database**: A powerful, scalable database to store and manage your data. 55 | - **GraphQL API**: A flexible and powerful GraphQL API (following GraphQL Relay standard) to interact with your data. 56 | - **Hooks**: Powerful hooks system to execute custom actions before or after database requests. 57 | - **Email**: Send emails with your favorite provider with very simple integration. 58 | - **Payment**: Accept payments with Stripe or create your own payment provider adapter. 59 | 60 | ## Contributing 61 | 62 | Contributions are always welcome! If you have an idea for something that should be added, modified, or removed, please don't hesitate to create a pull request (I promise a quick review). 63 | 64 | You can also create an issue to propose your ideas or report a bug. 65 | 66 | Of course, you can also use Wabe for your backend; that is the better contribution at this day ❤️. 67 | 68 | If you like the project don't forget to share it. 69 | 70 | More information on the [Contribution guide](https://github.com/palixir/wabe/blob/main/CONTRIBUTING.md) 71 | 72 | ## License 73 | 74 | Distributed under the Apache License 2.0 [License](https://github.com/palixir/wabe/blob/main/LICENSE). 75 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", 3 | "files": { 4 | "include": ["**/*.ts", "**/*.js", "**/*.vue", "**/*.tsx"], 5 | "ignore": [ 6 | "**/dist/**", 7 | "**/node_modules/**", 8 | "build/**", 9 | "**/generated/**", 10 | ".nuxt/**" 11 | ], 12 | "ignoreUnknown": true 13 | }, 14 | "organizeImports": { 15 | "enabled": false 16 | }, 17 | "linter": { 18 | "enabled": true, 19 | "rules": { 20 | "recommended": true, 21 | "suspicious": { 22 | "noExplicitAny": "off", 23 | "useAwait": "error" 24 | }, 25 | "style": { 26 | "useTemplate": "off" 27 | }, 28 | "performance": { 29 | "noAccumulatingSpread": "off" 30 | }, 31 | "correctness": { 32 | "noUnusedImports": "error", 33 | "noUnusedVariables": "error" 34 | } 35 | } 36 | }, 37 | "formatter": { 38 | "enabled": true, 39 | "formatWithErrors": false, 40 | "indentStyle": "space" 41 | }, 42 | "javascript": { 43 | "formatter": { 44 | "quoteStyle": "single", 45 | "semicolons": "asNeeded" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | check: 5 | glob: '*.{js,ts,jsx,tsx}' 6 | run: bun biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "main", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "@biomejs/biome": "1.9.3", 6 | "lefthook": "1.7.18", 7 | "typescript": "5.8.2" 8 | }, 9 | "private": true, 10 | "workspaces": ["packages/*"], 11 | "scripts": { 12 | "build:wabe": "bun --filter './packages/wabe' build", 13 | "build:wabe-mongodb-launcher": "bun --filter './packages/wabe-mongodb-launcher' build", 14 | "build:wabe-postgres-launcher": "bun --filter './packages/wabe-postgres-launcher' build", 15 | "build": "bun build:wabe-mongodb-launcher && bun build:wabe-postgres-launcher && bun build:wabe && CI_BUILD=true bun --filter './packages/wabe-*' build", 16 | "ci": "bun --filter './packages/*' ci", 17 | "format": "bun --filter './packages/*' format && biome format --write ./*.json", 18 | "lint": "bun --filter './packages/*' lint", 19 | "squash": "base_branch=${1:-main} && git fetch origin $base_branch && branch=$(git branch --show-current) && git checkout $branch && git reset $(git merge-base origin/$base_branch $branch) && git add -A" 20 | }, 21 | "trustedDependencies": [ 22 | "@biomejs/biome", 23 | "lefthook", 24 | "mongodb-memory-server" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/wabe-build/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mongodb-launcher 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mongodb-launcher # On bun 21 | npm install wabe-mongodb-launcher # On npm 22 | yarn add wabe-mongodb-launcher # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mongodb-launcher usage 26 | 27 | ```ts 28 | import { runDatabase } from 'wabe-mongodb-launcher' 29 | 30 | 31 | await runDatabase() 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/wabe-build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-build", 3 | "version": "0.5.0", 4 | "license": "Apache-2.0", 5 | "main": "src/index.ts", 6 | "description": "Utils package to build other packages", 7 | "scripts": { 8 | "build:package": "bun run src/index.ts" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest", 12 | "oxc-transform": "0.58.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/wabe-build/src/bunVersion.ts: -------------------------------------------------------------------------------- 1 | import type { BunPlugin } from 'bun' 2 | import { isolatedDeclaration } from 'oxc-transform' 3 | 4 | // From https://github.com/oven-sh/bun/issues/5141 5 | const getDtsBunPlugin = (): BunPlugin => { 6 | const wroteTrack = new Set() 7 | return { 8 | name: 'oxc-transform-dts', 9 | setup(builder) { 10 | if (builder.config.root && builder.config.outdir) { 11 | const rootPath = Bun.pathToFileURL(builder.config.root).pathname 12 | const outPath = Bun.pathToFileURL(builder.config.outdir).pathname 13 | builder.onStart(() => wroteTrack.clear()) 14 | builder.onLoad({ filter: /\.ts$/ }, async (args) => { 15 | if (args.path.startsWith(rootPath) && !wroteTrack.has(args.path)) { 16 | wroteTrack.add(args.path) 17 | const { code } = isolatedDeclaration( 18 | args.path, 19 | await Bun.file(args.path).text(), 20 | ) 21 | await Bun.write( 22 | args.path 23 | .replace(new RegExp(`^${rootPath}`), outPath) 24 | .replace(/\.ts$/, '.d.ts'), 25 | code, 26 | ) 27 | } 28 | return undefined 29 | }) 30 | } 31 | }, 32 | } 33 | } 34 | 35 | const directory = process.argv[2] 36 | const target = process.argv[3] as 'node' | 'browser' | 'bun' 37 | 38 | export const bunCompilation = async () => { 39 | await Bun.$`rm -rf ${directory}/dist` 40 | 41 | const result = await Bun.build({ 42 | entrypoints: [`${directory}/src/index.ts`], 43 | root: `${directory}/src`, 44 | outdir: `${directory}/dist`, 45 | minify: false, 46 | target: target || 'node', 47 | plugins: [getDtsBunPlugin()], 48 | external: ['@node-rs/argon2', 'dockerode'], 49 | }) 50 | 51 | if (!result.success) for (const log of result.logs) console.error(log) 52 | } 53 | -------------------------------------------------------------------------------- /packages/wabe-build/src/index.ts: -------------------------------------------------------------------------------- 1 | import { bunCompilation } from './bunVersion' 2 | 3 | bunCompilation() 4 | -------------------------------------------------------------------------------- /packages/wabe-build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "types": ["bun-types"], 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/wabe-buns3/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-buns3 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-buns3 # On bun 21 | npm install wabe-buns3 # On npm 22 | yarn add wabe-buns3 # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-buns3 usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | import { Buns3Adapter } from "wabe-buns3"; 31 | 32 | const run = async () => { 33 | // Ensure your database is running before run the file 34 | 35 | const wabe = new Wabe({ 36 | isProduction: process.env.NODE_ENV === 'production', 37 | // Root key example (must be long minimal 64 characters, you can generate it online) 38 | rootKey: 39 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 40 | database: { 41 | adapter: new MongoAdapter({ 42 | databaseName: "WabeApp", 43 | url: "mongodb://127.0.0.1:27045", 44 | }) 45 | }, 46 | file: { 47 | adapter : new Buns3Adapter({ 48 | accessKeyId: 'accessKeyId', 49 | secretAccessKey: 'secretAccessKey', 50 | bucket: 'bucketName', 51 | endpoint: 'endpoint', 52 | }), 53 | } 54 | port: 3001, 55 | }); 56 | 57 | await wabe.start(); 58 | 59 | // The upload file and the read file is automatically managed in the GraphQL API 60 | await wabe.controllers.file.uploadFile(new File(['test'], 'test.txt')); 61 | 62 | const url = await wabe.controllers.file.readFile('test.txt'); 63 | }; 64 | 65 | await run(); 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/wabe-buns3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-buns3", 3 | "version": "0.5.0", 4 | "description": "Bun S3 adapter for Wabe", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/palixir/wabe.git" 20 | }, 21 | "main": "dist/index.js", 22 | "scripts": { 23 | "build": "bun --filter wabe-build build:package $(pwd) bun", 24 | "check": "tsc --project $(pwd)/tsconfig.json", 25 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 26 | "ci": "bun check && bun lint $(pwd) && bun test src", 27 | "format": "biome format --write . --config-path=../../" 28 | }, 29 | "devDependencies": { 30 | "@types/bun": "latest", 31 | "wabe": "workspace:*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/wabe-buns3/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { S3Client, S3Options } from 'bun' 2 | import type { FileAdapter, ReadFileOptions } from 'wabe' 3 | import { getS3Client } from './utils' 4 | 5 | export type Acl = 6 | | 'private' 7 | | 'public-read' 8 | | 'public-read-write' 9 | | 'aws-exec-read' 10 | | 'authenticated-read' 11 | | 'bucket-owner-read' 12 | | 'bucket-owner-full-control' 13 | | 'log-delivery-write' 14 | 15 | export class Buns3Adapter implements FileAdapter { 16 | public s3Client: S3Client 17 | private aclForUrl: Acl 18 | 19 | constructor(options: S3Options & { aclForUrl?: Acl }) { 20 | this.s3Client = getS3Client(options) 21 | 22 | this.aclForUrl = options.aclForUrl || 'private' 23 | } 24 | 25 | async uploadFile(file: File | Blob): Promise { 26 | await this.s3Client.write(file.name, file) 27 | } 28 | 29 | async readFile(fileName: string, options?: ReadFileOptions) { 30 | if (!(await this.s3Client.exists(fileName))) return null 31 | 32 | const s3file = this.s3Client.file(fileName) 33 | 34 | return s3file.presign({ 35 | expiresIn: options?.urlExpiresIn || 3600 * 24, 36 | acl: this.aclForUrl, 37 | }) 38 | } 39 | 40 | async deleteFile(fileName: string): Promise { 41 | await this.s3Client.delete(fileName) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/wabe-buns3/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, type S3Options } from 'bun' 2 | 3 | export class S3FileTest { 4 | presign() {} 5 | } 6 | 7 | export class S3Test { 8 | write() {} 9 | 10 | file() { 11 | return new S3FileTest() 12 | } 13 | 14 | exists() {} 15 | 16 | delete() {} 17 | } 18 | 19 | export const getS3Client = (options: S3Options): S3Client => 20 | // @ts-expect-error 21 | process.env.TEST ? new S3Test() : new S3Client(options) 22 | -------------------------------------------------------------------------------- /packages/wabe-buns3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "noEmit": true, 14 | 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noPropertyAccessFromIndexSignature": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/wabe-documentation/.gitignore: -------------------------------------------------------------------------------- 1 | # Local 2 | .DS_Store 3 | *.local 4 | *.log* 5 | 6 | # Dist 7 | node_modules 8 | dist/ 9 | doc_build/ 10 | 11 | # IDE 12 | .vscode/* 13 | !.vscode/extensions.json 14 | .idea 15 | -------------------------------------------------------------------------------- /packages/wabe-documentation/README.md: -------------------------------------------------------------------------------- 1 | # Rspress Website 2 | 3 | ## Setup 4 | 5 | Install the dependencies: 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | ## Get Started 12 | 13 | Start the dev server: 14 | 15 | ```bash 16 | npm run dev 17 | ``` 18 | 19 | Build the website for production: 20 | 21 | ```bash 22 | npm run build 23 | ``` 24 | 25 | Preview the production build locally: 26 | 27 | ```bash 28 | npm run preview 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/blog/footerArticle.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody } from "@heroui/react" 2 | import { CallToAction } from '../landing/callToAction' 3 | 4 | const view = () => ( 5 | <> 6 |
7 |
8 | 9 | 10 |

11 | ShipMySaaS 12 |

13 | 14 |

15 | The SaaS boilerplate with NextJS focus on quality, efficiency and 16 | security to build powerful SaaS applications. 17 |

18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | ) 26 | 27 | export const FooterArticle = () => view() 28 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/blog/header.tsx: -------------------------------------------------------------------------------- 1 | const view = () => ( 2 |
3 |
4 |
5 |
6 |

7 | Blog 8 |

9 | 10 |

11 | Read the latest news and updates from the team. 12 |

13 |
14 |
15 |
16 |
17 | ) 18 | 19 | export const BlogHeader = () => view() 20 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/index.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from '@heroui/divider' 2 | import { Features } from '../components/landing/features' 3 | import { Footer } from '../components/landing/footer' 4 | import Hero from '../components/landing/hero' 5 | import { Presentation } from '../components/landing/presentation' 6 | import { BlogHeader } from './blog/header' 7 | import { ListBlogArticles } from './blog/listBlogArticles' 8 | import { HeroUIProvider } from '@heroui/react' 9 | import { Example } from './landing/example' 10 | 11 | const view = () => ( 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

31 | Wabe powered{' '} 32 | 33 | ShipMySaaS 34 | {' '} 35 | ! 36 |

37 |

38 | Wabe is powered by{' '} 39 | 40 | Bun 41 | {' '} 42 | ! 43 |

44 |
45 |
46 | 47 |
48 |
49 |
50 | ) 51 | 52 | const view2 = () => ( 53 |
54 | 55 | 56 | 57 | 58 | 59 |
60 | ) 61 | 62 | export const Landing = () => view() 63 | export const Blog = () => view2() 64 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/landing/callToAction.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@heroui/button' 2 | import { Link } from '@heroui/link' 3 | import { Image } from '@heroui/image' 4 | 5 | interface Props { 6 | fullWidth?: boolean 7 | } 8 | 9 | const view = ({ fullWidth }: Props) => ( 10 | 25 | ) 26 | 27 | export const CallToAction = (props: Props) => view(props) 28 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/landing/example.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from '@heroui/image' 2 | import { computePublicPath } from '../utils' 3 | import { Card, CardBody } from '@heroui/react' 4 | 5 | const view = () => ( 6 |
7 |
8 |
9 |

10 | Start in just a few lines of code 11 |

12 |
13 | 14 | 15 | 16 |
17 | Example 18 | 19 |
20 |

21 | Launch Your Backend in Minutes 🚀 22 |

23 |

24 | Skip the hassle of backend development and focus on what truly 25 | matters your product. With just a few lines of code, get instant 26 | access to a powerful, scalable, and secure{' '} 27 | backend. 28 |

29 |
    30 |
  • 31 | ✅ Database Integration – Connect seamlessly 32 | to your preferred database with a fully typed GraphQL API. 33 |
  • 34 |
  • 35 | ✅ Authentication & Security – Built-in 36 | authentication (Google, GitHub, email/password) with 37 | role-based permissions. 38 |
  • 39 |
  • 40 | ✅ File Storage & Payments – Store files 41 | effortlessly and accept secure payments via Stripe. 42 |
  • 43 |
  • 44 | ✅ Auto-Generated GraphQL API – Define your 45 | schemas, and we’ll generate a fully functional API for you. 46 |
  • 47 |
  • 48 | ✅ AI Integration – Easily interact with AI 49 | models while we handle API complexities. 50 |
  • 51 |
52 |

53 | 💡 Stop wasting time. Get started in seconds and scale 54 | effortlessly! 55 |

56 |
57 |
58 |
59 |
60 |
61 |
62 | ) 63 | 64 | export const Example = () => view() 65 | -------------------------------------------------------------------------------- /packages/wabe-documentation/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { normalizeImagePath } from 'rspress/runtime' 2 | 3 | export const computePublicPath = (path: string) => { 4 | return normalizeImagePath(path) 5 | } 6 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/ai/index.md: -------------------------------------------------------------------------------- 1 | # Use AI model 2 | 3 | With Wabe, you have the ability to use some AI models like OpenAI either by using official adapters or by creating your own. You can then access it in your controllers or send an email using the GraphQL sendEmail mutation. 4 | 5 | ## Initialize the adapter 6 | 7 | ```ts 8 | import { Wabe } from "wabe"; 9 | import { MongoAdapter } from "wabe-mongodb" 10 | import { OpenAIAdapter } from "wabe-openai"; 11 | 12 | const run = async () => { 13 | // Ensure your database is running before run the file 14 | 15 | const wabe = new Wabe({ 16 | isProduction: process.env.NODE_ENV === 'production', 17 | // Root key example (must be long minimal 64 characters, you can generate it online) 18 | rootKey: 19 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 20 | database: { 21 | adapter: new MongoAdapter({ 22 | databaseName: "WabeApp", 23 | url: "mongodb://127.0.0.1:27045", 24 | }) 25 | }, 26 | ai: { 27 | adapter : new OpenAIAdapter("YOUR_OPENAI_SECRET_KEY", { model : "gpt-4o" }), 28 | } 29 | port: 3001, 30 | }); 31 | 32 | await wabe.start(); 33 | }; 34 | 35 | await run(); 36 | ``` 37 | 38 | ## Use controller 39 | 40 | ```ts 41 | // With controller 42 | const fn = async (context: WabeContext) => { 43 | await context.wabe.controllers.ai.createCompletion({ 44 | content: "What is the best Backend as a Service ?" 45 | }); 46 | } 47 | ``` 48 | 49 | ## OpenAI adapter 50 | 51 | You can easily initialize an adapter like this by passing your API key as a parameter to the adapter. 52 | 53 | ```ts 54 | import { Wabe } from "wabe"; 55 | import { OpenAIAdapter } from "wabe-openai"; 56 | 57 | const run = async () => { 58 | const wabe = new Wabe({ 59 | // ... others config fields 60 | email: { 61 | adapter: new OpenAIAdapter("API_KEY"), 62 | }, 63 | }); 64 | 65 | await wabe.start(); 66 | }; 67 | 68 | await run(); 69 | ``` 70 | 71 | ## Create your own adapter 72 | 73 | You can create your own adapter implementing the interface [here](https://github.com/palixir/wabe/blob/main/packages/wabe/src/ai/interface.ts) 74 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/defaultMethods.md: -------------------------------------------------------------------------------- 1 | # Default authentication methods 2 | 3 | Wabe provides some default authentication methods that you can use in your project. 4 | 5 | Here is a list of the default authentication methods: 6 | 7 | - Email password 8 | - Phone password 9 | - Google 10 | 11 | ```graphql 12 | # Email password 13 | mutation signUpWith { 14 | signUpWith( 15 | input: {authentication: {emailPassword: {email: "your.email@gmail.com", password: "password"}}} 16 | ) { 17 | id 18 | accessToken 19 | refreshToken 20 | } 21 | } 22 | # Phone password 23 | mutation signUpWith { 24 | signUpWith( 25 | input: {authentication: {phonePassword: {phone: "+33601020304", password: "password"}}} 26 | ) { 27 | id 28 | accessToken 29 | refreshToken 30 | } 31 | } 32 | 33 | #Google 34 | mutation signUpWith { 35 | signUpWith( 36 | input: {authentication: {google: {authorizationCode: "authorizationCode", codeVerifier: "codeVerifier"}}} 37 | ) { 38 | id 39 | accessToken 40 | refreshToken 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/interact.md: -------------------------------------------------------------------------------- 1 | # Interact with authentications methods 2 | 3 | To interact with your various authentication methods, you can use the `GraphQL API`, which by default provides `mutations`for signing up, logging in, and logging out. Here’s how you can perform these three actions: 4 | 5 | When logging in or out, the inputs you pass as parameters to the mutation correspond to the `inputs` defined during the configuration of the `authentication` method. When logging in or signing up, a session is created (if you have chosen a session system based on cookies, a cookie will also be created). 6 | 7 | ## Sign up 8 | 9 | ```graphql 10 | mutation signUpWith { 11 | signUpWith( 12 | input: {authentication: {emailPassword: {email: "your.email@gmail.com", password: "password"}}} 13 | ) { 14 | id 15 | accessToken 16 | refreshToken 17 | } 18 | } 19 | ``` 20 | 21 | ## Sign in 22 | 23 | ```graphql 24 | mutation signInWith { 25 | signInWith( 26 | input: {authentication: {emailPassword: {email: "your.email@gmail.com", password: "password"}}} 27 | ) { 28 | id 29 | accessToken 30 | refreshToken 31 | } 32 | } 33 | ``` 34 | 35 | ## Sign out 36 | 37 | ```graphql 38 | mutation signOut{ 39 | signOut 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/oauth.md: -------------------------------------------------------------------------------- 1 | # OAuth 2 | 3 | Wabe offers authentication methods following the `OAuth` protocol, allowing you to enable `sign-in` with services like Google. To set this up, you first need to define a `success path` and a `failure path`. The success path is where the user will be redirected if the login is successful; otherwise, they will be redirected to the error path. 4 | 5 | Once this is configured, on the front-end, you only need to create a "Sign in with ..." button that redirects to the URL `https://127.0.0.1:3001/auth/oauth?provider=google` (example for **google** provider). 6 | 7 | ```ts 8 | import { Wabe } from "wabe"; 9 | 10 | const run = async () => { 11 | const wabe = new Wabe({ 12 | // ... others config fields 13 | authentication: { 14 | session: { 15 | cookieSession: true, 16 | accessTokenExpiresInMs: 1000 * 60 * 15, // 15 minutes 17 | refreshTokenExpiresInMs: 1000 * 60 * 60 * 24 * 7, // 7 days 18 | }, 19 | backDomain: process.env.BACK_DOMAIN, 20 | successRedirectPath: 'https://app.com/dashboard', 21 | failureRedirectPath: 'https://app.com/signin', 22 | roles: ['Admin', 'Client'], 23 | providers: { 24 | google: { 25 | clientId: process.env.GOOGLE_CLIENT_ID || '', 26 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | await wabe.start(); 33 | }; 34 | 35 | await run(); 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/resetPassword.md: -------------------------------------------------------------------------------- 1 | # Reset password 2 | 3 | To reset password for provider like `emailPassword` you will need 2 mutations. The first one will send an email to the user with an OTP code valid for 5 minutes. The second one will be used to reset the password (with the OTP code in parameter). 4 | 5 | ## Send OTP code 6 | 7 | First we send the OTP code to the user. 8 | 9 | ```graphql 10 | mutation sendOtpCode { 11 | sendOtpCode(input: {email: "your.email@gmail.com"}) 12 | } 13 | ``` 14 | 15 | ## Update the password of the user 16 | 17 | In a second time, we can reset the password with the OTP code. 18 | 19 | ```graphql 20 | mutation resetPassword { 21 | resetPassword( 22 | input: { 23 | email: "your.email@gmail.com" 24 | password: "newPassword" 25 | otp: "123456" 26 | provider: emailPassword 27 | } 28 | ) 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/roles.md: -------------------------------------------------------------------------------- 1 | # Roles 2 | 3 | ## Configure user's role 4 | 5 | In Wabe, you have the ability to create as many `roles` as you like. This allows you to assign specific permissions to these roles by managing user roles. When the server is launched, roles will be created in the Role table if they don't already exist in the database. 6 | 7 | It’s really simple to do: 8 | 9 | ```ts 10 | import { Wabe } from "wabe"; 11 | 12 | const run = async () => { 13 | const wabe = new Wabe({ 14 | // ... others config fields 15 | authentication: { 16 | roles: ["Admin", "Client"], 17 | }, 18 | }); 19 | 20 | await wabe.start(); 21 | }; 22 | 23 | await run(); 24 | ``` 25 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/authentication/sessions.md: -------------------------------------------------------------------------------- 1 | # Sessions 2 | 3 | ## Configuration session parameters 4 | 5 | Wabe gives you the ability to configure your `session` parameters. You can choose the duration of the generated `Access Token` and the duration of the generated `Refresh Token`. You can also decide whether to manage your sessions via `cookies` (so the frontend doesn’t need to do anything) or to avoid storing Access Tokens in cookies (in which case the frontend must send the Access Token in the `Wabe-Access-Token` header with each request). 6 | 7 | The `refreshToken` and the `accessToken` are store in the `Session` table in database. The `refreshToken` and the `accessToken` are automatically change after each request when the `cookieSession` is used to limit the possibilities in case of steal. 8 | 9 | ```ts 10 | import { Wabe } from "wabe"; 11 | 12 | const run = async () => { 13 | const wabe = new Wabe({ 14 | // ... others config fields 15 | authentication: { 16 | session: { 17 | // 15 minutes in ms 18 | accessTokenExpiresInMs: 1000 * 60 * 15, 19 | // 1 day in ms 20 | refreshTokenExpiresInMs: 1000 * 60 * 60 * 24, 21 | cookieSession: true, 22 | }, 23 | }, 24 | }); 25 | 26 | await wabe.start(); 27 | }; 28 | 29 | await run(); 30 | ``` 31 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/codegen.md: -------------------------------------------------------------------------------- 1 | # Codegen 2 | 3 | With Wabe, you have the option to configure code generation (`codegen`). This feature allows you to generate a schema.graphql file that represents the entirety of the schema you've defined using Wabe. This can be particularly useful for generating types for the front-end or for other uses. 4 | 5 | Additionally, the `codegen` creates a `wabe.ts` file in the specified folder. This file contains all the TypeScript types corresponding to your schema. You can, for instance, use it to define [WabeTypes](/wabe/concepts.md#wabetypes) or to have fully typed elements in your backend. 6 | 7 | ```ts 8 | import { Wabe } from "wabe"; 9 | 10 | const run = async () => { 11 | const wabe = new Wabe({ 12 | // ... others config fields 13 | codegen: { 14 | enabled: true, 15 | path: `${import.meta.dirname}/../generated/`, 16 | }, 17 | }); 18 | 19 | await wabe.start(); 20 | }; 21 | 22 | await run(); 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/cron/index.md: -------------------------------------------------------------------------------- 1 | # Crons 2 | 3 | Wabe allows you to easily schedule recurring tasks with the integration of crons. You can configure tasks to run at specific intervals without manually managing their execution. 4 | 5 | :::warning Warning: 6 | The cron system is not designed to handle heavy background tasks. Tasks run on the main server, and heavy tasks could overload the server. Currently, Wabe does not support the management of heavy background tasks (lasting several minutes). 7 | ::: 8 | 9 | ## Configuration 10 | 11 | To configure a cron in Wabe, you can use the `crons` option when initializing your Wabe instance. Here is an example configuration: 12 | 13 | ```ts 14 | import { Wabe, cron, CronExpressions } from "wabe"; 15 | import { MongoAdapter } from "wabe-mongodb" 16 | 17 | const run = async () => { 18 | // Ensure your database is running before run the file 19 | const wabe = new Wabe({ 20 | isProduction: process.env.NODE_ENV === 'production', 21 | // Root key example (must be long minimal 64 characters, you can generate it online) 22 | rootKey: 23 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 24 | database: { 25 | adapter: new MongoAdapter({ 26 | databaseName: "WabeApp", 27 | url: "mongodb://127.0.0.1:27045", 28 | }) 29 | }, 30 | crons: [ 31 | { 32 | name: 'test', 33 | cron: cron({ 34 | pattern: '* * * * * *', 35 | run: (wabe) => console.log('test', wabe.config.port), 36 | }), 37 | }, 38 | ], 39 | port: 3001, 40 | }); 41 | 42 | await wabe.start(); 43 | }; 44 | 45 | await run(); 46 | ``` 47 | 48 | ## Usage 49 | 50 | Crons can be used for various tasks, such as cleaning up the database, sending periodic notifications, or any other recurring task needed for your application. 51 | 52 | - Pattern: The cron pattern follows the standard cron format, where each asterisk (*) represents a unit of time (second, minute, hour, day of the month, month, day of the week). 53 | - Run: The run function contains the code to be executed at each interval defined by the pattern. 54 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wabe - Your backend in minutes not days for Node.js / Bun 3 | layout: page 4 | sidebar: false 5 | --- 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/auth.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/auth.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/copy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/copy.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/cover2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/cover2.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/database.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/database.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/email.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/email.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/favicon.ico -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/github.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/github.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/graphql.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/graphql.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/graphqlPlayground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/graphqlPlayground.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/graphqlPlayground2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/graphqlPlayground2.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/hooks.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/hooks.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/logo.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/payment.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/payment.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/permissions.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/documentation/public/permissions.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Disallow: /cgi-bin/ 4 | Sitemap: https://wabe.dev/sitemap.xml 5 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/rootKey.md: -------------------------------------------------------------------------------- 1 | # Wabe root key 2 | 3 | The root key is a critical element of a Wabe application. It allows you to execute a request (via the GraphQL API or the DatabaseController) by bypassing all security checks. It should be used ONLY by the backend for root operations, such as in recurring application cron jobs or within a resolver. It should never be passed to the frontend and must be stored as a secret in the hosting infrastructure. It can also be used in the GraphQL playground to test queries without passing a user ID in the request header. To function, the root key must be included in the request header as Wabe-Root-Key. In the backend, the isRoot field of the WabeContext can also be set to true. 4 | 5 | ```ts 6 | import { Wabe } from "wabe"; 7 | 8 | const run = async () => { 9 | const wabe = new Wabe({ 10 | // ... others config fields 11 | rootKey: 12 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 13 | }); 14 | 15 | await wabe.start(); 16 | }; 17 | 18 | await run(); 19 | ``` 20 | 21 | Example of GraphQLClient creates with root key : 22 | 23 | ```ts 24 | const client = new GraphQLClient(`http://127.0.0.1:3001/graphql`, { 25 | headers: { 26 | "Wabe-Root-Key": "YourRootKeyAsLongAsPossible", 27 | }, 28 | }); 29 | ``` 30 | 31 | **Recommendations:** 32 | The **rootKey** should be as long as possible (at least 64 characters) and include letters, special characters, and numbers. 33 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/routes.md: -------------------------------------------------------------------------------- 1 | # Routes 2 | 3 | Because we know that every application is unique, you can also add custom REST endpoints. Wabe uses Wobe under the hood as its web framework. You can create routes just like in the example below. The handler's parameter is a `WobeContext` (see [here](https://wobe.dev) for more information). 4 | 5 | ```ts 6 | import { Wabe } from "wabe"; 7 | 8 | const run = async () => { 9 | const wabe = new Wabe({ 10 | // ... others config fields 11 | routes: [ 12 | { 13 | path: "/hello", 14 | method: "GET", 15 | handler: (ctx) => { 16 | return ctx.res.send("Hello world !"); 17 | }, 18 | }, 19 | ], 20 | }); 21 | 22 | await wabe.start(); 23 | }; 24 | 25 | await run(); 26 | ``` 27 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/schema/enums.md: -------------------------------------------------------------------------------- 1 | # Enums 2 | 3 | With Wabe, you can create your own `enums` (a TypeScript enum will also be generated if you have codegen enabled). After **restarting** your server (to trigger the codegen), you can easily use it as a field type, as shown in the example below. 4 | 5 | ```ts 6 | import { Wabe } from "wabe"; 7 | 8 | const run = async () => { 9 | const wabe = new Wabe({ 10 | // ... others config fields 11 | schema: { 12 | classes: [ 13 | { 14 | name: "Company", 15 | fields: { 16 | country: { 17 | type: "Country", 18 | }, 19 | }, 20 | }, 21 | ], 22 | enums: [ 23 | { 24 | name: "Country", 25 | description: "Enum that represents all the Country", 26 | values: { 27 | France: "FR", 28 | UnitedStates: "USA", 29 | }, 30 | }, 31 | ], 32 | }, 33 | }); 34 | 35 | await wabe.start(); 36 | }; 37 | 38 | await run(); 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/schema/resolvers.md: -------------------------------------------------------------------------------- 1 | # Resolvers 2 | 3 | In the `schema` object of the `Wabe` configuration, you can also define resolvers. These resolvers correspond to GraphQL resolvers (when we integrate automatic REST API generation, they will correspond to REST endpoints). They are divided into two categories: on one side, queries, which allow you to request data or return a result without performing any mutations on the data in the database; and on the other side, mutations, which, as their name suggests, allow you to modify the data in the database. 4 | 5 | ## Queries 6 | 7 | For each `query` you choose to create, you can give it a name (in the example below, "helloWorld"). You can also specify a return type (supported types in Wabe include String, Int, Float, Boolean, File, etc.). Additionally, you can provide arguments if needed. Finally, you must assign it a resolver function that contains the code to execute when your query is called. 8 | 9 | ```ts 10 | import { Wabe } from "wabe"; 11 | 12 | const run = async () => { 13 | const wabe = new Wabe({ 14 | // ... others configs fields 15 | schema: { 16 | resolvers: { 17 | queries: { 18 | helloWorld: { 19 | // Output type 20 | type: "String", 21 | // Description of the query 22 | description: "Hello world description", 23 | // Arguments of the query 24 | args: { 25 | name: { 26 | type: "String", 27 | required: true, 28 | }, 29 | }, 30 | // The resolver to call when we call the query 31 | // Context argument contains the Wabe context (you can see more information about it in the Context sections in Wabe notions) 32 | resolve: (root, args, context) => "Hello World", 33 | }, 34 | }, 35 | }, 36 | }, 37 | }); 38 | 39 | await wabe.start(); 40 | }; 41 | 42 | await run(); 43 | ``` 44 | 45 | ## Mutations 46 | 47 | Just like with queries, you can create `mutations` (such as "sumAndUpdateResult" in the example below) with a return type, arguments (within the input object), and a resolver function. 48 | 49 | ```ts 50 | import { Wabe } from "wabe"; 51 | 52 | const run = async () => { 53 | const wabe = new Wabe({ 54 | // ... others configs fields 55 | schema: { 56 | resolvers: { 57 | sumAndUpdateResult: { 58 | type: "Int", 59 | args: { 60 | input: { 61 | a: { 62 | type: "Int", 63 | }, 64 | b: { 65 | type: "Int", 66 | }, 67 | }, 68 | }, 69 | resolve: (root, args) => { 70 | const sum = args.input.a + args.input.b; 71 | 72 | // ... suppose we update sum in database 73 | 74 | return sum; 75 | }, 76 | }, 77 | }, 78 | }, 79 | }); 80 | 81 | await wabe.start(); 82 | }; 83 | 84 | await run(); 85 | ``` 86 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/schema/scalars.md: -------------------------------------------------------------------------------- 1 | # Scalars 2 | 3 | With Wabe, you can create your own `scalars` (a TypeScript type will also be generated if you have codegen enabled). After **restarting** your server (to trigger the codegen), you can easily use them as a field type, as shown in the example below. 4 | 5 | To define them, you can specify the same fields as for a GraphQL scalar (parseValue, parseLiteral, serialize, see [here](https://www.apollographql.com/docs/apollo-server/schema/custom-scalars/#serialize) for more information). 6 | 7 | ```ts 8 | import { Wabe } from "wabe"; 9 | 10 | const run = async () => { 11 | const wabe = new Wabe({ 12 | // ... others config fields 13 | schema: { 14 | classes: [ 15 | { 16 | name: "Company", 17 | fields: { 18 | name: { 19 | type: "String", 20 | }, 21 | contactPhone: { 22 | type: "Phone", 23 | }, 24 | }, 25 | }, 26 | ], 27 | scalars: [ 28 | { 29 | name: "Phone", 30 | description: "Phone scalar", 31 | parseValue(value: any) { 32 | if (typeof value !== "string") { 33 | throw new Error("Invalid phone"); 34 | } 35 | 36 | if ( 37 | !value.match( 38 | /^(?:(?:\+33|0033)[\s.-]?)?[1-9](?:[\s.-]?\d{2}){4}$/, 39 | ) 40 | ) { 41 | throw new Error("Invalid phone"); 42 | } 43 | 44 | return value; 45 | }, 46 | }, 47 | ], 48 | }, 49 | }); 50 | 51 | await wabe.start(); 52 | }; 53 | 54 | await run(); 55 | ``` 56 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/wabe/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | Wabe has several concepts unique to it. We will define and explain them here, as you may encounter them in various parts of the documentation. 4 | 5 | ## WabeTypes 6 | 7 | `Wabe` includes a configuration option that generates two codegen files (this option can be disabled). If enabled, you will get a `schema.graphql` file as well as a `wabe.ts` file in the directory you specified. The wabe.ts file contains your entire schema in the form of TypeScript types, including the types of your various classes, enums you've defined, and scalars. 8 | 9 | When declaring multiple types (like the Wabe class, the WabeContext, etc.), you can define a generic type to propagate your codegen-generated types (see example below). 10 | 11 | ```ts 12 | import type { WabeSchemaEnums, WabeSchemaTypes } from "../generated/wabe"; 13 | 14 | export type YourTypes = { 15 | enums: WabeSchemaEnums; 16 | types: WabeSchemaTypes; 17 | scalars: WabeSchemaScalars; 18 | }; 19 | 20 | // Now you can add this types in the declaration of others class/types to propagate the types 21 | 22 | const wabe = new Wabe(...) 23 | 24 | // or 25 | 26 | const anyFunction = (context: WabeContext) => {} 27 | ``` 28 | 29 | ## WabeContext 30 | 31 | The Wabe context is an object used in multiple places within Wabe, such as in GraphQL resolvers or in every call to the `DatabaseController`. It contains information about the request (including the person who initiated it). It also includes the Wabe configuration, allowing access to information like schemas or authentication elements within resolvers and during processing. Here is the `WabeContext` type : 32 | 33 | ```ts 34 | export interface WabeContext { 35 | response?: WobeResponse; 36 | user?: User | null; 37 | sessionId?: string; 38 | isRoot: boolean; 39 | wabe: Wabe; 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/documentation/wabe/motivations.md: -------------------------------------------------------------------------------- 1 | # Motivations 2 | 3 | ## What is Wabe ? 4 | 5 | Wabe is a solution that simplifies the creation of your backends in TypeScript. It handles all the complexities of a backend for you and offers an all-in-one solution that can be set up in just a few minutes. It manages the database, authentication, automatic generation of your GraphQL API based on the schema you’ve defined, security with a multi-level permission system, a hook system that allows you to perform actions before or after each database query, and many other features that you can explore by browsing the documentation. And because we know that every application is unique, Wabe allows you to customize everything to meet your needs. You can, for example, add your own authentication methods, your own GraphQL resolvers, your own REST routes, your own enums, your own GraphQL scalars, and much more. Wabe is both a comprehensive and customizable toolbox. 6 | 7 | ## Why should you use Wabe ? 8 | 9 | After working with Parse Server for a long time, which also offers a simplified backend solution, I identified several issues that make the tool difficult for many users (especially those not deeply familiar with the project): the absence of TypeScript, unclear documentation, meaning no type safety during development, the lack of key features like typing GraphQL objects without using Nexus, and the project's very limited evolution. On the other hand, recent solutions like Supabase have emerged, but they severely lack customization options. Supabase can be used for relatively simple applications (CRUD), but when it comes to complex applications that require multiple elements, such as connections to external APIs, new GraphQL resolvers with very specific behaviors, etc., it falls short. With Wabe, we aimed to combine the best of both worlds: the modernity of one and the flexibility of the other. It is with this idea in mind that Wabe was born, and it has a bright future ahead! 10 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | pageType: custom 3 | --- 4 | import {Landing} from '../components/index' 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/appleTouchIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/appleTouchIcon.png -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/auth.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/auth.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/code.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/code.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/copy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/copy.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/cover.png -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/database.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/database.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/email.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/email.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/favicon.ico -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/google.svg: -------------------------------------------------------------------------------- 1 | 7 | Google logo 8 | 12 | 16 | 20 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/graphql.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 72 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/graphql.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/graphql.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/graphqlPlayground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/graphqlPlayground.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/graphqlPlayground2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/graphqlPlayground2.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/hooks.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/hooks.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/logo.png -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/logoWithoutBackground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/logoWithoutBackground.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/logoXWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/logoXWhite.png -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/mongodb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/payment.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/payment.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/permissions.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/permissions.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/resend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Disallow: /cgi-bin/ 4 | Sitemap: https://wabe.dev/sitemap.xml 5 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/schema.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe-documentation/docs/public/assets/schema.webp -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/assets/stripe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | Disallow: /cgi-bin/ 4 | Sitemap: https://shipmysaas.com/sitemap.xml 5 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/scripts/data.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";(t=>{const{screen:{width:e,height:a},navigator:{language:r},location:n,document:i,history:s}=t,{hostname:c,href:o,origin:u}=n,{currentScript:l,referrer:d}=i,h=o.startsWith("data:")?void 0:t.localStorage;if(!l)return;const m="data-",f="true",p=l.getAttribute.bind(l),g=p(m+"website-id"),y=p(m+"host-url"),b=p(m+"tag"),v="false"!==p(m+"auto-track"),w=p(m+"exclude-search")===f,S=p(m+"exclude-hash")===f,N=p(m+"domains")||"",T=N.split(",").map((t=>t.trim())),A=`${(y||"https://api-gateway.umami.dev"||l.src.split("/").slice(0,-1).join("/")).replace(/\/$/,"")}/api/send`,j=`${e}x${a}`,x=/data-umami-event-([\w-_]+)/,O=m+"umami-event",k=300,E=t=>{try{const{pathname:e,search:a,hash:r}=new URL(t,n.href);return e+(w?"":a)+(S?"":r)}catch(e){return t}},L=()=>({website:g,hostname:c,screen:j,language:r,title:J,url:C,referrer:I,tag:b||void 0}),$=(t,e,a)=>{a&&(I=C,C=E(a.toString()),C!==I&&setTimeout(D,k))},K=()=>M||!g||h&&h.getItem("umami.disabled")||N&&!T.includes(c),U=async(t,e="event")=>{if(K())return;const a={"Content-Type":"application/json"};void 0!==_&&(a["x-umami-cache"]=_);try{const r=await fetch(A,{method:"POST",body:JSON.stringify({type:e,payload:t}),headers:a}),n=await r.json();n&&(M=!!n.disabled,_=n.cache)}catch(t){}},B=()=>{q||(D(),(()=>{const t=(t,e,a)=>{const r=t[e];return(...e)=>(a.apply(null,e),r.apply(t,e))};s.pushState=t(s,"pushState",$),s.replaceState=t(s,"replaceState",$)})(),(()=>{const t=new MutationObserver((([t])=>{J=t&&t.target?t.target.text:void 0})),e=i.querySelector("head > title");e&&t.observe(e,{subtree:!0,characterData:!0,childList:!0})})(),i.addEventListener("click",(async t=>{const e=t=>["BUTTON","A"].includes(t),a=async t=>{const e=t.getAttribute.bind(t),a=e(O);if(a){const r={};return t.getAttributeNames().forEach((t=>{const a=t.match(x);a&&(r[a[1]]=e(t))})),D(a,r)}},r=t.target,i=e(r.tagName)?r:((t,a)=>{let r=t;for(let t=0;t{c||(n.href=e)}))}else if("BUTTON"===i.tagName)return a(i)}}),!0),q=!0)},D=(t,e)=>U("string"==typeof t?{...L(),name:t,data:"object"==typeof e?e:void 0}:"object"==typeof t?t:"function"==typeof t?t(L()):L()),W=t=>U({...L(),data:t},"identify");t.umami||(t.umami={track:D,identify:W});let _,q,C=E(o),I=d.startsWith(u)?"":d,J=i.title,M=!1;v&&!K()&&("complete"===i.readyState?B():i.addEventListener("readystatechange",B,!0))})(window)}(); 2 | -------------------------------------------------------------------------------- /packages/wabe-documentation/docs/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://shipmysaas.com/ 12 | 2024-12-19T12:14:01+00:00 13 | 1.00 14 | 15 | 16 | https://shipmysaas.com/index.html 17 | 2024-12-19T12:14:01+00:00 18 | 0.80 19 | 20 | 21 | https://shipmysaas.com/.html 22 | 2024-12-19T12:14:01+00:00 23 | 0.80 24 | 25 | 26 | https://shipmysaas.com/privacy 27 | 2024-12-19T12:14:01+00:00 28 | 0.80 29 | 30 | 31 | https://shipmysaas.com/terms-of-services 32 | 2024-12-19T12:14:01+00:00 33 | 0.80 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/wabe-documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-documentation", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "release": "bun run build && vercel doc_build --prod", 7 | "dev": "rspress dev", 8 | "build": "if [ -z \"$CI_BUILD\" ]; then rspress build; else echo \"CI_BUILD is defined, skipping build\"; fi", 9 | "preview": "rspress preview" 10 | }, 11 | "dependencies": { 12 | "rspress": "1.40.2", 13 | "@heroui/react": "2.6.13", 14 | "@heroicons/react": "2.2.0" 15 | }, 16 | "devDependencies": { 17 | "postcss": "8.4.49", 18 | "tailwindcss": "3.4.16" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/wabe-documentation/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/wabe-documentation/styles/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | /* body { 6 | background-color: #121212 !important ; 7 | } 8 | 9 | .rspress-nav { 10 | background-color: #121212 !important; 11 | } 12 | 13 | .rspress-sidebar-menu { 14 | background-color: #121212 !important; 15 | } 16 | 17 | .rspress-sidebar { 18 | background-color: #121212 !important; 19 | } */ 20 | -------------------------------------------------------------------------------- /packages/wabe-documentation/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { heroui } = require('@heroui/react') 2 | 3 | module.exports = { 4 | content: [ 5 | './components/**/*.tsx', 6 | './docs/**/*.mdx', 7 | '../../node_modules/@heroui/**/*.{js,ts,jsx,tsx,mjs}', 8 | './node_modules/@heroui/**/*.{js,ts,jsx,tsx,mjs}', 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: 'class', 14 | plugins: [ 15 | heroui({ 16 | defaultTheme: 'light', 17 | addCommonColors: true, 18 | themes: { 19 | dark: { 20 | colors: { 21 | background: '#121212', 22 | 'background-secondary': '#18181b', 23 | 'muted-foreground': '#ABA9A3', 24 | foreground: '#EEEEEC', 25 | }, 26 | }, 27 | light: { 28 | colors: { 29 | background: '#ffffff', 30 | 'background-secondary': '#f8f9fa', 31 | }, 32 | }, 33 | }, 34 | }), 35 | ], 36 | } 37 | -------------------------------------------------------------------------------- /packages/wabe-documentation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | ".next/types/**/*.ts", 28 | "tests/**/*.ts" 29 | ], 30 | "exclude": ["node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /packages/wabe-mistralai/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mistralai 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mistralai # On bun 21 | npm install wabe-mistralai # On npm 22 | yarn add wabe-mistralai # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mistralai usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | import { MistralAIAdapter } from "wabe-mistralai"; 31 | 32 | const run = async () => { 33 | // Ensure your database is running before run the file 34 | 35 | const wabe = new Wabe({ 36 | isProduction: process.env.NODE_ENV === 'production', 37 | // Root key example (must be long minimal 64 characters, you can generate it online) 38 | rootKey: 39 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 40 | database: { 41 | adapter: new MongoAdapter({ 42 | databaseName: "WabeApp", 43 | url: "mongodb://127.0.0.1:27045", 44 | }) 45 | }, 46 | ai: { 47 | adapter : new MistralAIAdapter("YOUR_MISTRAL_SECRET_KEY"), 48 | } 49 | port: 3001, 50 | }); 51 | 52 | await wabe.start(); 53 | 54 | await wabe.controllers.ai.createCompletion({ 55 | content: "What is the best Backend as a Service ?" 56 | }); 57 | }; 58 | 59 | await run(); 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/wabe-mistralai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-mistralai", 3 | "version": "0.5.0", 4 | "description": "MistralAI adapter for Wabe (official)", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas", 16 | "mistral", 17 | "ai" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/palixir/wabe.git" 22 | }, 23 | "main": "dist/index.js", 24 | "scripts": { 25 | "build": "bun --filter wabe-build build:package $(pwd)", 26 | "check": "tsc --project $(pwd)/tsconfig.json", 27 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 28 | "ci": "bun check && bun lint $(pwd) && bun test src", 29 | "format": "biome format --write . --config-path=../../" 30 | }, 31 | "dependencies": { 32 | "@mistralai/mistralai": "1.5.1" 33 | }, 34 | "devDependencies": { 35 | "@types/bun": "latest", 36 | "wabe": "workspace:*" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/wabe-mistralai/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock, afterEach } from 'bun:test' 2 | 3 | const mockCreateCompletion = mock(() => 4 | Promise.resolve({ 5 | choices: [{ message: { content: 'Mocked response' } }], 6 | }), 7 | ) 8 | 9 | mock.module('@mistralai/mistralai', () => ({ 10 | Mistral: class { 11 | chat = { 12 | complete: mockCreateCompletion, 13 | } 14 | }, 15 | })) 16 | 17 | import { MistralAIAdapter } from '.' 18 | 19 | describe('MistralAIAdapter', () => { 20 | afterEach(() => { 21 | mockCreateCompletion.mockClear() 22 | }) 23 | 24 | it('should create a completion with OpenAI', async () => { 25 | const adapter = new MistralAIAdapter('FAKE_API_KEY') 26 | 27 | await adapter.createCompletion({ 28 | content: 'my content', 29 | }) 30 | 31 | expect(mockCreateCompletion).toHaveBeenCalledTimes(1) 32 | expect(mockCreateCompletion).toHaveBeenCalledWith({ 33 | messages: [{ role: 'user', content: 'my content' }], 34 | model: 'mistral-small-latest', 35 | stream: false, 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/wabe-mistralai/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Mistral } from '@mistralai/mistralai' 2 | import type { AIAdapter, CreateCompletionOptions } from 'wabe' 3 | 4 | export class MistralAIAdapter implements AIAdapter { 5 | public mistral: Mistral 6 | 7 | constructor(apiKey: string) { 8 | this.mistral = new Mistral({ apiKey }) 9 | } 10 | 11 | async createCompletion({ content }: CreateCompletionOptions) { 12 | const result = await this.mistral.chat.complete({ 13 | model: 'mistral-small-latest', 14 | stream: false, 15 | messages: [ 16 | { 17 | content, 18 | role: 'user', 19 | }, 20 | ], 21 | }) 22 | 23 | const output = result.choices?.[0].message.content as string 24 | 25 | if (!output) throw new Error('No content found') 26 | 27 | return output 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/wabe-mistralai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-mongodb-launcher/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mongodb-launcher 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mongodb-launcher # On bun 21 | npm install wabe-mongodb-launcher # On npm 22 | yarn add wabe-mongodb-launcher # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mongodb-launcher usage 26 | 27 | ```ts 28 | import { runDatabase } from 'wabe-mongodb-launcher' 29 | 30 | 31 | await runDatabase() 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/wabe-mongodb-launcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-mongodb-launcher", 3 | "version": "0.5.2", 4 | "license": "Apache-2.0", 5 | "main": "dist/index.js", 6 | "description": "Package to launch the mongodb for test", 7 | "scripts": { 8 | "ci":"bun lint", 9 | "build": "bun --filter wabe-build build:package $(pwd)", 10 | "lint": "biome lint . --config-path=../../ --no-errors-on-unmatched", 11 | "format": "biome format --config-path=../../ --write ." 12 | }, 13 | "dependencies": { 14 | "tcp-port-used": "1.0.2", 15 | "mongodb-memory-server": "10.1.4" 16 | }, 17 | "devDependencies": { 18 | "@types/bun": "latest", 19 | "@types/tcp-port-used": "1.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/wabe-mongodb-launcher/src/index.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server' 2 | import tcpPortUsed from 'tcp-port-used' 3 | 4 | export const runDatabase = async (): Promise => { 5 | if (await tcpPortUsed.check(27045, '127.0.0.1')) return 6 | 7 | await MongoMemoryServer.create({ 8 | binary: { 9 | version: '8.0.5', 10 | }, 11 | instance: { 12 | port: 27045, 13 | }, 14 | }) 15 | 16 | console.info('MongoDB started') 17 | } 18 | -------------------------------------------------------------------------------- /packages/wabe-mongodb-launcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-mongodb/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mongodb 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mongodb # On bun 21 | npm install wabe-mongodb # On npm 22 | yarn add wabe-mongodb # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mongodb usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | 31 | const run = async () => { 32 | // Ensure your database is running before run the file 33 | 34 | const wabe = new Wabe({ 35 | isProduction: process.env.NODE_ENV === 'production', 36 | // Root key example (must be long minimal 64 characters, you can generate it online) 37 | rootKey: 38 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 39 | database: { 40 | adapter: new MongoAdapter({ 41 | databaseName: "WabeApp", 42 | url: "mongodb://127.0.0.1:27045", 43 | }) 44 | }, 45 | port: 3001, 46 | }); 47 | 48 | await wabe.start(); 49 | }; 50 | 51 | await run(); 52 | ``` 53 | -------------------------------------------------------------------------------- /packages/wabe-mongodb/bunfig.toml: -------------------------------------------------------------------------------- 1 | telemetry = false 2 | 3 | [test] 4 | preload = ['./utils/preload.ts'] 5 | -------------------------------------------------------------------------------- /packages/wabe-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-mongodb", 3 | "version": "0.5.0", 4 | "description": "MongoDB adapter for Wabe (official)", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas", 16 | "mongodb" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/palixir/wabe.git" 21 | }, 22 | "main": "dist/index.js", 23 | "scripts": { 24 | "build": "bun --filter wabe-build build:package $(pwd)", 25 | "check": "tsc --project $(pwd)/tsconfig.json", 26 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 27 | "ci": "bun check && bun lint $(pwd) && bun test src", 28 | "format": "biome format --write . --config-path=../../" 29 | }, 30 | "dependencies": { 31 | "mongodb": "6.15.0" 32 | }, 33 | "devDependencies": { 34 | "@types/bun": "latest", 35 | "wabe": "workspace:*", 36 | "wabe-mongodb-launcher": "workspace:*", 37 | "get-port": "7.1.0", 38 | "uuid": "10.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/wabe-mongodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-mongodb/utils/preload.ts: -------------------------------------------------------------------------------- 1 | import { runDatabase } from 'wabe-mongodb-launcher' 2 | 3 | const setupEnvironment = () => { 4 | process.env.TEST = 'true' 5 | } 6 | 7 | await runDatabase() 8 | setupEnvironment() 9 | -------------------------------------------------------------------------------- /packages/wabe-openai/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-openai 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-openai # On bun 21 | npm install wabe-openai # On npm 22 | yarn add wabe-openai # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-openai usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | import { OpenAIAdapter } from "wabe-openai"; 31 | 32 | const run = async () => { 33 | // Ensure your database is running before run the file 34 | 35 | const wabe = new Wabe({ 36 | isProduction: process.env.NODE_ENV === 'production', 37 | // Root key example (must be long minimal 64 characters, you can generate it online) 38 | rootKey: 39 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 40 | database: { 41 | adapter: new MongoAdapter({ 42 | databaseName: "WabeApp", 43 | url: "mongodb://127.0.0.1:27045", 44 | }) 45 | }, 46 | ai: { 47 | adapter : new OpenAIAdapter("YOUR_OPENAI_SECRET_KEY", { model : "gpt-4o" }), 48 | } 49 | port: 3001, 50 | }); 51 | 52 | await wabe.start(); 53 | 54 | await wabe.controllers.ai.createCompletion({ 55 | content: "What is the best Backend as a Service ?" 56 | }); 57 | }; 58 | 59 | await run(); 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/wabe-openai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-openai", 3 | "version": "0.5.0", 4 | "description": "OpenAI adapter for Wabe (official)", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas", 16 | "openai" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/palixir/wabe.git" 21 | }, 22 | "main": "dist/index.js", 23 | "scripts": { 24 | "build": "bun --filter wabe-build build:package $(pwd)", 25 | "check": "tsc --project $(pwd)/tsconfig.json", 26 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 27 | "ci": "bun check && bun lint $(pwd) && bun test src", 28 | "format": "biome format --write . --config-path=../../" 29 | }, 30 | "dependencies": { 31 | "openai": "4.79.1" 32 | }, 33 | "devDependencies": { 34 | "@types/bun": "latest", 35 | "wabe": "workspace:*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/wabe-openai/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock, afterEach } from 'bun:test' 2 | 3 | // Mock pour OpenAI 4 | const mockCreateCompletion = mock(() => 5 | Promise.resolve({ 6 | choices: [{ message: { content: 'Mocked response' } }], 7 | }), 8 | ) 9 | 10 | mock.module('openai', () => ({ 11 | default: class { 12 | chat = { 13 | completions: { 14 | create: mockCreateCompletion, 15 | }, 16 | } 17 | }, 18 | })) 19 | 20 | import { OpenAIAdapter } from '.' 21 | 22 | describe('OpenAIAdapter', () => { 23 | afterEach(() => { 24 | mockCreateCompletion.mockClear() 25 | }) 26 | 27 | it('should create a completion with OpenAI', async () => { 28 | const adapter = new OpenAIAdapter('FAKE_API_KEY') 29 | 30 | await adapter.createCompletion({ 31 | content: 'my content', 32 | }) 33 | 34 | expect(mockCreateCompletion).toHaveBeenCalledTimes(1) 35 | expect(mockCreateCompletion).toHaveBeenCalledWith({ 36 | messages: [{ role: 'user', content: 'my content' }], 37 | store: false, 38 | model: 'gpt-4o', 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/wabe-openai/src/index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai' 2 | import type { AIAdapter, CreateCompletionOptions } from 'wabe' 3 | 4 | export class OpenAIAdapter implements AIAdapter { 5 | public openai: OpenAI 6 | private model: string 7 | 8 | constructor(apiKey: string, options?: { model?: string }) { 9 | this.openai = new OpenAI({ apiKey }) 10 | this.model = options?.model || 'gpt-4o' 11 | } 12 | 13 | async createCompletion({ content }: CreateCompletionOptions) { 14 | const chatCompletion = await this.openai.chat.completions.create({ 15 | messages: [{ role: 'user', content }], 16 | model: this.model, 17 | store: false, 18 | }) 19 | 20 | return chatCompletion.choices[0].message.content || '' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/wabe-openai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-pluralize/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-pluralize", 3 | "version": "0.0.1", 4 | "license": "Apache-2.0", 5 | "main": "src/index.ts", 6 | "description": "Package to pluralize a word in english", 7 | "scripts": { 8 | "build": "bun --filter wabe-build build:package $(pwd)", 9 | "check": "tsc --project $(pwd)/tsconfig.json", 10 | "ci":"bun lint && bun test", 11 | "lint": "biome lint $(pwd)/src/*.ts --config-path=../../ --no-errors-on-unmatched", 12 | "format": "biome format --write . --config-path=../../" 13 | }, 14 | "devDependencies": { 15 | "@types/bun": "latest" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/wabe-pluralize/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { pluralize } from './index' 3 | 4 | describe('Pluralize', () => { 5 | it('should pluralize a simple noun', () => { 6 | expect(pluralize('cat')).toBe('cats') 7 | expect(pluralize('boat')).toBe('boats') 8 | }) 9 | 10 | it('should pluralize a noun ending by s,x,z,ch,sh', () => { 11 | expect(pluralize('bus')).toBe('buses') 12 | expect(pluralize('box')).toBe('boxes') 13 | expect(pluralize('buzz')).toBe('buzzes') 14 | expect(pluralize('church')).toBe('churches') 15 | expect(pluralize('wish')).toBe('wishes') 16 | }) 17 | 18 | it('should pluralize a noun ending in a consonant and then y', () => { 19 | expect(pluralize('baby')).toBe('babies') 20 | expect(pluralize('story')).toBe('stories') 21 | expect(pluralize('city')).toBe('cities') 22 | expect(pluralize('penny')).toBe('pennies') 23 | }) 24 | 25 | it('should pluralize an irregular noun', () => { 26 | expect(pluralize('child')).toBe('children') 27 | expect(pluralize('goose')).toBe('geese') 28 | expect(pluralize('potato')).toBe('potatoes') 29 | expect(pluralize('radius')).toBe('radii') 30 | expect(pluralize('parenthesis')).toBe('parentheses') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/wabe-pluralize/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2022", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "declaration": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist", 16 | "**/*.test.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/wabe-postgres-launcher/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mongodb-launcher 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mongodb-launcher # On bun 21 | npm install wabe-mongodb-launcher # On npm 22 | yarn add wabe-mongodb-launcher # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mongodb-launcher usage 26 | 27 | ```ts 28 | import { runDatabase } from 'wabe-mongodb-launcher' 29 | 30 | 31 | await runDatabase() 32 | ``` 33 | -------------------------------------------------------------------------------- /packages/wabe-postgres-launcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-postgres-launcher", 3 | "version": "0.5.0", 4 | "license": "Apache-2.0", 5 | "main": "dist/index.js", 6 | "description": "Package to launch the postgres database for test", 7 | "scripts": { 8 | "ci": "bun lint", 9 | "build": "bun --filter wabe-build build:package $(pwd) node", 10 | "lint": "biome lint . --config-path=../../ --no-errors-on-unmatched", 11 | "format": "biome format --config-path=../../ --write ." 12 | }, 13 | "dependencies": { 14 | "dockerode": "4.0.5", 15 | "tcp-port-used": "1.0.2" 16 | }, 17 | "devDependencies": { 18 | "@types/bun": "latest", 19 | "@types/dockerode": "3.3.37", 20 | "@types/tcp-port-used": "1.0.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/wabe-postgres-launcher/src/index.ts: -------------------------------------------------------------------------------- 1 | import Docker from 'dockerode' 2 | import tcpPortUsed from 'tcp-port-used' 3 | 4 | const docker = new Docker() 5 | 6 | // URL: 'postgres://username:password@localhost:5432/databaseName' 7 | export const runDatabase = async (): Promise => { 8 | try { 9 | const port = 5432 10 | 11 | if (await tcpPortUsed.check(port, '127.0.0.1')) return 12 | 13 | const imageName = 'postgres:17.4-alpine' 14 | 15 | // Check if the image already exists locally 16 | const images = await docker.listImages() 17 | 18 | if (!images.find((image) => image.RepoTags?.includes(imageName))) { 19 | console.info(`Pulling ${imageName}`) 20 | 21 | const stream = await docker.pull(imageName) 22 | 23 | await new Promise((resolve, reject) => { 24 | docker.modem.followProgress(stream, (err, res) => 25 | err ? reject(err) : resolve(res), 26 | ) 27 | }) 28 | } 29 | 30 | const container = await docker.createContainer({ 31 | Image: imageName, 32 | name: 'wabe-postgres', 33 | Env: ['POSTGRES_USER=wabe', 'POSTGRES_PASSWORD=wabe', 'POSTGRES_DB=Wabe'], 34 | HostConfig: { 35 | PortBindings: { 36 | '5432/tcp': [{ HostPort: `${port}` }], 37 | }, 38 | }, 39 | AttachStdin: false, 40 | AttachStdout: true, 41 | AttachStderr: true, 42 | Tty: true, 43 | OpenStdin: false, 44 | StdinOnce: false, 45 | }) 46 | 47 | await container.start() 48 | 49 | while (!(await tcpPortUsed.check(port, '127.0.0.1'))) { 50 | await Bun.sleep(1000) 51 | } 52 | 53 | // 1000 ms more to let the time to established connection 54 | await Bun.sleep(1000) 55 | 56 | console.info('PostgreSQL started') 57 | } catch (error: any) { 58 | if (error.message.includes('there a typo in the url or port')) { 59 | console.error('You need to run Docker on your machine') 60 | process.exit(1) 61 | } 62 | 63 | // Try to find and remove the container if it exists 64 | try { 65 | const containers = await docker.listContainers({ all: true }) 66 | const existingContainer = containers.find((container) => 67 | container.Names.includes('/Wabe-Postgres'), 68 | ) 69 | 70 | if (existingContainer) { 71 | const container = docker.getContainer(existingContainer.Id) 72 | await container.stop() 73 | await container.remove() 74 | 75 | // We retry to run the database 76 | return runDatabase() 77 | } 78 | } catch (cleanupError) { 79 | console.error('Error during cleanup:', cleanupError) 80 | } 81 | 82 | console.error('An error occurred:', error) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/wabe-postgres-launcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-postgres/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-mongodb 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-mongodb # On bun 21 | npm install wabe-mongodb # On npm 22 | yarn add wabe-mongodb # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-mongodb usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | 31 | const run = async () => { 32 | // Ensure your database is running before run the file 33 | 34 | const wabe = new Wabe({ 35 | isProduction: process.env.NODE_ENV === 'production', 36 | // Root key example (must be long minimal 64 characters, you can generate it online) 37 | rootKey: 38 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 39 | database: { 40 | adapter: new MongoAdapter({ 41 | databaseName: "WabeApp", 42 | url: "mongodb://127.0.0.1:27045", 43 | }) 44 | }, 45 | port: 3001, 46 | }); 47 | 48 | await wabe.start(); 49 | }; 50 | 51 | await run(); 52 | ``` 53 | -------------------------------------------------------------------------------- /packages/wabe-postgres/bunfig.toml: -------------------------------------------------------------------------------- 1 | telemetry = false 2 | 3 | [test] 4 | preload = ['./utils/preload.ts'] 5 | -------------------------------------------------------------------------------- /packages/wabe-postgres/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-postgres", 3 | "version": "0.5.0", 4 | "description": "PostgreSQL adapter for Wabe (official)", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "type": "module", 8 | "keywords": [ 9 | "wabe", 10 | "postgres", 11 | "postgresql", 12 | "database", 13 | "adapter" 14 | ], 15 | "scripts": { 16 | "build": "bun --filter wabe-build build:package $(pwd)", 17 | "check": "tsc --project $(pwd)/tsconfig.json", 18 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 19 | "ci": "bun check && bun lint $(pwd) && bun test src", 20 | "format": "biome format --write . --config-path=../../" 21 | }, 22 | "license": "Apache-2.0", 23 | "dependencies": { 24 | "p-retry": "5.1.2", 25 | "pg": "8.14.1" 26 | }, 27 | "devDependencies": { 28 | "@types/pg": "8.11.11", 29 | "wabe": "workspace:*", 30 | "wabe-postgres-launcher": "workspace:*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/wabe-postgres/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "allowJs": true, 11 | "jsx": "react-jsx", 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | "declarationDir": "./dist", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/wabe-postgres/utils/preload.ts: -------------------------------------------------------------------------------- 1 | const setupEnvironment = () => { 2 | process.env.TEST = 'true' 3 | } 4 | 5 | setupEnvironment() 6 | -------------------------------------------------------------------------------- /packages/wabe-resend/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-resend 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-resend # On bun 21 | npm install wabe-resend # On npm 22 | yarn add wabe-resend # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-resend usage 26 | 27 | ```ts 28 | import { Wabe } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | import { ResendAdapter } from "wabe-resend"; 31 | 32 | const run = async () => { 33 | // Ensure your database is running before run the file 34 | 35 | const wabe = new Wabe({ 36 | isProduction: process.env.NODE_ENV === 'production', 37 | // Root key example (must be long minimal 64 characters, you can generate it online) 38 | rootKey: 39 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 40 | database: { 41 | adapter: new MongoAdapter({ 42 | databaseName: "WabeApp", 43 | url: "mongodb://127.0.0.1:27045", 44 | }) 45 | }, 46 | email: { 47 | adapter : new ResendAdapter("YOUR_RESEND_API_KEY"), 48 | } 49 | port: 3001, 50 | }); 51 | 52 | await wabe.start(); 53 | 54 | await wabe.controllers.email.send({ 55 | from : "test@test.com", 56 | to: ["target@gmail.com"], 57 | subject: "Test", 58 | text: "Test", 59 | }); 60 | }; 61 | 62 | await run(); 63 | ``` 64 | -------------------------------------------------------------------------------- /packages/wabe-resend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-resend", 3 | "version": "0.5.1", 4 | "description": "Resend email adapter for Wabe (official)", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/palixir/wabe.git" 20 | }, 21 | "main": "dist/index.js", 22 | "scripts": { 23 | "build": "bun --filter wabe-build build:package $(pwd)", 24 | "check": "tsc --project $(pwd)/tsconfig.json", 25 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 26 | "ci": "bun check && bun lint $(pwd) && bun test src", 27 | "format": "biome format --write . --config-path=../../" 28 | }, 29 | "dependencies": { 30 | "resend": "4.1.2" 31 | }, 32 | "devDependencies": { 33 | "@types/bun": "latest", 34 | "@types/react": "19.0.10", 35 | "wabe": "workspace:*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/wabe-resend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock, spyOn, beforeEach } from 'bun:test' 2 | import { ResendAdapter } from '.' 3 | import { Resend } from 'resend' 4 | 5 | describe('Resend', () => { 6 | const mockSend = mock(() => {}).mockResolvedValueOnce({ 7 | data: { id: 'id' }, 8 | error: null, 9 | } as never) 10 | 11 | spyOn(Resend.prototype, 'emails').mockReturnValue({ 12 | send: mockSend, 13 | } as never) 14 | 15 | beforeEach(() => { 16 | mockSend.mockClear() 17 | }) 18 | 19 | it('should send email with resend', async () => { 20 | const adapter = new ResendAdapter('FAKE_API_KEY') 21 | 22 | await adapter.send({ 23 | from: 'test@wabe.dev', 24 | to: ['delivered@resend.dev'], 25 | subject: 'Test', 26 | text: 'Content of the email', 27 | }) 28 | 29 | expect(mockSend).toHaveBeenCalledTimes(1) 30 | expect(mockSend).toHaveBeenCalledWith({ 31 | from: 'test@wabe.dev', 32 | to: ['delivered@resend.dev'], 33 | subject: 'Test', 34 | text: 'Content of the email', 35 | }) 36 | }) 37 | 38 | it('should throw error if something wrong', () => { 39 | mockSend.mockResolvedValueOnce({ 40 | data: { id: 'id' }, 41 | error: { 42 | message: 'error message', 43 | }, 44 | } as never) 45 | 46 | const adapter = new ResendAdapter('FAKE_API_KEY') 47 | 48 | expect( 49 | adapter.send({ 50 | from: 'test@wabe.dev', 51 | to: ['delivered@resend.dev'], 52 | subject: 'Test', 53 | text: 'Content of the email', 54 | }), 55 | ).rejects.toThrow('error message') 56 | 57 | expect(mockSend).toHaveBeenCalledTimes(1) 58 | expect(mockSend).toHaveBeenCalledWith({ 59 | from: 'test@wabe.dev', 60 | to: ['delivered@resend.dev'], 61 | subject: 'Test', 62 | text: 'Content of the email', 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /packages/wabe-resend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from 'resend' 2 | import type { EmailAdapter, EmailSendOptions } from 'wabe' 3 | 4 | export class ResendAdapter implements EmailAdapter { 5 | private resend: Resend 6 | 7 | constructor(apiKey: string) { 8 | this.resend = new Resend(apiKey) 9 | } 10 | 11 | async send({ node, ...input }: EmailSendOptions) { 12 | const { data, error } = await this.resend.emails.send({ 13 | ...input, 14 | react: node, 15 | }) 16 | 17 | if (error) throw new Error(error.message) 18 | 19 | if (!data) throw new Error('Email not send') 20 | 21 | return data.id 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/wabe-resend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/wabe-stripe/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, and more for you. 12 | 13 | ## Install for wabe-stripe 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | 20 | bun install wabe-stripe # On bun 21 | npm install wabe-stripe # On npm 22 | yarn add wabe-stripe # On yarn 23 | ``` 24 | 25 | ## Basic example of wabe-stripe usage 26 | 27 | ```ts 28 | import { Wabe, PaymentMode, Currency } from "wabe"; 29 | import { MongoAdapter } from "wabe-mongodb" 30 | import { StripeAdapter } from "wabe-stripe"; 31 | 32 | const run = async () => { 33 | // Ensure your database is running before run the file 34 | 35 | const wabe = new Wabe({ 36 | isProduction: process.env.NODE_ENV === 'production', 37 | // Root key example (must be long minimal 64 characters, you can generate it online) 38 | rootKey: 39 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 40 | database: { 41 | adapter: new MongoAdapter({ 42 | databaseName: "WabeApp", 43 | url: "mongodb://127.0.0.1:27045", 44 | }) 45 | }, 46 | payment: { 47 | adapter: new StripeAdapter('YOU_STRIPE_SECRET_KEY'), 48 | currency: Currency.USD, 49 | supportedPaymentMethods: ['card', 'paypal'], 50 | }, 51 | port: 3001, 52 | }); 53 | 54 | await wabe.start(); 55 | 56 | await wabe.controllers.payment.createPayment({ 57 | cancelUrl: 'https://example.com/cancel', 58 | successUrl: 'https://example.com/success', 59 | customerEmail: 'john.doe@example.com', 60 | paymentMode: PaymentMode.Subscription, 61 | // Compute the taxe automatically or not 62 | automaticTax: true, 63 | recurringInterval: 'month', 64 | products: [{ name: 'MacBook Pro', unitAmount: 100, quantity: 1 }], 65 | }) 66 | }; 67 | 68 | await run(); 69 | ``` 70 | -------------------------------------------------------------------------------- /packages/wabe-stripe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe-stripe", 3 | "version": "0.5.15", 4 | "description": "Stripe payment adapter for Wabe (official)", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/palixir/wabe.git" 20 | }, 21 | "main": "dist/index.js", 22 | "scripts": { 23 | "build": "bun --filter wabe-build build:package $(pwd)", 24 | "check": "tsc --project $(pwd)/tsconfig.json", 25 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 26 | "ci": "bun check && bun lint $(pwd) && bun test src", 27 | "format": "biome format --write . --config-path=../../" 28 | }, 29 | "dependencies": { 30 | "stripe": "17.5.0" 31 | }, 32 | "devDependencies": { 33 | "@types/bun": "latest", 34 | "wabe": "workspace:*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/wabe-stripe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/wabe/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wabe logo 3 |

4 | 5 |
6 | Documentation 7 |
8 | 9 | ## What is Wabe? 10 | 11 | Wabe is an open-source backend as a service that allows you to create your own fully customizable backend in just a few minutes. It handles database access, automatic GraphQL API generation, authentication with various methods (classic or OAuth), permissions, security, payment, emails, and more for you. 12 | 13 | ## Install 14 | 15 | ```sh 16 | bun install wabe # On bun 17 | npm install wabe # On npm 18 | yarn add wabe # On yarn 19 | ``` 20 | 21 | ## Basic example 22 | 23 | ```ts 24 | import { Wabe } from "wabe"; 25 | import { MongoAdapter } from "wabe-mongodb" 26 | 27 | const run = async () => { 28 | // Ensure your database is running before run the file 29 | 30 | const wabe = new Wabe({ 31 | isProduction: process.env.NODE_ENV === "production", 32 | // Root key example (must be long minimal 64 characters, you can generate it online) 33 | rootKey: 34 | "0uwFvUxM$ceFuF1aEtTtZMa7DUN2NZudqgY5ve5W*QCyb58cwMj9JeoaV@d#%29v&aJzswuudVU1%nAT+rxS0Bh&OkgBYc0PH18*", 35 | database: { 36 | adapter: new MongoAdapter({ 37 | databaseName: "WabeApp", 38 | url: "mongodb://127.0.0.1:27045", 39 | }) 40 | }, 41 | port: 3001, 42 | }); 43 | 44 | await wabe.start(); 45 | }; 46 | 47 | await run(); 48 | ``` 49 | 50 | ## Features 51 | 52 | - **Authentication**: Secure and scalable authentication for your applications. 53 | - **Permissions**: Granular permissions control to secure your resources. 54 | - **Database**: A powerful, scalable database to store and manage you data. 55 | - **GraphQL API**: A flexible and powerful GraphQL API (following GraphQL Relay standard) to interact with your data. 56 | - **Hooks**: Powerful hooks system to execute custom actions before or after database requests. 57 | - **Email**: Send emails with your favorite provider with very simple integration. 58 | - **Payment**: Accept payments with Stripe or create your own payment provider adapter. 59 | 60 | ## Contributing 61 | 62 | Contributions are always welcome! If you have an idea for something that should be added, modified, or removed, please don't hesitate to create a pull request (I promise a quick review). 63 | 64 | You can also create an issue to propose your ideas or report a bug. 65 | 66 | Of course, you can also use Wabe for your backend; that is the better contribution at this day ❤️. 67 | 68 | If you like the project don't forget to share it. 69 | 70 | More information on the [Contribution guide](https://github.com/palixir/wabe/blob/main/CONTRIBUTING.md) 71 | 72 | ## License 73 | 74 | Distributed under the Apache License 2.0 [License](https://github.com/palixir/wabe/blob/main/LICENSE). 75 | -------------------------------------------------------------------------------- /packages/wabe/bunfig.toml: -------------------------------------------------------------------------------- 1 | telemetry = false 2 | 3 | [test] 4 | preload = ['./src/utils/preload.ts'] -------------------------------------------------------------------------------- /packages/wabe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wabe", 3 | "version": "0.6.7", 4 | "description": "Your backend in minutes not days", 5 | "homepage": "https://wabe.dev", 6 | "author": { 7 | "name": "coratgerl", 8 | "url": "https://github.com/coratgerl" 9 | }, 10 | "license": "Apache-2.0", 11 | "keywords": [ 12 | "backend", 13 | "wabe", 14 | "graphql", 15 | "baas" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/palixir/wabe.git" 20 | }, 21 | "main": "dist/index.js", 22 | "scripts": { 23 | "build": "bun --filter wabe-build build:package $(pwd)", 24 | "check": "tsc --project $(pwd)/tsconfig.json", 25 | "lint": "biome lint . --no-errors-on-unmatched --config-path=../../", 26 | "ci": "bun generate:codegen && bun lint $(pwd) && bun check && bun test src", 27 | "format": "biome format --write . --config-path=../../", 28 | "dev": "bun run --watch dev/index.ts", 29 | "generate:codegen": "touch generated/wabe.ts && CODEGEN=true bun dev/index.ts" 30 | }, 31 | "dependencies": { 32 | "@graphql-yoga/plugin-disable-introspection": "2.10.9", 33 | "@node-rs/argon2": "2.0.2", 34 | "croner": "9.0.0", 35 | "js-srp6a": "1.0.2", 36 | "jsonwebtoken": "9.0.2", 37 | "libphonenumber-js": "1.11.18", 38 | "mongodb": "6.13.1", 39 | "otplib": "12.0.1", 40 | "p-retry": "6.2.1", 41 | "wobe": "1.1.10", 42 | "wobe-graphql-yoga": "1.2.6" 43 | }, 44 | "devDependencies": { 45 | "@parcel/watcher": "2.3.0", 46 | "@types/bun": "latest", 47 | "@types/jsonwebtoken": "9.0.6", 48 | "@types/uuid": "9.0.6", 49 | "graphql-request": "6.1.0", 50 | "get-port": "7.1.0", 51 | "uuid": "10.0.0", 52 | "wabe-postgres-launcher": "workspace:*", 53 | "wabe-mongodb-launcher": "workspace:*", 54 | "wabe-pluralize": "workspace:*", 55 | "wabe-build": "workspace:*", 56 | "wabe-mongodb": "workspace:*" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/wabe/src/ai/AIController.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import { AIController } from './AIController' 3 | 4 | describe('AIController', () => { 5 | it('should create completion using correct adapter', async () => { 6 | const mockCreateCompletion = mock(() => {}) 7 | const dummyAdapter = { 8 | createCompletion: mockCreateCompletion, 9 | } 10 | 11 | // @ts-expect-error 12 | const controller = new AIController(dummyAdapter) 13 | 14 | await controller.createCompletion({ 15 | content: 'content', 16 | }) 17 | 18 | expect(mockCreateCompletion).toHaveBeenCalledTimes(1) 19 | expect(mockCreateCompletion).toHaveBeenCalledWith({ 20 | content: 'content', 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/wabe/src/ai/AIController.ts: -------------------------------------------------------------------------------- 1 | import type { AIAdapter, CreateCompletionOptions } from './interface' 2 | 3 | export class AIController implements AIAdapter { 4 | public adapter: AIAdapter 5 | 6 | constructor(adapter: AIAdapter) { 7 | this.adapter = adapter 8 | } 9 | 10 | createCompletion(options: CreateCompletionOptions): Promise { 11 | return this.adapter.createCompletion(options) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/wabe/src/ai/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface' 2 | -------------------------------------------------------------------------------- /packages/wabe/src/ai/interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateCompletionOptions { 2 | content: string 3 | } 4 | 5 | export interface AIAdapter { 6 | createCompletion(options: CreateCompletionOptions): Promise 7 | } 8 | 9 | export interface AIConfig { 10 | adapter: AIAdapter 11 | } 12 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/OTP.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'bun:test' 2 | import { OTP } from './OTP' 3 | 4 | describe('OTP', () => { 5 | it('should generate a valid OTP code', () => { 6 | const otp = new OTP('rootKey') 7 | 8 | const otpValue = otp.generate('userId') 9 | 10 | expect(otpValue.length).toBe(6) 11 | }) 12 | 13 | it('should verify a valid OTP code', () => { 14 | const otp = new OTP('rootKey') 15 | 16 | const otpValue = otp.generate('userId') 17 | 18 | expect(otpValue.length).toBe(6) 19 | 20 | expect(otp.verify(otpValue, 'userId')).toBe(true) 21 | }) 22 | 23 | it('should not verify an invalid OTP code', () => { 24 | const otp = new OTP('rootKey') 25 | 26 | const otpValue = otp.generate('userId') 27 | 28 | expect(otpValue.length).toBe(6) 29 | 30 | expect(otp.verify('invalidOtp', 'userId')).toBe(false) 31 | 32 | const otpValue2 = otp.generate('invalidUserId') 33 | 34 | expect(otpValue2.length).toBe(6) 35 | 36 | expect(otp.verify(otpValue2, 'userId')).toBe(false) 37 | }) 38 | 39 | it('should not verify an invalid OTP code (more than 5 minutes)', () => { 40 | // Directly test the timeout is flaky we only test that the correct value is passed to totp 41 | const otp = new OTP('rootKey') 42 | 43 | expect(otp.internalTotp.options.window).toEqual([5, 0]) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/OTP.ts: -------------------------------------------------------------------------------- 1 | import { totp } from 'otplib' 2 | import type { TOTP } from 'otplib/core' 3 | import { createHash } from 'node:crypto' 4 | 5 | const FIVE_MINUTES = 5 6 | 7 | export class OTP { 8 | private secret: string 9 | public internalTotp: TOTP 10 | 11 | constructor(rootKey: string) { 12 | this.secret = rootKey 13 | this.internalTotp = totp.clone({ 14 | window: [FIVE_MINUTES, 0], 15 | }) 16 | } 17 | 18 | generate(userId: string): string { 19 | const hashedSecret = createHash('sha256') 20 | .update(`${this.secret}:${userId}`) 21 | .digest('hex') 22 | 23 | return this.internalTotp.generate(hashedSecret) 24 | } 25 | 26 | verify(otp: string, userId: string): boolean { 27 | const hashedSecret = createHash('sha256') 28 | .update(`${this.secret}:${userId}`) 29 | .digest('hex') 30 | 31 | return this.internalTotp.verify({ secret: hashedSecret, token: otp }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface' 2 | export * from './oauth' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/oauth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Oauth2Client' 2 | export * from './Google' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/oauth/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { base64URLencode, generateRandomValues } from './utils' 3 | 4 | describe('Oauth utils', () => { 5 | it('should encode url with base64', () => { 6 | const content = 'test' 7 | 8 | // Keep Bun. here to be sure the compatibility between node and Bun implem 9 | const hasher = new Bun.CryptoHasher('sha256') 10 | hasher.update(new TextEncoder().encode(content)) 11 | const resultWithPadding = hasher.digest('base64') 12 | 13 | const result = base64URLencode(content) 14 | 15 | expect(resultWithPadding).toBe( 16 | 'n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=', 17 | ) 18 | expect(result).toBe('n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg') 19 | }) 20 | 21 | // Real use case check with oauth simulator 22 | it('should encode correctly with base64', () => { 23 | const content = 'bIaNJCsNzrZE7QEzYjwbl0fa1CyzF49moM6Ua4H0d5cG-l7d' 24 | const result = base64URLencode(content) 25 | 26 | expect(result).toBe('1pdL2CLvBbNBnrfBZeNYlFzpedMhUTbgyhn0CnWVYoc') 27 | }) 28 | 29 | it('should generate random values for code_verifier or state', () => { 30 | const randomValue = generateRandomValues() 31 | 32 | // Google recommends an entropy between 43 and 128 characters for the code_verifier 33 | expect(randomValue.length).toEqual(80) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/oauth/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto' 2 | 3 | export interface Tokens { 4 | accessToken: string 5 | refreshToken?: string | null 6 | accessTokenExpiresAt?: Date 7 | refreshTokenExpiresAt?: Date | null 8 | idToken?: string 9 | } 10 | 11 | export interface OAuth2ProviderWithPKCE { 12 | createAuthorizationURL(state: string, codeVerifier: string): URL 13 | validateAuthorizationCode(code: string, codeVerifier: string): Promise 14 | refreshAccessToken?(refreshToken: string): Promise 15 | } 16 | 17 | // https://datatracker.ietf.org/doc/html/rfc7636#appendix-A 18 | export const base64URLencode = (content: string) => { 19 | const hasher = crypto.createHash('sha256').update(content) 20 | 21 | const result = hasher.digest('base64') 22 | 23 | // @ts-ignore 24 | return result.split('=')[0].replaceAll('+', '-').replaceAll('/', '_') 25 | } 26 | 27 | export const generateRandomValues = () => 28 | crypto.randomBytes(60).toString('base64url') 29 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/providers/EmailOTP.ts: -------------------------------------------------------------------------------- 1 | import { contextWithRoot } from '../..' 2 | import { sendOtpCodeTemplate } from '../../email/templates/sendOtpCode' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | import type { 5 | OnSendChallengeOptions, 6 | OnVerifyChallengeOptions, 7 | SecondaryProviderInterface, 8 | } from '../interface' 9 | import { OTP } from '../OTP' 10 | 11 | type EmailOTPInterface = { 12 | email: string 13 | otp: string 14 | } 15 | 16 | export class EmailOTP 17 | implements SecondaryProviderInterface 18 | { 19 | async onSendChallenge({ 20 | context, 21 | user, 22 | }: OnSendChallengeOptions) { 23 | const emailController = context.wabe.controllers.email 24 | 25 | if (!emailController) throw new Error('Email controller not found') 26 | 27 | const mainEmail = context.wabe.config.email?.mainEmail 28 | 29 | if (!mainEmail) throw new Error('No main email found') 30 | 31 | if (!user.email) throw new Error('No user email found') 32 | 33 | const otpClass = new OTP(context.wabe.config.rootKey) 34 | 35 | const otp = otpClass.generate(user.id) 36 | 37 | const template = context.wabe.config.email?.htmlTemplates?.sendOTPCode 38 | 39 | await emailController.send({ 40 | from: mainEmail, 41 | to: [user.email], 42 | subject: template?.subject || 'Your OTP code', 43 | html: template?.fn 44 | ? await template.fn({ otp }) 45 | : sendOtpCodeTemplate(otp), 46 | }) 47 | } 48 | 49 | async onVerifyChallenge({ 50 | context, 51 | input, 52 | }: OnVerifyChallengeOptions) { 53 | const users = await context.wabe.controllers.database.getObjects({ 54 | className: 'User', 55 | where: { 56 | email: { 57 | equalTo: input.email, 58 | }, 59 | }, 60 | select: { id: true, secondFA: true }, 61 | first: 1, 62 | context: contextWithRoot(context), 63 | }) 64 | 65 | if (users.length === 0) return null 66 | 67 | const user = users[0] 68 | 69 | if (!user) return null 70 | 71 | const userId = user.id 72 | 73 | if (!userId) return null 74 | 75 | const otpClass = new OTP(context.wabe.config.rootKey) 76 | 77 | if (!context.wabe.config.isProduction && input.otp === '000000') 78 | return { userId } 79 | 80 | if (!otpClass.verify(input.otp, userId)) return null 81 | 82 | return { userId } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/providers/GitHub.ts: -------------------------------------------------------------------------------- 1 | import type { DevWabeTypes } from '../../utils/helper' 2 | import { 3 | AuthenticationProvider, 4 | type AuthenticationEventsOptions, 5 | type ProviderInterface, 6 | } from '../interface' 7 | import { oAuthAuthentication } from './OAuth' 8 | 9 | type GitHubInterface = { 10 | authorizationCode: string 11 | codeVerifier: string 12 | } 13 | 14 | export class GitHub 15 | implements ProviderInterface 16 | { 17 | name = 'github' 18 | onSignIn( 19 | options: AuthenticationEventsOptions, 20 | ) { 21 | return oAuthAuthentication(AuthenticationProvider.GitHub)(options) 22 | } 23 | 24 | // @ts-expect-error 25 | onSignUp() { 26 | throw new Error( 27 | 'SignUp is not implemented for Oauth provider, you should use signIn instead.', 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/providers/Google.ts: -------------------------------------------------------------------------------- 1 | import type { DevWabeTypes } from '../../utils/helper' 2 | import { 3 | AuthenticationProvider, 4 | type AuthenticationEventsOptions, 5 | type ProviderInterface, 6 | } from '../interface' 7 | import { oAuthAuthentication } from './OAuth' 8 | 9 | type GoogleInterface = { 10 | authorizationCode: string 11 | codeVerifier: string 12 | } 13 | 14 | export class Google 15 | implements ProviderInterface 16 | { 17 | name = 'google' 18 | onSignIn( 19 | options: AuthenticationEventsOptions, 20 | ) { 21 | return oAuthAuthentication(AuthenticationProvider.Google)(options) 22 | } 23 | 24 | // @ts-expect-error 25 | onSignUp() { 26 | throw new Error( 27 | 'SignUp is not implemented for Oauth provider, you should use signIn instead.', 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/providers/OAuth.ts: -------------------------------------------------------------------------------- 1 | import type { WabeContext } from '../../server/interface' 2 | import { contextWithRoot } from '../../utils/export' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | import { 5 | type AuthenticationEventsOptions, 6 | AuthenticationProvider, 7 | } from '../interface' 8 | import { Google } from '../oauth' 9 | import { GitHub } from '../oauth/GitHub' 10 | 11 | export type OAuthAuthenticationInterface = { 12 | authorizationCode: string 13 | codeVerifier: string 14 | } 15 | 16 | export const getProvider = ( 17 | context: WabeContext, 18 | provider: AuthenticationProvider, 19 | ) => { 20 | const config = context.wabe.config 21 | 22 | switch (provider) { 23 | case AuthenticationProvider.Google: 24 | return new Google(config) 25 | case AuthenticationProvider.GitHub: 26 | return new GitHub(config) 27 | default: 28 | throw new Error(`Provider ${provider} not found`) 29 | } 30 | } 31 | 32 | export const oAuthAuthentication = 33 | (oAuthProvider: AuthenticationProvider) => 34 | async ({ 35 | context, 36 | input, 37 | }: AuthenticationEventsOptions< 38 | DevWabeTypes, 39 | OAuthAuthenticationInterface 40 | >) => { 41 | const { authorizationCode, codeVerifier } = input 42 | 43 | const provider = getProvider(context, oAuthProvider) 44 | 45 | const { accessToken } = await provider.validateAuthorizationCode( 46 | authorizationCode, 47 | codeVerifier, 48 | ) 49 | 50 | const userInfoToSave = await provider.getUserInfo(accessToken) 51 | 52 | const user = await context.wabe.controllers.database.getObjects({ 53 | className: 'User', 54 | where: { 55 | authentication: { 56 | [oAuthProvider]: { 57 | email: { equalTo: userInfoToSave.email }, 58 | }, 59 | }, 60 | }, 61 | context: contextWithRoot(context), 62 | first: 1, 63 | select: { id: true }, 64 | }) 65 | 66 | if (user.length === 0) { 67 | const createdUser = await context.wabe.controllers.database.createObject({ 68 | className: 'User', 69 | data: { 70 | provider: oAuthProvider, 71 | isOauth: true, 72 | authentication: { 73 | [oAuthProvider]: userInfoToSave, 74 | }, 75 | }, 76 | context: contextWithRoot(context), 77 | }) 78 | 79 | if (!createdUser) throw new Error('User not found') 80 | 81 | return { 82 | user: createdUser, 83 | } 84 | } 85 | 86 | if (!user[0]) throw new Error('User not found') 87 | 88 | return { 89 | user: user[0], 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EmailPassword' 2 | export * from './Google' 3 | export * from './GitHub' 4 | export * from './PhonePassword' 5 | export * from './EmailOTP' 6 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/refreshResolver.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, spyOn } from 'bun:test' 2 | import { refreshResolver } from './refreshResolver' 3 | import type { WabeContext } from '../../server/interface' 4 | import { Session } from '../Session' 5 | 6 | const context: WabeContext = { 7 | sessionId: 'sessionId', 8 | user: {} as any, 9 | isRoot: false, 10 | } as WabeContext 11 | 12 | describe('refreshResolver', () => { 13 | it('should refresh the session', async () => { 14 | const spyRefreshSession = spyOn( 15 | Session.prototype, 16 | 'refresh', 17 | ).mockResolvedValue({} as any) 18 | 19 | await refreshResolver( 20 | null, 21 | { 22 | input: { 23 | accessToken: 'accessToken', 24 | refreshToken: 'refreshToken', 25 | }, 26 | }, 27 | context, 28 | ) 29 | 30 | expect(spyRefreshSession).toHaveBeenCalledTimes(1) 31 | expect(spyRefreshSession).toHaveBeenCalledWith( 32 | 'accessToken', 33 | 'refreshToken', 34 | context, 35 | ) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/refreshResolver.ts: -------------------------------------------------------------------------------- 1 | import type { WabeContext } from '../../server/interface' 2 | import type { DevWabeTypes } from '../../utils/helper' 3 | import { Session } from '../Session' 4 | 5 | export const refreshResolver = async ( 6 | _: any, 7 | args: any, 8 | context: WabeContext, 9 | ) => { 10 | const { 11 | input: { refreshToken, accessToken }, 12 | } = args 13 | 14 | const session = new Session() 15 | 16 | const { accessToken: newAccessToken, refreshToken: newRefreshToken } = 17 | await session.refresh(accessToken, refreshToken, context) 18 | 19 | return { accessToken: newAccessToken, refreshToken: newRefreshToken } 20 | } 21 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/signInWithResolver.ts: -------------------------------------------------------------------------------- 1 | import type { SignInWithInput } from '../../../generated/wabe' 2 | import type { WabeContext } from '../../server/interface' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | import { Session } from '../Session' 5 | import type { 6 | ProviderInterface, 7 | SecondaryProviderInterface, 8 | } from '../interface' 9 | import { getAuthenticationMethod } from '../utils' 10 | 11 | // 0 - Get the authentication method 12 | // 1 - We check if the signIn is possible (call onSign) 13 | // 2 - If secondaryFactor is present, we call the onSendChallenge method of the provider 14 | // 3 - We create session 15 | export const signInWithResolver = async ( 16 | _: any, 17 | { 18 | input, 19 | }: { 20 | input: SignInWithInput 21 | }, 22 | context: WabeContext, 23 | ) => { 24 | const { provider, name } = getAuthenticationMethod< 25 | DevWabeTypes, 26 | ProviderInterface 27 | >(Object.keys(input.authentication || {}), context) 28 | 29 | const inputOfTheGoodAuthenticationMethod = 30 | // @ts-expect-error 31 | input.authentication[name] 32 | 33 | // 1 - We call the onSignIn method of the provider 34 | const { user, srp } = await provider.onSignIn({ 35 | input: inputOfTheGoodAuthenticationMethod, 36 | context, 37 | }) 38 | 39 | const userId = user.id 40 | 41 | if (!userId) throw new Error('Authentication failed') 42 | 43 | const secondFAObject = user.secondFA 44 | 45 | // 2 - We call the onSendChallenge method of the provider 46 | if (secondFAObject?.enabled) { 47 | const secondaryProvider = getAuthenticationMethod< 48 | DevWabeTypes, 49 | SecondaryProviderInterface 50 | >([secondFAObject.provider], context) 51 | 52 | await secondaryProvider.provider.onSendChallenge?.({ 53 | context, 54 | // @ts-expect-error 55 | user, 56 | }) 57 | 58 | return { accessToken: null, refreshToken: null, id: userId } 59 | } 60 | 61 | const session = new Session() 62 | 63 | const { refreshToken, accessToken } = await session.create(userId, context) 64 | 65 | if (context.wabe.config.authentication?.session?.cookieSession) { 66 | const accessTokenExpiresAt = session.getAccessTokenExpireAt( 67 | context.wabe.config, 68 | ) 69 | const refreshTokenExpiresAt = session.getRefreshTokenExpireAt( 70 | context.wabe.config, 71 | ) 72 | 73 | context.response?.setCookie('refreshToken', refreshToken, { 74 | httpOnly: true, 75 | path: '/', 76 | sameSite: 'None', 77 | secure: true, 78 | expires: refreshTokenExpiresAt, 79 | }) 80 | 81 | context.response?.setCookie('accessToken', accessToken, { 82 | httpOnly: true, 83 | path: '/', 84 | sameSite: 'None', 85 | secure: true, 86 | expires: accessTokenExpiresAt, 87 | }) 88 | } 89 | 90 | return { accessToken, refreshToken, id: userId, srp } 91 | } 92 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/signOutResolver.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock, spyOn } from 'bun:test' 2 | import { signOutResolver } from './signOutResolver' 3 | import { Session } from '../Session' 4 | 5 | describe('signOut', () => { 6 | const mockDeleteCookie = mock(() => {}) 7 | 8 | const context = { 9 | sessionId: 'sessionId', 10 | wabe: { 11 | config: { 12 | authentication: { 13 | session: { 14 | cookieSession: true, 15 | }, 16 | }, 17 | }, 18 | }, 19 | response: { 20 | deleteCookie: mockDeleteCookie, 21 | }, 22 | } as any 23 | 24 | it('should sign out the current user', async () => { 25 | const spyDeleteSession = spyOn( 26 | Session.prototype, 27 | 'delete', 28 | ).mockResolvedValue(undefined) 29 | 30 | const res = await signOutResolver(undefined, {}, context) 31 | 32 | expect(res).toBe(true) 33 | 34 | expect(spyDeleteSession).toHaveBeenCalledTimes(1) 35 | expect(spyDeleteSession).toHaveBeenCalledWith(context) 36 | 37 | expect(mockDeleteCookie).toHaveBeenCalledTimes(2) 38 | expect(mockDeleteCookie).toHaveBeenNthCalledWith(1, 'accessToken') 39 | expect(mockDeleteCookie).toHaveBeenNthCalledWith(2, 'refreshToken') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/signOutResolver.ts: -------------------------------------------------------------------------------- 1 | import type { WabeContext } from '../../server/interface' 2 | import type { DevWabeTypes } from '../../utils/helper' 3 | import { Session } from '../Session' 4 | 5 | export const signOutResolver = async ( 6 | _: any, 7 | __: any, 8 | context: WabeContext, 9 | ) => { 10 | const session = new Session() 11 | 12 | // For the moment we only delete the session because we suppose the token 13 | // are used with headers. We will need to delete the cookies in the future. 14 | await session.delete(context) 15 | 16 | if (context.wabe.config.authentication?.session?.cookieSession) { 17 | context.response?.deleteCookie('accessToken') 18 | context.response?.deleteCookie('refreshToken') 19 | } 20 | 21 | return true 22 | } 23 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/signUpWithResolver.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, afterAll, describe, expect, it } from 'bun:test' 2 | import { type DevWabeTypes, getAnonymousClient } from '../../utils/helper' 3 | import type { Wabe } from '../../server' 4 | import { gql } from 'graphql-request' 5 | import { setupTests, closeTests } from '../../utils/testHelper' 6 | 7 | describe('SignUpWith', () => { 8 | let wabe: Wabe 9 | 10 | beforeAll(async () => { 11 | const setup = await setupTests() 12 | wabe = setup.wabe 13 | }) 14 | 15 | afterAll(async () => { 16 | await closeTests(wabe) 17 | }) 18 | 19 | it('should block the signUpWith if the user creation is blocked for anonymous (the creation is done with root to avoid ACL issues)', () => { 20 | const anonymousClient = getAnonymousClient(wabe.config.port) 21 | 22 | const userSchema = wabe.config.schema?.classes?.find( 23 | (classItem) => classItem.name === 'User', 24 | ) 25 | 26 | if (!userSchema) throw new Error('Failed to find user schema') 27 | 28 | // @ts-expect-error 29 | userSchema.permissions.create.requireAuthentication = true 30 | 31 | expect( 32 | anonymousClient.request( 33 | gql` 34 | mutation signUpWith($input: SignUpWithInput!) { 35 | signUpWith(input: $input) { 36 | id 37 | } 38 | } 39 | `, 40 | { 41 | input: { 42 | authentication: { 43 | emailPassword: { 44 | email: 'email@test.fr', 45 | password: 'password', 46 | }, 47 | }, 48 | }, 49 | }, 50 | ), 51 | ).rejects.toThrow('Permission denied to create class User') 52 | 53 | // @ts-expect-error 54 | userSchema.permissions.create.requireAuthentication = false 55 | }) 56 | 57 | it('should signUpWith email and password when the user not exist', async () => { 58 | const anonymousClient = getAnonymousClient(wabe.config.port) 59 | 60 | const res = await anonymousClient.request( 61 | gql` 62 | mutation signUpWith($input: SignUpWithInput!) { 63 | signUpWith(input: $input) { 64 | id 65 | } 66 | } 67 | `, 68 | { 69 | input: { 70 | authentication: { 71 | emailPassword: { 72 | email: 'test@gmail.com', 73 | password: 'password', 74 | }, 75 | }, 76 | }, 77 | }, 78 | ) 79 | 80 | expect(res.signUpWith.id).toEqual(expect.any(String)) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/signUpWithResolver.ts: -------------------------------------------------------------------------------- 1 | import type { SignUpWithInput } from '../../../generated/wabe' 2 | import type { WabeContext } from '../../server/interface' 3 | import { Session } from '../Session' 4 | 5 | // 0 - Get the authentication method 6 | // 1 - We check if the signUp is possible (call onSign) 7 | // 2 - We create the user 8 | // 3 - We create session 9 | export const signUpWithResolver = async ( 10 | _: any, 11 | { 12 | input, 13 | }: { 14 | input: SignUpWithInput 15 | }, 16 | context: WabeContext, 17 | ) => { 18 | // Create object call the provider signUp 19 | const res = await context.wabe.controllers.database.createObject({ 20 | className: 'User', 21 | data: { 22 | authentication: input.authentication, 23 | }, 24 | context, 25 | select: { id: true }, 26 | }) 27 | 28 | const createdUserId = res?.id 29 | 30 | const session = new Session() 31 | 32 | if (!createdUserId) throw new Error('User not created') 33 | 34 | const { accessToken, refreshToken } = await session.create( 35 | createdUserId, 36 | context, 37 | ) 38 | 39 | if (context.wabe.config.authentication?.session?.cookieSession) { 40 | context.response?.setCookie('refreshToken', refreshToken, { 41 | httpOnly: true, 42 | path: '/', 43 | sameSite: 'None', 44 | secure: true, 45 | expires: session.getRefreshTokenExpireAt(context.wabe.config), 46 | }) 47 | 48 | context.response?.setCookie('accessToken', accessToken, { 49 | httpOnly: true, 50 | path: '/', 51 | sameSite: 'None', 52 | secure: true, 53 | expires: session.getAccessTokenExpireAt(context.wabe.config), 54 | }) 55 | } 56 | 57 | return { accessToken, refreshToken, id: createdUserId } 58 | } 59 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/resolvers/verifyChallenge.ts: -------------------------------------------------------------------------------- 1 | import type { VerifyChallengeInput } from '../../../generated/wabe' 2 | import type { WabeContext } from '../../server/interface' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | import { Session } from '../Session' 5 | import type { SecondaryProviderInterface } from '../interface' 6 | import { getAuthenticationMethod } from '../utils' 7 | 8 | export const verifyChallengeResolver = async ( 9 | _: any, 10 | { 11 | input, 12 | }: { 13 | input: VerifyChallengeInput 14 | }, 15 | context: WabeContext, 16 | ) => { 17 | if (!input.secondFA) throw new Error('One factor is required') 18 | 19 | const listOfFactor = Object.keys(input.secondFA) 20 | 21 | if (listOfFactor.length > 1) throw new Error('Only one factor is allowed') 22 | 23 | const { provider, name } = getAuthenticationMethod< 24 | any, 25 | SecondaryProviderInterface 26 | >(listOfFactor, context) 27 | 28 | const result = await provider.onVerifyChallenge({ 29 | context, 30 | // @ts-expect-error 31 | input: input.secondFA[name], 32 | }) 33 | 34 | if (!result?.userId) throw new Error('Invalid challenge') 35 | 36 | const session = new Session() 37 | 38 | const { accessToken, refreshToken } = await session.create( 39 | result.userId, 40 | context, 41 | ) 42 | 43 | if (context.wabe.config.authentication?.session?.cookieSession) { 44 | const accessTokenExpiresAt = session.getAccessTokenExpireAt( 45 | context.wabe.config, 46 | ) 47 | const refreshTokenExpiresAt = session.getRefreshTokenExpireAt( 48 | context.wabe.config, 49 | ) 50 | 51 | context.response?.setCookie('refreshToken', refreshToken, { 52 | httpOnly: true, 53 | path: '/', 54 | sameSite: 'None', 55 | secure: true, 56 | expires: refreshTokenExpiresAt, 57 | }) 58 | 59 | context.response?.setCookie('accessToken', accessToken, { 60 | httpOnly: true, 61 | path: '/', 62 | sameSite: 'None', 63 | secure: true, 64 | expires: accessTokenExpiresAt, 65 | }) 66 | } 67 | 68 | return { accessToken, srp: result.srp } 69 | } 70 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/roles.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, it, expect } from 'bun:test' 2 | import type { Wabe } from '../server' 3 | import type { DevWabeTypes } from '../utils/helper' 4 | import { initializeRoles } from './roles' 5 | import { setupTests, closeTests } from '../utils/testHelper' 6 | 7 | describe('roles', () => { 8 | let wabe: Wabe 9 | 10 | beforeAll(async () => { 11 | const setup = await setupTests() 12 | wabe = setup.wabe 13 | }) 14 | 15 | afterAll(async () => { 16 | await closeTests(wabe) 17 | }) 18 | 19 | it('should create all roles', async () => { 20 | await wabe.controllers.database.clearDatabase() 21 | 22 | await initializeRoles(wabe) 23 | 24 | const res = await wabe.controllers.database.getObjects({ 25 | className: 'Role', 26 | context: { isRoot: true, wabe: wabe }, 27 | select: { name: true }, 28 | }) 29 | 30 | expect(res.length).toEqual(4) 31 | expect(res.map((role) => role?.name)).toEqual([ 32 | 'Client', 33 | 'Client2', 34 | 'Client3', 35 | 'Admin', 36 | ]) 37 | }) 38 | 39 | it('should not create all roles if there already exist', async () => { 40 | await wabe.controllers.database.clearDatabase() 41 | 42 | await initializeRoles(wabe) 43 | await initializeRoles(wabe) 44 | 45 | const res = await wabe.controllers.database.getObjects({ 46 | className: 'Role', 47 | context: { isRoot: true, wabe: wabe }, 48 | select: { name: true }, 49 | }) 50 | 51 | expect(res.length).toEqual(4) 52 | expect(res.map((role) => role?.name)).toEqual([ 53 | 'Client', 54 | 'Client2', 55 | 'Client3', 56 | 'Admin', 57 | ]) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/roles.ts: -------------------------------------------------------------------------------- 1 | import { notEmpty, type Wabe } from '..' 2 | import type { DevWabeTypes } from '../utils/helper' 3 | 4 | export const initializeRoles = async (wabe: Wabe) => { 5 | const roles = wabe.config?.authentication?.roles || [] 6 | 7 | if (roles.length === 0) return 8 | 9 | const res = await wabe.controllers.database.getObjects({ 10 | className: 'Role', 11 | context: { 12 | isRoot: true, 13 | wabe, 14 | }, 15 | select: { name: true }, 16 | where: { 17 | name: { 18 | in: roles, 19 | }, 20 | }, 21 | }) 22 | 23 | const alreadyCreatedRoles = res.map((role) => role?.name).filter(notEmpty) 24 | 25 | const objectsToCreate = roles 26 | .filter((role) => !alreadyCreatedRoles.includes(role)) 27 | .map((role) => ({ name: role })) 28 | 29 | if (objectsToCreate.length === 0) return 30 | 31 | await wabe.controllers.database.createObjects({ 32 | className: 'Role', 33 | context: { 34 | isRoot: true, 35 | wabe, 36 | }, 37 | data: objectsToCreate, 38 | select: {}, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /packages/wabe/src/authentication/utils.ts: -------------------------------------------------------------------------------- 1 | import { Algorithm, hash } from '@node-rs/argon2' 2 | import type { WabeTypes } from '../server' 3 | import type { WabeContext } from '../server/interface' 4 | import type { 5 | CustomAuthenticationMethods, 6 | ProviderInterface, 7 | SecondaryProviderInterface, 8 | } from './interface' 9 | 10 | export const getAuthenticationMethod = < 11 | T extends WabeTypes, 12 | U = ProviderInterface | SecondaryProviderInterface, 13 | >( 14 | listOfMethods: string[], 15 | context: WabeContext, 16 | ): CustomAuthenticationMethods => { 17 | const customAuthenticationConfig = 18 | context.wabe.config?.authentication?.customAuthenticationMethods 19 | 20 | if (!customAuthenticationConfig) 21 | throw new Error('No custom authentication methods found') 22 | 23 | // We remove the secondary factor to only get all authentication methods 24 | const authenticationMethods = listOfMethods.filter( 25 | (method) => method !== 'secondaryFactor', 26 | ) 27 | 28 | // We check if the client don't use multiple authentication methods at the same time 29 | if (authenticationMethods.length > 1 || authenticationMethods.length === 0) 30 | throw new Error('One authentication method is required at the time') 31 | 32 | const authenticationMethod = authenticationMethods[0] 33 | 34 | // We check if the authentication method is valid 35 | const validAuthenticationMethod = customAuthenticationConfig.find( 36 | (method) => 37 | method.name.toLowerCase() === authenticationMethod.toLowerCase(), 38 | ) 39 | 40 | if (!validAuthenticationMethod) 41 | throw new Error('No available custom authentication methods found') 42 | 43 | return validAuthenticationMethod as CustomAuthenticationMethods 44 | } 45 | 46 | export const hashPassword = (password: string) => 47 | hash(password, { 48 | algorithm: Algorithm.Argon2id, 49 | }) 50 | -------------------------------------------------------------------------------- /packages/wabe/src/cache/InMemoryCache.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, beforeEach, spyOn } from 'bun:test' 2 | import { InMemoryCache } from './InMemoryCache' 3 | 4 | describe('InMemoryCache', () => { 5 | const inMemoryCache = new InMemoryCache({ 6 | interval: 100, 7 | }) 8 | 9 | beforeEach(() => { 10 | inMemoryCache.clear() 11 | }) 12 | 13 | it('should init a InMemoryCache', () => { 14 | const spySetInterval = spyOn(global, 'setInterval') 15 | const spyClearInterval = spyOn(global, 'clearInterval') 16 | 17 | const store = new InMemoryCache({ interval: 100 }) 18 | 19 | store.stop() 20 | 21 | expect(spySetInterval).toHaveBeenCalledTimes(1) 22 | expect(spySetInterval).toHaveBeenCalledWith(expect.any(Function), 100) 23 | expect(spyClearInterval).toHaveBeenCalledTimes(1) 24 | expect(spyClearInterval).toHaveBeenCalledWith(store.intervalId) 25 | }) 26 | 27 | it('should store a value', () => { 28 | inMemoryCache.set('key', 'value') 29 | 30 | expect(inMemoryCache.get('key')).toBe('value') 31 | }) 32 | 33 | it('should clear an in memory cache', () => { 34 | inMemoryCache.set('key', 'value') 35 | 36 | expect(inMemoryCache.get('key')).toBe('value') 37 | 38 | inMemoryCache.clear() 39 | 40 | expect(inMemoryCache.get('key')).toBeUndefined() 41 | }) 42 | 43 | it('should return undefined if the key does not exist', () => { 44 | expect(inMemoryCache.get('key2')).toBeUndefined() 45 | }) 46 | 47 | it('should clear a cache after timeLimit', () => { 48 | const localInMemoryCache = new InMemoryCache({ 49 | interval: 100, 50 | }) 51 | 52 | localInMemoryCache.set('key', 'value') 53 | 54 | setTimeout(() => { 55 | expect(localInMemoryCache.get('key')).not.toBeUndefined() 56 | }, 50) 57 | 58 | setTimeout(() => { 59 | expect(localInMemoryCache.get('key')).toBeUndefined() 60 | }, 100) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/wabe/src/cache/InMemoryCache.ts: -------------------------------------------------------------------------------- 1 | export interface InMemoryCacheOptions { 2 | /** 3 | * Interval in ms to clear the cache 4 | */ 5 | interval: number 6 | } 7 | 8 | /** 9 | * InMemoryCache is a class that stores data for a certain amount of time 10 | */ 11 | export class InMemoryCache { 12 | private options: InMemoryCacheOptions 13 | private store: Record 14 | 15 | public intervalId: Timer | undefined = undefined 16 | 17 | constructor(options: InMemoryCacheOptions) { 18 | this.options = options 19 | this.store = {} 20 | 21 | this._init() 22 | } 23 | 24 | _init() { 25 | this.intervalId = setInterval(() => { 26 | this.clear() 27 | }, this.options.interval) 28 | } 29 | 30 | set(key: string, value: T) { 31 | this.store[key] = value 32 | } 33 | 34 | get(key: string): T | undefined { 35 | return this.store[key] 36 | } 37 | 38 | clear() { 39 | this.store = {} 40 | } 41 | 42 | stop() { 43 | clearInterval(this.intervalId) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/wabe/src/cron/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import { cron } from '.' 3 | 4 | describe('cron', () => { 5 | it('should run the function', () => { 6 | const run = mock() 7 | 8 | const job = cron({ 9 | pattern: '* * * * * *', 10 | run, 11 | })({} as any) 12 | 13 | job.trigger() 14 | 15 | expect(run).toHaveBeenCalled() 16 | 17 | expect(run).toHaveBeenCalled() 18 | }) 19 | 20 | it('should should enable protected runs', async () => { 21 | const run = mock() 22 | 23 | cron({ 24 | pattern: '* * * * * *', 25 | run: async () => { 26 | run() 27 | await new Promise((resolve) => setTimeout(resolve, 4000)) 28 | }, 29 | enabledProtectedRuns: true, 30 | })({} as any) 31 | 32 | await new Promise((resolve) => setTimeout(resolve, 2100)) 33 | 34 | expect(run).toHaveBeenCalledTimes(1) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/wabe/src/cron/index.ts: -------------------------------------------------------------------------------- 1 | import { Cron } from 'croner' 2 | import type { Wabe, WabeTypes } from '../server' 3 | 4 | export type OutputCron = (wabe: Wabe) => Cron 5 | 6 | export const cron = 7 | ({ 8 | pattern, 9 | run, 10 | maxRuns, 11 | enabledProtectedRuns, 12 | }: { 13 | pattern: string 14 | maxRuns?: number 15 | enabledProtectedRuns?: boolean 16 | run: (wabe: Wabe) => any | Promise 17 | }): OutputCron => 18 | (wabe: Wabe) => 19 | new Cron(pattern, { maxRuns, protect: enabledProtectedRuns }, () => 20 | run(wabe), 21 | ) 22 | 23 | export enum CronExpressions { 24 | EVERY_SECOND = '* * * * * *', 25 | EVERY_MINUTE = '0 * * * * *', 26 | EVERY_HOUR = '0 0 * * * *', 27 | EVERY_DAY_AT_MIDNIGHT = '0 0 0 * * *', 28 | EVERY_WEEK = '0 0 0 * * 0', 29 | EVERY_MONTH = '0 0 0 1 * *', 30 | EVERY_YEAR = '0 0 0 1 1 *', 31 | WEEKDAYS_MORNING = '0 0 7 * * 1-5', // Every weekday at 7 AM 32 | WEEKENDS_EVENING = '0 0 19 * * 6-7', // Every weekend at 7 PM 33 | FIRST_DAY_OF_MONTH = '0 0 0 1 * *', 34 | LAST_DAY_OF_MONTH = '0 0 0 L * *', // L stands for the last day of the month 35 | EVERY_15_MINUTES = '0 */15 * * * *', 36 | EVERY_30_MINUTES = '0 */30 * * * *', 37 | EVERY_2_HOURS = '0 0 */2 * * *', 38 | EVERY_6_HOURS = '0 0 */6 * * *', 39 | EVERY_12_HOURS = '0 0 */12 * * *', 40 | } 41 | 42 | export type CronConfig = Array<{ 43 | name: string 44 | cron: OutputCron 45 | job?: Cron 46 | }> 47 | -------------------------------------------------------------------------------- /packages/wabe/src/database/index.ts: -------------------------------------------------------------------------------- 1 | import type { WabeTypes } from '../server' 2 | import type { DatabaseAdapter } from './interface' 3 | 4 | export interface DatabaseConfig { 5 | adapter: DatabaseAdapter 6 | } 7 | 8 | export * from './DatabaseController' 9 | export * from './interface' 10 | -------------------------------------------------------------------------------- /packages/wabe/src/email/DevAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { EmailAdapter } from './interface' 2 | 3 | export class EmailDevAdapter implements EmailAdapter { 4 | // biome-ignore lint/suspicious/useAwait: false 5 | async send() { 6 | return '123456' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/wabe/src/email/EmailController.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, mock } from 'bun:test' 2 | import { EmailController } from './EmailController' 3 | 4 | describe('EmailController', () => { 5 | it('should send email using correct adapter', async () => { 6 | const mockSend = mock(() => {}) 7 | const dummyAdapter = { 8 | send: mockSend, 9 | } 10 | 11 | // @ts-expect-error 12 | const controller = new EmailController(dummyAdapter) 13 | 14 | await controller.send({ 15 | from: 'from', 16 | to: ['to'], 17 | subject: 'subject', 18 | text: 'text', 19 | }) 20 | 21 | expect(mockSend).toHaveBeenCalledTimes(1) 22 | expect(mockSend).toHaveBeenCalledWith({ 23 | from: 'from', 24 | to: ['to'], 25 | subject: 'subject', 26 | text: 'text', 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/wabe/src/email/EmailController.ts: -------------------------------------------------------------------------------- 1 | import type { EmailAdapter, EmailSendOptions } from './interface' 2 | 3 | export class EmailController implements EmailAdapter { 4 | public adapter: EmailAdapter 5 | 6 | constructor(adapter: EmailAdapter) { 7 | this.adapter = adapter 8 | } 9 | 10 | send(options: EmailSendOptions) { 11 | return this.adapter.send(options) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/wabe/src/email/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface' 2 | export * from './DevAdapter' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/email/interface.ts: -------------------------------------------------------------------------------- 1 | export type HtmlTemplates = { 2 | sendOTPCode: { 3 | fn: (options: { otp: string }) => string | Promise 4 | subject: string 5 | } 6 | } 7 | 8 | export interface EmailSendOptions { 9 | from: string 10 | to: Array 11 | subject: string 12 | node?: React.ReactNode 13 | html?: string 14 | text?: string 15 | } 16 | 17 | export interface EmailAdapter { 18 | /** 19 | * Send an email using the provided adapter 20 | * @param options Mail options (expeditor, recipient, subject ...) 21 | * @return The id of the email sended, throw an error if something wrong 22 | */ 23 | send(options: EmailSendOptions): Promise 24 | } 25 | 26 | /** 27 | * Configuration for the email in Wabe 28 | * @property adapter The adapter to use to send emails 29 | * @property mainEmail The email to use as sender for emails sent by Wabe 30 | * @property templates The html templates to use for a specific email. If not provided, Wabe will use the default templates 31 | */ 32 | export interface EmailConfig { 33 | adapter: EmailAdapter 34 | mainEmail?: string 35 | htmlTemplates?: HtmlTemplates 36 | } 37 | -------------------------------------------------------------------------------- /packages/wabe/src/files/FileController.ts: -------------------------------------------------------------------------------- 1 | import type { Wabe } from '../server' 2 | import type { DevWabeTypes } from '../utils/helper' 3 | import type { FileAdapter, ReadFileOptions } from './interface' 4 | 5 | export class FileController implements FileAdapter { 6 | public adapter: FileAdapter 7 | private wabe: Wabe 8 | 9 | constructor(adapter: FileAdapter, wabe: Wabe) { 10 | this.adapter = adapter 11 | this.wabe = wabe 12 | } 13 | 14 | uploadFile(file: File | Blob) { 15 | return this.adapter.uploadFile(file) 16 | } 17 | 18 | readFile(fileName: string, options?: ReadFileOptions) { 19 | return this.adapter.readFile(fileName, { 20 | ...options, 21 | port: this.wabe.config.port, 22 | }) 23 | } 24 | 25 | deleteFile(fileName: string) { 26 | return this.adapter.deleteFile(fileName) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/wabe/src/files/FileDevAdapter.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, mkdir, rm, access, constants } from 'node:fs/promises' 2 | import path from 'node:path' 3 | import type { FileAdapter, ReadFileOptions } from '.' 4 | 5 | export class FileDevAdapter implements FileAdapter { 6 | private basePath = 'bucket' 7 | private rootPath = process.cwd() 8 | 9 | async uploadFile(file: File): Promise { 10 | const fullPath = path.join(this.rootPath, this.basePath) 11 | 12 | await mkdir(fullPath, { recursive: true }) 13 | 14 | const fileType = file.type 15 | 16 | let fileContent: Buffer 17 | 18 | if (fileType.startsWith('text') || fileType.includes('json')) { 19 | const textContent = await file.text() 20 | fileContent = Buffer.from(textContent, 'utf-8') 21 | } else { 22 | const arrayBuffer = await file.arrayBuffer() 23 | fileContent = Buffer.from(arrayBuffer) 24 | } 25 | 26 | await writeFile(path.join(fullPath, file.name), fileContent) 27 | } 28 | 29 | async readFile( 30 | fileName: string, 31 | options?: ReadFileOptions, 32 | ): Promise { 33 | const filePath = path.join(this.rootPath, this.basePath, fileName) 34 | 35 | try { 36 | await access(filePath, constants.F_OK) 37 | return `http://127.0.0.1:${options?.port || 3001}/${this.basePath}/${fileName}` 38 | } catch { 39 | return null 40 | } 41 | } 42 | 43 | async deleteFile(fileName: string): Promise { 44 | const filePath = path.join(this.rootPath, this.basePath, fileName) 45 | 46 | try { 47 | await access(filePath, constants.F_OK) 48 | 49 | await rm(filePath) 50 | } catch { 51 | // Do nothing 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/wabe/src/files/hookDeleteFile.ts: -------------------------------------------------------------------------------- 1 | import type { HookObject } from '../hooks/HookObject' 2 | 3 | const deleteFile = async (hookObject: HookObject) => { 4 | const schema = hookObject.context.wabe.config.schema?.classes?.find( 5 | (currentClass) => currentClass.name === hookObject.className, 6 | ) 7 | 8 | if (!schema) return 9 | 10 | Object.entries(schema.fields) 11 | .filter(([_, value]) => value.type === 'File') 12 | .map(async ([fieldName]) => { 13 | const fileName = hookObject.originalObject?.[fieldName]?.name as string 14 | 15 | if (!fileName) return 16 | 17 | if (!hookObject.context.wabe.controllers.file) 18 | throw new Error('No file adapter found') 19 | 20 | await hookObject.context.wabe.controllers.file?.deleteFile(fileName) 21 | }) 22 | } 23 | 24 | export const defaultAfterDeleteFile = (hookObject: HookObject) => 25 | deleteFile(hookObject) 26 | -------------------------------------------------------------------------------- /packages/wabe/src/files/hookReadFile.ts: -------------------------------------------------------------------------------- 1 | import type { HookObject } from '../hooks/HookObject' 2 | 3 | const getFile = async (hookObject: HookObject) => { 4 | const schema = hookObject.context.wabe.config.schema?.classes?.find( 5 | (currentClass) => currentClass.name === hookObject.className, 6 | ) 7 | 8 | if (!schema) return 9 | 10 | const urlCacheInSeconds = 11 | hookObject.context.wabe.config.file?.urlCacheInSeconds || 3600 * 24 12 | 13 | // After read we get the file URL and we update the field url with an URL. 14 | // For security purpose we recommend to use a presigned URL 15 | await Promise.all( 16 | Object.entries(schema.fields) 17 | .filter(([_, value]) => value.type === 'File') 18 | .map(async ([fieldName]) => { 19 | const fileInfo = hookObject.object?.[fieldName] 20 | 21 | if (!fileInfo) return 22 | 23 | const fileName = fileInfo.name as string 24 | 25 | if (!fileName && fileInfo.url) return fileInfo.url 26 | 27 | const fileUrlGeneratedAt = new Date(fileInfo.urlGeneratedAt) 28 | 29 | if ( 30 | fileUrlGeneratedAt && 31 | fileUrlGeneratedAt.getTime() + urlCacheInSeconds * 1000 > 32 | new Date().getTime() 33 | ) 34 | return 35 | 36 | if (!hookObject.context.wabe.controllers.file) 37 | throw new Error('No file adapter found') 38 | 39 | const fileUrlFromBucket = 40 | await hookObject.context.wabe.controllers.file?.readFile(fileName) 41 | 42 | return hookObject.context.wabe.controllers.database.updateObject({ 43 | className: hookObject.className, 44 | context: hookObject.context, 45 | id: hookObject.object?.id || '', 46 | data: { 47 | [fieldName]: { 48 | ...fileInfo, 49 | urlGeneratedAt: new Date(), 50 | url: fileUrlFromBucket || fileInfo.url, 51 | }, 52 | }, 53 | skipHooks: true, 54 | }) 55 | }), 56 | ) 57 | } 58 | 59 | export const defaultAfterReadFile = (hookObject: HookObject) => 60 | getFile(hookObject) 61 | -------------------------------------------------------------------------------- /packages/wabe/src/files/hookUploadFile.ts: -------------------------------------------------------------------------------- 1 | import type { HookObject } from '../hooks/HookObject' 2 | 3 | const handleFile = async (hookObject: HookObject) => { 4 | const newData = hookObject.getNewData() 5 | 6 | const schema = hookObject.context.wabe.config.schema?.classes?.find( 7 | (currentClass) => currentClass.name === hookObject.className, 8 | ) 9 | 10 | if (!schema) return 11 | 12 | await Promise.all( 13 | Object.keys(newData).map(async (keyName) => { 14 | const file = newData[keyName]?.file as File 15 | const url = newData[keyName]?.url as string 16 | 17 | if (!file && !url) return 18 | 19 | if (url) { 20 | hookObject.upsertNewData(keyName, { url, isPresignedUrl: false }) 21 | return 22 | } 23 | 24 | if (schema.fields[keyName].type !== 'File' || !(file instanceof File)) 25 | return 26 | 27 | if (!hookObject.context.wabe.controllers.file) 28 | throw new Error('No file adapter found') 29 | 30 | // We upload the file and set the name of the file in the newData 31 | await hookObject.context.wabe.controllers.file?.uploadFile(file) 32 | 33 | hookObject.upsertNewData(keyName, { 34 | name: file.name, 35 | isPresignedUrl: true, 36 | }) 37 | }), 38 | ) 39 | } 40 | 41 | export const defaultBeforeCreateUpload = (hookObject: HookObject) => 42 | handleFile(hookObject) 43 | 44 | export const defaultBeforeUpdateUpload = (hookObject: HookObject) => 45 | handleFile(hookObject) 46 | -------------------------------------------------------------------------------- /packages/wabe/src/files/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FileDevAdapter' 2 | export * from './interface' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/files/interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The file config contains the adapter to use to upload file 3 | * @param adapter: FileAdapter 4 | * @param urlCacheInSeconds: number Number of seconds to cache the url, equal to the number of seconds the url will be valid 5 | * @param devDirectory: string The directory where the files will be uploaded 6 | */ 7 | export interface FileConfig { 8 | adapter: FileAdapter 9 | urlCacheInSeconds?: number 10 | devDirectory?: string 11 | } 12 | 13 | export interface ReadFileOptions { 14 | urlExpiresIn?: number 15 | port?: number 16 | } 17 | 18 | export interface FileAdapter { 19 | /** 20 | * Upload a file and returns the url of the file 21 | * @param file: File 22 | */ 23 | uploadFile(file: File | Blob): Promise 24 | /** 25 | * Read a file and returns the url of the file 26 | * @param fileName: string 27 | * @param urlExpiresIn: number Number of seconds to expire the url 28 | * @returns The url of file or null if the file doesn't exist 29 | */ 30 | readFile( 31 | fileName: string, 32 | options?: ReadFileOptions, 33 | ): Promise | string | null 34 | /*+ 35 | * Delete a file 36 | * @param fileName: string 37 | */ 38 | deleteFile(fileName: string): Promise 39 | } 40 | -------------------------------------------------------------------------------- /packages/wabe/src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './GraphQLSchema' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/graphql/parseGraphqlSchema.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLSchema } from 'graphql' 2 | 3 | export interface GraphqlSchemaOutput { 4 | input: Record 5 | output?: string 6 | } 7 | 8 | const _getQueryFields = ( 9 | name: string, 10 | schema: GraphQLSchema, 11 | ): GraphqlSchemaOutput => { 12 | const query = schema.getQueryType()?.getFields()[name] 13 | 14 | if (!query) throw new Error('Type not found in schema') 15 | 16 | const inputFields = query.args.reduce( 17 | (acc, arg) => ({ ...acc, [arg.name]: arg.type.toString() }), 18 | {}, 19 | ) 20 | 21 | const outputFields = query.type.toString() 22 | 23 | return { 24 | input: inputFields, 25 | output: outputFields, 26 | } 27 | } 28 | 29 | const _getMutationFields = ( 30 | name: string, 31 | schema: GraphQLSchema, 32 | ): GraphqlSchemaOutput => { 33 | const mutation = schema.getMutationType()?.getFields()[name] 34 | 35 | if (!mutation) throw new Error('Type not found in schema') 36 | 37 | const inputFields = mutation.args.reduce( 38 | (acc, arg) => ({ ...acc, [arg.name]: arg.type.toString() }), 39 | {}, 40 | ) 41 | 42 | const outputFields = mutation.type.toString() 43 | 44 | return { 45 | input: inputFields, 46 | output: outputFields, 47 | } 48 | } 49 | 50 | const _getTypeFields = ( 51 | name: string, 52 | schema: GraphQLSchema, 53 | ): GraphqlSchemaOutput => { 54 | const type = schema.getType(name) 55 | 56 | if (!type) throw new Error('Type not found in schema') 57 | 58 | // @ts-expect-error 59 | const fields = (type?.getFields?.() || {}) as Record 60 | 61 | const formatedFields = Object.entries(fields).reduce( 62 | (acc, [fieldName, fieldType]) => ({ 63 | ...acc, 64 | [fieldName]: fieldType.type.toString(), 65 | }), 66 | {}, 67 | ) 68 | 69 | return { 70 | input: formatedFields, 71 | } 72 | } 73 | 74 | export const getTypeFromGraphQLSchema = ({ 75 | type, 76 | name, 77 | schema, 78 | }: { 79 | type: 'Type' | 'Query' | 'Mutation' 80 | name: string 81 | filter?: Array 82 | schema: GraphQLSchema 83 | }): GraphqlSchemaOutput => { 84 | switch (type) { 85 | case 'Query': 86 | return _getQueryFields(name, schema) 87 | case 'Mutation': 88 | return _getMutationFields(name, schema) 89 | case 'Type': 90 | return _getTypeFields(name, schema) 91 | default: 92 | throw new Error('Not implemented') 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/authentication.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderInterface } from '../authentication' 2 | import { getAuthenticationMethod } from '../authentication/utils' 3 | import type { DevWabeTypes } from '../utils/helper' 4 | import type { HookObject } from './HookObject' 5 | 6 | export const defaultCallAuthenticationProviderOnBeforeCreateUser = async ( 7 | hookObject: HookObject, 8 | ) => { 9 | if ( 10 | !hookObject.isFieldUpdated('authentication') || 11 | hookObject.getNewData().isOauth 12 | ) 13 | return 14 | 15 | const context = hookObject.context 16 | 17 | const authentication = hookObject.getNewData().authentication 18 | 19 | // Exception for SRP 20 | if (authentication.emailPasswordSRP) return 21 | 22 | const { provider, name } = getAuthenticationMethod< 23 | DevWabeTypes, 24 | ProviderInterface 25 | >(Object.keys(authentication), context) 26 | 27 | const inputOfTheGoodAuthenticationMethod = authentication[name] 28 | 29 | const { authenticationDataToSave } = await provider.onSignUp({ 30 | input: inputOfTheGoodAuthenticationMethod, 31 | context, 32 | }) 33 | 34 | hookObject.upsertNewData('authentication', { 35 | [name]: authenticationDataToSave, 36 | }) 37 | } 38 | 39 | export const defaultCallAuthenticationProviderOnBeforeUpdateUser = async ( 40 | hookObject: HookObject, 41 | ) => { 42 | if ( 43 | !hookObject.isFieldUpdated('authentication') || 44 | hookObject.getNewData().isOauth 45 | ) 46 | return 47 | 48 | const context = hookObject.context 49 | 50 | const authentication = hookObject.getNewData().authentication 51 | 52 | // Exception for SRP 53 | if (authentication.emailPasswordSRP) return 54 | 55 | const { provider, name } = getAuthenticationMethod< 56 | DevWabeTypes, 57 | ProviderInterface 58 | >(Object.keys(authentication), context) 59 | 60 | if (!provider.onUpdateAuthenticationData) return 61 | 62 | const inputOfTheGoodAuthenticationMethod = authentication[name] 63 | 64 | if (!hookObject.object?.id) return 65 | 66 | const { authenticationDataToSave } = 67 | await provider.onUpdateAuthenticationData({ 68 | context, 69 | input: inputOfTheGoodAuthenticationMethod, 70 | userId: hookObject.object.id, 71 | }) 72 | 73 | hookObject.upsertNewData('authentication', { 74 | [name]: authenticationDataToSave, 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/defaultFields.ts: -------------------------------------------------------------------------------- 1 | import { getClassFromClassName } from '../utils' 2 | import type { DevWabeTypes } from '../utils/helper' 3 | import type { HookObject } from './HookObject' 4 | 5 | export const defaultBeforeCreateForCreatedAt = ( 6 | object: HookObject, 7 | ) => { 8 | if (!object.isFieldUpdated('createdAt')) 9 | object.upsertNewData('createdAt', new Date()) 10 | 11 | if (!object.isFieldUpdated('updatedAt')) 12 | object.upsertNewData('updatedAt', new Date()) 13 | } 14 | 15 | export const defaultBeforeUpdateForUpdatedAt = ( 16 | object: HookObject, 17 | ) => { 18 | object.upsertNewData('updatedAt', new Date()) 19 | } 20 | 21 | export const defaultBeforeCreateForDefaultValue = async ( 22 | object: HookObject, 23 | ) => { 24 | const schemaClass = getClassFromClassName( 25 | object.className, 26 | object.context.wabe.config, 27 | ) 28 | const allFields = Object.keys(schemaClass.fields) 29 | allFields.map((field) => { 30 | const currentSchemaField = schemaClass.fields[field] 31 | if ( 32 | !object.isFieldUpdated(field) && 33 | currentSchemaField.type !== 'Pointer' && 34 | currentSchemaField.type !== 'Relation' && 35 | currentSchemaField.type !== 'File' && 36 | currentSchemaField.defaultValue !== undefined 37 | ) 38 | object.upsertNewData(field, currentSchemaField.defaultValue) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/deleteSession.ts: -------------------------------------------------------------------------------- 1 | import { contextWithRoot } from '../utils/export' 2 | import type { DevWabeTypes } from '../utils/helper' 3 | import type { HookObject } from './HookObject' 4 | 5 | // TODO: It should better to do this in after delete to avoid case when deleteUser failed 6 | // For the moment KISS 7 | export const defaultDeleteSessionOnDeleteUser = async ( 8 | object: HookObject, 9 | ) => { 10 | const userId = object.object?.id 11 | 12 | await object.context.wabe.controllers.database.deleteObjects({ 13 | className: '_Session', 14 | context: contextWithRoot(object.context), 15 | where: { 16 | user: { id: { equalTo: userId } }, 17 | }, 18 | select: {}, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/protected.ts: -------------------------------------------------------------------------------- 1 | import { OperationType } from '.' 2 | import type { DevWabeTypes } from '../utils/helper' 3 | import type { HookObject } from './HookObject' 4 | 5 | const _checkProtected = ( 6 | hookObject: HookObject, 7 | operationType: OperationType, 8 | ) => { 9 | const schemaClass = hookObject.context.wabe.config.schema?.classes?.find( 10 | (currentClass) => currentClass.name === hookObject.className, 11 | ) 12 | 13 | if (!schemaClass) return 14 | 15 | const userRole = hookObject.getUser()?.role?.name || '' 16 | const isRoot = hookObject.context.isRoot 17 | 18 | if (operationType === OperationType.BeforeRead) { 19 | Object.keys(hookObject.select).map((fieldName) => { 20 | const protectedForCurrentField = schemaClass.fields[fieldName]?.protected 21 | 22 | if (protectedForCurrentField?.protectedOperations.includes('read')) { 23 | if ( 24 | isRoot && 25 | protectedForCurrentField.authorizedRoles.includes('rootOnly') 26 | ) 27 | return 28 | 29 | // @ts-expect-error 30 | if (!protectedForCurrentField.authorizedRoles.includes(userRole)) 31 | throw new Error('You are not authorized to read this field') 32 | } 33 | }) 34 | 35 | return 36 | } 37 | 38 | const fieldsUpdated = hookObject.getNewData() 39 | 40 | const operation = 41 | operationType === OperationType.BeforeUpdate ? 'update' : 'create' 42 | 43 | Object.keys(fieldsUpdated).map((fieldName) => { 44 | const protectedForCurrentField = schemaClass.fields[fieldName]?.protected 45 | 46 | if (protectedForCurrentField?.protectedOperations.includes(operation)) { 47 | if ( 48 | isRoot && 49 | protectedForCurrentField.authorizedRoles.includes('rootOnly') 50 | ) 51 | return 52 | 53 | // @ts-expect-error 54 | if (!protectedForCurrentField.authorizedRoles.includes(userRole)) 55 | throw new Error(`You are not authorized to ${operation} this field`) 56 | } 57 | }) 58 | } 59 | 60 | export const defaultCheckProtectedOnBeforeRead = ( 61 | object: HookObject, 62 | ) => _checkProtected(object, OperationType.BeforeRead) 63 | 64 | export const defaultCheckProtectedOnBeforeUpdate = ( 65 | object: HookObject, 66 | ) => _checkProtected(object, OperationType.BeforeUpdate) 67 | 68 | export const defaultCheckProtectedOnBeforeCreate = ( 69 | object: HookObject, 70 | ) => _checkProtected(object, OperationType.BeforeCreate) 71 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/session.ts: -------------------------------------------------------------------------------- 1 | import type { DevWabeTypes } from '../utils/helper' 2 | import { notEmpty } from '../utils/export' 3 | import type { HookObject } from './HookObject' 4 | 5 | export const defaultAfterCreateSession = async ( 6 | hookObject: HookObject, 7 | ) => { 8 | const object = hookObject.object 9 | // @ts-expect-error 10 | const userId = object?.user as string 11 | 12 | if (!userId) return 13 | 14 | const databaseController = hookObject.context.wabe.controllers.database 15 | 16 | const user = await databaseController.getObject({ 17 | className: 'User', 18 | id: userId, 19 | select: { 20 | sessions: true, 21 | }, 22 | context: hookObject.context, 23 | }) 24 | 25 | const sessionsId = user?.sessions?.map((session) => session.id) || [] 26 | 27 | await databaseController.updateObject({ 28 | className: 'User', 29 | id: userId, 30 | context: hookObject.context, 31 | data: { 32 | sessions: [...sessionsId, object?.id].filter(notEmpty), 33 | }, 34 | }) 35 | } 36 | 37 | export const defaultAfterDeleteSession = async ( 38 | hookObject: HookObject, 39 | ) => { 40 | const object = hookObject.object 41 | // @ts-expect-error 42 | const userId = object?.user as string 43 | 44 | if (!userId) return 45 | 46 | const databaseController = hookObject.context.wabe.controllers.database 47 | 48 | const user = await databaseController.getObject({ 49 | className: 'User', 50 | id: userId, 51 | select: { 52 | sessions: true, 53 | }, 54 | context: hookObject.context, 55 | }) 56 | 57 | const newSessionsId = user?.sessions 58 | ?.filter((session) => session.id !== object?.id) 59 | .map((session) => session.id) 60 | 61 | await databaseController.updateObject({ 62 | className: 'User', 63 | id: userId, 64 | context: hookObject.context, 65 | data: { 66 | sessions: newSessionsId, 67 | }, 68 | }) 69 | } 70 | 71 | export const defaultBeforeUpdateSessionOnUser = ( 72 | hookObject: HookObject, 73 | ) => { 74 | if (hookObject.context.isRoot) return 75 | 76 | if (hookObject.isFieldUpdated('sessions')) 77 | throw new Error('Not authorized to update user sessions') 78 | } 79 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/setEmail.ts: -------------------------------------------------------------------------------- 1 | import type { DevWabeTypes } from '../utils/helper' 2 | import type { HookObject } from './HookObject' 3 | 4 | const updateEmail = (object: HookObject) => { 5 | const authentication = object.getNewData().authentication 6 | 7 | if (!authentication) return 8 | 9 | // Considering that we only have one authentication provider (for double auth maybe need an adjustment) 10 | const provider = Object.keys(authentication)[0] 11 | 12 | const emailToSave = authentication[provider].email 13 | 14 | if (!emailToSave) return 15 | 16 | object.upsertNewData('email', emailToSave) 17 | 18 | if (provider) object.upsertNewData('provider', provider) 19 | } 20 | 21 | // This hook works for official authentication provider that store email in "email" field 22 | // Maybe need custom hook for custom authentication provider 23 | export const defaultSetEmail = (object: HookObject) => { 24 | if (object.isFieldUpdated('email')) return 25 | 26 | updateEmail(object) 27 | } 28 | 29 | export const defaultSetEmailOnUpdate = ( 30 | object: HookObject, 31 | ) => { 32 | if (object.isFieldUpdated('email')) return 33 | 34 | updateEmail(object) 35 | } 36 | -------------------------------------------------------------------------------- /packages/wabe/src/hooks/setupAcl.ts: -------------------------------------------------------------------------------- 1 | import type { HookObject } from './HookObject' 2 | 3 | const setupAcl = async (hookObject: HookObject) => { 4 | const schemaPermissionsObject = 5 | hookObject.context.wabe.config.schema?.classes?.find( 6 | (c) => c.name === hookObject.className, 7 | )?.permissions 8 | 9 | if (!schemaPermissionsObject) return 10 | 11 | const { acl } = schemaPermissionsObject 12 | 13 | if (hookObject.isFieldUpdated('acl') || !acl) return 14 | 15 | if (acl) await acl(hookObject) 16 | } 17 | 18 | export const defaultSetupAclBeforeCreate = async ( 19 | hookObject: HookObject, 20 | ) => { 21 | // ACL on user need an update mutation not upsertNewData 22 | if (hookObject.className === 'User') return 23 | 24 | await setupAcl(hookObject) 25 | } 26 | 27 | export const defaultSetupAclOnUserAfterCreate = async ( 28 | hookObject: HookObject, 29 | ) => setupAcl(hookObject) 30 | -------------------------------------------------------------------------------- /packages/wabe/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server' 2 | export * from './hooks' 3 | export * from './schema' 4 | export * from './database' 5 | export * from './authentication' 6 | export * from './files' 7 | export * from './email' 8 | export * from './payment' 9 | export * from './ai' 10 | export * from './utils/export' 11 | export * from './cron' 12 | -------------------------------------------------------------------------------- /packages/wabe/src/payment/DevAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { PaymentAdapter } from './interface' 2 | 3 | export class PaymentDevAdapter implements PaymentAdapter { 4 | adapter = {} 5 | // biome-ignore lint/suspicious/useAwait: false 6 | async getCustomerById() { 7 | return { 8 | email: 'customer@test.com', 9 | } 10 | } 11 | 12 | async deleteCoupon() {} 13 | 14 | async updatePromotionCode() {} 15 | 16 | // biome-ignore lint/suspicious/useAwait: false 17 | async createCoupon() { 18 | return { code: '', id: '' } 19 | } 20 | 21 | // biome-ignore lint/suspicious/useAwait: false 22 | async createPromotionCode() { 23 | return { code: '', id: '' } 24 | } 25 | 26 | // biome-ignore lint/suspicious/useAwait: false 27 | async validateWebhook() { 28 | return { isValid: true, payload: {}, type: '' } 29 | } 30 | 31 | // biome-ignore lint/suspicious/useAwait: false 32 | async createCustomer() { 33 | return '' 34 | } 35 | 36 | // biome-ignore lint/suspicious/useAwait: false 37 | async createPayment() { 38 | return '' 39 | } 40 | 41 | async cancelSubscription() {} 42 | 43 | // biome-ignore lint/suspicious/useAwait: false 44 | async getInvoices() { 45 | return [] 46 | } 47 | 48 | // biome-ignore lint/suspicious/useAwait: false 49 | async getTotalRevenue() { 50 | return 0 51 | } 52 | 53 | // biome-ignore lint/suspicious/useAwait: false 54 | async getAllTransactions() { 55 | return [] 56 | } 57 | 58 | // biome-ignore lint/suspicious/useAwait: false 59 | async getHypotheticalSubscriptionRevenue() { 60 | return 0 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/wabe/src/payment/PaymentController.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CancelSubscriptionOptions, 3 | CreateCustomerOptions, 4 | CreatePaymentOptions, 5 | GetInvoicesOptions, 6 | PaymentAdapter, 7 | PaymentConfig, 8 | GetTotalRevenueOptions, 9 | GetAllTransactionsOptions, 10 | GetCustomerByIdOptions, 11 | CreateCouponOptions, 12 | CreatePromotionCodeOptions, 13 | DeleteCouponOptions, 14 | UpdatePromotionCodeOptions, 15 | } from './interface' 16 | 17 | export class PaymentController implements PaymentAdapter { 18 | public adapter: PaymentAdapter 19 | private config: PaymentConfig 20 | 21 | constructor(paymentConfig: PaymentConfig) { 22 | this.adapter = paymentConfig.adapter 23 | this.config = paymentConfig 24 | } 25 | 26 | deleteCoupon(options: DeleteCouponOptions) { 27 | return this.adapter.deleteCoupon(options) 28 | } 29 | 30 | updatePromotionCode(options: UpdatePromotionCodeOptions) { 31 | return this.adapter.updatePromotionCode(options) 32 | } 33 | 34 | createCoupon(options: CreateCouponOptions) { 35 | return this.adapter.createCoupon(options) 36 | } 37 | 38 | createPromotionCode(options: CreatePromotionCodeOptions) { 39 | return this.adapter.createPromotionCode(options) 40 | } 41 | 42 | getCustomerById(options: GetCustomerByIdOptions) { 43 | return this.adapter.getCustomerById(options) 44 | } 45 | 46 | validateWebhook(ctx: any) { 47 | return this.adapter.validateWebhook(ctx) 48 | } 49 | 50 | createCustomer(options: CreateCustomerOptions) { 51 | return this.adapter.createCustomer(options) 52 | } 53 | 54 | createPayment( 55 | options: Omit, 56 | ) { 57 | return this.adapter.createPayment({ 58 | ...options, 59 | currency: this.config.currency, 60 | paymentMethod: this.config.supportedPaymentMethods, 61 | }) 62 | } 63 | 64 | cancelSubscription(options: CancelSubscriptionOptions) { 65 | return this.adapter.cancelSubscription(options) 66 | } 67 | 68 | getInvoices(options: GetInvoicesOptions) { 69 | return this.adapter.getInvoices(options) 70 | } 71 | 72 | getTotalRevenue(options: GetTotalRevenueOptions) { 73 | return this.adapter.getTotalRevenue(options) 74 | } 75 | 76 | getAllTransactions(options: GetAllTransactionsOptions) { 77 | return this.adapter.getAllTransactions(options) 78 | } 79 | 80 | getHypotheticalSubscriptionRevenue() { 81 | return this.adapter.getHypotheticalSubscriptionRevenue() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/wabe/src/payment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface' 2 | export * from './DevAdapter' 3 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/defaultResolvers.ts: -------------------------------------------------------------------------------- 1 | import type { MutationResolver, QueryResolver } from './Schema' 2 | import { meResolver } from './resolvers/meResolver' 3 | import { sendEmailResolver } from './resolvers/sendEmail' 4 | import { resetPasswordResolver } from './resolvers/resetPassword' 5 | import { sendOtpCodeResolver } from './resolvers/sendOtpCode' 6 | 7 | export const defaultQueries: { 8 | [key: string]: QueryResolver 9 | } = { 10 | me: { 11 | type: 'Object', 12 | outputObject: { 13 | name: 'MeOutput', 14 | fields: { 15 | user: { 16 | type: 'Pointer', 17 | class: 'User', 18 | }, 19 | }, 20 | }, 21 | resolve: meResolver, 22 | }, 23 | } 24 | 25 | export const defaultMutations: { 26 | [key: string]: MutationResolver 27 | } = { 28 | resetPassword: { 29 | type: 'Boolean', 30 | description: 'Mutation to reset the password of the user', 31 | args: { 32 | input: { 33 | password: { 34 | type: 'String', 35 | required: true, 36 | }, 37 | email: { 38 | type: 'Email', 39 | }, 40 | phone: { 41 | type: 'String', 42 | }, 43 | otp: { 44 | type: 'String', 45 | required: true, 46 | }, 47 | }, 48 | }, 49 | resolve: resetPasswordResolver, 50 | }, 51 | sendOtpCode: { 52 | type: 'Boolean', 53 | description: 'Send an OTP code by email to the user', 54 | args: { 55 | input: { 56 | email: { 57 | type: 'Email', 58 | required: true, 59 | }, 60 | }, 61 | }, 62 | resolve: sendOtpCodeResolver, 63 | }, 64 | sendEmail: { 65 | type: 'String', 66 | description: 67 | 'Send basic email with text and html, returns the id of the email', 68 | args: { 69 | input: { 70 | from: { 71 | type: 'String', 72 | required: true, 73 | }, 74 | to: { 75 | type: 'Array', 76 | typeValue: 'String', 77 | required: true, 78 | requiredValue: true, 79 | }, 80 | subject: { 81 | type: 'String', 82 | required: true, 83 | }, 84 | text: { 85 | type: 'String', 86 | }, 87 | html: { 88 | type: 'String', 89 | }, 90 | }, 91 | }, 92 | resolve: sendEmailResolver, 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Schema' 2 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/meResolver.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeAll, afterAll, it, expect } from 'bun:test' 2 | import type { Wabe } from '../../server' 3 | import { getAdminUserClient, type DevWabeTypes } from '../../utils/helper' 4 | import { setupTests, closeTests } from '../../utils/testHelper' 5 | import { gql } from 'graphql-request' 6 | 7 | describe('me', () => { 8 | let wabe: Wabe 9 | 10 | beforeAll(async () => { 11 | const setup = await setupTests() 12 | wabe = setup.wabe 13 | }) 14 | 15 | afterAll(async () => { 16 | await closeTests(wabe) 17 | }) 18 | 19 | it('should return information about current user', async () => { 20 | const adminClient = await getAdminUserClient(wabe.config.port, wabe, { 21 | email: 'admin@wabe.dev', 22 | password: 'admin', 23 | }) 24 | 25 | const { 26 | me: { user }, 27 | } = await adminClient.request(graphql.me) 28 | 29 | expect(user.role.name).toBe('Admin') 30 | 31 | expect(user.authentication.emailPassword.email).toBe('admin@wabe.dev') 32 | }) 33 | }) 34 | 35 | const graphql = { 36 | signUpWith: gql` 37 | mutation signUpWith($input: SignUpWithInput!) { 38 | signUpWith(input: $input){ 39 | id 40 | accessToken 41 | refreshToken 42 | } 43 | } 44 | `, 45 | me: gql` 46 | query me { 47 | me { 48 | user{ 49 | id 50 | authentication{ 51 | emailPassword{ 52 | email 53 | } 54 | } 55 | role { 56 | name 57 | } 58 | } 59 | } 60 | } 61 | `, 62 | } 63 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/meResolver.ts: -------------------------------------------------------------------------------- 1 | import type { WabeContext } from '../../server/interface' 2 | import type { DevWabeTypes } from '../../utils/helper' 3 | 4 | export const meResolver = ( 5 | _: any, 6 | __: any, 7 | context: WabeContext, 8 | ) => { 9 | if (!context.user?.id) return { user: undefined } 10 | 11 | return { 12 | user: context.user, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/newFile.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palixir/wabe/941659c4244b9465334f662e72c71631e0a16880/packages/wabe/src/schema/resolvers/newFile.ts -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/resetPassword.ts: -------------------------------------------------------------------------------- 1 | import type { MutationResetPasswordArgs } from '../../../generated/wabe' 2 | import { OTP } from '../../authentication/OTP' 3 | import type { WabeContext } from '../../server/interface' 4 | import { contextWithRoot } from '../../utils/export' 5 | import type { DevWabeTypes } from '../../utils/helper' 6 | 7 | export const resetPasswordResolver = async ( 8 | _: any, 9 | { input: { email, phone, password, otp } }: MutationResetPasswordArgs, 10 | context: WabeContext, 11 | ) => { 12 | if (!email && !phone) throw new Error('Email or phone is required') 13 | 14 | const users = await context.wabe.controllers.database.getObjects({ 15 | className: 'User', 16 | where: { 17 | ...(email && { email: { equalTo: email } }), 18 | ...(phone && { 19 | authentication: { phonePassword: { phone: { equalTo: phone } } }, 20 | }), 21 | }, 22 | select: { id: true, authentication: true }, 23 | first: 1, 24 | context: contextWithRoot(context), 25 | }) 26 | 27 | // We return true if the user doesn't exist to avoid leaking that the user exists or not 28 | if (users.length === 0) return true 29 | 30 | const user = users[0] 31 | 32 | if (!user) return true 33 | 34 | const otpClass = new OTP(context.wabe.config.rootKey) 35 | 36 | const isOtpValid = otpClass.verify(otp, user.id) 37 | 38 | if (process.env.NODE_ENV === 'production' && !isOtpValid) 39 | throw new Error('Invalid OTP code') 40 | 41 | if (process.env.NODE_ENV !== 'production' && otp !== '000000' && !isOtpValid) 42 | throw new Error('Invalid OTP code') 43 | 44 | const nameOfProvider = phone ? 'phonePassword' : 'emailPassword' 45 | 46 | await context.wabe.controllers.database.updateObject({ 47 | className: 'User', 48 | id: user.id, 49 | data: { 50 | authentication: { 51 | [nameOfProvider]: { 52 | ...(phone && { phone: user.authentication?.phonePassword?.phone }), 53 | ...(email && { email }), 54 | // The password is already hashed in the hook 55 | password, 56 | }, 57 | }, 58 | }, 59 | select: {}, 60 | context: contextWithRoot(context), 61 | }) 62 | 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import type { MutationSendEmailArgs } from '../../../generated/wabe' 2 | import type { WabeContext } from '../../server/interface' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | 5 | export const sendEmailResolver = ( 6 | _: any, 7 | { input }: MutationSendEmailArgs, 8 | context: WabeContext, 9 | ) => { 10 | if (!context.user && !context.isRoot) throw new Error('Permission denied') 11 | 12 | const emailController = context.wabe.controllers.email 13 | 14 | if (!emailController) throw new Error('Email adapter not defined') 15 | 16 | return emailController.send({ 17 | ...input, 18 | text: input.text ?? undefined, 19 | html: input.html ?? undefined, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/wabe/src/schema/resolvers/sendOtpCode.ts: -------------------------------------------------------------------------------- 1 | import type { MutationSendOtpCodeArgs } from '../../../generated/wabe' 2 | import type { WabeContext } from '../../server/interface' 3 | import type { DevWabeTypes } from '../../utils/helper' 4 | import { sendOtpCodeTemplate } from '../../email/templates/sendOtpCode' 5 | import { OTP } from '../../authentication/OTP' 6 | import { contextWithRoot } from '../../utils/export' 7 | 8 | export const sendOtpCodeResolver = async ( 9 | _: any, 10 | { input }: MutationSendOtpCodeArgs, 11 | context: WabeContext, 12 | ) => { 13 | const emailController = context.wabe.controllers.email 14 | 15 | if (!emailController) throw new Error('Email adapter not defined') 16 | 17 | const user = await context.wabe.controllers.database.getObjects({ 18 | className: 'User', 19 | where: { 20 | email: { 21 | equalTo: input.email, 22 | }, 23 | }, 24 | select: { id: true }, 25 | first: 1, 26 | context: contextWithRoot(context), 27 | }) 28 | 29 | // We return true if the user doesn't exist to avoid leaking that the user exists or not 30 | if (user.length === 0) return true 31 | 32 | const userId = user[0]?.id 33 | 34 | if (!userId) return false 35 | 36 | const otpClass = new OTP(context.wabe.config.rootKey) 37 | 38 | const otp = otpClass.generate(userId) 39 | 40 | const mainEmail = context.wabe.config.email?.mainEmail || 'noreply@wabe.com' 41 | 42 | const template = context.wabe.config.email?.htmlTemplates?.sendOTPCode 43 | 44 | await emailController.send({ 45 | from: mainEmail, 46 | to: [input.email], 47 | subject: template?.subject || 'Your OTP code', 48 | html: template?.fn ? await template.fn({ otp }) : sendOtpCodeTemplate(otp), 49 | }) 50 | 51 | return true 52 | } 53 | -------------------------------------------------------------------------------- /packages/wabe/src/server/defaultHandlers.ts: -------------------------------------------------------------------------------- 1 | import type { Wabe, WobeCustomContext } from '.' 2 | import { Session } from '../authentication/Session' 3 | import { getCookieInRequestHeaders } from '../utils' 4 | import type { DevWabeTypes } from '../utils/helper' 5 | 6 | export const defaultSessionHandler = 7 | (wabe: Wabe) => 8 | async (ctx: WobeCustomContext) => { 9 | const headers = ctx.request.headers 10 | 11 | if (headers.get('Wabe-Root-Key') === wabe.config.rootKey) { 12 | ctx.wabe = { 13 | isRoot: true, 14 | wabe, 15 | response: ctx.res, 16 | } 17 | return 18 | } 19 | 20 | const getAccessToken = () => { 21 | if (headers.get('Wabe-Access-Token')) 22 | return { accessToken: headers.get('Wabe-Access-Token') } 23 | 24 | const isCookieSession = 25 | !!wabe.config.authentication?.session?.cookieSession 26 | 27 | if (isCookieSession) 28 | return { 29 | accessToken: getCookieInRequestHeaders( 30 | 'accessToken', 31 | ctx.request.headers, 32 | ), 33 | } 34 | 35 | return { accessToken: null } 36 | } 37 | 38 | const { accessToken } = getAccessToken() 39 | 40 | if (!accessToken) { 41 | ctx.wabe = { 42 | isRoot: false, 43 | wabe, 44 | response: ctx.res, 45 | } 46 | return 47 | } 48 | 49 | const session = new Session() 50 | 51 | const { 52 | user, 53 | sessionId, 54 | accessToken: newAccessToken, 55 | refreshToken: newRefreshToken, 56 | } = await session.meFromAccessToken(accessToken, { 57 | wabe, 58 | isRoot: true, 59 | }) 60 | 61 | ctx.wabe = { 62 | isRoot: false, 63 | sessionId, 64 | user, 65 | wabe, 66 | response: ctx.res, 67 | } 68 | 69 | if ( 70 | wabe.config.authentication?.session?.cookieSession && 71 | newAccessToken && 72 | newRefreshToken && 73 | newAccessToken !== accessToken 74 | ) { 75 | ctx.res.setCookie('accessToken', newAccessToken, { 76 | httpOnly: true, 77 | path: '/', 78 | expires: session.getAccessTokenExpireAt(wabe.config), 79 | sameSite: 'None', 80 | secure: true, 81 | }) 82 | 83 | ctx.res.setCookie('refreshToken', newRefreshToken, { 84 | httpOnly: true, 85 | path: '/', 86 | expires: session.getAccessTokenExpireAt(wabe.config), 87 | sameSite: 'None', 88 | secure: true, 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/wabe/src/server/interface.ts: -------------------------------------------------------------------------------- 1 | import type { WobeResponse } from 'wobe' 2 | import type { Wabe, WabeTypes } from '.' 3 | 4 | export interface WabeContext { 5 | response?: WobeResponse 6 | user?: T['types']['User'] | null 7 | sessionId?: string | null 8 | isRoot: boolean 9 | wabe: Wabe 10 | } 11 | -------------------------------------------------------------------------------- /packages/wabe/src/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { type WobeHandler, uploadDirectory } from 'wobe' 2 | import type { ProviderEnum } from '../../authentication/interface' 3 | import { authHandler, oauthHandlerCallback } from './authHandler' 4 | import type { WobeCustomContext } from '..' 5 | 6 | export interface WabeRoute { 7 | method: 'GET' | 'POST' | 'PUT' | 'DELETE' 8 | path: string 9 | handler: WobeHandler> 10 | } 11 | 12 | export const defaultRoutes = (devDirectory: string): WabeRoute[] => { 13 | const routes: WabeRoute[] = [ 14 | { 15 | method: 'GET', 16 | path: '/auth/oauth', 17 | handler: (context) => { 18 | const provider = context.query.provider 19 | 20 | if (!provider) 21 | throw new Error('Authentication failed, provider not found') 22 | 23 | // TODO: Maybe check if the value is in the enum 24 | return authHandler(context, context.wabe, provider as ProviderEnum) 25 | }, 26 | }, 27 | { 28 | method: 'GET', 29 | path: '/auth/oauth/callback', 30 | handler: (context) => oauthHandlerCallback(context, context.wabe), 31 | }, 32 | { 33 | method: 'GET', 34 | path: '/bucket/:filename', 35 | handler: uploadDirectory({ directory: devDirectory }), 36 | }, 37 | ] 38 | 39 | return routes 40 | } 41 | -------------------------------------------------------------------------------- /packages/wabe/src/utils/export.ts: -------------------------------------------------------------------------------- 1 | import type { WabeContext } from '../server/interface' 2 | 3 | export const contextWithRoot = ( 4 | context: WabeContext, 5 | ): WabeContext => ({ 6 | ...context, 7 | isRoot: true, 8 | }) 9 | 10 | export const notEmpty = (value: T | null | undefined): value is T => 11 | value !== null && value !== undefined 12 | -------------------------------------------------------------------------------- /packages/wabe/src/utils/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test' 2 | import { firstLetterInLowerCase } from '.' 3 | 4 | describe('utils', () => { 5 | it('should put the first letter in lowercase', () => { 6 | expect(firstLetterInLowerCase('Hello')).toEqual('hello') 7 | expect(firstLetterInLowerCase('User')).toEqual('user') 8 | expect(firstLetterInLowerCase('USer')).toEqual('uSer') 9 | expect(firstLetterInLowerCase('99 User')).toEqual('99 user') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/wabe/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClassInterface } from '../schema' 2 | import type { WabeTypes, WabeConfig } from '../server' 3 | 4 | export const firstLetterInUpperCase = (str: string) => { 5 | const indexOfFirstLetter = str.search(/[a-z]/i) 6 | 7 | return ( 8 | str.slice(0, indexOfFirstLetter) + 9 | str[indexOfFirstLetter].toUpperCase() + 10 | str.slice(indexOfFirstLetter + 1) 11 | ) 12 | } 13 | 14 | export const firstLetterInLowerCase = (str: string) => { 15 | const indexOfFirstLetter = str.search(/[a-z]/i) 16 | 17 | return ( 18 | str.slice(0, indexOfFirstLetter) + 19 | str[indexOfFirstLetter].toLowerCase() + 20 | str.slice(indexOfFirstLetter + 1) 21 | ) 22 | } 23 | 24 | export const getClassFromClassName = ( 25 | className: string, 26 | config: WabeConfig, 27 | ): ClassInterface => { 28 | const classInSchema = config.schema?.classes?.find( 29 | (schemaClass) => schemaClass.name === className, 30 | ) 31 | 32 | if (!classInSchema) throw new Error('Class not found in schema') 33 | 34 | return classInSchema 35 | } 36 | 37 | // TODO: Put this in wobe 38 | export const getCookieInRequestHeaders = ( 39 | cookieName: string, 40 | headers: Headers, 41 | ) => { 42 | const cookies = headers.get('Cookie') 43 | 44 | if (!cookies) return 45 | 46 | const cookie = cookies.split(';').find((c) => c.includes(cookieName)) 47 | 48 | if (!cookie) return 49 | 50 | return cookie.split('=')[1] 51 | } 52 | 53 | /** 54 | * This apply the following transformations on string: 55 | * - lowercase 56 | * - normalize with NFD 57 | * - remove diacritics and accents characters 58 | * - replace matching abbreviation with long version (if disableAbbrevations is not set) 59 | * - replace 2 or more spaces by one 60 | * - replace all non alpha characters by a space 61 | * - trim 62 | */ 63 | export const tokenize = (value: string) => { 64 | const tmpValue = value 65 | .toLowerCase() 66 | .normalize('NFD') 67 | // biome-ignore lint/suspicious/noMisleadingCharacterClass: 68 | .replace(/[\u0300-\u036f]/g, '') 69 | .replace(/\s\s+/g, ' ') 70 | 71 | return ( 72 | tmpValue 73 | // Replace all non alpha 74 | .replace(/[\W_]+/g, ' ') 75 | .trim() 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /packages/wabe/src/utils/preload.ts: -------------------------------------------------------------------------------- 1 | import { runDatabase as runPostgresDatabase } from 'wabe-postgres-launcher' 2 | 3 | const setupEnvironment = () => { 4 | process.env.TEST = 'true' 5 | } 6 | 7 | await runPostgresDatabase() 8 | setupEnvironment() 9 | -------------------------------------------------------------------------------- /packages/wabe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | "moduleResolution": "bundler", 11 | "verbatimModuleSyntax": false, 12 | "outDir":"dist", 13 | "noEmit": true, 14 | 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "noFallthroughCasesInSwitch": true, 18 | 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noPropertyAccessFromIndexSignature": false 22 | }, 23 | "exclude": ["node_modules", "dist", "generated", "dev"], 24 | "include": ["src/**/*.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "types": ["bun-types"], 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "declaration":true 10 | }, 11 | } 12 | --------------------------------------------------------------------------------