├── .github ├── CODEOWNERS ├── FUNDING.yml ├── release.yml └── workflows │ ├── deploy-npm.yml │ └── deploy-website.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── admin ├── .env.development ├── .eslintrc ├── .gitignore ├── index.html ├── package.json ├── postcss.config.js ├── public │ └── zed-mono │ │ ├── zed-mono-bold.woff2 │ │ ├── zed-mono-italic.woff2 │ │ ├── zed-mono-medium.woff2 │ │ └── zed-mono-regular.woff2 ├── src │ ├── app.tsx │ ├── client │ │ ├── README.md │ │ ├── app.ts │ │ └── collection.ts │ ├── components │ │ ├── button.tsx │ │ ├── chart.tsx │ │ ├── chip.tsx │ │ ├── chips-input.tsx │ │ ├── cm-dark.ts │ │ ├── code-editor.tsx │ │ ├── collection-form.tsx │ │ ├── collections-empty-state.tsx │ │ ├── copy.tsx │ │ ├── date.tsx │ │ ├── dev-dialog.tsx │ │ ├── field.tsx │ │ ├── filter-input.tsx │ │ ├── flow-handle.tsx │ │ ├── general-error.tsx │ │ ├── hook-node.tsx │ │ ├── hooks-search.tsx │ │ ├── id-tag.tsx │ │ ├── input.tsx │ │ ├── nav-links.tsx │ │ ├── popover.tsx │ │ ├── schema-fields.tsx │ │ ├── select.tsx │ │ ├── service-node.tsx │ │ ├── snacks.tsx │ │ └── value.tsx │ ├── data │ │ ├── app-developers.ts │ │ ├── collections.ts │ │ ├── hooks-registry.ts │ │ ├── logs.ts │ │ └── schema-refs.ts │ ├── extras.css │ ├── index.css │ ├── layouts │ │ ├── AdminLayout.tsx │ │ └── NavContentLayout.tsx │ ├── lib │ │ ├── app-error.ts │ │ ├── append-index-fields.ts │ │ ├── append-schema-fields.ts │ │ ├── avatar-colors.ts │ │ ├── clean-date.ts │ │ ├── definition-from-field.ts │ │ ├── field-types.ts │ │ ├── get-new-field-name.ts │ │ ├── get-schema.tsx │ │ ├── indexed.ts │ │ ├── node-types.ts │ │ ├── random-str.ts │ │ ├── remove-fields-item.ts │ │ ├── request-status.ts │ │ ├── schema-from-fields.ts │ │ ├── slugify.ts │ │ ├── snacks.ts │ │ └── use-color-scheme.ts │ ├── main.tsx │ ├── mangobase-app.ts │ ├── pages │ │ ├── collections │ │ │ ├── [name] │ │ │ │ ├── _layout.tsx │ │ │ │ ├── edit.tsx │ │ │ │ ├── hooks.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── login.tsx │ │ ├── logs.tsx │ │ ├── notfound.tsx │ │ └── settings │ │ │ ├── devs.tsx │ │ │ ├── index.tsx │ │ │ ├── profile.tsx │ │ │ └── schemas │ │ │ ├── [name].tsx │ │ │ └── index.tsx │ ├── routes.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── assets ├── ss-dark.png └── ss-light.png ├── base ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.mjs ├── package.json ├── src │ ├── app.spec.ts │ ├── app.ts │ ├── authentication.spec.ts │ ├── authentication.ts │ ├── collection-service.ts │ ├── collection.test.ts │ ├── collection.ts │ ├── context.ts │ ├── database.ts │ ├── db-migrations.ts │ ├── errors.ts │ ├── hook.ts │ ├── hooks-registry.ts │ ├── hooks.ts │ ├── index.ts │ ├── lib │ │ ├── __snapshots__ │ │ │ └── export-schema.test.ts.snap │ │ ├── api-paths.ts │ │ ├── clone.ts │ │ ├── export-schema.test.ts │ │ ├── export-schema.ts │ │ ├── get-collection.ts │ │ ├── get-ref-usage.test.ts │ │ ├── get-ref-usage.ts │ │ ├── index.ts │ │ ├── method-from-http.ts │ │ ├── random-str.ts │ │ ├── set-with-path.test.ts │ │ └── set-with-path.ts │ ├── logger.ts │ ├── manifest.ts │ ├── method.ts │ ├── schema.test.ts │ ├── schema.ts │ └── users.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts ├── bun-server ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.js ├── package.json ├── src │ └── index.ts ├── tsconfig.build.json └── tsconfig.json ├── create-mango ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── package-lock.json ├── package.json ├── readme.md ├── source │ ├── app.tsx │ ├── cli.tsx │ └── create-project.ts ├── templates │ ├── _common │ │ ├── .env │ │ ├── Dockerfile │ │ ├── _gitattributes │ │ ├── _gitignore │ │ ├── _prettierignore │ │ └── readme.md │ ├── js │ │ ├── _package.json │ │ └── src │ │ │ └── index.mjs │ └── ts │ │ ├── _package.json │ │ ├── build.js │ │ ├── src │ │ └── index.ts │ │ └── tsconfig.json ├── tsconfig.json └── yarn.lock ├── eslint-config-base ├── index.js ├── package.json └── yarn.lock ├── examples ├── base-bun-mongo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── base-express-mongo │ ├── .gitignore │ ├── index.ts │ └── package.json ├── express-server ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.mjs ├── package.json ├── src │ └── index.ts ├── tsconfig.build.json └── tsconfig.json ├── lerna.json ├── mongo-db ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.mjs ├── global.setup.ts ├── package.json ├── src │ ├── index.ts │ ├── mongodb.test.ts │ └── mongodb.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts ├── package.json ├── turbo.json ├── website ├── .gitignore ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.js │ │ ├── mango-spline.vue │ │ └── styles.css ├── api │ └── index.md ├── generate-api.mjs ├── guide │ ├── authentication.md │ ├── context.md │ ├── dashboard.md │ ├── database-adapters.md │ ├── deployment.md │ ├── faqs.md │ ├── getting-started.md │ ├── hooks.md │ ├── index.md │ ├── plugins.md │ ├── query.md │ ├── rest.md │ └── server-adapters.md ├── index.md └── package.json └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @blackmann 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [blackmann] 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: Breaking Changes 🛠 9 | labels: 10 | - Semver-Major 11 | - breaking-change 12 | - title: Exciting New Features 🎉 13 | labels: 14 | - Semver-Minor 15 | - enhancement 16 | - title: Other Changes 17 | labels: 18 | - "*" 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy-npm.yml: -------------------------------------------------------------------------------- 1 | name: Publish packages to npm 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: choice 7 | description: 'Version to publish' 8 | required: true 9 | default: patch 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: 18 29 | registry-url: 'https://registry.npmjs.org' 30 | - run: git config --global user.email "mail@degreat.co.uk" 31 | - run: git config --global user.name "De-Great Yartey" 32 | - name: Install dependencies 33 | run: yarn boot 34 | - name: Turbo+Build 35 | run: yarn build 36 | - name: Test 37 | run: yarn test 38 | - name: Bump version 39 | run: npx lerna version ${{ github.event.inputs.version }} --conventional-commits --yes --no-private 40 | - name: Publish 41 | run: npx lerna publish from-package --yes --no-private 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Deploy VitePress site to Pages 4 | 5 | on: 6 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 7 | # using the `master` branch as the default branch. 8 | push: 9 | branches: [master] 10 | paths: 11 | - website/** 12 | - base/** 13 | - mongodb/** 14 | - express 15 | - bun-server 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 21 | permissions: 22 | contents: read 23 | pages: write 24 | id-token: write 25 | 26 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 27 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 28 | concurrency: 29 | group: pages 30 | cancel-in-progress: false 31 | 32 | jobs: 33 | # Build job 34 | build: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 41 | # - uses: pnpm/action-setup@v2 # Uncomment this if you're using pnpm 42 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun 43 | - name: Setup Node 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: 18 47 | cache: npm # or pnpm / yarn 48 | - name: Setup Pages 49 | uses: actions/configure-pages@v3 50 | - name: Install dependencies 51 | run: yarn --frozen-lockfile 52 | - name: Build with VitePress 53 | run: | 54 | yarn build 55 | touch website/.vitepress/dist/.nojekyll 56 | - name: Upload artifact 57 | uses: actions/upload-pages-artifact@v2 58 | with: 59 | path: website/.vitepress/dist 60 | 61 | # Deployment job 62 | deploy: 63 | environment: 64 | name: github-pages 65 | url: ${{ steps.deployment.outputs.page_url }} 66 | needs: build 67 | runs-on: ubuntu-latest 68 | name: Deploy 69 | steps: 70 | - name: Deploy to GitHub Pages 71 | id: deployment 72 | uses: actions/deploy-pages@v2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | 3 | node_modules/ 4 | coverage/ 5 | .turbo 6 | .mangobase 7 | 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "runtimeExecutable": "yarn", 10 | "runtimeArgs": ["dev:admin"], 11 | "name": "dev:admin", 12 | "request": "launch", 13 | }, 14 | { 15 | "type": "node", 16 | "runtimeExecutable": "yarn", 17 | "runtimeArgs": ["dev:express-mongo"], 18 | "name": "Example - express/mongo", 19 | "request": "launch" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [{ "mode": "auto" }] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | > 💽 You should have [nvm](https://github.com/nvm-sh/nvm) installed; then run `nvm use`. If you can't use nvm, you should use Node 18 for development for consistency sake. 4 | 5 | This is a monorepo. Run `yarn boot` to install all dependencies. 6 | 7 | ### Design summary 8 | 9 | The `base` package contains the core of the project. In there, you'll find code related to dealing with collections, plugins, handling requests (aka contexts), etc. 10 | 11 | You'll need to interface with the base using a server. So there are two implementations: `@mangobase/express` and `@mangobase/bun`. You'll also need to provide a database but we have an implementation: `@mangobase/mongodb`. 12 | 13 | ### Contributions 14 | 15 | It's recommended to create a separate branch for your contributions. First, fork this project then make your changes. Submit a PR when you think its ready to be merged. 16 | 17 | See: https://docs.github.com/en/get-started/quickstart/contributing-to-projects 18 | 19 | #### base 20 | 21 | When contributing to the `base`, you many need to test your implementation using unit tests/specs. 22 | 23 | To actually validate your code, run `yarn build:base && yarn copy-admin`. You can then use one of the example projects to test your code. Do `yarn dev:express-mongo` or `bun dev:bun-mongo` 24 | 25 | > You should have [bun](https://bun.sh) installed to try the bun example. 26 | 27 | You can then use an HTTP client to test endpoints. 28 | 29 | Another way to validate your addition is to use the [admin/dev](#admin) dashboard. 30 | 31 | #### admin 32 | 33 | The [dev] admin panel is where the low-code experience happens. It's developed with Vite/Preact. 34 | 35 | Run it with `yarn dev:admin`. You need to have a server running. Run one with `yarn dev:express-mongo` 36 | 37 | Whenever you make changes to `base`, you'll need to restart `express-mongo` for backend changes to reflect. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 De-Great Yartey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mangobase 2 | 3 | [![npm](https://img.shields.io/npm/dm/mangobase)](https://www.npmjs.com/package/mangobase) 4 | [![npm](https://img.shields.io/npm/v/mangobase)](https://www.npmjs.com/package/mangobase) 5 | [![npm](https://img.shields.io/npm/l/mangobase)](https://www.npmjs.com/package/mangobase) 6 | 7 | Low-code Javascript backend framework for Node and Bun runtimes. Docs [here](https://degreat.co.uk/mangobase). 8 | 9 | ```sh 10 | npm create mango@latest 11 | ``` 12 | 13 |

14 | 15 | 16 | 17 | Mangobase dashboard 18 | 19 |

