mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 16:16:02 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a046fa18e | |||
| 786d1d1bd4 | |||
| c384bf4dd4 | |||
| db2a994a01 | |||
| 166ea4a71b | |||
| 0d3bb3b7d6 | |||
| cfbec5b6ea | |||
| 5143ee5726 | |||
| fe1716ed01 | |||
| 0c31dcdb95 | |||
| 4cc0f8e172 | |||
| c161a26ec0 | |||
| 6e78a4c238 | |||
| 42edce904f |
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"bknd": {
|
|
||||||
"url": "http://localhost:28623/api/system/mcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,21 +9,6 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:17
|
|
||||||
env:
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: bknd
|
|
||||||
ports:
|
|
||||||
- 5430:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -35,11 +20,11 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: "1.3.3"
|
bun-version: "1.2.14"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
run: bun install #--linker=hoisted
|
run: bun install
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
working-directory: ./app
|
working-directory: ./app
|
||||||
|
|||||||
+1
-4
@@ -27,11 +27,8 @@ packages/media/.env
|
|||||||
.npmrc
|
.npmrc
|
||||||
/.verdaccio
|
/.verdaccio
|
||||||
.idea
|
.idea
|
||||||
.vscode/*
|
.vscode
|
||||||
!.vscode/settings.json
|
|
||||||
.git_old
|
.git_old
|
||||||
docker/tmp
|
docker/tmp
|
||||||
.debug
|
.debug
|
||||||
.history
|
.history
|
||||||
.aider*
|
|
||||||
.vercel
|
|
||||||
|
|||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"biome.enabled": true,
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
//"source.organizeImports.biome": "explicit",
|
|
||||||
"source.fixAll.biome": "explicit"
|
|
||||||
},
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
|
||||||
"**/dist/**",
|
|
||||||
"**/node_modules/**/dist/**",
|
|
||||||
"**/node_modules/**/!(src|lib|esm)/**" // optional, stricter
|
|
||||||
],
|
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
|
||||||
"typescript.tsserver.watchOptions": {
|
|
||||||
"excludeDirectories": ["**/dist", "**/node_modules/**/dist"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+77
-169
@@ -1,202 +1,110 @@
|
|||||||
|
# Functional Source License, Version 1.1, MIT Future License
|
||||||
|
|
||||||
Apache License
|
## Abbreviation
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
FSL-1.1-MIT
|
||||||
|
|
||||||
1. Definitions.
|
## Notice
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
Copyright 2025 Dennis Senn
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
## Terms and Conditions
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
### Licensor ("We")
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
The party offering the Software under these Terms and Conditions.
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
### The Software
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
The "Software" is each version of the software that we make available under
|
||||||
transformation or translation of a Source form, including but
|
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
||||||
not limited to compiled object code, generated documentation,
|
Conditions with the Software.
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
### License Grant
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
Subject to your compliance with this License Grant and the Patents,
|
||||||
form, that is based on (or derived from) the Work and for which the
|
Redistribution and Trademark clauses below, we hereby grant you the right to
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
use, copy, modify, create derivative works, publicly perform, publicly display
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
and redistribute the Software for any Permitted Purpose identified below.
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
### Permitted Purpose
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
means making the Software available to others in a commercial product or
|
||||||
subsequently incorporated within the Work.
|
service that:
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
1. substitutes for the Software;
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
2. substitutes for any other product or service we offer using the Software
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
that exists as of the date we make the Software available; or
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
3. offers the same or substantially similar functionality as the Software.
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
Permitted Purposes specifically include using the Software:
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
1. for your internal use and access;
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
2. for non-commercial education;
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
3. for non-commercial research; and
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
4. in connection with professional services that you provide to a licensee
|
||||||
may provide additional or different license terms and conditions
|
using the Software in accordance with these Terms and Conditions.
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
### Patents
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
To the extent your use for a Permitted Purpose would necessarily infringe our
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
patents, the license grant above includes a license under our patents. If you
|
||||||
except as required for reasonable and customary use in describing the
|
make a claim against any party that the Software infringes or contributes to
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
the infringement of any patent, then your patent license to the Software ends
|
||||||
|
immediately.
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
### Redistribution
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
The Terms and Conditions apply to all copies, modifications and derivatives of
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
the Software.
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
If you redistribute any copies, modifications or derivatives of the Software,
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
you must include a copy of or a link to these Terms and Conditions and not
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
remove any copyright notices provided in or with the Software.
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
### Disclaimer
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
||||||
replaced with your own identifying information. (Don't include
|
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright 2026 Dennis Senn
|
### Trademarks
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Except for displaying the License Details and identifying us as the origin of
|
||||||
you may not use this file except in compliance with the License.
|
the Software, you have no right under these Terms and Conditions to use our
|
||||||
You may obtain a copy of the License at
|
trademarks, trade names, service marks or product names.
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
## Grant of Future License
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
We hereby irrevocably grant you an additional license to use the Software under
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
the MIT license that is effective on the second anniversary of the date we make
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
the Software available. On or after that date, you may use the Software under
|
||||||
See the License for the specific language governing permissions and
|
the MIT license, in which case the following will apply:
|
||||||
limitations under the License.
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,50 +1,34 @@
|
|||||||
[](https://npmjs.org/package/bknd)
|
[](https://npmjs.org/package/bknd)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<p align="center" width="100%">
|
<p align="center" width="100%">
|
||||||
<a href="https://stackblitz.com/github/bknd-io/bknd-demo?hideExplorer=1&embed=1&view=preview&initialPath=%2Fdata%2Fschema" target="_blank">
|
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema" target="_blank">
|
||||||
<strong>⭐ Live Demo</strong>
|
<strong>⭐ Live Demo</strong>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
bknd simplifies app development by providing a fully functional visual backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
|
||||||
|
|
||||||
It's designed to avoid vendor lock-in and architectural limitations. Built exclusively on [WinterTC Minimum Common Web Platform API](https://min-common-api.proposal.wintertc.org/) for universal compatibility, all functionality (data, auth, media, flows) is modular and opt-in, and infrastructure access is adapter-based with direct access to underlying drivers giving you full control without abstractions getting in your way.
|
|
||||||
|
|
||||||
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
|
||||||
* **Databases**:
|
* **Databases**:
|
||||||
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
|
||||||
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
|
||||||
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
|
||||||
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem, Origin Private File System (OPFS)
|
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
|
||||||
* **Deployment**: Standalone, Docker, Cloudflare Workers, Vercel, Netlify, Deno Deploy, AWS Lambda, Valtown etc.
|
|
||||||
|
|
||||||
**For documentation and examples, please visit https://docs.bknd.io.**
|
**For documentation and examples, please visit https://docs.bknd.io.**
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This project requires Node.js 22.13 or higher (because of `node:sqlite`).
|
> This project requires Node.js 22 or higher (because of `node:sqlite`).
|
||||||
>
|
>
|
||||||
> Please keep in mind that **bknd** is still under active development
|
> Please keep in mind that **bknd** is still under active development
|
||||||
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
bknd is a general purpose backend system that implements the primitives almost any backend needs. This way, you can use it for any backend use case, including but not limited to:
|
|
||||||
|
|
||||||
- **Content Management System (CMS)** as Wordpress alternative, hosted separately or embedded in your frontend
|
|
||||||
- **AI Agent Backends** for managing agent state with built-in data persistence, regardless where it is hosted. Optionally communicate over the integrated MCP server.
|
|
||||||
- **SaaS Products** with multi-tenant data isolation (RLS) and user management, with freedom to choose your own database and storage provider
|
|
||||||
- **Prototypes & MVPs** to validate ideas quickly without infrastructure overhead
|
|
||||||
- **API-First Applications** where you need a reliable, type-safe backend without vendor lock-in either with the integrated TypeScript SDK or REST API using OpenAPI
|
|
||||||
- **IoT & Embedded Devices** where minimal footprint matters
|
|
||||||
|
|
||||||
|
|
||||||
## Size
|
## Size
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
The size on npm is misleading, as the `bknd` package includes the backend, the ui components as well as the whole backend bundled into the cli including static assets.
|
||||||
|
|
||||||
@@ -62,7 +46,6 @@ Creating digital products always requires developing both the backend (the logic
|
|||||||
* **Media**: Effortlessly manage and serve all your media files.
|
* **Media**: Effortlessly manage and serve all your media files.
|
||||||
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
|
||||||
* 🌐 Built on Web Standards for maximum compatibility
|
* 🌐 Built on Web Standards for maximum compatibility
|
||||||
* 🛠️ MCP server, client and UI built-in to control your backend
|
|
||||||
* 🏃♂️ Multiple run modes
|
* 🏃♂️ Multiple run modes
|
||||||
* standalone using the CLI
|
* standalone using the CLI
|
||||||
* using a JavaScript runtime (Node, Bun, workerd)
|
* using a JavaScript runtime (Node, Bun, workerd)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ VITE_SHOW_ROUTES=
|
|||||||
|
|
||||||
# ===== Test Credentials =====
|
# ===== Test Credentials =====
|
||||||
RESEND_API_KEY=
|
RESEND_API_KEY=
|
||||||
PLUNK_API_KEY=
|
|
||||||
R2_TOKEN=
|
R2_TOKEN=
|
||||||
|
|
||||||
R2_ACCESS_KEY=
|
R2_ACCESS_KEY=
|
||||||
|
|||||||
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.preferences.includePackageJsonAutoImports": "off",
|
|
||||||
"typescript.suggest.autoImports": true,
|
|
||||||
"typescript.preferences.importModuleSpecifier": "relative",
|
|
||||||
"search.exclude": {
|
|
||||||
"**/dist/**": true,
|
|
||||||
"**/node_modules/**": true
|
|
||||||
},
|
|
||||||
"files.exclude": {
|
|
||||||
"**/dist/**": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import { afterEach, describe, test, expect, beforeAll, afterAll } from "bun:test";
|
import { afterEach, describe, test, expect } from "bun:test";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import * as proto from "../src/data/prototype";
|
import * as proto from "../src/data/prototype";
|
||||||
import { pick } from "lodash-es";
|
import { pick } from "lodash-es";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterEach(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterEach(afterAllCleanup);
|
||||||
|
|
||||||
describe("App tests", async () => {
|
describe("App tests", async () => {
|
||||||
test("boots and pongs", async () => {
|
test("boots and pongs", async () => {
|
||||||
@@ -22,7 +19,7 @@ describe("App tests", async () => {
|
|||||||
test("plugins", async () => {
|
test("plugins", async () => {
|
||||||
const called: string[] = [];
|
const called: string[] = [];
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@@ -108,7 +105,7 @@ describe("App tests", async () => {
|
|||||||
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("drivers", async () => {
|
test.only("drivers", async () => {
|
||||||
const called: string[] = [];
|
const called: string[] = [];
|
||||||
const app = new App(dummyConnection, undefined, {
|
const app = new App(dummyConnection, undefined, {
|
||||||
drivers: {
|
drivers: {
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
tmp/*
|
tmp/*
|
||||||
!tmp/.gitkeep
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
hello
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
|
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
|
||||||
import * as adapter from "adapter";
|
import * as adapter from "adapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
import { adapterTestSuite } from "adapter/adapter-test-suite";
|
||||||
@@ -9,49 +9,60 @@ beforeAll(disableConsoleLog);
|
|||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("adapter", () => {
|
describe("adapter", () => {
|
||||||
it("makes config", async () => {
|
it("makes config", () => {
|
||||||
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
|
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
|
||||||
expect(
|
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
|
||||||
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
|
{},
|
||||||
).toEqual({});
|
);
|
||||||
|
|
||||||
// merges everything returned from `app` with the config
|
// merges everything returned from `app` with the config
|
||||||
expect(
|
expect(
|
||||||
omitKeys(
|
omitKeys(
|
||||||
await adapter.makeConfig(
|
adapter.makeConfig(
|
||||||
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
|
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
|
||||||
{ env: { TEST: "test" } },
|
{ env: { TEST: "test" } },
|
||||||
),
|
),
|
||||||
["connection"],
|
["connection"],
|
||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
config: { server: { cors: { origin: "test" } } },
|
initialConfig: { server: { cors: { origin: "test" } } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows all properties in app function", async () => {
|
/* it.only("...", async () => {
|
||||||
const called = mock(() => null);
|
const app = await adapter.createAdapterApp();
|
||||||
const config = await adapter.makeConfig(
|
}); */
|
||||||
|
|
||||||
|
it("reuses apps correctly", async () => {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
const first = await adapter.createAdapterApp(
|
||||||
{
|
{
|
||||||
app: (env) => ({
|
initialConfig: { server: { cors: { origin: "random" } } },
|
||||||
connection: { url: "test" },
|
|
||||||
config: { server: { cors: { origin: "test" } } },
|
|
||||||
options: {
|
|
||||||
mode: "db",
|
|
||||||
},
|
|
||||||
onBuilt: () => {
|
|
||||||
called();
|
|
||||||
expect(env).toEqual({ foo: "bar" });
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{ foo: "bar" },
|
undefined,
|
||||||
|
{ id },
|
||||||
);
|
);
|
||||||
expect(config.connection).toEqual({ url: "test" });
|
const second = await adapter.createAdapterApp();
|
||||||
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
|
const third = await adapter.createAdapterApp(undefined, undefined, { id });
|
||||||
expect(config.options).toEqual({ mode: "db" });
|
|
||||||
await config.onBuilt?.(null as any);
|
await first.build();
|
||||||
expect(called).toHaveBeenCalled();
|
await second.build();
|
||||||
|
await third.build();
|
||||||
|
|
||||||
|
expect(first.toJSON().server.cors.origin).toEqual("random");
|
||||||
|
expect(first).toBe(third);
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
expect(second).not.toBe(third);
|
||||||
|
expect(second.toJSON().server.cors.origin).toEqual("*");
|
||||||
|
|
||||||
|
// recreate the first one
|
||||||
|
const first2 = await adapter.createAdapterApp(undefined, undefined, { id, force: true });
|
||||||
|
await first2.build();
|
||||||
|
expect(first2).not.toBe(first);
|
||||||
|
expect(first2).not.toBe(third);
|
||||||
|
expect(first2).not.toBe(second);
|
||||||
|
expect(first2.toJSON().server.cors.origin).toEqual("*");
|
||||||
});
|
});
|
||||||
|
|
||||||
adapterTestSuite(bunTestRunner, {
|
adapterTestSuite(bunTestRunner, {
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ describe("Api", async () => {
|
|||||||
it("should construct without options", () => {
|
it("should construct without options", () => {
|
||||||
const api = new Api();
|
const api = new Api();
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
// verified is true, because no token, user, headers or request given
|
|
||||||
// therefore nothing to check, auth state is verified
|
|
||||||
expect(api.isAuthVerified()).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore force verify if no claims given", () => {
|
it("should ignore force verify if no claims given", () => {
|
||||||
const api = new Api({ verified: true });
|
const api = new Api({ verified: true });
|
||||||
expect(api.baseUrl).toBe("http://localhost");
|
expect(api.baseUrl).toBe("http://localhost");
|
||||||
expect(api.isAuthVerified()).toBe(true);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should construct from request (token)", async () => {
|
it("should construct from request (token)", async () => {
|
||||||
@@ -45,6 +42,7 @@ describe("Api", async () => {
|
|||||||
expect(api.isAuthVerified()).toBe(false);
|
expect(api.isAuthVerified()).toBe(false);
|
||||||
|
|
||||||
const params = api.getParams();
|
const params = api.getParams();
|
||||||
|
console.log(params);
|
||||||
expect(params.token).toBe(token);
|
expect(params.token).toBe(token);
|
||||||
expect(params.token_transport).toBe("cookie");
|
expect(params.token_transport).toBe("cookie");
|
||||||
expect(params.host).toBe("http://example.com");
|
expect(params.host).toBe("http://example.com");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { Guard } from "../../src/auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
import { DataApi } from "../../src/data/api/DataApi";
|
import { DataApi } from "../../src/data/api/DataApi";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { schemaToEm } from "../helper";
|
import { schemaToEm } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
||||||
import { parse } from "core/utils/schema";
|
import { parse } from "core/object/schema";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -202,7 +202,7 @@ describe("DataApi", () => {
|
|||||||
{
|
{
|
||||||
// create many
|
// create many
|
||||||
const res = await api.createMany("posts", payload);
|
const res = await api.createMany("posts", payload);
|
||||||
expect(res.data?.length).toEqual(4);
|
expect(res.data.length).toEqual(4);
|
||||||
expect(res.ok).toBeTrue();
|
expect(res.ok).toBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
import { describe, expect, it, mock } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
|
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
|
||||||
import { MediaApi } from "media/api/MediaApi";
|
import { MediaApi } from "media/api/MediaApi";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath } from "../helper";
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ const mockedBackend = new Hono()
|
|||||||
.get("/file/:name", async (c) => {
|
.get("/file/:name", async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const file = Bun.file(`${assetsPath}/${name}`);
|
const file = Bun.file(`${assetsPath}/${name}`);
|
||||||
return new Response(new File([await file.bytes()], name, { type: file.type }), {
|
return new Response(file, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": file.type,
|
"Content-Type": file.type,
|
||||||
"Content-Length": file.size.toString(),
|
"Content-Length": file.size.toString(),
|
||||||
@@ -67,7 +67,7 @@ describe("MediaApi", () => {
|
|||||||
const res = await mockedBackend.request("/api/media/file/" + name);
|
const res = await mockedBackend.request("/api/media/file/" + name);
|
||||||
await Bun.write(path, res);
|
await Bun.write(path, res);
|
||||||
|
|
||||||
const file = Bun.file(path);
|
const file = await Bun.file(path);
|
||||||
expect(file.size).toBeGreaterThan(0);
|
expect(file.size).toBeGreaterThan(0);
|
||||||
expect(file.type).toBe("image/png");
|
expect(file.type).toBe("image/png");
|
||||||
await file.delete();
|
await file.delete();
|
||||||
@@ -98,7 +98,7 @@ describe("MediaApi", () => {
|
|||||||
expect(isReadableStream(res.body)).toBe(true);
|
expect(isReadableStream(res.body)).toBe(true);
|
||||||
expect(isReadableStream(res.res.body)).toBe(true);
|
expect(isReadableStream(res.res.body)).toBe(true);
|
||||||
|
|
||||||
const blob = (await res.res.blob()) as File;
|
const blob = await res.res.blob();
|
||||||
expect(isFile(blob)).toBe(true);
|
expect(isFile(blob)).toBe(true);
|
||||||
expect(blob.size).toBeGreaterThan(0);
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
expect(blob.type).toBe("image/png");
|
expect(blob.type).toBe("image/png");
|
||||||
@@ -113,7 +113,7 @@ describe("MediaApi", () => {
|
|||||||
const res = await api.getFileStream(name);
|
const res = await api.getFileStream(name);
|
||||||
expect(isReadableStream(res)).toBe(true);
|
expect(isReadableStream(res)).toBe(true);
|
||||||
|
|
||||||
const blob = (await new Response(res).blob()) as File;
|
const blob = await new Response(res).blob();
|
||||||
expect(isFile(blob)).toBe(true);
|
expect(isFile(blob)).toBe(true);
|
||||||
expect(blob.size).toBeGreaterThan(0);
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
expect(blob.type).toBe("image/png");
|
expect(blob.type).toBe("image/png");
|
||||||
@@ -154,38 +154,15 @@ describe("MediaApi", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upload via readable from bun
|
// upload via readable from bun
|
||||||
await matches(api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
|
||||||
|
|
||||||
// upload via readable from response
|
// upload via readable from response
|
||||||
{
|
{
|
||||||
const response = (await mockedBackend.request(url)) as Response;
|
const response = (await mockedBackend.request(url)) as Response;
|
||||||
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
|
await matches(
|
||||||
|
await api.upload(response.body!, { filename: "readable.png" }),
|
||||||
|
"readable.png",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add overwrite query for entity upload", async (c) => {
|
|
||||||
const call = mock(() => null);
|
|
||||||
const hono = new Hono().post(
|
|
||||||
"/api/media/entity/:entity/:id/:field",
|
|
||||||
jsc("query", s.object({ overwrite: s.boolean().optional() })),
|
|
||||||
async (c) => {
|
|
||||||
const { overwrite } = c.req.valid("query");
|
|
||||||
expect(overwrite).toBe(true);
|
|
||||||
call();
|
|
||||||
return c.json({ ok: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const api = new MediaApi(
|
|
||||||
{
|
|
||||||
upload_fetcher: hono.request,
|
|
||||||
},
|
|
||||||
hono.request,
|
|
||||||
);
|
|
||||||
const file = Bun.file(`${assetsPath}/image.png`);
|
|
||||||
const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
|
|
||||||
overwrite: true,
|
|
||||||
});
|
|
||||||
expect(res.ok).toBe(true);
|
|
||||||
expect(call).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { App, createApp } from "core/test/utils";
|
import { App, createApp } from "core/test/utils";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
test("use db mode by default", async () => {
|
|
||||||
const app = createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.mode).toBe("db");
|
|
||||||
expect(app.isReadOnly()).toBe(false);
|
|
||||||
expect(app.modules instanceof DbModuleManager).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("seed includes ctx and app", async () => {
|
test("seed includes ctx and app", async () => {
|
||||||
const called = mock(() => null);
|
const called = mock(() => null);
|
||||||
await createApp({
|
await createApp({
|
||||||
@@ -34,7 +20,6 @@ describe("App", () => {
|
|||||||
"guard",
|
"guard",
|
||||||
"flags",
|
"flags",
|
||||||
"logger",
|
"logger",
|
||||||
"mcp",
|
|
||||||
"helper",
|
"helper",
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@@ -43,7 +28,7 @@ describe("App", () => {
|
|||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
data: proto
|
data: proto
|
||||||
.em({
|
.em({
|
||||||
todos: proto.entity("todos", {
|
todos: proto.entity("todos", {
|
||||||
@@ -150,21 +135,4 @@ describe("App", () => {
|
|||||||
// expect async listeners to be executed sync after request
|
// expect async listeners to be executed sync after request
|
||||||
expect(called).toHaveBeenCalled();
|
expect(called).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMcpClient", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
const client = app.getMcpClient();
|
|
||||||
const res = await client.listTools();
|
|
||||||
expect(res).toBeDefined();
|
|
||||||
expect(res?.tools.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,16 +8,10 @@ describe("AppServer", () => {
|
|||||||
expect(server).toBeDefined();
|
expect(server).toBeDefined();
|
||||||
expect(server.config).toEqual({
|
expect(server.config).toEqual({
|
||||||
cors: {
|
cors: {
|
||||||
allow_credentials: true,
|
|
||||||
origin: "*",
|
origin: "*",
|
||||||
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
},
|
},
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
logLevel: "warning",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,16 +25,10 @@ describe("AppServer", () => {
|
|||||||
expect(server).toBeDefined();
|
expect(server).toBeDefined();
|
||||||
expect(server.config).toEqual({
|
expect(server.config).toEqual({
|
||||||
cors: {
|
cors: {
|
||||||
allow_credentials: true,
|
|
||||||
origin: "https",
|
origin: "https",
|
||||||
allow_methods: ["GET", "POST"],
|
allow_methods: ["GET", "POST"],
|
||||||
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
|
||||||
},
|
},
|
||||||
mcp: {
|
|
||||||
enabled: false,
|
|
||||||
path: "/api/system/mcp",
|
|
||||||
logLevel: "warning",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
import { describe, expect, mock, test, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp as internalCreateApp, type CreateAppConfig } from "bknd";
|
|
||||||
import { getDummyConnection } from "../../__test__/helper";
|
|
||||||
import { ModuleManager } from "modules/ModuleManager";
|
|
||||||
import { em, entity, text } from "data/prototype";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
async function createApp(config: CreateAppConfig = {}) {
|
|
||||||
const app = internalCreateApp({
|
|
||||||
connection: getDummyConnection().dummyConnection,
|
|
||||||
...config,
|
|
||||||
options: {
|
|
||||||
...config.options,
|
|
||||||
mode: "code",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("code-only", () => {
|
|
||||||
test("should create app with correct manager", async () => {
|
|
||||||
const app = await createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
expect(app.version()).toBeDefined();
|
|
||||||
expect(app.modules).toBeInstanceOf(ModuleManager);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not perform database syncs", async () => {
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["test"]);
|
|
||||||
expect(
|
|
||||||
await app.em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute(),
|
|
||||||
).toEqual([]);
|
|
||||||
|
|
||||||
// only perform when explicitly forced
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
expect(
|
|
||||||
await app.em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute()
|
|
||||||
.then((r) => r.map((r) => r.name)),
|
|
||||||
).toEqual(["test", "sqlite_sequence"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not perform seeding", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
seed: async (ctx) => {
|
|
||||||
called();
|
|
||||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
await app.em
|
|
||||||
.repo("test")
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data),
|
|
||||||
).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should sync and perform seeding", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = await createApp({
|
|
||||||
config: {
|
|
||||||
data: em({
|
|
||||||
test: entity("test", {
|
|
||||||
name: text(),
|
|
||||||
}),
|
|
||||||
}).toJSON(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
seed: async (ctx) => {
|
|
||||||
called();
|
|
||||||
await ctx.em.mutator("test").insertOne({ name: "test" });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.em.schema().sync({ force: true });
|
|
||||||
await app.options?.seed?.({
|
|
||||||
...app.modules.ctx(),
|
|
||||||
app: app,
|
|
||||||
});
|
|
||||||
expect(called).toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
await app.em
|
|
||||||
.repo("test")
|
|
||||||
.findMany({})
|
|
||||||
.then((r) => r.data),
|
|
||||||
).toEqual([{ id: 1, name: "test" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not allow to modify config", async () => {
|
|
||||||
const app = await createApp();
|
|
||||||
// biome-ignore lint/suspicious/noPrototypeBuiltins: <explanation>
|
|
||||||
expect(app.modules.hasOwnProperty("mutateConfigSafe")).toBe(false);
|
|
||||||
expect(() => {
|
|
||||||
app.modules.configs().auth.enabled = true;
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] auth_me
|
|
||||||
* - [x] auth_strategies
|
|
||||||
* - [x] auth_user_create
|
|
||||||
* - [x] auth_user_token
|
|
||||||
* - [x] auth_user_password_change
|
|
||||||
* - [x] auth_user_password_test
|
|
||||||
* - [x] config_auth_get
|
|
||||||
* - [x] config_auth_update
|
|
||||||
* - [x] config_auth_strategies_get
|
|
||||||
* - [x] config_auth_strategies_add
|
|
||||||
* - [x] config_auth_strategies_update
|
|
||||||
* - [x] config_auth_strategies_remove
|
|
||||||
* - [x] config_auth_roles_get
|
|
||||||
* - [x] config_auth_roles_add
|
|
||||||
* - [x] config_auth_roles_update
|
|
||||||
* - [x] config_auth_roles_remove
|
|
||||||
*/
|
|
||||||
describe("mcp auth", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("auth_*", async () => {
|
|
||||||
const me = await tool(server, "auth_me", {});
|
|
||||||
expect(me.user).toBeNull();
|
|
||||||
|
|
||||||
// strategies
|
|
||||||
const strategies = await tool(server, "auth_strategies", {});
|
|
||||||
expect(Object.keys(strategies.strategies).length).toEqual(1);
|
|
||||||
expect(strategies.strategies.password.enabled).toBe(true);
|
|
||||||
|
|
||||||
// create user
|
|
||||||
const user = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_create",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "12345678",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// create token
|
|
||||||
const token = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_token",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(token.token).toBeDefined();
|
|
||||||
expect(token.user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// me
|
|
||||||
const me2 = await tool(
|
|
||||||
server,
|
|
||||||
"auth_me",
|
|
||||||
{},
|
|
||||||
new Request("http://localhost", {
|
|
||||||
headers: new Headers({
|
|
||||||
Authorization: `Bearer ${token.token}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(me2.user.email).toBe("test@test.com");
|
|
||||||
|
|
||||||
// change password
|
|
||||||
const changePassword = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_password_change",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "87654321",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(changePassword.changed).toBe(true);
|
|
||||||
|
|
||||||
// test password
|
|
||||||
const testPassword = await tool(
|
|
||||||
server,
|
|
||||||
"auth_user_password_test",
|
|
||||||
{
|
|
||||||
email: "test@test.com",
|
|
||||||
password: "87654321",
|
|
||||||
},
|
|
||||||
new Headers(),
|
|
||||||
);
|
|
||||||
expect(testPassword.valid).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_{get,update}", async () => {
|
|
||||||
expect(await tool(server, "config_auth_get", {})).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: app.toJSON().auth,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_auth_update", {
|
|
||||||
value: {
|
|
||||||
allow_register: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.allow_register).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_strategies_{get,add,update,remove}", async () => {
|
|
||||||
const strategies = await tool(server, "config_auth_strategies_get", {
|
|
||||||
key: "password",
|
|
||||||
});
|
|
||||||
expect(strategies).toEqual({
|
|
||||||
secrets: false,
|
|
||||||
module: "auth",
|
|
||||||
key: "password",
|
|
||||||
value: {
|
|
||||||
enabled: true,
|
|
||||||
type: "password",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// add google oauth
|
|
||||||
const addGoogleOauth = await tool(server, "config_auth_strategies_add", {
|
|
||||||
key: "google",
|
|
||||||
value: {
|
|
||||||
type: "oauth",
|
|
||||||
enabled: true,
|
|
||||||
config: {
|
|
||||||
name: "google",
|
|
||||||
type: "oidc",
|
|
||||||
client: {
|
|
||||||
client_id: "client_id",
|
|
||||||
client_secret: "client_secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(addGoogleOauth.config.google.enabled).toBe(true);
|
|
||||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(true);
|
|
||||||
|
|
||||||
// update (disable) google oauth
|
|
||||||
await tool(server, "config_auth_strategies_update", {
|
|
||||||
key: "google",
|
|
||||||
value: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.strategies.google?.enabled).toBe(false);
|
|
||||||
|
|
||||||
// remove google oauth
|
|
||||||
await tool(server, "config_auth_strategies_remove", {
|
|
||||||
key: "google",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.strategies.google).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_auth_roles_{get,add,update,remove}", async () => {
|
|
||||||
// add role
|
|
||||||
const addGuestRole = await tool(server, "config_auth_roles_add", {
|
|
||||||
key: "guest",
|
|
||||||
value: {
|
|
||||||
permissions: ["read", "write"],
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(addGuestRole.config.guest.permissions.map((p) => p.permission)).toEqual([
|
|
||||||
"read",
|
|
||||||
"write",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// update role
|
|
||||||
await tool(server, "config_auth_roles_update", {
|
|
||||||
key: "guest",
|
|
||||||
value: {
|
|
||||||
permissions: ["read"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.roles?.guest?.permissions?.map((p) => p.permission)).toEqual([
|
|
||||||
"read",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// get role
|
|
||||||
const getGuestRole = await tool(server, "config_auth_roles_get", {
|
|
||||||
key: "guest",
|
|
||||||
});
|
|
||||||
expect(getGuestRole.value.permissions.map((p) => p.permission)).toEqual(["read"]);
|
|
||||||
|
|
||||||
// remove role
|
|
||||||
await tool(server, "config_auth_roles_remove", {
|
|
||||||
key: "guest",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().auth.roles?.guest).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import { registries } from "index";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("mcp", () => {
|
|
||||||
it("should have tools", async () => {
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
// expect mcp to not be loaded yet
|
|
||||||
expect(app.mcp).toBeNull();
|
|
||||||
|
|
||||||
// after first request, mcp should be loaded
|
|
||||||
await app.getMcpClient().listTools();
|
|
||||||
expect(app.mcp?.tools.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
import { describe, test, expect, beforeEach, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
|
||||||
import { pickKeys, type McpServer } from "bknd/utils";
|
|
||||||
import { entity, text } from "bknd";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [ ] data_sync
|
|
||||||
* - [x] data_entity_fn_count
|
|
||||||
* - [x] data_entity_fn_exists
|
|
||||||
* - [x] data_entity_read_one
|
|
||||||
* - [x] data_entity_read_many
|
|
||||||
* - [x] data_entity_insert
|
|
||||||
* - [x] data_entity_update_many
|
|
||||||
* - [x] data_entity_update_one
|
|
||||||
* - [x] data_entity_delete_one
|
|
||||||
* - [x] data_entity_delete_many
|
|
||||||
* - [x] data_entity_info
|
|
||||||
* - [ ] config_data_get
|
|
||||||
* - [ ] config_data_update
|
|
||||||
* - [x] config_data_entities_get
|
|
||||||
* - [x] config_data_entities_add
|
|
||||||
* - [x] config_data_entities_update
|
|
||||||
* - [x] config_data_entities_remove
|
|
||||||
* - [x] config_data_relations_add
|
|
||||||
* - [x] config_data_relations_get
|
|
||||||
* - [x] config_data_relations_update
|
|
||||||
* - [x] config_data_relations_remove
|
|
||||||
* - [x] config_data_indices_get
|
|
||||||
* - [x] config_data_indices_add
|
|
||||||
* - [x] config_data_indices_update
|
|
||||||
* - [x] config_data_indices_remove
|
|
||||||
*/
|
|
||||||
describe("mcp data", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
const time = performance.now();
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_data_entities_{add,get,update,remove}", async () => {
|
|
||||||
const result = await tool(server, "config_data_entities_add", {
|
|
||||||
key: "test",
|
|
||||||
return_config: true,
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.config.test?.type).toEqual("regular");
|
|
||||||
|
|
||||||
const entities = Object.keys(app.toJSON().data.entities ?? {});
|
|
||||||
expect(entities).toContain("test");
|
|
||||||
|
|
||||||
{
|
|
||||||
// get
|
|
||||||
const result = await tool(server, "config_data_entities_get", {
|
|
||||||
key: "test",
|
|
||||||
});
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.key).toBe("test");
|
|
||||||
expect(result.value.type).toEqual("regular");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// update
|
|
||||||
const result = await tool(server, "config_data_entities_update", {
|
|
||||||
key: "test",
|
|
||||||
return_config: true,
|
|
||||||
value: {
|
|
||||||
config: {
|
|
||||||
name: "Test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(result.config.test.config?.name).toEqual("Test");
|
|
||||||
expect(app.toJSON().data.entities?.test?.config?.name).toEqual("Test");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// remove
|
|
||||||
const result = await tool(server, "config_data_entities_remove", {
|
|
||||||
key: "test",
|
|
||||||
});
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
expect(result.module).toBe("data");
|
|
||||||
expect(app.toJSON().data.entities?.test).toBeUndefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_relations_{add,get,update,remove}", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "comments",
|
|
||||||
value: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(Object.keys(app.toJSON().data.entities ?? {})).toEqual(["posts", "comments"]);
|
|
||||||
|
|
||||||
// create relation
|
|
||||||
await tool(server, "config_data_relations_add", {
|
|
||||||
key: "", // doesn't matter
|
|
||||||
value: {
|
|
||||||
type: "n:1",
|
|
||||||
source: "comments",
|
|
||||||
target: "posts",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = app.toJSON().data;
|
|
||||||
expect(
|
|
||||||
pickKeys((config.relations?.n1_comments_posts as any) ?? {}, ["type", "source", "target"]),
|
|
||||||
).toEqual({
|
|
||||||
type: "n:1",
|
|
||||||
source: "comments",
|
|
||||||
target: "posts",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config.entities?.comments?.fields?.posts_id?.type).toBe("relation");
|
|
||||||
|
|
||||||
{
|
|
||||||
// info
|
|
||||||
const postsInfo = await tool(server, "data_entity_info", {
|
|
||||||
entity: "posts",
|
|
||||||
});
|
|
||||||
expect(postsInfo.fields).toEqual(["id"]);
|
|
||||||
expect(postsInfo.relations.all.length).toBe(1);
|
|
||||||
|
|
||||||
const commentsInfo = await tool(server, "data_entity_info", {
|
|
||||||
entity: "comments",
|
|
||||||
});
|
|
||||||
expect(commentsInfo.fields).toEqual(["id", "posts_id"]);
|
|
||||||
expect(commentsInfo.relations.all.length).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_data_relations_update", {
|
|
||||||
key: "n1_comments_posts",
|
|
||||||
value: {
|
|
||||||
config: {
|
|
||||||
with_limit: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect((app.toJSON().data.relations?.n1_comments_posts?.config as any)?.with_limit).toBe(10);
|
|
||||||
|
|
||||||
// delete
|
|
||||||
await tool(server, "config_data_relations_remove", {
|
|
||||||
key: "n1_comments_posts",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().data.relations?.n1_comments_posts).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_indices_update", async () => {
|
|
||||||
expect(server.tools.map((t) => t.name).includes("config_data_indices_update")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_data_indices_{add,get,remove}", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: entity("posts", {
|
|
||||||
title: text(),
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// add index on title
|
|
||||||
await tool(server, "config_data_indices_add", {
|
|
||||||
key: "", // auto generated
|
|
||||||
value: {
|
|
||||||
entity: "posts",
|
|
||||||
fields: ["title"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(app.toJSON().data.indices?.idx_posts_title).toEqual({
|
|
||||||
entity: "posts",
|
|
||||||
fields: ["title"],
|
|
||||||
unique: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete
|
|
||||||
await tool(server, "config_data_indices_remove", {
|
|
||||||
key: "idx_posts_title",
|
|
||||||
});
|
|
||||||
expect(app.toJSON().data.indices?.idx_posts_title).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("data_entity_*", async () => {
|
|
||||||
// create posts and comments
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "posts",
|
|
||||||
value: entity("posts", {
|
|
||||||
title: text(),
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
await tool(server, "config_data_entities_add", {
|
|
||||||
key: "comments",
|
|
||||||
value: entity("comments", {
|
|
||||||
content: text(),
|
|
||||||
}).toJSON(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// insert a few posts
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
await tool(server, "data_entity_insert", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
title: `Post ${i}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// insert a few comments
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
await tool(server, "data_entity_insert", {
|
|
||||||
entity: "comments",
|
|
||||||
json: {
|
|
||||||
content: `Comment ${i}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
expect(result.data.length).toBe(5);
|
|
||||||
expect(result.meta.items).toBe(5);
|
|
||||||
expect(result.meta.total).toBe(10);
|
|
||||||
expect(result.data[0].title).toBe("Post 0");
|
|
||||||
|
|
||||||
{
|
|
||||||
// count
|
|
||||||
const result = await tool(server, "data_entity_fn_count", {
|
|
||||||
entity: "posts",
|
|
||||||
});
|
|
||||||
expect(result.count).toBe(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// exists
|
|
||||||
const res = await tool(server, "data_entity_fn_exists", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
id: result.data[0].id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res.exists).toBe(true);
|
|
||||||
|
|
||||||
const res2 = await tool(server, "data_entity_fn_exists", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
id: "123",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res2.exists).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "data_entity_update_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[0].id,
|
|
||||||
json: {
|
|
||||||
title: "Post 0 updated",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result2 = await tool(server, "data_entity_read_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[0].id,
|
|
||||||
});
|
|
||||||
expect(result2.data.title).toBe("Post 0 updated");
|
|
||||||
|
|
||||||
// delete the second post
|
|
||||||
await tool(server, "data_entity_delete_one", {
|
|
||||||
entity: "posts",
|
|
||||||
id: result.data[1].id,
|
|
||||||
});
|
|
||||||
const result3 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 2,
|
|
||||||
});
|
|
||||||
expect(result3.data.map((p) => p.id)).toEqual([1, 3]);
|
|
||||||
|
|
||||||
// update many
|
|
||||||
await tool(server, "data_entity_update_many", {
|
|
||||||
entity: "posts",
|
|
||||||
update: {
|
|
||||||
title: "Post updated",
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
title: { $isnull: 0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result4 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
expect(result4.data.length).toBe(9);
|
|
||||||
expect(result4.data.map((p) => p.title)).toEqual(
|
|
||||||
Array.from({ length: 9 }, () => "Post updated"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete many
|
|
||||||
await tool(server, "data_entity_delete_many", {
|
|
||||||
entity: "posts",
|
|
||||||
json: {
|
|
||||||
title: { $isnull: 0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result5 = await tool(server, "data_entity_read_many", {
|
|
||||||
entity: "posts",
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
expect(result5.data.length).toBe(0);
|
|
||||||
expect(result5.meta.items).toBe(0);
|
|
||||||
expect(result5.meta.total).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import { getSystemMcp } from "modules/mcp/system-mcp";
|
|
||||||
import { registries } from "index";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] config_media_get
|
|
||||||
* - [x] config_media_update
|
|
||||||
* - [x] config_media_adapter_get
|
|
||||||
* - [x] config_media_adapter_update
|
|
||||||
*/
|
|
||||||
describe("mcp media", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeEach(async () => {
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
server.setLogLevel("error");
|
|
||||||
server.onNotification((message) => {
|
|
||||||
console.dir(message, { depth: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_media_{get,update}", async () => {
|
|
||||||
const result = await tool(server, "config_media_get", {});
|
|
||||||
expect(result).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: app.toJSON().media,
|
|
||||||
});
|
|
||||||
|
|
||||||
// partial
|
|
||||||
expect((await tool(server, "config_media_get", { path: "adapter" })).value).toEqual({
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_media_update", {
|
|
||||||
value: {
|
|
||||||
storage: {
|
|
||||||
body_max_size: 1024 * 1024 * 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
expect(app.toJSON().media.storage.body_max_size).toBe(1024 * 1024 * 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_media_adapter_{get,update}", async () => {
|
|
||||||
const result = await tool(server, "config_media_adapter_get", {});
|
|
||||||
expect(result).toEqual({
|
|
||||||
secrets: false,
|
|
||||||
value: app.toJSON().media.adapter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// update
|
|
||||||
await tool(server, "config_media_adapter_update", {
|
|
||||||
value: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./subdir",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const adapter = app.toJSON().media.adapter as any;
|
|
||||||
expect(adapter.config.path).toBe("./subdir");
|
|
||||||
expect(adapter.type).toBe("local");
|
|
||||||
|
|
||||||
// set to s3
|
|
||||||
{
|
|
||||||
await tool(server, "config_media_adapter_update", {
|
|
||||||
value: {
|
|
||||||
type: "s3",
|
|
||||||
config: {
|
|
||||||
access_key: "123",
|
|
||||||
secret_access_key: "456",
|
|
||||||
url: "https://example.com/what",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const adapter = app.toJSON(true).media.adapter as any;
|
|
||||||
expect(adapter.type).toBe("s3");
|
|
||||||
expect(adapter.config.url).toBe("https://example.com/what");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] config_server_get
|
|
||||||
* - [x] config_server_update
|
|
||||||
*/
|
|
||||||
describe("mcp system", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("config_server_get", async () => {
|
|
||||||
const result = await tool(server, "config_server_get", {});
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_server_get2", async () => {
|
|
||||||
const result = await tool(server, "config_server_get", {});
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
path: "",
|
|
||||||
secrets: false,
|
|
||||||
partial: false,
|
|
||||||
value: JSON.parse(JSON.stringify(app.toJSON().server)),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("config_server_update", async () => {
|
|
||||||
const original = JSON.parse(JSON.stringify(app.toJSON().server));
|
|
||||||
const result = await tool(server, "config_server_update", {
|
|
||||||
value: {
|
|
||||||
cors: {
|
|
||||||
origin: "http://localhost",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
return_config: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual({
|
|
||||||
success: true,
|
|
||||||
module: "server",
|
|
||||||
config: {
|
|
||||||
...original,
|
|
||||||
cors: {
|
|
||||||
...original.cors,
|
|
||||||
origin: "http://localhost",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(app.toJSON().server.cors.origin).toBe("http://localhost");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { AppEvents } from "App";
|
|
||||||
import { describe, test, expect, beforeAll, mock, afterAll } from "bun:test";
|
|
||||||
import { type App, createApp, createMcpToolCaller } from "core/test/utils";
|
|
||||||
import type { McpServer } from "bknd/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - [x] system_config
|
|
||||||
* - [x] system_build
|
|
||||||
* - [x] system_ping
|
|
||||||
* - [x] system_info
|
|
||||||
*/
|
|
||||||
describe("mcp system", async () => {
|
|
||||||
let app: App;
|
|
||||||
let server: McpServer;
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = createApp({
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
server = app.mcp!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createMcpToolCaller();
|
|
||||||
|
|
||||||
test("system_ping", async () => {
|
|
||||||
const result = await tool(server, "system_ping", {});
|
|
||||||
expect(result).toEqual({ pong: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_info", async () => {
|
|
||||||
const result = await tool(server, "system_info", {});
|
|
||||||
expect(Object.keys(result).length).toBeGreaterThan(0);
|
|
||||||
expect(Object.keys(result)).toContainValues(["version", "runtime", "connection"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_build", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
|
|
||||||
app.emgr.onEvent(AppEvents.AppBuiltEvent, () => void called(), { once: true });
|
|
||||||
|
|
||||||
const result = await tool(server, "system_build", {});
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("system_config", async () => {
|
|
||||||
const result = await tool(server, "system_config", {});
|
|
||||||
expect(result).toEqual(app.toJSON());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import { code, hybrid } from "modes";
|
|
||||||
|
|
||||||
describe("modes", () => {
|
|
||||||
describe("code", () => {
|
|
||||||
test("verify base configuration", async () => {
|
|
||||||
const c = code({}) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(Object.keys(config)).toEqual(["options"]);
|
|
||||||
expect(config.options.mode).toEqual("code");
|
|
||||||
expect(config.options.plugins).toEqual([]);
|
|
||||||
expect(config.options.manager.skipValidation).toEqual(false);
|
|
||||||
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keeps overrides", async () => {
|
|
||||||
const c = code({
|
|
||||||
connection: {
|
|
||||||
url: ":memory:",
|
|
||||||
},
|
|
||||||
}) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(config.connection.url).toEqual(":memory:");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("hybrid", () => {
|
|
||||||
test("fails if no reader is provided", () => {
|
|
||||||
// @ts-ignore
|
|
||||||
expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/);
|
|
||||||
});
|
|
||||||
test("verify base configuration", async () => {
|
|
||||||
const c = hybrid({ reader: async () => ({}) }) as any;
|
|
||||||
const config = await c.app?.({} as any);
|
|
||||||
expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]);
|
|
||||||
expect(config.options.mode).toEqual("db");
|
|
||||||
expect(config.options.plugins).toEqual([]);
|
|
||||||
expect(config.options.manager.skipValidation).toEqual(false);
|
|
||||||
expect(config.options.manager.onModulesBuilt).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { describe, it, expect, mock, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import { syncConfig } from "plugins/dev/sync-config.plugin";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("syncConfig", () => {
|
|
||||||
it("should only sync if enabled", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = createApp();
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
includeFirstBoot: false,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: () => {
|
|
||||||
called();
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should respect secrets", async () => {
|
|
||||||
const called = mock(() => null);
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: async (config) => {
|
|
||||||
called();
|
|
||||||
expect(config.auth.jwt.secret).toBe("test");
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeSecrets: true,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
|
|
||||||
await syncConfig({
|
|
||||||
write: async (config) => {
|
|
||||||
called();
|
|
||||||
// it's an important test, because the `jwt` part is omitted if secrets=false in general app.toJSON()
|
|
||||||
// but it's required to get the app running
|
|
||||||
expect(config.auth.jwt.secret).toBe("");
|
|
||||||
},
|
|
||||||
enabled: true,
|
|
||||||
includeSecrets: false,
|
|
||||||
includeFirstBoot: true,
|
|
||||||
})(app).onBuilt?.();
|
|
||||||
expect(called).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("repros", async () => {
|
describe("repros", async () => {
|
||||||
/**
|
/**
|
||||||
@@ -18,8 +13,8 @@ describe("repros", async () => {
|
|||||||
* There was an issue that AppData had old configs because of system entity "media"
|
* There was an issue that AppData had old configs because of system entity "media"
|
||||||
*/
|
*/
|
||||||
test("registers media entity correctly to relate to it", async () => {
|
test("registers media entity correctly to relate to it", async () => {
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
|
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -76,7 +71,7 @@ describe("repros", async () => {
|
|||||||
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("verify inversedBy", async () => {
|
test.only("verify inversedBy", async () => {
|
||||||
const schema = proto.em(
|
const schema = proto.em(
|
||||||
{
|
{
|
||||||
products: proto.entity("products", {
|
products: proto.entity("products", {
|
||||||
@@ -92,7 +87,7 @@ describe("repros", async () => {
|
|||||||
fns.relation(schema.product_likes).manyToOne(schema.users);
|
fns.relation(schema.product_likes).manyToOne(schema.users);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const app = createApp({ config: { data: schema.toJSON() } });
|
const app = createApp({ initialConfig: { data: schema.toJSON() } });
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
import { Authenticator } from "auth/authenticate/Authenticator";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||||
|
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||||
|
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||||
|
import { parse } from "core/object/schema";
|
||||||
|
|
||||||
|
/*class MemoryUserPool implements UserPool {
|
||||||
|
constructor(private users: User[] = []) {}
|
||||||
|
|
||||||
|
async findBy(prop: "id" | "email" | "username", value: string | number) {
|
||||||
|
return this.users.find((user) => user[prop] === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(user: Pick<User, "email" | "password">) {
|
||||||
|
const id = this.users.length + 1;
|
||||||
|
const newUser = { ...user, id, username: user.email };
|
||||||
|
this.users.push(newUser);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
describe("Authenticator", async () => {
|
describe("Authenticator", async () => {
|
||||||
test("should return auth cookie headers", async () => {
|
test("cookie options", async () => {
|
||||||
const auth = new Authenticator({}, null as any, {
|
console.log("parsed", parse(cookieConfig, undefined));
|
||||||
jwt: {
|
console.log(cookieConfig.template({}));
|
||||||
secret: "secret",
|
|
||||||
fields: [],
|
|
||||||
},
|
|
||||||
cookie: {
|
|
||||||
sameSite: "strict",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const headers = await auth.getAuthCookieHeader("token");
|
|
||||||
const cookie = headers.get("Set-Cookie");
|
|
||||||
expect(cookie).toStartWith("auth=");
|
|
||||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
|
||||||
|
|
||||||
// now expect it to be removed
|
|
||||||
const headers2 = await auth.removeAuthCookieHeader(headers);
|
|
||||||
const cookie2 = headers2.get("Set-Cookie");
|
|
||||||
expect(cookie2).toStartWith("auth=; Max-Age=0; Path=/; Expires=");
|
|
||||||
expect(cookie2).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
|
||||||
});
|
});
|
||||||
|
/*const userpool = new MemoryUserPool([
|
||||||
|
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||||
|
]);
|
||||||
|
|
||||||
test("should return auth cookie string", async () => {
|
test("sha256 login", async () => {
|
||||||
const auth = new Authenticator({}, null as any, {
|
const auth = new Authenticator(userpool, {
|
||||||
jwt: {
|
password: new PasswordStrategy({
|
||||||
secret: "secret",
|
hashing: "sha256",
|
||||||
fields: [],
|
}),
|
||||||
},
|
|
||||||
cookie: {
|
|
||||||
sameSite: "strict",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const cookie = await auth.unsafeGetAuthCookie("token");
|
|
||||||
expect(cookie).toStartWith("auth=");
|
const { token } = await auth.login("password", { email: "d", password: "test" });
|
||||||
expect(cookie).toEndWith("HttpOnly; Secure; SameSite=Strict");
|
expect(token).toBeDefined();
|
||||||
});
|
|
||||||
|
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||||
|
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||||
|
expect(await auth.verify(token)).toBe(true);
|
||||||
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Guard, type GuardConfig } from "auth/authorize/Guard";
|
import { Guard } from "../../../src/auth";
|
||||||
import { Permission } from "auth/authorize/Permission";
|
|
||||||
import { Role, type RoleSchema } from "auth/authorize/Role";
|
|
||||||
import { objectTransform, s } from "bknd/utils";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
function createGuard(
|
|
||||||
permissionNames: string[],
|
|
||||||
roles?: Record<string, Omit<RoleSchema, "name">>,
|
|
||||||
config?: GuardConfig,
|
|
||||||
) {
|
|
||||||
const _roles = roles
|
|
||||||
? objectTransform(roles, ({ permissions = [], is_default, implicit_allow }, name) => {
|
|
||||||
return Role.create(name, { permissions, is_default, implicit_allow });
|
|
||||||
})
|
|
||||||
: {};
|
|
||||||
const _permissions = permissionNames.map((name) => new Permission(name));
|
|
||||||
return new Guard(_permissions, Object.values(_roles), config);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("authorize", () => {
|
describe("authorize", () => {
|
||||||
const read = new Permission("read", {
|
|
||||||
filterable: true,
|
|
||||||
});
|
|
||||||
const write = new Permission("write");
|
|
||||||
|
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
const guard = createGuard(
|
const guard = Guard.create(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
@@ -42,14 +16,14 @@ describe("authorize", () => {
|
|||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
|
|
||||||
expect(() => guard.granted(new Permission("something"), {})).toThrow();
|
expect(() => guard.granted("something")).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("with default", async () => {
|
test("with default", async () => {
|
||||||
const guard = createGuard(
|
const guard = Guard.create(
|
||||||
["read", "write"],
|
["read", "write"],
|
||||||
{
|
{
|
||||||
admin: {
|
admin: {
|
||||||
@@ -63,26 +37,26 @@ describe("authorize", () => {
|
|||||||
{ enabled: true },
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(() => guard.granted(write, {})).toThrow();
|
expect(guard.granted("write")).toBe(false);
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard implicit allow", async () => {
|
test("guard implicit allow", async () => {
|
||||||
const guard = createGuard([], {}, { enabled: false });
|
const guard = Guard.create([], {}, { enabled: false });
|
||||||
|
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(guard.granted(write, {})).toBeUndefined();
|
expect(guard.granted("write")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("role implicit allow", async () => {
|
test("role implicit allow", async () => {
|
||||||
const guard = createGuard(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
admin: {
|
admin: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
},
|
},
|
||||||
@@ -92,12 +66,12 @@ describe("authorize", () => {
|
|||||||
role: "admin",
|
role: "admin",
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(guard.granted(read, user)).toBeUndefined();
|
expect(guard.granted("read", user)).toBe(true);
|
||||||
expect(guard.granted(write, user)).toBeUndefined();
|
expect(guard.granted("write", user)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guard with guest role implicit allow", async () => {
|
test("guard with guest role implicit allow", async () => {
|
||||||
const guard = createGuard(["read", "write"], {
|
const guard = Guard.create(["read", "write"], {
|
||||||
guest: {
|
guest: {
|
||||||
implicit_allow: true,
|
implicit_allow: true,
|
||||||
is_default: true,
|
is_default: true,
|
||||||
@@ -105,143 +79,7 @@ describe("authorize", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(guard.getUserRole()?.name).toBe("guest");
|
expect(guard.getUserRole()?.name).toBe("guest");
|
||||||
expect(guard.granted(read, {})).toBeUndefined();
|
expect(guard.granted("read")).toBe(true);
|
||||||
expect(guard.granted(write, {})).toBeUndefined();
|
expect(guard.granted("write")).toBe(true);
|
||||||
});
|
|
||||||
|
|
||||||
describe("cases", () => {
|
|
||||||
test("guest none, member deny if user.enabled is false", () => {
|
|
||||||
const guard = createGuard(
|
|
||||||
["read"],
|
|
||||||
{
|
|
||||||
guest: {
|
|
||||||
is_default: true,
|
|
||||||
},
|
|
||||||
member: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
type: "member",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
"user.enabled": false,
|
|
||||||
},
|
|
||||||
effect: "deny",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ enabled: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(() => guard.granted(read, { role: "guest" })).toThrow();
|
|
||||||
|
|
||||||
// member is allowed, because default role permission effect is allow
|
|
||||||
// and no deny policy is met
|
|
||||||
expect(guard.granted(read, { role: "member" })).toBeUndefined();
|
|
||||||
|
|
||||||
// member is allowed, because deny policy is not met
|
|
||||||
expect(guard.granted(read, { role: "member", enabled: true })).toBeUndefined();
|
|
||||||
|
|
||||||
// member is denied, because deny policy is met
|
|
||||||
expect(() => guard.granted(read, { role: "member", enabled: false })).toThrow();
|
|
||||||
|
|
||||||
// get the filter for member role
|
|
||||||
expect(guard.filters(read, { role: "member" }).filter).toEqual({
|
|
||||||
type: "member",
|
|
||||||
});
|
|
||||||
|
|
||||||
// get filter for guest
|
|
||||||
expect(guard.filters(read, {}).filter).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("guest should only read posts that are public", () => {
|
|
||||||
const read = new Permission(
|
|
||||||
"read",
|
|
||||||
{
|
|
||||||
// make this permission filterable
|
|
||||||
// without this, `filter` policies have no effect
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
// expect the context to match this schema
|
|
||||||
// otherwise exit with 500 to ensure proper policy checking
|
|
||||||
s.object({
|
|
||||||
entity: s.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const guard = createGuard(
|
|
||||||
["read"],
|
|
||||||
{
|
|
||||||
guest: {
|
|
||||||
// this permission is applied if no (or invalid) role is provided
|
|
||||||
is_default: true,
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
// effect deny means only having this permission, doesn't guarantee access
|
|
||||||
effect: "deny",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
// only if this condition is met
|
|
||||||
condition: {
|
|
||||||
entity: {
|
|
||||||
$in: ["posts"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// the effect is allow
|
|
||||||
effect: "allow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
// members should be allowed to read all
|
|
||||||
member: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "read",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ enabled: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// guest can only read posts
|
|
||||||
expect(guard.granted(read, {}, { entity: "posts" })).toBeUndefined();
|
|
||||||
expect(() => guard.granted(read, {}, { entity: "users" })).toThrow();
|
|
||||||
|
|
||||||
// and guests can only read public posts
|
|
||||||
expect(guard.filters(read, {}, { entity: "posts" }).filter).toEqual({
|
|
||||||
public: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// member can read posts and users
|
|
||||||
expect(guard.granted(read, { role: "member" }, { entity: "posts" })).toBeUndefined();
|
|
||||||
expect(guard.granted(read, { role: "member" }, { entity: "users" })).toBeUndefined();
|
|
||||||
|
|
||||||
// member should not have a filter
|
|
||||||
expect(
|
|
||||||
guard.filters(read, { role: "member" }, { entity: "posts" }).filter,
|
|
||||||
).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createApp } from "core/test/utils";
|
|
||||||
import type { CreateAppConfig } from "App";
|
|
||||||
import * as proto from "data/prototype";
|
|
||||||
import { mergeObject } from "core/utils/objects";
|
|
||||||
import type { App, DB } from "bknd";
|
|
||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
async function makeApp(config: Partial<CreateAppConfig["config"]> = {}) {
|
|
||||||
const app = createApp({
|
|
||||||
config: mergeObject(
|
|
||||||
{
|
|
||||||
data: proto
|
|
||||||
.em(
|
|
||||||
{
|
|
||||||
users: proto.systemEntity("users", {}),
|
|
||||||
posts: proto.entity("posts", {
|
|
||||||
title: proto.text(),
|
|
||||||
content: proto.text(),
|
|
||||||
}),
|
|
||||||
comments: proto.entity("comments", {
|
|
||||||
content: proto.text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
({ relation }, { users, posts, comments }) => {
|
|
||||||
relation(posts).manyToOne(users);
|
|
||||||
relation(comments).manyToOne(posts);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.toJSON(),
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "secret",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
config,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUsers(app: App, users: CreateUserPayload[]) {
|
|
||||||
return Promise.all(
|
|
||||||
users.map(async (user) => {
|
|
||||||
return await app.createUser(user);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFixtures(app: App, fixtures: Record<string, any[]> = {}) {
|
|
||||||
const results = {} as any;
|
|
||||||
for (const [entity, data] of Object.entries(fixtures)) {
|
|
||||||
results[entity] = await app.em
|
|
||||||
.mutator(entity as any)
|
|
||||||
.insertMany(data)
|
|
||||||
.then((result) => result.data);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("data permissions", async () => {
|
|
||||||
const app = await makeApp({
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
guard: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
roles: {
|
|
||||||
guest: {
|
|
||||||
is_default: true,
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
permission: "system.access.api",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.read",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.create",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.update",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: {
|
|
||||||
entity: "posts",
|
|
||||||
},
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permission: "data.entity.delete",
|
|
||||||
policies: [
|
|
||||||
{
|
|
||||||
condition: { entity: "posts" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: { entity: "posts" },
|
|
||||||
effect: "filter",
|
|
||||||
filter: {
|
|
||||||
users_id: { $isnull: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const users = [
|
|
||||||
{ email: "foo@example.com", password: "password" },
|
|
||||||
{ email: "bar@example.com", password: "password" },
|
|
||||||
];
|
|
||||||
const fixtures = {
|
|
||||||
posts: [
|
|
||||||
{ content: "post 1", users_id: 1 },
|
|
||||||
{ content: "post 2", users_id: 2 },
|
|
||||||
{ content: "post 3", users_id: null },
|
|
||||||
],
|
|
||||||
comments: [
|
|
||||||
{ content: "comment 1", posts_id: 1 },
|
|
||||||
{ content: "comment 2", posts_id: 2 },
|
|
||||||
{ content: "comment 3", posts_id: 3 },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await createUsers(app, users);
|
|
||||||
const results = await loadFixtures(app, fixtures);
|
|
||||||
|
|
||||||
describe("http", async () => {
|
|
||||||
it("read many", async () => {
|
|
||||||
// many only includes posts with users_id is null
|
|
||||||
const res = await app.server.request("/api/data/entity/posts");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(data).toEqual([results.posts[2]]);
|
|
||||||
|
|
||||||
// same with /query
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/query", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(data).toEqual([results.posts[2]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("read one", async () => {
|
|
||||||
// one only includes posts with users_id is null
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(404);
|
|
||||||
expect(data).toBeUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
// read one by allowed id
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(data).toEqual(results.posts[2]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("read many by reference", async () => {
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1/comments");
|
|
||||||
const data = await res.json().then((r: any) => r.data);
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(data).toEqual(results.comments.filter((c: any) => c.posts_id === 1));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation create one", async () => {
|
|
||||||
// not allowed
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ content: "post 4" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
}
|
|
||||||
// allowed
|
|
||||||
{
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ content: "post 4", users_id: null }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(201);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation update one", async () => {
|
|
||||||
// update one: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({ content: "post 4" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
|
|
||||||
{
|
|
||||||
// update one: allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
|
||||||
method: "PATCH",
|
|
||||||
body: JSON.stringify({ content: "post 3 (updated)" }),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.json().then((r: any) => r.data.content)).toBe("post 3 (updated)");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation update many", async () => {
|
|
||||||
// update many: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
update: { content: "post 4" },
|
|
||||||
where: { users_id: { $isnull: 0 } },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200); // because filtered
|
|
||||||
const _data = await res.json().then((r: any) => r.data.map((p: any) => p.users_id));
|
|
||||||
expect(_data.every((u: any) => u === null)).toBe(true);
|
|
||||||
|
|
||||||
// verify
|
|
||||||
const data = await app.em
|
|
||||||
.repo("posts")
|
|
||||||
.findMany({ select: ["content", "users_id"] })
|
|
||||||
.then((r) => r.data);
|
|
||||||
|
|
||||||
// expect non null users_id to not have content "post 4"
|
|
||||||
expect(
|
|
||||||
data.filter((p: any) => p.users_id !== null).every((p: any) => p.content !== "post 4"),
|
|
||||||
).toBe(true);
|
|
||||||
// expect null users_id to have content "post 4"
|
|
||||||
expect(
|
|
||||||
data.filter((p: any) => p.users_id === null).every((p: any) => p.content === "post 4"),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const count = async () => {
|
|
||||||
const {
|
|
||||||
data: { count: _count },
|
|
||||||
} = await app.em.repo("posts").count();
|
|
||||||
return _count;
|
|
||||||
};
|
|
||||||
it("mutation delete one", async () => {
|
|
||||||
const initial = await count();
|
|
||||||
|
|
||||||
// delete one: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/1", {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
expect(await count()).toBe(initial);
|
|
||||||
|
|
||||||
{
|
|
||||||
// delete one: allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts/3", {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await count()).toBe(initial - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("mutation delete many", async () => {
|
|
||||||
// delete many: not allowed
|
|
||||||
const res = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
where: {},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
|
|
||||||
// only deleted posts with users_id is null
|
|
||||||
const remaining = await app.em
|
|
||||||
.repo("posts")
|
|
||||||
.findMany()
|
|
||||||
.then((r) => r.data);
|
|
||||||
expect(remaining.every((p: any) => p.users_id !== null)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createAuthTestApp } from "./shared";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { em, entity, text } from "data/prototype";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const schema = em(
|
|
||||||
{
|
|
||||||
posts: entity("posts", {
|
|
||||||
title: text(),
|
|
||||||
content: text(),
|
|
||||||
}),
|
|
||||||
comments: entity("comments", {
|
|
||||||
content: text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
({ relation }, { posts, comments }) => {
|
|
||||||
relation(posts).manyToOne(comments);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("DataController (auth)", () => {
|
|
||||||
test("reading schema.json", async () => {
|
|
||||||
const { request } = await createAuthTestApp(
|
|
||||||
{
|
|
||||||
permission: ["system.access.api", "data.entity.read", "system.schema.read"],
|
|
||||||
request: new Request("http://localhost/api/data/schema.json"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { data: schema.toJSON() },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect((await request.guest()).status).toBe(403);
|
|
||||||
expect((await request.member()).status).toBe(403);
|
|
||||||
expect((await request.authorized()).status).toBe(200);
|
|
||||||
expect((await request.admin()).status).toBe(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { createAuthTestApp } from "./shared";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("SystemController (auth)", () => {
|
|
||||||
test("reading info", async () => {
|
|
||||||
const { request } = await createAuthTestApp({
|
|
||||||
permission: ["system.access.api", "system.info"],
|
|
||||||
request: new Request("http://localhost/api/system/info"),
|
|
||||||
});
|
|
||||||
expect((await request.guest()).status).toBe(403);
|
|
||||||
expect((await request.member()).status).toBe(403);
|
|
||||||
expect((await request.authorized()).status).toBe(200);
|
|
||||||
expect((await request.admin()).status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("reading permissions", async () => {
|
|
||||||
const { request } = await createAuthTestApp({
|
|
||||||
permission: ["system.access.api", "system.schema.read"],
|
|
||||||
request: new Request("http://localhost/api/system/permissions"),
|
|
||||||
});
|
|
||||||
expect((await request.guest()).status).toBe(403);
|
|
||||||
expect((await request.member()).status).toBe(403);
|
|
||||||
expect((await request.authorized()).status).toBe(200);
|
|
||||||
expect((await request.admin()).status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("access openapi", async () => {
|
|
||||||
const { request } = await createAuthTestApp({
|
|
||||||
permission: ["system.access.api", "system.openapi"],
|
|
||||||
request: new Request("http://localhost/api/system/openapi.json"),
|
|
||||||
});
|
|
||||||
expect((await request.guest()).status).toBe(403);
|
|
||||||
expect((await request.member()).status).toBe(403);
|
|
||||||
expect((await request.authorized()).status).toBe(200);
|
|
||||||
expect((await request.admin()).status).toBe(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import { createApp } from "core/test/utils";
|
|
||||||
import type { CreateAppConfig } from "App";
|
|
||||||
import type { RoleSchema } from "auth/authorize/Role";
|
|
||||||
import { isPlainObject } from "core/utils";
|
|
||||||
|
|
||||||
export type AuthTestConfig = {
|
|
||||||
guest?: RoleSchema;
|
|
||||||
member?: RoleSchema;
|
|
||||||
authorized?: RoleSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createAuthTestApp(
|
|
||||||
testConfig: {
|
|
||||||
permission: AuthTestConfig | string | string[];
|
|
||||||
request: Request;
|
|
||||||
},
|
|
||||||
config: Partial<CreateAppConfig> = {},
|
|
||||||
) {
|
|
||||||
let member: RoleSchema | undefined;
|
|
||||||
let authorized: RoleSchema | undefined;
|
|
||||||
let guest: RoleSchema | undefined;
|
|
||||||
if (isPlainObject(testConfig.permission)) {
|
|
||||||
if (testConfig.permission.guest)
|
|
||||||
guest = {
|
|
||||||
...testConfig.permission.guest,
|
|
||||||
is_default: true,
|
|
||||||
};
|
|
||||||
if (testConfig.permission.member) member = testConfig.permission.member;
|
|
||||||
if (testConfig.permission.authorized) authorized = testConfig.permission.authorized;
|
|
||||||
} else {
|
|
||||||
member = {
|
|
||||||
permissions: [],
|
|
||||||
};
|
|
||||||
authorized = {
|
|
||||||
permissions: Array.isArray(testConfig.permission)
|
|
||||||
? testConfig.permission
|
|
||||||
: [testConfig.permission],
|
|
||||||
};
|
|
||||||
guest = {
|
|
||||||
permissions: [],
|
|
||||||
is_default: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("authorized", authorized);
|
|
||||||
|
|
||||||
const app = createApp({
|
|
||||||
...config,
|
|
||||||
config: {
|
|
||||||
...config.config,
|
|
||||||
auth: {
|
|
||||||
...config.config?.auth,
|
|
||||||
enabled: true,
|
|
||||||
guard: {
|
|
||||||
enabled: true,
|
|
||||||
...config.config?.auth?.guard,
|
|
||||||
},
|
|
||||||
jwt: {
|
|
||||||
...config.config?.auth?.jwt,
|
|
||||||
secret: "secret",
|
|
||||||
},
|
|
||||||
roles: {
|
|
||||||
...config.config?.auth?.roles,
|
|
||||||
guest,
|
|
||||||
member,
|
|
||||||
authorized,
|
|
||||||
admin: {
|
|
||||||
implicit_allow: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
const users = {
|
|
||||||
guest: null,
|
|
||||||
member: await app.createUser({
|
|
||||||
email: "member@test.com",
|
|
||||||
password: "12345678",
|
|
||||||
role: "member",
|
|
||||||
}),
|
|
||||||
authorized: await app.createUser({
|
|
||||||
email: "authorized@test.com",
|
|
||||||
password: "12345678",
|
|
||||||
role: "authorized",
|
|
||||||
}),
|
|
||||||
admin: await app.createUser({
|
|
||||||
email: "admin@test.com",
|
|
||||||
password: "12345678",
|
|
||||||
role: "admin",
|
|
||||||
}),
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const tokens = {} as Record<keyof typeof users, string>;
|
|
||||||
for (const [key, user] of Object.entries(users)) {
|
|
||||||
if (user) {
|
|
||||||
tokens[key as keyof typeof users] = await app.module.auth.authenticator.jwt(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makeRequest(user: keyof typeof users, input: string, init: RequestInit = {}) {
|
|
||||||
const headers = new Headers(init.headers ?? {});
|
|
||||||
if (user in tokens) {
|
|
||||||
headers.set("Authorization", `Bearer ${tokens[user as keyof typeof tokens]}`);
|
|
||||||
}
|
|
||||||
const res = await app.server.request(input, {
|
|
||||||
...init,
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
let data: any;
|
|
||||||
if (res.headers.get("Content-Type")?.startsWith("application/json")) {
|
|
||||||
data = await res.json();
|
|
||||||
} else if (res.headers.get("Content-Type")?.startsWith("text/")) {
|
|
||||||
data = await res.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: res.status,
|
|
||||||
ok: res.ok,
|
|
||||||
headers: Object.fromEntries(res.headers.entries()),
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestFn = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get(_, prop: keyof typeof users) {
|
|
||||||
return async (input: string, init: RequestInit = {}) => {
|
|
||||||
return makeRequest(prop, input, init);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) as {
|
|
||||||
[K in keyof typeof users]: (
|
|
||||||
input: string,
|
|
||||||
init?: RequestInit,
|
|
||||||
) => Promise<{
|
|
||||||
status: number;
|
|
||||||
ok: boolean;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
data: any;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const request = new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
get(_, prop: keyof typeof users) {
|
|
||||||
return async () => {
|
|
||||||
return makeRequest(prop, testConfig.request.url, {
|
|
||||||
headers: testConfig.request.headers,
|
|
||||||
method: testConfig.request.method,
|
|
||||||
body: testConfig.request.body,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
) as {
|
|
||||||
[K in keyof typeof users]: () => Promise<{
|
|
||||||
status: number;
|
|
||||||
ok: boolean;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
data: any;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return { app, users, request, requestFn };
|
|
||||||
}
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { s } from "bknd/utils";
|
|
||||||
import { Permission } from "auth/authorize/Permission";
|
|
||||||
import { Policy } from "auth/authorize/Policy";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { getPermissionRoutes, permission } from "auth/middlewares/permission.middleware";
|
|
||||||
import { auth } from "auth/middlewares/auth.middleware";
|
|
||||||
import { Guard, mergeFilters, type GuardConfig } from "auth/authorize/Guard";
|
|
||||||
import { Role, RolePermission } from "auth/authorize/Role";
|
|
||||||
import { Exception } from "bknd";
|
|
||||||
import { convert } from "core/object/query/object-query";
|
|
||||||
|
|
||||||
describe("Permission", () => {
|
|
||||||
it("works with minimal schema", () => {
|
|
||||||
expect(() => new Permission("test")).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses context", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test3",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => p.parseContext({ a: [] })).toThrow();
|
|
||||||
expect(p.parseContext({ a: "test" })).toEqual({ a: "test" });
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(p.parseContext({ a: 1 })).toEqual({ a: "1" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Policy", () => {
|
|
||||||
it("works with minimal schema", () => {
|
|
||||||
expect(() => new Policy().toJSON()).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("checks condition", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(p.meetsCondition({ a: 1 })).toBe(true);
|
|
||||||
expect(p.meetsCondition({ a: 2 })).toBe(false);
|
|
||||||
expect(p.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
|
||||||
expect(p.meetsCondition({})).toBe(false);
|
|
||||||
|
|
||||||
const p2 = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $gt: 1 },
|
|
||||||
$or: {
|
|
||||||
b: { $lt: 2 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(p2.meetsCondition({ a: 2 })).toBe(true);
|
|
||||||
expect(p2.meetsCondition({ a: 1 })).toBe(false);
|
|
||||||
expect(p2.meetsCondition({ a: 1, b: 1 })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
filter: {
|
|
||||||
age: { $gt: 18 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const subjects = [{ age: 19 }, { age: 17 }, { age: 12 }];
|
|
||||||
|
|
||||||
expect(p.getFiltered(subjects)).toEqual([{ age: 19 }]);
|
|
||||||
|
|
||||||
expect(p.meetsFilter({ age: 19 })).toBe(true);
|
|
||||||
expect(p.meetsFilter({ age: 17 })).toBe(false);
|
|
||||||
expect(p.meetsFilter({ age: 12 })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces placeholders", () => {
|
|
||||||
const p = new Policy({
|
|
||||||
condition: {
|
|
||||||
a: "@auth.username",
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
a: "@auth.username",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const vars = { auth: { username: "test" } };
|
|
||||||
|
|
||||||
expect(p.meetsCondition({ a: "test" }, vars)).toBe(true);
|
|
||||||
expect(p.meetsCondition({ a: "test2" }, vars)).toBe(false);
|
|
||||||
expect(p.meetsCondition({ a: "test2" })).toBe(false);
|
|
||||||
expect(p.meetsFilter({ a: "test" }, vars)).toBe(true);
|
|
||||||
expect(p.meetsFilter({ a: "test2" }, vars)).toBe(false);
|
|
||||||
expect(p.meetsFilter({ a: "test2" })).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Guard", () => {
|
|
||||||
it("collects filters", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: { a: { $eq: 1 } },
|
|
||||||
filter: { foo: "bar" },
|
|
||||||
effect: "filter",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const guard = new Guard([p], [r], {
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
expect(guard.filters(p, { role: r.name }, { a: 1 }).filter).toEqual({ foo: "bar" });
|
|
||||||
expect(guard.filters(p, { role: r.name }, { a: 2 }).filter).toBeUndefined();
|
|
||||||
// if no user context given, filter cannot be applied
|
|
||||||
expect(guard.filters(p, {}, { a: 1 }).filter).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("collects filters for default role", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role(
|
|
||||||
"test",
|
|
||||||
[
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: { a: { $eq: 1 } },
|
|
||||||
filter: { foo: "bar" },
|
|
||||||
effect: "filter",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const guard = new Guard([p], [r], {
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
guard.filters(
|
|
||||||
p,
|
|
||||||
{
|
|
||||||
role: r.name,
|
|
||||||
},
|
|
||||||
{ a: 1 },
|
|
||||||
).filter,
|
|
||||||
).toEqual({ foo: "bar" });
|
|
||||||
expect(
|
|
||||||
guard.filters(
|
|
||||||
p,
|
|
||||||
{
|
|
||||||
role: r.name,
|
|
||||||
},
|
|
||||||
{ a: 2 },
|
|
||||||
).filter,
|
|
||||||
).toBeUndefined();
|
|
||||||
// if no user context given, the default role is applied
|
|
||||||
// hence it can be found
|
|
||||||
expect(guard.filters(p, {}, { a: 1 }).filter).toEqual({ foo: "bar" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("merges filters correctly", () => {
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { baz: "qux" })).toEqual({
|
|
||||||
foo: { $eq: "bar" },
|
|
||||||
baz: { $eq: "qux" },
|
|
||||||
});
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { baz: { $eq: "qux" } })).toEqual({
|
|
||||||
foo: { $eq: "bar" },
|
|
||||||
baz: { $eq: "qux" },
|
|
||||||
});
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { foo: "baz" })).toEqual({ foo: { $eq: "baz" } });
|
|
||||||
|
|
||||||
expect(mergeFilters({ foo: "bar" }, { foo: { $lt: 1 } })).toEqual({
|
|
||||||
foo: { $eq: "bar", $lt: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// overwrite base $or with priority
|
|
||||||
expect(mergeFilters({ $or: { foo: "one" } }, { foo: "bar" })).toEqual({
|
|
||||||
$or: {
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// ignore base $or if priority has different key
|
|
||||||
expect(mergeFilters({ $or: { other: "one" } }, { foo: "bar" })).toEqual({
|
|
||||||
$or: {
|
|
||||||
other: {
|
|
||||||
$eq: "one",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
foo: {
|
|
||||||
$eq: "bar",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("permission middleware", () => {
|
|
||||||
const makeApp = (
|
|
||||||
permissions: Permission<any, any, any, any>[],
|
|
||||||
roles: Role[] = [],
|
|
||||||
config: Partial<GuardConfig> = {},
|
|
||||||
) => {
|
|
||||||
const app = {
|
|
||||||
module: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
ctx: () => ({
|
|
||||||
guard: new Guard(permissions, roles, {
|
|
||||||
enabled: true,
|
|
||||||
...config,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return new Hono()
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("app", app);
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.use(auth())
|
|
||||||
.onError((err, c) => {
|
|
||||||
if (err instanceof Exception) {
|
|
||||||
return c.json(err.toJSON(), err.code as any);
|
|
||||||
}
|
|
||||||
return c.json({ error: err.message }, "code" in err ? (err.code as any) : 500);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
it("allows if guard is disabled", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const hono = makeApp([p], [], { enabled: false }).get("/test", permission(p, {}), async (c) =>
|
|
||||||
c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
expect(await res.text()).toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("denies if guard is enabled", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const hono = makeApp([p]).get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user has (plain) role", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = Role.create("test", { permissions: [p.name] });
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user has role with policy", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $gte: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r], {
|
|
||||||
context: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("denies if user with role doesn't meet condition", async () => {
|
|
||||||
const p = new Permission("test");
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(
|
|
||||||
p,
|
|
||||||
[
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $lt: 1 },
|
|
||||||
},
|
|
||||||
// default effect is allow
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// change default effect to deny if no condition is met
|
|
||||||
"deny",
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r], {
|
|
||||||
context: {
|
|
||||||
a: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get("/test", permission(p, {}), async (c) => c.text("test"));
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows if user with role doesn't meet condition (from middleware)", async () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/test",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if permission context is invalid", async () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number({ minimum: 2 }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
const hono = makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user: { id: 0, role: r.name } });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/test",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await hono.request("/test");
|
|
||||||
// expecting 500 because bknd should have handled it correctly
|
|
||||||
expect(res.status).toBe(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("checks context on routes with permissions", async () => {
|
|
||||||
const make = (user: any) => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{},
|
|
||||||
s.object({
|
|
||||||
a: s.number(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role("test", [
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
return makeApp([p], [r])
|
|
||||||
.use(async (c, next) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
c.set("auth", { registered: true, user });
|
|
||||||
await next();
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"/valid",
|
|
||||||
permission(p, {
|
|
||||||
context: (c) => ({
|
|
||||||
a: 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid",
|
|
||||||
permission(p, {
|
|
||||||
// @ts-expect-error
|
|
||||||
context: (c) => ({
|
|
||||||
b: "1",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid2",
|
|
||||||
permission(p, {
|
|
||||||
// @ts-expect-error
|
|
||||||
context: (c) => ({}),
|
|
||||||
}),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
"/invalid3",
|
|
||||||
// @ts-expect-error
|
|
||||||
permission(p),
|
|
||||||
async (c) => c.text("test"),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hono = make({ id: 0, role: "test" });
|
|
||||||
const valid = await hono.request("/valid");
|
|
||||||
expect(valid.status).toBe(200);
|
|
||||||
const invalid = await hono.request("/invalid");
|
|
||||||
expect(invalid.status).toBe(500);
|
|
||||||
const invalid2 = await hono.request("/invalid2");
|
|
||||||
expect(invalid2.status).toBe(500);
|
|
||||||
const invalid3 = await hono.request("/invalid3");
|
|
||||||
expect(invalid3.status).toBe(500);
|
|
||||||
|
|
||||||
{
|
|
||||||
const hono = make(null);
|
|
||||||
const valid = await hono.request("/valid");
|
|
||||||
expect(valid.status).toBe(403);
|
|
||||||
const invalid = await hono.request("/invalid");
|
|
||||||
expect(invalid.status).toBe(500);
|
|
||||||
const invalid2 = await hono.request("/invalid2");
|
|
||||||
expect(invalid2.status).toBe(500);
|
|
||||||
const invalid3 = await hono.request("/invalid3");
|
|
||||||
expect(invalid3.status).toBe(500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Role", () => {
|
|
||||||
it("serializes and deserializes", () => {
|
|
||||||
const p = new Permission(
|
|
||||||
"test",
|
|
||||||
{
|
|
||||||
filterable: true,
|
|
||||||
},
|
|
||||||
s.object({
|
|
||||||
a: s.number({ minimum: 2 }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const r = new Role(
|
|
||||||
"test",
|
|
||||||
[
|
|
||||||
new RolePermission(p, [
|
|
||||||
new Policy({
|
|
||||||
condition: {
|
|
||||||
a: { $eq: 1 },
|
|
||||||
},
|
|
||||||
effect: "deny",
|
|
||||||
filter: {
|
|
||||||
b: { $lt: 1 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const json = JSON.parse(JSON.stringify(r.toJSON()));
|
|
||||||
const r2 = Role.create(p.name, json);
|
|
||||||
expect(r2.toJSON()).toEqual(r.toJSON());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { PasswordStrategy } from "auth/authenticate/strategies/PasswordStrategy";
|
|
||||||
import { describe, expect, it } from "bun:test";
|
|
||||||
|
|
||||||
describe("PasswordStrategy", () => {
|
|
||||||
it("should enforce provided minimum length", async () => {
|
|
||||||
const strategy = new PasswordStrategy({ minLength: 8, hashing: "plain" });
|
|
||||||
|
|
||||||
expect(strategy.verify("password")({} as any)).rejects.toThrow();
|
|
||||||
expect(
|
|
||||||
strategy.verify("password1234")({ strategy_value: "password1234" } as any),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
||||||
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Registry } from "core/registry/Registry";
|
import { Registry } from "core";
|
||||||
import { s } from "core/utils/schema";
|
import { s } from "core/object/schema";
|
||||||
|
|
||||||
type Constructor<T> = new (...args: any[]) => T;
|
type Constructor<T> = new (...args: any[]) => T;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { s } from "core/utils/schema";
|
import { SchemaObject } from "../../../src/core";
|
||||||
import { SchemaObject } from "core/object/SchemaObject";
|
import { s } from "core/object/schema";
|
||||||
|
|
||||||
describe("SchemaObject", async () => {
|
describe("SchemaObject", async () => {
|
||||||
test("basic", async () => {
|
test("basic", async () => {
|
||||||
|
|||||||
@@ -66,14 +66,4 @@ describe("object-query", () => {
|
|||||||
expect(result).toBe(expected);
|
expect(result).toBe(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("paths", () => {
|
|
||||||
const result = validate({ "user.age": { $lt: 18 } }, { user: { age: 17 } });
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("empty filters", () => {
|
|
||||||
const result = validate({}, { user: { age: 17 } });
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import {
|
|
||||||
makeValidator,
|
|
||||||
exp,
|
|
||||||
Expression,
|
|
||||||
isPrimitive,
|
|
||||||
type Primitive,
|
|
||||||
} from "../../../src/core/object/query/query";
|
|
||||||
|
|
||||||
describe("query", () => {
|
|
||||||
test("isPrimitive", () => {
|
|
||||||
expect(isPrimitive(1)).toBe(true);
|
|
||||||
expect(isPrimitive("1")).toBe(true);
|
|
||||||
expect(isPrimitive(true)).toBe(true);
|
|
||||||
expect(isPrimitive(false)).toBe(true);
|
|
||||||
|
|
||||||
// not primitives
|
|
||||||
expect(isPrimitive(null)).toBe(false);
|
|
||||||
expect(isPrimitive(undefined)).toBe(false);
|
|
||||||
expect(isPrimitive([])).toBe(false);
|
|
||||||
expect(isPrimitive({})).toBe(false);
|
|
||||||
expect(isPrimitive(Symbol("test"))).toBe(false);
|
|
||||||
expect(isPrimitive(new Date())).toBe(false);
|
|
||||||
expect(isPrimitive(new Error())).toBe(false);
|
|
||||||
expect(isPrimitive(new Set())).toBe(false);
|
|
||||||
expect(isPrimitive(new Map())).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("strict expression creation", () => {
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp()).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("invalid")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq")).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", 1)).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", () => null)).toThrow();
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(() => exp("$eq", () => null, 1)).toThrow();
|
|
||||||
expect(
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
).toBeInstanceOf(Expression);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("$eq is required", () => {
|
|
||||||
expect(() => makeValidator([])).toThrow();
|
|
||||||
expect(() =>
|
|
||||||
makeValidator([
|
|
||||||
exp(
|
|
||||||
"$valid",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
).toThrow();
|
|
||||||
expect(
|
|
||||||
makeValidator([
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
() => true,
|
|
||||||
() => null,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("validates filter structure", () => {
|
|
||||||
const validator = makeValidator([
|
|
||||||
exp(
|
|
||||||
"$eq",
|
|
||||||
(v: Primitive) => isPrimitive(v),
|
|
||||||
(e, a) => e === a,
|
|
||||||
),
|
|
||||||
exp(
|
|
||||||
"$like",
|
|
||||||
(v: string) => typeof v === "string",
|
|
||||||
(e, a) => e === a,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// @ts-expect-error intentionally typed as union of given expression keys
|
|
||||||
expect(validator.expressionKeys).toEqual(["$eq", "$like"]);
|
|
||||||
|
|
||||||
// @ts-expect-error "$and" is not allowed
|
|
||||||
expect(() => validator.convert({ $and: {} })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "$or" must be an object
|
|
||||||
expect(() => validator.convert({ $or: [] })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "invalid" is not a valid expression key
|
|
||||||
expect(() => validator.convert({ foo: { invalid: "bar" } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "invalid" is not a valid expression key
|
|
||||||
expect(() => validator.convert({ foo: { $invalid: "bar" } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error "null" is not a valid value
|
|
||||||
expect(() => validator.convert({ foo: null })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error only primitives are allowed for $eq
|
|
||||||
expect(() => validator.convert({ foo: { $eq: [] } })).toThrow();
|
|
||||||
|
|
||||||
// @ts-expect-error only strings are allowed for $like
|
|
||||||
expect(() => validator.convert({ foo: { $like: 1 } })).toThrow();
|
|
||||||
|
|
||||||
// undefined values are ignored
|
|
||||||
expect(validator.convert({ foo: undefined })).toEqual({});
|
|
||||||
|
|
||||||
expect(validator.convert({ foo: "bar" })).toEqual({ foo: { $eq: "bar" } });
|
|
||||||
expect(validator.convert({ foo: { $eq: "bar" } })).toEqual({ foo: { $eq: "bar" } });
|
|
||||||
expect(validator.convert({ foo: { $like: "bar" } })).toEqual({ foo: { $like: "bar" } });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -194,182 +194,6 @@ describe("Core Utils", async () => {
|
|||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("recursivelyReplacePlaceholders", () => {
|
|
||||||
// test basic replacement with simple pattern
|
|
||||||
const obj1 = { a: "Hello, {$name}!", b: { c: "Hello, {$name}!" } };
|
|
||||||
const variables1 = { name: "John" };
|
|
||||||
const result1 = utils.recursivelyReplacePlaceholders(obj1, /\{\$(\w+)\}/g, variables1);
|
|
||||||
expect(result1).toEqual({ a: "Hello, John!", b: { c: "Hello, John!" } });
|
|
||||||
|
|
||||||
// test the specific example from the user request
|
|
||||||
const obj2 = { some: "value", here: "@auth.user" };
|
|
||||||
const variables2 = { auth: { user: "what" } };
|
|
||||||
const result2 = utils.recursivelyReplacePlaceholders(obj2, /^@([a-z\.]+)$/, variables2);
|
|
||||||
expect(result2).toEqual({ some: "value", here: "what" });
|
|
||||||
|
|
||||||
// test with arrays
|
|
||||||
const obj3 = { items: ["@config.name", "static", "@config.version"] };
|
|
||||||
const variables3 = { config: { name: "MyApp", version: "1.0.0" } };
|
|
||||||
const result3 = utils.recursivelyReplacePlaceholders(obj3, /^@([a-z\.]+)$/, variables3);
|
|
||||||
expect(result3).toEqual({ items: ["MyApp", "static", "1.0.0"] });
|
|
||||||
|
|
||||||
// test with nested objects and deep paths
|
|
||||||
const obj4 = {
|
|
||||||
user: "@auth.user.name",
|
|
||||||
settings: {
|
|
||||||
theme: "@ui.theme",
|
|
||||||
nested: {
|
|
||||||
value: "@deep.nested.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const variables4 = {
|
|
||||||
auth: { user: { name: "Alice" } },
|
|
||||||
ui: { theme: "dark" },
|
|
||||||
deep: { nested: { value: "found" } },
|
|
||||||
};
|
|
||||||
const result4 = utils.recursivelyReplacePlaceholders(obj4, /^@([a-z\.]+)$/, variables4);
|
|
||||||
expect(result4).toEqual({
|
|
||||||
user: "Alice",
|
|
||||||
settings: {
|
|
||||||
theme: "dark",
|
|
||||||
nested: {
|
|
||||||
value: "found",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// test with missing paths (should return original match)
|
|
||||||
const obj5 = { value: "@missing.path" };
|
|
||||||
const variables5 = { existing: "value" };
|
|
||||||
const result5 = utils.recursivelyReplacePlaceholders(obj5, /^@([a-z\.]+)$/, variables5);
|
|
||||||
expect(result5).toEqual({ value: "@missing.path" });
|
|
||||||
|
|
||||||
// test with non-matching strings (should remain unchanged)
|
|
||||||
const obj6 = { value: "normal string", other: "not@matching" };
|
|
||||||
const variables6 = { some: "value" };
|
|
||||||
const result6 = utils.recursivelyReplacePlaceholders(obj6, /^@([a-z\.]+)$/, variables6);
|
|
||||||
expect(result6).toEqual({ value: "normal string", other: "not@matching" });
|
|
||||||
|
|
||||||
// test with primitive values (should handle gracefully)
|
|
||||||
expect(
|
|
||||||
utils.recursivelyReplacePlaceholders("@test.value", /^@([a-z\.]+)$/, {
|
|
||||||
test: { value: "replaced" },
|
|
||||||
}),
|
|
||||||
).toBe("replaced");
|
|
||||||
expect(utils.recursivelyReplacePlaceholders(123, /^@([a-z\.]+)$/, {})).toBe(123);
|
|
||||||
expect(utils.recursivelyReplacePlaceholders(null, /^@([a-z\.]+)$/, {})).toBe(null);
|
|
||||||
|
|
||||||
// test type preservation for full string matches
|
|
||||||
const variables7 = { test: { value: 123, flag: true, data: null, arr: [1, 2, 3] } };
|
|
||||||
const result7 = utils.recursivelyReplacePlaceholders(
|
|
||||||
{
|
|
||||||
number: "@test.value",
|
|
||||||
boolean: "@test.flag",
|
|
||||||
nullValue: "@test.data",
|
|
||||||
array: "@test.arr",
|
|
||||||
},
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables7,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result7).toEqual({
|
|
||||||
number: 123,
|
|
||||||
boolean: true,
|
|
||||||
nullValue: null,
|
|
||||||
array: [1, 2, 3],
|
|
||||||
});
|
|
||||||
|
|
||||||
// test partial string replacement (should convert to string)
|
|
||||||
const result8 = utils.recursivelyReplacePlaceholders(
|
|
||||||
{ message: "The value is @test.value!" },
|
|
||||||
/@([a-z\.]+)/g,
|
|
||||||
variables7,
|
|
||||||
);
|
|
||||||
expect(result8).toEqual({ message: "The value is 123!" });
|
|
||||||
|
|
||||||
// test with fallback parameter
|
|
||||||
const obj9 = { user: "@user.id", config: "@config.theme" };
|
|
||||||
const variables9 = {}; // empty context
|
|
||||||
const result9 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj9,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables9,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result9).toEqual({ user: null, config: null });
|
|
||||||
|
|
||||||
// test with fallback for partial matches
|
|
||||||
const obj10 = { message: "Hello @user.name, welcome!" };
|
|
||||||
const variables10 = {}; // empty context
|
|
||||||
const result10 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj10,
|
|
||||||
/@([a-z\.]+)/g,
|
|
||||||
variables10,
|
|
||||||
"Guest",
|
|
||||||
);
|
|
||||||
expect(result10).toEqual({ message: "Hello Guest, welcome!" });
|
|
||||||
|
|
||||||
// test with different fallback types
|
|
||||||
const obj11 = {
|
|
||||||
stringFallback: "@missing.string",
|
|
||||||
numberFallback: "@missing.number",
|
|
||||||
booleanFallback: "@missing.boolean",
|
|
||||||
objectFallback: "@missing.object",
|
|
||||||
};
|
|
||||||
const variables11 = {};
|
|
||||||
const result11 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj11,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables11,
|
|
||||||
"default",
|
|
||||||
);
|
|
||||||
expect(result11).toEqual({
|
|
||||||
stringFallback: "default",
|
|
||||||
numberFallback: "default",
|
|
||||||
booleanFallback: "default",
|
|
||||||
objectFallback: "default",
|
|
||||||
});
|
|
||||||
|
|
||||||
// test fallback with arrays
|
|
||||||
const obj12 = { items: ["@item1", "@item2", "static"] };
|
|
||||||
const variables12 = { item1: "found" }; // item2 is missing
|
|
||||||
const result12 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj12,
|
|
||||||
/^@([a-zA-Z0-9\.]+)$/,
|
|
||||||
variables12,
|
|
||||||
"missing",
|
|
||||||
);
|
|
||||||
expect(result12).toEqual({ items: ["found", "missing", "static"] });
|
|
||||||
|
|
||||||
// test fallback with nested objects
|
|
||||||
const obj13 = {
|
|
||||||
user: "@user.id",
|
|
||||||
settings: {
|
|
||||||
theme: "@theme.name",
|
|
||||||
nested: {
|
|
||||||
value: "@deep.value",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const variables13 = {}; // empty context
|
|
||||||
const result13 = utils.recursivelyReplacePlaceholders(
|
|
||||||
obj13,
|
|
||||||
/^@([a-z\.]+)$/,
|
|
||||||
variables13,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
expect(result13).toEqual({
|
|
||||||
user: null,
|
|
||||||
settings: {
|
|
||||||
theme: null,
|
|
||||||
nested: {
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("file", async () => {
|
describe("file", async () => {
|
||||||
@@ -424,7 +248,7 @@ describe("Core Utils", async () => {
|
|||||||
expect(utils.getContentName(request)).toBe(name);
|
expect(utils.getContentName(request)).toBe(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detectImageDimensions", async () => {
|
test.only("detectImageDimensions", async () => {
|
||||||
// wrong
|
// wrong
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
|
||||||
@@ -440,44 +264,15 @@ describe("Core Utils", async () => {
|
|||||||
height: 512,
|
height: 512,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isFileAccepted", () => {
|
|
||||||
const file = new File([""], "file.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
expect(utils.isFileAccepted(file, "text/plain")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "text/plain,text/html")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "text/html")).toBe(false);
|
|
||||||
|
|
||||||
{
|
|
||||||
const file = new File([""], "file.jpg", {
|
|
||||||
type: "image/jpeg",
|
|
||||||
});
|
|
||||||
expect(utils.isFileAccepted(file, "image/jpeg")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "image/jpeg,image/png")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, "image/png")).toBe(false);
|
|
||||||
expect(utils.isFileAccepted(file, "image/*")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".jpg")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".jpg,.png")).toBe(true);
|
|
||||||
expect(utils.isFileAccepted(file, ".png")).toBe(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const file = new File([""], "file.png");
|
|
||||||
expect(utils.isFileAccepted(file, undefined as any)).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => utils.isFileAccepted(null as any, "text/plain")).toThrow();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("dates", () => {
|
describe("dates", () => {
|
||||||
test("formats local time", () => {
|
test.only("formats local time", () => {
|
||||||
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
expect(utils.datetimeStringUTC("2025-02-21T16:48:25.841Z")).toBe("2025-02-21 16:48:25");
|
||||||
/*console.log(utils.datetimeStringUTC(new Date()));
|
console.log(utils.datetimeStringUTC(new Date()));
|
||||||
console.log(utils.datetimeStringUTC());
|
console.log(utils.datetimeStringUTC());
|
||||||
console.log(new Date());
|
console.log(new Date());
|
||||||
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone); */
|
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
import { Guard } from "../../src/auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
import { parse } from "core/utils/schema";
|
import { parse } from "core/object/schema";
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
type EntityData,
|
||||||
|
EntityManager,
|
||||||
|
ManyToOneRelation,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { DataController } from "../../src/data/api/DataController";
|
import { DataController } from "../../src/data/api/DataController";
|
||||||
import { dataConfigSchema } from "../../src/data/data-schema";
|
import { dataConfigSchema } from "../../src/data/data-schema";
|
||||||
import { getDummyConnection } from "../helper";
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
|
||||||
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
|
||||||
import { Entity, EntityManager, type EntityData } from "data/entities";
|
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { ManyToOneRelation } from "data/relations";
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
const dataConfig = parse(dataConfigSchema, {});
|
const dataConfig = parse(dataConfigSchema, {});
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { TextField, PrimaryField, NumberField } from "data/fields";
|
Entity,
|
||||||
|
EntityManager,
|
||||||
|
NumberField,
|
||||||
|
PrimaryField,
|
||||||
|
Repository,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -30,9 +36,9 @@ describe("some tests", async () => {
|
|||||||
const query = await em.repository(users).findId(1);
|
const query = await em.repository(users).findId(1);
|
||||||
|
|
||||||
expect(query.sql).toBe(
|
expect(query.sql).toBe(
|
||||||
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? order by "users"."id" asc limit ? offset ?',
|
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
|
||||||
);
|
);
|
||||||
expect(query.parameters).toEqual([1, 1, 0]);
|
expect(query.parameters).toEqual([1, 1]);
|
||||||
expect(query.data).toBeUndefined();
|
expect(query.data).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SqliteDatabase } from "kysely";
|
import type { SqliteDatabase } from "kysely";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
import { SqliteLocalConnection } from "../../src/data";
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import {
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
Entity,
|
||||||
import { ManyToOneRelation } from "data/relations";
|
EntityManager,
|
||||||
import { NumberField, TextField } from "data/fields";
|
ManyToOneRelation,
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
NumberField,
|
||||||
|
SchemaManager,
|
||||||
|
TextField,
|
||||||
|
} from "../../src/data";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, Mutator, NumberField, TextField } from "../../src/data";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import { TransformPersistFailedException } from "../../src/data/errors";
|
||||||
import { TransformPersistFailedException } from "data/errors";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
import { afterAll, expect as bunExpect, describe, test } from "bun:test";
|
||||||
import { stripMark } from "core/utils/schema";
|
import { stripMark } from "core/object/schema";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { PolymorphicRelation } from "data/relations";
|
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { describe, beforeAll, afterAll, test } from "bun:test";
|
|
||||||
import type { PostgresConnection } from "data/connection/postgres/PostgresConnection";
|
|
||||||
import { pg, postgresJs } from "bknd";
|
|
||||||
import { Pool } from "pg";
|
|
||||||
import postgres from "postgres";
|
|
||||||
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
|
||||||
import { $ } from "bun";
|
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
|
|
||||||
const credentials = {
|
|
||||||
host: "localhost",
|
|
||||||
port: 5430,
|
|
||||||
user: "postgres",
|
|
||||||
password: "postgres",
|
|
||||||
database: "bknd",
|
|
||||||
};
|
|
||||||
|
|
||||||
async function cleanDatabase(connection: InstanceType<typeof PostgresConnection>) {
|
|
||||||
const kysely = connection.kysely;
|
|
||||||
|
|
||||||
// drop all tables+indexes & create new schema
|
|
||||||
await kysely.schema.dropSchema("public").ifExists().cascade().execute();
|
|
||||||
await kysely.schema.dropIndex("public").ifExists().cascade().execute();
|
|
||||||
await kysely.schema.createSchema("public").execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isPostgresRunning() {
|
|
||||||
try {
|
|
||||||
// Try to actually connect to PostgreSQL
|
|
||||||
const conn = pg({ pool: new Pool(credentials) });
|
|
||||||
await conn.ping();
|
|
||||||
await conn.close();
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("postgres", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
if (!(await isPostgresRunning())) {
|
|
||||||
await $`docker run --rm --name bknd-test-postgres -d -e POSTGRES_PASSWORD=${credentials.password} -e POSTGRES_USER=${credentials.user} -e POSTGRES_DB=${credentials.database} -p ${credentials.port}:5432 postgres:17`;
|
|
||||||
await $waitUntil("Postgres is running", isPostgresRunning);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
disableConsoleLog();
|
|
||||||
});
|
|
||||||
afterAll(async () => {
|
|
||||||
if (await isPostgresRunning()) {
|
|
||||||
try {
|
|
||||||
await $`docker stop bknd-test-postgres`;
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableConsoleLog();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.serial.each([
|
|
||||||
["pg", () => pg({ pool: new Pool(credentials) })],
|
|
||||||
["postgresjs", () => postgresJs({ postgres: postgres(credentials) })],
|
|
||||||
])("%s", (name, createConnection) => {
|
|
||||||
connectionTestSuite(
|
|
||||||
{
|
|
||||||
...bunTestRunner,
|
|
||||||
test: test.serial,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
makeConnection: () => {
|
|
||||||
const connection = createConnection();
|
|
||||||
return {
|
|
||||||
connection,
|
|
||||||
dispose: async () => {
|
|
||||||
await cleanDatabase(connection);
|
|
||||||
await connection.close();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
rawDialectDetails: [],
|
|
||||||
disableConsoleLog: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2,20 +2,19 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateField,
|
DateField,
|
||||||
|
Entity,
|
||||||
|
EntityIndex,
|
||||||
|
EntityManager,
|
||||||
EnumField,
|
EnumField,
|
||||||
JsonField,
|
JsonField,
|
||||||
NumberField,
|
|
||||||
TextField,
|
|
||||||
EntityIndex,
|
|
||||||
} from "data/fields";
|
|
||||||
import { Entity, EntityManager } from "data/entities";
|
|
||||||
import {
|
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
|
NumberField,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
} from "data/relations";
|
TextField,
|
||||||
import { DummyConnection } from "data/connection/DummyConnection";
|
} from "../../src/data";
|
||||||
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import {
|
import {
|
||||||
FieldPrototype,
|
FieldPrototype,
|
||||||
type FieldSchema,
|
type FieldSchema,
|
||||||
@@ -33,8 +32,8 @@ import {
|
|||||||
number,
|
number,
|
||||||
relation,
|
relation,
|
||||||
text,
|
text,
|
||||||
} from "data/prototype";
|
} from "../../src/data/prototype";
|
||||||
import { MediaField } from "media/MediaField";
|
import { MediaField } from "../../src/media/MediaField";
|
||||||
|
|
||||||
describe("prototype", () => {
|
describe("prototype", () => {
|
||||||
test("...", () => {
|
test("...", () => {
|
||||||
@@ -297,9 +296,9 @@ describe("prototype", () => {
|
|||||||
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
|
||||||
new Entity("comments", [new TextField("some")]),
|
new Entity("comments", [new TextField("some")]),
|
||||||
new Entity("users", [new TextField("email")]),
|
new Entity("users", [new TextField("email")]),
|
||||||
] as const;
|
];
|
||||||
const _em2 = new EntityManager(
|
const _em2 = new EntityManager(
|
||||||
[...es],
|
es,
|
||||||
new DummyConnection(),
|
new DummyConnection(),
|
||||||
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, TextField } from "../../src/data";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import {
|
import {
|
||||||
ManyToManyRelation,
|
ManyToManyRelation,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
PolymorphicRelation,
|
PolymorphicRelation,
|
||||||
RelationField,
|
RelationField,
|
||||||
} from "data/relations";
|
} from "../../src/data/relations";
|
||||||
import { getDummyConnection } from "./helper";
|
import { getDummyConnection } from "./helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -78,7 +77,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0]!;
|
const rel = em.relations.all[0];
|
||||||
expect(rel.source.entity.name).toBe(posts.name);
|
expect(rel.source.entity.name).toBe(posts.name);
|
||||||
expect(rel.source.reference).toBe(posts.name);
|
expect(rel.source.reference).toBe(posts.name);
|
||||||
expect(rel.target.entity.name).toBe(users.name);
|
expect(rel.target.entity.name).toBe(users.name);
|
||||||
@@ -90,11 +89,11 @@ describe("Relations", async () => {
|
|||||||
// verify low level relation
|
// verify low level relation
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||||
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
expect(posts.field("author_id")).toBeInstanceOf(RelationField);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(posts);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(posts);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userPostsRel = em.relationOf(users.name, "posts");
|
const userPostsRel = em.relationOf(users.name, "posts");
|
||||||
@@ -192,7 +191,7 @@ describe("Relations", async () => {
|
|||||||
const em = new EntityManager(entities, dummyConnection, relations);
|
const em = new EntityManager(entities, dummyConnection, relations);
|
||||||
|
|
||||||
// verify naming
|
// verify naming
|
||||||
const rel = em.relations.all[0]!;
|
const rel = em.relations.all[0];
|
||||||
expect(rel.source.entity.name).toBe(users.name);
|
expect(rel.source.entity.name).toBe(users.name);
|
||||||
expect(rel.source.reference).toBe(users.name);
|
expect(rel.source.reference).toBe(users.name);
|
||||||
expect(rel.target.entity.name).toBe(settings.name);
|
expect(rel.target.entity.name).toBe(settings.name);
|
||||||
@@ -203,8 +202,8 @@ describe("Relations", async () => {
|
|||||||
|
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name).length).toBe(1);
|
expect(em.relationsOf(users.name).length).toBe(1);
|
||||||
expect(em.relationsOf(users.name)[0]!.source.entity).toBe(users);
|
expect(em.relationsOf(users.name)[0].source.entity).toBe(users);
|
||||||
expect(em.relationsOf(users.name)[0]!.target.entity).toBe(settings);
|
expect(em.relationsOf(users.name)[0].target.entity).toBe(settings);
|
||||||
|
|
||||||
// verify high level relation (from users)
|
// verify high level relation (from users)
|
||||||
const userSettingRel = em.relationOf(users.name, settings.name);
|
const userSettingRel = em.relationOf(users.name, settings.name);
|
||||||
@@ -324,7 +323,7 @@ describe("Relations", async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// mutation info
|
// mutation info
|
||||||
expect(relations[0]!.helper(posts.name)!.getMutationInfo()).toEqual({
|
expect(relations[0].helper(posts.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "categories",
|
reference: "categories",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
@@ -335,7 +334,7 @@ describe("Relations", async () => {
|
|||||||
cardinality: undefined,
|
cardinality: undefined,
|
||||||
relation_type: "m:n",
|
relation_type: "m:n",
|
||||||
});
|
});
|
||||||
expect(relations[0]!.helper(categories.name)!.getMutationInfo()).toEqual({
|
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
|
||||||
reference: "posts",
|
reference: "posts",
|
||||||
local_field: undefined,
|
local_field: undefined,
|
||||||
$set: false,
|
$set: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Entity, NumberField, TextField } from "data";
|
||||||
import { NumberField, TextField } from "data/fields";
|
import * as p from "data/prototype";
|
||||||
|
|
||||||
describe("[data] Entity", async () => {
|
describe("[data] Entity", async () => {
|
||||||
const entity = new Entity("test", [
|
const entity = new Entity("test", [
|
||||||
@@ -47,4 +47,8 @@ describe("[data] Entity", async () => {
|
|||||||
entity.addField(field);
|
entity.addField(field);
|
||||||
expect(entity.getField("new_field")).toBe(field);
|
expect(entity.getField("new_field")).toBe(field);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.only("types", async () => {
|
||||||
|
console.log(entity.toTypes());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { ManyToManyRelation, ManyToOneRelation } from "data/relations";
|
Entity,
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
EntityManager,
|
||||||
import { UnableToConnectException } from "data/errors";
|
ManyToManyRelation,
|
||||||
|
ManyToOneRelation,
|
||||||
|
SchemaManager,
|
||||||
|
} from "../../../src/data";
|
||||||
|
import { UnableToConnectException } from "../../../src/data/errors";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityManager, ManyToOneRelation, TextField } from "../../../src/data";
|
||||||
import { ManyToOneRelation } from "data/relations";
|
import { JoinBuilder } from "../../../src/data/entities/query/JoinBuilder";
|
||||||
import { TextField } from "data/fields";
|
|
||||||
import { JoinBuilder } from "data/entities/query/JoinBuilder";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
describe("[data] JoinBuilder", async () => {
|
describe("[data] JoinBuilder", async () => {
|
||||||
test("missing relation", async () => {
|
test("missing relation", async () => {
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import type { EventManager } from "../../../src/core/events";
|
import type { EventManager } from "../../../src/core/events";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
|
||||||
import {
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityManager,
|
||||||
ManyToOneRelation,
|
ManyToOneRelation,
|
||||||
|
MutatorEvents,
|
||||||
|
NumberField,
|
||||||
OneToOneRelation,
|
OneToOneRelation,
|
||||||
RelationField,
|
type RelationField,
|
||||||
RelationMutator,
|
RelationMutator,
|
||||||
} from "data/relations";
|
TextField,
|
||||||
import { NumberField, TextField } from "data/fields";
|
} from "../../../src/data";
|
||||||
import * as proto from "data/prototype";
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { MutatorEvents } from "data/events";
|
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
afterAll(afterAllCleanup);
|
afterAll(afterAllCleanup);
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
beforeAll(() => disableConsoleLog(["log", "warn"]));
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
||||||
|
|
||||||
describe("[data] Mutator (base)", async () => {
|
describe("[data] Mutator (base)", async () => {
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Kysely, Transaction } from "kysely";
|
import type { Kysely, Transaction } from "kysely";
|
||||||
import { TextField } from "data/fields";
|
import { Perf } from "core/utils";
|
||||||
import { em as $em, entity as $entity, text as $text } from "data/prototype";
|
import {
|
||||||
import { Entity, EntityManager } from "data/entities";
|
Entity,
|
||||||
import { ManyToOneRelation } from "data/relations";
|
EntityManager,
|
||||||
import { RepositoryEvents } from "data/events";
|
LibsqlConnection,
|
||||||
|
ManyToOneRelation,
|
||||||
|
RepositoryEvents,
|
||||||
|
TextField,
|
||||||
|
entity as $entity,
|
||||||
|
text as $text,
|
||||||
|
em as $em,
|
||||||
|
} from "data";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
type E = Kysely<any> | Transaction<any>;
|
type E = Kysely<any> | Transaction<any>;
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
beforeAll(() => disableConsoleLog());
|
afterAll(afterAllCleanup);
|
||||||
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
|
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -124,81 +129,6 @@ describe("[Repository]", async () => {
|
|||||||
.then((r) => [r.count, r.total]),
|
.then((r) => [r.count, r.total]),
|
||||||
).resolves.toEqual([undefined, undefined]);
|
).resolves.toEqual([undefined, undefined]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auto join", async () => {
|
|
||||||
const schema = $em(
|
|
||||||
{
|
|
||||||
posts: $entity("posts", {
|
|
||||||
title: $text(),
|
|
||||||
content: $text(),
|
|
||||||
}),
|
|
||||||
comments: $entity("comments", {
|
|
||||||
content: $text(),
|
|
||||||
}),
|
|
||||||
another: $entity("another", {
|
|
||||||
title: $text(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
({ relation }, { posts, comments }) => {
|
|
||||||
relation(comments).manyToOne(posts);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const em = schema.proto.withConnection(getDummyConnection().dummyConnection);
|
|
||||||
await em.schema().sync({ force: true });
|
|
||||||
|
|
||||||
await em.mutator("posts").insertOne({ title: "post1", content: "content1" });
|
|
||||||
await em
|
|
||||||
.mutator("comments")
|
|
||||||
.insertMany([{ content: "comment1", posts_id: 1 }, { content: "comment2" }] as any);
|
|
||||||
|
|
||||||
const res = await em.repo("comments").findMany({
|
|
||||||
where: {
|
|
||||||
"posts.title": "post1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res.data as any).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
content: "comment1",
|
|
||||||
posts_id: 1,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
{
|
|
||||||
// manual join should still work
|
|
||||||
const res = await em.repo("comments").findMany({
|
|
||||||
join: ["posts"],
|
|
||||||
where: {
|
|
||||||
"posts.title": "post1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(res.data as any).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
content: "comment1",
|
|
||||||
posts_id: 1,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// inexistent should be detected and thrown
|
|
||||||
expect(
|
|
||||||
em.repo("comments").findMany({
|
|
||||||
where: {
|
|
||||||
"random.title": "post1",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(/Invalid where field/);
|
|
||||||
|
|
||||||
// existing alias, but not a relation should throw
|
|
||||||
expect(
|
|
||||||
em.repo("comments").findMany({
|
|
||||||
where: {
|
|
||||||
"another.title": "post1",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(/Invalid where field/);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("[data] Repository (Events)", async () => {
|
describe("[data] Repository (Events)", async () => {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-unresolved
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { afterAll, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "core/utils";
|
import { randomString } from "../../../src/core/utils";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import { Entity, EntityIndex, EntityManager, SchemaManager, TextField } from "../../../src/data";
|
||||||
import { TextField, EntityIndex } from "data/fields";
|
|
||||||
import { SchemaManager } from "data/schema/SchemaManager";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
@@ -268,39 +266,4 @@ describe("SchemaManager tests", async () => {
|
|||||||
const diffAfter = await em.schema().getDiff();
|
const diffAfter = await em.schema().getDiff();
|
||||||
expect(diffAfter.length).toBe(0);
|
expect(diffAfter.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns statements", async () => {
|
|
||||||
const amount = 5;
|
|
||||||
const entities = new Array(amount)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
|
||||||
const em = new EntityManager(entities, dummyConnection);
|
|
||||||
const statements = await em.schema().sync({ force: true });
|
|
||||||
expect(statements.length).toBe(amount);
|
|
||||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("batches statements", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const entities = new Array(20)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => new Entity(randomString(16), [new TextField("text")]));
|
|
||||||
const em = new EntityManager(entities, dummyConnection);
|
|
||||||
const spy = spyOn(em.connection, "executeQueries");
|
|
||||||
const statements = await em.schema().sync();
|
|
||||||
expect(statements.length).toBe(entities.length);
|
|
||||||
expect(statements.every((stmt) => Object.keys(stmt).join(",") === "sql,parameters")).toBe(
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
await em.schema().sync({ force: true });
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1);
|
|
||||||
const tables = await em.connection.kysely
|
|
||||||
.selectFrom("sqlite_master")
|
|
||||||
.where("type", "=", "table")
|
|
||||||
.selectAll()
|
|
||||||
.execute();
|
|
||||||
expect(tables.length).toBe(entities.length + 1); /* 1+ for sqlite_sequence */
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
import { describe, test, expect } from "bun:test";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { WhereBuilder, type WhereQuery } from "data/entities/query/WhereBuilder";
|
import { type WhereQuery, WhereBuilder } from "data";
|
||||||
|
|
||||||
function qb() {
|
function qb() {
|
||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity, EntityManager } from "data/entities";
|
import {
|
||||||
import { ManyToManyRelation, ManyToOneRelation, PolymorphicRelation } from "data/relations";
|
Entity,
|
||||||
import { TextField } from "data/fields";
|
EntityManager,
|
||||||
import * as proto from "data/prototype";
|
ManyToManyRelation,
|
||||||
import { WithBuilder } from "data/entities/query/WithBuilder";
|
ManyToOneRelation,
|
||||||
|
PolymorphicRelation,
|
||||||
|
TextField,
|
||||||
|
WithBuilder,
|
||||||
|
} from "../../../src/data";
|
||||||
|
import * as proto from "../../../src/data/prototype";
|
||||||
import { schemaToEm } from "../../helper";
|
import { schemaToEm } from "../../helper";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EntityManager } from "../../../../src/data";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
|
|
||||||
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe("SqliteIntrospector", () => {
|
|||||||
dataType: "INTEGER",
|
dataType: "INTEGER",
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: true,
|
hasDefaultValue: false,
|
||||||
comment: undefined,
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@ describe("SqliteIntrospector", () => {
|
|||||||
dataType: "INTEGER",
|
dataType: "INTEGER",
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
isAutoIncrementing: true,
|
isAutoIncrementing: true,
|
||||||
hasDefaultValue: true,
|
hasDefaultValue: false,
|
||||||
comment: undefined,
|
comment: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { BooleanField } from "data/fields";
|
import { BooleanField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] BooleanField", async () => {
|
describe("[data] BooleanField", async () => {
|
||||||
fieldTestSuite(bunTestRunner, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
fieldTestSuite({ expect, test }, BooleanField, { defaultValue: true, schemaType: "boolean" });
|
||||||
|
|
||||||
test("transformRetrieve", async () => {
|
test("transformRetrieve", async () => {
|
||||||
const field = new BooleanField("test");
|
const field = new BooleanField("test");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { DateField } from "data/fields";
|
import { DateField, dateFieldConfigSchema } from "../../../../src/data";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { EnumField } from "data/fields";
|
import { EnumField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
function options(strings: string[]) {
|
function options(strings: string[]) {
|
||||||
@@ -9,7 +8,7 @@ function options(strings: string[]) {
|
|||||||
|
|
||||||
describe("[data] EnumField", async () => {
|
describe("[data] EnumField", async () => {
|
||||||
fieldTestSuite(
|
fieldTestSuite(
|
||||||
bunTestRunner,
|
{ expect, test },
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
EnumField,
|
EnumField,
|
||||||
{ defaultValue: "a", schemaType: "text" },
|
{ defaultValue: "a", schemaType: "text" },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|||||||
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
import { stripMark } from "core/utils/schema";
|
import { stripMark } from "core/object/schema";
|
||||||
|
|
||||||
describe("[data] Field", async () => {
|
describe("[data] Field", async () => {
|
||||||
class FieldSpec extends Field {
|
class FieldSpec extends Field {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Entity } from "data/entities";
|
import { Entity, EntityIndex, Field } from "../../../../src/data";
|
||||||
import { Field, EntityIndex } from "data/fields";
|
import { s } from "core/object/schema";
|
||||||
import { s } from "core/utils/schema";
|
|
||||||
|
|
||||||
class TestField extends Field {
|
class TestField extends Field {
|
||||||
protected getSchema(): any {
|
protected getSchema(): any {
|
||||||
@@ -23,4 +22,11 @@ describe("FieldIndex", async () => {
|
|||||||
expect(index.name).toEqual("idx_test_name");
|
expect(index.name).toEqual("idx_test_name");
|
||||||
expect(index.unique).toEqual(false);
|
expect(index.unique).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("it fails on non-unique", async () => {
|
||||||
|
const field = new TestField("name", { required: false });
|
||||||
|
|
||||||
|
expect(() => new EntityIndex(entity, [field], true)).toThrowError();
|
||||||
|
expect(() => new EntityIndex(entity, [field])).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonField } from "data/fields";
|
import { JsonField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonField", async () => {
|
describe("[data] JsonField", async () => {
|
||||||
const field = new JsonField("test");
|
const field = new JsonField("test");
|
||||||
fieldTestSuite(bunTestRunner, JsonField, {
|
fieldTestSuite({ expect, test }, JsonField, {
|
||||||
defaultValue: { a: 1 },
|
defaultValue: { a: 1 },
|
||||||
//sampleValues: ["string", { test: 1 }, 1],
|
sampleValues: ["string", { test: 1 }, 1],
|
||||||
schemaType: "text",
|
schemaType: "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,9 +32,9 @@ describe("[data] JsonField", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("getValue", async () => {
|
test("getValue", async () => {
|
||||||
expect(field.getValue({ test: 1 }, "form")).toEqual({ test: 1 });
|
expect(field.getValue({ test: 1 }, "form")).toBe('{\n "test": 1\n}');
|
||||||
expect(field.getValue("string", "form")).toBe("string");
|
expect(field.getValue("string", "form")).toBe('"string"');
|
||||||
expect(field.getValue(1, "form")).toBe(1);
|
expect(field.getValue(1, "form")).toBe("1");
|
||||||
|
|
||||||
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
expect(field.getValue('{"test":1}', "submit")).toEqual({ test: 1 });
|
||||||
expect(field.getValue('"string"', "submit")).toBe("string");
|
expect(field.getValue('"string"', "submit")).toBe("string");
|
||||||
@@ -43,5 +42,6 @@ describe("[data] JsonField", async () => {
|
|||||||
|
|
||||||
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
expect(field.getValue({ test: 1 }, "table")).toBe('{"test":1}');
|
||||||
expect(field.getValue("string", "table")).toBe('"string"');
|
expect(field.getValue("string", "table")).toBe('"string"');
|
||||||
|
expect(field.getValue(1, "form")).toBe("1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { JsonSchemaField } from "data/fields";
|
import { JsonSchemaField } from "../../../../src/data";
|
||||||
import { fieldTestSuite } from "data/fields/field-test-suite";
|
import { fieldTestSuite } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] JsonSchemaField", async () => {
|
describe("[data] JsonSchemaField", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { bunTestRunner } from "adapter/bun/test";
|
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { NumberField } from "data/fields";
|
import { NumberField } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
|
|
||||||
describe("[data] NumberField", async () => {
|
describe("[data] NumberField", async () => {
|
||||||
@@ -16,5 +15,5 @@ describe("[data] NumberField", async () => {
|
|||||||
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
expect(transformPersist(field2, 10000)).resolves.toBe(10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
fieldTestSuite(bunTestRunner, NumberField, { defaultValue: 12, schemaType: "integer" });
|
fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { PrimaryField } from "data/fields";
|
import { PrimaryField } from "../../../../src/data";
|
||||||
|
|
||||||
describe("[data] PrimaryField", async () => {
|
describe("[data] PrimaryField", async () => {
|
||||||
const field = new PrimaryField("primary");
|
const field = new PrimaryField("primary");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { TextField } from "data/fields";
|
import { TextField, textFieldConfigSchema } from "../../../../src/data";
|
||||||
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
|
||||||
import { bunTestRunner } from "adapter/bun/test";
|
import { bunTestRunner } from "adapter/bun/test";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { describe, expect, it, test } from "bun:test";
|
import { describe, expect, it, test } from "bun:test";
|
||||||
import { Entity, type EntityManager } from "data/entities";
|
import { Entity, type EntityManager } from "../../../../src/data";
|
||||||
import {
|
import {
|
||||||
type BaseRelationConfig,
|
type BaseRelationConfig,
|
||||||
EntityRelation,
|
EntityRelation,
|
||||||
EntityRelationAnchor,
|
EntityRelationAnchor,
|
||||||
ManyToManyRelation,
|
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
} from "data/relations";
|
} from "../../../../src/data/relations";
|
||||||
import * as proto from "data/prototype";
|
|
||||||
|
|
||||||
class TestEntityRelation extends EntityRelation {
|
class TestEntityRelation extends EntityRelation {
|
||||||
constructor(config?: BaseRelationConfig) {
|
constructor(config?: BaseRelationConfig) {
|
||||||
@@ -26,11 +24,11 @@ class TestEntityRelation extends EntityRelation {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildWith(): any {
|
buildWith(a: any, b: any, c: any): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildJoin(): any {
|
buildJoin(a: any, b: any): any {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,15 +75,4 @@ describe("[data] EntityRelation", async () => {
|
|||||||
const relation2 = new TestEntityRelation({ required: true });
|
const relation2 = new TestEntityRelation({ required: true });
|
||||||
expect(relation2.required).toBe(true);
|
expect(relation2.required).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly produces the relation name", async () => {
|
|
||||||
const relation = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"));
|
|
||||||
expect(relation.getName()).not.toContain(",");
|
|
||||||
expect(relation.getName()).toBe("mn_apps_organizations");
|
|
||||||
|
|
||||||
const relation2 = new ManyToManyRelation(new Entity("apps"), new Entity("organizations"), {
|
|
||||||
connectionTableMappedName: "appOrganizations",
|
|
||||||
});
|
|
||||||
expect(relation2.getName()).toBe("mn_apps_organizations_appOrganizations");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import * as sDirect from "jsonv-ts";
|
|
||||||
import { s as sFromBknd } from "bknd/utils";
|
|
||||||
|
|
||||||
describe("jsonv-ts resolution", () => {
|
|
||||||
it("should resolve to a single instance", () => {
|
|
||||||
const sameNamespace = sDirect === (sFromBknd as unknown as typeof sDirect);
|
|
||||||
// If this fails, two instances are being loaded via different specifiers/paths
|
|
||||||
expect(sameNamespace).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve specifiers to a single package path", async () => {
|
|
||||||
const base = await import.meta.resolve("jsonv-ts");
|
|
||||||
const hono = await import.meta.resolve("jsonv-ts/hono");
|
|
||||||
const mcp = await import.meta.resolve("jsonv-ts/mcp");
|
|
||||||
expect(typeof base).toBe("string");
|
|
||||||
expect(typeof hono).toBe("string");
|
|
||||||
expect(typeof mcp).toBe("string");
|
|
||||||
// They can be different files (subpath exports), but they should share the same package root
|
|
||||||
const pkgRoot = (p: string) => p.slice(0, p.lastIndexOf("jsonv-ts") + "jsonv-ts".length);
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(hono));
|
|
||||||
expect(pkgRoot(base)).toBe(pkgRoot(mcp));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
|
||||||
import { s } from "core/utils/schema";
|
import { s } from "core/object/schema";
|
||||||
|
|
||||||
export class StringifyTask<Output extends string> extends Task<
|
export class StringifyTask<Output extends string> extends Task<
|
||||||
typeof StringifyTask.schema,
|
typeof StringifyTask.schema,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Task } from "../../src/flows";
|
import { Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
import { s } from "core/utils/schema";
|
import { s } from "core/object/schema";
|
||||||
|
|
||||||
describe.skip("Task", async () => {
|
describe.skip("Task", async () => {
|
||||||
test("resolveParams: template with parse", async () => {
|
test("resolveParams: template with parse", async () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Event, EventManager } from "../../src/core/events";
|
import { Event, EventManager } from "../../src/core/events";
|
||||||
import { s, parse } from "core/utils/schema";
|
import { s, parse } from "core/object/schema";
|
||||||
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
import { EventTrigger, Flow, HttpTrigger, type InputsMap, Task } from "../../src/flows";
|
||||||
import { dynamic } from "../../src/flows/tasks/Task";
|
import { dynamic } from "../../src/flows/tasks/Task";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { isEqual } from "lodash-es";
|
import { isEqual } from "lodash-es";
|
||||||
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
import { _jsonp, withDisabledConsole } from "../../src/core/utils";
|
||||||
import { s } from "core/utils/schema";
|
import { s } from "core/object/schema";
|
||||||
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
import { Condition, ExecutionEvent, FetchTask, Flow, LogTask, Task } from "../../src/flows";
|
||||||
|
|
||||||
/*beforeAll(disableConsoleLog);
|
/*beforeAll(disableConsoleLog);
|
||||||
|
|||||||
+23
-4
@@ -2,12 +2,11 @@ import { unlink } from "node:fs/promises";
|
|||||||
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
import type { SelectQueryBuilder, SqliteDatabase } from "kysely";
|
||||||
import Database from "libsql";
|
import Database from "libsql";
|
||||||
import { format as sqlFormat } from "sql-formatter";
|
import { format as sqlFormat } from "sql-formatter";
|
||||||
|
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
|
||||||
import type { em as protoEm } from "../src/data/prototype";
|
import type { em as protoEm } from "../src/data/prototype";
|
||||||
import { writeFile } from "node:fs/promises";
|
import { writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { slugify } from "bknd/utils";
|
import { slugify } from "core/utils/strings";
|
||||||
import { type Connection, SqliteLocalConnection } from "data/connection";
|
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
|
||||||
|
|
||||||
export function getDummyDatabase(memory: boolean = true): {
|
export function getDummyDatabase(memory: boolean = true): {
|
||||||
dummyDb: SqliteDatabase;
|
dummyDb: SqliteDatabase;
|
||||||
@@ -39,6 +38,26 @@ export function getLocalLibsqlConnection() {
|
|||||||
return { url: "http://127.0.0.1:8080" };
|
return { url: "http://127.0.0.1:8080" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConsoleSeverity = "debug" | "log" | "warn" | "error";
|
||||||
|
const _oldConsoles = {
|
||||||
|
debug: console.debug,
|
||||||
|
log: console.log,
|
||||||
|
warn: console.warn,
|
||||||
|
error: console.error,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
|
||||||
|
severities.forEach((severity) => {
|
||||||
|
console[severity] = () => null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enableConsoleLog() {
|
||||||
|
Object.entries(_oldConsoles).forEach(([severity, fn]) => {
|
||||||
|
console[severity as ConsoleSeverity] = fn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
return { sql, parameters };
|
return { sql, parameters };
|
||||||
@@ -46,7 +65,7 @@ export function compileQb(qb: SelectQueryBuilder<any, any, any>) {
|
|||||||
|
|
||||||
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
export function prettyPrintQb(qb: SelectQueryBuilder<any, any, any>) {
|
||||||
const { sql, parameters } = qb.compile();
|
const { sql, parameters } = qb.compile();
|
||||||
console.info("$", sqlFormat(sql), "\n[params]", parameters);
|
console.log("$", sqlFormat(sql), "\n[params]", parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): EntityManager<any> {
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { App, createApp, type AuthResponse } from "../../src";
|
import { App, createApp } from "../../src";
|
||||||
import { auth } from "../../src/modules/middlewares";
|
import type { AuthResponse } from "../../src/auth";
|
||||||
import {
|
import { auth } from "../../src/auth/middlewares";
|
||||||
mergeObject,
|
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
|
||||||
randomString,
|
import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
|
||||||
secureRandomString,
|
|
||||||
withDisabledConsole,
|
const { dummyConnection, afterAllCleanup } = getDummyConnection();
|
||||||
} from "../../src/core/utils";
|
afterEach(afterAllCleanup);
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import { getDummyConnection } from "../helper";
|
|
||||||
import type { AppAuthSchema } from "auth/auth-schema";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
beforeAll(disableConsoleLog);
|
||||||
afterAll(enableConsoleLog);
|
afterAll(enableConsoleLog);
|
||||||
@@ -68,12 +65,11 @@ const configs = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createAuthApp(config?: Partial<AppAuthSchema>) {
|
function createAuthApp() {
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: mergeObject(configs.auth, config ?? {}),
|
auth: configs.auth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,16 +134,6 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
|
|||||||
|
|
||||||
return { res, data };
|
return { res, data };
|
||||||
},
|
},
|
||||||
register: async (user: any): Promise<{ res: Response; data: AuthResponse }> => {
|
|
||||||
const res = (await app.server.request("/api/auth/password/register", {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers(),
|
|
||||||
body: body(user),
|
|
||||||
})) as Response;
|
|
||||||
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
|
|
||||||
|
|
||||||
return { res, data };
|
|
||||||
},
|
|
||||||
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
me: async (token?: string): Promise<Pick<AuthResponse, "user">> => {
|
||||||
const res = (await app.server.request("/api/auth/me", {
|
const res = (await app.server.request("/api/auth/me", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -165,8 +151,8 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
const { data: users } = await app.em.repository("users").findMany();
|
const { data: users } = await app.em.repository("users").findMany();
|
||||||
expect(users.length).toBe(2);
|
expect(users.length).toBe(2);
|
||||||
expect(users[0]?.email).toBe(configs.users.normal.email);
|
expect(users[0].email).toBe(configs.users.normal.email);
|
||||||
expect(users[1]?.email).toBe(configs.users.admin.email);
|
expect(users[1].email).toBe(configs.users.admin.email);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should log you in with API", async () => {
|
it("should log you in with API", async () => {
|
||||||
@@ -237,7 +223,7 @@ describe("integration auth", () => {
|
|||||||
|
|
||||||
app.server.get("/get", auth(), async (c) => {
|
app.server.get("/get", auth(), async (c) => {
|
||||||
return c.json({
|
return c.json({
|
||||||
user: c.get("auth")?.user ?? null,
|
user: c.get("auth").user ?? null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.server.get("/wait", auth(), async (c) => {
|
app.server.get("/wait", auth(), async (c) => {
|
||||||
@@ -256,66 +242,9 @@ describe("integration auth", () => {
|
|||||||
{
|
{
|
||||||
await new Promise((r) => setTimeout(r, 10));
|
await new Promise((r) => setTimeout(r, 10));
|
||||||
const res = await app.server.request("/get");
|
const res = await app.server.request("/get");
|
||||||
const data = (await res.json()) as any;
|
const data = await res.json();
|
||||||
expect(data.user).toBe(null);
|
expect(data.user).toBe(null);
|
||||||
expect(await $fns.me()).toEqual({ user: null as any });
|
expect(await $fns.me()).toEqual({ user: null as any });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register users with default role", async () => {
|
|
||||||
const app = createAuthApp({ default_role_register: "guest" });
|
|
||||||
await app.build();
|
|
||||||
const $fns = fns(app);
|
|
||||||
|
|
||||||
// takes default role
|
|
||||||
expect(
|
|
||||||
await app
|
|
||||||
.createUser({
|
|
||||||
email: "test@bknd.io",
|
|
||||||
password: "12345678",
|
|
||||||
})
|
|
||||||
.then((r) => r.role),
|
|
||||||
).toBe("guest");
|
|
||||||
|
|
||||||
// throws error if role doesn't exist
|
|
||||||
expect(
|
|
||||||
app.createUser({
|
|
||||||
email: "test@bknd.io",
|
|
||||||
password: "12345678",
|
|
||||||
role: "doesnt exist",
|
|
||||||
}),
|
|
||||||
).rejects.toThrow();
|
|
||||||
|
|
||||||
// takes role if provided
|
|
||||||
expect(
|
|
||||||
await app
|
|
||||||
.createUser({
|
|
||||||
email: "test2@bknd.io",
|
|
||||||
password: "12345678",
|
|
||||||
role: "admin",
|
|
||||||
})
|
|
||||||
.then((r) => r.role),
|
|
||||||
).toBe("admin");
|
|
||||||
|
|
||||||
// registering with role is not allowed
|
|
||||||
expect(
|
|
||||||
await $fns
|
|
||||||
.register({
|
|
||||||
email: "test3@bknd.io",
|
|
||||||
password: "12345678",
|
|
||||||
role: "admin",
|
|
||||||
})
|
|
||||||
.then((r) => r.res.ok),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
// takes default role
|
|
||||||
expect(
|
|
||||||
await $fns
|
|
||||||
.register({
|
|
||||||
email: "test3@bknd.io",
|
|
||||||
password: "12345678",
|
|
||||||
})
|
|
||||||
.then((r) => r.data.user.role),
|
|
||||||
).toBe("guest");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { Api } from "../../src/Api";
|
import { Api } from "../../src/Api";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("integration config", () => {
|
describe("integration config", () => {
|
||||||
it("should create an entity", async () => {
|
it("should create an entity", async () => {
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
/// <reference types="@types/bun" />
|
/// <reference types="@types/bun" />
|
||||||
|
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { registries } from "../../src";
|
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { mergeObject, randomString } from "../../src/core/utils";
|
import { mergeObject, randomString } from "../../src/core/utils";
|
||||||
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
import type { TAppMediaConfig } from "../../src/media/media-schema";
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { assetsPath, assetsTmpPath } from "../helper";
|
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
import * as proto from "data/prototype";
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
disableConsoleLog();
|
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
|
||||||
});
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
const path = `${assetsPath}/image.png`;
|
const path = `${assetsPath}/image.png`;
|
||||||
|
|
||||||
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
media: mergeObject(
|
media: mergeObject(
|
||||||
{
|
{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -36,6 +27,8 @@ async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -44,6 +37,9 @@ function makeName(ext: string) {
|
|||||||
return randomString(10) + "." + ext;
|
return randomString(10) + "." + ext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeAll(disableConsoleLog);
|
||||||
|
afterAll(enableConsoleLog);
|
||||||
|
|
||||||
describe("MediaController", () => {
|
describe("MediaController", () => {
|
||||||
test("accepts direct", async () => {
|
test("accepts direct", async () => {
|
||||||
const app = await makeApp();
|
const app = await makeApp();
|
||||||
@@ -55,6 +51,7 @@ describe("MediaController", () => {
|
|||||||
body: file,
|
body: file,
|
||||||
});
|
});
|
||||||
const result = (await res.json()) as any;
|
const result = (await res.json()) as any;
|
||||||
|
console.log(result);
|
||||||
expect(result.name).toBe(name);
|
expect(result.name).toBe(name);
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
||||||
@@ -95,121 +92,4 @@ describe("MediaController", () => {
|
|||||||
expect(res.status).toBe(413);
|
expect(res.status).toBe(413);
|
||||||
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audio files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.mp3`);
|
|
||||||
const name = makeName("mp3");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("audio/mpeg");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("text files", async () => {
|
|
||||||
const app = await makeApp();
|
|
||||||
const file = Bun.file(`${assetsPath}/test.txt`);
|
|
||||||
const name = makeName("txt");
|
|
||||||
const res = await app.server.request("/api/media/upload/" + name, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
const result = (await res.json()) as any;
|
|
||||||
expect(result.data.mime_type).toStartWith("text/plain");
|
|
||||||
expect(result.name).toBe(name);
|
|
||||||
|
|
||||||
const destFile = Bun.file(assetsTmpPath + "/" + name);
|
|
||||||
expect(destFile.exists()).resolves.toBe(true);
|
|
||||||
await destFile.delete();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("entity upload with max_items and overwrite", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
media: mergeObject(
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: assetsTmpPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
data: {
|
|
||||||
entities: {
|
|
||||||
posts: proto
|
|
||||||
.entity("posts", {
|
|
||||||
title: proto.text(),
|
|
||||||
cover: proto.medium(),
|
|
||||||
})
|
|
||||||
.toJSON(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
// create a post first
|
|
||||||
const createRes = await app.server.request("/api/data/entity/posts", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ title: "Test Post" }),
|
|
||||||
});
|
|
||||||
expect(createRes.status).toBe(201);
|
|
||||||
const { data: post } = (await createRes.json()) as any;
|
|
||||||
|
|
||||||
const file = Bun.file(path);
|
|
||||||
const uploadedFiles: string[] = [];
|
|
||||||
|
|
||||||
// upload first file to entity (should succeed)
|
|
||||||
const res1 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(201);
|
|
||||||
const result1 = (await res1.json()) as any;
|
|
||||||
uploadedFiles.push(result1.name);
|
|
||||||
|
|
||||||
// upload second file without overwrite (should fail - max_items reached)
|
|
||||||
const res2 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
});
|
|
||||||
expect(res2.status).toBe(400);
|
|
||||||
const result2 = (await res2.json()) as any;
|
|
||||||
expect(result2.error).toContain("Max items");
|
|
||||||
|
|
||||||
// upload third file with overwrite=true (should succeed and delete old file)
|
|
||||||
const res3 = await app.server.request(
|
|
||||||
`/api/media/entity/posts/${post.id}/cover?overwrite=true`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: file,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(res3.status).toBe(201);
|
|
||||||
const result3 = (await res3.json()) as any;
|
|
||||||
uploadedFiles.push(result3.name);
|
|
||||||
|
|
||||||
// verify old file was deleted from storage
|
|
||||||
const oldFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[0]);
|
|
||||||
expect(await oldFile.exists()).toBe(false);
|
|
||||||
|
|
||||||
// verify new file exists
|
|
||||||
const newFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[1]);
|
|
||||||
expect(await newFile.exists()).toBe(true);
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
await newFile.delete();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
import { StorageAdapter } from "media/storage/StorageAdapter";
|
import { StorageAdapter } from "media";
|
||||||
|
|
||||||
class TestAdapter extends StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|||||||
@@ -71,8 +71,6 @@ describe("media/mime-types", () => {
|
|||||||
["application/zip", "zip"],
|
["application/zip", "zip"],
|
||||||
["text/tab-separated-values", "tsv"],
|
["text/tab-separated-values", "tsv"],
|
||||||
["application/zip", "zip"],
|
["application/zip", "zip"],
|
||||||
["application/pdf", "pdf"],
|
|
||||||
["audio/mpeg", "mp3"],
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const [mime, ext] of tests) {
|
for (const [mime, ext] of tests) {
|
||||||
@@ -90,9 +88,6 @@ describe("media/mime-types", () => {
|
|||||||
["image.jpeg", "jpeg"],
|
["image.jpeg", "jpeg"],
|
||||||
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
||||||
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
|
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
|
||||||
["file.pdf", "pdf"],
|
|
||||||
["file.mp3", "mp3"],
|
|
||||||
["robots.txt", "txt"],
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
for (const [filename, ext] of tests) {
|
for (const [filename, ext] of tests) {
|
||||||
@@ -107,36 +102,4 @@ describe("media/mime-types", () => {
|
|||||||
const [, ext] = getRandomizedFilename(file).split(".");
|
const [, ext] = getRandomizedFilename(file).split(".");
|
||||||
expect(ext).toBe("jpg");
|
expect(ext).toBe("jpg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getRandomizedFilename with body", async () => {
|
|
||||||
// should keep "pdf"
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.pdf", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("pdf");
|
|
||||||
|
|
||||||
{
|
|
||||||
// no ext, should use "pdf" only for known formats
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// wrong ext, should keep the wrong one
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.what", { type: "application/pdf" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("what");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// txt
|
|
||||||
const [, ext] = getRandomizedFilename(
|
|
||||||
new File([""], "file.txt", { type: "text/plain" }),
|
|
||||||
).split(".");
|
|
||||||
expect(ext).toBe("txt");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { AuthController } from "../../src/auth/api/AuthController";
|
import { AuthController } from "../../src/auth/api/AuthController";
|
||||||
import { em, entity, make, text } from "data/prototype";
|
import { em, entity, make, text } from "../../src/data";
|
||||||
import { AppAuth, type ModuleBuildContext } from "modules";
|
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
|
||||||
|
import { disableConsoleLog, enableConsoleLog } from "../helper";
|
||||||
|
// @ts-ignore
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("AppAuth", () => {
|
describe("AppAuth", () => {
|
||||||
|
test.only("...", () => {
|
||||||
|
const auth = new AppAuth({});
|
||||||
|
console.log(auth.toJSON());
|
||||||
|
console.log(auth.config);
|
||||||
|
});
|
||||||
|
|
||||||
moduleTestSuite(AppAuth);
|
moduleTestSuite(AppAuth);
|
||||||
|
|
||||||
let ctx: ModuleBuildContext;
|
let ctx: ModuleBuildContext;
|
||||||
@@ -33,9 +37,11 @@ describe("AppAuth", () => {
|
|||||||
await auth.build();
|
await auth.build();
|
||||||
|
|
||||||
const oldConfig = auth.toJSON(true);
|
const oldConfig = auth.toJSON(true);
|
||||||
|
//console.log(oldConfig);
|
||||||
await auth.schema().patch("enabled", true);
|
await auth.schema().patch("enabled", true);
|
||||||
await auth.build();
|
await auth.build();
|
||||||
const newConfig = auth.toJSON(true);
|
const newConfig = auth.toJSON(true);
|
||||||
|
//console.log(newConfig);
|
||||||
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
|
expect(newConfig.jwt.secret).not.toBe(oldConfig.jwt.secret);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +67,7 @@ describe("AppAuth", () => {
|
|||||||
const app = new AuthController(auth).getController();
|
const app = new AuthController(auth).getController();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
disableConsoleLog();
|
||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -71,6 +78,7 @@ describe("AppAuth", () => {
|
|||||||
password: "12345678",
|
password: "12345678",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
enableConsoleLog();
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
@@ -109,6 +117,7 @@ describe("AppAuth", () => {
|
|||||||
const app = new AuthController(auth).getController();
|
const app = new AuthController(auth).getController();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
disableConsoleLog();
|
||||||
const res = await app.request("/password/register", {
|
const res = await app.request("/password/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -119,6 +128,7 @@ describe("AppAuth", () => {
|
|||||||
password: "12345678",
|
password: "12345678",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
enableConsoleLog();
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
const { data: users } = await ctx.em.repository("users").findMany();
|
const { data: users } = await ctx.em.repository("users").findMany();
|
||||||
@@ -138,7 +148,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("registers auth middleware for bknd routes only", async () => {
|
test("registers auth middleware for bknd routes only", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
jwt: {
|
jwt: {
|
||||||
@@ -168,7 +178,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("should allow additional user fields", async () => {
|
test("should allow additional user fields", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
entity_name: "users",
|
entity_name: "users",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -192,7 +202,7 @@ describe("AppAuth", () => {
|
|||||||
|
|
||||||
test("ensure user field configs is always correct", async () => {
|
test("ensure user field configs is always correct", async () => {
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
auth: {
|
auth: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@@ -223,32 +233,4 @@ describe("AppAuth", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("default role for registration must be a valid role", async () => {
|
|
||||||
const app = createApp({
|
|
||||||
config: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "123456",
|
|
||||||
},
|
|
||||||
allow_register: true,
|
|
||||||
roles: {
|
|
||||||
guest: {
|
|
||||||
is_default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.build();
|
|
||||||
|
|
||||||
const auth = app.module.auth;
|
|
||||||
// doesn't allow invalid role
|
|
||||||
expect(auth.schema().patch("default_role_register", "admin")).rejects.toThrow();
|
|
||||||
// allows valid role
|
|
||||||
await auth.schema().patch("default_role_register", "guest");
|
|
||||||
expect(auth.toJSON().default_role_register).toBe("guest");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test";
|
import { beforeEach, describe, expect, test } from "bun:test";
|
||||||
import { parse } from "core/utils/schema";
|
import { parse } from "core/object/schema";
|
||||||
import { fieldsSchema } from "../../src/data/data-schema";
|
import { fieldsSchema } from "../../src/data/data-schema";
|
||||||
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
import { AppData, type ModuleBuildContext } from "../../src/modules";
|
||||||
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
import { makeCtx, moduleTestSuite } from "./module-test-suite";
|
||||||
|
|||||||
@@ -1,28 +1,18 @@
|
|||||||
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createApp } from "core/test/utils";
|
import { createApp } from "core/test/utils";
|
||||||
import { em, entity, text } from "data/prototype";
|
import { em, entity, text } from "../../src/data";
|
||||||
import { registries } from "modules/registries";
|
|
||||||
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
|
||||||
import { AppMedia } from "../../src/media/AppMedia";
|
import { AppMedia } from "../../src/media/AppMedia";
|
||||||
import { moduleTestSuite } from "./module-test-suite";
|
import { moduleTestSuite } from "./module-test-suite";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("AppMedia", () => {
|
describe("AppMedia", () => {
|
||||||
test.skip("...", () => {
|
|
||||||
const media = new AppMedia();
|
|
||||||
console.log(media.toJSON());
|
|
||||||
});
|
|
||||||
|
|
||||||
moduleTestSuite(AppMedia);
|
moduleTestSuite(AppMedia);
|
||||||
|
|
||||||
test("should allow additional fields", async () => {
|
test("should allow additional fields", async () => {
|
||||||
registries.media.register("local", StorageLocalAdapter);
|
//registries.media.register("local", StorageLocalAdapter);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
config: {
|
initialConfig: {
|
||||||
media: {
|
media: {
|
||||||
entity_name: "media",
|
entity_name: "media",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -40,6 +30,7 @@ describe("AppMedia", () => {
|
|||||||
}).toJSON(),
|
}).toJSON(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
app.module.media.adapters.set("local", StorageLocalAdapter);
|
||||||
|
|
||||||
await app.build();
|
await app.build();
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { it, expect, describe, beforeAll, afterAll } from "bun:test";
|
|
||||||
import { DbModuleManager } from "modules/db/DbModuleManager";
|
|
||||||
import { getDummyConnection } from "../helper";
|
|
||||||
import { TABLE_NAME } from "modules/db/migrations";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(disableConsoleLog);
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("DbModuleManager", () => {
|
|
||||||
it("should extract secrets", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const m = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m.build();
|
|
||||||
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
|
||||||
await m.save();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with initial secrets", async () => {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
|
||||||
const db = dummyConnection.kysely;
|
|
||||||
const m = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
secrets: {
|
|
||||||
"auth.jwt.secret": "test",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m.build();
|
|
||||||
expect(m.toJSON(true).auth.jwt.secret).toBe("test");
|
|
||||||
|
|
||||||
const getSecrets = () =>
|
|
||||||
db
|
|
||||||
.selectFrom(TABLE_NAME)
|
|
||||||
.selectAll()
|
|
||||||
.where("type", "=", "secrets")
|
|
||||||
.executeTakeFirst()
|
|
||||||
.then((r) => r?.json);
|
|
||||||
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
|
|
||||||
// also after rebuild
|
|
||||||
await m.build();
|
|
||||||
await m.save();
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
|
|
||||||
// and ignore if already present
|
|
||||||
const m2 = new DbModuleManager(dummyConnection, {
|
|
||||||
initial: {
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
jwt: {
|
|
||||||
secret: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
secrets: {
|
|
||||||
"auth.jwt.secret": "something completely different",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await m2.build();
|
|
||||||
await m2.save();
|
|
||||||
expect(await getSecrets()).toEqual({ "auth.jwt.secret": "test" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { s, stripMark } from "core/object/schema";
|
||||||
import { em, entity, index, text } from "data/prototype";
|
import { EntityManager, em, entity, index, text } from "../../src/data";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
|
||||||
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
import { DummyConnection } from "../../src/data/connection/DummyConnection";
|
||||||
import { Module } from "../../src/modules/Module";
|
import { Module } from "../../src/modules/Module";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
import { disableConsoleLog, enableConsoleLog } from "core/utils";
|
||||||
|
import { Connection, entity, text } from "data";
|
||||||
import { Module } from "modules/Module";
|
import { Module } from "modules/Module";
|
||||||
import { getDefaultConfig } from "modules/ModuleManager";
|
import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
|
||||||
import { type ConfigTable, DbModuleManager as ModuleManager } from "modules/db/DbModuleManager";
|
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
|
||||||
|
|
||||||
import { CURRENT_VERSION, TABLE_NAME } from "modules/db/migrations";
|
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { s, stripMark } from "core/utils/schema";
|
import { s, stripMark } from "core/object/schema";
|
||||||
import { Connection } from "data/connection/Connection";
|
|
||||||
import { entity, text } from "data/prototype";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
describe("ModuleManager", async () => {
|
describe("ModuleManager", async () => {
|
||||||
test("s1: no config, no build", async () => {
|
test("s1: no config, no build", async () => {
|
||||||
@@ -82,6 +75,7 @@ describe("ModuleManager", async () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
|
//const { version, ...json } = mm.toJSON() as any;
|
||||||
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
const db = dummyConnection.kysely;
|
const db = dummyConnection.kysely;
|
||||||
@@ -96,6 +90,10 @@ describe("ModuleManager", async () => {
|
|||||||
|
|
||||||
await mm2.build();
|
await mm2.build();
|
||||||
|
|
||||||
|
/* console.log({
|
||||||
|
json,
|
||||||
|
configs: mm2.configs(),
|
||||||
|
}); */
|
||||||
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
|
||||||
expect(mm2.configs().data.entities?.test).toBeDefined();
|
expect(mm2.configs().data.entities?.test).toBeDefined();
|
||||||
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
|
||||||
@@ -133,7 +131,7 @@ describe("ModuleManager", async () => {
|
|||||||
const db = c2.dummyConnection.kysely;
|
const db = c2.dummyConnection.kysely;
|
||||||
|
|
||||||
const mm2 = new ModuleManager(c2.dummyConnection, {
|
const mm2 = new ModuleManager(c2.dummyConnection, {
|
||||||
initial: { version: version - 1, ...json } as any,
|
initial: { version: version - 1, ...json },
|
||||||
});
|
});
|
||||||
await mm2.syncConfigTable();
|
await mm2.syncConfigTable();
|
||||||
await db
|
await db
|
||||||
@@ -223,6 +221,8 @@ describe("ModuleManager", async () => {
|
|||||||
const c = getDummyConnection();
|
const c = getDummyConnection();
|
||||||
const mm = new ModuleManager(c.dummyConnection);
|
const mm = new ModuleManager(c.dummyConnection);
|
||||||
await mm.build();
|
await mm.build();
|
||||||
|
console.log("==".repeat(30));
|
||||||
|
console.log("");
|
||||||
const json = mm.configs();
|
const json = mm.configs();
|
||||||
|
|
||||||
const c2 = getDummyConnection();
|
const c2 = getDummyConnection();
|
||||||
@@ -268,6 +268,7 @@ describe("ModuleManager", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async build() {
|
override async build() {
|
||||||
|
//console.log("building FailingModule", this.config);
|
||||||
if (this.config.value && this.config.value < 0) {
|
if (this.config.value && this.config.value < 0) {
|
||||||
throw new Error("value must be positive, given: " + this.config.value);
|
throw new Error("value must be positive, given: " + this.config.value);
|
||||||
}
|
}
|
||||||
@@ -288,6 +289,9 @@ describe("ModuleManager", async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
|
||||||
|
afterEach(enableConsoleLog);
|
||||||
|
|
||||||
test("it builds", async () => {
|
test("it builds", async () => {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
const mm = new TestModuleManager(dummyConnection);
|
const mm = new TestModuleManager(dummyConnection);
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { App, type InitialModuleConfigs, createApp } from "/";
|
import { type InitialModuleConfigs, createApp } from "../../../src";
|
||||||
|
|
||||||
import { type Kysely, sql } from "kysely";
|
import { type Kysely, sql } from "kysely";
|
||||||
import { getDummyConnection } from "../../helper";
|
import { getDummyConnection } from "../../helper";
|
||||||
import v7 from "./samples/v7.json";
|
import v7 from "./samples/v7.json";
|
||||||
import v8 from "./samples/v8.json";
|
import v8 from "./samples/v8.json";
|
||||||
import v8_2 from "./samples/v8-2.json";
|
import v8_2 from "./samples/v8-2.json";
|
||||||
import v9 from "./samples/v9.json";
|
|
||||||
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
|
|
||||||
|
|
||||||
beforeAll(() => disableConsoleLog());
|
|
||||||
afterAll(enableConsoleLog);
|
|
||||||
|
|
||||||
// app expects migratable config to be present in database
|
// app expects migratable config to be present in database
|
||||||
async function createVersionedApp(
|
async function createVersionedApp(config: InitialModuleConfigs | any) {
|
||||||
config: InitialModuleConfigs | any,
|
|
||||||
opts?: { beforeCreateApp?: (db: Kysely<any>) => Promise<void> },
|
|
||||||
) {
|
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
|
|
||||||
if (!("version" in config)) throw new Error("config must have a version");
|
if (!("version" in config)) throw new Error("config must have a version");
|
||||||
@@ -42,10 +34,6 @@ async function createVersionedApp(
|
|||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
if (opts?.beforeCreateApp) {
|
|
||||||
await opts.beforeCreateApp(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
connection: dummyConnection,
|
connection: dummyConnection,
|
||||||
});
|
});
|
||||||
@@ -53,19 +41,6 @@ async function createVersionedApp(
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRawConfig(
|
|
||||||
app: App,
|
|
||||||
opts?: { version?: number; types?: ("config" | "diff" | "backup" | "secrets")[] },
|
|
||||||
) {
|
|
||||||
const db = app.em.connection.kysely;
|
|
||||||
return await db
|
|
||||||
.selectFrom("__bknd")
|
|
||||||
.selectAll()
|
|
||||||
.$if(!!opts?.version, (qb) => qb.where("version", "=", opts?.version))
|
|
||||||
.$if((opts?.types?.length ?? 0) > 0, (qb) => qb.where("type", "in", opts?.types))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Migrations", () => {
|
describe("Migrations", () => {
|
||||||
/**
|
/**
|
||||||
* updated auth strategies to have "enabled" prop
|
* updated auth strategies to have "enabled" prop
|
||||||
@@ -103,30 +78,4 @@ describe("Migrations", () => {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
expect(app.toJSON(true).server.admin).toBeUndefined();
|
expect(app.toJSON(true).server.admin).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("migration from 9 to 10", async () => {
|
|
||||||
expect(v9.version).toBe(9);
|
|
||||||
|
|
||||||
const app = await createVersionedApp(v9);
|
|
||||||
|
|
||||||
expect(app.version()).toBeGreaterThan(9);
|
|
||||||
// @ts-expect-error
|
|
||||||
expect(app.toJSON(true).media.adapter.config.secret_access_key).toBe(
|
|
||||||
"^^s3.secret_access_key^^",
|
|
||||||
);
|
|
||||||
const [config, secrets] = (await getRawConfig(app, {
|
|
||||||
version: 10,
|
|
||||||
types: ["config", "secrets"],
|
|
||||||
})) as any;
|
|
||||||
|
|
||||||
expect(config.json.auth.jwt.secret).toBe("");
|
|
||||||
expect(config.json.media.adapter.config.access_key).toBe("");
|
|
||||||
expect(config.json.media.adapter.config.secret_access_key).toBe("");
|
|
||||||
|
|
||||||
expect(secrets.json["auth.jwt.secret"]).toBe("^^jwt.secret^^");
|
|
||||||
expect(secrets.json["media.adapter.config.access_key"]).toBe("^^s3.access_key^^");
|
|
||||||
expect(secrets.json["media.adapter.config.secret_access_key"]).toBe(
|
|
||||||
"^^s3.secret_access_key^^",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,612 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 9,
|
|
||||||
"server": {
|
|
||||||
"cors": {
|
|
||||||
"origin": "*",
|
|
||||||
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
|
|
||||||
"allow_headers": [
|
|
||||||
"Content-Type",
|
|
||||||
"Content-Length",
|
|
||||||
"Authorization",
|
|
||||||
"Accept"
|
|
||||||
],
|
|
||||||
"allow_credentials": true
|
|
||||||
},
|
|
||||||
"mcp": { "enabled": false, "path": "/api/system/mcp" }
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"basepath": "/api/data",
|
|
||||||
"default_primary_format": "integer",
|
|
||||||
"entities": {
|
|
||||||
"media": {
|
|
||||||
"type": "system",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"path": { "type": "text", "config": { "required": true } },
|
|
||||||
"folder": {
|
|
||||||
"type": "boolean",
|
|
||||||
"config": {
|
|
||||||
"default_value": false,
|
|
||||||
"hidden": true,
|
|
||||||
"fillable": ["create"],
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mime_type": { "type": "text", "config": { "required": false } },
|
|
||||||
"size": { "type": "number", "config": { "required": false } },
|
|
||||||
"scope": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"hidden": true,
|
|
||||||
"fillable": ["create"],
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"etag": { "type": "text", "config": { "required": false } },
|
|
||||||
"modified_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"reference": { "type": "text", "config": { "required": false } },
|
|
||||||
"entity_id": { "type": "number", "config": { "required": false } },
|
|
||||||
"metadata": { "type": "json", "config": { "required": false } }
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "asc" }
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"type": "system",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"email": { "type": "text", "config": { "required": true } },
|
|
||||||
"strategy": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["password"] },
|
|
||||||
"required": true,
|
|
||||||
"hidden": ["update", "form"],
|
|
||||||
"fillable": ["create"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strategy_value": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"fillable": ["create"],
|
|
||||||
"hidden": ["read", "table", "update", "form"],
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["admin", "guest"] },
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"type": "strings",
|
|
||||||
"values": ["18-24", "25-34", "35-44", "45-64", "65+"]
|
|
||||||
},
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"height": { "type": "number", "config": { "required": false } },
|
|
||||||
"gender": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": { "type": "strings", "values": ["male", "female"] },
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "asc" }
|
|
||||||
},
|
|
||||||
"avatars": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"identifier": { "type": "text", "config": { "required": false } },
|
|
||||||
"payload": {
|
|
||||||
"type": "json",
|
|
||||||
"config": { "required": false, "hidden": ["table"] }
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"started_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"completed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"input": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "avatars"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "avatars"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"tryons": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"completed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"avatars_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Avatars",
|
|
||||||
"required": false,
|
|
||||||
"reference": "avatars",
|
|
||||||
"target": "avatars",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"output": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "tryons",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"products_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Products",
|
|
||||||
"required": false,
|
|
||||||
"reference": "products",
|
|
||||||
"target": "products",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"payload": {
|
|
||||||
"type": "json",
|
|
||||||
"config": { "required": false, "hidden": ["table"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"products": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enabled": { "type": "boolean", "config": { "required": false } },
|
|
||||||
"title": { "type": "text", "config": { "required": false } },
|
|
||||||
"url": { "type": "text", "config": { "required": false } },
|
|
||||||
"image": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "products",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"sites_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Sites",
|
|
||||||
"required": false,
|
|
||||||
"reference": "sites",
|
|
||||||
"target": "sites",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"garment_type": {
|
|
||||||
"type": "enum",
|
|
||||||
"config": {
|
|
||||||
"options": {
|
|
||||||
"type": "strings",
|
|
||||||
"values": ["auto", "tops", "bottoms", "one-pieces"]
|
|
||||||
},
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"sites": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": {
|
|
||||||
"format": "integer",
|
|
||||||
"fillable": false,
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"origin": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"pattern": "^(https?):\\/\\/([a-zA-Z0-9.-]+)(:\\d+)?$",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": { "type": "text", "config": { "required": false } },
|
|
||||||
"active": { "type": "boolean", "config": { "required": false } },
|
|
||||||
"logo": {
|
|
||||||
"type": "media",
|
|
||||||
"config": {
|
|
||||||
"required": false,
|
|
||||||
"fillable": ["update"],
|
|
||||||
"hidden": false,
|
|
||||||
"mime_types": [],
|
|
||||||
"virtual": true,
|
|
||||||
"entity": "sites",
|
|
||||||
"max_items": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"instructions": {
|
|
||||||
"type": "text",
|
|
||||||
"config": {
|
|
||||||
"html_config": {
|
|
||||||
"element": "textarea",
|
|
||||||
"props": { "rows": "2" }
|
|
||||||
},
|
|
||||||
"required": false,
|
|
||||||
"hidden": ["table"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"type": "regular",
|
|
||||||
"fields": {
|
|
||||||
"id": {
|
|
||||||
"type": "primary",
|
|
||||||
"config": { "format": "uuid", "fillable": false, "required": false }
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": true }
|
|
||||||
},
|
|
||||||
"claimed_at": {
|
|
||||||
"type": "date",
|
|
||||||
"config": { "type": "datetime", "required": false }
|
|
||||||
},
|
|
||||||
"url": { "type": "text", "config": { "required": false } },
|
|
||||||
"sites_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Sites",
|
|
||||||
"required": false,
|
|
||||||
"reference": "sites",
|
|
||||||
"target": "sites",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"users_id": {
|
|
||||||
"type": "relation",
|
|
||||||
"config": {
|
|
||||||
"label": "Users",
|
|
||||||
"required": false,
|
|
||||||
"reference": "users",
|
|
||||||
"target": "users",
|
|
||||||
"target_field": "id",
|
|
||||||
"target_field_type": "integer",
|
|
||||||
"on_delete": "set null"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": { "sort_field": "id", "sort_dir": "desc" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"relations": {
|
|
||||||
"poly_avatars_media_input": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "input" }
|
|
||||||
},
|
|
||||||
"poly_avatars_media_output": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "output" }
|
|
||||||
},
|
|
||||||
"n1_avatars_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "avatars",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_tryons_avatars": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "avatars",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_tryons_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"poly_tryons_media_output": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "output", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"poly_products_media_image": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "products",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "image", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"n1_tryons_products": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "tryons",
|
|
||||||
"target": "products",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"poly_sites_media_logo": {
|
|
||||||
"type": "poly",
|
|
||||||
"source": "sites",
|
|
||||||
"target": "media",
|
|
||||||
"config": { "mappedBy": "logo", "targetCardinality": 1 }
|
|
||||||
},
|
|
||||||
"n1_sessions_sites": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "sessions",
|
|
||||||
"target": "sites",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_sessions_users": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "sessions",
|
|
||||||
"target": "users",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"n1_products_sites": {
|
|
||||||
"type": "n:1",
|
|
||||||
"source": "products",
|
|
||||||
"target": "sites",
|
|
||||||
"config": {
|
|
||||||
"mappedBy": "",
|
|
||||||
"inversedBy": "",
|
|
||||||
"required": false,
|
|
||||||
"with_limit": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indices": {
|
|
||||||
"idx_unique_media_path": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["path"],
|
|
||||||
"unique": true
|
|
||||||
},
|
|
||||||
"idx_media_reference": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["reference"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_media_entity_id": {
|
|
||||||
"entity": "media",
|
|
||||||
"fields": ["entity_id"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_unique_users_email": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["email"],
|
|
||||||
"unique": true
|
|
||||||
},
|
|
||||||
"idx_users_strategy": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["strategy"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_users_strategy_value": {
|
|
||||||
"entity": "users",
|
|
||||||
"fields": ["strategy_value"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_sites_origin_active": {
|
|
||||||
"entity": "sites",
|
|
||||||
"fields": ["origin", "active"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_sites_active": {
|
|
||||||
"entity": "sites",
|
|
||||||
"fields": ["active"],
|
|
||||||
"unique": false
|
|
||||||
},
|
|
||||||
"idx_products_url": {
|
|
||||||
"entity": "products",
|
|
||||||
"fields": ["url"],
|
|
||||||
"unique": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"enabled": true,
|
|
||||||
"basepath": "/api/auth",
|
|
||||||
"entity_name": "users",
|
|
||||||
"allow_register": true,
|
|
||||||
"jwt": {
|
|
||||||
"secret": "^^jwt.secret^^",
|
|
||||||
"alg": "HS256",
|
|
||||||
"expires": 999999999,
|
|
||||||
"issuer": "issuer",
|
|
||||||
"fields": ["id", "email", "role"]
|
|
||||||
},
|
|
||||||
"cookie": {
|
|
||||||
"path": "/",
|
|
||||||
"sameSite": "none",
|
|
||||||
"secure": true,
|
|
||||||
"httpOnly": true,
|
|
||||||
"expires": 604800,
|
|
||||||
"partitioned": false,
|
|
||||||
"renew": true,
|
|
||||||
"pathSuccess": "/admin",
|
|
||||||
"pathLoggedOut": "/"
|
|
||||||
},
|
|
||||||
"strategies": {
|
|
||||||
"password": {
|
|
||||||
"enabled": true,
|
|
||||||
"type": "password",
|
|
||||||
"config": { "hashing": "sha256" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"guard": { "enabled": false },
|
|
||||||
"roles": {
|
|
||||||
"admin": { "implicit_allow": true },
|
|
||||||
"guest": { "is_default": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"enabled": true,
|
|
||||||
"basepath": "/api/media",
|
|
||||||
"entity_name": "media",
|
|
||||||
"storage": { "body_max_size": 0 },
|
|
||||||
"adapter": {
|
|
||||||
"type": "s3",
|
|
||||||
"config": {
|
|
||||||
"access_key": "^^s3.access_key^^",
|
|
||||||
"secret_access_key": "^^s3.secret_access_key^^",
|
|
||||||
"url": "https://1234.r2.cloudflarestorage.com/bucket-name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flows": { "basepath": "/api/flows", "flows": {} }
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { beforeEach, describe, expect, it } from "bun:test";
|
import { beforeEach, describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { Guard } from "auth/authorize/Guard";
|
import { Guard } from "../../src/auth";
|
||||||
import { EventManager } from "core/events";
|
import { DebugLogger } from "../../src/core";
|
||||||
import { EntityManager } from "data/entities/EntityManager";
|
import { EventManager } from "../../src/core/events";
|
||||||
import { Module, type ModuleBuildContext } from "modules/Module";
|
import { EntityManager } from "../../src/data";
|
||||||
|
import { Module, type ModuleBuildContext } from "../../src/modules/Module";
|
||||||
import { getDummyConnection } from "../helper";
|
import { getDummyConnection } from "../helper";
|
||||||
import { ModuleHelper } from "modules/ModuleHelper";
|
import { ModuleHelper } from "modules/ModuleHelper";
|
||||||
import { DebugLogger, McpServer } from "bknd/utils";
|
|
||||||
|
|
||||||
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
|
||||||
const { dummyConnection } = getDummyConnection();
|
const { dummyConnection } = getDummyConnection();
|
||||||
@@ -19,7 +19,6 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
|
|||||||
guard: new Guard(),
|
guard: new Guard(),
|
||||||
flags: Module.ctx_flags,
|
flags: Module.ctx_flags,
|
||||||
logger: new DebugLogger(false),
|
logger: new DebugLogger(false),
|
||||||
mcp: new McpServer(),
|
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -102,9 +102,7 @@ describe("json form", () => {
|
|||||||
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
|
||||||
|
|
||||||
for (const [pointer, schema, output] of examples) {
|
for (const [pointer, schema, output] of examples) {
|
||||||
expect(utils.isRequired(new Draft2019(schema), pointer, schema), `${pointer} `).toBe(
|
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
|
||||||
output,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -1,9 +1,6 @@
|
|||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
import c from "picocolors";
|
import c from "picocolors";
|
||||||
import { formatNumber } from "bknd/utils";
|
import { formatNumber } from "core/utils";
|
||||||
|
|
||||||
const deps = Object.keys(pkg.dependencies);
|
|
||||||
const external = ["jsonv-ts/*", "wrangler", "bknd", "bknd/*", ...deps];
|
|
||||||
|
|
||||||
const result = await Bun.build({
|
const result = await Bun.build({
|
||||||
entrypoints: ["./src/cli/index.ts"],
|
entrypoints: ["./src/cli/index.ts"],
|
||||||
@@ -11,7 +8,6 @@ const result = await Bun.build({
|
|||||||
outdir: "./dist/cli",
|
outdir: "./dist/cli",
|
||||||
env: "PUBLIC_*",
|
env: "PUBLIC_*",
|
||||||
minify: true,
|
minify: true,
|
||||||
external,
|
|
||||||
define: {
|
define: {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
|
|||||||
+83
-166
@@ -1,9 +1,6 @@
|
|||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import * as tsup from "tsup";
|
import * as tsup from "tsup";
|
||||||
import pkg from "./package.json" with { type: "json" };
|
import pkg from "./package.json" with { type: "json" };
|
||||||
import c from "picocolors";
|
|
||||||
import { watch as fsWatch } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
@@ -12,14 +9,6 @@ const types = args.includes("--types");
|
|||||||
const sourcemap = args.includes("--sourcemap");
|
const sourcemap = args.includes("--sourcemap");
|
||||||
const clean = args.includes("--clean");
|
const clean = args.includes("--clean");
|
||||||
|
|
||||||
// silence tsup
|
|
||||||
const oldConsole = {
|
|
||||||
log: console.log,
|
|
||||||
warn: console.warn,
|
|
||||||
};
|
|
||||||
console.log = () => {};
|
|
||||||
console.warn = () => {};
|
|
||||||
|
|
||||||
const define = {
|
const define = {
|
||||||
__isDev: "0",
|
__isDev: "0",
|
||||||
__version: JSON.stringify(pkg.version),
|
__version: JSON.stringify(pkg.version),
|
||||||
@@ -38,11 +27,11 @@ function buildTypes() {
|
|||||||
Bun.spawn(["bun", "build:types"], {
|
Bun.spawn(["bun", "build:types"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
oldConsole.log(c.cyan("[Types]"), c.green("built"));
|
console.info("Types built");
|
||||||
Bun.spawn(["bun", "tsc-alias"], {
|
Bun.spawn(["bun", "tsc-alias"], {
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
|
console.info("Types aliased");
|
||||||
types_running = false;
|
types_running = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -50,10 +39,6 @@ function buildTypes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (types && !watch) {
|
|
||||||
buildTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
let watcher_timeout: any;
|
let watcher_timeout: any;
|
||||||
function delayTypes() {
|
function delayTypes() {
|
||||||
if (!watch || !types) return;
|
if (!watch || !types) return;
|
||||||
@@ -63,51 +48,57 @@ function delayTypes() {
|
|||||||
watcher_timeout = setTimeout(buildTypes, 1000);
|
watcher_timeout = setTimeout(buildTypes, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependencies = Object.keys(pkg.dependencies);
|
if (types && !watch) {
|
||||||
|
buildTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function banner(title: string) {
|
||||||
|
console.info("");
|
||||||
|
console.info("=".repeat(40));
|
||||||
|
console.info(title.toUpperCase());
|
||||||
|
console.info("-".repeat(40));
|
||||||
|
}
|
||||||
|
|
||||||
// collection of always-external packages
|
// collection of always-external packages
|
||||||
const external = [
|
const external = [
|
||||||
...dependencies,
|
|
||||||
"bun:test",
|
"bun:test",
|
||||||
"node:test",
|
"node:test",
|
||||||
"node:assert/strict",
|
"node:assert/strict",
|
||||||
"@libsql/client",
|
"@libsql/client",
|
||||||
"bknd",
|
"bknd",
|
||||||
/^bknd\/.*/,
|
/^bknd\/.*/,
|
||||||
"jsonv-ts",
|
|
||||||
/^jsonv-ts\/.*/,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Building backend and general API
|
* Building backend and general API
|
||||||
*/
|
*/
|
||||||
async function buildApi() {
|
async function buildApi() {
|
||||||
|
banner("Building API");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
// don't use tsup's broken watch, we'll handle it ourselves
|
watch,
|
||||||
watch: false,
|
|
||||||
define,
|
define,
|
||||||
entry: [
|
entry: [
|
||||||
"src/index.ts",
|
"src/index.ts",
|
||||||
|
"src/core/index.ts",
|
||||||
"src/core/utils/index.ts",
|
"src/core/utils/index.ts",
|
||||||
|
"src/data/index.ts",
|
||||||
|
"src/media/index.ts",
|
||||||
"src/plugins/index.ts",
|
"src/plugins/index.ts",
|
||||||
"src/modes/index.ts",
|
|
||||||
],
|
],
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
external: [...external],
|
external: [...external],
|
||||||
metafile: true,
|
metafile: true,
|
||||||
target: "esnext",
|
|
||||||
platform: "browser",
|
platform: "browser",
|
||||||
removeNodeProtocol: false,
|
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
splitting: false,
|
splitting: false,
|
||||||
|
treeshake: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svg": "dataurl",
|
".svg": "dataurl",
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[API]"), c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,7 +115,7 @@ async function buildUi() {
|
|||||||
const base = {
|
const base = {
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch: false,
|
watch,
|
||||||
define,
|
define,
|
||||||
external: [
|
external: [
|
||||||
...external,
|
...external,
|
||||||
@@ -151,6 +142,7 @@ async function buildUi() {
|
|||||||
},
|
},
|
||||||
} satisfies tsup.Options;
|
} satisfies tsup.Options;
|
||||||
|
|
||||||
|
banner("Building UI");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
|
||||||
@@ -158,10 +150,10 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/index.js");
|
await rewriteClient("./dist/ui/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
banner("Building Client");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
...base,
|
...base,
|
||||||
entry: ["src/ui/client/index.ts"],
|
entry: ["src/ui/client/index.ts"],
|
||||||
@@ -169,7 +161,6 @@ async function buildUi() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/client/index.js");
|
await rewriteClient("./dist/ui/client/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -180,18 +171,16 @@ async function buildUi() {
|
|||||||
* - ui/client is external, and after built replaced with "bknd/client"
|
* - ui/client is external, and after built replaced with "bknd/client"
|
||||||
*/
|
*/
|
||||||
async function buildUiElements() {
|
async function buildUiElements() {
|
||||||
|
banner("Building UI Elements");
|
||||||
await tsup.build({
|
await tsup.build({
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch: false,
|
watch,
|
||||||
define,
|
define,
|
||||||
entry: ["src/ui/elements/index.ts"],
|
entry: ["src/ui/elements/index.ts"],
|
||||||
outDir: "dist/ui/elements",
|
outDir: "dist/ui/elements",
|
||||||
external: [
|
external: [
|
||||||
"ui/client",
|
"ui/client",
|
||||||
"bknd",
|
|
||||||
/^bknd\/.*/,
|
|
||||||
"wouter",
|
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
"react/jsx-runtime",
|
"react/jsx-runtime",
|
||||||
@@ -216,7 +205,6 @@ async function buildUiElements() {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await rewriteClient("./dist/ui/elements/index.js");
|
await rewriteClient("./dist/ui/elements/index.js");
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,17 +216,15 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
return {
|
return {
|
||||||
minify,
|
minify,
|
||||||
sourcemap,
|
sourcemap,
|
||||||
watch: false,
|
watch,
|
||||||
entry: [`src/adapter/${adapter}/index.ts`],
|
entry: [`src/adapter/${adapter}/index.ts`],
|
||||||
format: ["esm"],
|
format: ["esm"],
|
||||||
platform: "neutral",
|
platform: "neutral",
|
||||||
outDir: `dist/adapter/${adapter}`,
|
outDir: `dist/adapter/${adapter}`,
|
||||||
metafile: true,
|
metafile: true,
|
||||||
splitting: false,
|
splitting: false,
|
||||||
removeNodeProtocol: false,
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
delayTypes();
|
delayTypes();
|
||||||
oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
|
|
||||||
},
|
},
|
||||||
...overrides,
|
...overrides,
|
||||||
define: {
|
define: {
|
||||||
@@ -257,134 +243,65 @@ function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildAdapters() {
|
async function buildAdapters() {
|
||||||
await Promise.all([
|
banner("Building Adapters");
|
||||||
// base adapter handles
|
// base adapter handles
|
||||||
tsup.build({
|
await tsup.build({
|
||||||
...baseConfig(""),
|
...baseConfig(""),
|
||||||
target: "esnext",
|
entry: ["src/adapter/index.ts"],
|
||||||
platform: "neutral",
|
outDir: "dist/adapter",
|
||||||
entry: ["src/adapter/index.ts"],
|
|
||||||
outDir: "dist/adapter",
|
|
||||||
// only way to keep @vite-ignore comments
|
|
||||||
minify: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// specific adatpers
|
|
||||||
tsup.build(baseConfig("react-router")),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("browser", {
|
|
||||||
external: [/^sqlocal\/?.*?/, "wouter"],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("bun", {
|
|
||||||
external: [/^bun\:.*/],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tsup.build(baseConfig("astro")),
|
|
||||||
tsup.build(baseConfig("aws")),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("cloudflare", {
|
|
||||||
external: ["wrangler", "node:process"],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
tsup.build(
|
|
||||||
baseConfig("cloudflare/proxy", {
|
|
||||||
target: "esnext",
|
|
||||||
entry: ["src/adapter/cloudflare/proxy.ts"],
|
|
||||||
outDir: "dist/adapter/cloudflare",
|
|
||||||
metafile: false,
|
|
||||||
external: [/bknd/, "wrangler", "node:process"],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("vite"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("nextjs"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sveltekit"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("node"),
|
|
||||||
platform: "node",
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/edge"),
|
|
||||||
entry: ["src/adapter/sqlite/edge.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/node"),
|
|
||||||
entry: ["src/adapter/sqlite/node.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
platform: "node",
|
|
||||||
metafile: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
tsup.build({
|
|
||||||
...baseConfig("sqlite/bun"),
|
|
||||||
entry: ["src/adapter/sqlite/bun.ts"],
|
|
||||||
outDir: "dist/adapter/sqlite",
|
|
||||||
metafile: false,
|
|
||||||
external: [/^bun\:.*/],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function buildAll() {
|
|
||||||
await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial build
|
|
||||||
await buildAll();
|
|
||||||
|
|
||||||
// custom watcher since tsup's watch is broken in 8.3.5+
|
|
||||||
if (watch) {
|
|
||||||
oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
let isBuilding = false;
|
|
||||||
|
|
||||||
const rebuild = async () => {
|
|
||||||
if (isBuilding) return;
|
|
||||||
isBuilding = true;
|
|
||||||
oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
|
|
||||||
try {
|
|
||||||
await buildAll();
|
|
||||||
oldConsole.log(c.cyan("[Watch]"), c.green("done"));
|
|
||||||
} catch (e) {
|
|
||||||
oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
|
|
||||||
}
|
|
||||||
isBuilding = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedRebuild = () => {
|
|
||||||
if (debounceTimer) clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(rebuild, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// watch src directory recursively
|
|
||||||
fsWatch(join(import.meta.dir, "src"), { recursive: true }, (event, filename) => {
|
|
||||||
if (!filename) return;
|
|
||||||
// ignore non-source files
|
|
||||||
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx") && !filename.endsWith(".css"))
|
|
||||||
return;
|
|
||||||
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
|
|
||||||
debouncedRebuild();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// keep process alive
|
// specific adatpers
|
||||||
await new Promise(() => {});
|
await tsup.build(baseConfig("react-router"));
|
||||||
|
await tsup.build(
|
||||||
|
baseConfig("bun", {
|
||||||
|
external: [/^bun\:.*/],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await tsup.build(baseConfig("astro"));
|
||||||
|
await tsup.build(baseConfig("aws"));
|
||||||
|
await tsup.build(baseConfig("cloudflare"));
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("vite"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("nextjs"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("node"),
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/edge"),
|
||||||
|
entry: ["src/adapter/sqlite/edge.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/node"),
|
||||||
|
entry: ["src/adapter/sqlite/node.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
platform: "node",
|
||||||
|
metafile: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tsup.build({
|
||||||
|
...baseConfig("sqlite/bun"),
|
||||||
|
entry: ["src/adapter/sqlite/bun.ts"],
|
||||||
|
outDir: "dist/adapter/sqlite",
|
||||||
|
metafile: false,
|
||||||
|
external: [/^bun\:.*/],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await buildApi();
|
||||||
|
await buildUi();
|
||||||
|
await buildUiElements();
|
||||||
|
await buildAdapters();
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[install]
|
||||||
|
#registry = "http://localhost:4873"
|
||||||
|
|
||||||
|
[test]
|
||||||
|
coverageSkipTestFiles = true
|
||||||
+6
-6
@@ -17,7 +17,7 @@ async function run(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Read from stdout
|
// Read from stdout
|
||||||
const reader = (proc.stdout as ReadableStream).getReader();
|
const reader = proc.stdout.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
// Function to read chunks
|
// Function to read chunks
|
||||||
@@ -30,7 +30,7 @@ async function run(
|
|||||||
|
|
||||||
const text = decoder.decode(value);
|
const text = decoder.decode(value);
|
||||||
if (!resolveCalled) {
|
if (!resolveCalled) {
|
||||||
console.info(c.dim(text.replace(/\n$/, "")));
|
console.log(c.dim(text.replace(/\n$/, "")));
|
||||||
}
|
}
|
||||||
onChunk(
|
onChunk(
|
||||||
text,
|
text,
|
||||||
@@ -189,21 +189,21 @@ const adapters = {
|
|||||||
|
|
||||||
async function testAdapter(name: keyof typeof adapters) {
|
async function testAdapter(name: keyof typeof adapters) {
|
||||||
const config = adapters[name];
|
const config = adapters[name];
|
||||||
console.info("adapter", c.cyan(name));
|
console.log("adapter", c.cyan(name));
|
||||||
await config.clean();
|
await config.clean();
|
||||||
|
|
||||||
const { proc, data } = await config.start();
|
const { proc, data } = await config.start();
|
||||||
console.info("proc:", proc.pid, "data:", c.cyan(data));
|
console.log("proc:", proc.pid, "data:", c.cyan(data));
|
||||||
//proc.kill();process.exit(0);
|
//proc.kill();process.exit(0);
|
||||||
|
|
||||||
const add_env = "env" in config && config.env ? config.env : "";
|
const add_env = "env" in config && config.env ? config.env : "";
|
||||||
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
|
||||||
console.info("DONE!");
|
console.log("DONE!");
|
||||||
|
|
||||||
while (!proc.killed) {
|
while (!proc.killed) {
|
||||||
proc.kill("SIGINT");
|
proc.kill("SIGINT");
|
||||||
await Bun.sleep(250);
|
await Bun.sleep(250);
|
||||||
console.info("Waiting for process to exit...");
|
console.log("Waiting for process to exit...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { createApp } from "bknd/adapter/bun";
|
|
||||||
|
|
||||||
async function generate() {
|
|
||||||
console.info("Generating MCP documentation...");
|
|
||||||
const app = await createApp({
|
|
||||||
connection: {
|
|
||||||
url: ":memory:",
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
server: {
|
|
||||||
mcp: {
|
|
||||||
enabled: true,
|
|
||||||
path: "/mcp2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
media: {
|
|
||||||
enabled: true,
|
|
||||||
adapter: {
|
|
||||||
type: "local",
|
|
||||||
config: {
|
|
||||||
path: "./",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await app.build();
|
|
||||||
await app.getMcpClient().ping();
|
|
||||||
|
|
||||||
const { tools, resources } = app.mcp!.toJSON();
|
|
||||||
await Bun.write("../docs/mcp.json", JSON.stringify({ tools, resources }, null, 2));
|
|
||||||
|
|
||||||
console.info("MCP documentation generated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
void generate();
|
|
||||||
+73
-93
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.20.0",
|
"version": "0.16.0-rc.0",
|
||||||
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
|
||||||
"homepage": "https://bknd.io",
|
"homepage": "https://bknd.io",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -13,9 +13,8 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/bknd-io/bknd/issues"
|
"url": "https://github.com/bknd-io/bknd/issues"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.3.3",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.13"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
"dev": "BKND_CLI_LOG_LEVEL=debug vite",
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
|
||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && NODE_NO_WARNINGS=1 VITE_DB_URL=:memory: bun run test:e2e && bun run build:all && cp ../README.md ./",
|
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run test:e2e && bun run build:all && cp ../README.md ./",
|
||||||
"postpublish": "rm -f README.md",
|
"postpublish": "rm -f README.md",
|
||||||
"test": "ALL_TESTS=1 bun test --bail",
|
"test": "ALL_TESTS=1 bun test --bail",
|
||||||
"test:all": "bun run test && bun run test:node",
|
"test:all": "bun run test && bun run test:node",
|
||||||
@@ -40,112 +39,99 @@
|
|||||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||||
"test:vitest:coverage": "vitest run --coverage",
|
"test:vitest:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:adapters": "NODE_NO_WARNINGS=1 bun run e2e/adapters.ts",
|
"test:e2e:adapters": "bun run e2e/adapters.ts",
|
||||||
"test:e2e:ui": "VITE_DB_URL=:memory: playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:debug": "VITE_DB_URL=:memory: playwright test --debug",
|
"test:e2e:debug": "playwright test --debug",
|
||||||
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
|
"test:e2e:report": "playwright show-report"
|
||||||
"docs:build-assets": "bun internal/docs.build-assets.ts"
|
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@codemirror/lang-html": "^6.4.11",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@hono/swagger-ui": "^0.5.2",
|
"@hono/swagger-ui": "^0.5.1",
|
||||||
"@mantine/core": "^7.17.1",
|
"@mantine/core": "^7.17.1",
|
||||||
"@mantine/hooks": "^7.17.1",
|
"@mantine/hooks": "^7.17.1",
|
||||||
"@tanstack/react-form": "^1.0.5",
|
"@tanstack/react-form": "^1.0.5",
|
||||||
"@uiw/react-codemirror": "^4.25.2",
|
"@uiw/react-codemirror": "^4.23.10",
|
||||||
"@xyflow/react": "^12.9.2",
|
"@xyflow/react": "^12.4.4",
|
||||||
"aws4fetch": "^1.0.20",
|
"aws4fetch": "^1.0.20",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.2",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^5.3.1",
|
"fast-xml-parser": "^5.0.8",
|
||||||
"hono": "4.10.4",
|
"hono": "^4.7.11",
|
||||||
|
"json-schema-form-react": "^0.0.2",
|
||||||
"json-schema-library": "10.0.0-rc7",
|
"json-schema-library": "10.0.0-rc7",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"jsonv-ts": "^0.10.1",
|
"kysely": "^0.27.6",
|
||||||
"kysely": "0.28.8",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"oauth4webapi": "^2.11.1",
|
"oauth4webapi": "^2.11.1",
|
||||||
"object-path-immutable": "^4.1.2",
|
"object-path-immutable": "^4.1.2",
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"radix-ui": "^1.1.3",
|
"radix-ui": "^1.1.3",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.3"
|
||||||
"use-sync-external-store": "^1.6.0",
|
|
||||||
"zustand": "^4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.922.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@bluwy/giget-core": "^0.1.6",
|
"@bluwy/giget-core": "^0.1.2",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||||
"@cloudflare/vitest-pool-workers": "^0.10.4",
|
"@cloudflare/workers-types": "^4.20250606.0",
|
||||||
"@cloudflare/workers-types": "^4.20251014.0",
|
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hono/vite-dev-server": "^0.23.0",
|
"@hono/vite-dev-server": "^0.19.1",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^4.1.3",
|
||||||
"@libsql/client": "^0.15.15",
|
"@libsql/client": "^0.15.9",
|
||||||
"@mantine/modals": "^7.17.1",
|
"@mantine/modals": "^7.17.1",
|
||||||
"@mantine/notifications": "^7.17.1",
|
"@mantine/notifications": "^7.17.1",
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@rjsf/core": "5.22.2",
|
"@rjsf/core": "5.22.2",
|
||||||
"@rjsf/utils": "5.22.0",
|
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@tabler/icons-react": "3.35.0",
|
"@tabler/icons-react": "3.18.0",
|
||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.0.12",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.0.12",
|
||||||
"@tanstack/react-store": "^0.8.0",
|
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^22.13.10",
|
||||||
"@types/pg": "^8.15.6",
|
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "3.0.9",
|
"@vitest/coverage-v8": "^3.0.9",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"commander": "^14.0.2",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"jotai": "^2.12.2",
|
"jotai": "^2.12.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.0.0",
|
||||||
|
"jsonv-ts": "^0.2.2",
|
||||||
|
"kysely-d1": "^0.3.0",
|
||||||
"kysely-generic-sqlite": "^1.2.1",
|
"kysely-generic-sqlite": "^1.2.1",
|
||||||
"kysely-postgres-js": "^2.0.0",
|
|
||||||
"libsql": "^0.5.22",
|
|
||||||
"libsql-stateless-easy": "^1.8.0",
|
"libsql-stateless-easy": "^1.8.0",
|
||||||
"miniflare": "^4.20251011.2",
|
"open": "^10.1.0",
|
||||||
"open": "^10.2.0",
|
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"pg": "^8.16.3",
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"postgres": "^3.4.7",
|
"posthog-js-lite": "^3.4.2",
|
||||||
"posthog-js-lite": "^3.6.0",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.2.1",
|
||||||
"react-json-view-lite": "^2.5.0",
|
"react-json-view-lite": "^2.4.1",
|
||||||
"sql-formatter": "^15.6.10",
|
"sql-formatter": "^15.4.11",
|
||||||
"sqlocal": "^0.16.0",
|
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.0.12",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tsc-alias": "^1.8.16",
|
"tsc-alias": "^1.8.11",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.4.0",
|
||||||
"tsx": "^4.20.6",
|
"tsx": "^4.19.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^11.1.0",
|
||||||
"vite": "^7.1.12",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-circular-dependency": "^0.5.0",
|
"vite-plugin-circular-dependency": "^0.5.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "3.0.9",
|
"vitest": "^3.0.9",
|
||||||
"wouter": "^3.7.1",
|
"wouter": "^3.6.0"
|
||||||
"wrangler": "^4.45.4"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@hono/node-server": "^1.19.6"
|
"@hono/node-server": "^1.14.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=19",
|
"react": ">=19",
|
||||||
@@ -175,6 +161,16 @@
|
|||||||
"import": "./dist/ui/client/index.js",
|
"import": "./dist/ui/client/index.js",
|
||||||
"require": "./dist/ui/client/index.js"
|
"require": "./dist/ui/client/index.js"
|
||||||
},
|
},
|
||||||
|
"./data": {
|
||||||
|
"types": "./dist/types/data/index.d.ts",
|
||||||
|
"import": "./dist/data/index.js",
|
||||||
|
"require": "./dist/data/index.js"
|
||||||
|
},
|
||||||
|
"./core": {
|
||||||
|
"types": "./dist/types/core/index.d.ts",
|
||||||
|
"import": "./dist/core/index.js",
|
||||||
|
"require": "./dist/core/index.js"
|
||||||
|
},
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./dist/types/core/utils/index.d.ts",
|
"types": "./dist/types/core/utils/index.d.ts",
|
||||||
"import": "./dist/core/utils/index.js",
|
"import": "./dist/core/utils/index.js",
|
||||||
@@ -185,16 +181,16 @@
|
|||||||
"import": "./dist/cli/index.js",
|
"import": "./dist/cli/index.js",
|
||||||
"require": "./dist/cli/index.js"
|
"require": "./dist/cli/index.js"
|
||||||
},
|
},
|
||||||
|
"./media": {
|
||||||
|
"types": "./dist/types/media/index.d.ts",
|
||||||
|
"import": "./dist/media/index.js",
|
||||||
|
"require": "./dist/media/index.js"
|
||||||
|
},
|
||||||
"./plugins": {
|
"./plugins": {
|
||||||
"types": "./dist/types/plugins/index.d.ts",
|
"types": "./dist/types/plugins/index.d.ts",
|
||||||
"import": "./dist/plugins/index.js",
|
"import": "./dist/plugins/index.js",
|
||||||
"require": "./dist/plugins/index.js"
|
"require": "./dist/plugins/index.js"
|
||||||
},
|
},
|
||||||
"./modes": {
|
|
||||||
"types": "./dist/types/modes/index.d.ts",
|
|
||||||
"import": "./dist/modes/index.js",
|
|
||||||
"require": "./dist/modes/index.js"
|
|
||||||
},
|
|
||||||
"./adapter/sqlite": {
|
"./adapter/sqlite": {
|
||||||
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
"types": "./dist/types/adapter/sqlite/edge.d.ts",
|
||||||
"import": {
|
"import": {
|
||||||
@@ -214,11 +210,6 @@
|
|||||||
"import": "./dist/adapter/cloudflare/index.js",
|
"import": "./dist/adapter/cloudflare/index.js",
|
||||||
"require": "./dist/adapter/cloudflare/index.js"
|
"require": "./dist/adapter/cloudflare/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/cloudflare/proxy": {
|
|
||||||
"types": "./dist/types/adapter/cloudflare/proxy.d.ts",
|
|
||||||
"import": "./dist/adapter/cloudflare/proxy.js",
|
|
||||||
"require": "./dist/adapter/cloudflare/proxy.js"
|
|
||||||
},
|
|
||||||
"./adapter": {
|
"./adapter": {
|
||||||
"types": "./dist/types/adapter/index.d.ts",
|
"types": "./dist/types/adapter/index.d.ts",
|
||||||
"import": "./dist/adapter/index.js"
|
"import": "./dist/adapter/index.js"
|
||||||
@@ -253,30 +244,22 @@
|
|||||||
"import": "./dist/adapter/astro/index.js",
|
"import": "./dist/adapter/astro/index.js",
|
||||||
"require": "./dist/adapter/astro/index.js"
|
"require": "./dist/adapter/astro/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/sveltekit": {
|
|
||||||
"types": "./dist/types/adapter/sveltekit/index.d.ts",
|
|
||||||
"import": "./dist/adapter/sveltekit/index.js",
|
|
||||||
"require": "./dist/adapter/sveltekit/index.js"
|
|
||||||
},
|
|
||||||
"./adapter/aws": {
|
"./adapter/aws": {
|
||||||
"types": "./dist/types/adapter/aws/index.d.ts",
|
"types": "./dist/types/adapter/aws/index.d.ts",
|
||||||
"import": "./dist/adapter/aws/index.js",
|
"import": "./dist/adapter/aws/index.js",
|
||||||
"require": "./dist/adapter/aws/index.js"
|
"require": "./dist/adapter/aws/index.js"
|
||||||
},
|
},
|
||||||
"./adapter/browser": {
|
|
||||||
"types": "./dist/types/adapter/browser/index.d.ts",
|
|
||||||
"import": "./dist/adapter/browser/index.js",
|
|
||||||
"require": "./dist/adapter/browser/index.js"
|
|
||||||
},
|
|
||||||
"./dist/main.css": "./dist/ui/main.css",
|
"./dist/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
|
||||||
"./static/*": "./dist/static/*"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
"data": ["./dist/types/data/index.d.ts"],
|
||||||
|
"core": ["./dist/types/core/index.d.ts"],
|
||||||
"utils": ["./dist/types/core/utils/index.d.ts"],
|
"utils": ["./dist/types/core/utils/index.d.ts"],
|
||||||
"cli": ["./dist/types/cli/index.d.ts"],
|
"cli": ["./dist/types/cli/index.d.ts"],
|
||||||
|
"media": ["./dist/types/media/index.d.ts"],
|
||||||
"plugins": ["./dist/types/plugins/index.d.ts"],
|
"plugins": ["./dist/types/plugins/index.d.ts"],
|
||||||
"adapter": ["./dist/types/adapter/index.d.ts"],
|
"adapter": ["./dist/types/adapter/index.d.ts"],
|
||||||
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
"adapter/cloudflare": ["./dist/types/adapter/cloudflare/index.d.ts"],
|
||||||
@@ -285,7 +268,6 @@
|
|||||||
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
"adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"],
|
||||||
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
|
||||||
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
|
||||||
"adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"],
|
|
||||||
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -315,8 +297,6 @@
|
|||||||
"remix",
|
"remix",
|
||||||
"react-router",
|
"react-router",
|
||||||
"astro",
|
"astro",
|
||||||
"sveltekit",
|
|
||||||
"svelte",
|
|
||||||
"bun",
|
"bun",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
|
|||||||
+37
-51
@@ -1,10 +1,10 @@
|
|||||||
import type { SafeUser } from "bknd";
|
import type { SafeUser } from "auth";
|
||||||
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
import { AuthApi, type AuthApiOptions } from "auth/api/AuthApi";
|
||||||
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
import { DataApi, type DataApiOptions } from "data/api/DataApi";
|
||||||
import { decode } from "hono/jwt";
|
import { decode } from "hono/jwt";
|
||||||
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
import { MediaApi, type MediaApiOptions } from "media/api/MediaApi";
|
||||||
import { SystemApi } from "modules/SystemApi";
|
import { SystemApi } from "modules/SystemApi";
|
||||||
import { omitKeys } from "bknd/utils";
|
import { omitKeys } from "core/utils";
|
||||||
import type { BaseModuleApiOptions } from "modules";
|
import type { BaseModuleApiOptions } from "modules";
|
||||||
|
|
||||||
export type TApiUser = SafeUser;
|
export type TApiUser = SafeUser;
|
||||||
@@ -40,11 +40,10 @@ export type ApiOptions = {
|
|||||||
data?: SubApiOptions<DataApiOptions>;
|
data?: SubApiOptions<DataApiOptions>;
|
||||||
auth?: SubApiOptions<AuthApiOptions>;
|
auth?: SubApiOptions<AuthApiOptions>;
|
||||||
media?: SubApiOptions<MediaApiOptions>;
|
media?: SubApiOptions<MediaApiOptions>;
|
||||||
credentials?: RequestCredentials;
|
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
token?: string;
|
token?: string;
|
||||||
user?: TApiUser | null;
|
user?: TApiUser;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
request: Request;
|
request: Request;
|
||||||
@@ -61,14 +60,14 @@ export class Api {
|
|||||||
private token?: string;
|
private token?: string;
|
||||||
private user?: TApiUser;
|
private user?: TApiUser;
|
||||||
private verified = false;
|
private verified = false;
|
||||||
public token_transport: "header" | "cookie" | "none" = "header";
|
private token_transport: "header" | "cookie" | "none" = "header";
|
||||||
|
|
||||||
public system!: SystemApi;
|
public system!: SystemApi;
|
||||||
public data!: DataApi;
|
public data!: DataApi;
|
||||||
public auth!: AuthApi;
|
public auth!: AuthApi;
|
||||||
public media!: MediaApi;
|
public media!: MediaApi;
|
||||||
|
|
||||||
constructor(public options: ApiOptions = {}) {
|
constructor(private options: ApiOptions = {}) {
|
||||||
// only mark verified if forced
|
// only mark verified if forced
|
||||||
this.verified = options.verified === true;
|
this.verified = options.verified === true;
|
||||||
|
|
||||||
@@ -130,45 +129,29 @@ export class Api {
|
|||||||
} else if (this.storage) {
|
} else if (this.storage) {
|
||||||
this.storage.getItem(this.tokenKey).then((token) => {
|
this.storage.getItem(this.tokenKey).then((token) => {
|
||||||
this.token_transport = "header";
|
this.token_transport = "header";
|
||||||
this.updateToken(token ? String(token) : undefined, {
|
this.updateToken(token ? String(token) : undefined);
|
||||||
verified: true,
|
|
||||||
trigger: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Make storage async to allow async storages even if sync given
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private get storage() {
|
private get storage() {
|
||||||
const storage = this.options.storage;
|
if (!this.options.storage) return null;
|
||||||
return new Proxy(
|
return {
|
||||||
{},
|
getItem: async (key: string) => {
|
||||||
{
|
return await this.options.storage!.getItem(key);
|
||||||
get(_, prop) {
|
|
||||||
return (...args: any[]) => {
|
|
||||||
const response = storage ? storage[prop](...args) : undefined;
|
|
||||||
if (response instanceof Promise) {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
// biome-ignore lint/suspicious/noThenProperty: it's a promise :)
|
|
||||||
then: (fn) => fn(response),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
) as any;
|
setItem: async (key: string, value: string) => {
|
||||||
|
return await this.options.storage!.setItem(key, value);
|
||||||
|
},
|
||||||
|
removeItem: async (key: string) => {
|
||||||
|
return await this.options.storage!.removeItem(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToken(
|
updateToken(token?: string, opts?: { rebuild?: boolean; trigger?: boolean }) {
|
||||||
token?: string,
|
|
||||||
opts?: { rebuild?: boolean; verified?: boolean; trigger?: boolean },
|
|
||||||
) {
|
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.verified = opts?.verified === true;
|
this.verified = false;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
|
||||||
@@ -176,22 +159,21 @@ export class Api {
|
|||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = () => {
|
|
||||||
if (opts?.trigger !== false) {
|
|
||||||
this.options.onAuthStateChange?.(this.getAuthState());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.storage) {
|
if (this.storage) {
|
||||||
const key = this.tokenKey;
|
const key = this.tokenKey;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.storage.setItem(key, token).then(emit);
|
this.storage.setItem(key, token).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.storage.removeItem(key).then(emit);
|
this.storage.removeItem(key).then(() => {
|
||||||
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (opts?.trigger !== false) {
|
if (opts?.trigger !== false) {
|
||||||
emit();
|
this.options.onAuthStateChange?.(this.getAuthState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +182,6 @@ export class Api {
|
|||||||
|
|
||||||
private markAuthVerified(verfied: boolean) {
|
private markAuthVerified(verfied: boolean) {
|
||||||
this.verified = verfied;
|
this.verified = verfied;
|
||||||
this.options.onAuthStateChange?.(this.getAuthState());
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +208,11 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyAuth() {
|
async verifyAuth() {
|
||||||
|
if (!this.token) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { ok, data } = await this.auth.me();
|
const { ok, data } = await this.auth.me();
|
||||||
const user = data?.user;
|
const user = data?.user;
|
||||||
@@ -235,10 +221,10 @@ export class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.user = user;
|
this.user = user;
|
||||||
} catch (e) {
|
|
||||||
this.updateToken(undefined);
|
|
||||||
} finally {
|
|
||||||
this.markAuthVerified(true);
|
this.markAuthVerified(true);
|
||||||
|
} catch (e) {
|
||||||
|
this.markAuthVerified(false);
|
||||||
|
this.updateToken(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +239,6 @@ export class Api {
|
|||||||
headers: this.options.headers,
|
headers: this.options.headers,
|
||||||
token_transport: this.token_transport,
|
token_transport: this.token_transport,
|
||||||
verbose: this.options.verbose,
|
verbose: this.options.verbose,
|
||||||
credentials: this.options.credentials,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,9 +257,10 @@ export class Api {
|
|||||||
this.auth = new AuthApi(
|
this.auth = new AuthApi(
|
||||||
{
|
{
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
credentials: this.options.storage ? "omit" : "include",
|
||||||
...this.options.auth,
|
...this.options.auth,
|
||||||
onTokenUpdate: (token, verified) => {
|
onTokenUpdate: (token) => {
|
||||||
this.updateToken(token, { rebuild: true, verified, trigger: true });
|
this.updateToken(token, { rebuild: true });
|
||||||
this.options.auth?.onTokenUpdate?.(token);
|
this.options.auth?.onTokenUpdate?.(token);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user