20 | 21 | See [Contributing](CONTRIBUTING.md) for development guide on how contribute. -------------------------------------------------------------------------------- /admin/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_URL=http://localhost:3000/api -------------------------------------------------------------------------------- /admin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["base"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Mangobase 10 | 11 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mangobase/admin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "clean": "rm -rf dist", 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@codemirror/autocomplete": "^6.11.1", 14 | "@codemirror/commands": "^6.3.3", 15 | "@codemirror/lang-json": "^6.0.1", 16 | "@codemirror/language": "^6.10.0", 17 | "@codemirror/state": "^6.4.0", 18 | "@codemirror/view": "^6.23.0", 19 | "@lezer/highlight": "^1.2.0", 20 | "@monaco-editor/react": "4.5.2", 21 | "@preact/signals": "^1.1.3", 22 | "@radix-ui/react-popover": "^1.0.7", 23 | "autoprefixer": "^10.4.14", 24 | "boring-avatars": "^1.10.1", 25 | "chart.js": "^4.4.0", 26 | "clsx": "^1.2.1", 27 | "dayjs": "^1.11.9", 28 | "mangobase": "*", 29 | "preact": "^10.13.2", 30 | "qs": "^6.11.2", 31 | "react-hook-form": "^7.44.3", 32 | "react-router-dom": "^6.13.0", 33 | "reactflow": "^11.7.4", 34 | "redaxios": "^0.5.1" 35 | }, 36 | "devDependencies": { 37 | "@preact/preset-vite": "^2.5.0", 38 | "@tailwindcss/forms": "^0.5.7", 39 | "@testing-library/preact": "^3.2.3", 40 | "@types/jsdom": "^21.1.5", 41 | "@types/react": "npm:@preact/compat@*", 42 | "@types/react-dom": "npm:@preact/compat@*", 43 | "eslint-config-base": "*", 44 | "eslint-config-preact": "^1.3.0", 45 | "jsdom": "^22.1.0", 46 | "monaco-editor": "^0.41.0", 47 | "postcss": "^8.4.28", 48 | "tailwindcss": "^3.4.0", 49 | "typescript": "^5.0.2", 50 | "vite": "^4.3.9", 51 | "vitest": "^0.34.6", 52 | "vitest-dom": "^0.1.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /admin/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {}, 4 | tailwindcss: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /admin/public/zed-mono/zed-mono-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/admin/public/zed-mono/zed-mono-bold.woff2 -------------------------------------------------------------------------------- /admin/public/zed-mono/zed-mono-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/admin/public/zed-mono/zed-mono-italic.woff2 -------------------------------------------------------------------------------- /admin/public/zed-mono/zed-mono-medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/admin/public/zed-mono/zed-mono-medium.woff2 -------------------------------------------------------------------------------- /admin/public/zed-mono/zed-mono-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/admin/public/zed-mono/zed-mono-regular.woff2 -------------------------------------------------------------------------------- /admin/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from 'react-router-dom' 2 | import { Snacks } from './components/snacks' 3 | import routes from './routes' 4 | 5 | export function App() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /admin/src/client/README.md: -------------------------------------------------------------------------------- 1 | # @mangobase/client 2 | 3 | This is the client library for mangobase and can be used with any Javascript framework or vanilla. 4 | 5 | Currently being developed inside the `admin` project, but will moved out when it's mature. 6 | -------------------------------------------------------------------------------- /admin/src/client/app.ts: -------------------------------------------------------------------------------- 1 | import Collection, { type CollectionProps } from './collection' 2 | import axios, { RequestHeaders } from 'redaxios' 3 | 4 | type Req = typeof axios 5 | interface Auth { 6 | auth: { token: string; type: 'Bearer' } 7 | user: { 8 | fullname: string 9 | username: string 10 | } 11 | } 12 | 13 | class App { 14 | host: string 15 | req: Req 16 | auth?: Auth 17 | 18 | constructor(host: string) { 19 | this.host = host 20 | this.auth = this.get('auth') 21 | this.req = this.getRequests() 22 | } 23 | 24 | private getRequests() { 25 | const headers: RequestHeaders = {} 26 | 27 | if (this.auth) { 28 | headers[ 29 | 'authorization' 30 | ] = `${this.auth.auth.type} ${this.auth.auth.token}` 31 | } 32 | 33 | return axios.create({ baseURL: this.host, headers }) 34 | } 35 | 36 | async collections(): Promise { 37 | const { data } = await this.req.get('collections?$sort[name]=1') 38 | return data.map((it: CollectionProps) => new Collection(this, it)) 39 | } 40 | 41 | async collection(name: string): Promise { 42 | const { data } = await this.req.get(`collections/${name}`) 43 | return new Collection(this, data) 44 | } 45 | 46 | async addCollection(collection: any) { 47 | const { data } = await this.req.post('collections', collection) 48 | return new Collection(this, data) 49 | } 50 | 51 | async editCollection(name: string, collection: any) { 52 | const { data } = await this.req.patch(`collections/${name}`, collection) 53 | return new Collection(this, data) 54 | } 55 | 56 | async hookRegistry() { 57 | const { data } = await this.req.get('_dev/hooks-registry') 58 | return data 59 | } 60 | 61 | set(key: string, value: any) { 62 | localStorage.setItem(key, JSON.stringify(value)) 63 | 64 | if (key === 'auth') { 65 | this.auth = value 66 | this.req = this.getRequests() 67 | } 68 | } 69 | 70 | get(key: string) { 71 | const value = localStorage.getItem(key) 72 | return value && JSON.parse(value) 73 | } 74 | 75 | remove(key: string) { 76 | localStorage.removeItem(key) 77 | } 78 | } 79 | 80 | export default App 81 | -------------------------------------------------------------------------------- /admin/src/client/collection.ts: -------------------------------------------------------------------------------- 1 | import type { Index, SchemaDefinitions } from 'mangobase' 2 | import type App from './app' 3 | import { ReactFlowJsonObject } from 'reactflow' 4 | import qs from 'qs' 5 | 6 | type Editor = ReactFlowJsonObject 7 | 8 | interface CollectionProps { 9 | name: string 10 | schema: SchemaDefinitions 11 | exposed: boolean 12 | readOnlySchema?: boolean 13 | template: boolean 14 | indexes: Index[] 15 | } 16 | 17 | class Collection { 18 | app: App 19 | name: string 20 | schema: SchemaDefinitions 21 | exposed: boolean 22 | template: boolean 23 | readOnlySchema?: boolean 24 | indexes: Index[] 25 | 26 | constructor(app: App, data: CollectionProps) { 27 | this.app = app 28 | 29 | this.name = data.name 30 | this.schema = data.schema 31 | this.exposed = data.exposed 32 | this.template = data.template 33 | this.readOnlySchema = data.readOnlySchema 34 | this.indexes = data.indexes 35 | } 36 | 37 | private get base() { 38 | if (this.exposed) { 39 | return this.name 40 | } 41 | 42 | return `_x/${this.name}` 43 | } 44 | 45 | async find(query: Record = {}) { 46 | const endpoint = this.getEndpoint(`/${this.base}`, query) 47 | 48 | const { data } = await this.app.req.get(endpoint) 49 | return data 50 | } 51 | 52 | async delete(id: string) { 53 | const endpoint = `/${this.base}/${id}` 54 | await this.app.req.delete(endpoint) 55 | } 56 | 57 | async hooks(): Promise { 58 | const { data } = await this.app.req.get(`/_dev/hooks/${this.name}`) 59 | return data 60 | } 61 | 62 | async editor() { 63 | const { data } = await this.app.req.get(`/_dev/editors/${this.name}`) 64 | return data as Editor 65 | } 66 | 67 | async setHooks(hooks: HooksConfig) { 68 | await this.app.req.patch(`/_dev/hooks/${this.name}`, hooks) 69 | } 70 | 71 | async setEditor(editor: Editor) { 72 | await this.app.req.patch(`/_dev/editors/${this.name}`, editor) 73 | } 74 | 75 | private getEndpoint(path: string, filter: Record = {}) { 76 | const endpointParts = [path] 77 | const queryString = qs.stringify(filter) 78 | 79 | if (queryString) { 80 | endpointParts.push(queryString) 81 | } 82 | 83 | return endpointParts.join('?') 84 | } 85 | } 86 | 87 | const HOOK_STAGES = ['before', 'after'] as const 88 | const METHODS = ['find', 'get', 'create', 'patch', 'remove'] as const 89 | 90 | type Stage = `${(typeof HOOK_STAGES)[number]}` 91 | type Method = `${(typeof METHODS)[number]}` 92 | type HookId = string 93 | type HookOptions = Record 94 | type Hook = [HookId, HookOptions?] 95 | type HooksConfig = Record>> 96 | 97 | export default Collection 98 | export { HOOK_STAGES, METHODS } 99 | export type { CollectionProps, Stage, Method, HookOptions, Hook, HooksConfig } 100 | -------------------------------------------------------------------------------- /admin/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'preact/compat' 2 | import clsx from 'clsx' 3 | 4 | type Variant = 'primary' | 'secondary' | 'muted' 5 | 6 | interface Props extends React.ComponentProps<'button'> { 7 | variant?: Variant 8 | } 9 | 10 | const styles: Record = { 11 | muted: 'bg-zinc-300 dark:bg-neutral-500 dark:text-neutral-100', 12 | primary: 13 | 'bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-75 disabled:hover:bg-blue-600', 14 | secondary: 'bg-green-600 text-white hover:bg-green-700', 15 | } 16 | 17 | function Button({ className, variant = 'muted', ...props }: Props) { 18 | const style = styles[variant] 19 | return ( 20 | 122 | 123 | 124 | ) 125 | } 126 | 127 | function parseQuery(query: string) { 128 | if (!/^\{.*:.*\}$/.test(query)) { 129 | return null 130 | } 131 | const evalQuery = new Function(`return ${query}`)() as Record 132 | 133 | return JSON.parse(JSON.stringify(evalQuery)) 134 | } 135 | 136 | export default FilterInput 137 | -------------------------------------------------------------------------------- /admin/src/components/flow-handle.tsx: -------------------------------------------------------------------------------- 1 | import { Handle, HandleProps, Position } from 'reactflow' 2 | import clsx from 'clsx' 3 | 4 | interface Props extends HandleProps { 5 | className?: string 6 | } 7 | 8 | function FlowHandle({ className, ...props }: Props) { 9 | return ( 10 | 21 |
22 | 23 | ) 24 | } 25 | 26 | export default FlowHandle 27 | -------------------------------------------------------------------------------- /admin/src/components/general-error.tsx: -------------------------------------------------------------------------------- 1 | import AppError from '@/lib/app-error' 2 | import { Link } from 'react-router-dom' 3 | import { useRouteError } from 'react-router-dom' 4 | 5 | interface Props { 6 | error: AppError 7 | } 8 | 9 | function GeneralErrorBoundary({ error }: Props) { 10 | console.error(error) 11 | 12 | return ( 13 |
14 |
15 | {error.data?.status ? ( 16 | 17 | ) : ( 18 | 19 | )} 20 | 21 |
🥭
22 |
23 |
24 | ) 25 | } 26 | 27 | function ResponseError({ error }: Props) { 28 | return ( 29 | <> 30 |
31 | 32 | release_alert 33 | 34 |
35 |

An API error occurred

36 |

37 | Status code: {error.data.status} 38 |

39 | 40 | {error.data.detail && ( 41 | <> 42 |

43 | {error.data.detail} 44 |

45 | 46 | )} 47 | 48 | {error.data.status === 401 && ( 49 |
50 |

51 | You may need to login again. If you think this is a mistake, check{' '} 52 | 56 | our issues 57 | {' '} 58 | or raise one.{' '} 59 |

60 | 61 |
62 | 68 | Login 69 | 70 |
71 |
72 | )} 73 | 74 | {error.data.status === 404 && ( 75 |
76 |

The resource you are looking for does not exist.

77 |
78 | )} 79 | 80 | {![401, 404].includes(error.data?.status) && ( 81 |
82 | This error is unexpected. Check the console logs to get an idea of 83 | what could be happening. 84 |
85 | )} 86 | 87 | ) 88 | } 89 | 90 | function UnknownErrorContent() { 91 | const error = useRouteError() as Error 92 | 93 | return ( 94 | <> 95 |
96 | 97 | release_alert 98 | 99 |
100 |

An error occurred

101 |

102 | The app encountered an error it could not handle 103 |

104 | 105 |

Message

106 |

{error.message}

107 | 108 |

Stacktrace

109 |
110 | Check stacktrace from the browser's developer Console. If you think this 111 | is unexpected, please check{' '} 112 | 118 | our issues 119 | {' '} 120 | or raise one. 121 |
122 | 123 | ) 124 | } 125 | 126 | function LoaderErrorBoundary() { 127 | const error = useRouteError() as AppError 128 | 129 | return 130 | } 131 | 132 | export { LoaderErrorBoundary, GeneralErrorBoundary } 133 | -------------------------------------------------------------------------------- /admin/src/components/hook-node.tsx: -------------------------------------------------------------------------------- 1 | import { FieldValues, useForm } from 'react-hook-form' 2 | import { NodeProps, Position, useReactFlow } from 'reactflow' 3 | import React, { memo } from 'preact/compat' 4 | import hooksRegistry, { Hook } from '@/data/hooks-registry' 5 | import Button from './button' 6 | import FlowHandle from './flow-handle' 7 | import SchemaFields from './schema-fields' 8 | import clsx from 'clsx' 9 | 10 | interface Data { 11 | id: string 12 | config?: any 13 | } 14 | 15 | function HookNode({ data, id: nodeId, selected }: NodeProps) { 16 | const reactFlow = useReactFlow() 17 | const hookInfo = hooksRegistry.value.find(({ id }) => data.id === id) 18 | 19 | function handleConfigChange(config: any) { 20 | reactFlow.setNodes((nodes) => 21 | nodes.map((node) => { 22 | if (node.id !== nodeId) { 23 | return node 24 | } 25 | 26 | return { 27 | ...node, 28 | data: { ...node.data, config }, 29 | } 30 | }) 31 | ) 32 | } 33 | 34 | if (!hookInfo) { 35 | return
Loading hook…
36 | } 37 | 38 | return ( 39 |
45 |
46 | 51 | 52 |
53 |
54 | line_start_circle 55 |
56 |
57 |
{hookInfo.name}
58 |

{hookInfo.description}

59 |
60 |
61 | 62 | 67 |
68 | {hookInfo.configSchema && ( 69 | 74 | )} 75 |
76 | ) 77 | } 78 | 79 | interface ConfigFormProps { 80 | hookInfo: Hook 81 | config: any 82 | onSave: (config: any) => void 83 | } 84 | 85 | const ConfigForm = memo(({ config, hookInfo, onSave }: ConfigFormProps) => { 86 | const { control, handleSubmit, register, setValue, watch } = useForm() 87 | 88 | const showSave = 89 | config !== null && JSON.stringify(watch()) !== JSON.stringify(config) 90 | 91 | function saveConfig(form: FieldValues) { 92 | onSave(form) 93 | } 94 | 95 | React.useEffect(() => { 96 | if (!config) { 97 | return 98 | } 99 | 100 | for (const [key] of Object.entries(hookInfo.configSchema)) { 101 | setValue(key, config[key]) 102 | } 103 | }, [config, hookInfo, setValue]) 104 | 105 | return ( 106 |
107 | 112 | 113 | {showSave && ( 114 |
115 | 118 |
119 | )} 120 | 121 | ) 122 | }) 123 | 124 | const HOOK_NODE_TYPE = 'hook-node-type' 125 | 126 | export default memo(HookNode) 127 | export { HOOK_NODE_TYPE } 128 | -------------------------------------------------------------------------------- /admin/src/components/hooks-search.tsx: -------------------------------------------------------------------------------- 1 | import hooksRegistry, { loadHooksRegistry } from '@/data/hooks-registry' 2 | import Input from './input' 3 | import React from 'preact/compat' 4 | import clsx from 'clsx' 5 | 6 | interface Props { 7 | onSelect?: (hookId: string) => void 8 | } 9 | 10 | function HooksSearch({ onSelect }: Props) { 11 | const [searchKey, setSearchKey] = React.useState('') 12 | const [focused, setFocused] = React.useState(false) 13 | 14 | function selectHook(id: string) { 15 | setSearchKey('') 16 | onSelect?.(id) 17 | } 18 | 19 | const results = React.useMemo(() => { 20 | const key = searchKey.trim().toLocaleLowerCase() 21 | if (!key) return hooksRegistry.value 22 | 23 | return hooksRegistry.value.filter( 24 | (hook) => 25 | hook.name.toLowerCase().includes(key) || 26 | hook.description.toLowerCase().includes(key) 27 | ) 28 | }, [searchKey, hooksRegistry.value]) 29 | 30 | const showResults = focused && Boolean(results.length) 31 | 32 | React.useEffect(() => { 33 | loadHooksRegistry() 34 | }, []) 35 | 36 | return ( 37 |
38 | setSearchKey((e.target as HTMLInputElement).value)} 41 | placeholder="Search and add hook" 42 | type="search" 43 | value={searchKey} 44 | onFocus={() => setFocused(true)} 45 | onBlur={() => setFocused(false)} 46 | /> 47 | 48 | {showResults && ( 49 |
50 |
    51 | {results.map((hook) => ( 52 |
  • selectHook(hook.id)} 58 | > 59 |
    {hook.name}
    60 |

    61 | {hook.description} 62 |

    63 |
  • 64 | ))} 65 |
66 |
67 | )} 68 |
69 | ) 70 | } 71 | 72 | export default HooksSearch 73 | -------------------------------------------------------------------------------- /admin/src/components/id-tag.tsx: -------------------------------------------------------------------------------- 1 | import Chip from './chip' 2 | import Copy from './copy' 3 | 4 | interface Props { 5 | id: string 6 | } 7 | 8 | function IdTag({ id }: Props) { 9 | if (!id) return null 10 | return ( 11 | 12 | {id.substring(14)} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default IdTag 19 | -------------------------------------------------------------------------------- /admin/src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'preact/compat' 2 | import clsx from 'clsx' 3 | 4 | const Input = React.forwardRef>( 5 | ({ className, ...props }, ref) => { 6 | return ( 7 | 15 | ) 16 | } 17 | ) 18 | 19 | export default Input 20 | -------------------------------------------------------------------------------- /admin/src/components/nav-links.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom' 2 | import React from 'preact/compat' 3 | import clsx from 'clsx' 4 | 5 | type LinkProp = { 6 | leading?: React.ReactNode 7 | path: string 8 | title: React.ReactNode 9 | } 10 | 11 | interface Props { 12 | links: (LinkProp & { 13 | // no nesting above 2 levels 14 | children?: LinkProp[] 15 | })[] 16 | } 17 | 18 | function L({ 19 | expanded, 20 | hasChildren, 21 | link, 22 | onClick, 23 | }: { 24 | expanded?: boolean 25 | hasChildren?: boolean 26 | link: LinkProp 27 | onClick?: () => void 28 | }) { 29 | return ( 30 | 32 | clsx( 33 | 'group flex justify-between items-center text-secondary no-underline px-0 hover:text-zinc-800 dark:hover:!text-neutral-200', 34 | { 35 | 'text-zinc-800 dark:!text-neutral-200 is-active': isActive, 36 | } 37 | ) 38 | } 39 | to={link.path} 40 | onClick={onClick} 41 | > 42 |
43 | {link.leading} 44 | 45 | 46 | {link.title} 47 | 48 |
49 | 50 | {hasChildren && ( 51 | 52 | {expanded ? 'expand_less' : 'expand_more'} 53 | 54 | )} 55 |
56 | ) 57 | } 58 | 59 | function LinkItem({ 60 | link, 61 | children, 62 | }: { 63 | link: LinkProp 64 | children?: LinkProp[] 65 | }) { 66 | const [expanded, setExpanded] = React.useState(false) 67 | 68 | return ( 69 |
70 | setExpanded((v) => !v)} 73 | hasChildren={Boolean(children?.length)} 74 | expanded={expanded} 75 | /> 76 | 77 | {children && expanded && ( 78 |
    79 | {children.map((link) => ( 80 |
  1. 81 | 82 |
  2. 83 | ))} 84 |
85 | )} 86 |
87 | ) 88 | } 89 | 90 | function NavLinks({ links }: Props) { 91 | return ( 92 |
    93 | {links.map((link) => ( 94 |
  1. 95 | 96 |
  2. 97 | ))} 98 |
99 | ) 100 | } 101 | 102 | export default NavLinks 103 | -------------------------------------------------------------------------------- /admin/src/components/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as RadixPopover from '@radix-ui/react-popover' 2 | import React from 'preact/compat' 3 | 4 | interface Props extends React.PropsWithChildren { 5 | trigger: React.ReactNode 6 | } 7 | 8 | function Popover({ children, trigger }: Props) { 9 | return ( 10 | 11 | {trigger} 12 | 13 | {children} 14 | 15 | 16 | ) 17 | } 18 | 19 | export { Popover } 20 | -------------------------------------------------------------------------------- /admin/src/components/schema-fields.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Control, 3 | Controller, 4 | FieldValues, 5 | UseFormRegister, 6 | } from 'react-hook-form' 7 | import CodeEditor from './code-editor' 8 | import Input from './input' 9 | import type { SchemaDefinitions } from 'mangobase' 10 | import clsx from 'clsx' 11 | 12 | interface Props { 13 | schema: SchemaDefinitions 14 | register: UseFormRegister 15 | control: Control 16 | } 17 | 18 | const EDITOR_OPTIONS = { 19 | acceptSuggestionOnEnter: 'off', 20 | codeLens: false, 21 | contextmenu: false, 22 | foldingHighlight: false, 23 | fontFamily: 'IBM Plex Mono, Zed Mono, monospace', 24 | fontLigatures: true, 25 | fontSize: 14, 26 | hover: { 27 | enabled: false, 28 | }, 29 | lineDecorationsWidth: 0, 30 | lineNumbersMinChars: 2, 31 | minimap: { enabled: false }, 32 | overviewRulerBorder: false, 33 | overviewRulerLanes: 0, 34 | parameterHints: { 35 | enabled: false, 36 | }, 37 | quickSuggestions: { 38 | comments: false, 39 | other: false, 40 | strings: false, 41 | }, 42 | renderLineHighlight: 'none', 43 | scrollbar: { 44 | horizontalSliderSize: 5, 45 | verticalSliderSize: 5, 46 | }, 47 | suggestOnTriggerCharacters: false, 48 | tabCompletion: 'off', 49 | wordBasedSuggestions: false, 50 | } as const 51 | 52 | function SchemaFields({ control, register, schema }: Props) { 53 | return ( 54 |
55 | {Object.entries(schema).map(([name, definition]) => { 56 | const singleColumn = 57 | definition.type === 'string' && definition.treatAs === 'code' 58 | const labelText = {name} 59 | 60 | return ( 61 | 135 | ) 136 | })} 137 |
138 | ) 139 | } 140 | 141 | export default SchemaFields 142 | -------------------------------------------------------------------------------- /admin/src/components/select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'preact/compat' 2 | import clsx from 'clsx' 3 | 4 | type Props = React.ComponentProps<'select'> 5 | 6 | const Select = React.forwardRef( 7 | ( 8 | { className, ...props }: Props, 9 | ref: React.ForwardedRef 10 | ) => { 11 | return ( 12 | 71 | 72 | 76 |

New collection

77 | {showingForm && ( 78 | 79 | )} 80 |
81 | 82 | 83 | 84 | } 85 | > 86 | 87 | 88 | ) 89 | } 90 | 91 | const loader = async () => { 92 | try { 93 | // [ ]: move away from using signals for state management 94 | await loadSchemaRefs() 95 | await loadCollections() 96 | return null 97 | } catch (err) { 98 | throw new AppError((err as any).message, err) 99 | } 100 | } 101 | 102 | export { Component, loader } 103 | -------------------------------------------------------------------------------- /admin/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { FieldValues, useForm } from 'react-hook-form' 2 | import AVATAR_COLORS from '@/lib/avatar-colors' 3 | import Avatar from 'boring-avatars' 4 | import Button from '@/components/button' 5 | import Input from '@/components/input' 6 | import React from 'preact/compat' 7 | import RequestStatus from '@/lib/request-status' 8 | import { addSnack } from '@/lib/snacks' 9 | import app from '../mangobase-app' 10 | import { useNavigate } from 'react-router-dom' 11 | 12 | function Login() { 13 | const { handleSubmit, register, watch } = useForm() 14 | const [username, setUsername] = React.useState('') 15 | const [isNewEnv, setIsNewEnv] = React.useState(false) 16 | const [status, setStatus] = React.useState('idle') 17 | 18 | const navigate = useNavigate() 19 | 20 | const nameChangeTime = React.useRef>() 21 | 22 | async function login(form: FieldValues) { 23 | setStatus('in-progress') 24 | try { 25 | if (isNewEnv) { 26 | await app.req.post('users', { 27 | ...form, 28 | fullname: form.username, 29 | role: 'dev', 30 | }) 31 | } 32 | 33 | const { data } = await app.req.post('login', { ...form }) 34 | app.set('auth', data) 35 | 36 | navigate('/collections', { replace: true }) 37 | } catch (err: any) { 38 | setStatus('failed') 39 | const message = err?.data 40 | ? err.data.error 41 | : err.message || 'failed to login. check logs or try again!' 42 | 43 | addSnack({ content: message, duration: 2500, type: 'error' }) 44 | } 45 | } 46 | 47 | const $username = watch('username') 48 | React.useEffect(() => { 49 | clearTimeout(nameChangeTime.current) 50 | nameChangeTime.current = setTimeout(() => { 51 | setUsername($username) 52 | }, 1000) 53 | }, [$username]) 54 | 55 | React.useEffect(() => { 56 | app.req.get('_dev/dev-setup').then((res) => setIsNewEnv(!res.data)) 57 | }, []) 58 | 59 | return ( 60 |
61 |
62 |
66 |
67 | 72 |
73 | {isNewEnv && ( 74 | <> 75 |

76 | Sweet Mango 🥭 77 |
78 | Be the first dev. 79 |

80 | 86 | 87 | )} 88 | 89 | 95 | 96 | 102 | 103 |
104 | 107 |
108 |
109 |
110 |
111 |
112 |
113 | ) 114 | } 115 | 116 | export default Login 117 | -------------------------------------------------------------------------------- /admin/src/pages/logs.tsx: -------------------------------------------------------------------------------- 1 | import logs, { Log, loadLogStats, loadLogs, logStats } from '@/data/logs' 2 | import BarChart from '@/components/chart' 3 | import Chip from '@/components/chip' 4 | import CleanDate from '@/components/date' 5 | import FilterInput from '@/components/filter-input' 6 | import React from 'preact/compat' 7 | 8 | function Component() { 9 | React.useEffect(() => { 10 | loadLogs() 11 | loadLogStats() 12 | }, []) 13 | 14 | return ( 15 |
16 |

Logs

17 | 18 | 19 |
20 | ({ 22 | x: new Date(value._id).getHours().toString(), 23 | y: value.requests, 24 | }))} 25 | /> 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {logs.value.map((log) => ( 42 | 46 | 49 | 52 | 55 | 58 | 61 | 62 | ))} 63 | 64 |
DateCategoryLabelStatusTime
47 | 48 | 50 | {log.category} 51 | 53 | 54 | 56 | 57 | 59 | {typeof log.time === 'number' ? `${log.time}ms` : ''} 60 |
65 |
66 |
67 | ) 68 | } 69 | 70 | interface StatusProps { 71 | status: number 72 | } 73 | 74 | function Status({ status }: StatusProps) { 75 | if (status >= 200 && status < 400) { 76 | return <>{status} 77 | } 78 | 79 | if (status >= 400 && status < 500) { 80 | return ( 81 |
82 | {status} 83 | 84 |
85 | ) 86 | } 87 | 88 | return ( 89 |
90 | {status} 91 | 92 |
93 | ) 94 | } 95 | 96 | interface LogContentProps { 97 | log: Log 98 | } 99 | 100 | function LogContent({ log }: LogContentProps) { 101 | const [expanded, setExpanded] = React.useState(false) 102 | 103 | return ( 104 |
105 |
106 | {log.label}{' '} 107 | {log.data && ( 108 | 117 | )} 118 |
119 | 120 | {expanded && ( 121 |
122 |           {JSON.stringify(log.data, null, 2)}
123 |         
124 | )} 125 |
126 | ) 127 | } 128 | 129 | export { Component } 130 | -------------------------------------------------------------------------------- /admin/src/pages/notfound.tsx: -------------------------------------------------------------------------------- 1 | function NotFound() { 2 | return ( 3 |
4 |
5 |
6 | join_right 7 |
8 |

It's not clear what should be here.

9 |

You must be missing.

10 | 11 |

— The Router

12 |
13 |
14 | ) 15 | } 16 | 17 | export default NotFound 18 | -------------------------------------------------------------------------------- /admin/src/pages/settings/devs.tsx: -------------------------------------------------------------------------------- 1 | import appDevelopers, { loadAppDevelopers } from '../../data/app-developers' 2 | import AVATAR_COLORS from '@/lib/avatar-colors' 3 | import Avatar from 'boring-avatars' 4 | import React from 'preact/compat' 5 | 6 | function Devs() { 7 | React.useEffect(() => { 8 | loadAppDevelopers() 9 | }, []) 10 | 11 | return ( 12 |
13 |

Devs

14 |

15 | Accounts with developer permissions for the app 16 |

17 | 18 |
19 | {appDevelopers.value.map((dev) => { 20 | return ( 21 |
22 | 27 | 28 |
29 | @ 30 | {dev.username} 31 |
32 |
33 | ) 34 | })} 35 |
36 |
37 | ) 38 | } 39 | 40 | export default Devs 41 | -------------------------------------------------------------------------------- /admin/src/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import schemaRefs, { loadSchemaRefs } from '../../data/schema-refs' 2 | import NavContentLayout from '../../layouts/NavContentLayout' 3 | import NavLinks from '@/components/nav-links' 4 | import { Outlet } from 'react-router-dom' 5 | import React from 'preact/compat' 6 | 7 | function Settings() { 8 | const links = React.useMemo( 9 | () => [ 10 | { 11 | leading: ( 12 | 13 | supervised_user_circle 14 | 15 | ), 16 | path: 'devs', 17 | title: 'Devs', 18 | }, 19 | { 20 | children: schemaRefs.value.map((ref) => ({ 21 | path: `schemas/${ref.name}`, 22 | title: ref.name, 23 | })), 24 | leading: ( 25 | 26 | code_blocks 27 | 28 | ), 29 | path: 'schemas', 30 | title: 'Validation Schemas', 31 | }, 32 | { 33 | leading: ( 34 | 35 | face_4 36 | 37 | ), 38 | path: 'profile', 39 | title: 'Profile', 40 | }, 41 | ], 42 | [schemaRefs.value] 43 | ) 44 | 45 | React.useEffect(() => { 46 | loadSchemaRefs() 47 | }, []) 48 | 49 | return ( 50 | 53 |
Configurations
54 | 55 | 56 | 57 | } 58 | > 59 | 60 |
61 | ) 62 | } 63 | 64 | export default Settings 65 | -------------------------------------------------------------------------------- /admin/src/pages/settings/profile.tsx: -------------------------------------------------------------------------------- 1 | function Profile() { 2 | return ( 3 |
4 |

Your profile

5 |

6 | To edit your profile, use the /users endpoint in the 7 | meantime. 8 |

9 |
10 | ) 11 | } 12 | 13 | export default Profile 14 | -------------------------------------------------------------------------------- /admin/src/pages/settings/schemas/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@/components/button' 2 | import { useNavigate } from 'react-router-dom' 3 | 4 | function Schemas() { 5 | const navigate = useNavigate() 6 | 7 | function addNew() { 8 | navigate('/settings/schemas/new') 9 | } 10 | return ( 11 |
12 |
13 | 14 | reply 15 | 16 |

17 | Expand Validations Schema and select a schema name to view or edit. 18 |

19 | 20 |
21 | 24 |
25 |
26 |
27 | ) 28 | } 29 | 30 | export default Schemas 31 | -------------------------------------------------------------------------------- /admin/src/routes.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, createBrowserRouter } from 'react-router-dom' 2 | import AdminLayout from '@/layouts/AdminLayout' 3 | import Collection from './client/collection' 4 | import CollectionEmptyState from './components/collections-empty-state' 5 | import Devs from './pages/settings/devs' 6 | import { LoaderErrorBoundary } from './components/general-error' 7 | import Login from './pages/login' 8 | import NotFound from './pages/notfound' 9 | import Profile from './pages/settings/profile' 10 | import Schemas from './pages/settings/schemas' 11 | import Settings from './pages/settings' 12 | 13 | interface CollectionRouteData { 14 | collection: Collection 15 | } 16 | 17 | const routes = createBrowserRouter( 18 | [ 19 | { 20 | children: [ 21 | { 22 | children: [ 23 | { 24 | children: [ 25 | { 26 | lazy: () => import('./pages/collections/[name]/index.tsx'), 27 | path: '', 28 | }, 29 | { 30 | lazy: () => import('./pages/collections/[name]/hooks'), 31 | path: 'hooks', 32 | }, 33 | { 34 | lazy: () => import('./pages/collections/[name]/edit'), 35 | path: 'edit', 36 | }, 37 | ], 38 | id: 'collection', 39 | lazy: () => import('./pages/collections/[name]/_layout.tsx'), 40 | path: ':name', 41 | }, 42 | { 43 | element: , 44 | path: '', 45 | }, 46 | ], 47 | lazy: () => import('./pages/collections'), 48 | path: 'collections', 49 | }, 50 | { 51 | lazy: () => import('./pages/logs'), 52 | path: 'logs', 53 | }, 54 | { 55 | children: [ 56 | { 57 | element: , 58 | path: 'schemas', 59 | }, 60 | { 61 | lazy: () => import('./pages/settings/schemas/[name].tsx'), 62 | path: 'schemas/:name', 63 | }, 64 | { 65 | lazy: () => import('./pages/settings/schemas/[name].tsx'), 66 | path: 'schemas/collections/:name', 67 | }, 68 | { 69 | element: , 70 | path: 'profile', 71 | }, 72 | { 73 | element: , 74 | path: 'devs', 75 | }, 76 | { 77 | element: , 78 | path: '', 79 | }, 80 | ], 81 | element: , 82 | path: 'settings', 83 | }, 84 | { 85 | element: , 86 | path: '', 87 | }, 88 | ], 89 | element: , 90 | errorElement: , 91 | path: '', 92 | }, 93 | { 94 | element: , 95 | path: 'login', 96 | }, 97 | { 98 | element: , 99 | path: '*', 100 | }, 101 | ], 102 | { 103 | basename: '/_', 104 | } 105 | ) 106 | 107 | export default routes 108 | export type { CollectionRouteData } 109 | -------------------------------------------------------------------------------- /admin/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 | plugins: [require('@tailwindcss/forms')], 5 | theme: { 6 | extend: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "preact", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | }, 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "types": ["preact/compat"] 27 | }, 28 | "include": ["src", "./*.js", "./*.ts"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /admin/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "types": ["preact/compat"] 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /admin/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | import preact from '@preact/preset-vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: '/_', 8 | plugins: [preact()], 9 | resolve: { 10 | alias: { 11 | '@': '/src', 12 | }, 13 | }, 14 | build: { 15 | rollupOptions: { 16 | external: ['node:buffer', 'node:crypto', 'node:util'], 17 | }, 18 | }, 19 | test: { 20 | environment: 'jsdom', 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /assets/ss-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/assets/ss-dark.png -------------------------------------------------------------------------------- /assets/ss-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackmann/mangobase/ddf71b8b4e80945e84e7e0f0c4db9714cbfed300/assets/ss-light.png -------------------------------------------------------------------------------- /base/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["base"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | dist/ 3 | .mangobase -------------------------------------------------------------------------------- /base/README.md: -------------------------------------------------------------------------------- 1 | # mangobase 2 | 3 | [![npm](https://img.shields.io/npm/dm/mangobase)](https://www.npmjs.com/package/mangobase) 4 | [![npm](https://img.shields.io/npm/v/mangobase)](https://www.npmjs.com/package/mangobase) 5 | [![npm](https://img.shields.io/npm/l/mangobase)](https://www.npmjs.com/package/mangobase) 6 | 7 | This is the core library of the [mangobase](https://degreat.co.uk/mangobase) suite. 8 | 9 | This package basically exposes a Mangobase [`App`](https://degreat.co.uk/mangobase/api/base/App.html) class which provides the [`api`](https://degreat.co.uk/mangobase/api/base/App.html#api) and [`admin`](https://degreat.co.uk/mangobase/api/base/App.html#admin) methods to interact with. 10 | 11 | Visit https://degreat.co.uk/mangobase to get started. 12 | 13 | ```bash 14 | npm create mango@latest 15 | yarn create mango 16 | ``` 17 | 18 | ![Screenshot](https://github.com/blackmann/mangobase/raw/master/assets/ss-light.png) -------------------------------------------------------------------------------- /base/build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const commonConfig = { 4 | bundle: true, 5 | external: ['bcrypt', 'jose'], 6 | format: 'esm', 7 | platform: 'node', 8 | target: 'esnext', 9 | } 10 | 11 | // package 12 | await esbuild.build({ 13 | ...commonConfig, 14 | entryPoints: ['src/index.ts'], 15 | outdir: 'dist/', 16 | }) 17 | 18 | // utitlities 19 | await esbuild.build({ 20 | ...commonConfig, 21 | entryPoints: ['src/lib/index.ts'], 22 | outdir: 'dist/lib', 23 | }) 24 | -------------------------------------------------------------------------------- /base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mangobase", 3 | "version": "0.9.14", 4 | "description": "Low-code backend framework", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/blackmann/mangobase", 7 | "homepage": "https://degreat.co.uk/mangobase", 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "exports": { 14 | ".": "./dist/index.js", 15 | "./lib": "./dist/lib/index.js" 16 | }, 17 | "scripts": { 18 | "build": "yarn clean && node ./build.mjs && tsc -p tsconfig.build.json", 19 | "clean": "rm -rf dist", 20 | "copy-admin": "cp -R ../admin/dist dist/admin", 21 | "prepare": "yarn build && yarn copy-admin", 22 | "test": "vitest" 23 | }, 24 | "author": "De-Great Yartey ", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/bcrypt": "^5.0.2", 28 | "@vitest/coverage-v8": "^0.34.4", 29 | "@vitest/ui": "^0.31.0", 30 | "esbuild": "^0.17.19", 31 | "eslint-config-base": "*", 32 | "mongodb-memory-server-core": "8.15.1", 33 | "radix3": "^1.0.1", 34 | "typescript": "^5.0.4", 35 | "vitest": "^0.34.4" 36 | }, 37 | "dependencies": { 38 | "bcrypt": "^5.1.1", 39 | "jose": "^5.1.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /base/src/authentication.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, assert, beforeAll, describe, expect, it } from 'vitest' 2 | import { App } from './app.js' 3 | import { MongoDb } from '@mangobase/mongodb' 4 | import { MongoMemoryServer } from 'mongodb-memory-server-core' 5 | import { context } from './context.js' 6 | import fs from 'fs' 7 | 8 | let app: App 9 | let mongod: MongoMemoryServer 10 | 11 | async function setup() { 12 | process.env.SECRET_KEY = 'test' 13 | mongod = await MongoMemoryServer.create() 14 | app = new App({ 15 | db: new MongoDb(mongod.getUri('auth-test')), 16 | }) 17 | } 18 | 19 | async function teardown() { 20 | await mongod.stop() 21 | fs.rmSync('./.mangobase', { force: true, recursive: true }) 22 | } 23 | 24 | beforeAll(setup, 30_000) 25 | afterAll(teardown) 26 | 27 | describe('authentication', () => { 28 | beforeAll(async () => { 29 | const { result } = await app.api( 30 | context({ 31 | data: { 32 | email: 'mock-1@mail.com', 33 | fullname: 'Mock User', 34 | password: 'helloworld', 35 | username: 'mock-1', 36 | }, 37 | method: 'create', 38 | path: 'users', 39 | }) 40 | ) 41 | 42 | assert(result._id) 43 | }) 44 | 45 | describe('login with username', () => { 46 | it('should return a token', async () => { 47 | const { result } = await app.api( 48 | context({ 49 | data: { 50 | password: 'helloworld', 51 | username: 'mock-1', 52 | }, 53 | method: 'create', 54 | path: 'login', 55 | }) 56 | ) 57 | 58 | expect(result.auth.token).toBeDefined() 59 | expect(result.user).toBeDefined() 60 | }) 61 | }) 62 | 63 | describe('login with email', () => { 64 | it('should return a token', async () => { 65 | const { result } = await app.api( 66 | context({ 67 | data: { 68 | email: 'mock-1@mail.com', 69 | password: 'helloworld', 70 | }, 71 | method: 'create', 72 | path: 'login', 73 | }) 74 | ) 75 | 76 | expect(result.auth.token).toBeDefined() 77 | expect(result.user).toBeDefined() 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /base/src/context.ts: -------------------------------------------------------------------------------- 1 | import { type Method } from './method.js' 2 | 3 | interface Context { 4 | data?: any 5 | headers: Record 6 | 7 | /** 8 | * Use this to store any abitrary data that can be accessed from another 9 | * hook. 10 | */ 11 | locals: Record 12 | method: `${Method}` 13 | 14 | /** 15 | * The path part of the URL without the query parameters. If you want the full URL, 16 | * see {@link Context.url} 17 | */ 18 | path: string 19 | 20 | /** Params are values parsed from the URL path. For example, if a service handles the 21 | * path pattern `/songs/:songId`, you get params for a url `/songs/123` as 22 | * 23 | * ```javascript 24 | * { 25 | * songId: '123' 26 | * } 27 | * ``` 28 | */ 29 | params?: Record 30 | 31 | /** Query are request query parameters */ 32 | query: Record 33 | 34 | /** 35 | * Set this with the result of processing a context. When set, subsequent `before` hooks are not called. 36 | */ 37 | result?: any 38 | 39 | /** 40 | * The status code the server adapter sets on the response. 41 | */ 42 | statusCode?: number 43 | 44 | url: string 45 | 46 | /** 47 | * The authenticated user. This is normally set by an authentication hook. 48 | */ 49 | user?: any 50 | } 51 | 52 | function context(ctx: Partial): Context { 53 | return { 54 | headers: {}, 55 | locals: {}, 56 | method: 'find', 57 | path: '', 58 | query: {}, 59 | url: '/', 60 | ...ctx, 61 | } 62 | } 63 | 64 | export { context } 65 | export type { Context } 66 | -------------------------------------------------------------------------------- /base/src/database.ts: -------------------------------------------------------------------------------- 1 | import type { Definition, DefinitionType, SchemaDefinitions } from './schema.js' 2 | 3 | type SortOrder = -1 | 1 4 | 5 | interface Cursor { 6 | exec(): Promise 7 | limit(n: number): Cursor 8 | populate( 9 | fields: (string | { collection: string; field: string })[] 10 | ): Cursor 11 | select(fields: string[]): Cursor 12 | skip(n: number): Cursor 13 | sort(config: Record): Cursor 14 | } 15 | 16 | interface Filter { 17 | limit?: number 18 | populate?: string[] 19 | select?: string[] 20 | skip?: number 21 | sort?: Record 22 | } 23 | 24 | type Data = Record 25 | 26 | interface Index { 27 | // when sort is not specified, it defaults to ascending (1) 28 | fields: [string, SortOrder][] | string[] 29 | options?: { 30 | unique?: boolean 31 | sparse?: boolean 32 | } 33 | } 34 | 35 | interface RenameField { 36 | type: 'rename-field' 37 | collection: string 38 | from: string 39 | to: string 40 | } 41 | 42 | interface RemoveField { 43 | type: 'remove-field' 44 | collection: string 45 | field: string 46 | } 47 | 48 | interface AddField { 49 | collection: string 50 | type: 'add-field' 51 | name: string 52 | definition: Definition 53 | } 54 | 55 | interface RenameCollection { 56 | type: 'rename-collection' 57 | collection: string 58 | to: string 59 | } 60 | 61 | interface CreateCollection { 62 | type: 'create-collection' 63 | name: string 64 | collection: { name: string; schema: SchemaDefinitions; indexes: Index[] } 65 | } 66 | 67 | interface AddIndex { 68 | type: 'add-index' 69 | collection: string 70 | index: Index 71 | } 72 | 73 | interface RemoveIndex { 74 | type: 'remove-index' 75 | collection: string 76 | index: Index 77 | } 78 | 79 | interface UpdateConstraints { 80 | type: 'update-constraints' 81 | collection: string 82 | field: string 83 | constraints: { 84 | unique?: boolean 85 | } 86 | } 87 | 88 | // When adding support for RDBMS, we need to add `CreateCollection`, `DropCollection`, etc. 89 | type MigrationStep = 90 | | CreateCollection 91 | | RenameField 92 | | RemoveField 93 | | AddField 94 | | RenameCollection 95 | | AddIndex 96 | | RemoveIndex 97 | | UpdateConstraints 98 | 99 | interface Migration { 100 | /** 101 | * This is an autogenerated randomstring of the migration. This is useful especially in 102 | * version control. When there's a conflict on this ID in the manifest file, it signals 103 | * a clash in the migration, so `version` number should be bumped 104 | */ 105 | id: string 106 | version: number 107 | steps: MigrationStep[] 108 | } 109 | 110 | interface Database { 111 | /** 112 | * Casting is an affordance to convert data in to formats that database prefers. 113 | * This is normally called during schema validation. 114 | */ 115 | cast(value: any, type: DefinitionType): any 116 | count(collection: string, query: Record): Promise 117 | find(collection: string, query: Record): Cursor 118 | /** 119 | * @param data data or array of data to be inserted 120 | */ 121 | create(collection: string, data: Data | Data[]): Cursor 122 | patch( 123 | collection: string, 124 | id: string | string[], 125 | data: Record 126 | ): Cursor 127 | remove(collection: string, id: string | string[]): Promise 128 | migrate(migration: Migration): Promise 129 | addIndexes(collection: string, indexes: Index[]): Promise 130 | removeIndexes(collection: string, indexes: Index[]): Promise 131 | 132 | // [ ] Properly standardize this API 133 | aggregate( 134 | collection: string, 135 | query: Record, 136 | filter: Filter, 137 | operations: Record[] 138 | ): Promise 139 | } 140 | 141 | export type { 142 | Cursor, 143 | Database, 144 | Filter as DatabaseFilter, 145 | Index, 146 | Migration, 147 | MigrationStep, 148 | SortOrder, 149 | } 150 | -------------------------------------------------------------------------------- /base/src/db-migrations.ts: -------------------------------------------------------------------------------- 1 | import { type App } from './app.js' 2 | import type { Migration } from './database.js' 3 | import { type SchemaDefinitions } from './schema.js' 4 | import getCollection from './lib/get-collection.js' 5 | 6 | const migrationSchema: SchemaDefinitions = { 7 | id: { required: true, type: 'string' }, 8 | version: { required: true, type: 'number' }, 9 | } 10 | 11 | const collectionName = '_migrations' 12 | 13 | async function dbMigrations(app: App) { 14 | if (!(await app.manifest.collection(collectionName))) { 15 | const indexes = [{ fields: ['version'], options: { unique: true } }] 16 | await app.manifest.collection(collectionName, { 17 | exposed: false, 18 | indexes, 19 | name: collectionName, 20 | schema: migrationSchema, 21 | }) 22 | 23 | await app.database.addIndexes(collectionName, indexes) 24 | } 25 | 26 | const migrationsCollection = getCollection( 27 | app, 28 | collectionName, 29 | migrationSchema 30 | ) 31 | 32 | const { 33 | data: [latestMigration], 34 | } = await migrationsCollection.find({ 35 | filter: { $limit: 1, $sort: { created_at: -1 } }, 36 | query: {}, 37 | }) 38 | 39 | let latestMigrationVersion = latestMigration?.version || 0 40 | const latestCommitVersion = 41 | (await app.manifest.getLastMigrationCommit())?.version || 0 42 | 43 | latestMigrationVersion++ 44 | 45 | if (latestMigrationVersion <= latestCommitVersion) { 46 | console.log('applying migrations') 47 | } 48 | 49 | while (latestMigrationVersion <= latestCommitVersion) { 50 | const migration = await app.manifest.getMigration(latestMigrationVersion) 51 | await app.database.migrate(migration) 52 | 53 | await saveMigration(app, migration) 54 | 55 | console.log(`[ ] migration ${migration.id} applied`) 56 | latestMigrationVersion++ 57 | } 58 | } 59 | 60 | async function saveMigration(app: App, migration: Migration) { 61 | const migrationsCollection = getCollection( 62 | app, 63 | collectionName, 64 | migrationSchema 65 | ) 66 | 67 | await migrationsCollection.create({ 68 | id: migration.id, 69 | version: migration.version, 70 | }) 71 | } 72 | 73 | export default dbMigrations 74 | export { saveMigration } 75 | -------------------------------------------------------------------------------- /base/src/errors.ts: -------------------------------------------------------------------------------- 1 | class ServiceError extends Error { 2 | name = 'GeneralError' 3 | statusCode = 500 4 | data: any 5 | 6 | constructor(message?: string, data?: any) { 7 | super(message) 8 | this.data = data 9 | } 10 | } 11 | 12 | class BadRequest extends ServiceError { 13 | name = 'BadRequest' 14 | statusCode = 400 15 | } 16 | 17 | class Conflict extends ServiceError { 18 | name = 'Conflict' 19 | statusCode = 409 20 | } 21 | 22 | class InternalServerError extends ServiceError { 23 | name = 'InternalServerError' 24 | statusCode = 500 25 | } 26 | 27 | class MethodNotAllowed extends ServiceError { 28 | name = 'MethodNotAllowed' 29 | statusCode = 405 30 | } 31 | 32 | class NotFound extends ServiceError { 33 | name = 'NotFound' 34 | statusCode = 404 35 | } 36 | 37 | class Unauthorized extends ServiceError { 38 | name = 'Unauthorized' 39 | statusCode = 401 40 | } 41 | 42 | class AppError extends Error {} 43 | 44 | export { 45 | AppError, 46 | BadRequest, 47 | Conflict, 48 | InternalServerError, 49 | MethodNotAllowed, 50 | NotFound, 51 | ServiceError, 52 | Unauthorized, 53 | } 54 | -------------------------------------------------------------------------------- /base/src/hook.ts: -------------------------------------------------------------------------------- 1 | import { App } from './app.js' 2 | import type { Context } from './context.js' 3 | import { type Definition } from './schema.js' 4 | import { type Method } from './method.js' 5 | 6 | type Config = Record 7 | 8 | type HookFn = ( 9 | ctx: Context, 10 | config: Config | undefined, 11 | app: App 12 | ) => Promise 13 | 14 | interface Hook { 15 | id: string 16 | name: string 17 | description?: string 18 | configSchema?: Record 19 | run: HookFn 20 | } 21 | 22 | type Hooks = { 23 | after: Record<`${Method}`, [HookFn, Config?][]> 24 | before: Record<`${Method}`, [HookFn, Config?][]> 25 | } 26 | 27 | export type { Hook, HookFn, Hooks, Config as HookConfig } 28 | -------------------------------------------------------------------------------- /base/src/hooks-registry.ts: -------------------------------------------------------------------------------- 1 | import type { Hook } from './hook.js' 2 | import { Schema } from './schema.js' 3 | import allHooks from './hooks.js' 4 | 5 | class HooksRegistry { 6 | private registry: Record = {} 7 | 8 | installCommon() { 9 | for (const hook of allHooks) { 10 | this.registry[hook.id] = hook 11 | } 12 | } 13 | 14 | register(...hooks: Hook[]) { 15 | for (const hook of hooks) { 16 | if (hook.configSchema) { 17 | Schema.validateSchema(hook.configSchema) 18 | } 19 | 20 | if (this.registry[hook.id]) { 21 | throw new Error(`A hook with id ${hook.id} is already registered`) 22 | } 23 | 24 | this.registry[hook.id] = hook 25 | } 26 | } 27 | 28 | get(id: string): Hook { 29 | const hook = this.registry[id] 30 | if (!hook) { 31 | throw new Error(`Hook ${id} not found`) 32 | } 33 | return hook 34 | } 35 | 36 | list(): Hook[] { 37 | return Object.values(this.registry) 38 | } 39 | } 40 | 41 | export { HooksRegistry } 42 | -------------------------------------------------------------------------------- /base/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Hook, HookFn } from './hook.js' 2 | import { MethodNotAllowed } from './errors.js' 3 | 4 | const LogData: Hook = { 5 | description: 'Logs data. Check logs console.', 6 | id: 'log-data', 7 | name: 'Log data', 8 | run: async (ctx) => { 9 | console.log(`[${new Date().toISOString()}]`, ctx.path, ctx.method, ctx.data) 10 | return ctx 11 | }, 12 | } 13 | 14 | const RestrictMethod: Hook = { 15 | configSchema: { 16 | allowDevs: { type: 'boolean' }, 17 | }, 18 | description: 'Restrict access to connected method', 19 | id: 'restrict-method', 20 | name: 'Restrict method', 21 | run: async (ctx, config) => { 22 | if (config?.allowDevs && ctx.user?.role === 'dev') { 23 | return ctx 24 | } 25 | 26 | throw new MethodNotAllowed() 27 | }, 28 | } 29 | 30 | const CustomCode: Hook = { 31 | configSchema: { 32 | code: { 33 | defaultValue: `return async (ctx, app) => { 34 | return ctx 35 | }`, 36 | required: true, 37 | treatAs: 'code', 38 | type: 'string', 39 | }, 40 | }, 41 | description: 'Run custom code', 42 | id: 'custom-code', 43 | name: 'Custom Code', 44 | async run(ctx, config, app) { 45 | if (!config) { 46 | return ctx 47 | } 48 | 49 | // we need to warm up the code 50 | // config stays the same between runs, unless the hook is 51 | // updated 52 | if (!config.exec) { 53 | config.exec = new Function(config.code)() as HookFn 54 | } 55 | 56 | return await config.exec(ctx, app) 57 | }, 58 | } 59 | 60 | const allHooks = [LogData, CustomCode, RestrictMethod] 61 | 62 | export default allHooks 63 | 64 | export { LogData } 65 | -------------------------------------------------------------------------------- /base/src/index.ts: -------------------------------------------------------------------------------- 1 | export { App, Pipeline } from './app.js' 2 | export { CollectionService } from './collection-service.js' 3 | export { Collection } from './collection.js' 4 | export { context } from './context.js' 5 | export { HooksRegistry } from './hooks-registry.js' 6 | export { Manifest } from './manifest.js' 7 | export { Schema, ValidationError } from './schema.js' 8 | 9 | export { methodFromHttp } from './lib/method-from-http.js' 10 | export { getRefUsage } from './lib/get-ref-usage.js' 11 | export { exportSchema } from './lib/export-schema.js' 12 | 13 | export type { Handle, Service } from './app.js' 14 | export type { Filter, FilterOperators, Query } from './collection.js' 15 | export type { Context } from './context.js' 16 | export * from './database.js' 17 | export * as errors from './errors.js' 18 | export type { Hook, HookFn, Hooks, HookConfig } from './hook.js' 19 | export type { CollectionConfig, Ref } from './manifest.js' 20 | export { Method } from './method.js' 21 | export type { SchemaDefinitions, Definition, DefinitionType } from './schema.js' 22 | -------------------------------------------------------------------------------- /base/src/lib/__snapshots__/export-schema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`export-schema > typescript > should return typescript definition [include object schema] 1`] = ` 4 | "interface Test { 5 | _id: string 6 | address: Address 7 | age?: number 8 | alive: boolean 9 | connection?: string 10 | created_at: Date 11 | friends_contacts: FriendsContacts[] 12 | name: string 13 | tags?: string[] 14 | gender?: \\"male\\" | \\"female\\" 15 | } 16 | 17 | interface Address { 18 | country: Country 19 | line1: string 20 | } 21 | 22 | interface Country { 23 | code?: string 24 | title: string 25 | } 26 | 27 | interface FriendsContacts { 28 | email: string 29 | name: string 30 | }" 31 | `; 32 | 33 | exports[`export-schema > typescript > should return typescript definition [inline object] 1`] = ` 34 | "interface Test { 35 | _id: string 36 | address: { 37 | country: { 38 | code?: string 39 | title: string 40 | } 41 | line1: string 42 | } 43 | age?: number 44 | alive: boolean 45 | connection?: string 46 | created_at: Date 47 | friends_contacts: { 48 | email: string 49 | name: string 50 | }[] 51 | name: string 52 | tags?: string[] 53 | gender?: \\"male\\" | \\"female\\" 54 | } 55 | 56 | " 57 | `; 58 | -------------------------------------------------------------------------------- /base/src/lib/api-paths.ts: -------------------------------------------------------------------------------- 1 | function onDev(path: string) { 2 | return `_dev/${path}` 3 | } 4 | 5 | function unexposed(path: string) { 6 | return `_x/${path}` 7 | } 8 | 9 | export { onDev, unexposed } 10 | -------------------------------------------------------------------------------- /base/src/lib/clone.ts: -------------------------------------------------------------------------------- 1 | function clone(obj: T) { 2 | return JSON.parse(JSON.stringify(obj)) as T 3 | } 4 | 5 | export { clone } 6 | -------------------------------------------------------------------------------- /base/src/lib/export-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { type ExportResult, exportSchema } from './export-schema.js' 2 | import { describe, expect, it } from 'vitest' 3 | import { SchemaDefinitions } from '../schema.js' 4 | 5 | function render({ definition, includes }: ExportResult) { 6 | return [definition, Object.values(includes).join('\n\n')].join('\n\n') 7 | } 8 | 9 | describe('export-schema', () => { 10 | describe('typescript', () => { 11 | const schema: SchemaDefinitions = { 12 | address: { 13 | required: true, 14 | schema: { 15 | country: { 16 | required: true, 17 | schema: { 18 | code: { defaultValue: 'GH', type: 'string' }, 19 | title: { required: true, type: 'string' }, 20 | }, 21 | type: 'object', 22 | }, 23 | line1: { required: true, type: 'string' }, 24 | }, 25 | type: 'object', 26 | }, 27 | age: { type: 'number' }, 28 | alive: { required: true, type: 'boolean' }, 29 | connection: { relation: 'something', type: 'id' }, 30 | created_at: { required: true, type: 'date' }, 31 | friends_contacts: { 32 | items: { 33 | schema: { 34 | email: { required: true, type: 'string' }, 35 | name: { required: true, type: 'string' }, 36 | }, 37 | type: 'object', 38 | }, 39 | required: true, 40 | type: 'array', 41 | }, 42 | name: { required: true, type: 'string' }, 43 | tags: { items: { type: 'string' }, type: 'array' }, 44 | gender: { type: 'string', enum: ['male', 'female'] }, 45 | } 46 | 47 | it('should return typescript definition [include object schema]', async () => { 48 | expect( 49 | render( 50 | await exportSchema({ 51 | getRef: async () => ({}), 52 | includeObjectSchema: true, 53 | language: 'typescript', 54 | name: 'test', 55 | schema, 56 | }) 57 | ) 58 | ).toMatchSnapshot() 59 | }) 60 | 61 | it('should return typescript definition [inline object]', async () => { 62 | expect( 63 | render( 64 | await exportSchema({ 65 | getRef: async () => ({}), 66 | includeObjectSchema: true, 67 | inlineObjectSchema: true, 68 | language: 'typescript', 69 | name: 'test', 70 | schema, 71 | }) 72 | ) 73 | ).toMatchSnapshot() 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /base/src/lib/get-collection.ts: -------------------------------------------------------------------------------- 1 | import { Schema, type SchemaDefinitions } from '../schema.js' 2 | import { type App } from '../app.js' 3 | import { Collection } from '../collection.js' 4 | 5 | function getCollection(app: App, name: string, schema: SchemaDefinitions) { 6 | return new Collection(name, { 7 | db: app.database, 8 | schema: Promise.resolve(new Schema(schema, { parser: app.database.cast })), 9 | }) 10 | } 11 | 12 | export default getCollection 13 | -------------------------------------------------------------------------------- /base/src/lib/get-ref-usage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { SchemaDefinitions } from '../schema.js' 3 | import { getRefUsage } from './get-ref-usage.js' 4 | 5 | describe('getRefUsage', () => { 6 | const schema: SchemaDefinitions = { 7 | arr1: { 8 | items: { 9 | schema: 'ref1', 10 | type: 'object', 11 | }, 12 | type: 'array', 13 | }, 14 | arr2: { 15 | items: [ 16 | { 17 | schema: 'ref1', 18 | type: 'object', 19 | }, 20 | { 21 | schema: { 22 | prop1: { 23 | schema: 'ref1', 24 | type: 'object', 25 | }, 26 | }, 27 | type: 'object', 28 | }, 29 | ], 30 | type: 'array', 31 | }, 32 | obj1: { 33 | schema: { 34 | obj2: { 35 | schema: { 36 | ref1: { 37 | schema: 'ref1', 38 | type: 'object', 39 | }, 40 | }, 41 | type: 'object', 42 | }, 43 | }, 44 | type: 'object', 45 | }, 46 | ref1: { 47 | schema: { 48 | prop1: { 49 | type: 'string', 50 | }, 51 | }, 52 | type: 'object', 53 | }, 54 | } 55 | 56 | it('should return an empty array if refName is not found in schema', () => { 57 | const refName = 'ref2' 58 | const usage = getRefUsage(refName, schema) 59 | expect(usage).toEqual([]) 60 | }) 61 | 62 | it('should return an array of paths where refName is used in schema', () => { 63 | const refName = 'ref1' 64 | const usage = getRefUsage(refName, schema) 65 | expect(usage).toEqual([ 66 | ['arr1', 'items', 'schema'], 67 | ['arr2', 'items', '0', 'schema'], 68 | ['arr2', 'items', '1', 'schema', 'prop1', 'schema'], 69 | ['obj1', 'schema', 'obj2', 'schema', 'ref1', 'schema'], 70 | ]) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /base/src/lib/get-ref-usage.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaDefinitions } from '../schema.js' 2 | 3 | function getRefUsage(refName: string, schema: SchemaDefinitions) { 4 | const usage: string[][] = [] 5 | 6 | for (const [key, definition] of Object.entries(schema)) { 7 | if (definition.type === 'object') { 8 | if (typeof definition.schema === 'string') { 9 | if (definition.schema === refName) { 10 | usage.push([key, 'schema']) 11 | } 12 | continue 13 | } 14 | 15 | const nestedUsage = getRefUsage(refName, definition.schema) 16 | for (const path of nestedUsage) { 17 | usage.push([key, 'schema', ...path]) 18 | } 19 | } 20 | 21 | if (definition.type === 'array') { 22 | if (Array.isArray(definition.items)) { 23 | for (const [i, itemDefinition] of definition.items.entries()) { 24 | if (itemDefinition.type === 'object') { 25 | if (typeof itemDefinition.schema === 'string') { 26 | if (itemDefinition.schema === refName) { 27 | usage.push([key, 'items', i.toString(), 'schema']) 28 | } 29 | continue 30 | } 31 | 32 | const nestedUsage = getRefUsage(refName, itemDefinition.schema) 33 | for (const path of nestedUsage) { 34 | usage.push([key, 'items', i.toString(), 'schema', ...path]) 35 | } 36 | } 37 | } 38 | } else if (definition.items.type === 'object') { 39 | if (typeof definition.items.schema === 'string') { 40 | if (definition.items.schema === refName) { 41 | usage.push([key, 'items', 'schema']) 42 | } 43 | } else { 44 | const nestedUsage = getRefUsage(refName, definition.items.schema) 45 | for (const path of nestedUsage) { 46 | usage.push([key, 'items', ...path]) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | return usage 54 | } 55 | 56 | export { getRefUsage } 57 | -------------------------------------------------------------------------------- /base/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | exportSchema, 3 | type ExportOptions, 4 | type ExportResult, 5 | } from './export-schema.js' 6 | -------------------------------------------------------------------------------- /base/src/lib/method-from-http.ts: -------------------------------------------------------------------------------- 1 | import { Method } from '../method.js' 2 | 3 | const lookup: Record = { 4 | DELETE: Method.remove, 5 | GET: Method.find, 6 | PATCH: Method.patch, 7 | POST: Method.create, 8 | } 9 | 10 | function methodFromHttp(method: string) { 11 | return lookup[method] || Method.find 12 | } 13 | 14 | export { methodFromHttp } 15 | -------------------------------------------------------------------------------- /base/src/lib/random-str.ts: -------------------------------------------------------------------------------- 1 | const candidates = 2 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 3 | 4 | function randomStr(length = 8) { 5 | return Array.from({ length }) 6 | .map(() => candidates[Math.floor(Math.random() * candidates.length)]) 7 | .join('') 8 | } 9 | 10 | export default randomStr 11 | -------------------------------------------------------------------------------- /base/src/lib/set-with-path.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import setWithPath from './set-with-path.js' 3 | 4 | const data = { 5 | address: { 6 | city: 'Accra', 7 | country: 'Ghana', 8 | }, 9 | name: 'mock', 10 | } 11 | 12 | describe('set-with-path', () => { 13 | it('should set value of field', () => { 14 | const path = ['name'] 15 | const value = 'mock-change' 16 | 17 | setWithPath(data, path, value) 18 | 19 | expect(data.name).toBe(value) 20 | }) 21 | 22 | it('should set a value in a nested object', () => { 23 | const path = ['address', 'city'] 24 | const value = 'Kumasi' 25 | 26 | setWithPath(data, path, value) 27 | 28 | expect(data.address.city).toBe(value) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /base/src/lib/set-with-path.ts: -------------------------------------------------------------------------------- 1 | function setWithPath( 2 | obj: Record, 3 | path: (string | number)[], 4 | value: any 5 | ) { 6 | let current = obj 7 | let i = 0 8 | for (; i < path.length - 1; i++) { 9 | current = current[path[i]] 10 | } 11 | 12 | current[path[i]] = value 13 | } 14 | 15 | export default setWithPath 16 | -------------------------------------------------------------------------------- /base/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { type App } from './app.js' 2 | import { CollectionService } from './collection-service.js' 3 | import { HookFn } from './hook.js' 4 | import { MethodNotAllowed } from './errors.js' 5 | import { Schema } from './schema.js' 6 | import { onDev } from './lib/api-paths.js' 7 | 8 | const getPath = () => onDev('logs') 9 | const getStatsPath = () => onDev('log-stats') 10 | 11 | async function logger(app: App) { 12 | const collectionName = '_logs' 13 | 14 | const schema = new Schema( 15 | { 16 | category: { required: true, type: 'string' }, 17 | data: { type: 'any' }, 18 | label: { required: true, type: 'string' }, 19 | status: { type: 'number' }, 20 | time: { type: 'number' }, 21 | }, 22 | { parser: app.database.cast } 23 | ) 24 | const service = new CollectionService(app, collectionName, { schema }) 25 | 26 | app.use(getStatsPath(), async (ctx, app) => { 27 | if (ctx.method !== 'find') { 28 | throw new MethodNotAllowed() 29 | } 30 | 31 | const maxDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000 // 48 hours ago 32 | 33 | const query = { 34 | created_at: { $gte: new Date(maxDaysAgo) }, 35 | } 36 | 37 | const ops = [ 38 | { 39 | $group: { 40 | _id: { 41 | $dateToString: { 42 | date: '$created_at', 43 | format: '%Y-%m-%dT%H:00:00', 44 | }, 45 | }, 46 | date: { $first: '$created_at' }, 47 | requests: { $sum: 1 }, 48 | }, 49 | }, 50 | { 51 | $sort: { date: 1 }, 52 | }, 53 | ] 54 | 55 | const results = await app.database.aggregate( 56 | collectionName, 57 | query, 58 | { sort: { created_at: 1 } }, 59 | ops 60 | ) 61 | ctx.result = results 62 | 63 | return ctx 64 | }) 65 | 66 | app.use(getPath(), service) 67 | } 68 | 69 | const logStart: HookFn = async (ctx) => { 70 | ctx.locals['reqstart'] = Date.now() 71 | return ctx 72 | } 73 | 74 | const logEnd: HookFn = async (ctx, _, app) => { 75 | const service = app.service(getPath()) as CollectionService 76 | await service.collection.create({ 77 | category: ctx.method, 78 | data: (ctx.statusCode || 200) > 399 ? ctx.result : undefined, 79 | label: ctx.url, 80 | status: ctx.statusCode, 81 | time: Date.now() - ctx.locals['reqstart'], 82 | }) 83 | 84 | return ctx 85 | } 86 | 87 | export default logger 88 | export { logEnd, logStart } 89 | -------------------------------------------------------------------------------- /base/src/method.ts: -------------------------------------------------------------------------------- 1 | enum Method { 2 | create = 'create', 3 | find = 'find', 4 | get = 'get', 5 | patch = 'patch', 6 | remove = 'remove', 7 | } 8 | 9 | export { Method } 10 | -------------------------------------------------------------------------------- /base/src/users.ts: -------------------------------------------------------------------------------- 1 | import { App } from './app.js' 2 | import { SchemaDefinitions } from './schema.js' 3 | 4 | const usersSchema: SchemaDefinitions = { 5 | avatar: { type: 'string' }, 6 | email: { required: true, type: 'string', unique: true }, 7 | fullname: { required: true, type: 'string' }, 8 | // [ ] Prevent anyone from just creating a 'dev' account. Use hook 9 | role: { 10 | defaultValue: 'basic', 11 | enum: ['dev', 'basic'], 12 | required: true, 13 | type: 'string', 14 | }, 15 | username: { required: true, type: 'string', unique: true }, 16 | verified: { type: 'boolean' }, 17 | } 18 | 19 | async function users(app: App) { 20 | if (!(await app.manifest.collection('users'))) { 21 | const indexes = [ 22 | { fields: ['username'], options: { unique: true } }, 23 | { fields: ['email'], options: { unique: true } }, 24 | ] 25 | 26 | await app.manifest.collection('users', { 27 | exposed: true, 28 | indexes, 29 | name: 'users', 30 | readOnlySchema: true, 31 | schema: usersSchema, 32 | }) 33 | 34 | await app.database.addIndexes('users', indexes) 35 | } 36 | } 37 | 38 | export default users 39 | -------------------------------------------------------------------------------- /base/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "global.setup.ts", 5 | "vitest.config.ts", 6 | "src/**/*.test.ts", 7 | "src/**/*.spec.ts", 8 | "dist/**/**" 9 | ] 10 | } -------------------------------------------------------------------------------- /base/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | test: { 6 | clearMocks: true, 7 | coverage: { 8 | enabled: true, 9 | }, 10 | reporters: ['default', 'html'], 11 | threads: false, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /bun-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | *.tsbuildinfo -------------------------------------------------------------------------------- /bun-server/README.md: -------------------------------------------------------------------------------- 1 | # @mangobase/bun 2 | 3 | Bun server adapter for mangobase. 4 | -------------------------------------------------------------------------------- /bun-server/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | esbuild.build({ 4 | bundle: true, 5 | entryPoints: ['src/index.ts'], 6 | outdir: 'dist/', 7 | external: ['mangobase'], 8 | platform: 'node', 9 | }) 10 | -------------------------------------------------------------------------------- /bun-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mangobase/bun", 3 | "module": "src/index.ts", 4 | "version": "0.9.14", 5 | "repository": "https://github.com/blackmann/mangobase", 6 | "homepage": "https://degreat.co.uk/mangobase", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "files": [ 10 | "dist/", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "build": "node ./build.js && tsc -p tsconfig.build.json", 15 | "clean": "rm -rf ./dist", 16 | "prepare": "yarn clean && yarn build" 17 | }, 18 | "devDependencies": { 19 | "bun-types": "latest", 20 | "esbuild": "^0.19.3", 21 | "mangobase": "^0.9.14" 22 | }, 23 | "peerDependencies": { 24 | "mangobase": "*", 25 | "typescript": "^5.0.0" 26 | }, 27 | "dependencies": { 28 | "qs": "^6.11.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bun-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'mangobase' 2 | import { context, methodFromHttp } from 'mangobase' 3 | import qs from 'qs' 4 | 5 | const API_PATH_REGEX = /^\/api(?:\/.*)?$/ 6 | const ADMIN_PATH_REGEX = /^\/_(?:\/.*)?$/ 7 | 8 | function bunServer(port = 5000) { 9 | return (app: App) => 10 | Bun.serve({ 11 | async fetch(req, server) { 12 | const url = new URL(req.url) 13 | 14 | if (API_PATH_REGEX.test(url.pathname)) { 15 | const ctx = context({ 16 | // [ ] Handle bad json parse 17 | data: req.body ? await req.json() : null, 18 | headers: req.headers.toJSON(), 19 | method: methodFromHttp(req.method), 20 | path: url.pathname.replace(/^\/api\/?/, ''), 21 | query: qs.parse(url.search.replace(/^\?/, '')), 22 | url: req.url, 23 | }) 24 | 25 | const res = await app.api(ctx) 26 | return new Response(JSON.stringify(res.result), { 27 | headers: { 'Content-Type': 'application/json' }, 28 | status: res.statusCode, 29 | }) 30 | } 31 | 32 | if (ADMIN_PATH_REGEX.test(url.pathname)) { 33 | const [path, queryParams] = url.pathname 34 | .replace(/^\/_/, '') 35 | .split('?') 36 | const file = await app.admin(path || 'index.html') 37 | return new Response(Bun.file(file)) 38 | } 39 | 40 | return new Response(JSON.stringify({ detail: 'Not route found' }), { 41 | status: 404, 42 | }) 43 | }, 44 | port, 45 | }) 46 | } 47 | 48 | export { bunServer } 49 | -------------------------------------------------------------------------------- /bun-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "dist/**/**", 5 | "build.js" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /bun-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "composite": true, 10 | "strict": true, 11 | "downlevelIteration": true, 12 | "skipLibCheck": true, 13 | "jsx": "preserve", 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "allowJs": true, 17 | "types": [ 18 | "bun-types" // add Bun global 19 | ], 20 | "tsBuildInfoFile": "dist/.tsbuildinfo", 21 | "declaration": true, 22 | "emitDeclarationOnly": true, 23 | "outDir": "./dist", 24 | "rootDir": "./src", 25 | "resolveJsonModule": true 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /create-mango/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /create-mango/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /create-mango/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /create-mango/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /create-mango/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-mango", 3 | "version": "0.9.14", 4 | "license": "MIT", 5 | "bin": "dist/cli.js", 6 | "repository": "https://github.com/blackmann/mangobase", 7 | "homepage": "https://degreat.co.uk/mangobase", 8 | "type": "module", 9 | "engines": { 10 | "node": ">=16" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "clean": "rm -rf dist", 15 | "copy-files": "cp -R templates ./dist/templates", 16 | "dev": "tsc --watch", 17 | "test": "prettier --check . && xo && ava", 18 | "prepare": "yarn clean && yarn build && yarn copy-files" 19 | }, 20 | "files": [ 21 | "dist", 22 | "readme.md" 23 | ], 24 | "dependencies": { 25 | "@types/listr": "^0.14.5", 26 | "execa": "^8.0.1", 27 | "ink": "^4.1.0", 28 | "ink-select-input": "^5.0.0", 29 | "ink-text-input": "^5.0.1", 30 | "listr": "^0.14.3", 31 | "make-dir": "^4.0.0", 32 | "meow": "^11.0.0", 33 | "react": "^18.2.0", 34 | "replace-string": "^4.0.0", 35 | "slugify": "^1.6.6" 36 | }, 37 | "devDependencies": { 38 | "@next/env": "^13.4.19", 39 | "@sindresorhus/tsconfig": "^3.0.1", 40 | "@types/react": "^18.0.32", 41 | "ava": "^5.2.0", 42 | "chalk": "^5.2.0", 43 | "eslint-config-xo-react": "^0.27.0", 44 | "eslint-plugin-react": "^7.32.2", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "ink-testing-library": "^3.0.0", 47 | "prettier": "^2.8.7", 48 | "ts-node": "^10.9.1", 49 | "typescript": "^5.0.3", 50 | "xo": "^0.53.1" 51 | }, 52 | "ava": { 53 | "extensions": { 54 | "ts": "module", 55 | "tsx": "module" 56 | }, 57 | "nodeArguments": [ 58 | "--loader=ts-node/esm" 59 | ] 60 | }, 61 | "xo": { 62 | "extends": "xo-react", 63 | "prettier": true, 64 | "rules": { 65 | "react/prop-types": "off" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /create-mango/readme.md: -------------------------------------------------------------------------------- 1 | # create-mango 2 | 3 | > This readme is automatically generated by [create-ink-app](https://github.com/vadimdemedes/create-ink-app) 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ npm install --global create-mango 9 | ``` 10 | 11 | ## CLI 12 | 13 | ``` 14 | $ create-mango --help 15 | 16 | Usage 17 | $ create-mango 18 | 19 | Options 20 | --name Your name 21 | 22 | Examples 23 | $ create-mango --name=Jane 24 | Hello, Jane 25 | ``` 26 | -------------------------------------------------------------------------------- /create-mango/source/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Text, useApp, useInput } from 'ink' 3 | import SelectInput from 'ink-select-input' 4 | import TextInput from 'ink-text-input' 5 | import { 6 | CreateProjectOptions, 7 | Language, 8 | PackageManager, 9 | createProject, 10 | } from './create-project.js' 11 | 12 | const LANGUAGE_SELECT = 1 13 | const PACKAGE_MANAGER_SELECT = 2 14 | const DESTINATION_SELECT = 3 15 | const CONFIRM_OPTIONS = 4 16 | const CREATE_PROJECT = 5 17 | 18 | const languageOptions = [ 19 | { 20 | label: 'Typescript', 21 | value: 'typescript', 22 | }, 23 | { 24 | label: 'Javascript', 25 | value: 'javascript', 26 | }, 27 | ] 28 | 29 | const packageManagerOptions = [ 30 | { 31 | label: 'NPM', 32 | value: 'npm', 33 | }, 34 | { 35 | label: 'Yarn', 36 | value: 'yarn', 37 | }, 38 | ] 39 | 40 | function Wizard() { 41 | const [stage, setStage] = React.useState(LANGUAGE_SELECT) 42 | const languageSelection = React.useRef() 43 | const packageManagerSelection = React.useRef() 44 | const [projectName, setProjectName] = React.useState('') 45 | 46 | useInput((_, key) => { 47 | if (stage !== CONFIRM_OPTIONS) { 48 | return 49 | } 50 | 51 | if (key.return) { 52 | setStage(CREATE_PROJECT) 53 | } 54 | }) 55 | 56 | function handleLanguageSelect({ value }: { value: string }) { 57 | languageSelection.current = value 58 | setStage(PACKAGE_MANAGER_SELECT) 59 | } 60 | 61 | function handlePackageManagerSelect({ value }: { value: string }) { 62 | packageManagerSelection.current = value 63 | setStage(DESTINATION_SELECT) 64 | } 65 | 66 | function handleDestinationSelect() { 67 | if (!projectName.trim().length) { 68 | return 69 | } 70 | 71 | setStage(CONFIRM_OPTIONS) 72 | } 73 | 74 | if (stage === LANGUAGE_SELECT) { 75 | return ( 76 | <> 77 | [🈷️] What language do you prefer? 78 | 79 | 80 | ) 81 | } 82 | 83 | if (stage === PACKAGE_MANAGER_SELECT) { 84 | return ( 85 | <> 86 | [🚌] Package Manager? 87 | 91 | 92 | ) 93 | } 94 | 95 | if (stage === DESTINATION_SELECT) { 96 | return ( 97 | <> 98 | [💽] Enter name of this project 99 | 100 | 101 | App will be created in a folder with this name in this directory.{' '} 102 | 103 | setProjectName(value)} 105 | value={projectName} 106 | onSubmit={handleDestinationSelect} 107 | showCursor 108 | /> 109 | 110 | ) 111 | } 112 | 113 | if (stage === CONFIRM_OPTIONS) { 114 | return ( 115 | <> 116 | [🚪] Creating project with the following options 117 | 118 | {' '} 119 | Language: {languageSelection.current} 120 | 121 | 122 | {' '} 123 | Project name: {projectName} 124 | 125 | Press enter to confirm 126 | 127 | ) 128 | } 129 | 130 | return ( 131 | 136 | ) 137 | } 138 | 139 | function CreateProject(options: CreateProjectOptions) { 140 | const { exit } = useApp() 141 | 142 | React.useEffect(() => { 143 | createProject(options) 144 | .then(() => { 145 | console.log('') 146 | console.log('🌵 Done setting up project.') 147 | console.log('') 148 | console.log('Start project with `npm run dev`') 149 | exit() 150 | }) 151 | .catch((error) => { 152 | console.error('') 153 | console.error(`❌ ${error.message}`) 154 | exit(error) 155 | }) 156 | }, [options]) 157 | 158 | return null 159 | } 160 | 161 | export default function App() { 162 | return 163 | } 164 | -------------------------------------------------------------------------------- /create-mango/source/cli.tsx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import React from 'react' 3 | import { render } from 'ink' 4 | import meow from 'meow' 5 | import App from './app.js' 6 | 7 | meow( 8 | ` 9 | Usage 10 | $ create-mango 11 | 12 | Examples 13 | $ create-mango awesome-app 14 | `, 15 | { 16 | importMeta: import.meta, 17 | booleanDefault: undefined, 18 | } 19 | ) 20 | 21 | render() 22 | -------------------------------------------------------------------------------- /create-mango/templates/_common/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mongodb://127.0.0.1:27017/%NAME%-db 2 | SECRET_KEY=%SECRET_KEY% 3 | -------------------------------------------------------------------------------- /create-mango/templates/_common/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine as build 2 | 3 | COPY . . 4 | RUN yarn install --frozen-lockfile 5 | RUN yarn build 6 | 7 | FROM node:18-alpine as production 8 | 9 | WORKDIR /app 10 | 11 | ENV NODE_ENV=production 12 | 13 | COPY --from=build package.json . 14 | COPY --from=build dist/ dist/ 15 | 16 | RUN yarn install --frozen-lockfile --production 17 | 18 | # copies admin static files 19 | # [ ] Use symlinks instead of copying files 20 | COPY --from=build node_modules/mangobase/dist/admin dist/admin 21 | 22 | EXPOSE 5000 23 | ENV PORT=5000 24 | 25 | CMD ["node", "dist/index.js"] 26 | -------------------------------------------------------------------------------- /create-mango/templates/_common/_gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /create-mango/templates/_common/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /create-mango/templates/_common/_prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /create-mango/templates/_common/readme.md: -------------------------------------------------------------------------------- 1 | # %NAME% 2 | 3 | This is a [Mangobase](https://degreat.co.uk/mangobase) project. 4 | 5 | Run in development with: 6 | 7 | ```sh 8 | npm run dev 9 | ``` 10 | 11 | See [here](https://degreat.co.uk/mangobase/guide/dev-prod) for production guide. 12 | -------------------------------------------------------------------------------- /create-mango/templates/js/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%NAME%", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.mjs", 6 | "scripts": { 7 | "dev": "tsx watch src/index.mjs", 8 | "start": "node src/index.mjs" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC" 13 | } 14 | -------------------------------------------------------------------------------- /create-mango/templates/js/src/index.mjs: -------------------------------------------------------------------------------- 1 | import { App } from 'mangobase' 2 | import { MongoDb } from '@mangobase/mongodb' 3 | import { expressServer } from '@mangobase/express' 4 | import env from '@next/env' 5 | 6 | env.loadEnvConfig('.', process.env.NODE_ENV !== 'production') 7 | 8 | const app = new App({ 9 | db: new MongoDb(process.env.DATABASE_URL), 10 | }) 11 | 12 | const PORT = process.env.PORT || 3000 13 | 14 | app.serve(expressServer).listen(PORT, () => { 15 | console.log(`App started`) 16 | console.log(`API base url: http://localhost:${PORT}/api/`) 17 | console.log(`Admin: http://localhost:${PORT}/_/`) 18 | }) 19 | -------------------------------------------------------------------------------- /create-mango/templates/ts/_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "%NAME%", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx watch src/index.ts", 9 | "build": "node build.js", 10 | "start": "node dist/index.js" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /create-mango/templates/ts/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | esbuild.build({ 4 | entryPoints: ['src/index.ts'], 5 | bundle: true, 6 | outfile: 'dist/index.js', 7 | format: 'esm', 8 | platform: 'node', 9 | target: 'node18', 10 | external: [ 11 | '@mangobase/express', 12 | '@mangobase/mongodb', 13 | '@next/env', 14 | 'express', 15 | 'mangobase', 16 | 'mongodb', 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /create-mango/templates/ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'mangobase' 2 | import { MongoDb } from '@mangobase/mongodb' 3 | import { expressServer } from '@mangobase/express' 4 | import env from '@next/env' 5 | 6 | env.loadEnvConfig('.', process.env.NODE_ENV !== 'production') 7 | 8 | const app = new App({ 9 | db: new MongoDb(process.env.DATABASE_URL!), 10 | }) 11 | 12 | const PORT = process.env.PORT || 3000 13 | 14 | app.serve(expressServer).listen(PORT, () => { 15 | console.log(`App started`) 16 | console.log(`API base url: http://localhost:${PORT}/api/`) 17 | console.log(`Admin: http://localhost:${PORT}/_/`) 18 | }) 19 | -------------------------------------------------------------------------------- /create-mango/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "types": ["react"] 6 | }, 7 | "include": ["source"] 8 | } 9 | -------------------------------------------------------------------------------- /eslint-config-base/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'preact', 11 | 'prettier', 12 | ], 13 | overrides: [], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | plugins: ['@typescript-eslint', 'react', 'prettier'], 20 | rules: { 21 | 'no-unused-vars': 'off', 22 | 'no-dupe-class-members': 'off', 23 | 'no-duplicate-imports': 'off', 24 | 'prettier/prettier': 'warn', 25 | 'sort-keys': 'warn', 26 | 'sort-imports': 'warn', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'off', 29 | '@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true }], 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /eslint-config-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-base", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"✅ All good\"" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@typescript-eslint/eslint-plugin": "^5.59.5", 14 | "@typescript-eslint/parser": "^5.59.5", 15 | "eslint": "^8.0.1", 16 | "eslint-config-preact": "^1.3.0", 17 | "eslint-config-prettier": "^8.8.0", 18 | "eslint-plugin-import": "^2.25.2", 19 | "eslint-plugin-n": "^15.0.0", 20 | "eslint-plugin-prettier": "^4.2.1", 21 | "eslint-plugin-promise": "^6.0.0", 22 | "jest": "^29.6.0", 23 | "typescript": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/base-bun-mongo/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | -------------------------------------------------------------------------------- /examples/base-bun-mongo/README.md: -------------------------------------------------------------------------------- 1 | # base-bun-mongo 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run src/index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v0.8.1. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /examples/base-bun-mongo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base-bun-mongo", 3 | "module": "src/index.ts", 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "bun src/index.ts" 8 | }, 9 | "dependencies": { 10 | "mangobase": "*", 11 | "@mangobase/mongodb": "*", 12 | "jose": "4.14.6" 13 | }, 14 | "devDependencies": { 15 | "bun-types": "latest" 16 | }, 17 | "peerDependencies": { 18 | "typescript": "^5.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/base-bun-mongo/src/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'mangobase' 2 | import { MongoDb } from '@mangobase/mongodb' 3 | import { bunServer } from '@mangobase/bun' 4 | 5 | process.env.SECRET_KEY = 'mango-bun' 6 | const app = new App({ 7 | db: new MongoDb('mongodb://localhost:27017/bun-mango'), 8 | }) 9 | 10 | const PORT = 4000 11 | 12 | app.serve(bunServer(PORT)) 13 | console.log(`App started`) 14 | console.log(`API base url: http://localhost:${PORT}/api/`) 15 | console.log(`Admin: http://localhost:${PORT}/_/`) 16 | -------------------------------------------------------------------------------- /examples/base-bun-mongo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "preserve", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/base-express-mongo/.gitignore: -------------------------------------------------------------------------------- 1 | .mangobase 2 | dist/ 3 | -------------------------------------------------------------------------------- /examples/base-express-mongo/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'mangobase' 2 | import { MongoDb } from '@mangobase/mongodb' 3 | import { expressServer } from '@mangobase/express' 4 | 5 | const app = new App({ 6 | db: new MongoDb('mongodb://127.0.0.1:27017/mangobase-demo'), 7 | }) 8 | 9 | app.hooksRegistry.register({ 10 | id: 'throttle', 11 | name: 'Throttle', 12 | description: 'Adds artificial latency to requests', 13 | configSchema: { 14 | delay: { type: 'number', required: true, defaultValue: 200 }, 15 | prod_only: { type: 'boolean', defaultValue: true }, 16 | except_email: { type: 'string' }, 17 | }, 18 | async run(ctx, config) { 19 | // add delay 20 | return ctx 21 | }, 22 | }) 23 | 24 | process.env.SECRET_KEY = 'test-key' 25 | const PORT = process.env.PORT || 3000 26 | 27 | app.serve(expressServer).listen(PORT, () => { 28 | console.log(`App started`) 29 | console.log(`API base url: http://localhost:${PORT}/api/`) 30 | console.log(`Admin: http://localhost:${PORT}/_/`) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/base-express-mongo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base-express-mongo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx watch index.ts", 9 | "build": "npx esbuild index.ts --platform=node --format=esm --target=node18 --outfile=dist/index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@mangobase/express": "*", 16 | "@mangobase/mongodb": "*", 17 | "mangobase": "*" 18 | }, 19 | "devDependencies": { 20 | "tsx": "^4.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /express-server/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /express-server/README.md: -------------------------------------------------------------------------------- 1 | # @mangobase/express 2 | 3 | Express server adapter for mangobase. 4 | -------------------------------------------------------------------------------- /express-server/build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | esbuild.build({ 4 | bundle: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['express', 'mangobase', 'cors'], 7 | format: 'esm', 8 | outdir: 'dist/', 9 | platform: 'node', 10 | }) 11 | -------------------------------------------------------------------------------- /express-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mangobase/express", 3 | "version": "0.9.14", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/blackmann/mangobase", 7 | "homepage": "https://degreat.co.uk/mangobase", 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "clean": "rm -rf dist", 15 | "build": "node ./build.mjs && tsc -p tsconfig.build.json", 16 | "prepare": "yarn build" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "cors": "^2.8.5" 23 | }, 24 | "devDependencies": { 25 | "@types/cors": "^2.8.13", 26 | "@types/express": "^4.17.17", 27 | "esbuild": "^0.17.19", 28 | "express": "^4.18.2", 29 | "mangobase": "^0.9.14" 30 | }, 31 | "peerDependencies": { 32 | "express": "^4.18.2", 33 | "mangobase": "*" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /express-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { type App, context, methodFromHttp } from 'mangobase' 3 | import cors from 'cors' 4 | 5 | /** 6 | * No-op server adapter for express 7 | */ 8 | function expressServer(mangobaseApp: App): express.Express { 9 | const app = express() 10 | 11 | app.use(cors()) 12 | app.use(express.json()) 13 | app.use(express.urlencoded({ extended: true })) 14 | 15 | return withExpress(app, mangobaseApp) 16 | } 17 | 18 | /** 19 | * Use this function if you have a custom express instance. Remember to set `cors`, `json` and `urlencoded`. 20 | * These are critical to how to how mangobase works. 21 | * 22 | * @example 23 | * ``` 24 | * import cors from 'cors' 25 | * 26 | * const app = express() 27 | * app.use(cors()) 28 | * app.use(express.json()) 29 | * app.use(express.urlencoded({ extended: true })) 30 | * ``` 31 | */ 32 | function withExpress(app: express.Express, mangobaseApp: App): express.Express { 33 | app.all(['/api', '/api/*'], (req, res) => { 34 | // [ ]: handle OPTIONS 35 | const ctx = context({ 36 | data: req.body, 37 | headers: req.headers as Record, 38 | method: methodFromHttp(req.method), 39 | path: req.path.replace(/^\/api\/?/, ''), 40 | query: req.query, 41 | url: req.url, 42 | }) 43 | 44 | mangobaseApp.api(ctx).then((context) => { 45 | res.status(context.statusCode || 200).json(context.result) 46 | }) 47 | }) 48 | 49 | app.get(['/_', '/_/*'], (req, res) => { 50 | const [path, queryParams] = req.url.replace(/^\/_/, '').split('?') 51 | 52 | mangobaseApp.admin(path || 'index.html').then((file) => { 53 | res.sendFile(file) 54 | }) 55 | }) 56 | 57 | return app 58 | } 59 | 60 | export { expressServer, withExpress } 61 | -------------------------------------------------------------------------------- /express-server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "dist/**/**" 5 | ] 6 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.9.14", 4 | "packages": [ 5 | "base", 6 | "create-mango", 7 | "express-server", 8 | "mongo-db", 9 | "bun-server" 10 | ], 11 | "npmClient": "yarn" 12 | } 13 | -------------------------------------------------------------------------------- /mongo-db/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["base"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mongo-db/.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | dist/ 3 | -------------------------------------------------------------------------------- /mongo-db/README.md: -------------------------------------------------------------------------------- 1 | # @mangobase/mongodb 2 | 3 | MongoDb database adapter for mangobase. 4 | -------------------------------------------------------------------------------- /mongo-db/build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | esbuild.build({ 4 | bundle: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['mongodb', 'mangobase'], 7 | format: 'esm', 8 | outdir: 'dist/', 9 | platform: 'node', 10 | }) 11 | -------------------------------------------------------------------------------- /mongo-db/global.setup.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server-core' 2 | 3 | let mongod: MongoMemoryServer 4 | 5 | export async function setup() { 6 | console.log('Starting up mongo server') 7 | mongod = await MongoMemoryServer.create() 8 | process.env.MONGO_URL = mongod.getUri('test') 9 | 10 | console.log('MONGO_URL', process.env.MONGO_URL) 11 | } 12 | 13 | export async function teardown() { 14 | mongod?.stop() 15 | } 16 | -------------------------------------------------------------------------------- /mongo-db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mangobase/mongodb", 3 | "version": "0.9.14", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "repository": "https://github.com/blackmann/mangobase", 8 | "homepage": "https://degreat.co.uk/mangobase", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "scripts": { 14 | "clean": "rm -rf dist", 15 | "build": "node ./build.mjs && tsc -p tsconfig.build.json", 16 | "prepare": "yarn build", 17 | "test": "vitest -w false" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@vitest/coverage-c8": "^0.31.0", 24 | "@vitest/ui": "^0.31.0", 25 | "eslint-config-base": "*", 26 | "mangobase": "^0.9.14", 27 | "mongodb": "^6.3.0", 28 | "mongodb-memory-server-core": "^8.12.2", 29 | "typescript": "^5.0.4", 30 | "vitest": "^0.31.1" 31 | }, 32 | "peerDependencies": { 33 | "mangobase": "*", 34 | "mongodb": "^6.3.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mongo-db/src/index.ts: -------------------------------------------------------------------------------- 1 | export { MongoDb, MongoCursor } from './mongodb.js' 2 | -------------------------------------------------------------------------------- /mongo-db/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "global.setup.ts", 5 | "vitest.config.ts", 6 | "src/**/*.test.ts", 7 | "dist/**/**" 8 | ] 9 | } -------------------------------------------------------------------------------- /mongo-db/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | test: { 6 | clearMocks: true, 7 | coverage: { 8 | enabled: true, 9 | }, 10 | globalSetup: ['global.setup.ts'], 11 | reporters: ['default', 'html'], 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mangobase-monorepo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "boot": "yarn install && cd create-mango && yarn install", 9 | "build:admin": "yarn workspace @mangobase/admin run build", 10 | "build:base": "yarn workspace mangobase run build", 11 | "build:bun": "yarn workspace @mangobase/bun run build", 12 | "build:express-server": "yarn workspace @mangobase/express run build", 13 | "build:mongodb": "yarn workspace @mangobase/mongodb run build", 14 | "build:website": "yarn workspace website run build", 15 | "build": "yarn turbo build --filter=!website && yarn turbo build --filter=website && yarn turbo build --filter=@mangobase/admin", 16 | "clean": "yarn turbo clean", 17 | "dev:admin": "yarn workspace @mangobase/admin run dev", 18 | "dev:cli": "cd create-mango && yarn dev", 19 | "dev:express-mongo": "yarn workspace base-express-mongo run dev", 20 | "dev:bun-mongo": "yarn workspace base-bun-mongo run dev", 21 | "dev:website": "yarn workspace website run dev", 22 | "docs:generate": "yarn workspace website run docs", 23 | "publish:base": "yarn workspace mangobase publish", 24 | "publish:bun": "yarn workspace @mangobase/bun publish --access public", 25 | "publish:cli": "cd create-mango && yarn publish --access public", 26 | "publish:express": "yarn workspace @mangobase/express publish --access public", 27 | "publish:mongodb": "yarn workspace @mangobase/mongodb publish --access public", 28 | "test:base": "yarn workspace mangobase run test", 29 | "test:mongodb": "yarn workspace @mangobase/mongodb run test", 30 | "test": "yarn test:base && yarn test:mongodb" 31 | }, 32 | "keywords": [], 33 | "author": "", 34 | "license": "MIT", 35 | "workspaces": { 36 | "packages": [ 37 | "admin", 38 | "base", 39 | "bun-server", 40 | "eslint-config-base", 41 | "examples/*", 42 | "express-server", 43 | "mongo-db", 44 | "website" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "lerna": "^7.3.0", 49 | "prettier": "^2.8.8", 50 | "turbo": "^1.10.3" 51 | }, 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "clean": { 5 | "cache": false 6 | }, 7 | "build": { 8 | "outputs": ["dist/**", "api/**"], 9 | "dependsOn": ["^build"] 10 | }, 11 | "copy-admin": { 12 | "dependsOn": ["build"], 13 | "outputs": ["base/dist/admin/**"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/cache 2 | .vitepress/dist 3 | .vitepress/api-paths.json 4 | 5 | api/ 6 | !api/index.md -------------------------------------------------------------------------------- /website/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import apiPaths from './api-paths.json' 3 | 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | title: 'Mango 🥭', 7 | description: 'Mangobase: Low-code Javscript backend framework', 8 | base: '/mangobase/', 9 | sitemap: { 10 | hostname: 'https://degreat.co.uk/mangobase/', 11 | lastmodDateOnly: true, 12 | }, 13 | themeConfig: { 14 | // https://vitepress.dev/reference/default-theme-config 15 | nav: [ 16 | { text: 'Home', link: '/' }, 17 | { text: 'Guide', link: '/guide/' }, 18 | { text: 'API', link: '/api/' }, 19 | ], 20 | 21 | sidebar: { 22 | '/guide/': [ 23 | { 24 | text: 'Start here', 25 | items: [ 26 | { text: 'Introduction', link: '/guide/' }, 27 | { text: 'Getting started', link: '/guide/getting-started' }, 28 | { text: 'Recap on REST', link: '/guide/rest' }, 29 | { text: 'Dashboard', link: '/guide/dashboard' }, 30 | { text: 'Deployment', link: '/guide/deployment' }, 31 | ], 32 | }, 33 | { 34 | text: 'Concepts', 35 | items: [ 36 | { text: 'Context', link: '/guide/context' }, 37 | { text: 'Hooks', link: '/guide/hooks' }, 38 | { text: 'Paths & Queries', link: '/guide/query' }, 39 | { text: 'Migrations', link: '/guide/migrations' }, 40 | ], 41 | }, 42 | { 43 | text: 'Extras', 44 | items: [ 45 | { text: 'Authentication', link: '/guide/authentication' }, 46 | { text: 'Server adapters', link: '/guide/server-adapters' }, 47 | { text: 'Database adapters', link: '/guide/database-adapters' }, 48 | { text: 'Version control', link: '/guide/version-control' }, 49 | { text: 'Plugins', link: '/guide/plugins' }, 50 | { text: 'FAQs', link: '/guide/faqs' }, 51 | ], 52 | }, 53 | ], 54 | '/api/': apiPaths, 55 | }, 56 | 57 | socialLinks: [ 58 | { icon: 'github', link: 'https://github.com/blackmann/mangobase' }, 59 | ], 60 | 61 | search: { 62 | provider: 'local', 63 | }, 64 | outline: [2, 4], 65 | }, 66 | head: [ 67 | [ 68 | 'meta', 69 | { 70 | property: 'og:image', 71 | content: 'https://github.com/blackmann/mangobase/raw/master/assets/ss-dark.png' 72 | } 73 | ], 74 | [ 75 | 'script', 76 | { 77 | async: '', 78 | src: 'https://analytics.umami.is/script.js', 79 | 'data-website-id': '6cc8bf19-147d-45a2-b7c9-75b7c1b607bf', 80 | }, 81 | ], 82 | [ 83 | 'script', 84 | { 85 | async: '', 86 | src: 'https://0.observe.so/script.js', 87 | 'data-app': 'clws46cwl00go8k13jz4apght', 88 | }, 89 | ], 90 | ], 91 | }) 92 | -------------------------------------------------------------------------------- /website/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import DefaultTheme from 'vitepress/theme' 3 | import MangoSpline from './mango-spline.vue' 4 | import './styles.css' 5 | 6 | export default { 7 | extends: DefaultTheme, 8 | Layout() { 9 | return h(DefaultTheme.Layout, null, { 10 | 'home-hero-image': () => h(MangoSpline) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /website/.vitepress/theme/mango-spline.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /website/.vitepress/theme/styles.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 960px) { 2 | .VPHero .image { 3 | opacity: 0; 4 | margin-top: -260px; 5 | } 6 | } 7 | 8 | .hidden { 9 | display: none; 10 | } 11 | -------------------------------------------------------------------------------- /website/api/index.md: -------------------------------------------------------------------------------- 1 | # Modules -------------------------------------------------------------------------------- /website/generate-api.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import fsSync from 'fs' 3 | import typedoc, { Comment } from 'typedoc' 4 | 5 | const projects = ['base', 'express-server', 'bun-server', 'mongo-db'] 6 | 7 | const apiDocPaths = [] 8 | 9 | const SKIP_KINDS = [typedoc.ReflectionKind.TypeAlias] 10 | 11 | async function generateApis() { 12 | for (const projectDir of projects) { 13 | const app = await typedoc.Application.bootstrap({ 14 | entryPoints: [`../${projectDir}/src/index.ts`], 15 | exclude: ["**/*+(.spec|.test).ts"], 16 | tsconfig: `../${projectDir}/tsconfig.build.json`, 17 | excludePrivate: true, 18 | }) 19 | 20 | console.log('[-] Parsing ' + projectDir + '...') 21 | 22 | const pkg = JSON.parse( 23 | await fs.readFile(`../${projectDir}/package.json`, { encoding: 'utf-8' }) 24 | ) 25 | 26 | const group = { 27 | collapsed: true, 28 | text: pkg.name, 29 | items: [], 30 | } 31 | 32 | try { 33 | await fs.mkdir(`./api/${projectDir}/`) 34 | } catch (err) { 35 | // 36 | } 37 | 38 | const project = await app.convert() 39 | if (project) { 40 | project.traverse((node) => { 41 | if (SKIP_KINDS.includes(node.kind)) { 42 | return 43 | } 44 | 45 | console.log(' * ', `${node.name}.md`) 46 | let path = `/api/${projectDir}/${node.name}` 47 | 48 | if (node.name[0] === node.name[0].toLocaleLowerCase()) { 49 | // doing this because some function names clashed with interface/class 50 | // names, causing an override of content. 51 | path = `/api/${projectDir}/${node.name}_function` 52 | } 53 | 54 | group.items.push({ 55 | text: node.name, 56 | link: path, 57 | }) 58 | 59 | fsSync.writeFileSync(`.${path}.md`, composeDoc(node, projectDir), { 60 | encoding: 'utf-8', 61 | }) 62 | }) 63 | } 64 | 65 | group.items.sort((a, b) => (a.text > b.text ? 1 : -1)) 66 | 67 | apiDocPaths.push(group) 68 | } 69 | } 70 | 71 | generateApis().then(() => { 72 | console.log('[x] Completed API generation') 73 | fsSync.writeFileSync( 74 | '.vitepress/api-paths.json', 75 | JSON.stringify(apiDocPaths, null, 2) 76 | ) 77 | }) 78 | 79 | function composeDoc(node, packagePath) { 80 | const urlGenerator = (ref) => urlTo(ref, packagePath) 81 | 82 | const content = [ 83 | `# ${node.name}`, 84 | `${typedoc.ReflectionKind.singularString(node.kind)}`, 85 | ] 86 | 87 | if (node.comment) { 88 | content.push(Comment.displayPartsToMarkdown(node.comment.summary, urlGenerator)) 89 | } 90 | 91 | node.traverse((innerNode) => { 92 | // [ ] Show signature if any 93 | // [ ] Show item type (string, number, etc.) 94 | if (SKIP_KINDS.includes(innerNode.kind)) { 95 | return 96 | } 97 | 98 | const innerContent = [] 99 | 100 | innerContent.push(`## ${innerNode.name}`) 101 | if (innerNode.comment) { 102 | innerContent.push( 103 | Comment.displayPartsToMarkdown(innerNode.comment.summary, urlGenerator) 104 | ) 105 | } 106 | 107 | switch (innerNode.kind) { 108 | case typedoc.ReflectionKind.Method: { 109 | innerNode.signatures.forEach((signature) => { 110 | const parameters = [] 111 | 112 | for (const p of signature.parameters) { 113 | parameters.push( 114 | `${p.name}: ${typedoc.ReflectionKind.singularString(p.kind)}` 115 | ) 116 | } 117 | 118 | const signatureCode = ` 119 | \`\`\`typescript 120 | public ${signature.name}(${parameters.join(', ')}): Promise 121 | \`\`\` 122 | ` 123 | innerContent.push(signatureCode) 124 | 125 | if (signature.hasComment()) { 126 | innerContent.push( 127 | Comment.displayPartsToMarkdown(signature.comment.summary, (ref) => 128 | urlTo(ref, packagePath) 129 | ) 130 | ) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | content.push(innerContent.join('\n')) 137 | }) 138 | 139 | return content.join('\n\n') 140 | } 141 | 142 | function urlTo(ref, base) { 143 | const parts = [] 144 | parts.push(ref.name) 145 | 146 | let parent = ref.parent 147 | while (parent) { 148 | parts.push(parent.name) 149 | parent = parent.parent 150 | } 151 | 152 | parts.reverse() 153 | parts.shift() 154 | 155 | return `/api/${base}/` + parts.join('#') 156 | } 157 | -------------------------------------------------------------------------------- /website/guide/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Mangobase comes with a basic built-in authentication [plugin](/guide/plugins). It provides a simple way to register and authenticate users. 4 | 5 | To create a new user, you make a `POST` (aka `create`) request to the `/api/users` endpoint with body: 6 | 7 | ```json 8 | { 9 | "email": "carlson@gmail.com", 10 | "username": "carlson", 11 | "password": "gocarl" 12 | } 13 | ``` 14 | 15 | You can then login by making a `POST` request to `/api/login` with body: 16 | 17 | ```json 18 | { 19 | "username": "carlson", 20 | // or "email": "carlson@gmail", 21 | "password": "gocarl" 22 | } 23 | ``` 24 | 25 | You get a response in the format: 26 | 27 | ```json 28 | { 29 | "user": { 30 | "id": "5f1e5a8b9d6b2b0017b4e6b1", 31 | "username": "carlson", 32 | "email": "carlson@gmail.com", 33 | }, 34 | "auth": { 35 | "type": "Bearer", 36 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." 37 | } 38 | } 39 | ``` 40 | 41 | When making authenticated requests using the built-in method, you add an `Authorization` header with the value `${auth.type} ${auth.token}`. 42 | 43 | ## Custom authentication 44 | 45 | You can choose to implement your own authentication process, like using an auth library or service. To do this, you can create a [plugin](/guide/plugins). See this [implementation](https://github.com/blackmann/mangobase/blob/de9281da1e840ed28acbf715f8417f29b3da56dc/base/src/authentication.ts#L115) as a reference. 46 | -------------------------------------------------------------------------------- /website/guide/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | Context describes a single request to the app. After processing a request, the app returns a context containing a [`result`](/api/base/Context#result). 4 | 5 | How _processing_ works is described as a pipeline. A pipeline runs in the following sequence: 6 | 7 | 1. Runs all `before` hooks [in sequence] installed on the app 8 | 1. Runs all `before` hooks [in sequence] installed on the service 9 | 1. Passes `context` to the service to handle. 10 | 1. The result from the service is passed [in sequence] to all `after` hooks installed on the service 11 | 1. Runs all `after` hooks [in sequence] installed on the app 12 | 13 | The current state of the context is passed with every call to a hook in the pipeline or the service. 14 | 15 | ::: info 16 | Subsequent `before` hooks stop executing the moment `context.result` is set to a non-null value, then skips the service and proceeds with all `after` hooks. 17 | ::: 18 | 19 | See the structure of a context [here](/api/base/Context). 20 | 21 | A context is normally created by the [server](/guide/server-adapters) and passed to the app with [`app.api(ctx)`](/api/base/App#api) 22 | 23 | ## Methods 24 | 25 | In Mangobase, the following methods are used. It exists in the context as `ctx.method`. 26 | 27 | ### create 28 | 29 | The equivalent HTTP method is `POST`. It is used to create a new resource. When you make a `POST` request to `/songs` for example, this is a `create` request. 30 | 31 | Using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), this will look like: 32 | 33 | ```javascript{7} 34 | // The API endpoint is [conventionally] prefixed with `/api` 35 | const data = { 36 | title: 'Hello', 37 | artist: 'Adele', 38 | } 39 | 40 | fetch('/api/songs', { 41 | method: 'POST', 42 | body: JSON.stringify(data), 43 | }) 44 | ``` 45 | 46 | A successful call to `create` will return a `201` status code with the created resource in the response body. 47 | 48 | ### find 49 | 50 | The equivalent HTTP method is `GET`. It is used to retrieve a list of resources. A `GET` request to `/songs` for example is a `find` request. This method is intended to always return a paginated response in the following format: 51 | 52 | ```typescript 53 | interface PaginatedResponse { 54 | data: any[] 55 | total: number 56 | limit: number 57 | skip: number 58 | } 59 | ``` 60 | 61 | In frontend code, this will look like: 62 | 63 | ```javascript 64 | fetch('/api/songs', { 65 | method: 'GET', 66 | }) 67 | ``` 68 | 69 | The response will look like: 70 | 71 | ```json 72 | { 73 | "data": [ 74 | { 75 | "id": 1, 76 | "title": "Hello", 77 | "artist": "Adele" 78 | }, 79 | { 80 | "id": 2, 81 | "title": "Someone Like You", 82 | "artist": "Adele" 83 | } 84 | ], 85 | "total": 2, 86 | "limit": 10, 87 | "skip": 0 88 | } 89 | ``` 90 | 91 | If you wanted to limit the number of items returned, you can pass a `limit` query parameter. For example, `/songs?$limit=1` will return only one item. 92 | 93 | Pagination is done using the `skip` query parameter. For example, `/songs?$skip=5` will skip the first 5 items. 94 | 95 | :::info 96 | You may notice a `$` prefix on the query parameter. This is to prevent conflicts with the fields of a collection. For example, if you have a `limit` field on your collection, you can make a request to `/?limit=100km&$limit=3` _find_ items with a limit of 100km but only return 3 items. 97 | ::: 98 | 99 | ### get 100 | 101 | Making a `GET` request to `/songs/1` for example is `get`. This method is intended to always return a single item. 102 | 103 | In frontend code, this will look like: 104 | 105 | ```javascript 106 | fetch('/api/songs/1', { 107 | method: 'GET', 108 | }) 109 | ``` 110 | 111 | A status code of `404` will be returned if the item is not found. 112 | 113 | ### update 114 | 115 | The equivalent HTTP method is `PATCH`. It is used to update a single resource. For example, when you make a `PATCH` request to `/songs/1`. 116 | 117 | ### remove 118 | 119 | The equivalent HTTP method is `DELETE`. A `DELETE` request to `/songs/1` for example, this is a `remove` request. Note that this requires the `id` of the resource to be passed as a parameter. 120 | 121 | :::tip 122 | All these methods can accept a query parameter. For example, with `find`, you can make a GET `/songs?title=hello` request to search for songs with the title `hello`. 123 | ::: 124 | 125 | ## Invalid requests 126 | 127 | As far as Mangobase is concerned, some requests like the following are not supported: 128 | 129 | - POST `/songs/1` - You cannot create an item on a detail path. 130 | - PATCH `/songs` - You cannot patch a base path. 131 | - DELETE `/songs` - You cannot delete a base path. 132 | - PUT - Mangobase does not support `PUT` requests. 133 | -------------------------------------------------------------------------------- /website/guide/dashboard.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | 3 | The dashboard is central to how you use Mangobase. It offers a very intuitive experience. 4 | 5 | ## Collections 6 | 7 | This page is where you see all your collections, add new ones or edit existing ones. Selecting a collection allows you to see all the data that has been created (in a paginated format). 8 | 9 | It also provides a filter/query input to help you search for specific data. 10 | 11 | :::info 12 | The query filter, at the moment, is a work-in-progress 13 | ::: 14 | 15 | ### Hooks editor 16 | 17 | The hooks editor allows you to compose before and after hooks for your collection's service. It is a very intuitive and creative way to extend the functionality of your service. 18 | 19 | You can select from a number of pre-installed plugin hooks or [write your own](/guide/hooks#registering-a-plugin-hook). 20 | 21 | See [hooks](/guide/hooks) for more information. Below is a video demonstrating how to use the hooks editor. 22 | 23 | 24 | 25 | ## Logs 26 | 27 | There is a page to show request logs. When a request is not _ok_, the error data is logged and you can see it here. 28 | 29 | ## Settings 30 | 31 | The settings page allows you to configure your app. At the moment, it only shows the developers added to the project and your profile. 32 | -------------------------------------------------------------------------------- /website/guide/database-adapters.md: -------------------------------------------------------------------------------- 1 | # Database adapters 2 | 3 | Mangobase is designed to be database agnostic. This means you can implement any [Database](/api/base/Database) for your app. However, there's an official support for just Mongo at the moment. 4 | 5 | :::info 6 | You're welcome to author a database adapter for your favorite database and share with the community. [Here](https://github.com/blackmann/mangobase/blob/master/mongo-db/src/mongodb.ts) is an implementation of the MongoDb adapter. 7 | ::: 8 | -------------------------------------------------------------------------------- /website/guide/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | :::warning 4 | It's not advisable to make changes to schemas, hooks, or other configuration in production. If you need to make changes, it's best to do so in a development environment and then deploy the changes to production. 5 | ::: 6 | 7 | ## Dokku 8 | 9 | If you boostrapped the app with the CLI, you can deploy to your Dokku instance/app with the following command: 10 | 11 | ```bash 12 | git push dokku master 13 | ``` 14 | 15 | :::info 16 | Dokku is a self-hosted Heroku alternative. You can read more about it [here](https://dokku.com). 17 | ::: 18 | 19 | Set the proxy port if your app is not receiving requests: 20 | 21 | ```bash 22 | dokku ports:set http:80:5000 23 | ``` 24 | 25 | ## Docker-based deploys 26 | 27 | You can deploy your Mangobase app using Docker. When you use the [Mangobase CLI](/guide/getting-started#starting-a-new-project), it will generate a `Dockerfile` for you. You can then build and run the Docker image: 28 | 29 | ```bash 30 | $ docker build -t my-mangobase-app . 31 | $ docker run -p 8000:5000 my-mangobase-app --env=SECRET_KEY=xA90boi2 --env=DATABASE_URL=mongodb://host.docker.internal:27017/mangobase-app-db 32 | ``` 33 | 34 | ## Deploying to Heroku 35 | 36 | Add a `heroku.yml` file with the following content: 37 | 38 | ```yaml 39 | build: 40 | docker: 41 | web: Dockerfile 42 | ``` 43 | 44 | Then make a `git push` to your project. 45 | 46 | Remember to do set the `SECRET_KEY` and `DATABASE_URL` environment variables in your Heroku app. 47 | 48 | ```bash 49 | $ heroku config:set SECRET_KEY=xA90boi2 DATABASE_URL=mongodb://host.docker.internal:27017/mangobase-app-db 50 | ``` 51 | 52 | Or you can use the [Heroku dashboard](https://devcenter.heroku.com/articles/config-vars#using-the-heroku-dashboard) to set the environment variables. 53 | 54 | Read more from [here](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml) on how to deploy Dockerfile based apps to Heroku. 55 | -------------------------------------------------------------------------------- /website/guide/faqs.md: -------------------------------------------------------------------------------- 1 | # FAQs 2 | 3 | ## Who is this for? 4 | 5 | - Beginners who want to ease into backend development. 6 | - Fronted [focused] developers who want to build a backend for their app. 7 | - Fullstack developers who want to quickly bootstrap a backend for their app without having to write boilerplate code. 8 | 9 | Though Mangobase is low-code and easy to use, it is also very flexible and can be used to build complex applications. 10 | 11 | ## How do I make requests to my API? 12 | 13 | You can use any HTTP client to make requests to your API. For example, you can use [HTTPie](https://httpie.io) or [Postman](https://www.postman.com). 14 | 15 | Inside your frontend app, you can use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or [Axios](https://axios-http.com), etc. 16 | 17 | If you added a collection with name `songs` for example, you can make a `GET` request to `http://localhost:3000/api/songs` to get all songs. You can also make a `POST` request to `http://localhost:3000/api/songs` to create a new song. 18 | 19 | Other ways to interact with your API can be found in [Context methods](/guide/context#methods). 20 | -------------------------------------------------------------------------------- /website/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Mangobase is fundamentally designed to be easy to use while providing a cohesion of developer experience; that said, it's easy to get started. 4 | 5 | :::info 6 | We currently provide a database adapter for Mongo only, so you may need to install [Mongo](https://mongodb.com) to be able to use Mango. You can however implement a custom database adapter for any database of choice. See [database adapters](/guide/database-adapters) 7 | ::: 8 | 9 | ## Starting a new project 10 | 11 | You can use the command below to bootstrap a project 12 | 13 | ::: code-group 14 | 15 | ```sh [npm] 16 | $ npm create mango@latest 17 | ``` 18 | 19 | ```sh [yarn] 20 | $ yarn create mango 21 | ``` 22 | 23 | ::: 24 | 25 | ## Adding to an existing project 26 | 27 | You can add Mango to any existing project. At the moment, [@mangobase/express](/guide/server-adapters#express) and [@mangobase/bun](/guide/server-adapters#bun) are the officially supported server adapters. You can add Mango to your project in a few easy steps if you're using any of those frameworks. 28 | 29 | Otherwise, you can implement a custom adapter for your project. See [implement a custom server adapter](/guide/server-adapters#other-servers) 30 | 31 | :::tip 32 | Reference the [examples](https://github.com/blackmann/mangobase/tree/master/examples) on how to set up your project. 33 | ::: 34 | -------------------------------------------------------------------------------- /website/guide/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ::: warning 4 | Mango is still under heavy development; it's not advisable to use this for a production app as the API/design may change rapidly. You can however try it on side projects and proof-of-concepts. 5 | ::: 6 | 7 | Mango (short for Mangobase) is a Javascript low-code backend framework for building [RESTful](rest.md) applications. Given that REST apps follow a convention on how to treat data, Mango provides the following benefits: 8 | 9 | - Reduces boilerplate for creating services for data models 10 | - Performs schema validation 11 | - Handles errors 12 | - Provides convenient querying mechanisms 13 | 14 | When building REST backends, it's ubiquitous that you implement all of these features and some. Mango takes care of all of those. 15 | 16 | ::: tip 17 | Being a low-code framework, you get to write code too, though a lot of things have been taken care of for you. 18 | ::: 19 | 20 | ## How it works 21 | 22 | A Mangobase app works by accepting a [`context`](/guide/context) from a server (like express, bun, etc.), processing it and returning a `context` back. 23 | 24 | To demonstrate with code, this is how it looks like: 25 | 26 | :::warning 27 | Just so it's not misleading, you wouldn't write any of the code demonstrated below. It's only an illustration (with code) about how it works. 28 | ::: 29 | 30 | ```javascript{5,12,15} 31 | const mangobaseApp = new App({}) 32 | const app = express() 33 | 34 | app.get(['/songs', '/songs/:id'], async (req, res) => { 35 | // create context 36 | const context = { 37 | path: req.path, 38 | headers: req.headers, 39 | // ... other properties here 40 | } 41 | 42 | // pass context to Mangobase and get result and statusCode back 43 | const { result, statusCode} = await app.api(context) 44 | 45 | // send response with express 46 | res.status(statusCode).json(result) 47 | }) 48 | ``` 49 | 50 | But with Mangobase, you don't have to do any of those. That was just a demonstration of the process looks like. 51 | 52 | If it wasn't clear, Mangobase only processes a context and does not deal with how an http request comes in or how a response is sent. 53 | This allows Mangobase to be able to work with any server (express, bun, nestjs, etc. etc.) 54 | 55 | ### Mangobase Project 56 | 57 | A Mangobase projects combines a database adapter and a server adapter to handle RESTful requests. You can bootstrap a Mangobase project using the [CLI](/guide/getting-started). The code generated by the CLI may be all you may ever need. 58 | 59 | You will spend the rest of your time in the dashboard (which runs in the browser) preparing your resource schema and setting up hooks. 60 | 61 | ### Dashboard 62 | 63 | Mangobase ships with a dashboard to allow you view your data, create collections, configure hooks and more. See [dashboard](/guide/dashboard). 64 | -------------------------------------------------------------------------------- /website/guide/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | -------------------------------------------------------------------------------- /website/guide/query.md: -------------------------------------------------------------------------------- 1 | # Paths and Queries 2 | 3 | This document describes how paths and query parameters are treated in Mangobase. 4 | 5 | ## Paths 6 | 7 | When you add a collection with name `tasks` (from the dashboard), a service is registered to handle requests to `/tasks` and `/tasks/:id` paths. 8 | 9 | See [context methods](/guide/context#methods) for the different methods (HTTP method + paths) that can be used to interact with the service. 10 | 11 | ## Queries 12 | 13 | Queries are standardized to match the fields of a collection's schema. Therefore, if you have a schema for `users` collection with the following structure: 14 | 15 | ```json 16 | { 17 | "name": { "type": "string" }, 18 | "age": { "type": "number" }, 19 | "address": { 20 | "type": "object", 21 | "schema": { 22 | "city": { "type": "string" }, 23 | "houseNumber": { "type": "string" }, 24 | "state": { "type": "string" } 25 | } 26 | } 27 | } 28 | ``` 29 | 30 | We can perform the following filter 31 | 32 | ``` javascript 33 | '/users?name=Not+Gr' // gets all users with name == 'Not Gr' 34 | '/users?age[$gt]=17&address.city=Accra' // get all users above 17 years of age and live Accra 35 | ``` 36 | 37 | You may notice that there's a [pattern](/guide/rest) between the query parameters and the fields of the schema 38 | 39 | ### Query operators 40 | 41 | As you may have already noticed from the example above, there is an operator `$gt` used. These are special operators that allow us 42 | to perform advanced queries beside simple equality check. 43 | 44 | :::info 45 | These query operators are converted to the right filter and query by the database adapters. 46 | ::: 47 | 48 | Lets talk about them: 49 | 50 | #### $gt/gte 51 | 52 | `/users?age[$gt]=17` - gets all users above 17 years of age 53 | 54 | `/users?age[$gte]=17` - gets all users above or equal to 17 years of age 55 | 56 | This query operator works for fields of number and date types. 57 | 58 | #### $lt/lte 59 | 60 | `/users?age[$lt]=17` - gets all users below 17 years of age 61 | 62 | `/users?age[$lte]=17` - gets all users below or equal to 17 years of age 63 | 64 | :::tip 65 | If you wanted to get all users between 17 and 20 years of age, you can combine the two operators like so: 66 | 67 | `/users?age[$gt]=17&age[$lt]=20` 68 | ::: 69 | -------------------------------------------------------------------------------- /website/guide/server-adapters.md: -------------------------------------------------------------------------------- 1 | # Server adapters 2 | 3 | You can use Mango with any server of choice. This is possible because the core tries to be agnostic of transport mechanisms or server implementations and uses [`contexts`](/guide/context) instead. 4 | 5 | This way, servers can form a context, pass it to [`app.api()`](/api/base/App#api) and then get a context back. With this result, the server can use it to form a response per its design. 6 | 7 | Mango, however, offers server implementations for Express and Bun. But you can be able to [implement a server](#other-servers) for any Javascript framework. _It's actually very easy._ 8 | 9 | ## Express 10 | 11 | For express servers, install [@mangobase/express](https://www.npmjs.com/package/@mangobase/express) 12 | 13 | ```bash 14 | yarn add express @mangobase/express 15 | ``` 16 | 17 | Usage example: 18 | 19 | ```javascript{2,5} 20 | import { App } from 'mangobase' 21 | import expressServer from '@mangobase/express' 22 | 23 | const app = new App({}) 24 | app.serve(expressServer).listen(4000) 25 | ``` 26 | 27 | ### Existing projects 28 | 29 | If you're adding Mangobase to an existing project, or need more control over the express app instance, you can do the following instead: 30 | 31 | ```javascript 32 | import { App } from 'mangobase' 33 | import express from 'express' 34 | import { withExpress } from '@mangobase/express' 35 | 36 | const app = new App({}) 37 | let expressApp = express() 38 | 39 | // do stuff with `expressApp` as usual, like add middleware, etc. 40 | 41 | expressApp = app.serve(withExpress(expressApp, app)) 42 | 43 | // do more stuff with `expressApp` ... 44 | 45 | expressApp.listen(4000) 46 | ``` 47 | 48 | ## Bun 49 | 50 | Mango works with [bun](https://bun.sh) too. 51 | 52 | To use Mango with Bun, install [@mangobase/bun](https://www.npmjs.com/package/@mangobase/bun) 53 | 54 | Usage example: 55 | 56 | ```typescript 57 | import { App } from 'mangobase' 58 | import { bunServer } from '@mangobase/bun' 59 | 60 | const app = new App({}) 61 | 62 | app.serve(bunServer(4000)) 63 | ``` 64 | 65 | ## Other servers 66 | 67 | If you're using other frameworks other than `express` or `Bun`, you can still use Mango but you'll have to implement an adapter to create and handle contexts for Mangobase [`App`](/api/base/App). 68 | 69 | This involves only a few lines of code. You can reference the implementation for express here: https://github.com/blackmann/mangobase/blob/master/express-server/src/index.ts 70 | -------------------------------------------------------------------------------- /website/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | title: Home 6 | 7 | hero: 8 | name: "Mangobase" 9 | text: "Low-code Javascript backend framework" 10 | tagline: Build RESTful backend services quickly, with very few concepts to learn 11 | actions: 12 | - theme: brand 13 | text: Get started 14 | link: /guide/getting-started 15 | - theme: alt 16 | text: API Reference 17 | link: /api/ 18 | 19 | features: 20 | - title: Amazing UI 21 | icon: ❇️ 22 | details: You get a free dashboard to create your schema, view your data and configure parts of your project. 23 | - title: Few concepts 24 | icon: ✍🏽 25 | details: You have to know very few concepts to get started with Mangobase. Context, hook and queries. 26 | - title: Add to existing projects 27 | icon: 🚡 28 | details: You can add Mangobase to existing projects 29 | - title: Works with version control 30 | icon: 🏂 31 | details: Changes to your project can be tracked with version control. This allows for transparent collaboration with teams. 32 | --- 33 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "vitepress dev", 6 | "build": "yarn docs && vitepress build", 7 | "preview": "vitepress preview", 8 | "docs": "node generate-api.mjs", 9 | "clean": "find api/ -mindepth 1 -not -name 'index.md' -exec rm -rf {} +" 10 | }, 11 | "devDependencies": { 12 | "typedoc": "^0.25.1", 13 | "vitepress": "^1.0.0-rc.10" 14 | } 15 | } 16 | --------------------------------------------------------------------------------