Compare commits

...

338 Commits

Author SHA1 Message Date
dswbx 7a046fa18e updated references 2025-07-07 08:39:21 +02:00
dswbx 786d1d1bd4 init media adapter refactoring 2025-07-05 18:12:55 +02:00
dswbx c384bf4dd4 remove unneeded vite dep 2025-07-05 14:17:26 +02:00
dswbx db2a994a01 Merge pull request #192 from bknd-io/feat/jsonv-refactor
feat/jsonv-refactor
2025-07-05 14:02:22 +02:00
dswbx 166ea4a71b removed @sinclair/typebox 2025-07-05 13:57:36 +02:00
dswbx 0d3bb3b7d6 added validator for rjsf, hook form via standard schema 2025-07-05 13:29:30 +02:00
dswbx cfbec5b6ea Merge remote-tracking branch 'origin/main' into feat/jsonv-refactor
# Conflicts:
#	bun.lock
2025-07-05 11:11:06 +02:00
dswbx 957355c862 bump v0.15 2025-07-05 11:08:26 +02:00
dswbx fc282b795e Merge pull request #189 from bknd-io/release/0.15
Release 0.15
2025-07-05 10:24:03 +02:00
dswbx bc65f5f311 Merge remote-tracking branch 'origin/main' into release/0.15 2025-07-05 10:21:25 +02:00
dswbx 5355f2593e readme: add filesystem 2025-07-05 09:43:22 +02:00
dswbx e939debba5 auth: add delete auth after flash in case only one is picked up 2025-07-05 09:43:14 +02:00
dswbx b35ee36fb1 admin: fix back behavior to not rely on history object 2025-07-05 09:42:53 +02:00
dswbx 109c72e84f update readme and docs on dbs 2025-07-03 15:31:14 +02:00
dswbx 7e0b02a125 Merge pull request #194 from stormbyte/main
Update docker.mdx
2025-07-03 15:16:06 +02:00
dswbx d4076803c6 docs: move file name to block, slightly reformulate docker compose instructions 2025-07-03 15:15:18 +02:00
dswbx a5959acb34 fix admin's assetPath for remote static assets 2025-07-03 15:06:21 +02:00
N.A. Walhof a4ddf780ce Finish the sentences 2025-07-03 15:05:49 +02:00
dswbx 2f684765de libsql: update typings to allow minimal client fns (execute, batch) 2025-07-03 14:11:58 +02:00
dswbx 5143ee5726 updated schemas, fixed tests, skipping flow tests for now 2025-07-03 14:00:29 +02:00
dswbx 22b54862e8 admin: fix theme flash 2025-07-02 18:14:35 +02:00
dswbx fe1716ed01 fix secret schema 2025-07-02 17:59:38 +02:00
dswbx 0c31dcdb95 updated lock 2025-07-02 17:58:47 +02:00
dswbx 4cc0f8e172 Merge branch 'release/0.15' into feat/jsonv-refactor
# Conflicts:
#	app/build.ts
#	app/package.json
#	app/src/App.ts
#	app/src/adapter/cloudflare/storage/StorageR2Adapter.ts
#	app/src/auth/authenticate/Authenticator.ts
#	app/src/auth/authenticate/strategies/PasswordStrategy.ts
#	app/src/data/entities/Entity.ts
#	app/src/data/fields/DateField.ts
#	app/src/data/server/query.ts
#	app/src/flows/flows/triggers/EventTrigger.ts
#	app/src/flows/tasks/presets/LogTask.ts
#	app/src/media/AppMedia.ts
#	app/src/modules/server/AppServer.ts
#	app/src/modules/server/SystemController.ts
#	app/vite.dev.ts
#	bun.lock
2025-07-02 17:18:12 +02:00
dswbx 45138c25f0 refactor auth/media entities to separate files, suppress node:sqlite warning 2025-07-02 16:36:06 +02:00
dswbx 80034b9b0a fix plugins imports 2025-07-02 16:13:22 +02:00
dswbx e6ee75c712 update edge sqlite import (libsql) 2025-07-02 16:08:33 +02:00
dswbx 56287eb05e fixing sqlite imports, clean up bun and cf examples 2025-07-02 15:46:57 +02:00
dswbx 144f35ec66 Merge pull request #195 from bknd-io/fix/cloudflare-media-range
fix cloudflare r2 adapter range requests
2025-07-02 14:10:43 +02:00
dswbx d1378c6c51 fix cloudflare r2 adapter range requests 2025-07-02 14:07:26 +02:00
dswbx 4e10b36d0d Merge pull request #193 from bknd-io/feat/refactor-sqlites
feat/refactor-sqlites
2025-07-02 14:05:58 +02:00
dswbx 70737c04cd refactor console imports, added config update event 2025-07-02 14:02:50 +02:00
dswbx d41fd5541f rewrite libsql and cloudflare sqlite's to use the generic adapter 2025-07-02 14:02:33 +02:00
stormbyte e76e3531d7 Update docker.mdx
Add docker compose self build image
2025-06-26 15:43:51 +02:00
dswbx a9f3a582eb fix tests and imports 2025-06-25 09:42:25 +02:00
dswbx 57ae2f333c inlined libsql dialect, rewrote d1 to use generic sqlite 2025-06-25 09:35:47 +02:00
dswbx c161a26ec0 test secrets extraction 2025-06-25 07:49:39 +02:00
dswbx 6e78a4c238 fixes 2025-06-21 17:05:27 +02:00
dswbx 42edce904f initial refactor 2025-06-21 13:35:58 +02:00
dswbx b2086c4da7 fix: plugin schema reconciliation 2025-06-18 10:31:40 +02:00
dswbx 344d729320 Merge pull request #191 from bknd-io/feat/admin-add-options-for-static
admin: add options such as logo return path when served static
2025-06-18 08:15:45 +02:00
dswbx c86f4c12b7 admin: add options such as logo return path when served static 2025-06-18 07:56:58 +02:00
dswbx aaa97ed113 finalize initial app resources/drivers 2025-06-17 19:51:12 +02:00
dswbx 69c8aec6fb Merge pull request #190 from bknd-io/feat/app-resources
init app resources
2025-06-17 19:49:09 +02:00
dswbx f5ceffd80e fix tests 2025-06-17 13:34:31 +02:00
dswbx a9f367aa2c updated build to exclude libsql 2025-06-17 13:18:13 +02:00
dswbx b87696a0db init app resources 2025-06-14 16:59:03 +02:00
dswbx 3338804c34 simplify and export generic sqlite functions 2025-06-14 16:58:38 +02:00
dswbx 6b3ac9e6e2 fix toDriver mutation convertion not respecting default values, react re-renders on navigation, mutator result logging 2025-06-14 08:06:05 +02:00
dswbx 8e1ecfcfe3 change app plugins to be a map to prevent duplicates and provide easier access 2025-06-14 07:22:44 +02:00
dswbx 6e2596fb77 fixing tests 2025-06-14 07:08:46 +02:00
dswbx 2ada4e9f20 various fixes: refactored imports, introduced fromDriver/toDriver to improve compat 2025-06-13 21:15:29 +02:00
dswbx cc038a0a9a Merge pull request #187 from bknd-io/feat/plugin-improvements
improved app plugins
2025-06-13 17:29:42 +02:00
dswbx af6d1960b9 fix tests 2025-06-13 17:27:58 +02:00
dswbx 96ae8f9d63 Merge remote-tracking branch 'origin/release/0.15' into feat/plugin-improvements
# Conflicts:
#	app/package.json
#	app/src/App.ts
2025-06-13 17:24:54 +02:00
dswbx c09d1d53b3 Merge pull request #186 from bknd-io/feat/unify-connections
feat/unify-connections
2025-06-13 17:20:05 +02:00
dswbx 2239333f08 updated docs on databases 2025-06-13 17:18:29 +02:00
dswbx 0710b5bf1f Merge pull request #188 from bknd-io/feat/generic-sqlite
env aware sqlite (node, bun)
2025-06-13 16:00:08 +02:00
dswbx 03c4c41d2d cli now uses adapter logic, require node v22 2025-06-13 15:58:06 +02:00
dswbx 85d8542a2e test with node setup pre step 2025-06-13 14:44:55 +02:00
dswbx f8d2a9090e fixing tests, move node tests to vitest 2025-06-13 14:38:30 +02:00
dswbx bbb7bfb7a1 feat: adding env aware endpoint to obtain sqlite connection, remove libsql hard dependency 2025-06-13 11:09:47 +02:00
dswbx 28e277afe1 updated cf image optimization plugin 2025-06-13 08:30:54 +02:00
dswbx 8517c9b90b added a few initial plugins 2025-06-12 19:58:18 +02:00
dswbx fe5ccd4206 refactor and move cloudflare image transformation plugin 2025-06-12 17:00:06 +02:00
dswbx 8b4b63b3cd feat: improved abilities of plugins, moved schema fns to ctx 2025-06-12 15:29:53 +02:00
dswbx 9c4aac8843 docs: simplified custom postgres description 2025-06-12 10:42:49 +02:00
dswbx d5bb6ffa61 fix adapters, handle entity enum more gracefully 2025-06-12 10:24:50 +02:00
dswbx fc513bb413 Merge pull request #184 from bknd-io/release/0.14
Release 0.14
2025-06-12 09:52:01 +02:00
dswbx 4162b9878a fix admin controller to only serve if defined, and only from specified endpoints 2025-06-12 09:45:14 +02:00
dswbx c75f8d0937 reduce schema manager query log to debug, fix useSearch 2025-06-12 09:23:31 +02:00
dswbx 046c1d21b1 fixed tests 2025-06-12 09:12:40 +02:00
dswbx 6c2e579596 connection: rewrote query execution, batching, added generic sqlite, added node/bun sqlite, aligned repo/mutator results 2025-06-12 09:02:18 +02:00
dswbx 88419548c7 admin: fix useSearch 2025-06-10 08:38:10 +02:00
dswbx 12c955155d bump rc 2025-06-09 20:27:02 +02:00
dswbx 15af9f5261 fix use-search 2025-06-09 20:25:57 +02:00
dswbx 39efb911e9 admin header: added links to docs and version 2025-06-09 07:29:20 +02:00
dswbx 4729203d47 fix useSearch and admin path registration 2025-06-09 07:21:32 +02:00
dswbx 0db052acca Merge pull request #182 from bknd-io/feat/postgres-improvements
postgres: added `pg` and `postgres`, and examples for xata and neon
2025-06-07 11:14:45 +02:00
dswbx 6e08f45857 updated docs on new postgres instructions 2025-06-07 11:11:42 +02:00
dswbx 7bff84d601 updated postgres package build 2025-06-07 10:31:22 +02:00
dswbx e66e05b2b0 postgres: make sure to store id as varchar if uuid 2025-06-07 09:59:50 +02:00
dswbx 5b318ce485 Merge remote-tracking branch 'origin/release/0.14' into feat/postgres-improvements 2025-06-07 09:41:32 +02:00
dswbx 22de16fe17 Merge pull request #183 from bknd-io/feat/uuid
feat: adding initial uuid support
2025-06-07 09:40:43 +02:00
dswbx 5d26673bc6 Merge remote-tracking branch 'origin/release/0.14' into feat/uuid
# Conflicts:
#	app/tsconfig.json
#	bun.lock
2025-06-07 09:38:59 +02:00
dswbx dfa4bc0e33 Merge pull request #181 from bknd-io/feat/cf-sessions
adding d1 session support
2025-06-07 09:32:16 +02:00
dswbx e51b89a18a store session cookie/header name in constants, updated docs 2025-06-07 09:29:56 +02:00
dswbx 63988e0c5f store session cookie/header name in constants, updated docs 2025-06-07 09:29:38 +02:00
dswbx 297fd85a4f Merge branch 'release/0.14' into feat/postgres-improvements 2025-06-07 09:01:49 +02:00
dswbx a82fbe7400 Merge branch 'release/0.14' into feat/cf-sessions 2025-06-07 09:01:05 +02:00
dswbx 74e1e9a03f Merge pull request #179 from bknd-io/fix/hono-cookie-body-disturb
fix: lock hono version because of changed header setting in 4.7.7
2025-06-07 08:59:44 +02:00
dswbx 05a81b5bdc Merge remote-tracking branch 'origin/release/0.14' into fix/hono-cookie-body-disturb 2025-06-07 08:52:54 +02:00
dswbx 9ff49103fb bump rc 2025-06-07 08:50:23 +02:00
dswbx a5b59c004e fix logout by setting the api route as logout target 2025-06-07 08:48:28 +02:00
dswbx a0edcf483b added more tests, updated json schema generation 2025-06-07 08:24:21 +02:00
dswbx 9a18e354cd feat: adding initial uuid support 2025-06-06 20:46:01 +02:00
dswbx 58c7aba1a4 postgres: added pg and postgres, and examples for xata and neon 2025-06-06 16:52:07 +02:00
dswbx f8aa242d2b fix tests 2025-06-06 11:30:27 +02:00
dswbx 3da9570abe reduce worker config type 2025-06-06 11:18:44 +02:00
dswbx 6ee898e606 adding d1 session support 2025-06-06 11:13:35 +02:00
dswbx abbd372ddf Merge pull request #180 from bknd-io/docs/bknd-config-ts
docs: added docs about how to use bknd.config.ts
2025-06-05 17:14:30 +02:00
dswbx 3e77982996 docs: added docs about how to use bknd.config.ts 2025-06-05 17:11:50 +02:00
dswbx 3fbea8ace7 fix: cookie setting by only setting on admin routes 2025-06-03 13:11:18 +02:00
dswbx a41f943b43 fix: cookie setting by only setting on admin routes 2025-06-03 13:06:49 +02:00
dswbx 15a9c549e7 fix: cookie setting by only setting on admin routes 2025-06-03 13:04:03 +02:00
dswbx 50cadbaa8e fix: lock hono version because of changed header setting in 4.7.7 2025-06-03 11:12:47 +02:00
dswbx 7b128c9701 Merge pull request #176 from bknd-io/chores/repo-cleanup
repo chores: fixed root dir, removed unused class, added .env.example
2025-05-27 20:27:38 +02:00
dswbx 061181d59d repo chores: fixed root dir, removed unused class, added .env.example 2025-05-27 20:25:37 +02:00
dswbx af6cb0c8f0 bump 0.13.0 + separated cli build into separate file 2025-05-27 16:53:49 +02:00
dswbx 5a693c0370 Merge pull request #169 from bknd-io/release/0.13
Release 0.13
2025-05-27 16:29:43 +02:00
dswbx 262588decc update github action to use bun 1.2.14 + added .nvmrc (24 breaks for node tests) 2025-05-27 13:30:02 +02:00
dswbx 17ab35e245 api: added custom storage option (#174) 2025-05-27 13:09:24 +02:00
dswbx db795ec050 Controllers: New validation + auto OpenAPI (#173)
* updated controllers to use custom json schema and added auto openapi specs

* fix data routes parsing body

* added schema exports to core

* added swagger link to Admin, switched use-search
2025-05-27 09:06:36 +02:00
dswbx 773df544dd feat/custom-json-schema (#172)
* init

* update

* finished new repo query, removed old implementation

* remove debug folder
2025-05-22 08:52:25 +02:00
dswbx 0ac7d1fd6e remove batching workaround for Turso AWS endpoints (#171)
the underlying issue with batching on Turso AWS endpoints appears resolved, making the workaround unnecessary.
2025-05-21 07:47:24 +02:00
dswbx 6694c63990 admin: data/auth route-driven settings and collapsible components (#168)
introduced `useRoutePathState` for managing active states via routes, added `CollapsibleList` for reusable collapsible UI, and updated various components to leverage route awareness for improved navigation state handling. Also adjusted routing for entities, strategies, and schema to support optional sub-paths.
2025-05-03 11:05:38 +02:00
dswbx b3f95f9552 v0.12.0 2025-05-01 10:13:40 +02:00
dswbx 372f94d22a Release 0.12 (#143)
* changed tb imports

* cleanup: replace console.log/warn with $console, remove commented-out code

Removed various commented-out code and replaced direct `console.log` and `console.warn` usage across the codebase with `$console` from "core" for standardized logging. Also adjusted linting rules in biome.json to enable warnings for `console.log` usage.

* ts: enable incremental

* fix imports in test files

reorganize imports to use "@sinclair/typebox" directly, replacing local utility references, and add missing "override" keywords in test classes.

* added media permissions (#142)

* added permissions support for media module

introduced `MediaPermissions` for fine-grained access control in the media module, updated routes to enforce these permissions, and adjusted permission registration logic.

* fix: handle token absence in getUploadHeaders and add tests for transport modes

ensure getUploadHeaders does not set Authorization header when token is missing. Add unit tests to validate behavior for different token_transport options.

* remove console.log on DropzoneContainer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add bcrypt and refactored auth resolve (#147)

* reworked auth architecture with improved password handling and claims

Refactored password strategy to prepare supporting bcrypt, improving hashing/encryption flexibility. Updated authentication flow with enhanced user resolution mechanisms, safe JWT generation, and consistent profile handling. Adjusted dependencies to include bcryptjs and updated lock files accordingly.

* fix strategy forms handling, add register route and hidden fields

Refactored strategy forms to include hidden fields for type and name. Added a registration route with necessary adjustments to the admin controller and routes. Corrected field handling within relevant forms and components.

* refactored auth handling to support bcrypt, extracted user pool

* update email regex to allow '+' and '_' characters

* update test stub password for AppAuth spec

* update data exceptions to use HttpStatus constants, adjust logging level in AppUserPool

* rework strategies to extend a base class instead of interface

* added simple bcrypt test

* add validation logs and improve data validation handling (#157)

Added warning logs for invalid data during mutator validation, refined field validation logic to handle undefined values, and adjusted event validation comments for clarity. Minor improvements include exporting events from core and handling optional chaining in entity field validation.

* modify MediaApi to support custom fetch implementation, defaults to native fetch (#158)

* modify MediaApi to support custom fetch implementation, defaults to native fetch

added an optional `fetcher` parameter to allow usage of a custom fetch function in both `upload` and `fetcher` methods. Defaults to the standard `fetch` if none is provided.

* fix tests and improve api fetcher types

* update admin basepath handling and window context integration (#155)

Refactored `useBkndWindowContext` to include `admin_basepath` and updated its usage in routing. Improved type consistency with `AdminBkndWindowContext` and ensured default values are applied for window context.

* trigger `repository-find-[one|many]-[before|after]` based on `limit` (#160)

* refactor error handling in authenticator and password strategy (#161)

made `respondWithError` method public, updated login and register routes in `PasswordStrategy` to handle errors using `respondWithError` for consistency.

* add disableSubmitOnError prop to NativeForm and export getFlashMessage (#162)

Introduced a `disableSubmitOnError` prop to NativeForm to control submit button behavior when errors are present. Also exported `getFlashMessage` from the core for external usage.

* update dependencies in package.json (#156)

moved several dependencies between devDependencies and dependencies for better categorization and removed redundant entries.

* update imports to adjust nodeTestRunner path and remove unused export (#163)

updated imports in test files to reflect the correct path for nodeTestRunner. removed redundant export of nodeTestRunner from index file to clean up module structure. In some environments this could cause issues requiring to exclude `node:test`, just removing it for now.

* fix sync events not awaited (#164)

* refactor(dropzone): extract DropzoneInner and unify state management with zustand (#165)

Simplified Dropzone implementation by extracting inner logic to a new component, `DropzoneInner`. Replaced local dropzone state logic with centralized state management using zustand. Adjusted API exports and props accordingly for consistency and maintainability.

* replace LiquidJs rendering with simplified renderer (#167)

* replace LiquidJs rendering with simplified renderer

Removed dependency on LiquidJS and replaced it with a custom templating solution using lodash `get`. Updated corresponding components, editors, and tests to align with the new rendering approach. Removed unused filters and tags.

* remove liquid js from package json

* feat/cli-generate-types (#166)

* init types generation

* update type generation for entities and fields

Refactored `EntityTypescript` to support improved field types and relations. Added `toType` method overrides for various fields to define accurate TypeScript types. Enhanced CLI `types` command with new options for output style and file handling. Removed redundant test files.

* update type generation code and CLI option description

removed unused imports definition, adjusted formatting in EntityTypescript, and clarified the CLI style option description.

* fix json schema field type generation

* reworked system entities to prevent recursive types

* reworked system entities to prevent recursive types

* remove unused object function

* types: use number instead of Generated

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

* update data hooks and api types

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-01 10:12:18 +02:00
dswbx d6f94a2ce1 fix docs mdx snippets imports 2025-04-26 07:34:56 +02:00
dswbx 89a39a7dc6 fix docs: try locking version 2025-04-25 08:52:52 +02:00
dswbx 88cf65f792 fix docs by tmp renaming intro page 2025-04-24 21:20:12 +02:00
dswbx 07723ce6ae bump to 0.11.2 2025-04-22 15:55:20 +02:00
dswbx 9010401af6 remove unused useTheme import and add loading state for entity detail view (#154) 2025-04-22 15:43:27 +02:00
dswbx 4c11789ea8 Fix Release 0.11.1 (#150)
* fix strategy forms handling, add register route and hidden fields

Refactored strategy forms to include hidden fields for type and name. Added a registration route with necessary adjustments to the admin controller and routes. Corrected field handling within relevant forms and components.

* fix admin access permissions and refactor routing structure

display a fixed error for unmet permissions when retrieving the schema. moved auth routes outside of BkndProvider and reorganized remaining routes to include BkndWrapper.

* fix: properly type BkndWrapper

* bump fix release version

* ModuleManager: update diff checking and AppData validation

Revised diff handling includes validation of diffs, reverting changes on failure, and enforcing module constraints with onBeforeUpdate hooks. Introduced `validateDiffs` and backup of stable configs. Applied changes in related modules, tests, and UI layer to align with updated diff logic.

* fix: cli: running from config file were using invalid args

* fix: cli: improve sequence of onBuilt trigger to allow custom routes from cli

* fix e2e tests
2025-04-20 09:29:58 +02:00
dswbx 2988e4c3bd fix: force json-schema-library version 2025-04-18 16:06:32 +02:00
dswbx ad7926db4c Merge pull request #121 from bknd-io/release/0.11
Release 0.11
2025-04-08 12:51:34 +02:00
dswbx 53a3dcdee7 release: v0.11 2025-04-08 12:50:37 +02:00
dswbx 53467d6750 fix: create: cloudflare starter wasn't creating a r2 bucket 2025-04-08 12:43:50 +02:00
dswbx a80a731498 fix: cli: user command now uses the same app env setup as run 2025-04-08 11:22:10 +02:00
dswbx 7e1757b7f4 fix: updated cloudflare adapter to use runtime config, aligned vite 2025-04-05 18:03:58 +02:00
dswbx 2c29e06fb8 fix: media infinite should be disabled for dropzone in entities 2025-04-05 18:01:04 +02:00
dswbx ca86fa58ac cli: user: add token generation (#140)
* cli: user: add token generation

* cli: user: add token generation

* cli: user: check for value being cancel before continuing
2025-04-05 17:57:03 +02:00
dswbx de984fa101 fixes issues in firefox where view transitions are not available (#139) 2025-04-04 09:00:50 +02:00
dswbx a12d4e13d0 e2e: added script to auto test adapters 2025-04-03 16:40:51 +02:00
dswbx fa6c7acaf5 implement/init e2e tests (#135)
* init e2e

* updated/moved vitest, finished merge

* fix bun picking up e2e tests

* e2e: overwrite webserver config with env

* e2e: added adapter configs

* e2e: replaced image
2025-04-03 11:08:16 +02:00
dswbx 0b41aa5a2d cli: create: allow non-interactive create (#137) 2025-04-03 09:17:29 +02:00
dswbx a5ec40c517 docker: add option to overwrite bknd version used (#136) 2025-04-03 07:58:00 +02:00
dswbx 75e2b96344 fixed limbo batching issue by disabling batching (#133)
* fixed limbo batching issue by disabling batching

* updated @libsql/client to `0.15.2`
2025-04-02 20:19:20 +02:00
dswbx aaae8d9681 aws cli create: added guided db creation (#134) 2025-04-02 20:18:52 +02:00
dswbx e4608b7df7 cosmetics: fixed admin skeleton, use password field on auth, use $console in auth middleware 2025-04-01 13:49:58 +02:00
dswbx 44b3f72005 added media overlay preview fallback 2025-04-01 13:37:11 +02:00
dswbx 9134d121cd keep extension from file when generating random name (#127)
* keep extension from file when generating random name

* added test for random name generation
2025-04-01 13:24:32 +02:00
Cameron Pak 2f067451b4 chore: update dependencies and enhance TypeScript configuration for Astro example (#128) 2025-04-01 12:58:13 +02:00
dswbx 3f26c45dd9 refactored adapters to run test suites (#126)
* refactored adapters to run test suites

* fix bun version for tests

* added missing adapter tests and refactored examples to use `bknd.config.ts` where applicable
2025-04-01 11:43:11 +02:00
dswbx 36e4224b33 refactored EventManager to run asyncs on call only, app defaults to run before response (#129)
* refactored EventManager to run asyncs on call only, app defaults to run before response

* fix tests
2025-04-01 11:19:55 +02:00
dswbx 434d56672c Merge pull request #125 from bknd-io/feat/cli-load-creds
improve cli creds extraction
2025-03-29 08:11:43 +01:00
dswbx b2fd907e8c updated docs, fixed run with node/tsx 2025-03-28 21:12:50 +01:00
dswbx b29c04e8c9 added more cli instructions 2025-03-28 20:52:00 +01:00
dswbx 11a28eba88 improve cli creds extraction 2025-03-28 18:03:09 +01:00
dswbx 9e3c081e50 reorganized storage adapter and added test suites for adapter and fields (#124)
* reorganized storage adapter and added test suites for adapter and fields

* added build command in ci pipeline

* updated workflow to also run node tests

* updated workflow: try with separate tasks

* updated workflow: try with separate tasks

* updated workflow: added tsx as dev dependency

* updated workflow: try with find instead of glob
2025-03-27 20:41:42 +01:00
dswbx 40c9ef9d90 Merge pull request #123 from bknd-io/feat/media-dialog-and-infinite
add media detail dialog and infinite loading
2025-03-27 10:24:48 +01:00
dswbx 7facef47da fix media styling on mobile 2025-03-27 09:57:31 +01:00
dswbx e2bf6a0724 Merge remote-tracking branch 'origin/feat/media-detect-dimensions' into feat/media-dialog-and-infinite 2025-03-27 09:26:09 +01:00
dswbx f6a511d998 add media detail dialog and infinite loading 2025-03-27 09:23:14 +01:00
dswbx 9407f3d212 add image dimension detection for most common formats 2025-03-27 09:21:58 +01:00
dswbx 0424c08a9e Merge pull request #120 from bknd-io/feat/cf-create-infra
cli create: improve cloudflare (create d1, r2)
2025-03-26 10:32:48 +01:00
dswbx 9d122da4b9 cli create: improve cloudflare (create d1, r2) 2025-03-26 10:30:47 +01:00
dswbx 0f56d554b0 Merge pull request #119 from bknd-io/fix/cf-d1-introspect-issue-2
fix d1 introspect issue by excluding `_cf_METADATA`
2025-03-26 09:59:38 +01:00
dswbx 06e92d268b fix d1 introspect issue by excluding _cf_METADATA 2025-03-26 09:58:49 +01:00
dswbx b44b87923b Merge pull request #118 from bknd-io/fix/cf-d1-introspect-issue
fix d1 introspect issue by excluding `_cf_METADATA`
2025-03-26 09:21:59 +01:00
dswbx 6e6c65b375 fix d1 introspect issue by excluding _cf_METADATA 2025-03-26 09:19:54 +01:00
dswbx c9c00adf6c docs: added database overview 2025-03-25 14:21:00 +01:00
dswbx ee15150c78 Merge pull request #109 from bknd-io/release/0.10
Release 0.10
2025-03-25 13:04:25 +01:00
dswbx f8f5ef9c98 bump version to 0.10 2025-03-25 13:03:15 +01:00
dswbx ec015b7849 docs: add postgres and sqlocal instructions 2025-03-25 13:02:09 +01:00
dswbx 67e0374c04 add change set to mutator insert/update after event 2025-03-21 19:32:24 +01:00
dswbx 9380091ba9 Merge pull request #116 from bknd-io/fix/s3-upload-in-node
fix s3 media upload in node environments by adding content length to request
2025-03-21 18:05:04 +01:00
dswbx aa66a81b27 Merge pull request #114 from bknd-io/feat/admin-colors-and-resize
admin ui: started color centralization + made sidebar resizable
2025-03-21 18:04:26 +01:00
dswbx 0c15ec1434 hide oauth client details with type password 2025-03-21 18:03:45 +01:00
dswbx 7b8c7f1ae4 fix s3 media upload in node environments by adding content length to request 2025-03-21 18:01:44 +01:00
dswbx f6996b1953 admin ui: started color centralization + made sidebar resizable 2025-03-18 10:56:39 +01:00
dswbx ea2aa7c76c fix react example styling 2025-03-15 16:44:58 +01:00
dswbx 2531c2d8d4 updated react example with todo example 2025-03-15 16:37:03 +01:00
dswbx 0e81e14421 bump rc version 2025-03-15 15:08:04 +01:00
dswbx b994c2dfb0 bump rc version 2025-03-15 15:07:29 +01:00
dswbx 4085d569a8 Merge pull request #113 from bknd-io/feat/data-sqlocal-connection
add sqlocal connection including example
2025-03-15 14:43:03 +01:00
dswbx 622a7b2b9a add sqlocal connection including example 2025-03-15 14:40:41 +01:00
dswbx 5697b7891a update cli description for run 2025-03-14 15:37:51 +01:00
dswbx 6a6a7a9f26 Merge pull request #112 from bknd-io/feat/react-router-adapter
replaced remix with react-router
2025-03-14 15:36:27 +01:00
dswbx b763826754 replaced remix with react-router 2025-03-14 15:32:43 +01:00
dswbx b3a0ecbe6d Merge pull request #111 from bknd-io/feat/cli-telemetry
added basic cli telemetry
2025-03-14 11:46:17 +01:00
dswbx 181443b320 fix is_toggled test 2025-03-14 11:45:02 +01:00
dswbx 6015acb914 added basic cli telemetry 2025-03-14 11:40:56 +01:00
dswbx a55d93b5ff data schema: disable primary field 2025-03-14 10:03:40 +01:00
dswbx 1ed94716bd Merge pull request #105 from bknd-io/feat/add-postgres-and-prepare-others
support for postgres and others
2025-03-14 09:57:44 +01:00
dswbx 178838e06c remove postgres test from main package 2025-03-14 09:55:00 +01:00
dswbx c4aaa5a90b move postgres to a separate package 2025-03-14 09:42:51 +01:00
dswbx b1b2e97a5a Merge remote-tracking branch 'origin/release/0.10' into feat/add-postgres-and-prepare-others
# Conflicts:
#	app/package.json
#	bun.lock
2025-03-14 08:06:50 +01:00
dswbx e9f2da82d7 Merge pull request #110 from bknd-io/fix/nextjs-linting-and-adapter
fix/nextjs-linting-and-adapter
2025-03-14 07:59:38 +01:00
dswbx 9cba9ada84 Merge remote-tracking branch 'origin/release/0.10' into fix/nextjs-linting-and-adapter 2025-03-14 07:56:37 +01:00
dswbx c8dcf964bc Merge pull request #103 from bknd-io/feat/remove-admin-config
remove admin config, improve theme handling
2025-03-14 07:55:43 +01:00
dswbx 1ea22248b4 fix nextjs linting hints and improve adapter to support config as function 2025-03-14 07:55:05 +01:00
dswbx 4f01004262 update override in examples 2025-03-11 19:53:31 +01:00
dswbx 051c508254 add override to skeleton 2025-03-11 19:52:44 +01:00
dswbx a72b11e9fb fix tests 2025-03-11 17:45:27 +01:00
dswbx 96781f5d3d removed admin config from server, theme is now client side, fixed module manager migrations 2025-03-11 17:41:33 +01:00
dswbx de89e26ffb Merge remote-tracking branch 'origin/release/0.10' into feat/remove-admin-config
# Conflicts:
#	app/src/modules/server/AdminController.tsx
#	app/src/ui/Admin.tsx
2025-03-11 13:56:27 +01:00
dswbx 166a7d4d0a Merge pull request #108 from bknd-io/feat/update-react-deps
upgrade dependencies
2025-03-11 13:52:21 +01:00
dswbx 640f015812 upgrade tailwind to v4 2025-03-11 13:43:40 +01:00
dswbx d90f14c707 upgrade hook form resolvers 2025-03-11 10:17:07 +01:00
dswbx 0922d90aff upgrade hook form 2025-03-11 10:11:57 +01:00
dswbx 81ca7c0e28 upgrade typebox 2025-03-11 10:08:56 +01:00
dswbx 4f87f6857f upgrade liquid, fix editor themes 2025-03-11 09:57:05 +01:00
dswbx 0dd29c3b07 fix test typings 2025-03-11 09:38:14 +01:00
dswbx eb451a8e1a upgrade kysely and hono 2025-03-11 09:29:08 +01:00
dswbx 37df32b51b upgrade fast-xml-parser (prepare to replace) 2025-03-11 09:27:40 +01:00
dswbx 722d0bd29e upgrade cf json schema, liquid, xyflow, jotai 2025-03-11 09:02:38 +01:00
dswbx 03d3ab8b80 upgrade swr and wouter 2025-03-11 08:59:33 +01:00
dswbx dc2d39fa14 upgrade dev dependencies 2025-03-11 08:57:01 +01:00
dswbx 368c60d7c7 upgrade vite to 6 2025-03-11 08:51:37 +01:00
dswbx 8df30f4dae upgrade to react 19 2025-03-11 08:44:49 +01:00
dswbx 2d7257d2bc updated react-form to latest 2025-03-11 06:49:38 +01:00
dswbx 7e2de1bd0c add a fixed release to docker build 2025-03-10 13:05:09 +01:00
dswbx b786fbcd13 Merge pull request #107 from bknd-io/hotfix/0.9.1-docker-open
hotfix 0.9.1 docker fails due to trying to open a browser
2025-03-10 12:37:02 +01:00
dswbx eabae5d3df hotfix 0.9.1 docker fails due to trying to open a browser 2025-03-10 12:28:38 +01:00
dswbx a4cb012ce8 added batching for postgres 2025-03-07 19:40:58 +01:00
dswbx 3704cad53a added BaseIntrospector and reorganized connections 2025-03-07 17:20:37 +01:00
dswbx 25a3cb9655 ignore pg test for now (missing env on ci) 2025-03-07 15:04:44 +01:00
dswbx a5c422d45d initialized postgres support 2025-03-07 15:02:19 +01:00
dswbx 8550aef606 update nextjs example to add credentials 2025-03-07 08:56:35 +01:00
dswbx 01cb9d21b0 update nextjs example's readme 2025-03-05 09:37:23 +01:00
dswbx c5ce79102e bump 0.9 and update readme with entity prefix 2025-03-05 09:15:16 +01:00
dswbx 67ab3d2e38 Merge pull request #93 from bknd-io/release/0.9
Release 0.9
2025-03-05 09:13:33 +01:00
dswbx b1b88e37f1 updated readme 2025-03-05 09:10:49 +01:00
dswbx 7e61ff9995 fix refine fn typing 2025-03-05 08:35:57 +01:00
dswbx 58f9ea4cd7 Merge pull request #104 from bknd-io/feat/additional-data-api-methods
feat/additional-data-api-methods
2025-03-05 08:24:19 +01:00
dswbx dde5ad365e reduce api bundle size by omitting lodash 2025-03-05 08:23:40 +01:00
dswbx dda02807c1 fix typing to allow PrimaryFieldType as Primitive in where 2025-03-05 08:14:15 +01:00
dswbx ef629321ab added readOneBy, updateMany, deleteMany, exists 2025-03-05 08:02:57 +01:00
dswbx 4f52537ea0 updated nextjs example 2025-03-04 14:39:32 +01:00
dswbx e82b72275b moved flash message, removed theme from admin controller 2025-03-04 11:21:33 +01:00
dswbx ab73b02138 refactor console verbosity and internal env handling 2025-03-04 11:18:14 +01:00
dswbx 36fba4588f Merge pull request #102 from bknd-io/feat/aws-lambda-adapter
added aws lambda adapter + improvements to handle concurrency
2025-03-03 16:59:16 +01:00
dswbx ad353e59c9 added aws adapter docs 2025-03-03 16:56:48 +01:00
dswbx 675a39ad5c added aws lambda adapter + improvements to handle concurrency 2025-03-03 15:42:42 +01:00
dswbx 1a72b9e84b Merge pull request #89 from bknd-io/feat/app-api-exp-for-nextjs
optimized local api instantiation to prepare for nextjs app router
2025-03-03 07:16:01 +01:00
dswbx 27a282ea6b Merge pull request #101 from bknd-io/fix/add-cache-directives-for-media
added missing cache directives to media adapter serving
2025-03-03 07:15:03 +01:00
dswbx 6740e20f72 Merge pull request #100 from bknd-io/fix/minor-admin-ui-fixes
fix active sidebar active item, added error boundaries for relational form fields, fixed schema diagram for m:n relations
2025-03-03 07:14:43 +01:00
dswbx 6c1eea76e5 added missing cache directives to media adapter serving 2025-03-03 07:14:01 +01:00
dswbx 5ca21b6c01 fix active sidebar active item, added error boundaries for relational form fields, fixed schema diagram for m:n relations 2025-03-03 07:13:04 +01:00
dswbx 00f52eb096 minor fixes for nextjs 2025-03-03 07:10:32 +01:00
dswbx 09074f6591 updated nextjs example with app router 2025-02-27 20:09:03 +01:00
dswbx 2d5da63eb2 Merge remote-tracking branch 'origin/release/0.9' into feat/app-api-exp-for-nextjs
# Conflicts:
#	app/src/adapter/nextjs/nextjs.adapter.ts
#	app/src/index.ts
2025-02-27 14:21:00 +01:00
dswbx 3bf3bb32a4 fix field date error in modal, adjust notification spacing, added entity quick switcher 2025-02-27 14:07:49 +01:00
dswbx 3e28124494 fix many to many reference query 2025-02-27 11:43:12 +01:00
dswbx dd48962901 added auth strategies migration, fixed rebuild of modules on migrations 2025-02-27 10:08:22 +01:00
dswbx 7743f71a11 added format command and added trailing commas to reduce conflicts 2025-02-26 20:06:03 +01:00
dswbx 88b5359f1c Merge pull request #99 from bknd-io/feat/data-api-bulk-update
feat/data-api-bulk-update
2025-02-26 19:55:22 +01:00
dswbx 6acbe1189e use new bun text lock file 2025-02-26 19:54:40 +01:00
dswbx 7c1fb86b20 Merge pull request #98 from bknd-io/feat/auth-ui-improvements
feat/auth-ui-improvements
2025-02-26 19:52:39 +01:00
dswbx 36e033b0de Merge remote-tracking branch 'origin/release/0.9' into feat/auth-ui-improvements
# Conflicts:
#	app/package.json
2025-02-26 19:51:41 +01:00
dswbx 4ca68e837c added auth getting started box 2025-02-26 19:50:35 +01:00
dswbx 9a683c8e35 add auth enabling hints 2025-02-26 18:44:16 +01:00
dswbx 4a2dbf8f79 added bulk updates to data controller 2025-02-26 18:04:07 +01:00
dswbx 4d75cfe0ea updated data api docs 2025-02-26 18:03:50 +01:00
dswbx b6d28bbf42 feat/ci-tests (#97) 2025-02-26 17:15:51 +01:00
dswbx 2a9c1be151 added auth strategies form + add ability to disable strategies 2025-02-26 14:53:32 +01:00
dswbx d4a6a9326f fix schema form 2025-02-26 08:22:05 +01:00
dswbx de854eec3a added new settings UI for auth 2025-02-25 13:59:44 +01:00
dswbx 253174c14e Merge pull request #96 from bknd-io/feat/report-query-without-index
added index on media entity_id, report query with unindexed fields
2025-02-25 08:17:54 +01:00
dswbx 9a741f49a0 added index on media entity_id, report query with unindexed fields 2025-02-22 22:09:50 +01:00
dswbx b8f87a2097 Merge pull request #92 from bknd-io/fix/many-to-many-respect-with-select
fix ManyToManyRelation to respect `select` from `with` query
2025-02-22 13:23:58 +01:00
dswbx 4625151db8 fix relations test with new select 2025-02-22 13:23:37 +01:00
dswbx 280d2dccfb Merge pull request #95 from bknd-io/feat/media-more-mime-and-add-dropzone-checker
media more mime types and add dropzone mime checker
2025-02-22 13:20:35 +01:00
dswbx 3015385290 Merge pull request #94 from bknd-io/feat/improve-console-with-timestamps
improve console with timestamps
2025-02-22 13:19:46 +01:00
dswbx 147d6b7ff7 added timestamps to app console logs 2025-02-22 13:18:23 +01:00
dswbx 82fba39684 added timestamps to app console logs 2025-02-22 13:17:43 +01:00
dswbx f801d3a556 media: added more mime types, added mime type check on dropzone 2025-02-22 13:14:32 +01:00
dswbx 837b0a3d43 fix ManyToManyRelation to respect select from with query 2025-02-21 11:12:58 +01:00
dswbx 9ecfcb3e05 Merge pull request #91 from bknd-io/release/0.8.1
Fix Release 0.8.1
2025-02-21 08:18:32 +01:00
dswbx fb3dbd343b bump fix release version to 0.8.1 2025-02-21 08:18:13 +01:00
dswbx 060df210f9 fix Api import in App 2025-02-21 08:14:22 +01:00
dswbx fb725be0ed dev: only report env once 2025-02-21 07:44:07 +01:00
dswbx 1a95b4c8bb Merge remote-tracking branch 'origin/release/0.8.1' into release/0.8.1
# Conflicts:
#	app/src/ui/elements/media/Dropzone.tsx
2025-02-21 07:41:45 +01:00
dswbx cfa950aa46 fix media upload without name, add data result to uploaded payload 2025-02-21 07:40:43 +01:00
dswbx 5771298bfa Merge pull request #90 from bknd-io/fix/media-mime-guessing-on-entity
fix media mime guessing when uploading to entity
2025-02-20 11:34:36 +01:00
dswbx 94c40de011 fix media mime guessing when uploading to entity 2025-02-20 11:32:24 +01:00
dswbx 6f776aeff7 optimized local api instantiation to prepare for nextjs app router 2025-02-20 10:02:44 +01:00
dswbx eaa7276173 update docs on new cli starters 2025-02-19 21:27:04 +01:00
dswbx 89871a8ef7 Merge pull request #75 from bknd-io/release/0.8
Release 0.8
2025-02-19 19:06:16 +01:00
dswbx 7f0f6bec08 bump version 0.8.0 2025-02-19 19:05:29 +01:00
dswbx 8e1ad98964 Merge pull request #88 from bknd-io/refactor/improve-remix-example
refactor/improve-remix-example
2025-02-19 17:44:10 +01:00
dswbx a29e824ecc minor fixes in build and console 2025-02-19 17:41:48 +01:00
dswbx b436758fe6 updated remix integration docs 2025-02-19 14:28:22 +01:00
dswbx ee7dcd44f4 updated remix adapter for non-middleware env, fixed console mute 2025-02-19 14:13:29 +01:00
dswbx f2e5815e24 adjusted remix example 2025-02-19 14:12:40 +01:00
dswbx 335fe42a22 adjusted nextjs example theme 2025-02-18 20:14:08 +01:00
dswbx a01b99be27 updated App options "seed", changed examples to file db 2025-02-18 20:01:08 +01:00
dswbx a4e46a2768 Merge pull request #87 from bknd-io/fix/many-to-many
fix persisting of many to many entity
2025-02-18 19:03:56 +01:00
dswbx f494735a79 fix persisting of many to many entity 2025-02-18 15:43:19 +01:00
dswbx 400db84dd5 change default mode of cloudflare example to "warm", suppress listener info 2025-02-18 13:26:26 +01:00
dswbx 964bacd824 updated examples package.json's and README.md's 2025-02-18 13:19:35 +01:00
dswbx 7994eabe7d export RepoQueryIn from client 2025-02-18 12:54:13 +01:00
dswbx dec382b49d cli create: add default database name for cloudflare 2025-02-18 10:49:37 +01:00
dswbx 93cee4f8aa Merge pull request #83 from bknd-io/refactor/data-api-entity-prefix
prefixed data entity endpoints with /entity
2025-02-18 10:22:31 +01:00
dswbx e99abb09b6 Merge branch 'release/0.8' into refactor/data-api-entity-prefix 2025-02-18 10:22:15 +01:00
dswbx a5ab102b34 Merge pull request #86 from bknd-io/feat/cli-prefer-file
cli run will prefer data file now, improved console coloring
2025-02-18 10:21:34 +01:00
dswbx 1ca6c5b43d Merge pull request #85 from bknd-io/refactor/app-options-manager
refactor app constructor params to move manager into app options
2025-02-18 10:21:27 +01:00
dswbx 64658184ac Merge pull request #84 from bknd-io/feat/debug-routes
added deboug routes cli action
2025-02-18 10:21:17 +01:00
dswbx 61ad4a3109 Merge pull request #82 from bknd-io/feat/ui-admin-theme
add admin "system" theme option that checks user prefs
2025-02-18 10:21:02 +01:00
dswbx e12ed1e4b3 cli run will prefer data file now, improved console coloring 2025-02-18 10:11:59 +01:00
dswbx e103d3ac33 refactor app constructor params to move manager into app options 2025-02-18 09:36:59 +01:00
dswbx e2217c411a added deboug routes cli action 2025-02-18 09:25:34 +01:00
dswbx 3e6d381239 prefixed data entity endpoints with /entity 2025-02-18 09:12:12 +01:00
dswbx 0823b182f6 admin: added "system" theme option, check user pref 2025-02-18 08:20:41 +01:00
dswbx bd362607ae cli create: fix cloudflare overriding name in wrangler 2025-02-17 20:46:20 +01:00
dswbx 61ac31eba9 Merge pull request #81 from bknd-io/feat/app-auth-context
feat/app-auth-context
2025-02-14 20:33:12 +01:00
dswbx 21a8f56c48 moved auth ctx to request context, cleaned up system controller 2025-02-14 20:32:09 +01:00
dswbx fb76f8d789 improve media mime type inferring + added uploadToEntity in media api 2025-02-14 18:02:43 +01:00
dswbx 4bd201d454 cli create: updated templates, reworked examples to be standalone 2025-02-14 14:56:35 +01:00
dswbx b412da836c cli create: fix installing deps spinner 2025-02-14 14:08:15 +01:00
dswbx 425465becd Merge pull request #79 from bknd-io/feat/cli-starters
added cli-starters
2025-02-14 13:57:48 +01:00
dswbx 3b487ade2a finalize initial starters 2025-02-14 13:55:46 +01:00
dswbx 7d3c76d7c1 Merge remote-tracking branch 'origin/release/0.8' into feat/cli-starters 2025-02-14 11:54:15 +01:00
dswbx fd297c6c50 bump rc version 2025-02-14 11:48:53 +01:00
dswbx 149a6d768c Merge pull request #80 from bknd-io/fix/api-and-media
fix/api-and-media
2025-02-14 11:42:50 +01:00
dswbx 24e69eec90 fix s3 adapter 2025-02-14 11:42:26 +01:00
dswbx 9c38c8f2ad improved Dropzone error handling 2025-02-14 11:21:11 +01:00
dswbx f0bdb3a75e improved typing, renamed functions for clarity 2025-02-14 11:04:50 +01:00
dswbx c4e505582b updated media api and added tests, fixed body limit 2025-02-14 10:42:36 +01:00
dswbx cc938db4b8 add default fetcher fallback for ModuleApi 2025-02-13 11:24:35 +01:00
dswbx 68277a68d5 cloudflare update assets dir 2025-02-13 09:12:08 +01:00
dswbx 8e72b5b615 init create cli, added node and partially cloudflare 2025-02-13 08:51:48 +01:00
dswbx 07422f30ba Merge pull request #78 from bknd-io/feat/improve-cli-with-env
cli: fallback to env if present
2025-02-12 09:54:29 +01:00
dswbx d84731d89e cli: try to fallback to env if present 2025-02-12 09:52:18 +01:00
dswbx 4afea14893 Merge pull request #77 from bknd-io/feat/conn-remove-type
update app create config to accept libsql config directly
2025-02-12 09:39:16 +01:00
dswbx 2c6a4d2bed update app create config to accept libsql config directly 2025-02-12 09:21:51 +01:00
dswbx f466dd166d Merge pull request #73 from bknd-io/feat/cf-d1-and-r2
adding d1 and r2 for cloudflare environments
2025-02-12 09:04:47 +01:00
dswbx 6d8f86b2c9 Merge pull request #76 from bknd-io/feat/mm-revert-on-error
run schema mutations in safe proxy and revert to previous on error
2025-02-12 09:04:24 +01:00
dswbx c8fa704e32 running schema mutations in safe proxy and revert to previous on error 2025-02-12 09:01:56 +01:00
dswbx 61d06973a4 ignore d1 system tables, deploy verified 2025-02-12 07:29:56 +01:00
dswbx 7a6321f4ce added r2 binding support to cf adapter 2025-02-11 10:04:53 +01:00
dswbx 4dde67ca21 update README package sizes 2025-02-10 17:56:08 +01:00
dswbx f9c612e0fa Merge pull request #74 from bknd-io/fix/ui-modal-and-media-reload
fixing ui modal styling and make sure schema reloads on media setting changes
2025-02-10 17:49:53 +01:00
dswbx d7e772efd9 fixing ui modal styling and make sure schema reloads on media setting changes 2025-02-10 17:47:56 +01:00
dswbx be39d1c374 adding d1 for cloudflare environments 2025-02-10 17:46:22 +01:00
dswbx 1e5c0dbc22 Merge pull request #72 from bknd-io/fix/0.7.1
fix: popover regression & dropzone rerender lost state
2025-02-10 14:53:14 +01:00
dswbx d79da9d204 fix: popover regression & dropzone rerender lost state 2025-02-10 14:52:14 +01:00
773 changed files with 43875 additions and 14052 deletions
+39
View File
@@ -0,0 +1,39 @@
name: Run Tests
on:
pull_request:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: "1.2.14"
- name: Install dependencies
working-directory: ./app
run: bun install
- name: Build
working-directory: ./app
run: bun run build:ci
- name: Run Bun tests
working-directory: ./app
run: bun run test:bun
- name: Run Node tests
working-directory: ./app
run: npm run test:node
+4 -1
View File
@@ -19,6 +19,7 @@ packages/media/.env
.history .history
**/*/.db/* **/*/.db/*
**/*/.configs/* **/*/.configs/*
**/*/.template/*
**/*/*.db **/*/*.db
**/*/*.db-shm **/*/*.db-shm
**/*/*.db-wal **/*/*.db-wal
@@ -28,4 +29,6 @@ packages/media/.env
.idea .idea
.vscode .vscode
.git_old .git_old
docker/tmp docker/tmp
.debug
.history
+1
View File
@@ -0,0 +1 @@
22
+17 -8
View File
@@ -1,30 +1,39 @@
[![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd) [![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd)
[![npm downloads](https://img.shields.io/npm/dm/bknd)](https://www.npmjs.com/package/bknd)
![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png) ![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png)
<p align="center" width="100%"> <p align="center" width="100%">
<a href="https://stackblitz.com/github/bknd-io/bknd-examples?hideExplorer=1&embed=1&view=preview&startScript=example-admin-rich&initialPath=%2Fdata%2Fschema"> <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 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!
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
* **Databases**:
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
**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 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.
## Size ## Size
![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/index.js?compression=gzip&label=bknd) ![gzipped size of bknd](https://img.shields.io/bundlejs/size/bknd?label=bknd)
![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/client/index.js?compression=gzip&label=bknd/client) ![gzipped size of bknd/client](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/client/index.js?compression=gzip&label=bknd/client)
![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/elements/index.js?compression=gzip&label=bknd/elements) ![gzipped size of bknd/elements](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@0.6.1/dist/ui/index.js?compression=gzip&label=bknd/ui) ![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
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.
Depending on what you use, the size can be higher as additional dependencies are getting pulled in. The minimal size of a full `bknd` app as an API is around 300 kB gzipped (e.g. deployed as Cloudflare Worker).
## Motivation ## Motivation
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them. Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
@@ -40,7 +49,7 @@ Creating digital products always requires developing both the backend (the logic
* 🏃‍♂️ 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)
* using a React framework (Astro, Remix, Next.js) * using a React framework (Next.js, React Router, Astro)
* 📦 Official API and React SDK with type-safety * 📦 Official API and React SDK with type-safety
* ⚛️ React elements for auto-configured authentication and media components * ⚛️ React elements for auto-configured authentication and media components
@@ -78,7 +87,7 @@ export default function AdminPage() {
### Using the REST API or TypeScript SDK (`bknd/client`) ### Using the REST API or TypeScript SDK (`bknd/client`)
If you're not using a JavaScript environment, you can still access any endpoint using the REST API: If you're not using a JavaScript environment, you can still access any endpoint using the REST API:
```bash ```bash
curl -XGET <your-endpoint>/api/data/<entity> curl -XGET <your-endpoint>/api/data/entity/<entity>
{ {
"data": [ "data": [
{ "id": 1, ... }, { "id": 1, ... },
+42
View File
@@ -0,0 +1,42 @@
# ===== DB Settings =====
VITE_DB_URL=:memory:
# you can set a location for a database here, it'll overwrite the previous setting
# ideally use the ".db" folder (create it first), it's git ignored
VITE_DB_URL=file:.db/dev.db
# alternatively, you can use url/token combination
#VITE_DB_URL=
#VITE_DB_TOKEN=
# ===== DEV Server =====
# restart the dev server on every change (enable with "1")
VITE_APP_FRESH=
# displays react-scan widget (enable with "1")
VITE_DEBUG_RERENDERS=
# console logs registered routes on start (enable with "1")
VITE_SHOW_ROUTES=
# ===== Test Credentials =====
RESEND_API_KEY=
R2_TOKEN=
R2_ACCESS_KEY=
R2_SECRET_ACCESS_KEY=
R2_URL=
AWS_ACCESS_KEY=
AWS_SECRET_KEY=
AWS_S3_URL=
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=
# ===== Internals =====
BKND_CLI_CREATE_REF=main
BKND_CLI_LOG_LEVEL=debug
BKND_MODULES_DEBUG=1
+4
View File
@@ -0,0 +1,4 @@
playwright-report
test-results
bknd.config.*
__test__/helper.d.ts
+134 -4
View File
@@ -1,15 +1,145 @@
import { afterAll, describe, expect, test } from "bun:test"; import { afterEach, describe, test, expect } from "bun:test";
import { App } from "../src"; import { App, createApp } from "core/test/utils";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
import { Hono } from "hono";
import * as proto from "../src/data/prototype";
import { pick } from "lodash-es";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup); afterEach(afterAllCleanup);
describe("App tests", async () => { describe("App tests", async () => {
test("boots and pongs", async () => { test("boots and pongs", async () => {
const app = new App(dummyConnection); const app = new App(dummyConnection);
await app.build(); await app.build();
//expect(await app.data?.em.ping()).toBeTrue(); expect(await app.em.ping()).toBeTrue();
});
test("plugins", async () => {
const called: string[] = [];
const app = createApp({
initialConfig: {
auth: {
enabled: true,
},
},
options: {
plugins: [
(app) => {
expect(app).toBeDefined();
expect(app).toBeInstanceOf(App);
return {
name: "test",
schema: () => {
called.push("schema");
return proto.em(
{
posts: proto.entity("posts", {
title: proto.text(),
}),
comments: proto.entity("comments", {
content: proto.text(),
}),
users: proto.entity("users", {
email_verified: proto.boolean(),
}),
},
(fn, s) => {
fn.relation(s.comments).manyToOne(s.posts);
fn.index(s.posts).on(["title"]);
},
);
},
onBoot: async () => {
called.push("onBoot");
},
beforeBuild: async () => {
called.push("beforeBuild");
},
onBuilt: async () => {
called.push("onBuilt");
},
onServerInit: async (server) => {
called.push("onServerInit");
expect(server).toBeDefined();
expect(server).toBeInstanceOf(Hono);
},
onFirstBoot: async () => {
called.push("onFirstBoot");
},
};
},
],
},
});
await app.build();
expect(app.em.entities.map((e) => e.name)).toEqual(["users", "posts", "comments"]);
expect(app.em.indices.map((i) => i.name)).toEqual([
"idx_unique_users_email",
"idx_users_strategy",
"idx_users_strategy_value",
"idx_posts_title",
]);
expect(
app.em.relations.all.map((r) => pick(r.toJSON(), ["type", "source", "target"])),
).toEqual([
{
type: "n:1",
source: "comments",
target: "posts",
},
]);
expect(called).toEqual([
"onBoot",
"onServerInit",
"beforeBuild",
"onServerInit",
"schema",
"onFirstBoot",
"onBuilt",
]);
expect(app.plugins.size).toBe(1);
expect(Array.from(app.plugins.keys())).toEqual(["test"]);
});
test.only("drivers", async () => {
const called: string[] = [];
const app = new App(dummyConnection, undefined, {
drivers: {
email: {
send: async (to, subject, body) => {
called.push("email.send");
return {
id: "",
};
},
},
cache: {
get: async (key) => {
called.push("cache.get");
return "";
},
set: async (key, value, ttl) => {
called.push("cache.set");
},
del: async (key) => {
called.push("cache.del");
},
},
},
});
await app.build();
expect(app.drivers.cache).toBeDefined();
expect(app.drivers.email).toBeDefined();
await app.drivers.email.send("", "", "");
await app.drivers.cache.get("");
await app.drivers.cache.set("", "", 0);
await app.drivers.cache.del("");
expect(called).toEqual(["email.send", "cache.get", "cache.set", "cache.del"]);
}); });
}); });
+1
View File
@@ -0,0 +1 @@
tmp/*
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+77
View File
@@ -0,0 +1,77 @@
import { expect, describe, it, beforeAll, afterAll } from "bun:test";
import * as adapter from "adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { omitKeys } from "core/utils";
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("adapter", () => {
it("makes config", () => {
expect(omitKeys(adapter.makeConfig({}), ["connection"])).toEqual({});
expect(omitKeys(adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"])).toEqual(
{},
);
// merges everything returned from `app` with the config
expect(
omitKeys(
adapter.makeConfig(
{ app: (a) => ({ initialConfig: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } },
),
["connection"],
),
).toEqual({
initialConfig: { server: { cors: { origin: "test" } } },
});
});
/* it.only("...", async () => {
const app = await adapter.createAdapterApp();
}); */
it("reuses apps correctly", async () => {
const id = crypto.randomUUID();
const first = await adapter.createAdapterApp(
{
initialConfig: { server: { cors: { origin: "random" } } },
},
undefined,
{ id },
);
const second = await adapter.createAdapterApp();
const third = await adapter.createAdapterApp(undefined, undefined, { id });
await first.build();
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, {
makeApp: adapter.createFrameworkApp,
label: "framework app",
});
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createRuntimeApp,
label: "runtime app",
});
});
+4 -4
View File
@@ -19,8 +19,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234"); const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", { const request = new Request("http://example.com/test", {
headers: { headers: {
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`,
} },
}); });
const api = new Api({ request }); const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false); expect(api.isAuthVerified()).toBe(false);
@@ -35,8 +35,8 @@ describe("Api", async () => {
const token = await sign({ foo: "bar" }, "1234"); const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", { const request = new Request("http://example.com/test", {
headers: { headers: {
Cookie: `auth=${token}` Cookie: `auth=${token}`,
} },
}); });
const api = new Api({ request }); const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false); expect(api.isAuthVerified()).toBe(false);
+165 -13
View File
@@ -1,11 +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"; import { Guard } from "../../src/auth";
import { parse } from "../../src/core/utils";
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 { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper"; import { schemaToEm } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import { parse } from "core/object/schema";
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -17,16 +18,16 @@ describe("DataApi", () => {
const get = api.readMany("a".repeat(300), { select: ["id", "name"] }); const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
expect(get.request.method).toBe("GET"); expect(get.request.method).toBe("GET");
expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(300)}`); expect(new URL(get.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(300)}`);
const post = api.readMany("a".repeat(1000), { select: ["id", "name"] }); const post = api.readMany("a".repeat(1000), { select: ["id", "name"] });
expect(post.request.method).toBe("POST"); expect(post.request.method).toBe("POST");
expect(new URL(post.request.url).pathname).toBe(`/api/data/${"a".repeat(1000)}/query`); expect(new URL(post.request.url).pathname).toBe(`/api/data/entity/${"a".repeat(1000)}/query`);
}); });
it("returns result", async () => { it("returns result", async () => {
const schema = proto.em({ const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }) posts: proto.entity("posts", { title: proto.text() }),
}); });
const em = schemaToEm(schema); const em = schemaToEm(schema);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -39,20 +40,18 @@ describe("DataApi", () => {
const app = controller.getController(); const app = controller.getController();
{ {
const res = (await app.request("/posts")) as Response; const res = (await app.request("/entity/posts")) as Response;
const { data } = await res.json(); const { data } = (await res.json()) as any;
expect(data.length).toEqual(3); expect(data.length).toEqual(3);
} }
// @ts-ignore tests // @ts-ignore tests
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }); const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
// @ts-ignore protected
api.fetcher = app.request as typeof fetch;
{ {
const req = api.readMany("posts", { select: ["title"] }); const req = api.readMany("posts", { select: ["title"] });
expect(req.request.method).toBe("GET"); expect(req.request.method).toBe("GET");
const res = await req; const res = await req;
expect(res.data).toEqual(payload); expect(res.data).toEqual(payload as any);
} }
{ {
@@ -60,11 +59,164 @@ describe("DataApi", () => {
select: ["title"], select: ["title"],
limit: 100000, limit: 100000,
offset: 0, offset: 0,
sort: "id" sort: "id",
}); });
expect(req.request.method).toBe("POST"); expect(req.request.method).toBe("POST");
const res = await req; const res = await req;
expect(res.data).toEqual(payload); expect(res.data).toEqual(payload as any);
}
{
// make sure sort is working
const req = await api.readMany("posts", {
select: ["title"],
sort: "-id",
});
expect(req.data).toEqual(payload.reverse() as any);
}
});
it("updates many", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [
{ title: "foo", count: 0 },
{ title: "bar", count: 0 },
{ title: "baz", count: 0 },
{ title: "bla", count: 2 },
];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
// @ts-ignore tests
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
{
const req = api.readMany("posts", {
select: ["title", "count"],
});
const res = await req;
expect(res.data).toEqual(payload as any);
}
{
// update with empty where
expect(() => api.updateMany("posts", {}, { count: 1 })).toThrow();
expect(() => api.updateMany("posts", undefined, { count: 1 })).toThrow();
}
{
// update
const req = await api.updateMany("posts", { count: 0 }, { count: 1 });
expect(req.res.status).toBe(200);
}
{
// compare
const res = await api.readMany("posts", {
select: ["title", "count"],
});
expect(res.map((p) => p.count)).toEqual([1, 1, 1, 2]);
}
});
it("refines", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
const normalOne = api.readOne("posts", 1);
const normal = api.readMany("posts", { select: ["title"], where: { title: "baz" } });
expect((await normal).data).toEqual([{ title: "baz" }] as any);
// refine
const refined = normal.refine((data) => data[0]);
expect((await refined).data).toEqual({ title: "baz" } as any);
// one
const oneBy = api.readOneBy("posts", { where: { title: "baz" }, select: ["title"] });
const oneByRes = await oneBy;
expect(oneByRes.data).toEqual({ title: "baz" } as any);
expect(oneByRes.body.meta.items).toEqual(1);
});
it("exists/count", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }];
await em.mutator("posts").insertMany(payload);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
const exists = api.exists("posts", { id: 1 });
expect((await exists).exists).toBeTrue();
expect((await api.count("posts")).count).toEqual(3);
});
it("creates many", async () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text(), count: proto.number() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
const payload = [
{ title: "foo", count: 0 },
{ title: "bar", count: 0 },
{ title: "baz", count: 0 },
{ title: "bla", count: 2 },
];
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const app = controller.getController();
// @ts-ignore tests
const api = new DataApi({ basepath: "/" }, app.request as typeof fetch);
{
// create many
const res = await api.createMany("posts", payload);
expect(res.data.length).toEqual(4);
expect(res.ok).toBeTrue();
}
{
const req = api.readMany("posts", {
select: ["title", "count"],
});
const res = await req;
expect(res.data).toEqual(payload as any);
}
{
// create with empty
expect(() => api.createMany("posts", [])).toThrow();
} }
}); });
}); });
+168
View File
@@ -0,0 +1,168 @@
/// <reference types="@types/bun" />
import { describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
import { MediaApi } from "media/api/MediaApi";
import { assetsPath, assetsTmpPath } from "../helper";
const mockedBackend = new Hono()
.basePath("/api/media")
.post("/upload/:name", async (c) => {
const { name } = c.req.param();
const body = await getFileFromContext(c);
return c.json({ name, is_file: isFile(body), size: body.size });
})
.get("/file/:name", async (c) => {
const { name } = c.req.param();
const file = Bun.file(`${assetsPath}/${name}`);
return new Response(file, {
headers: {
"Content-Type": file.type,
"Content-Length": file.size.toString(),
},
});
});
describe("MediaApi", () => {
it("should give correct file upload url", () => {
const host = "http://localhost";
const basepath = "/api/media";
// @ts-ignore tests
const api = new MediaApi({
host,
basepath,
});
expect(api.getFileUploadUrl({ path: "path" } as any)).toBe(`${host}${basepath}/upload/path`);
});
it("should have correct upload headers", () => {
// @ts-ignore tests
const api = new MediaApi({
token: "token",
token_transport: "header",
});
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
});
it("should return empty headers if not using `header` transport", () => {
expect(
new MediaApi({
token_transport: "cookie",
})
.getUploadHeaders()
.has("Authorization"),
).toBe(false);
expect(
new MediaApi({
token_transport: "none",
})
.getUploadHeaders()
.has("Authorization"),
).toBe(false);
});
it("should get file: native", async () => {
const name = "image.png";
const path = `${assetsTmpPath}/${name}`;
const res = await mockedBackend.request("/api/media/file/" + name);
await Bun.write(path, res);
const file = await Bun.file(path);
expect(file.size).toBeGreaterThan(0);
expect(file.type).toBe("image/png");
await file.delete();
});
it("download", async () => {
// @ts-ignore tests
const api = new MediaApi({}, mockedBackend.request);
const name = "image.png";
const file = await api.download(name);
expect(isFile(file)).toBe(true);
expect(file.size).toBeGreaterThan(0);
expect(file.type).toBe("image/png");
expect(file.name).toContain(name);
});
it("getFile", async () => {
// @ts-ignore tests
const api = new MediaApi({}, mockedBackend.request);
const name = "image.png";
const res = await api.getFile(name);
expect(res.ok).toBe(true);
// make sure it's a normal api request as usual
expect(res.res.ok).toBe(true);
expect(isReadableStream(res)).toBe(true);
expect(isReadableStream(res.body)).toBe(true);
expect(isReadableStream(res.res.body)).toBe(true);
const blob = await res.res.blob();
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
expect(blob.name).toContain(name);
});
it("getFileStream", async () => {
// @ts-ignore tests
const api = new MediaApi({}, mockedBackend.request);
const name = "image.png";
const res = await api.getFileStream(name);
expect(isReadableStream(res)).toBe(true);
const blob = await new Response(res).blob();
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
expect(blob.name).toContain(name);
});
it("should upload file in various ways", async () => {
const api = new MediaApi(
{
upload_fetcher: mockedBackend.request,
},
mockedBackend.request,
);
const file = Bun.file(`${assetsPath}/image.png`);
async function matches(req: Promise<any>, filename: string) {
const res: any = await req;
expect(res.name).toBe(filename);
expect(res.is_file).toBe(true);
expect(res.size).toBe(file.size);
}
const url = "http://localhost/api/media/file/image.png";
// upload bun file
await matches(api.upload(file as any, { filename: "bunfile.png" }), "bunfile.png");
// upload via request
await matches(api.upload(new Request(url), { filename: "request.png" }), "request.png");
// upload via url
await matches(api.upload(url, { filename: "url.png" }), "url.png");
// upload via response
{
const response = await mockedBackend.request(url);
await matches(api.upload(response, { filename: "response.png" }), "response.png");
}
// upload via readable from bun
await matches(await api.upload(file.stream(), { filename: "readable.png" }), "readable.png");
// upload via readable from response
{
const response = (await mockedBackend.request(url)) as Response;
await matches(
await api.upload(response.body!, { filename: "readable.png" }),
"readable.png",
);
}
});
});
+10 -2
View File
@@ -61,7 +61,7 @@ describe("ModuleApi", () => {
it("adds additional headers from options", () => { it("adds additional headers from options", () => {
const headers = new Headers({ const headers = new Headers({
"X-Test": "123" "X-Test": "123",
}); });
const api = new Api({ host, headers }); const api = new Api({ host, headers });
expect(api.get("/").request.headers.get("X-Test")).toEqual("123"); expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
@@ -75,7 +75,7 @@ describe("ModuleApi", () => {
it("uses search params", () => { it("uses search params", () => {
const api = new Api({ host }); const api = new Api({ host });
const search = new URLSearchParams({ const search = new URLSearchParams({
foo: "bar" foo: "bar",
}); });
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString()); expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
}); });
@@ -89,6 +89,14 @@ describe("ModuleApi", () => {
expect(api.delete("/").request.method).toEqual("DELETE"); expect(api.delete("/").request.method).toEqual("DELETE");
}); });
it("refines", async () => {
const app = new Hono().get("/endpoint", (c) => c.json({ foo: ["bar"] }));
const api = new Api({ host }, app.request as typeof fetch);
expect((await api.get("/endpoint")).data).toEqual({ foo: ["bar"] });
expect((await api.get("/endpoint").refine((data) => data.foo)).data).toEqual(["bar"]);
});
// @todo: test error response // @todo: test error response
// @todo: test method shortcut functions // @todo: test method shortcut functions
}); });
+138
View File
@@ -0,0 +1,138 @@
import { describe, expect, mock, test } from "bun:test";
import type { ModuleBuildContext } from "../../src";
import { App, createApp } from "core/test/utils";
import * as proto from "../../src/data/prototype";
describe("App", () => {
test("seed includes ctx and app", async () => {
const called = mock(() => null);
await createApp({
options: {
seed: async ({ app, ...ctx }) => {
called();
expect(app).toBeDefined();
expect(ctx).toBeDefined();
expect(Object.keys(ctx)).toEqual([
"connection",
"server",
"em",
"emgr",
"guard",
"flags",
"logger",
"helper",
]);
},
},
}).build();
expect(called).toHaveBeenCalled();
const app = createApp({
initialConfig: {
data: proto
.em({
todos: proto.entity("todos", {
title: proto.text(),
}),
})
.toJSON(),
},
options: {
//manager: { verbosity: 2 },
seed: async ({ app, ...ctx }: ModuleBuildContext & { app: App }) => {
await ctx.em.mutator("todos").insertOne({ title: "ctx" });
await app.getApi().data.createOne("todos", { title: "api" });
},
},
});
await app.build();
const todos = await app.getApi().data.readMany("todos");
expect(todos.length).toBe(2);
expect(todos[0]?.title).toBe("ctx");
expect(todos[1]?.title).toBe("api");
});
test("lifecycle events are triggered", async () => {
const firstBoot = mock(() => null);
const configUpdate = mock(() => null);
const appBuilt = mock(() => null);
const appRequest = mock(() => null);
const beforeResponse = mock(() => null);
const app = createApp();
app.emgr.onEvent(
App.Events.AppFirstBoot,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
expect(event.params.app.version()).toBe(app.version());
firstBoot();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppBuiltEvent,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
expect(event.params.app.version()).toBe(app.version());
appBuilt();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppConfigUpdatedEvent,
() => {
configUpdate();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppRequest,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppRequest);
expect(event.params.app.version()).toBe(app.version());
expect(event.params.request).toBeInstanceOf(Request);
appRequest();
},
"sync",
);
app.emgr.onEvent(
App.Events.AppBeforeResponse,
(event) => {
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
expect(event.params.app.version()).toBe(app.version());
expect(event.params.response).toBeInstanceOf(Response);
beforeResponse();
},
"sync",
);
await app.build();
expect(firstBoot).toHaveBeenCalled();
expect(appBuilt).toHaveBeenCalled();
//expect(configUpdate).toHaveBeenCalled();
expect(appRequest).not.toHaveBeenCalled();
expect(beforeResponse).not.toHaveBeenCalled();
});
test("emgr exec modes", async () => {
const called = mock(() => null);
const app = createApp({
options: {
asyncEventsMode: "sync",
},
});
// register async listener
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
called();
});
await app.build();
await app.server.request(new Request("http://localhost"));
// expect async listeners to be executed sync after request
expect(called).toHaveBeenCalled();
});
});
+35
View File
@@ -0,0 +1,35 @@
import { AppServer, serverConfigSchema } from "modules/server/AppServer";
import { describe, test, expect } from "bun:test";
describe("AppServer", () => {
test("config", () => {
{
const server = new AppServer();
expect(server).toBeDefined();
expect(server.config).toEqual({
cors: {
origin: "*",
allow_methods: ["GET", "POST", "PATCH", "PUT", "DELETE"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
},
});
}
{
const server = new AppServer({
cors: {
origin: "https",
allow_methods: ["GET", "POST"],
},
});
expect(server).toBeDefined();
expect(server.config).toEqual({
cors: {
origin: "https",
allow_methods: ["GET", "POST"],
allow_headers: ["Content-Type", "Content-Length", "Authorization", "Accept"],
},
});
}
});
});
+42 -12
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { createApp, registries } from "../../src"; import { createApp } from "core/test/utils";
import * as proto from "../../src/data/prototype"; import * as proto from "../../src/data/prototype";
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
describe("repros", async () => { describe("repros", async () => {
/** /**
@@ -13,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();
{ {
@@ -24,9 +24,9 @@ describe("repros", async () => {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./" path: "./",
} },
} },
}); });
expect(config.enabled).toBe(true); expect(config.enabled).toBe(true);
@@ -38,9 +38,9 @@ describe("repros", async () => {
"entities.test", "entities.test",
proto proto
.entity("test", { .entity("test", {
content: proto.text() content: proto.text(),
}) })
.toJSON() .toJSON(),
); );
expect(app.em.entities.map((e) => e.name)).toContain("test"); expect(app.em.entities.map((e) => e.name)).toContain("test");
} }
@@ -54,8 +54,8 @@ describe("repros", async () => {
hidden: false, hidden: false,
mime_types: [], mime_types: [],
virtual: true, virtual: true,
entity: "test" entity: "test",
} },
}); });
expect( expect(
@@ -63,11 +63,41 @@ describe("repros", async () => {
type: "poly", type: "poly",
source: "test", source: "test",
target: "media", target: "media",
config: { mappedBy: "files" } config: { mappedBy: "files" },
}) }),
).resolves.toBeDefined(); ).resolves.toBeDefined();
} }
expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]); expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]);
}); });
test.only("verify inversedBy", async () => {
const schema = proto.em(
{
products: proto.entity("products", {
title: proto.text(),
}),
product_likes: proto.entity("product_likes", {
created_at: proto.date(),
}),
users: proto.entity("users", {}),
},
(fns, schema) => {
fns.relation(schema.product_likes).manyToOne(schema.products, { inversedBy: "likes" });
fns.relation(schema.product_likes).manyToOne(schema.users);
},
);
const app = createApp({ initialConfig: { data: schema.toJSON() } });
await app.build();
const info = (await (await app.server.request("/api/data/info/products")).json()) as any;
expect(info.fields).toEqual(["id", "title"]);
expect(info.relations.listable).toEqual([
{
entity: "product_likes",
ref: "likes",
},
]);
});
}); });
+2 -3
View File
@@ -2,8 +2,7 @@ import { describe, expect, test } from "bun:test";
import { Authenticator, type User, type UserPool } from "../../src/auth"; import { Authenticator, type User, type UserPool } from "../../src/auth";
import { cookieConfig } from "../../src/auth/authenticate/Authenticator"; import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy"; import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
import * as hash from "../../src/auth/utils/hash"; import { parse } from "core/object/schema";
import { Default, parse } from "../../src/core/utils";
/*class MemoryUserPool implements UserPool { /*class MemoryUserPool implements UserPool {
constructor(private users: User[] = []) {} constructor(private users: User[] = []) {}
@@ -23,7 +22,7 @@ import { Default, parse } from "../../src/core/utils";
describe("Authenticator", async () => { describe("Authenticator", async () => {
test("cookie options", async () => { test("cookie options", async () => {
console.log("parsed", parse(cookieConfig, undefined)); console.log("parsed", parse(cookieConfig, undefined));
console.log(Default(cookieConfig, {})); console.log(cookieConfig.template({}));
}); });
/*const userpool = new MemoryUserPool([ /*const userpool = new MemoryUserPool([
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") }, { id: 1, email: "d", username: "test", password: await hash.sha256("test") },
+22 -26
View File
@@ -7,19 +7,17 @@ describe("authorize", () => {
["read", "write"], ["read", "write"],
{ {
admin: { admin: {
permissions: ["read", "write"] permissions: ["read", "write"],
} },
}, },
{ enabled: true } { enabled: true },
); );
const user = { const user = {
role: "admin" role: "admin",
}; };
guard.setUserContext(user); expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write", user)).toBe(true);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(true);
expect(() => guard.granted("something")).toThrow(); expect(() => guard.granted("something")).toThrow();
}); });
@@ -29,27 +27,25 @@ describe("authorize", () => {
["read", "write"], ["read", "write"],
{ {
admin: { admin: {
permissions: ["read", "write"] permissions: ["read", "write"],
}, },
guest: { guest: {
permissions: ["read"], permissions: ["read"],
is_default: true is_default: true,
} },
}, },
{ enabled: true } { enabled: true },
); );
expect(guard.granted("read")).toBe(true); expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(false); expect(guard.granted("write")).toBe(false);
const user = { const user = {
role: "admin" role: "admin",
}; };
guard.setUserContext(user); expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write", user)).toBe(true);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(true);
}); });
test("guard implicit allow", async () => { test("guard implicit allow", async () => {
@@ -62,24 +58,24 @@ describe("authorize", () => {
test("role implicit allow", async () => { test("role implicit allow", async () => {
const guard = Guard.create(["read", "write"], { const guard = Guard.create(["read", "write"], {
admin: { admin: {
implicit_allow: true implicit_allow: true,
} },
}); });
guard.setUserContext({ const user = {
role: "admin" role: "admin",
}); };
expect(guard.granted("read")).toBe(true); expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write")).toBe(true); 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 = Guard.create(["read", "write"], { const guard = Guard.create(["read", "write"], {
guest: { guest: {
implicit_allow: true, implicit_allow: true,
is_default: true is_default: true,
} },
}); });
expect(guard.getUserRole()?.name).toBe("guest"); expect(guard.getUserRole()?.name).toBe("guest");
@@ -3,14 +3,16 @@ import { OAuthStrategy } from "../../../src/auth/authenticate/strategies";
const ALL_TESTS = !!process.env.ALL_TESTS; const ALL_TESTS = !!process.env.ALL_TESTS;
// @todo: add mock response
describe("OAuthStrategy", async () => { describe("OAuthStrategy", async () => {
const strategy = new OAuthStrategy({ return;
/*const strategy = new OAuthStrategy({
type: "oidc", type: "oidc",
client: { client: {
client_id: process.env.OAUTH_CLIENT_ID, client_id: process.env.OAUTH_CLIENT_ID!,
client_secret: process.env.OAUTH_CLIENT_SECRET client_secret: process.env.OAUTH_CLIENT_SECRET!,
}, },
name: "google" name: "google",
}); });
const state = "---"; const state = "---";
const redirect_uri = "http://localhost:3000/auth/google/callback"; const redirect_uri = "http://localhost:3000/auth/google/callback";
@@ -19,28 +21,29 @@ describe("OAuthStrategy", async () => {
const config = await strategy.getConfig(); const config = await strategy.getConfig();
console.log("config", JSON.stringify(config, null, 2)); console.log("config", JSON.stringify(config, null, 2));
const request = await strategy.request({
redirect_uri,
state
});
const server = Bun.serve({ const server = Bun.serve({
fetch: async (req) => { fetch: async (req) => {
console.log("req", req.method, req.url);
const url = new URL(req.url); const url = new URL(req.url);
if (url.pathname === "/auth/google/callback") { if (url.pathname === "/auth/google/callback") {
console.log("req", req); console.log("req", req);
const user = await strategy.callback(url, { const user = await strategy.callback(url, {
redirect_uri, redirect_uri,
state state,
}); });
console.log("---user", user); console.log("---user", user);
} }
return new Response("Bun!"); return new Response("Bun!");
} },
});
const request = await strategy.request({
redirect_uri,
state,
}); });
console.log("request", request); console.log("request", request);
await new Promise((resolve) => setTimeout(resolve, 100000)); await new Promise((resolve) => setTimeout(resolve, 100000));
}); });*/
}); });
+14 -11
View File
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
} }
return this.clone({ return this.clone({
foo: [this.params.foo, value].join("-") foo: [this.params.foo, value].join("-"),
}); });
} }
} }
@@ -52,7 +52,7 @@ describe("EventManager", async () => {
await new Promise((resolve) => setTimeout(resolve, 50)); await new Promise((resolve) => setTimeout(resolve, 50));
delayed(); delayed();
}, },
"sync" "sync",
); );
// don't allow unknown // don't allow unknown
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
new SpecialEvent({ foo: "bar" }); new SpecialEvent({ foo: "bar" });
new InformationalEvent(); new InformationalEvent();
// execute asyncs
await emgr.executeAsyncs();
expect(call).toHaveBeenCalledTimes(2); expect(call).toHaveBeenCalledTimes(2);
expect(delayed).toHaveBeenCalled(); expect(delayed).toHaveBeenCalled();
}); });
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
call(); call();
return Promise.all(p); return Promise.all(p);
}; };
const emgr = new EventManager( const emgr = new EventManager({ InformationalEvent });
{ InformationalEvent },
{
asyncExecutor
}
);
emgr.onEvent(InformationalEvent, async () => {}); emgr.onEvent(InformationalEvent, async () => {});
await emgr.emit(new InformationalEvent()); await emgr.emit(new InformationalEvent());
await emgr.executeAsyncs(asyncExecutor);
expect(call).toHaveBeenCalled(); expect(call).toHaveBeenCalled();
}); });
@@ -98,8 +97,8 @@ describe("EventManager", async () => {
const emgr = new EventManager( const emgr = new EventManager(
{ ReturnEvent, InformationalEvent }, { ReturnEvent, InformationalEvent },
{ {
onInvalidReturn onInvalidReturn,
} },
); );
// @ts-expect-error InformationalEvent has no return value // @ts-expect-error InformationalEvent has no return value
@@ -111,6 +110,7 @@ describe("EventManager", async () => {
emgr.onEvent(ReturnEvent, async () => "1", "sync"); emgr.onEvent(ReturnEvent, async () => "1", "sync");
emgr.onEvent(ReturnEvent, async () => "0", "sync"); emgr.onEvent(ReturnEvent, async () => "0", "sync");
// @todo: fix this
// @ts-expect-error must be string // @ts-expect-error must be string
emgr.onEvent(ReturnEvent, async () => 0, "sync"); emgr.onEvent(ReturnEvent, async () => 0, "sync");
@@ -124,6 +124,9 @@ describe("EventManager", async () => {
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" })); const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
expect(e2.returned).toBe(true); expect(e2.returned).toBe(true);
expect(e2.params.foo).toBe("bar-1-0"); expect(e2.params.foo).toBe("bar-1-0");
await emgr.executeAsyncs();
expect(onInvalidReturn).toHaveBeenCalled(); expect(onInvalidReturn).toHaveBeenCalled();
expect(asyncEventCallback).toHaveBeenCalled(); expect(asyncEventCallback).toHaveBeenCalled();
}); });
@@ -139,7 +142,7 @@ describe("EventManager", async () => {
expect(slug).toBe("informational-event"); expect(slug).toBe("informational-event");
call(); call();
}, },
{ mode: "sync", once: true } { mode: "sync", once: true },
); );
expect(emgr.getListeners().length).toBe(1); expect(emgr.getListeners().length).toBe(1);
+17 -16
View File
@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import type { TObject, TString } from "@sinclair/typebox"; import { Registry } from "core";
import { Registry } from "../../src/core/registry/Registry"; import { s } from "core/object/schema";
import { type TSchema, Type } from "../../src/core/utils";
type Constructor<T> = new (...args: any[]) => T; type Constructor<T> = new (...args: any[]) => T;
@@ -12,7 +11,7 @@ class What {
return null; return null;
} }
getType() { getType() {
return Type.Object({ type: Type.String() }); return s.object({ type: s.string() });
} }
} }
class What2 extends What {} class What2 extends What {}
@@ -20,7 +19,7 @@ class NotAllowed {}
type Test1 = { type Test1 = {
cls: new (...args: any[]) => What; cls: new (...args: any[]) => What;
schema: TObject<{ type: TString }>; schema: s.ObjectSchema<{ type: s.StringSchema }>;
enabled: boolean; enabled: boolean;
}; };
@@ -29,40 +28,40 @@ describe("Registry", () => {
const registry = new Registry<Test1>().set({ const registry = new Registry<Test1>().set({
first: { first: {
cls: What, cls: What,
schema: Type.Object({ type: Type.String(), what: Type.String() }), schema: s.object({ type: s.string(), what: s.string() }),
enabled: true enabled: true,
} },
} satisfies Record<string, Test1>); } satisfies Record<string, Test1>);
const item = registry.get("first"); const item = registry.get("first");
expect(item).toBeDefined(); expect(item).toBeDefined();
expect(item?.cls).toBe(What); expect(item?.cls).toBe(What);
const second = Type.Object({ type: Type.String(), what: Type.String() }); const second = s.object({ type: s.string(), what: s.string() });
registry.add("second", { registry.add("second", {
cls: What2, cls: What2,
schema: second, schema: second,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("second").schema).toEqual(second); expect(registry.get("second").schema).toEqual(second);
const third = Type.Object({ type: Type.String({ default: "1" }), what22: Type.String() }); const third = s.object({ type: s.string({ default: "1" }), what22: s.string() });
registry.add("third", { registry.add("third", {
// @ts-expect-error // @ts-expect-error
cls: NotAllowed, cls: NotAllowed,
schema: third, schema: third,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("third").schema).toEqual(third); expect(registry.get("third").schema).toEqual(third);
const fourth = Type.Object({ type: Type.Number(), what22: Type.String() }); const fourth = s.object({ type: s.number(), what22: s.string() });
registry.add("fourth", { registry.add("fourth", {
cls: What, cls: What,
// @ts-expect-error // @ts-expect-error
schema: fourth, schema: fourth,
enabled: true enabled: true,
}); });
// @ts-ignore // @ts-ignore
expect(registry.get("fourth").schema).toEqual(fourth); expect(registry.get("fourth").schema).toEqual(fourth);
@@ -75,13 +74,15 @@ describe("Registry", () => {
return { return {
cls: a, cls: a,
schema: a.prototype.getType(), schema: a.prototype.getType(),
enabled: true enabled: true,
}; };
}); });
registry.register("what2", What2); registry.register("what2", What2);
expect(registry.get("what2")).toBeDefined(); expect(registry.get("what2")).toBeDefined();
expect(registry.get("what2").cls).toBe(What2); expect(registry.get("what2").cls).toBe(What2);
expect(registry.get("what2").schema).toEqual(What2.prototype.getType()); expect(JSON.stringify(registry.get("what2").schema)).toEqual(
JSON.stringify(What2.prototype.getType()),
);
}); });
}); });
@@ -1,57 +0,0 @@
import * as assert from "node:assert/strict";
import { createWriteStream } from "node:fs";
import { after, beforeEach, describe, test } from "node:test";
import { Miniflare } from "miniflare";
import {
CloudflareKVCacheItem,
CloudflareKVCachePool
} from "../../../src/core/cache/adapters/CloudflareKvCache";
import { runTests } from "./cache-test-suite";
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
console.log = async (message: any) => {
const tty = createWriteStream("/dev/tty");
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
return tty.write(`${msg}\n`);
};
describe("CloudflareKv", async () => {
let mf: Miniflare;
runTests({
createCache: async () => {
if (mf) {
await mf.dispose();
}
mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
kvNamespaces: ["TEST"]
});
const kv = await mf.getKVNamespace("TEST");
return new CloudflareKVCachePool(kv as any);
},
createItem: (key, value) => new CloudflareKVCacheItem(key, value),
tester: {
test,
beforeEach,
expect: (actual?: any) => {
return {
toBe(expected: any) {
assert.equal(actual, expected);
},
toEqual(expected: any) {
assert.deepEqual(actual, expected);
},
toBeUndefined() {
assert.equal(actual, undefined);
}
};
}
}
});
after(async () => {
await mf?.dispose();
});
});
-15
View File
@@ -1,15 +0,0 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { MemoryCache, MemoryCacheItem } from "../../../src/core/cache/adapters/MemoryCache";
import { runTests } from "./cache-test-suite";
describe("MemoryCache", () => {
runTests({
createCache: async () => new MemoryCache(),
createItem: (key, value) => new MemoryCacheItem(key, value),
tester: {
test,
beforeEach,
expect
}
});
});
-84
View File
@@ -1,84 +0,0 @@
//import { beforeEach as bunBeforeEach, expect as bunExpect, test as bunTest } from "bun:test";
import type { ICacheItem, ICachePool } from "../../../src/core/cache/cache-interface";
export type TestOptions = {
createCache: () => Promise<ICachePool>;
createItem: (key: string, value: any) => ICacheItem;
tester: {
test: (name: string, fn: () => Promise<void>) => void;
beforeEach: (fn: () => Promise<void>) => void;
expect: (actual?: any) => {
toBe(expected: any): void;
toEqual(expected: any): void;
toBeUndefined(): void;
};
};
};
export function runTests({ createCache, createItem, tester }: TestOptions) {
let cache: ICachePool<string>;
const { test, beforeEach, expect } = tester;
beforeEach(async () => {
cache = await createCache();
});
test("getItem returns correct item", async () => {
const item = createItem("key1", "value1");
await cache.save(item);
const retrievedItem = await cache.get("key1");
expect(retrievedItem.value()).toEqual(item.value());
});
test("getItem returns new item when key does not exist", async () => {
const retrievedItem = await cache.get("key1");
expect(retrievedItem.key()).toEqual("key1");
expect(retrievedItem.value()).toBeUndefined();
});
test("getItems returns correct items", async () => {
const item1 = createItem("key1", "value1");
const item2 = createItem("key2", "value2");
await cache.save(item1);
await cache.save(item2);
const retrievedItems = await cache.getMany(["key1", "key2"]);
expect(retrievedItems.get("key1")?.value()).toEqual(item1.value());
expect(retrievedItems.get("key2")?.value()).toEqual(item2.value());
});
test("hasItem returns true when item exists and is a hit", async () => {
const item = createItem("key1", "value1");
await cache.save(item);
expect(await cache.has("key1")).toBe(true);
});
test("clear and deleteItem correctly clear the cache and delete items", async () => {
const item = createItem("key1", "value1");
await cache.save(item);
if (cache.supports().clear) {
await cache.clear();
} else {
await cache.delete("key1");
}
expect(await cache.has("key1")).toBe(false);
});
test("save correctly saves items to the cache", async () => {
const item = createItem("key1", "value1");
await cache.save(item);
expect(await cache.has("key1")).toBe(true);
});
test("putItem correctly puts items in the cache ", async () => {
await cache.put("key1", "value1", { ttl: 60 });
const item = await cache.get("key1");
expect(item.value()).toEqual("value1");
expect(item.hit()).toBe(true);
});
/*test("commit returns true", async () => {
expect(await cache.commit()).toBe(true);
});*/
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
describe("crypto", async () => { describe("crypto", async () => {
test("sha256", async () => { test("sha256", async () => {
expect(await hash.sha256("test")).toBe( expect(await hash.sha256("test")).toBe(
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
); );
}); });
test("sha1", async () => { test("sha1", async () => {
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, test } from "bun:test";
import { env, is_toggled } from "core/env";
describe("env", () => {
test("is_toggled", () => {
expect(is_toggled("true")).toBe(true);
expect(is_toggled("1")).toBe(true);
expect(is_toggled("false")).toBe(false);
expect(is_toggled("0")).toBe(false);
expect(is_toggled(true)).toBe(true);
expect(is_toggled(false)).toBe(false);
expect(is_toggled(undefined)).toBe(false);
expect(is_toggled(null)).toBe(false);
expect(is_toggled(1)).toBe(true);
expect(is_toggled(0)).toBe(false);
expect(is_toggled("anything else")).toBe(false);
expect(is_toggled(undefined, true)).toBe(true);
});
test("env()", () => {
expect(env("cli_log_level", undefined, { source: {} })).toBeUndefined();
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "log" } })).toBe(
"log" as any,
);
expect(env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "LOG" } })).toBe(
"log" as any,
);
expect(
env("cli_log_level", undefined, { source: { BKND_CLI_LOG_LEVEL: "asdf" } }),
).toBeUndefined();
expect(env("modules_debug", undefined, { source: {} })).toBeFalse();
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "1" } })).toBeTrue();
expect(env("modules_debug", undefined, { source: { BKND_MODULES_DEBUG: "0" } })).toBeFalse();
});
});
+120 -106
View File
@@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { SchemaObject } from "../../../src/core"; import { SchemaObject } from "../../../src/core";
import { Type } from "../../../src/core/utils"; import { s } from "core/object/schema";
describe("SchemaObject", async () => { describe("SchemaObject", async () => {
test("basic", async () => { test("basic", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ a: Type.String({ default: "b" }) }), s.strictObject({ a: s.string({ default: "b" }) }),
{ a: "test" }, { a: "test" },
{ {
forceParse: true forceParse: true,
} },
); );
expect(m.get()).toEqual({ a: "test" }); expect(m.get()).toEqual({ a: "test" });
@@ -23,28 +23,28 @@ describe("SchemaObject", async () => {
test("patch", async () => { test("patch", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
s: Type.Object( s: s.strictObject(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.strictObject(
{ {
c: Type.String({ default: "d" }), c: s.string({ default: "d" }),
e: Type.String({ default: "f" }) e: s.string({ default: "f" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {}, additionalProperties: false } { default: {} },
) ),
}) }),
); );
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
await m.patch("s.a", "c"); await m.patch("s.a", "c");
// non-existing path on no additional properties // non-existing path on no additional properties
expect(() => m.patch("s.s.s", "c")).toThrow(); expect(m.patch("s.s.s", "c")).rejects.toThrow();
// wrong type // wrong type
expect(() => m.patch("s.a", 1)).toThrow(); expect(() => m.patch("s.a", 1)).toThrow();
@@ -58,9 +58,9 @@ describe("SchemaObject", async () => {
test("patch array", async () => { test("patch array", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}) }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -75,20 +75,20 @@ describe("SchemaObject", async () => {
test("remove", async () => { test("remove", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.object({
s: Type.Object( s: s.object(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.object(
{ {
c: Type.String({ default: "d" }) c: s.string({ default: "d" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {} } { default: {} },
) ),
}) }),
); );
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
@@ -107,9 +107,9 @@ describe("SchemaObject", async () => {
test("set", async () => { test("set", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}) }),
); );
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] }); expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -124,8 +124,8 @@ describe("SchemaObject", async () => {
let called = false; let called = false;
let result: any; let result: any;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
await new Promise((r) => setTimeout(r, 10)); await new Promise((r) => setTimeout(r, 10));
called = true; called = true;
result = config; result = config;
} },
} },
); );
await m.set({ methods: ["GET", "POST"] }); await m.set({ methods: ["GET", "POST"] });
@@ -145,8 +145,8 @@ describe("SchemaObject", async () => {
test("listener: onBeforeUpdate", async () => { test("listener: onBeforeUpdate", async () => {
let called = false; let called = false;
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }) methods: s.array(s.string(), { default: ["GET", "PATCH"] }),
}), }),
undefined, undefined,
{ {
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
called = true; called = true;
to.methods.push("OPTIONS"); to.methods.push("OPTIONS");
return to; return to;
} },
} },
); );
const result = await m.set({ methods: ["GET", "POST"] }); const result = await m.set({ methods: ["GET", "POST"] });
@@ -167,8 +167,8 @@ describe("SchemaObject", async () => {
}); });
test("throwIfRestricted", async () => { test("throwIfRestricted", async () => {
const m = new SchemaObject(Type.Object({}), undefined, { const m = new SchemaObject(s.strictObject({}), undefined, {
restrictPaths: ["a.b"] restrictPaths: ["a.b"],
}); });
expect(() => m.throwIfRestricted("a.b")).toThrow(); expect(() => m.throwIfRestricted("a.b")).toThrow();
@@ -179,24 +179,24 @@ describe("SchemaObject", async () => {
test("restriction bypass", async () => { test("restriction bypass", async () => {
const m = new SchemaObject( const m = new SchemaObject(
Type.Object({ s.strictObject({
s: Type.Object( s: s.strictObject(
{ {
a: Type.String({ default: "b" }), a: s.string({ default: "b" }),
b: Type.Object( b: s.strictObject(
{ {
c: Type.String({ default: "d" }) c: s.string({ default: "d" }),
}, },
{ default: {} } { default: {} },
) ),
}, },
{ default: {} } { default: {} },
) ),
}), }),
undefined, undefined,
{ {
restrictPaths: ["s.b"] restrictPaths: ["s.b"],
} },
); );
expect(m.patch("s.b.c", "e")).rejects.toThrow(); expect(m.patch("s.b.c", "e")).rejects.toThrow();
@@ -205,7 +205,21 @@ describe("SchemaObject", async () => {
expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } }); expect(m.get()).toEqual({ s: { a: "b", b: { c: "e" } } });
}); });
const dataEntitiesSchema = Type.Object( const dataEntitiesSchema = s.strictObject({
entities: s.record(
s.object({
fields: s.record(
s.object({
type: s.string(),
config: s.object({}).optional(),
}),
),
config: s.record(s.string()).optional(),
}),
),
});
/* const dataEntitiesSchema = Type.Object(
{ {
entities: Type.Object( entities: Type.Object(
{}, {},
@@ -217,33 +231,33 @@ describe("SchemaObject", async () => {
additionalProperties: Type.Object({ additionalProperties: Type.Object({
type: Type.String(), type: Type.String(),
config: Type.Optional( config: Type.Optional(
Type.Object({}, { additionalProperties: Type.String() }) Type.Object({}, { additionalProperties: Type.String() }),
) ),
}) }),
} },
), ),
config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })) config: Type.Optional(Type.Object({}, { additionalProperties: Type.String() })),
}) }),
} },
) ),
}, },
{ {
additionalProperties: false additionalProperties: false,
} },
); ); */
test("patch safe object, overwrite", async () => { test("patch safe object, overwrite", async () => {
const data = { const data = {
entities: { entities: {
some: { some: {
fields: { fields: {
a: { type: "string", config: { some: "thing" } } a: { type: "string", config: { some: "thing" } },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config/] overwritePaths: [/^entities\..*\.fields\..*\.config/],
}); });
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } }); await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
@@ -252,10 +266,10 @@ describe("SchemaObject", async () => {
entities: { entities: {
some: { some: {
fields: { fields: {
a: { type: "string", config: { another: "one" } } a: { type: "string", config: { another: "one" } },
} },
} },
} },
}); });
}); });
@@ -265,22 +279,22 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/] overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
}); });
await m.patch("entities.test", { await m.patch("entities.test", {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
}); });
expect(m.get()).toEqual({ expect(m.get()).toEqual({
@@ -288,17 +302,17 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
}, },
test: { test: {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
} },
} },
}); });
}); });
@@ -308,14 +322,14 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
} },
} },
} },
}; };
const m = new SchemaObject(dataEntitiesSchema, data, { const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true, forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/] overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
}); });
expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow(); expect(m.patch("desc", "entities.users.config.sort_dir")).rejects.toThrow();
@@ -323,13 +337,13 @@ describe("SchemaObject", async () => {
await m.patch("entities.test", { await m.patch("entities.test", {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
}); });
await m.patch("entities.users.config", { await m.patch("entities.users.config", {
sort_dir: "desc" sort_dir: "desc",
}); });
expect(m.get()).toEqual({ expect(m.get()).toEqual({
@@ -337,20 +351,20 @@ describe("SchemaObject", async () => {
users: { users: {
fields: { fields: {
email: { type: "string" }, email: { type: "string" },
password: { type: "string" } password: { type: "string" },
}, },
config: { config: {
sort_dir: "desc" sort_dir: "desc",
} },
}, },
test: { test: {
fields: { fields: {
content: { content: {
type: "text" type: "text",
} },
} },
} },
} },
}); });
}); });
}); });
+44 -44
View File
@@ -13,8 +13,8 @@ describe("diff", () => {
t: "a", t: "a",
p: ["b"], p: ["b"],
o: undefined, o: undefined,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -35,8 +35,8 @@ describe("diff", () => {
t: "r", t: "r",
p: ["b"], p: ["b"],
o: 2, o: 2,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -57,8 +57,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -79,8 +79,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", "b"], p: ["a", "b"],
o: 1, o: 1,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -101,14 +101,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", 1], p: ["a", 1],
o: 2, o: 2,
n: 4 n: 4,
}, },
{ {
t: "a", t: "a",
p: ["a", 3], p: ["a", 3],
o: undefined, o: undefined,
n: 5 n: 5,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -129,20 +129,20 @@ describe("diff", () => {
t: "a", t: "a",
p: ["a", 0], p: ["a", 0],
o: undefined, o: undefined,
n: 1 n: 1,
}, },
{ {
t: "a", t: "a",
p: ["a", 1], p: ["a", 1],
o: undefined, o: undefined,
n: 2 n: 2,
}, },
{ {
t: "a", t: "a",
p: ["a", 2], p: ["a", 2],
o: undefined, o: undefined,
n: 3 n: 3,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -163,14 +163,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", 1], p: ["a", 1],
o: 2, o: 2,
n: 3 n: 3,
}, },
{ {
t: "r", t: "r",
p: ["a", 2], p: ["a", 2],
o: 3, o: 3,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -183,14 +183,14 @@ describe("diff", () => {
it("should handle complex nested changes", () => { it("should handle complex nested changes", () => {
const oldObj = { const oldObj = {
a: { a: {
b: [1, 2, { c: 3 }] b: [1, 2, { c: 3 }],
} },
}; };
const newObj = { const newObj = {
a: { a: {
b: [1, 2, { c: 4 }, 5] b: [1, 2, { c: 4 }, 5],
} },
}; };
const diffs = diff(oldObj, newObj); const diffs = diff(oldObj, newObj);
@@ -200,14 +200,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a", "b", 2, "c"], p: ["a", "b", 2, "c"],
o: 3, o: 3,
n: 4 n: 4,
}, },
{ {
t: "a", t: "a",
p: ["a", "b", 3], p: ["a", "b", 3],
o: undefined, o: undefined,
n: 5 n: 5,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -228,14 +228,14 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: undefined, o: undefined,
n: null n: null,
}, },
{ {
t: "e", t: "e",
p: ["b"], p: ["b"],
o: null, o: null,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -256,8 +256,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: "1" n: "1",
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -278,14 +278,14 @@ describe("diff", () => {
t: "r", t: "r",
p: ["b"], p: ["b"],
o: 2, o: 2,
n: undefined n: undefined,
}, },
{ {
t: "a", t: "a",
p: ["c"], p: ["c"],
o: undefined, o: undefined,
n: 3 n: 3,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -306,8 +306,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: [1, 2, 3], o: [1, 2, 3],
n: { b: 4 } n: { b: 4 },
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -328,8 +328,8 @@ describe("diff", () => {
t: "e", t: "e",
p: ["a"], p: ["a"],
o: { b: 1 }, o: { b: 1 },
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -350,14 +350,14 @@ describe("diff", () => {
t: "r", t: "r",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: undefined n: undefined,
}, },
{ {
t: "a", t: "a",
p: ["b"], p: ["b"],
o: undefined, o: undefined,
n: 2 n: 2,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -408,8 +408,8 @@ describe("diff", () => {
t: "a", t: "a",
p: ["a"], p: ["a"],
o: undefined, o: undefined,
n: 1 n: 1,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -430,8 +430,8 @@ describe("diff", () => {
t: "r", t: "r",
p: ["a"], p: ["a"],
o: 1, o: 1,
n: undefined n: undefined,
} },
]); ]);
const appliedObj = apply(oldObj, diffs); const appliedObj = apply(oldObj, diffs);
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type ObjectQuery, convert, validate } from "../../../src/core/object/query/object-query"; import { type ObjectQuery, convert, validate } from "core/object/query/object-query";
describe("object-query", () => { describe("object-query", () => {
const q: ObjectQuery = { name: "Michael" }; const q: ObjectQuery = { name: "Michael" };
@@ -9,7 +9,7 @@ describe("object-query", () => {
test("validates", async () => { test("validates", async () => {
const converted = convert({ const converted = convert({
name: { $eq: "ch" } name: { $eq: "ch" },
}); });
validate(converted, { name: "Michael" }); validate(converted, { name: "Michael" });
}); });
@@ -31,7 +31,7 @@ describe("object-query", () => {
[{ val: { $notnull: 1 } }, { val: null }, false], [{ val: { $notnull: 1 } }, { val: null }, false],
[{ val: { $regex: ".*" } }, { val: "test" }, true], [{ val: { $regex: ".*" } }, { val: "test" }, true],
[{ val: { $regex: /^t.*/ } }, { val: "test" }, true], [{ val: { $regex: /^t.*/ } }, { val: "test" }, true],
[{ val: { $regex: /^b.*/ } }, { val: "test" }, false] [{ val: { $regex: /^b.*/ } }, { val: "test" }, false],
]; ];
for (const [query, object, expected] of tests) { for (const [query, object, expected] of tests) {
@@ -55,10 +55,10 @@ describe("object-query", () => {
[ [
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, { $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
{ val1: "foo", val2: "bar" }, { val1: "foo", val2: "bar" },
true true,
], ],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true], [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 1 }, true],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false] [{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
]; ];
for (const [query, object, expected] of tests) { for (const [query, object, expected] of tests) {
+91 -10
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Perf } from "../../src/core/utils"; import { Perf, ucFirst } from "../../src/core/utils";
import * as utils from "../../src/core/utils"; import * as utils from "../../src/core/utils";
import { assetsPath } from "../helper";
async function wait(ms: number) { async function wait(ms: number) {
return new Promise((resolve) => { return new Promise((resolve) => {
@@ -16,7 +17,7 @@ describe("Core Utils", async () => {
expect(result).toEqual([ expect(result).toEqual([
{ key: "a", value: 1 }, { key: "a", value: 1 },
{ key: "b", value: 2 }, { key: "b", value: 2 },
{ key: "c", value: 3 } { key: "c", value: 3 },
]); ]);
}); });
@@ -51,7 +52,7 @@ describe("Core Utils", async () => {
const obj = utils.headersToObject(headers); const obj = utils.headersToObject(headers);
expect(obj).toEqual({ expect(obj).toEqual({
"content-type": "application/json", "content-type": "application/json",
authorization: "Bearer 123" authorization: "Bearer 123",
}); });
}); });
@@ -115,7 +116,7 @@ describe("Core Utils", async () => {
[{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }], [{ a: 1, b: 2, c: 3 }, ["b"], { a: 1, c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }], [{ a: 1, b: 2, c: 3 }, ["c"], { a: 1, b: 2 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }], [{ a: 1, b: 2, c: 3 }, ["a", "b"], { c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}] [{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
] as [object, string[], object][]; ] as [object, string[], object][];
for (const [obj, keys, expected] of objects) { for (const [obj, keys, expected] of objects) {
@@ -134,8 +135,8 @@ describe("Core Utils", async () => {
[true, true, true], [true, true, true],
[true, false, false], [true, false, false],
[false, false, true], [false, false, true],
[1, NaN, false], [1, Number.NaN, false],
[NaN, NaN, true], [Number.NaN, Number.NaN, true],
[null, null, true], [null, null, true],
[null, undefined, false], [null, undefined, false],
[undefined, undefined, true], [undefined, undefined, true],
@@ -146,9 +147,9 @@ describe("Core Utils", async () => {
new Map([["a", 1]]), new Map([["a", 1]]),
new Map([ new Map([
["a", 1], ["a", 1],
["b", 2] ["b", 2],
]), ]),
false false,
], ],
[{ a: 1 }, { a: 1 }, true], [{ a: 1 }, { a: 1 }, true],
[{ a: 1 }, { a: 2 }, false], [{ a: 1 }, { a: 2 }, false],
@@ -169,7 +170,7 @@ describe("Core Utils", async () => {
[[1, 2, 3], [1, 2, 3, 4], false], [[1, 2, 3], [1, 2, 3, 4], false],
[[{ a: 1 }], [{ a: 1 }], true], [[{ a: 1 }], [{ a: 1 }], true],
[[{ a: 1 }], [{ a: 2 }], false], [[{ a: 1 }], [{ a: 2 }], false],
[[{ a: 1 }], [{ b: 1 }], false] [[{ a: 1 }], [{ b: 1 }], false],
] as [any, any, boolean][]; ] as [any, any, boolean][];
for (const [a, b, expected] of objects) { for (const [a, b, expected] of objects) {
@@ -185,7 +186,7 @@ describe("Core Utils", async () => {
[{ a: { b: 1 } }, "a.b", 1], [{ a: { b: 1 } }, "a.b", 1],
[{ a: { b: 1 } }, "a.b.c", null, null], [{ a: { b: 1 } }, "a.b.c", null, null],
[{ a: { b: 1 } }, "a.b.c", 1, 1], [{ a: { b: 1 } }, "a.b.c", 1, 1],
[[[1]], "0.0", 1] [[[1]], "0.0", 1],
] as [object, string, any, any][]; ] as [object, string, any, any][];
for (const [obj, path, expected, defaultValue] of tests) { for (const [obj, path, expected, defaultValue] of tests) {
@@ -194,4 +195,84 @@ describe("Core Utils", async () => {
} }
}); });
}); });
describe("file", async () => {
describe("type guards", () => {
const types = {
blob: new Blob(),
file: new File([""], "file.txt"),
stream: new ReadableStream(),
arrayBuffer: new ArrayBuffer(10),
arrayBufferView: new Uint8Array(new ArrayBuffer(10)),
};
const fns = [
[utils.isReadableStream, "stream"],
[utils.isBlob, "blob", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isFile, "file", ["stream", "arrayBuffer", "arrayBufferView"]],
[utils.isArrayBuffer, "arrayBuffer"],
[utils.isArrayBufferView, "arrayBufferView"],
] as const;
const additional = [0, 0.0, "", null, undefined, {}, []];
for (const [fn, type, _to_test] of fns) {
test(`is${ucFirst(type)}`, () => {
const to_test = _to_test ?? (Object.keys(types) as string[]);
for (const key of to_test) {
const value = types[key as keyof typeof types];
const result = fn(value);
expect(result).toBe(key === type);
}
for (const value of additional) {
const result = fn(value);
expect(result).toBe(false);
}
});
}
});
test("getContentName", () => {
const name = "test.json";
const text = "attachment; filename=" + name;
const headers = new Headers({
"Content-Disposition": text,
});
const request = new Request("http://example.com", {
headers,
});
expect(utils.getContentName(text)).toBe(name);
expect(utils.getContentName(headers)).toBe(name);
expect(utils.getContentName(request)).toBe(name);
});
test.only("detectImageDimensions", async () => {
// wrong
// @ts-expect-error
expect(utils.detectImageDimensions(new ArrayBuffer(), "text/plain")).rejects.toThrow();
// successful ones
const getFile = (name: string): File => Bun.file(`${assetsPath}/${name}`) as any;
expect(await utils.detectImageDimensions(getFile("image.png"))).toEqual({
width: 362,
height: 387,
});
expect(await utils.detectImageDimensions(getFile("image.jpg"))).toEqual({
width: 453,
height: 512,
});
});
});
describe("dates", () => {
test.only("formats local time", () => {
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());
console.log(new Date());
console.log("timezone", Intl.DateTimeFormat().resolvedOptions().timeZone);
});
});
}); });
+51 -100
View File
@@ -1,19 +1,19 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Guard } from "../../src/auth"; import { Guard } from "../../src/auth";
import { parse } from "../../src/core/utils"; import { parse } from "core/object/schema";
import { import {
Entity, Entity,
type EntityData, type EntityData,
EntityManager, EntityManager,
ManyToOneRelation, ManyToOneRelation,
type MutatorResponse, TextField,
type RepositoryResponse,
TextField
} from "../../src/data"; } 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 { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
import type { RepositoryResultJSON } from "data/entities/query/RepositoryResult";
import type { MutatorResultJSON } from "data/entities/mutation/MutatorResult";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
beforeAll(() => disableConsoleLog(["log", "warn"])); beforeAll(() => disableConsoleLog(["log", "warn"]));
@@ -21,60 +21,14 @@ afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
const dataConfig = parse(dataConfigSchema, {}); const dataConfig = parse(dataConfigSchema, {});
describe("[data] DataController", async () => { describe("[data] DataController", async () => {
test("repoResult", async () => {
const em = new EntityManager<any>([], dummyConnection);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const res = controller.repoResult({
entity: null as any,
data: [] as any,
sql: "",
parameters: [] as any,
result: [] as any,
meta: {
total: 0,
count: 0,
items: 0
}
});
expect(res).toEqual({
meta: {
total: 0,
count: 0,
items: 0
},
data: []
});
});
test("mutatorResult", async () => {
const em = new EntityManager([], dummyConnection);
const ctx: any = { em, guard: new Guard() };
const controller = new DataController(ctx, dataConfig);
const res = controller.mutatorResult({
entity: null as any,
data: [] as any,
sql: "",
parameters: [] as any,
result: [] as any
});
expect(res).toEqual({
data: []
});
});
describe("getController", async () => { describe("getController", async () => {
const users = new Entity("users", [ const users = new Entity("users", [
new TextField("name", { required: true }), new TextField("name", { required: true }),
new TextField("bio") new TextField("bio"),
]); ]);
const posts = new Entity("posts", [new TextField("content")]); const posts = new Entity("posts", [new TextField("content")]);
const em = new EntityManager([users, posts], dummyConnection, [ const em = new EntityManager([users, posts], dummyConnection, [
new ManyToOneRelation(posts, users) new ManyToOneRelation(posts, users),
]); ]);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -83,12 +37,12 @@ describe("[data] DataController", async () => {
users: [ users: [
{ name: "foo", bio: "bar" }, { name: "foo", bio: "bar" },
{ name: "bar", bio: null }, { name: "bar", bio: null },
{ name: "baz", bio: "!!!" } { name: "baz", bio: "!!!" },
], ],
posts: [ posts: [
{ content: "post 1", users_id: 1 }, { content: "post 1", users_id: 1 },
{ content: "post 2", users_id: 2 } { content: "post 2", users_id: 2 },
] ],
}; };
const ctx: any = { em, guard: new Guard() }; const ctx: any = { em, guard: new Guard() };
@@ -116,12 +70,11 @@ describe("[data] DataController", async () => {
//console.log("app.routes", app.routes); //console.log("app.routes", app.routes);
// create users // create users
for await (const _user of fixtures.users) { for await (const _user of fixtures.users) {
const res = await app.request("/users", { const res = await app.request("/entity/users", {
method: "POST", method: "POST",
body: JSON.stringify(_user) body: JSON.stringify(_user),
}); });
//console.log("res", { _user }, res); const result = (await res.json()) as MutatorResultJSON;
const result = (await res.json()) as MutatorResponse;
const { id, ...data } = result.data as any; const { id, ...data } = result.data as any;
expect(res.status).toBe(201); expect(res.status).toBe(201);
@@ -131,11 +84,11 @@ describe("[data] DataController", async () => {
// create posts // create posts
for await (const _post of fixtures.posts) { for await (const _post of fixtures.posts) {
const res = await app.request("/posts", { const res = await app.request("/entity/posts", {
method: "POST", method: "POST",
body: JSON.stringify(_post) body: JSON.stringify(_post),
}); });
const result = (await res.json()) as MutatorResponse; const result = (await res.json()) as MutatorResultJSON;
const { id, ...data } = result.data as any; const { id, ...data } = result.data as any;
expect(res.status).toBe(201); expect(res.status).toBe(201);
@@ -145,91 +98,89 @@ describe("[data] DataController", async () => {
}); });
test("/:entity (read many)", async () => { test("/:entity (read many)", async () => {
const res = await app.request("/users"); const res = await app.request("/entity/users");
const data = (await res.json()) as RepositoryResponse; const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3); //expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(3); //expect(data.meta.count).toBe(3);
expect(data.meta.items).toBe(3); expect(data.meta.items).toBe(3);
expect(data.data.length).toBe(3); expect(data.data.length).toBe(3);
expect(data.data[0].name).toBe("foo"); expect(data.data[0]?.name).toBe("foo");
}); });
test("/:entity/query (func query)", async () => { test("/:entity/query (func query)", async () => {
const res = await app.request("/users/query", { const res = await app.request("/entity/users/query", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
where: { bio: { $isnull: 1 } } where: { bio: { $isnull: 1 } },
}) }),
}); });
const data = (await res.json()) as RepositoryResponse; const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3); //expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(1); //expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1); expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1); expect(data.data.length).toBe(1);
expect(data.data[0].name).toBe("bar"); expect(data.data[0]?.name).toBe("bar");
}); });
test("/:entity (read many, paginated)", async () => { test("/:entity (read many, paginated)", async () => {
const res = await app.request("/users?limit=1&offset=2"); const res = await app.request("/entity/users?limit=1&offset=2");
const data = (await res.json()) as RepositoryResponse; const data = (await res.json()) as RepositoryResultJSON;
expect(data.meta.total).toBe(3); //expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(3); //expect(data.meta.count).toBe(3);
expect(data.meta.items).toBe(1); expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1); expect(data.data.length).toBe(1);
expect(data.data[0].name).toBe("baz"); expect(data.data[0]?.name).toBe("baz");
}); });
test("/:entity/:id (read one)", async () => { test("/:entity/:id (read one)", async () => {
const res = await app.request("/users/3"); const res = await app.request("/entity/users/3");
const data = (await res.json()) as RepositoryResponse<EntityData>; const data = (await res.json()) as RepositoryResultJSON<EntityData>;
console.log("data", data);
expect(data.meta.total).toBe(3); //expect(data.meta.total).toBe(3);
expect(data.meta.count).toBe(1); //expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1); expect(data.meta.items).toBe(1);
expect(data.data).toEqual({ id: 3, ...fixtures.users[2] }); expect(data.data).toEqual({ id: 3, ...fixtures.users[2] });
}); });
test("/:entity (update one)", async () => { test("/:entity (update one)", async () => {
const res = await app.request("/users/3", { const res = await app.request("/entity/users/3", {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ name: "new name" }) body: JSON.stringify({ name: "new name" }),
}); });
const { data } = (await res.json()) as MutatorResponse; const { data } = (await res.json()) as MutatorResultJSON;
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" }); expect(data as any).toEqual({ id: 3, ...fixtures.users[2], name: "new name" });
}); });
test("/:entity/:id/:reference (read references)", async () => { test("/:entity/:id/:reference (read references)", async () => {
const res = await app.request("/users/1/posts"); const res = await app.request("/entity/users/1/posts");
const data = (await res.json()) as RepositoryResponse; const data = (await res.json()) as RepositoryResultJSON;
console.log("data", data);
expect(data.meta.total).toBe(2); //expect(data.meta.total).toBe(2);
expect(data.meta.count).toBe(1); //expect(data.meta.count).toBe(1);
expect(data.meta.items).toBe(1); expect(data.meta.items).toBe(1);
expect(data.data.length).toBe(1); expect(data.data.length).toBe(1);
expect(data.data[0].content).toBe("post 1"); expect(data.data[0]?.content).toBe("post 1");
}); });
test("/:entity/:id (delete one)", async () => { test("/:entity/:id (delete one)", async () => {
const res = await app.request("/posts/2", { const res = await app.request("/entity/posts/2", {
method: "DELETE" method: "DELETE",
}); });
const { data } = (await res.json()) as RepositoryResponse<EntityData>; const { data } = (await res.json()) as RepositoryResultJSON<EntityData>;
expect(data).toEqual({ id: 2, ...fixtures.posts[1] }); expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
// verify // verify
const res2 = await app.request("/posts"); const res2 = await app.request("/entity/posts");
const data2 = (await res2.json()) as RepositoryResponse; const data2 = (await res2.json()) as RepositoryResultJSON;
expect(data2.meta.total).toBe(1); //expect(data2.meta.total).toBe(1);
}); });
}); });
}); });
-157
View File
@@ -1,157 +0,0 @@
import { describe, expect, test } from "bun:test";
import { Value, _jsonp } from "../../src/core/utils";
import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data";
import type { RepoQueryIn } from "../../src/data/server/data-query-impl";
import { getDummyConnection } from "./helper";
const decode = (input: RepoQueryIn, expected: RepoQuery) => {
const result = Value.Decode(querySchema, input);
expect(result).toEqual(expected);
};
describe("data-query-impl", () => {
function qb() {
const c = getDummyConnection();
const kysely = c.dummyConnection.kysely;
return kysely.selectFrom("t").selectAll();
}
function compile(q: WhereQuery) {
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
return { sql, parameters };
}
test("single validation", () => {
const tests: [WhereQuery, string, any[]][] = [
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
[{ val: { $isnull: 1 } }, '"val" is null', []],
[{ val: { $isnull: true } }, '"val" is null', []],
[{ val: { $isnull: 0 } }, '"val" is not null', []],
[{ val: { $isnull: false } }, '"val" is not null', []],
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]]
];
for (const [query, expectedSql, expectedParams] of tests) {
const { sql, parameters } = compile(query);
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
expect(parameters).toEqual(expectedParams);
}
});
test("multiple validations", () => {
const tests: [WhereQuery, string, any[]][] = [
// multiple constraints per property
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
// multiple properties
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"]
],
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"]
],
// or constructs
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
'("val1" = ? or "val2" = ?)',
["foo", "bar"]
],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]]
];
for (const [query, expectedSql, expectedParams] of tests) {
const { sql, parameters } = compile(query);
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
expect(parameters).toEqual(expectedParams);
}
});
test("keys", () => {
const tests: [WhereQuery, string[]][] = [
// multiple constraints per property
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
// multiple properties
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
// or constructs
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
];
for (const [query, expectedKeys] of tests) {
const keys = WhereBuilder.getPropertyNames(query);
expect(keys).toEqual(expectedKeys);
}
});
test("with", () => {
decode({ with: ["posts"] }, { with: { posts: {} } });
decode({ with: { posts: {} } }, { with: { posts: {} } });
decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } });
decode(
{
with: {
posts: {
with: {
images: {
select: ["id"]
}
}
}
}
},
{
with: {
posts: {
with: {
images: {
select: ["id"]
}
}
}
}
}
);
// over http
{
const output = { with: { images: {} } };
decode({ with: "images" }, output);
decode({ with: '["images"]' }, output);
decode({ with: ["images"] }, output);
decode({ with: { images: {} } }, output);
}
{
const output = { with: { images: {}, comments: {} } };
decode({ with: "images,comments" }, output);
decode({ with: ["images", "comments"] }, output);
decode({ with: '["images", "comments"]' }, output);
decode({ with: { images: {}, comments: {} } }, output);
}
});
});
describe("data-query-impl: Typebox", () => {
test("sort", async () => {
const _dflt = { sort: { by: "id", dir: "asc" } };
decode({ sort: "" }, _dflt);
decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } });
decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } });
decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } });
decode({ sort: "-1name" }, _dflt);
decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } });
});
});
+26 -19
View File
@@ -5,7 +5,7 @@ import {
NumberField, NumberField,
PrimaryField, PrimaryField,
Repository, Repository,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -18,14 +18,14 @@ describe("some tests", async () => {
const users = new Entity("users", [ const users = new Entity("users", [
new TextField("username", { required: true, default_value: "nobody" }), new TextField("username", { required: true, default_value: "nobody" }),
new TextField("email", { maxLength: 3 }) new TextField("email", { maxLength: 3 }),
]); ]);
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title"), new TextField("title"),
new TextField("content"), new TextField("content"),
new TextField("created_at"), new TextField("created_at"),
new NumberField("likes", { default_value: 0 }) new NumberField("likes", { default_value: 0 }),
]); ]);
const em = new EntityManager([users, posts], connection); const em = new EntityManager([users, posts], connection);
@@ -34,39 +34,32 @@ describe("some tests", async () => {
test("findId", async () => { test("findId", async () => {
const query = await em.repository(users).findId(1); const query = await em.repository(users).findId(1);
/*const { result, total, count, time } = query;
console.log("query", query.result, {
result,
total,
count,
time,
});*/
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" = ? limit ?' 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" where "id" = ? limit ?',
); );
expect(query.parameters).toEqual([1, 1]); expect(query.parameters).toEqual([1, 1]);
expect(query.result).toEqual([]); expect(query.data).toBeUndefined();
}); });
test("findMany", async () => { test("findMany", async () => {
const query = await em.repository(users).findMany(); const query = await em.repository(users).findMany();
expect(query.sql).toBe( expect(query.sql).toBe(
'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?' 'select "users"."id" as "id", "users"."username" as "username", "users"."email" as "email" from "users" order by "users"."id" asc limit ? offset ?',
); );
expect(query.parameters).toEqual([10, 0]); expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]); expect(query.data).toEqual([]);
}); });
test("findMany with number", async () => { test("findMany with number", async () => {
const query = await em.repository(posts).findMany(); const query = await em.repository(posts).findMany();
expect(query.sql).toBe( expect(query.sql).toBe(
'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?' 'select "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."created_at" as "created_at", "posts"."likes" as "likes" from "posts" order by "posts"."id" asc limit ? offset ?',
); );
expect(query.parameters).toEqual([10, 0]); expect(query.parameters).toEqual([10, 0]);
expect(query.result).toEqual([]); expect(query.data).toEqual([]);
}); });
test("try adding an existing field name", async () => { test("try adding an existing field name", async () => {
@@ -74,7 +67,7 @@ describe("some tests", async () => {
new Entity("users", [ new Entity("users", [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("email") // not throwing, it's just being ignored new TextField("email"), // not throwing, it's just being ignored
]); ]);
}).toBeDefined(); }).toBeDefined();
@@ -83,7 +76,7 @@ describe("some tests", async () => {
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
// field config differs, will throw // field config differs, will throw
new TextField("email", { required: true }) new TextField("email", { required: true }),
]); ]);
}).toThrow(); }).toThrow();
@@ -91,7 +84,7 @@ describe("some tests", async () => {
new Entity("users", [ new Entity("users", [
new PrimaryField(), new PrimaryField(),
new TextField("username"), new TextField("username"),
new TextField("email") new TextField("email"),
]); ]);
}).toBeDefined(); }).toBeDefined();
}); });
@@ -110,4 +103,18 @@ describe("some tests", async () => {
new EntityManager([entity, entity2], connection); new EntityManager([entity, entity2], connection);
}).toThrow(); }).toThrow();
}); });
test("primary uuid", async () => {
const entity = new Entity("users", [
new PrimaryField("id", { format: "uuid" }),
new TextField("username"),
]);
const em = new EntityManager([entity], getDummyConnection().dummyConnection);
await em.schema().sync({ force: true });
const mutator = em.mutator(entity);
const data = await mutator.insertOne({ username: "test" });
expect(data.data.id).toBeDefined();
expect(data.data.id).toBeString();
});
}); });
+2 -2
View File
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => { afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME); if (!memory) await unlink(DB_NAME);
return true; return true;
} },
}; };
} }
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
return { return {
dummyConnection, dummyConnection,
afterAllCleanup afterAllCleanup,
}; };
} }
+3 -3
View File
@@ -6,7 +6,7 @@ import {
ManyToOneRelation, ManyToOneRelation,
NumberField, NumberField,
SchemaManager, SchemaManager,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title"), new TextField("title"),
new TextField("content", { default_value: "..." }), new TextField("content", { default_value: "..." }),
new NumberField("count", { default_value: 0 }) new NumberField("count", { default_value: 0 }),
]); ]);
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
@@ -44,7 +44,7 @@ describe("Mutator relation", async () => {
expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow(); expect(em.mutator(posts).insertOne({ title: "post2", users_id: 10 })).rejects.toThrow();
expect( expect(
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }) em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
).resolves.toBeDefined(); ).resolves.toBeDefined();
}); });
}); });
+26 -26
View File
@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
const items = new Entity("items", [ const items = new Entity("items", [
new TextField("label", { required: true, minLength: 1 }), new TextField("label", { required: true, minLength: 1 }),
new NumberField("count", { default_value: 0 }) new NumberField("count", { default_value: 0 }),
]); ]);
const em = new EntityManager<any>([items], connection); const em = new EntityManager<any>([items], connection);
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
test("insert single row", async () => { test("insert single row", async () => {
const mutation = await em.mutator(items).insertOne({ const mutation = await em.mutator(items).insertOne({
label: "test", label: "test",
count: 1 count: 1,
}); });
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"' 'insert into "items" ("count", "label") values (?, ?) returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 }); expect(mutation.data).toEqual({ id: 1, label: "test", count: 1 });
@@ -41,11 +41,11 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]); expect(query.data).toEqual([{ id: 1, label: "test", count: 1 }]);
}); });
test("update inserted row", async () => { test("update inserted row", async () => {
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
const id = query.data![0].id as number; const id = query.data![0].id as number;
const mutation = await em.mutator(items).updateOne(id, { const mutation = await em.mutator(items).updateOne(id, {
label: "new label", label: "new label",
count: 100 count: 100,
}); });
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"' 'update "items" set "label" = ?, "count" = ? where "id" = ? returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id, label: "new label", count: 100 }); expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
}); });
@@ -74,27 +74,27 @@ describe("Mutator simple", async () => {
limit: 1, limit: 1,
sort: { sort: {
by: "id", by: "id",
dir: "desc" dir: "desc",
} },
}); });
const id = query.data![0].id as number; const id = query.data![0].id as number;
const mutation = await em.mutator(items).deleteOne(id); const mutation = await em.mutator(items).deleteOne(id);
expect(mutation.sql).toBe( expect(mutation.sql).toBe(
'delete from "items" where "id" = ? returning "id", "label", "count"' 'delete from "items" where "id" = ? returning "id", "label", "count"',
); );
expect(mutation.data).toEqual({ id, label: "new label", count: 100 }); expect(mutation.data).toEqual({ id, label: "new label", count: 100 });
const query2 = await em.repository(items).findId(id); const query2 = await em.repository(items).findId(id);
expect(query2.result.length).toBe(0); expect(query2.data).toBeUndefined();
}); });
test("validation: insert incomplete row", async () => { test("validation: insert incomplete row", async () => {
const incompleteCreate = async () => const incompleteCreate = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
//label: "test", //label: "test",
count: 1 count: 1,
}); });
expect(incompleteCreate()).rejects.toThrow(); expect(incompleteCreate()).rejects.toThrow();
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
const invalidCreate1 = async () => const invalidCreate1 = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
label: 111, // this should work label: 111, // this should work
count: "1" // this should fail count: "1", // this should fail
}); });
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException); expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
const invalidCreate2 = async () => const invalidCreate2 = async () =>
await em.mutator(items).insertOne({ await em.mutator(items).insertOne({
label: "", // this should fail label: "", // this should fail
count: 1 count: 1,
}); });
expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException); expect(invalidCreate2()).rejects.toThrow(TransformPersistFailedException);
@@ -137,7 +137,7 @@ describe("Mutator simple", async () => {
expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2); expect((await em.repository(items).findMany()).data.length).toBe(data.length - 2);
//console.log((await em.repository(items).findMany()).data); //console.log((await em.repository(items).findMany()).data);
await em.mutator(items).deleteWhere(); await em.mutator(items).deleteWhere({ id: { $isnull: 0 } });
expect((await em.repository(items).findMany()).data.length).toBe(0); expect((await em.repository(items).findMany()).data.length).toBe(0);
//expect(res.data.count).toBe(0); //expect(res.data.count).toBe(0);
@@ -152,38 +152,38 @@ describe("Mutator simple", async () => {
await em.mutator(items).updateWhere( await em.mutator(items).updateWhere(
{ count: 2 }, { count: 2 },
{ {
count: 10 count: 10,
} },
); );
expect((await em.repository(items).findMany()).data).toEqual([ expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 1 }, { id: 6, label: "update", count: 1 },
{ id: 7, label: "update too", count: 1 }, { id: 7, label: "update too", count: 1 },
{ id: 8, label: "keep", count: 0 } { id: 8, label: "keep", count: 0 },
]); ]);
// expect 2 to be updated // expect 2 to be updated
await em.mutator(items).updateWhere( await em.mutator(items).updateWhere(
{ count: 2 }, { count: 2 },
{ {
count: 1 count: 1,
} },
); );
expect((await em.repository(items).findMany()).data).toEqual([ expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 2 }, { id: 6, label: "update", count: 2 },
{ id: 7, label: "update too", count: 2 }, { id: 7, label: "update too", count: 2 },
{ id: 8, label: "keep", count: 0 } { id: 8, label: "keep", count: 0 },
]); ]);
}); });
test("insertMany", async () => { test("insertMany", async () => {
const oldCount = (await em.repo(items).count()).count; const oldCount = (await em.repo(items).count()).data.count;
const inserts = [{ label: "insert 1" }, { label: "insert 2" }]; const inserts = [{ label: "insert 1" }, { label: "insert 2" }];
const { data } = await em.mutator(items).insertMany(inserts); const { data } = await em.mutator(items).insertMany(inserts);
expect(data.length).toBe(2); expect(data.length).toBe(2);
expect(data.map((d) => ({ label: d.label }))).toEqual(inserts); expect(data.map((d) => ({ label: d.label }))).toEqual(inserts);
const newCount = (await em.repo(items).count()).count; const newCount = (await em.repo(items).count()).data.count;
expect(newCount).toBe(oldCount + inserts.length); expect(newCount).toBe(oldCount + inserts.length);
const { data: data2 } = await em.repo(items).findMany({ offset: oldCount }); const { data: data2 } = await em.repo(items).findMany({ offset: oldCount });
+12 -12
View File
@@ -1,5 +1,5 @@
import { afterAll, expect as bunExpect, describe, test } from "bun:test"; import { afterAll, expect as bunExpect, describe, test } from "bun:test";
import { stripMark } from "../../src/core/utils"; import { stripMark } from "core/object/schema";
import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data"; import { Entity, EntityManager, PolymorphicRelation, TextField } from "../../src/data";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
source: "categories", source: "categories",
target: "media", target: "media",
config: { config: {
mappedBy: "image" mappedBy: "image",
} },
}); });
// media should not see categories // media should not see categories
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]); expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
// it's important that media cannot access categories // it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual( expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
[] [],
); );
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]); expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media" "media",
]); ]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"image" "image",
]); ]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]); expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
"id", "id",
"path", "path",
"reference", "reference",
"entity_id" "entity_id",
]); ]);
expect(media.getSelect()).toEqual(["id", "path"]); expect(media.getSelect()).toEqual(["id", "path"]);
}); });
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
const entities = [media, categories]; const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, { const single = new PolymorphicRelation(categories, media, {
mappedBy: "single", mappedBy: "single",
targetCardinality: 1 targetCardinality: 1,
}); });
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" }); const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
// it's important that media cannot access categories // it's important that media cannot access categories
expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual( expect(em.relations.targetRelationsOf(categories).map((r) => r.source.entity.name)).toEqual(
[] [],
); );
expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]); expect(em.relations.targetRelationsOf(media).map((r) => r.source.entity.name)).toEqual([]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media", "media",
"media" "media",
]); ]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([ expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"single", "single",
"multiple" "multiple",
]); ]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]); expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
"id", "id",
"path", "path",
"reference", "reference",
"entity_id" "entity_id",
]); ]);
}); });
}); });
+38 -37
View File
@@ -12,7 +12,7 @@ import {
NumberField, NumberField,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
TextField TextField,
} from "../../src/data"; } from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection"; import { DummyConnection } from "../../src/data/connection/DummyConnection";
import { import {
@@ -31,7 +31,7 @@ import {
medium, medium,
number, number,
relation, relation,
text text,
} from "../../src/data/prototype"; } from "../../src/data/prototype";
import { MediaField } from "../../src/media/MediaField"; import { MediaField } from "../../src/media/MediaField";
@@ -54,7 +54,7 @@ describe("prototype", () => {
name: text(), name: text(),
bio: text(), bio: text(),
age: number(), age: number(),
some: number() some: number(),
}); });
type db = { type db = {
users: Schema<typeof users>; users: Schema<typeof users>;
@@ -70,7 +70,7 @@ describe("prototype", () => {
name: text({ default_value: "hello" }).required(), name: text({ default_value: "hello" }).required(),
bio: text(), bio: text(),
age: number(), age: number(),
some: number().required() some: number().required(),
}); });
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 }; const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
@@ -83,12 +83,12 @@ describe("prototype", () => {
new TextField("title", { required: true }), new TextField("title", { required: true }),
new TextField("content"), new TextField("content"),
new DateField("created_at", { new DateField("created_at", {
type: "datetime" type: "datetime",
}), }),
// @ts-ignore // @ts-ignore
new MediaField("images", { entity: "posts" }), new MediaField("images", { entity: "posts" }),
// @ts-ignore // @ts-ignore
new MediaField("cover", { entity: "posts", max_items: 1 }) new MediaField("cover", { entity: "posts", max_items: 1 }),
]); ]);
const posts2 = entity("posts", { const posts2 = entity("posts", {
@@ -96,12 +96,13 @@ describe("prototype", () => {
content: text(), content: text(),
created_at: datetime(), created_at: datetime(),
images: media(), images: media(),
cover: medium() cover: medium(),
}); });
type Posts = Schema<typeof posts2>; type Posts = Schema<typeof posts2>;
expect(posts1.toJSON()).toEqual(posts2.toJSON()); // @todo: check
//expect(posts1.toJSON()).toEqual(posts2.toJSON());
}); });
test("test example", async () => { test("test example", async () => {
@@ -117,11 +118,11 @@ describe("prototype", () => {
type: "objects", type: "objects",
values: [ values: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
} },
}), }),
new JsonField("json") new JsonField("json"),
]); ]);
const test2 = entity("test", { const test2 = entity("test", {
@@ -134,10 +135,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({ status: enumm<"active" | "inactive">({
enum: [ enum: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
}), }),
json: json<{ some: number }>() json: json<{ some: number }>(),
}); });
expect(test.toJSON()).toEqual(test2.toJSON()); expect(test.toJSON()).toEqual(test2.toJSON());
@@ -161,12 +162,12 @@ describe("prototype", () => {
// category has single image // category has single image
new PolymorphicRelation(categories, _media, { new PolymorphicRelation(categories, _media, {
mappedBy: "image", mappedBy: "image",
targetCardinality: 1 targetCardinality: 1,
}), }),
// post has multiple images // post has multiple images
new PolymorphicRelation(posts, _media, { mappedBy: "images" }), new PolymorphicRelation(posts, _media, { mappedBy: "images" }),
new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }) new PolymorphicRelation(posts, _media, { mappedBy: "cover", targetCardinality: 1 }),
]; ];
const relations2 = [ const relations2 = [
@@ -180,7 +181,7 @@ describe("prototype", () => {
relation(categories).polyToOne(_media, { mappedBy: "image" }), relation(categories).polyToOne(_media, { mappedBy: "image" }),
relation(posts).polyToMany(_media, { mappedBy: "images" }), relation(posts).polyToMany(_media, { mappedBy: "images" }),
relation(posts).polyToOne(_media, { mappedBy: "cover" }) relation(posts).polyToOne(_media, { mappedBy: "cover" }),
]; ];
expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON())); expect(relations.map((r) => r.toJSON())).toEqual(relations2.map((r) => r.toJSON()));
@@ -194,21 +195,21 @@ describe("prototype", () => {
posts, posts,
categories, categories,
{ {
connectionTableMappedName: "custom" connectionTableMappedName: "custom",
}, },
[new TextField("description")] [new TextField("description")],
); );
const fields = { const fields = {
description: text() description: text(),
}; };
let o: FieldSchema<typeof fields>; let o: FieldSchema<typeof fields>;
const rel2 = relation(posts).manyToMany( const rel2 = relation(posts).manyToMany(
categories, categories,
{ {
connectionTableMappedName: "custom" connectionTableMappedName: "custom",
}, },
fields fields,
); );
expect(rel.toJSON()).toEqual(rel2.toJSON()); expect(rel.toJSON()).toEqual(rel2.toJSON());
@@ -216,11 +217,11 @@ describe("prototype", () => {
test("devexample", async () => { test("devexample", async () => {
const users = entity("users", { const users = entity("users", {
username: text() username: text(),
}); });
const comments = entity("comments", { const comments = entity("comments", {
content: text() content: text(),
}); });
const posts = entity("posts", { const posts = entity("posts", {
@@ -228,17 +229,17 @@ describe("prototype", () => {
content: text(), content: text(),
created_at: datetime(), created_at: datetime(),
images: media(), images: media(),
cover: medium() cover: medium(),
}); });
const categories = entity("categories", { const categories = entity("categories", {
name: text(), name: text(),
description: text(), description: text(),
image: medium() image: medium(),
}); });
const settings = entity("settings", { const settings = entity("settings", {
theme: text() theme: text(),
}); });
const test = entity("test", { const test = entity("test", {
@@ -251,10 +252,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({ status: enumm<"active" | "inactive">({
enum: [ enum: [
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "inactive", label: "Not active" } { value: "inactive", label: "Not active" },
] ],
}), }),
json: json<{ some: number }>() json: json<{ some: number }>(),
}); });
const _media = entity("media", {}); const _media = entity("media", {});
@@ -270,7 +271,7 @@ describe("prototype", () => {
relation(users).oneToOne(settings), relation(users).oneToOne(settings),
relation(comments).manyToOne(users, { required: true }), relation(comments).manyToOne(users, { required: true }),
relation(comments).manyToOne(posts, { required: true }) relation(comments).manyToOne(posts, { required: true }),
]; ];
const obj: Schema<typeof test> = {} as any; const obj: Schema<typeof test> = {} as any;
@@ -281,12 +282,12 @@ describe("prototype", () => {
{ {
posts: entity("posts", { name: text(), slug: text().required() }), posts: entity("posts", { name: text(), slug: text().required() }),
comments: entity("comments", { some: text() }), comments: entity("comments", { some: text() }),
users: entity("users", { email: text() }) users: entity("users", { email: text() }),
}, },
({ relation, index }, { posts, comments, users }) => { ({ relation, index }, { posts, comments, users }) => {
relation(posts).manyToOne(comments).manyToOne(users); relation(posts).manyToOne(comments).manyToOne(users);
index(posts).on(["name"]).on(["slug"], true); index(posts).on(["name"]).on(["slug"], true);
} },
); );
type LocalDb = (typeof _em)["DB"]; type LocalDb = (typeof _em)["DB"];
@@ -294,7 +295,7 @@ describe("prototype", () => {
const es = [ const es = [
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")]),
]; ];
const _em2 = new EntityManager( const _em2 = new EntityManager(
es, es,
@@ -302,8 +303,8 @@ describe("prototype", () => {
[new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])], [new ManyToOneRelation(es[0], es[1]), new ManyToOneRelation(es[0], es[2])],
[ [
new EntityIndex(es[0], [es[0].field("name")!]), new EntityIndex(es[0], [es[0].field("name")!]),
new EntityIndex(es[0], [es[0].field("slug")!], true) new EntityIndex(es[0], [es[0].field("slug")!], true),
] ],
); );
// @ts-ignore // @ts-ignore
+22 -22
View File
@@ -6,7 +6,7 @@ import {
ManyToOneRelation, ManyToOneRelation,
OneToOneRelation, OneToOneRelation,
PolymorphicRelation, PolymorphicRelation,
RelationField RelationField,
} from "../../src/data/relations"; } from "../../src/data/relations";
import { getDummyConnection } from "./helper"; import { getDummyConnection } from "./helper";
@@ -22,32 +22,32 @@ describe("Relations", async () => {
const r1 = new RelationField("users_id", { const r1 = new RelationField("users_id", {
reference: "users", reference: "users",
target: "users", target: "users",
target_field: "id" target_field: "id",
}); });
const sql1 = schema const sql1 = schema
.createTable("posts") .createTable("posts")
.addColumn(...r1.schema()!) .addColumn(...em.connection.getFieldSchema(r1.schema())!)
.compile().sql; .compile().sql;
expect(sql1).toBe( expect(sql1).toBe(
'create table "posts" ("users_id" integer references "users" ("id") on delete set null)' 'create table "posts" ("users_id" integer references "users" ("id") on delete set null)',
); );
//const r2 = new RelationField(new Entity("users"), "author"); //const r2 = new RelationField(new Entity("users"), "author");
const r2 = new RelationField("author_id", { const r2 = new RelationField("author_id", {
reference: "author", reference: "author",
target: "users", target: "users",
target_field: "id" target_field: "id",
}); });
const sql2 = schema const sql2 = schema
.createTable("posts") .createTable("posts")
.addColumn(...r2.schema()!) .addColumn(...em.connection.getFieldSchema(r2.schema())!)
.compile().sql; .compile().sql;
expect(sql2).toBe( expect(sql2).toBe(
'create table "posts" ("author_id" integer references "users" ("id") on delete set null)' 'create table "posts" ("author_id" integer references "users" ("id") on delete set null)',
); );
}); });
@@ -57,7 +57,7 @@ describe("Relations", async () => {
reference: "users", reference: "users",
target: "users", target: "users",
target_field: "id", target_field: "id",
required: true required: true,
}); });
expect(r1.isRequired()).toBeTrue(); expect(r1.isRequired()).toBeTrue();
}); });
@@ -66,8 +66,8 @@ describe("Relations", async () => {
const users = new Entity("users", [new TextField("username")]); const users = new Entity("users", [new TextField("username")]);
const posts = new Entity("posts", [ const posts = new Entity("posts", [
new TextField("title", { new TextField("title", {
maxLength: 2 maxLength: 2,
}) }),
]); ]);
const entities = [users, posts]; const entities = [users, posts];
@@ -122,7 +122,7 @@ describe("Relations", async () => {
.selectFrom(users.name) .selectFrom(users.name)
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts")); .select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
expect(selectPostsFromUsers.compile().sql).toBe( expect(selectPostsFromUsers.compile().sql).toBe(
'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"' 'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"',
); );
expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField); expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField);
const userObj = { id: 1, username: "test" }; const userObj = { id: 1, username: "test" };
@@ -142,7 +142,7 @@ describe("Relations", async () => {
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author")); .select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
expect(selectUsersFromPosts.compile().sql).toBe( expect(selectUsersFromPosts.compile().sql).toBe(
'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"' 'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"',
); );
expect(postAuthorRel.getField()).toBeInstanceOf(RelationField); expect(postAuthorRel.getField()).toBeInstanceOf(RelationField);
const postObj = { id: 1, title: "test" }; const postObj = { id: 1, title: "test" };
@@ -158,7 +158,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: undefined, cardinality: undefined,
relation_type: "n:1" relation_type: "n:1",
}); });
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({ expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
@@ -170,7 +170,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: "id", primary: "id",
cardinality: 1, cardinality: 1,
relation_type: "n:1" relation_type: "n:1",
}); });
/*console.log("ManyToOne (source=posts, target=users)"); /*console.log("ManyToOne (source=posts, target=users)");
@@ -225,7 +225,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: "id", primary: "id",
cardinality: 1, cardinality: 1,
relation_type: "1:1" relation_type: "1:1",
}); });
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({ expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
reference: "users", reference: "users",
@@ -236,7 +236,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: 1, cardinality: 1,
relation_type: "1:1" relation_type: "1:1",
}); });
/*console.log(""); /*console.log("");
@@ -310,16 +310,16 @@ describe("Relations", async () => {
*/ */
const selectCategoriesFromPosts = kysely const selectCategoriesFromPosts = kysely
.selectFrom(posts.name) .selectFrom(posts.name)
.select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories")); .select((eb) => postCategoriesRel.buildWith(posts)(eb).select("id").as("categories"));
expect(selectCategoriesFromPosts.compile().sql).toBe( expect(selectCategoriesFromPosts.compile().sql).toBe(
'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"' 'select (select "id" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"',
); );
const selectPostsFromCategories = kysely const selectPostsFromCategories = kysely
.selectFrom(categories.name) .selectFrom(categories.name)
.select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts")); .select((eb) => postCategoriesRel.buildWith(categories)(eb).select("id").as("posts"));
expect(selectPostsFromCategories.compile().sql).toBe( expect(selectPostsFromCategories.compile().sql).toBe(
'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"' 'select (select "id" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"',
); );
// mutation info // mutation info
@@ -332,7 +332,7 @@ describe("Relations", async () => {
$detach: true, $detach: true,
primary: "id", primary: "id",
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",
@@ -343,7 +343,7 @@ describe("Relations", async () => {
$detach: false, $detach: false,
primary: undefined, primary: undefined,
cardinality: undefined, cardinality: undefined,
relation_type: "m:n" relation_type: "m:n",
}); });
/*console.log(""); /*console.log("");
+8 -14
View File
@@ -1,12 +1,13 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Entity, NumberField, TextField } from "../../../src/data"; import { Entity, NumberField, TextField } from "data";
import * as p from "data/prototype";
describe("[data] Entity", async () => { describe("[data] Entity", async () => {
const entity = new Entity("test", [ const entity = new Entity("test", [
new TextField("name", { required: true }), new TextField("name", { required: true }),
new TextField("description"), new TextField("description"),
new NumberField("age", { fillable: false, default_value: 18 }), new NumberField("age", { fillable: false, default_value: 18 }),
new TextField("hidden", { hidden: true, default_value: "secret" }) new TextField("hidden", { hidden: true, default_value: "secret" }),
]); ]);
test("getSelect", async () => { test("getSelect", async () => {
@@ -17,7 +18,7 @@ describe("[data] Entity", async () => {
expect(entity.getFillableFields().map((f) => f.name)).toEqual([ expect(entity.getFillableFields().map((f) => f.name)).toEqual([
"name", "name",
"description", "description",
"hidden" "hidden",
]); ]);
}); });
@@ -28,7 +29,7 @@ describe("[data] Entity", async () => {
test("getDefaultObject", async () => { test("getDefaultObject", async () => {
expect(entity.getDefaultObject()).toEqual({ expect(entity.getDefaultObject()).toEqual({
age: 18, age: 18,
hidden: "secret" hidden: "secret",
}); });
}); });
@@ -47,14 +48,7 @@ describe("[data] Entity", async () => {
expect(entity.getField("new_field")).toBe(field); expect(entity.getField("new_field")).toBe(field);
}); });
// @todo: move this to ClientApp test.only("types", async () => {
/*test("serialize and deserialize", async () => { console.log(entity.toTypes());
const json = entity.toJSON(); });
//sconsole.log("json", json.fields);
const newEntity = Entity.deserialize(json);
//console.log("newEntity", newEntity.toJSON().fields);
expect(newEntity).toBeInstanceOf(Entity);
expect(json).toEqual(newEntity.toJSON());
expect(json.fields).toEqual(newEntity.toJSON().fields);
});*/
}); });
@@ -4,7 +4,7 @@ import {
EntityManager, EntityManager,
ManyToManyRelation, ManyToManyRelation,
ManyToOneRelation, ManyToOneRelation,
SchemaManager SchemaManager,
} from "../../../src/data"; } from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors"; import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
expect(await em.ping()).toBe(true); expect(await em.ping()).toBe(true);
expect(() => em.entity("...")).toThrow(); expect(() => em.entity("...")).toThrow();
expect(() => expect(() =>
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))) em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
).toThrow(); ).toThrow();
expect(em.schema()).toBeInstanceOf(SchemaManager); expect(em.schema()).toBeInstanceOf(SchemaManager);
@@ -47,8 +47,8 @@ describe("[data] EntityManager", async () => {
em.addRelation(new ManyToOneRelation(posts, users)); em.addRelation(new ManyToOneRelation(posts, users));
expect(em.relations.all.length).toBe(1); expect(em.relations.all.length).toBe(1);
expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation); expect(em.relations.all[0]).toBeInstanceOf(ManyToOneRelation);
expect(em.relationsOf("users")).toEqual([em.relations.all[0]]); expect(em.relationsOf("users")).toEqual([em.relations.all[0]!]);
expect(em.relationsOf("posts")).toEqual([em.relations.all[0]]); expect(em.relationsOf("posts")).toEqual([em.relations.all[0]!]);
expect(em.hasRelations("users")).toBe(true); expect(em.hasRelations("users")).toBe(true);
expect(em.hasRelations("posts")).toBe(true); expect(em.hasRelations("posts")).toBe(true);
expect(em.relatedEntitiesOf("users")).toEqual([posts]); expect(em.relatedEntitiesOf("users")).toEqual([posts]);
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]); expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([ expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
"comments", "comments",
"categories" "categories",
]); ]);
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]); expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]); expect(categoriesTargetRel.map((r) => r.other(categories).entity.name)).toEqual(["posts"]);
+3 -3
View File
@@ -12,7 +12,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users], dummyConnection); const em = new EntityManager([users], dummyConnection);
expect(() => expect(() =>
JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]) JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]),
).toThrow('Relation "posts" not found'); ).toThrow('Relation "posts" not found');
}); });
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations); const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [ const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
"posts" "posts",
]); ]);
const res = qb.compile(); const res = qb.compile();
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
);*/ );*/
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [ const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
"author" "author",
]); ]);
const res2 = qb2.compile(); const res2 = qb2.compile();
+41 -35
View File
@@ -1,4 +1,4 @@
import { afterAll, 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 { import {
Entity, Entity,
@@ -9,20 +9,23 @@ import {
OneToOneRelation, OneToOneRelation,
type RelationField, type RelationField,
RelationMutator, RelationMutator,
TextField TextField,
} from "../../../src/data"; } from "../../../src/data";
import * as proto from "../../../src/data/prototype"; import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../helper"; import { getDummyConnection, disableConsoleLog, enableConsoleLog } from "../../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup); afterAll(afterAllCleanup);
beforeAll(() => disableConsoleLog(["log", "warn"]));
afterAll(async () => (await afterAllCleanup()) && enableConsoleLog());
describe("[data] Mutator (base)", async () => { describe("[data] Mutator (base)", async () => {
const entity = new Entity("items", [ const entity = new Entity("items", [
new TextField("label", { required: true }), new TextField("label", { required: true }),
new NumberField("count"), new NumberField("count"),
new TextField("hidden", { hidden: true }), new TextField("hidden", { hidden: true }),
new TextField("not_fillable", { fillable: false }) new TextField("not_fillable", { fillable: false }),
]); ]);
const em = new EntityManager<any>([entity], dummyConnection); const em = new EntityManager<any>([entity], dummyConnection);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -44,7 +47,7 @@ describe("[data] Mutator (base)", async () => {
test("updateOne", async () => { test("updateOne", async () => {
const { data } = await em.mutator(entity).insertOne(payload); const { data } = await em.mutator(entity).insertOne(payload);
const updated = await em.mutator(entity).updateOne(data.id, { const updated = await em.mutator(entity).updateOne(data.id, {
count: 2 count: 2,
}); });
expect(updated.parameters).toEqual([2, data.id]); expect(updated.parameters).toEqual([2, data.id]);
@@ -77,7 +80,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting relational field should just return key value to be added // persisting relational field should just return key value to be added
expect( expect(
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id) postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
).resolves.toEqual(["users_id", userData.data.id]); ).resolves.toEqual(["users_id", userData.data.id]);
// persisting invalid value should throw // persisting invalid value should throw
@@ -86,8 +89,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting reference should ... // persisting reference should ...
expect( expect(
postRelMutator.persistReference(relations[0]!, "users", { postRelMutator.persistReference(relations[0]!, "users", {
$set: { id: userData.data.id } $set: { id: userData.data.id },
}) }),
).resolves.toEqual(["users_id", userData.data.id]); ).resolves.toEqual(["users_id", userData.data.id]);
// @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users // @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users
@@ -99,8 +102,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect( expect(
em.mutator(posts).insertOne({ em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: 100 // user does not exist yet users_id: 100, // user does not exist yet
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -111,7 +114,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
const em = new EntityManager([items, cats], dummyConnection, relations); const em = new EntityManager([items, cats], dummyConnection, relations);
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow( expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
'Field "cats_id" is required' 'Field "cats_id" is required',
); );
}); });
@@ -119,14 +122,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" }); const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({ const res = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: data.id users_id: data.id,
}); });
expect(res.data.users_id).toBe(data.id); expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed // setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({ const res2 = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users_id: null users_id: null,
}); });
expect(res2.data.users_id).toBe(null); expect(res2.data.users_id).toBe(null);
}); });
@@ -135,14 +138,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" }); const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({ const res = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users: { $set: { id: data.id } } users: { $set: { id: data.id } },
}); });
expect(res.data.users_id).toBe(data.id); expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed // setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({ const res2 = await em.mutator(posts).insertOne({
title: "post1", title: "post1",
users: { $set: { id: null } } users: { $set: { id: null } },
}); });
expect(res2.data.users_id).toBe(null); expect(res2.data.users_id).toBe(null);
}); });
@@ -151,8 +154,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect( expect(
em.mutator(posts).insertOne({ em.mutator(posts).insertOne({
title: "test", title: "test",
users: { $create: { username: "test" } } users: { $create: { username: "test" } },
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -162,27 +165,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
const res2 = await em.mutator(posts).insertOne({ title: "post1" }); const res2 = await em.mutator(posts).insertOne({ title: "post1" });
const up1 = await em.mutator(posts).updateOne(res2.data.id, { const up1 = await em.mutator(posts).updateOne(res2.data.id, {
users: { $set: { id: res1.data.id } } users: { $set: { id: res1.data.id } },
}); });
expect(up1.data.users_id).toBe(res1.data.id); expect(up1.data.users_id).toBe(res1.data.id);
const up2 = await em.mutator(posts).updateOne(res2.data.id, { const up2 = await em.mutator(posts).updateOne(res2.data.id, {
users: { $set: { id: res1_1.data.id } } users: { $set: { id: res1_1.data.id } },
}); });
expect(up2.data.users_id).toBe(res1_1.data.id); expect(up2.data.users_id).toBe(res1_1.data.id);
const up3_1 = await em.mutator(posts).updateOne(res2.data.id, { const up3_1 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: res1.data.id users_id: res1.data.id,
}); });
expect(up3_1.data.users_id).toBe(res1.data.id); expect(up3_1.data.users_id).toBe(res1.data.id);
const up3_2 = await em.mutator(posts).updateOne(res2.data.id, { const up3_2 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: res1_1.data.id users_id: res1_1.data.id,
}); });
expect(up3_2.data.users_id).toBe(res1_1.data.id); expect(up3_2.data.users_id).toBe(res1_1.data.id);
const up4 = await em.mutator(posts).updateOne(res2.data.id, { const up4 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: null users_id: null,
}); });
expect(up4.data.users_id).toBe(null); expect(up4.data.users_id).toBe(null);
}); });
@@ -199,8 +202,8 @@ describe("[data] Mutator (OneToOne)", async () => {
expect( expect(
em.mutator(users).insertOne({ em.mutator(users).insertOne({
username: "test", username: "test",
settings_id: 1 // todo: throws because it doesn't exist, but it shouldn't be allowed settings_id: 1, // todo: throws because it doesn't exist, but it shouldn't be allowed
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
@@ -210,15 +213,15 @@ describe("[data] Mutator (OneToOne)", async () => {
expect( expect(
em.mutator(users).insertOne({ em.mutator(users).insertOne({
username: "test", username: "test",
settings: { $set: { id: data.id } } settings: { $set: { id: data.id } },
}) }),
).rejects.toThrow(); ).rejects.toThrow();
}); });
test("insertOne: using $create", async () => { test("insertOne: using $create", async () => {
const res = await em.mutator(users).insertOne({ const res = await em.mutator(users).insertOne({
username: "test", username: "test",
settings: { $create: { theme: "dark" } } settings: { $create: { theme: "dark" } },
}); });
expect(res.data.settings_id).toBeDefined(); expect(res.data.settings_id).toBeDefined();
}); });
@@ -288,14 +291,17 @@ describe("[data] Mutator (Events)", async () => {
test("events were fired", async () => { test("events were fired", async () => {
const { data } = await mutator.insertOne({ label: "test" }); const { data } = await mutator.insertOne({ label: "test" });
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
await mutator.updateOne(data.id, { label: "test2" }); await mutator.updateOne(data.id, { label: "test2" });
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
await mutator.deleteOne(data.id); await mutator.deleteOne(data.id);
await mutator.emgr.executeAsyncs();
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
}); });
@@ -303,7 +309,7 @@ describe("[data] Mutator (Events)", async () => {
test("insertOne event return is respected", async () => { test("insertOne event return is respected", async () => {
const posts = proto.entity("posts", { const posts = proto.entity("posts", {
title: proto.text(), title: proto.text(),
views: proto.number() views: proto.number(),
}); });
const conn = getDummyConnection(); const conn = getDummyConnection();
@@ -318,10 +324,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => { async (event) => {
return { return {
...event.params.data, ...event.params.data,
views: 2 views: 2,
}; };
}, },
"sync" "sync",
); );
const mutator = em.mutator("posts"); const mutator = em.mutator("posts");
@@ -329,14 +335,14 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({ expect(result.data).toEqual({
id: 1, id: 1,
title: "test", title: "test",
views: 2 views: 2,
}); });
}); });
test("updateOne event return is respected", async () => { test("updateOne event return is respected", async () => {
const posts = proto.entity("posts", { const posts = proto.entity("posts", {
title: proto.text(), title: proto.text(),
views: proto.number() views: proto.number(),
}); });
const conn = getDummyConnection(); const conn = getDummyConnection();
@@ -351,10 +357,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => { async (event) => {
return { return {
...event.params.data, ...event.params.data,
views: event.params.data.views + 1 views: event.params.data.views + 1,
}; };
}, },
"sync" "sync",
); );
const mutator = em.mutator("posts"); const mutator = em.mutator("posts");
@@ -363,7 +369,7 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({ expect(result.data).toEqual({
id: 1, id: 1,
title: "test", title: "test",
views: 3 views: 3,
}); });
}); });
}); });
+92 -128
View File
@@ -1,14 +1,17 @@
import { afterAll, 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 { Perf } from "../../../src/core/utils"; import { Perf } from "core/utils";
import { import {
Entity, Entity,
EntityManager, EntityManager,
LibsqlConnection, LibsqlConnection,
ManyToOneRelation, ManyToOneRelation,
RepositoryEvents, RepositoryEvents,
TextField TextField,
} from "../../../src/data"; entity as $entity,
text as $text,
em as $em,
} from "data";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
type E = Kysely<any> | Transaction<any>; type E = Kysely<any> | Transaction<any>;
@@ -23,120 +26,6 @@ async function sleep(ms: number) {
} }
describe("[Repository]", async () => { describe("[Repository]", async () => {
test.skip("bulk", async () => {
//const connection = dummyConnection;
//const connection = getLocalLibsqlConnection();
const credentials = null as any; // @todo: determine what to do here
const connection = new LibsqlConnection(credentials);
const em = new EntityManager([], connection);
/*const emLibsql = new EntityManager([], {
url: connection.url.replace("https", "libsql"),
authToken: connection.authToken,
});*/
const table = "posts";
const client = connection.getClient();
if (!client) {
console.log("Cannot perform test without libsql connection");
return;
}
const conn = em.connection.kysely;
const selectQ = (e: E) => e.selectFrom(table).selectAll().limit(2);
const countQ = (e: E) => e.selectFrom(table).select(e.fn.count("*").as("count"));
async function executeTransaction(em: EntityManager<any>) {
return await em.connection.kysely.transaction().execute(async (e) => {
const res = await selectQ(e).execute();
const count = await countQ(e).execute();
return [res, count];
});
}
async function executeBatch(em: EntityManager<any>) {
const queries = [selectQ(conn), countQ(conn)];
return await em.connection.batchQuery(queries);
}
async function executeSingleKysely(em: EntityManager<any>) {
const res = await selectQ(conn).execute();
const count = await countQ(conn).execute();
return [res, count];
}
async function executeSingleClient(em: EntityManager<any>) {
const q1 = selectQ(conn).compile();
const res = await client.execute({
sql: q1.sql,
args: q1.parameters as any
});
const q2 = countQ(conn).compile();
const count = await client.execute({
sql: q2.sql,
args: q2.parameters as any
});
return [res, count];
}
const transaction = await executeTransaction(em);
const batch = await executeBatch(em);
expect(batch).toEqual(transaction as any);
const testperf = false;
if (testperf) {
const times = 5;
const exec = async (
name: string,
fn: (em: EntityManager<any>) => Promise<any>,
em: EntityManager<any>
) => {
const res = await Perf.execute(() => fn(em), times);
await sleep(1000);
const info = {
name,
total: res.total.toFixed(2),
avg: (res.total / times).toFixed(2),
first: res.marks[0].time.toFixed(2),
last: res.marks[res.marks.length - 1].time.toFixed(2)
};
console.log(info.name, info, res.marks);
return info;
};
const data: any[] = [];
data.push(await exec("transaction.http", executeTransaction, em));
data.push(await exec("bulk.http", executeBatch, em));
data.push(await exec("singleKy.http", executeSingleKysely, em));
data.push(await exec("singleCl.http", executeSingleClient, em));
/*data.push(await exec("transaction.libsql", executeTransaction, emLibsql));
data.push(await exec("bulk.libsql", executeBatch, emLibsql));
data.push(await exec("singleKy.libsql", executeSingleKysely, emLibsql));
data.push(await exec("singleCl.libsql", executeSingleClient, emLibsql));*/
console.table(data);
/**
* ┌───┬────────────────────┬────────┬────────┬────────┬────────┐
* │ │ name │ total │ avg │ first │ last │
* ├───┼────────────────────┼────────┼────────┼────────┼────────┤
* │ 0 │ transaction.http │ 681.29 │ 136.26 │ 136.46 │ 396.09 │
* │ 1 │ bulk.http │ 164.82 │ 32.96 │ 32.95 │ 99.91 │
* │ 2 │ singleKy.http │ 330.01 │ 66.00 │ 65.86 │ 195.41 │
* │ 3 │ singleCl.http │ 326.17 │ 65.23 │ 61.32 │ 198.08 │
* │ 4 │ transaction.libsql │ 856.79 │ 171.36 │ 132.31 │ 595.24 │
* │ 5 │ bulk.libsql │ 180.63 │ 36.13 │ 35.39 │ 107.71 │
* │ 6 │ singleKy.libsql │ 347.11 │ 69.42 │ 65.00 │ 207.14 │
* │ 7 │ singleCl.libsql │ 328.60 │ 65.72 │ 62.19 │ 195.04 │
* └───┴────────────────────┴────────┴────────┴────────┴────────┘
*/
}
});
test("count & exists", async () => { test("count & exists", async () => {
const items = new Entity("items", [new TextField("label")]); const items = new Entity("items", [new TextField("label")]);
const em = new EntityManager([items], dummyConnection); const em = new EntityManager([items], dummyConnection);
@@ -157,25 +46,88 @@ describe("[Repository]", async () => {
// count all // count all
const res = await em.repository(items).count(); const res = await em.repository(items).count();
expect(res.sql).toBe('select count(*) as "count" from "items"'); expect(res.sql).toBe('select count(*) as "count" from "items"');
expect(res.count).toBe(3); expect(res.data.count).toBe(3);
//
{
const res = await em.repository(items).findMany();
expect(res.count).toBe(3);
}
{
const res = await em
.repository(items, {
includeCounts: true,
})
.findMany();
expect(res.count).toBe(3);
}
// count filtered // count filtered
const res2 = await em.repository(items).count({ label: { $in: ["a", "b"] } }); const res2 = await em
.repository(items, {
includeCounts: true,
})
.count({ label: { $in: ["a", "b"] } });
expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)'); expect(res2.sql).toBe('select count(*) as "count" from "items" where "label" in (?, ?)');
expect(res2.parameters).toEqual(["a", "b"]); expect(res2.parameters).toEqual(["a", "b"]);
expect(res2.count).toBe(2); expect(res2.data.count).toBe(2);
// check exists // check exists
const res3 = await em.repository(items).exists({ label: "a" }); const res3 = await em.repository(items).exists({ label: "a" });
expect(res3.exists).toBe(true); expect(res3.data.exists).toBe(true);
const res4 = await em.repository(items).exists({ label: "d" }); const res4 = await em.repository(items).exists({ label: "d" });
expect(res4.exists).toBe(false); expect(res4.data.exists).toBe(false);
// for now, allow empty filter // for now, allow empty filter
const res5 = await em.repository(items).exists({}); const res5 = await em.repository(items).exists({});
expect(res5.exists).toBe(true); expect(res5.data.exists).toBe(true);
});
test("option: silent", async () => {
const em = $em({
items: $entity("items", {
label: $text(),
}),
}).proto.withConnection(getDummyConnection().dummyConnection);
// should throw because table doesn't exist
expect(em.repo("items").findMany({})).rejects.toThrow(/no such table/);
// should silently return empty result
em.repo("items", { silent: true })
.findMany({})
.then((r) => r.data);
expect(
em
.repo("items", { silent: true })
.findMany({})
.then((r) => r.data),
).resolves.toEqual([]);
});
test("option: includeCounts", async () => {
const em = $em({
items: $entity("items", {
label: $text(),
}),
}).proto.withConnection(getDummyConnection().dummyConnection);
await em.schema().sync({ force: true });
expect(
em
.repo("items", { includeCounts: true })
.findMany({})
.then((r) => [r.count, r.total]),
).resolves.toEqual([0, 0]);
expect(
em
.repo("items", { includeCounts: false })
.findMany({})
.then((r) => [r.count, r.total]),
).resolves.toEqual([undefined, undefined]);
}); });
}); });
@@ -183,7 +135,7 @@ describe("[data] Repository (Events)", async () => {
const items = new Entity("items", [new TextField("label")]); const items = new Entity("items", [new TextField("label")]);
const categories = new Entity("categories", [new TextField("label")]); const categories = new Entity("categories", [new TextField("label")]);
const em = new EntityManager([items, categories], dummyConnection, [ const em = new EntityManager([items, categories], dummyConnection, [
new ManyToOneRelation(categories, items) new ManyToOneRelation(categories, items),
]); ]);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
const events = new Map<string, any>(); const events = new Map<string, any>();
@@ -198,24 +150,36 @@ describe("[data] Repository (Events)", async () => {
}); });
test("events were fired", async () => { test("events were fired", async () => {
await em.repository(items).findId(1); const repo = em.repository(items);
await repo.findId(1);
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findOne({ id: 1 }); await repo.findOne({ id: 1 });
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findMany({ where: { id: 1 } }); await repo.findMany({ where: { id: 1 } });
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
events.clear(); events.clear();
await em.repository(items).findManyByReference(1, "categories"); await repo.findManyByReference(1, "categories");
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue(); expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
events.clear(); events.clear();
// check find one on findMany with limit 1
await repo.findMany({ where: { id: 1 }, limit: 1 });
await repo.emgr.executeAsyncs();
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
events.clear();
}); });
}); });
+33 -33
View File
@@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => {
const em = new EntityManager([entity], dummyConnection, [], [index]); const em = new EntityManager([entity], dummyConnection, [], [index]);
const schema = new SchemaManager(em); const schema = new SchemaManager(em);
const introspection = schema.getIntrospectionFromEntity(em.entities[0]); const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
expect(introspection).toEqual({ expect(introspection).toEqual({
name: "test", name: "test",
isView: false, isView: false,
@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: true, isAutoIncrementing: true,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "username", name: "username",
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "email", name: "email",
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
}, },
{ {
name: "bio", name: "bio",
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
isNullable: true, isNullable: true,
isAutoIncrementing: false, isAutoIncrementing: false,
hasDefaultValue: false, hasDefaultValue: false,
comment: undefined comment: undefined,
} },
], ],
indices: [ indices: [
{ {
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
columns: [ columns: [
{ {
name: "email", name: "email",
order: 0 order: 0,
} },
] ],
} },
] ],
}); });
}); });
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
new Entity(table, [ new Entity(table, [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("bio") new TextField("bio"),
]) ]),
], ],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -101,15 +101,15 @@ describe("SchemaManager tests", async () => {
name: table, name: table,
isNew: false, isNew: false,
columns: { add: ["bio"], drop: [], change: [] }, columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [index] } indices: { add: [], drop: [index] },
} },
]); ]);
// now sync // now sync
await schema.sync({ force: true, drop: true }); await schema.sync({ force: true, drop: true });
const diffAfter = await schema.getDiff(); const diffAfter = await schema.getDiff();
console.log("diffAfter", diffAfter); //console.log("diffAfter", diffAfter);
expect(diffAfter.length).toBe(0); expect(diffAfter.length).toBe(0);
await kysely.schema.dropTable(table).execute(); await kysely.schema.dropTable(table).execute();
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
const table = "drop_column"; const table = "drop_column";
const em = new EntityManager( const em = new EntityManager(
[new Entity(table, [new TextField("username")])], [new Entity(table, [new TextField("username")])],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
columns: { columns: {
add: [], add: [],
drop: ["email"], drop: ["email"],
change: [] change: [],
}, },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
} },
]); ]);
// now sync // now sync
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
new Entity(usersTable, [ new Entity(usersTable, [
new TextField("username"), new TextField("username"),
new TextField("email"), new TextField("email"),
new TextField("bio") new TextField("bio"),
]), ]),
new Entity(postsTable, [ new Entity(postsTable, [
new TextField("title"), new TextField("title"),
new TextField("content"), new TextField("content"),
new TextField("created_at") new TextField("created_at"),
]) ]),
], ],
dummyConnection dummyConnection,
); );
const kysely = em.connection.kysely; const kysely = em.connection.kysely;
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
name: usersTable, name: usersTable,
isNew: false, isNew: false,
columns: { add: ["bio"], drop: [], change: [] }, columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
}, },
{ {
name: postsTable, name: postsTable,
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
columns: { columns: {
add: ["id", "title", "content", "created_at"], add: ["id", "title", "content", "created_at"],
drop: [], drop: [],
change: [] change: [],
}, },
indices: { add: [], drop: [] } indices: { add: [], drop: [] },
} },
]); ]);
// now sync // now sync
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
name: entity.name, name: entity.name,
isNew: true, isNew: true,
columns: { add: ["id", "email"], drop: [], change: [] }, columns: { add: ["id", "email"], drop: [], change: [] },
indices: { add: [index.name!], drop: [] } indices: { add: [index.name!], drop: [] },
} },
]); ]);
// sync and then check again // sync and then check again
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
name: entity.name, name: entity.name,
isNew: false, isNew: false,
columns: { add: [], drop: [], change: [] }, columns: { add: [], drop: [], change: [] },
indices: { add: [index.name!], drop: [] } indices: { add: [index.name!], drop: [] },
} },
]); ]);
// sync and then check again // sync and then check again
@@ -0,0 +1,90 @@
import { describe, test, expect } from "bun:test";
import { getDummyConnection } from "../helper";
import { type WhereQuery, WhereBuilder } from "data";
function qb() {
const c = getDummyConnection();
const kysely = c.dummyConnection.kysely;
return kysely.selectFrom("t").selectAll();
}
function compile(q: WhereQuery) {
const { sql, parameters } = WhereBuilder.addClause(qb(), q).compile();
return { sql, parameters };
}
describe("WhereBuilder", () => {
test("single validation", () => {
const tests: [WhereQuery, string, any[]][] = [
[{ name: "Michael", age: 40 }, '("name" = ? and "age" = ?)', ["Michael", 40]],
[{ name: { $eq: "Michael" } }, '"name" = ?', ["Michael"]],
[{ int: { $between: [1, 2] } }, '"int" between ? and ?', [1, 2]],
[{ val: { $isnull: 1 } }, '"val" is null', []],
[{ val: { $isnull: true } }, '"val" is null', []],
[{ val: { $isnull: 0 } }, '"val" is not null', []],
[{ val: { $isnull: false } }, '"val" is not null', []],
[{ val: { $like: "what" } }, '"val" like ?', ["what"]],
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
];
for (const [query, expectedSql, expectedParams] of tests) {
const { sql, parameters } = compile(query);
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
expect(parameters).toEqual(expectedParams);
}
});
test("multiple validations", () => {
const tests: [WhereQuery, string, any[]][] = [
// multiple constraints per property
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
[{ val: { $lt: 10, $gte: 3 } }, '("val" < ? and "val" >= ?)', [10, 3]],
// multiple properties
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"],
],
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"],
],
// or constructs
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
'("val1" = ? or "val2" = ?)',
["foo", "bar"],
],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
];
for (const [query, expectedSql, expectedParams] of tests) {
const { sql, parameters } = compile(query);
expect(sql).toContain(`select * from "t" where ${expectedSql}`);
expect(parameters).toEqual(expectedParams);
}
});
test("keys", () => {
const tests: [WhereQuery, string[]][] = [
// multiple constraints per property
[{ val: { $lt: 10, $gte: 3 } }, ["val"]],
// multiple properties
[{ val1: { $eq: "foo" }, val2: { $eq: "bar" } }, ["val1", "val2"]],
// or constructs
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
];
for (const [query, expectedKeys] of tests) {
const keys = WhereBuilder.getPropertyNames(query);
expect(keys).toEqual(expectedKeys);
}
});
});
+90 -91
View File
@@ -1,5 +1,4 @@
import { afterAll, describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { _jsonp } from "../../../src/core/utils";
import { import {
Entity, Entity,
EntityManager, EntityManager,
@@ -7,10 +6,10 @@ import {
ManyToOneRelation, ManyToOneRelation,
PolymorphicRelation, PolymorphicRelation,
TextField, TextField,
WithBuilder WithBuilder,
} from "../../../src/data"; } from "../../../src/data";
import * as proto from "../../../src/data/prototype"; import * as proto from "../../../src/data/prototype";
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper"; import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
{ {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
users: proto.entity("users", {}), users: proto.entity("users", {}),
media: proto.entity("media", {}) media: proto.entity("media", {}),
}, },
({ relation }, { posts, users, media }) => { ({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users); relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0); expect(WithBuilder.validateWiths(em, "posts", undefined as any)).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0); expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0);
expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1); expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1);
expect( expect(
WithBuilder.validateWiths(em, "posts", { WithBuilder.validateWiths(em, "posts", {
users: { users: {
with: { avatar: {} } with: { avatar: {} },
} },
}) }),
).toBe(2); ).toBe(2);
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow(); expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
expect(() => expect(() =>
WithBuilder.validateWiths(em, "posts", { WithBuilder.validateWiths(em, "posts", {
users: { users: {
with: { glibberish: {} } with: { glibberish: {} },
} },
}) }),
).toThrow(); ).toThrow();
}); });
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
expect(() => expect(() =>
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {} posts: {},
}) }),
).toThrow('Relation "users<>posts" not found'); ).toThrow('Relation "users<>posts" not found');
}); });
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations); const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {} posts: {},
}); });
const res = qb.compile(); const res = qb.compile();
expect(res.sql).toBe( expect(res.sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"',
); );
expect(res.parameters).toEqual([10, 0]); expect(res.parameters).toEqual([10, 0]);
@@ -83,16 +82,16 @@ describe("[data] WithBuilder", async () => {
em.connection.kysely.selectFrom("posts"), em.connection.kysely.selectFrom("posts"),
posts, // @todo: try with "users", it gives output! posts, // @todo: try with "users", it gives output!
{ {
author: {} author: {},
} },
); );
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"' 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ?) as obj) as "author" from "posts"',
); );
expect(res2.parameters).toEqual([1, 0]); expect(res2.parameters).toEqual([1]);
}); });
test("test with empty join", async () => { test("test with empty join", async () => {
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
.values([ .values([
{ posts_id: 1, categories_id: 1 }, { posts_id: 1, categories_id: 1 },
{ posts_id: 2, categories_id: 2 }, { posts_id: 2, categories_id: 2 },
{ posts_id: 1, categories_id: 2 } { posts_id: 1, categories_id: 2 },
]) ])
.execute(); .execute();
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
title: "fashion post", title: "fashion post",
categories: [ categories: [
{ id: 1, label: "fashion" }, { id: 1, label: "fashion" },
{ id: 2, label: "beauty" } { id: 2, label: "beauty" },
] ],
}, },
{ {
id: 2, id: 2,
title: "beauty post", title: "beauty post",
categories: [{ id: 2, label: "beauty" }] categories: [{ id: 2, label: "beauty" }],
} },
]); ]);
const res2 = await em.repository(categories).findMany({ with: { posts: {} } }); const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
{ {
id: 1, id: 1,
label: "fashion", label: "fashion",
posts: [{ id: 1, title: "fashion post" }] posts: [{ id: 1, title: "fashion post" }],
}, },
{ {
id: 2, id: 2,
label: "beauty", label: "beauty",
posts: [ posts: [
{ id: 1, title: "fashion post" }, { id: 1, title: "fashion post" },
{ id: 2, title: "beauty post" } { id: 2, title: "beauty post" },
] ],
}, },
{ {
id: 3, id: 3,
label: "tech", label: "tech",
posts: [] posts: [],
} },
]); ]);
}); });
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
const entities = [media, categories]; const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, { const single = new PolymorphicRelation(categories, media, {
mappedBy: "single", mappedBy: "single",
targetCardinality: 1 targetCardinality: 1,
}); });
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" }); const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -191,23 +190,23 @@ describe("[data] WithBuilder", async () => {
em, em,
em.connection.kysely.selectFrom("categories"), em.connection.kysely.selectFrom("categories"),
categories, categories,
{ single: {} } { single: {} },
); );
const res = qb.compile(); const res = qb.compile();
expect(res.sql).toBe( expect(res.sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"' 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "single" from "categories"',
); );
expect(res.parameters).toEqual(["categories.single", 1, 0]); expect(res.parameters).toEqual(["categories.single", 1]);
const qb2 = WithBuilder.addClause( const qb2 = WithBuilder.addClause(
em, em,
em.connection.kysely.selectFrom("categories"), em.connection.kysely.selectFrom("categories"),
categories, categories,
{ multiple: {} } { multiple: {} },
); );
const res2 = qb2.compile(); const res2 = qb2.compile();
expect(res2.sql).toBe( expect(res2.sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"',
); );
expect(res2.parameters).toEqual(["categories.multiple", 10, 0]); expect(res2.parameters).toEqual(["categories.multiple", 10, 0]);
}); });
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
{ {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, users, media }) => { ({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users); relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
@@ -265,18 +264,18 @@ describe("[data] WithBuilder", async () => {
with: { with: {
avatar: { avatar: {
select: ["id", "path"], select: ["id", "path"],
limit: 2 // ignored limit: 2, // ignored
} },
} },
} },
} },
); );
//prettyPrintQb(qb); //prettyPrintQb(qb);
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toBe(
'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"' 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ?) as obj) as "users" from "posts"',
); );
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]); expect(qb.compile().parameters).toEqual(["users.avatar", 1, 1]);
}); });
test("compiles with many", async () => { test("compiles with many", async () => {
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
posts: proto.entity("posts", {}), posts: proto.entity("posts", {}),
comments: proto.entity("comments", {}), comments: proto.entity("comments", {}),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, comments, users, media }) => { ({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users); relation(comments).manyToOne(posts).manyToOne(users);
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
@@ -308,40 +307,40 @@ describe("[data] WithBuilder", async () => {
limit: 12, limit: 12,
with: { with: {
users: { users: {
select: ["username"] select: ["username"],
} },
} },
} },
} },
); );
expect(qb.compile().sql).toBe( expect(qb.compile().sql).toBe(
'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"' 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"',
); );
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]); expect(qb.compile().parameters).toEqual([1, 12, 0]);
}); });
test("returns correct result", async () => { test("returns correct result", async () => {
const schema = proto.em( const schema = proto.em(
{ {
posts: proto.entity("posts", { posts: proto.entity("posts", {
title: proto.text() title: proto.text(),
}), }),
comments: proto.entity("comments", { comments: proto.entity("comments", {
content: proto.text() content: proto.text(),
}), }),
users: proto.entity("users", { users: proto.entity("users", {
username: proto.text() username: proto.text(),
}), }),
media: proto.entity("media", { media: proto.entity("media", {
path: proto.text() path: proto.text(),
}) }),
}, },
({ relation }, { posts, comments, users, media }) => { ({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" }); relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users); relation(comments).manyToOne(posts).manyToOne(users);
} },
); );
const em = schemaToEm(schema); const em = schemaToEm(schema);
await em.schema().sync({ force: true }); await em.schema().sync({ force: true });
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
await em.mutator("posts").insertMany([ await em.mutator("posts").insertMany([
{ title: "post1", users_id: 1 }, { title: "post1", users_id: 1 },
{ title: "post2", users_id: 1 }, { title: "post2", users_id: 1 },
{ title: "post3", users_id: 2 } { title: "post3", users_id: 2 },
]); ]);
await em.mutator("comments").insertMany([ await em.mutator("comments").insertMany([
{ content: "comment1", posts_id: 1, users_id: 1 }, { content: "comment1", posts_id: 1, users_id: 1 },
@@ -360,7 +359,7 @@ describe("[data] WithBuilder", async () => {
{ content: "comment3", posts_id: 2, users_id: 1 }, { content: "comment3", posts_id: 2, users_id: 1 },
{ content: "comment4", posts_id: 2, users_id: 2 }, { content: "comment4", posts_id: 2, users_id: 2 },
{ content: "comment5", posts_id: 3, users_id: 1 }, { content: "comment5", posts_id: 3, users_id: 1 },
{ content: "comment6", posts_id: 3, users_id: 2 } { content: "comment6", posts_id: 3, users_id: 2 },
]); ]);
const result = await em.repo("posts").findMany({ const result = await em.repo("posts").findMany({
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
select: ["content"], select: ["content"],
with: { with: {
users: { users: {
select: ["username"] select: ["username"],
} },
} },
} },
} },
}); });
expect(result.data).toEqual([ expect(result.data).toEqual([
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment1", content: "comment1",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment1-1", content: "comment1-1",
users: { users: {
username: "user1" username: "user1",
} },
} },
] ],
}, },
{ {
title: "post2", title: "post2",
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment3", content: "comment3",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment4", content: "comment4",
users: { users: {
username: "user2" username: "user2",
} },
} },
] ],
}, },
{ {
title: "post3", title: "post3",
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
{ {
content: "comment5", content: "comment5",
users: { users: {
username: "user1" username: "user1",
} },
}, },
{ {
content: "comment6", content: "comment6",
users: { users: {
username: "user2" username: "user2",
} },
} },
] ],
} },
]); ]);
//console.log(_jsonp(result.data)); //console.log(_jsonp(result.data));
}); });
@@ -22,10 +22,10 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
} },
] ],
} },
]); ]);
}); });
@@ -54,14 +54,14 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
}, },
{ {
name: "desc", name: "desc",
order: 1 order: 1,
} },
] ],
} },
]); ]);
}); });
@@ -83,10 +83,10 @@ describe("Connection", async () => {
columns: [ columns: [
{ {
name: "name", name: "name",
order: 0 order: 0,
} },
] ],
} },
]); ]);
}); });
}); });
@@ -0,0 +1,107 @@
import { describe, expect, test } from "bun:test";
import { SqliteIntrospector } from "data/connection";
import { getDummyDatabase } from "../../helper";
import { Kysely, SqliteDialect } from "kysely";
function create() {
const database = getDummyDatabase().dummyDb;
return new Kysely({
dialect: new SqliteDialect({ database }),
});
}
function createLibsql() {
const database = getDummyDatabase().dummyDb;
return new Kysely({
dialect: new SqliteDialect({ database }),
});
}
describe("SqliteIntrospector", () => {
test("asdf", async () => {
const kysely = create();
await kysely.schema
.createTable("test")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
.addColumn("string", "text", (col) => col.notNull())
.addColumn("number", "integer")
.execute();
await kysely.schema
.createIndex("idx_test_string")
.on("test")
.columns(["string"])
.unique()
.execute();
await kysely.schema
.createTable("test2")
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement().notNull())
.addColumn("number", "integer")
.execute();
await kysely.schema.createIndex("idx_test2_number").on("test2").columns(["number"]).execute();
const introspector = new SqliteIntrospector(kysely, {});
const result = await introspector.getTables();
//console.log(_jsonp(result));
expect(result).toEqual([
{
name: "test",
isView: false,
columns: [
{
name: "id",
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
comment: undefined,
},
{
name: "string",
dataType: "TEXT",
isNullable: false,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined,
},
{
comment: undefined,
dataType: "INTEGER",
hasDefaultValue: false,
isAutoIncrementing: false,
isNullable: true,
name: "number",
},
],
},
{
name: "test2",
isView: false,
columns: [
{
name: "id",
dataType: "INTEGER",
isNullable: false,
isAutoIncrementing: true,
hasDefaultValue: false,
comment: undefined,
},
{
name: "number",
dataType: "INTEGER",
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined,
},
],
},
]);
});
});
@@ -1,9 +1,9 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { BooleanField } from "../../../../src/data"; import { BooleanField } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] BooleanField", async () => { describe("[data] BooleanField", async () => {
runBaseFieldTests(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,9 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { DateField } from "../../../../src/data"; import { DateField, dateFieldConfigSchema } from "../../../../src/data";
import { runBaseFieldTests } from "./inc"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("[data] DateField", async () => { describe("[data] DateField", async () => {
runBaseFieldTests(DateField, { defaultValue: new Date(), schemaType: "date" }); fieldTestSuite(
bunTestRunner,
DateField,
{ defaultValue: new Date(), schemaType: "date" },
{ type: "date" },
);
// @todo: add datefield tests // @todo: add datefield tests
test("week", async () => { test("week", async () => {
@@ -1,25 +1,29 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { EnumField } from "../../../../src/data"; import { EnumField } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
function options(strings: string[]) { function options(strings: string[]) {
return { type: "strings", values: strings }; return { type: "strings", values: strings };
} }
describe("[data] EnumField", async () => { describe("[data] EnumField", async () => {
runBaseFieldTests( fieldTestSuite(
{ expect, test },
// @ts-ignore
EnumField, EnumField,
{ defaultValue: "a", schemaType: "text" }, { defaultValue: "a", schemaType: "text" },
{ options: options(["a", "b", "c"]) } { options: options(["a", "b", "c"]) },
); );
test("yields if default value is not a valid option", async () => { test("yields if default value is not a valid option", async () => {
expect( expect(
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }) // @ts-ignore
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
).toThrow(); ).toThrow();
}); });
test("transformPersist (config)", async () => { test("transformPersist (config)", async () => {
// @ts-ignore
const field = new EnumField("test", { options: options(["a", "b", "c"]) }); const field = new EnumField("test", { options: options(["a", "b", "c"]) });
expect(transformPersist(field, null)).resolves.toBeUndefined(); expect(transformPersist(field, null)).resolves.toBeUndefined();
@@ -29,9 +33,10 @@ describe("[data] EnumField", async () => {
test("transformRetrieve", async () => { test("transformRetrieve", async () => {
const field = new EnumField("test", { const field = new EnumField("test", {
// @ts-ignore
options: options(["a", "b", "c"]), options: options(["a", "b", "c"]),
default_value: "a", default_value: "a",
required: true required: true,
}); });
expect(field.transformRetrieve(null)).toBe("a"); expect(field.transformRetrieve(null)).toBe("a");
+20 -13
View File
@@ -1,43 +1,50 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Default, parse, stripMark } from "../../../../src/core/utils"; import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data"; import { fieldTestSuite } from "data/fields/field-test-suite";
import { runBaseFieldTests, transformPersist } from "./inc"; import { bunTestRunner } from "adapter/bun/test";
import { stripMark } from "core/object/schema";
describe("[data] Field", async () => { describe("[data] Field", async () => {
class FieldSpec extends Field { class FieldSpec extends Field {
schema(): SchemaResponse {
return this.useSchemaHelper("text");
}
getSchema() { getSchema() {
return baseFieldConfigSchema; return baseFieldConfigSchema;
} }
} }
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" }); test("fieldSpec", () => {
expect(new FieldSpec("test").schema()).toEqual({
name: "test",
type: "text",
nullable: true, // always true
dflt: undefined, // never using default value
});
});
fieldTestSuite(bunTestRunner, FieldSpec, { defaultValue: "test", schemaType: "text" });
test("default config", async () => { test("default config", async () => {
const config = Default(baseFieldConfigSchema, {}); const config = baseFieldConfigSchema.template({});
expect(stripMark(new FieldSpec("test").config)).toEqual(config); expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
}); });
test("transformPersist (specific)", async () => { test("transformPersist (specific)", async () => {
const required = new FieldSpec("test", { required: true }); const required = new FieldSpec("test", { required: true });
const requiredDefault = new FieldSpec("test", { const requiredDefault = new FieldSpec("test", {
required: true, required: true,
default_value: "test" default_value: "test",
}); });
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow(); expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
expect( expect(
required.transformPersist(undefined, undefined as any, undefined as any) required.transformPersist(undefined, undefined as any, undefined as any),
).rejects.toThrow(); ).rejects.toThrow();
// works because it has a default value // works because it has a default value
expect( expect(
requiredDefault.transformPersist(null, undefined as any, undefined as any) requiredDefault.transformPersist(null, undefined as any, undefined as any),
).resolves.toBeDefined(); ).resolves.toBeDefined();
expect( expect(
requiredDefault.transformPersist(undefined, undefined as any, undefined as any) requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
).resolves.toBeDefined(); ).resolves.toBeDefined();
}); });
}); });
@@ -1,19 +1,13 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Type } from "../../../../src/core/utils"; import { Entity, EntityIndex, Field } from "../../../../src/data";
import { import { s } from "core/object/schema";
Entity,
EntityIndex,
type EntityManager,
Field,
type SchemaResponse
} from "../../../../src/data";
class TestField extends Field { class TestField extends Field {
protected getSchema(): any { protected getSchema(): any {
return Type.Any(); return s.any();
} }
schema(em: EntityManager<any>): SchemaResponse { override schema() {
return undefined as any; return undefined as any;
} }
} }
@@ -1,13 +1,13 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonField } from "../../../../src/data"; import { JsonField } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc"; 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");
runBaseFieldTests(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",
}); });
test("transformPersist (no config)", async () => { test("transformPersist (no config)", async () => {
@@ -1,9 +1,10 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { JsonSchemaField } from "../../../../src/data"; import { JsonSchemaField } from "../../../../src/data";
import { runBaseFieldTests } from "./inc"; import { fieldTestSuite } from "data/fields/field-test-suite";
describe("[data] JsonSchemaField", async () => { describe("[data] JsonSchemaField", async () => {
runBaseFieldTests(JsonSchemaField, { defaultValue: {}, schemaType: "text" }); // @ts-ignore
fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" });
// @todo: add JsonSchemaField tests // @todo: add JsonSchemaField tests
}); });
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { NumberField } from "../../../../src/data"; import { NumberField } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
describe("[data] NumberField", async () => { describe("[data] NumberField", async () => {
test("transformPersist (config)", async () => { test("transformPersist (config)", async () => {
@@ -15,5 +15,5 @@ describe("[data] NumberField", async () => {
expect(transformPersist(field2, 10000)).resolves.toBe(10000); expect(transformPersist(field2, 10000)).resolves.toBe(10000);
}); });
runBaseFieldTests(NumberField, { defaultValue: 12, schemaType: "integer" }); fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" });
}); });
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
test("schema", () => { test("schema", () => {
expect(field.name).toBe("primary"); expect(field.name).toBe("primary");
expect(field.schema()).toEqual(["primary", "integer", expect.any(Function)]); expect(field.schema()).toEqual({
name: "primary",
type: "integer" as const,
nullable: false,
primary: true,
});
}); });
test("hasDefault", async () => { test("hasDefault", async () => {
@@ -34,4 +39,28 @@ describe("[data] PrimaryField", async () => {
expect(field.transformPersist(1)).rejects.toThrow(); expect(field.transformPersist(1)).rejects.toThrow();
expect(field.transformRetrieve(1)).toBe(1); expect(field.transformRetrieve(1)).toBe(1);
}); });
test("format", () => {
const uuid = new PrimaryField("uuid", { format: "uuid" });
expect(uuid.format).toBe("uuid");
expect(uuid.fieldType).toBe("text");
expect(uuid.getNewValue()).toBeString();
expect(uuid.toType()).toEqual({
required: true,
comment: undefined,
type: "Generated<string>",
import: [{ package: "kysely", name: "Generated" }],
});
const integer = new PrimaryField("integer", { format: "integer" });
expect(integer.format).toBe("integer");
expect(integer.fieldType).toBe("integer");
expect(integer.getNewValue()).toBeUndefined();
expect(integer.toType()).toEqual({
required: true,
comment: undefined,
type: "Generated<number>",
import: [{ package: "kysely", name: "Generated" }],
});
});
}); });
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { TextField } from "../../../../src/data"; import { TextField, textFieldConfigSchema } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc"; import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite";
import { bunTestRunner } from "adapter/bun/test";
describe("[data] TextField", async () => { describe("[data] TextField", async () => {
test("transformPersist (config)", async () => { test("transformPersist (config)", async () => {
@@ -11,5 +12,5 @@ describe("[data] TextField", async () => {
expect(transformPersist(field, "abc")).resolves.toBe("abc"); expect(transformPersist(field, "abc")).resolves.toBe("abc");
}); });
runBaseFieldTests(TextField, { defaultValue: "abc", schemaType: "text" }); fieldTestSuite(bunTestRunner, TextField, { defaultValue: "abc", schemaType: "text" });
}); });
@@ -4,7 +4,7 @@ import {
type BaseRelationConfig, type BaseRelationConfig,
EntityRelation, EntityRelation,
EntityRelationAnchor, EntityRelationAnchor,
RelationTypes RelationTypes,
} from "../../../../src/data/relations"; } from "../../../../src/data/relations";
class TestEntityRelation extends EntityRelation { class TestEntityRelation extends EntityRelation {
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
super( super(
new EntityRelationAnchor(new Entity("source"), "source"), new EntityRelationAnchor(new Entity("source"), "source"),
new EntityRelationAnchor(new Entity("target"), "target"), new EntityRelationAnchor(new Entity("target"), "target"),
config config,
); );
} }
initialize(em: EntityManager<any>) {} initialize(em: EntityManager<any>) {}
+13 -13
View File
@@ -30,23 +30,23 @@ beforeAll(() =>
method: init?.method ?? "GET", method: init?.method ?? "GET",
// @ts-ignore // @ts-ignore
headers: Object.fromEntries(init?.headers?.entries() ?? []), headers: Object.fromEntries(init?.headers?.entries() ?? []),
body: init?.body body: init?.body,
}; };
return new Response(JSON.stringify({ todos: [1, 2], request }), { return new Response(JSON.stringify({ todos: [1, 2], request }), {
status: 200, status: 200,
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}); });
}) }),
); );
afterAll(unmockFetch); afterAll(unmockFetch);
describe("FetchTask", async () => { describe.skip("FetchTask", async () => {
test("Simple fetch", async () => { test("Simple fetch", async () => {
const task = new FetchTask("Fetch Something", { const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1", url: "https://jsonplaceholder.typicode.com/todos/1",
method: "GET", method: "GET",
headers: [{ key: "Content-Type", value: "application/json" }] headers: [{ key: "Content-Type", value: "application/json" }],
}); });
const result = await task.run(); const result = await task.run();
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
expect( expect(
// // @ts-expect-error // // @ts-expect-error
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }) () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
).toThrow(); ).toThrow();
expect( expect(
new FetchTask("", { new FetchTask("", {
url: "https://jsonplaceholder.typicode.com", url: "https://jsonplaceholder.typicode.com",
method: "invalid" method: "invalid",
}).execute() }).execute(),
).rejects.toThrow(/^Invalid method/); ).rejects.toThrow(/^Invalid method/);
expect( expect(
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }) () => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
).toBeDefined(); ).toBeDefined();
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow(); expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
method: "{{ flow.output.method }}", method: "{{ flow.output.method }}",
headers: [ headers: [
{ key: "Content-{{ flow.output.headerKey }}", value: "application/json" }, { key: "Content-{{ flow.output.headerKey }}", value: "application/json" },
{ key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" } { key: "Authorization", value: "Bearer {{ flow.output.apiKey }}" },
], ],
body: JSON.stringify({ body: JSON.stringify({
email: "{{ flow.output.email }}" email: "{{ flow.output.email }}",
}) }),
}); });
const inputs = { const inputs = {
headerKey: "Type", headerKey: "Type",
apiKey: 123, apiKey: 123,
email: "what@else.com", email: "what@else.com",
method: "PATCH" method: "PATCH",
}; };
const flow = new Flow("", [task]); const flow = new Flow("", [task]);
+26 -47
View File
@@ -1,19 +1,35 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows"; import { Flow, LogTask, SubFlowTask, RenderTask, Task } from "../../src/flows";
import { s } from "core/object/schema";
describe("SubFlowTask", async () => { export class StringifyTask<Output extends string> extends Task<
typeof StringifyTask.schema,
Output
> {
type = "stringify";
static override schema = s.object({
input: s.string().optional(),
});
async execute() {
return JSON.stringify(this.params.input) as Output;
}
}
describe.skip("SubFlowTask", async () => {
test("Simple Subflow", async () => { test("Simple Subflow", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "subflow" render: "subflow",
}); });
const subflow = new Flow("subflow", [subTask]); const subflow = new Flow("subflow", [subTask]);
const task = new LogTask("log"); const task = new LogTask("log");
const task2 = new SubFlowTask("sub", { const task2 = new SubFlowTask("sub", {
flow: subflow flow: subflow,
}); });
const task3 = new RenderTask("render2", { const task3 = new RenderTask("render2", {
render: "Subflow output: {{ sub.output }}" render: "Subflow output: {{ sub.output }}",
}); });
const flow = new Flow("test", [task, task2, task3], []); const flow = new Flow("test", [task, task2, task3], []);
@@ -22,15 +38,13 @@ describe("SubFlowTask", async () => {
const execution = flow.createExecution(); const execution = flow.createExecution();
await execution.start(); await execution.start();
/*console.log(execution.logs);
console.log(execution.getResponse());*/
expect(execution.getResponse()).toEqual("Subflow output: subflow"); expect(execution.getResponse()).toEqual("Subflow output: subflow");
}); });
test("Simple loop", async () => { test("Simple loop", async () => {
const subTask = new RenderTask("render", { const subTask = new RenderTask("render", {
render: "run {{ flow.output }}" render: "run {{ flow.output }}",
}); });
const subflow = new Flow("subflow", [subTask]); const subflow = new Flow("subflow", [subTask]);
@@ -38,10 +52,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", { const task2 = new SubFlowTask("sub", {
flow: subflow, flow: subflow,
loop: true, loop: true,
input: [1, 2, 3] input: [1, 2, 3],
}); });
const task3 = new RenderTask("render2", { const task3 = new StringifyTask("stringify", {
render: `Subflow output: {{ sub.output | join: ", " }}` input: "{{ sub.output }}",
}); });
const flow = new Flow("test", [task, task2, task3], []); const flow = new Flow("test", [task, task2, task3], []);
@@ -51,41 +65,6 @@ describe("SubFlowTask", async () => {
const execution = flow.createExecution(); const execution = flow.createExecution();
await execution.start(); await execution.start();
console.log("errors", execution.getErrors()); expect(execution.getResponse()).toEqual('"run 1,run 2,run 3"');
/*console.log(execution.logs);
console.log(execution.getResponse());*/
expect(execution.getResponse()).toEqual("Subflow output: run 1, run 2, run 3");
});
test("Simple loop from flow input", async () => {
const subTask = new RenderTask("render", {
render: "run {{ flow.output }}"
});
const subflow = new Flow("subflow", [subTask]);
const task = new LogTask("log");
const task2 = new SubFlowTask("sub", {
flow: subflow,
loop: true,
input: "{{ flow.output | json }}"
});
const task3 = new RenderTask("render2", {
render: `Subflow output: {{ sub.output | join: ", " }}`
});
const flow = new Flow("test", [task, task2, task3], []);
flow.task(task).asInputFor(task2);
flow.task(task2).asInputFor(task3);
const execution = flow.createExecution();
await execution.start([4, 5, 6]);
/*console.log(execution.logs);
console.log(execution.getResponse());*/
expect(execution.getResponse()).toEqual("Subflow output: run 4, run 5, run 6");
}); });
}); });
+16 -74
View File
@@ -1,20 +1,20 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { Type } from "../../src/core/utils";
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/object/schema";
describe("Task", async () => { describe.skip("Task", async () => {
test("resolveParams: template with parse", async () => { test("resolveParams: template with parse", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Number()) }), s.object({ test: dynamic(s.number()) }),
{ {
test: "{{ some.path }}" test: "{{ some.path }}",
}, },
{ {
some: { some: {
path: 1 path: 1,
} },
} },
); );
expect(result.test).toBe(1); expect(result.test).toBe(1);
@@ -22,15 +22,15 @@ describe("Task", async () => {
test("resolveParams: with string", async () => { test("resolveParams: with string", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: Type.String() }), s.object({ test: s.string() }),
{ {
test: "{{ some.path }}" test: "{{ some.path }}",
}, },
{ {
some: { some: {
path: "1/1" path: "1/1",
} },
} },
); );
expect(result.test).toBe("1/1"); expect(result.test).toBe("1/1");
@@ -38,75 +38,17 @@ describe("Task", async () => {
test("resolveParams: with object", async () => { test("resolveParams: with object", async () => {
const result = await Task.resolveParams( const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }), s.object({ test: dynamic(s.object({ key: s.string(), value: s.string() })) }),
{ {
test: { key: "path", value: "{{ some.path }}" } test: { key: "path", value: "{{ some.path }}" },
}, },
{ {
some: { some: {
path: "1/1" path: "1/1",
} },
}
);
expect(result.test).toEqual({ key: "path", value: "1/1" });
});
test("resolveParams: with json", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
}),
{
test: "{{ some | json }}"
}, },
{
some: {
key: "path",
value: "1/1"
}
}
); );
expect(result.test).toEqual({ key: "path", value: "1/1" }); expect(result.test).toEqual({ key: "path", value: "1/1" });
}); });
test("resolveParams: with array", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Array(Type.String()))
}),
{
test: '{{ "1,2,3" | split: "," | json }}'
}
);
expect(result.test).toEqual(["1", "2", "3"]);
});
test("resolveParams: boolean", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Boolean())
}),
{
test: "{{ true }}"
}
);
expect(result.test).toEqual(true);
});
test("resolveParams: float", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Number(), Number.parseFloat)
}),
{
test: "{{ 3.14 }}"
}
);
expect(result.test).toEqual(3.14);
});
}); });
+3 -3
View File
@@ -7,11 +7,11 @@ const first = getNamedTask(
//throw new Error("Error"); //throw new Error("Error");
return { return {
inner: { inner: {
result: 2 result: 2,
} },
}; };
}, },
1000 1000,
); );
const second = getNamedTask("second (if match)"); const second = getNamedTask("second (if match)");
const third = getNamedTask("third (if error)"); const third = getNamedTask("third (if error)");
+3 -3
View File
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor( constructor(
name: string, name: string,
params: any, params: any,
private fn: () => any private fn: () => any,
) { ) {
super(name, params); super(name, params);
} }
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
return new ExecTask( return new ExecTask(
name, name,
{ {
delay delay,
}, },
func func,
); );
} }
+1 -1
View File
@@ -4,7 +4,7 @@ const first = new LogTask("First", { delay: 1000 });
const second = new LogTask("Second", { delay: 1000 }); const second = new LogTask("Second", { delay: 1000 });
const third = new LogTask("Long Third", { delay: 2500 }); const third = new LogTask("Long Third", { delay: 2500 });
const fourth = new FetchTask("Fetch Something", { const fourth = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1" url: "https://jsonplaceholder.typicode.com/todos/1",
}); });
const fifth = new LogTask("Task 4", { delay: 500 }); // without connection const fifth = new LogTask("Task 4", { delay: 500 }); // without connection
+17 -17
View File
@@ -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 { type Static, type StaticDecode, Type, parse } from "../../src/core/utils"; 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";
@@ -14,19 +14,19 @@ class Passthrough extends Task {
} }
} }
type OutputIn = Static<typeof OutputParamTask.schema>; type OutputIn = s.Static<typeof OutputParamTask.schema>;
type OutputOut = StaticDecode<typeof OutputParamTask.schema>; type OutputOut = s.StaticCoerced<typeof OutputParamTask.schema>;
class OutputParamTask extends Task<typeof OutputParamTask.schema> { class OutputParamTask extends Task<typeof OutputParamTask.schema> {
type = "output-param"; type = "output-param";
static override schema = Type.Object({ static override schema = s.strictObject({
number: dynamic( number: dynamic(
Type.Number({ s.number({
title: "Output number" title: "Output number",
}), }),
Number.parseInt Number.parseInt,
) ),
}); });
async execute(inputs: InputsMap) { async execute(inputs: InputsMap) {
@@ -43,7 +43,7 @@ class PassthroughFlowInput extends Task {
} }
} }
describe("Flow task inputs", async () => { describe.skip("Flow task inputs", async () => {
test("types", async () => { test("types", async () => {
const schema = OutputParamTask.schema; const schema = OutputParamTask.schema;
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
test("output/input", async () => { test("output/input", async () => {
const task = new OutputParamTask("task1", { number: 111 }); const task = new OutputParamTask("task1", { number: 111 });
const task2 = new OutputParamTask("task2", { const task2 = new OutputParamTask("task2", {
number: "{{ task1.output }}" number: "{{ task1.output }}",
}); });
const flow = new Flow("test", [task, task2]); const flow = new Flow("test", [task, task2]);
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
test("input from flow", async () => { test("input from flow", async () => {
const task = new OutputParamTask("task1", { const task = new OutputParamTask("task1", {
number: "{{flow.output.someFancyParam}}" number: "{{flow.output.someFancyParam}}",
}); });
const task2 = new OutputParamTask("task2", { const task2 = new OutputParamTask("task2", {
number: "{{task1.output}}" number: "{{task1.output}}",
}); });
const flow = new Flow("test", [task, task2]); const flow = new Flow("test", [task, task2]);
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
const emgr = new EventManager({ EventTriggerClass }); const emgr = new EventManager({ EventTriggerClass });
const task = new OutputParamTask("event", { const task = new OutputParamTask("event", {
number: "{{flow.output.number}}" number: "{{flow.output.number}}",
}); });
const flow = new Flow( const flow = new Flow(
"test", "test",
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
[], [],
new EventTrigger({ new EventTrigger({
event: "test-event", event: "test-event",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);
flow.trigger.register(flow, emgr); flow.trigger.register(flow, emgr);
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);
+4 -4
View File
@@ -10,7 +10,7 @@ const flows = {
back, back,
fanout, fanout,
parallel, parallel,
simpleFetch simpleFetch,
}; };
const arg = process.argv[2]; const arg = process.argv[2];
@@ -32,7 +32,7 @@ const colors = [
"#F78F1E", // Saffron "#F78F1E", // Saffron
"#BD10E0", // Vivid Purple "#BD10E0", // Vivid Purple
"#50E3C2", // Turquoise "#50E3C2", // Turquoise
"#9013FE" // Grape "#9013FE", // Grape
]; ];
const colorsCache: Record<string, string> = {}; const colorsCache: Record<string, string> = {};
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
} }
return t; return t;
}) }),
); );
} }
}); });
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
console.log("done", response ? response : "(no response)"); console.log("done", response ? response : "(no response)");
console.log( console.log(
"Executed tasks:", "Executed tasks:",
execution.logs.map((l) => l.task.name) execution.logs.map((l) => l.task.name),
); );
console.log("Executed count:", execution.logs.length); console.log("Executed count:", execution.logs.length);
}); });
+8 -8
View File
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor( constructor(
name: string, name: string,
params: any, params: any,
private fn: () => any private fn: () => any,
) { ) {
super(name, params); super(name, params);
} }
@@ -30,7 +30,7 @@ class ExecTask extends Task {
} }
} }
describe("Flow trigger", async () => { describe.skip("Flow trigger", async () => {
test("manual trigger", async () => { test("manual trigger", async () => {
let called = false; let called = false;
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
"test", "test",
[task], [task],
[], [],
new EventTrigger({ event: "test-event", mode: "sync" }) new EventTrigger({ event: "test-event", mode: "sync" }),
); );
flow.trigger.register(flow, emgr); flow.trigger.register(flow, emgr);
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
const hono = new Hono(); const hono = new Hono();
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
test("http trigger with response", async () => { test("http trigger with response", async () => {
const task = ExecTask.create("http", () => ({ const task = ExecTask.create("http", () => ({
called: true called: true,
})); }));
const flow = new Flow( const flow = new Flow(
"test", "test",
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({ new HttpTrigger({
path: "/test", path: "/test",
method: "GET", method: "GET",
mode: "sync" mode: "sync",
}) }),
); );
flow.setRespondingTask(task); flow.setRespondingTask(task);
+20 -19
View File
@@ -1,7 +1,8 @@
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
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 { type Static, Type, _jsonp, withDisabledConsole } from "../../src/core/utils"; import { _jsonp, withDisabledConsole } from "../../src/core/utils";
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);
@@ -10,19 +11,19 @@ afterAll(enableConsoleLog);*/
class ExecTask extends Task<typeof ExecTask.schema> { class ExecTask extends Task<typeof ExecTask.schema> {
type = "exec"; type = "exec";
static override schema = Type.Object({ static override schema = s.object({
delay: Type.Number({ default: 10 }) delay: s.number({ default: 10 }),
}); });
constructor( constructor(
name: string, name: string,
params: Static<typeof ExecTask.schema>, params: s.Static<typeof ExecTask.schema>,
private func: () => Promise<any> private func: () => Promise<any>,
) { ) {
super(name, params); super(name, params);
} }
override clone(name: string, params: Static<typeof ExecTask.schema>) { override clone(name: string, params: s.Static<typeof ExecTask.schema>) {
return new ExecTask(name, params, this.func); return new ExecTask(name, params, this.func);
} }
@@ -36,12 +37,12 @@ function getTask(num: number = 0, delay: number = 5) {
return new ExecTask( return new ExecTask(
`Task ${num}`, `Task ${num}`,
{ {
delay delay,
}, },
async () => { async () => {
//console.log(`[DONE] Task: ${num}`); //console.log(`[DONE] Task: ${num}`);
return true; return true;
} },
); );
//return new LogTask(`Log ${num}`, { delay }); //return new LogTask(`Log ${num}`, { delay });
} }
@@ -56,9 +57,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
return new ExecTask( return new ExecTask(
name, name,
{ {
delay: delay ?? 0 delay: delay ?? 0,
}, },
func func,
); );
} }
@@ -77,7 +78,7 @@ function getObjectDiff(obj1, obj2) {
return diff; return diff;
} }
describe("Flow tests", async () => { describe.skip("Flow tests", async () => {
test("Simple single task", async () => { test("Simple single task", async () => {
const simple = getTask(0); const simple = getTask(0);
@@ -228,7 +229,7 @@ describe("Flow tests", async () => {
back back
.task(third) .task(third)
.getOutTasks() .getOutTasks()
.map((t) => t.name) .map((t) => t.name),
).toEqual(["second", "fourth"]); ).toEqual(["second", "fourth"]);
const execution = back.createExecution(); const execution = back.createExecution();
@@ -263,7 +264,7 @@ describe("Flow tests", async () => {
back back
.task(third) .task(third)
.getOutTasks() .getOutTasks()
.map((t) => t.name) .map((t) => t.name),
).toEqual(["second", "fourth"]); ).toEqual(["second", "fourth"]);
const execution = back.createExecution(); const execution = back.createExecution();
@@ -324,8 +325,8 @@ describe("Flow tests", async () => {
const first = getNamedTask("first", async () => { const first = getNamedTask("first", async () => {
return { return {
inner: { inner: {
result: 2 result: 2,
} },
}; };
}); });
const second = getNamedTask("second"); const second = getNamedTask("second");
@@ -361,7 +362,7 @@ describe("Flow tests", async () => {
"[event]", "[event]",
event.isStart() ? "start" : "end", event.isStart() ? "start" : "end",
event.task().name, event.task().name,
event.isStart() ? undefined : event.succeeded() event.isStart() ? undefined : event.succeeded(),
); );
} }
}); });
@@ -389,7 +390,7 @@ describe("Flow tests", async () => {
const second = new LogTask("Task 1"); const second = new LogTask("Task 1");
const third = new LogTask("Task 2", { delay: 50 }); const third = new LogTask("Task 2", { delay: 50 });
const fourth = new FetchTask("Fetch Something", { const fourth = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1" url: "https://jsonplaceholder.typicode.com/todos/1",
}); });
const fifth = new LogTask("Task 4"); // without connection const fifth = new LogTask("Task 4"); // without connection
@@ -405,7 +406,7 @@ describe("Flow tests", async () => {
// @todo: fix // @todo: fix
const deserialized = Flow.fromObject("", original, { const deserialized = Flow.fromObject("", original, {
fetch: { cls: FetchTask }, fetch: { cls: FetchTask },
log: { cls: LogTask } log: { cls: LogTask },
} as any); } as any);
const diffdeep = getObjectDiff(original, deserialized.toJSON()); const diffdeep = getObjectDiff(original, deserialized.toJSON());
@@ -414,7 +415,7 @@ describe("Flow tests", async () => {
expect(flow.startTask.name).toEqual(deserialized.startTask.name); expect(flow.startTask.name).toEqual(deserialized.startTask.name);
expect(flow.respondingTask?.name).toEqual( expect(flow.respondingTask?.name).toEqual(
// @ts-ignore // @ts-ignore
deserialized.respondingTask?.name deserialized.respondingTask?.name,
); );
//console.log("--- creating original sequence"); //console.log("--- creating original sequence");
+56 -5
View File
@@ -4,6 +4,9 @@ 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 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 { join } from "node:path";
import { slugify } from "core/utils/strings";
export function getDummyDatabase(memory: boolean = true): { export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase; dummyDb: SqliteDatabase;
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => { afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME); if (!memory) await unlink(DB_NAME);
return true; return true;
} },
}; };
} }
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
return { return {
dummyConnection, dummyConnection,
afterAllCleanup afterAllCleanup,
}; };
} }
@@ -35,14 +38,15 @@ export function getLocalLibsqlConnection() {
return { url: "http://127.0.0.1:8080" }; return { url: "http://127.0.0.1:8080" };
} }
type ConsoleSeverity = "log" | "warn" | "error"; type ConsoleSeverity = "debug" | "log" | "warn" | "error";
const _oldConsoles = { const _oldConsoles = {
debug: console.debug,
log: console.log, log: console.log,
warn: console.warn, warn: console.warn,
error: console.error error: console.error,
}; };
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) { export function disableConsoleLog(severities: ConsoleSeverity[] = ["debug", "log", "warn"]) {
severities.forEach((severity) => { severities.forEach((severity) => {
console[severity] = () => null; console[severity] = () => null;
}); });
@@ -68,3 +72,50 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
const connection = conn ? conn : getDummyConnection().dummyConnection; const connection = conn ? conn : getDummyConnection().dummyConnection;
return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices); return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices);
} }
export const assetsPath = `${import.meta.dir}/_assets`;
export const assetsTmpPath = `${import.meta.dir}/_assets/tmp`;
export async function enableFetchLogging() {
const originalFetch = global.fetch;
// @ts-ignore
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await originalFetch(input, init);
const url = input instanceof URL || typeof input === "string" ? input : input.url;
// Only clone if it's a supported content type
const contentType = response.headers.get("content-type") || "";
const isSupported =
contentType.includes("json") ||
contentType.includes("text") ||
contentType.includes("xml");
if (isSupported) {
const clonedResponse = response.clone();
let extension = "txt";
let body: string;
if (contentType.includes("json")) {
body = JSON.stringify(await clonedResponse.json(), null, 2);
extension = "json";
} else if (contentType.includes("xml")) {
body = await clonedResponse.text();
extension = "xml";
} else {
body = await clonedResponse.text();
}
const fileName = `${new Date().getTime()}_${init?.method ?? "GET"}_${slugify(String(url))}.${extension}`;
const filePath = join(assetsTmpPath, fileName);
await writeFile(filePath, body);
}
return response;
};
return () => {
global.fetch = originalFetch;
};
}
@@ -1,8 +1,12 @@
import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
import { App, createApp } from "../../src"; import { App, createApp } from "../../src";
import type { AuthResponse } from "../../src/auth"; import type { AuthResponse } from "../../src/auth";
import { auth } from "../../src/auth/middlewares";
import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils"; import { randomString, secureRandomString, withDisabledConsole } from "../../src/core/utils";
import { disableConsoleLog, enableConsoleLog } from "../helper"; import { disableConsoleLog, enableConsoleLog, getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterEach(afterAllCleanup);
beforeAll(disableConsoleLog); beforeAll(disableConsoleLog);
afterAll(enableConsoleLog); afterAll(enableConsoleLog);
@@ -15,25 +19,25 @@ const roles = {
"system.schema.read", "system.schema.read",
"system.access.api", "system.access.api",
"system.config.read", "system.config.read",
"data.entity.read" "data.entity.read",
], ],
is_default: true is_default: true,
}, },
admin: { admin: {
is_default: true, is_default: true,
implicit_allow: true implicit_allow: true,
} },
}, },
strict: { strict: {
guest: { guest: {
permissions: ["system.access.api", "system.config.read", "data.entity.read"], permissions: ["system.access.api", "system.config.read", "data.entity.read"],
is_default: true is_default: true,
}, },
admin: { admin: {
is_default: true, is_default: true,
implicit_allow: true implicit_allow: true,
} },
} },
}; };
const configs = { const configs = {
auth: { auth: {
@@ -41,31 +45,32 @@ const configs = {
entity_name: "users", entity_name: "users",
jwt: { jwt: {
secret: secureRandomString(20), secret: secureRandomString(20),
issuer: randomString(10) issuer: randomString(10),
}, },
roles: roles.strict, roles: roles.strict,
guard: { guard: {
enabled: true enabled: true,
} },
}, },
users: { users: {
normal: { normal: {
email: "normal@bknd.io", email: "normal@bknd.io",
password: "12345678" password: "12345678",
}, },
admin: { admin: {
email: "admin@bknd.io", email: "admin@bknd.io",
password: "12345678", password: "12345678",
role: "admin" role: "admin",
} },
} },
}; };
function createAuthApp() { function createAuthApp() {
const app = createApp({ const app = createApp({
connection: dummyConnection,
initialConfig: { initialConfig: {
auth: configs.auth auth: configs.auth,
} },
}); });
app.emgr.onEvent( app.emgr.onEvent(
@@ -74,7 +79,7 @@ function createAuthApp() {
await app.createUser(configs.users.normal); await app.createUser(configs.users.normal);
await app.createUser(configs.users.admin); await app.createUser(configs.users.admin);
}, },
"sync" "sync",
); );
return app; return app;
@@ -93,14 +98,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
if (mode === "cookie") { if (mode === "cookie") {
return { return {
cookie: `auth=${token};`, cookie: `auth=${token};`,
...additional ...additional,
}; };
} }
return { return {
Authorization: `Bearer ${token}`, Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json", "Content-Type": "application/json",
...additional ...additional,
}; };
} }
function body(obj?: Record<string, any>) { function body(obj?: Record<string, any>) {
@@ -117,12 +122,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
return { return {
login: async ( login: async (
user: any user: any,
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => { ): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
const res = (await app.server.request("/api/auth/password/login", { const res = (await app.server.request("/api/auth/password/login", {
method: "POST", method: "POST",
headers: headers(), headers: headers(),
body: body(user) body: body(user),
})) as Response; })) as Response;
const data = mode === "cookie" ? getCookie(res, "auth") : await res.json(); const data = mode === "cookie" ? getCookie(res, "auth") : await res.json();
@@ -132,10 +137,10 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
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",
headers: headers(token) headers: headers(token),
})) as Response; })) as Response;
return await res.json(); return await res.json();
} },
}; };
}; };
@@ -210,4 +215,36 @@ describe("integration auth", () => {
expect(res.status).toBe(403); expect(res.status).toBe(403);
}); });
}); });
it("context is exclusive", async () => {
const app = createAuthApp();
await app.build();
const $fns = fns(app);
app.server.get("/get", auth(), async (c) => {
return c.json({
user: c.get("auth").user ?? null,
});
});
app.server.get("/wait", auth(), async (c) => {
await new Promise((r) => setTimeout(r, 20));
return c.json({ ok: true });
});
const { data } = await $fns.login(configs.users.normal);
const me = await $fns.me(data.token);
expect(me.user.email).toBe(configs.users.normal.email);
app.server.request("/wait", {
headers: { Authorization: `Bearer ${data.token}` },
});
{
await new Promise((r) => setTimeout(r, 10));
const res = await app.server.request("/get");
const data = await res.json();
expect(data.user).toBe(null);
expect(await $fns.me()).toEqual({ user: null as any });
}
});
}); });
@@ -0,0 +1,23 @@
import { describe, expect, it } from "bun:test";
import { createApp } from "core/test/utils";
import { Api } from "../../src/Api";
describe("integration config", () => {
it("should create an entity", async () => {
const app = createApp();
await app.build();
const api = new Api({
host: "http://localhost",
fetcher: app.server.request as typeof fetch,
});
// create entity
await api.system.addConfig("data", "entities.posts", {
config: { sort_field: "id", sort_dir: "asc" },
fields: { id: { type: "primary" }, asdf: { type: "text" } },
type: "regular",
});
expect(app.em.entities.map((e) => e.name)).toContain("posts");
});
});
+84 -45
View File
@@ -1,56 +1,95 @@
import { describe, test } from "bun:test"; /// <reference types="@types/bun" />
import { Hono } from "hono";
import { Guard } from "../../src/auth";
import { EventManager } from "../../src/core/events";
import { EntityManager } from "../../src/data";
import { AppMedia } from "../../src/media/AppMedia";
import { MediaController } from "../../src/media/api/MediaController";
import { getDummyConnection } from "../helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection(); import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createApp } from "core/test/utils";
import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
/** const path = `${assetsPath}/image.png`;
* R2
* value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null,
* Node writefile
* data: string | NodeJS.ArrayBufferView | Iterable<string | NodeJS.ArrayBufferView> | AsyncIterable<string | NodeJS.ArrayBufferView> | Stream,
*/
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("MediaController", () => {
test("..", async () => {
const ctx: any = {
em: new EntityManager([], dummyConnection, []),
guard: new Guard(),
emgr: new EventManager(),
server: new Hono()
};
const media = new AppMedia( async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
// @ts-ignore const app = createApp({
{ initialConfig: {
enabled: true, media: mergeObject(
adapter: { {
type: "s3", enabled: true,
config: { adapter: {
access_key: process.env.R2_ACCESS_KEY as string, type: "local",
secret_access_key: process.env.R2_SECRET_ACCESS_KEY as string, config: {
url: process.env.R2_URL as string path: assetsTmpPath,
} },
} },
}, },
ctx mediaOverride,
); ),
await media.build(); },
const app = new MediaController(media).getController(); });
const file = Bun.file(`${import.meta.dir}/adapters/icon.png`); app.module.media.adapters.set("local", StorageLocalAdapter);
console.log("file", file);
await app.build();
return app;
}
function makeName(ext: string) {
return randomString(10) + "." + ext;
}
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("MediaController", () => {
test("accepts direct", async () => {
const app = await makeApp();
const file = Bun.file(path);
const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
body: file,
});
const result = (await res.json()) as any;
console.log(result);
expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name);
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
test("accepts form data", async () => {
const app = await makeApp();
const file = Bun.file(path);
const name = makeName("png");
const form = new FormData(); const form = new FormData();
form.append("file", file); form.append("file", file);
await app.request("/upload/test.png", { const res = await app.server.request("/api/media/upload/" + name, {
method: "POST", method: "POST",
body: file body: form,
}); });
const result = (await res.json()) as any;
expect(result.name).toBe(name);
const destFile = Bun.file(assetsTmpPath + "/" + name);
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
test("limits body", async () => {
const app = await makeApp({ storage: { body_max_size: 1 } });
const file = await Bun.file(path);
const name = makeName("png");
const res = await app.server.request("/api/media/upload/" + name, {
method: "POST",
body: file,
});
expect(res.status).toBe(413);
expect(await Bun.file(assetsTmpPath + "/" + name).exists()).toBe(false);
}); });
}); });
+6 -4
View File
@@ -1,8 +1,9 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type FileBody, Storage, type StorageAdapter } 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";
class TestAdapter implements StorageAdapter { class TestAdapter extends StorageAdapter {
files: Record<string, FileBody> = {}; files: Record<string, FileBody> = {};
getName() { getName() {
@@ -60,8 +61,8 @@ describe("Storage", async () => {
test("uploads a file", async () => { test("uploads a file", async () => {
const { const {
meta: { type, size } meta: { type, size },
} = await storage.uploadFile("hello", "world.txt"); } = await storage.uploadFile("hello" as any, "world.txt");
expect({ type, size }).toEqual({ type: "text/plain", size: 0 }); expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
}); });
@@ -71,6 +72,7 @@ describe("Storage", async () => {
}); });
test("events were fired", async () => { test("events were fired", async () => {
await storage.emgr.executeAsyncs();
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue(); expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue(); expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
// @todo: file access must be tested in controllers // @todo: file access must be tested in controllers
@@ -1,34 +0,0 @@
import * as assert from "node:assert/strict";
import { createWriteStream } from "node:fs";
import { test } from "node:test";
import { Miniflare } from "miniflare";
// https://github.com/nodejs/node/issues/44372#issuecomment-1736530480
console.log = async (message: any) => {
const tty = createWriteStream("/dev/tty");
const msg = typeof message === "string" ? message : JSON.stringify(message, null, 2);
return tty.write(`${msg}\n`);
};
test("what", async () => {
const mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
r2Buckets: ["BUCKET"]
});
const bucket = await mf.getR2Bucket("BUCKET");
console.log(await bucket.put("count", "1"));
const object = await bucket.get("count");
if (object) {
/*const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);*/
console.log("yo -->", await object.text());
assert.strictEqual(await object.text(), "1");
}
await mf.dispose();
});
@@ -1,61 +0,0 @@
import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageCloudinaryAdapter } from "../../../src/media";
import { config } from "dotenv";
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
const {
CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET,
CLOUDINARY_UPLOAD_PRESET
} = dotenvOutput.parsed!;
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
const adapter = new StorageCloudinaryAdapter({
cloud_name: CLOUDINARY_CLOUD_NAME as string,
api_key: CLOUDINARY_API_KEY as string,
api_secret: CLOUDINARY_API_SECRET as string,
upload_preset: CLOUDINARY_UPLOAD_PRESET as string
});
const file = Bun.file(`${import.meta.dir}/icon.png`);
const _filename = randomString(10);
const filename = `${_filename}.png`;
test("object exists", async () => {
expect(await adapter.objectExists("7fCTBi6L8c.png")).toBeTrue();
process.exit();
});
test("puts object", async () => {
expect(await adapter.objectExists(filename)).toBeFalse();
const result = await adapter.putObject(filename, file);
console.log("result", result);
expect(result).toBeDefined();
expect(result?.name).toBe(filename);
});
test("object exists", async () => {
await Bun.sleep(10000);
const one = await adapter.objectExists(_filename);
const two = await adapter.objectExists(filename);
expect(await adapter.objectExists(filename)).toBeTrue();
});
test("object meta", async () => {
const result = await adapter.getObjectMeta(filename);
console.log("objectMeta:result", result);
expect(result).toBeDefined();
expect(result.type).toBe("image/png");
expect(result.size).toBeGreaterThan(0);
});
test("list objects", async () => {
const result = await adapter.listObjects();
console.log("listObjects:result", result);
});
});
@@ -1,46 +0,0 @@
import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
describe("StorageLocalAdapter", () => {
const adapter = new StorageLocalAdapter({
path: `${import.meta.dir}/local`
});
const file = Bun.file(`${import.meta.dir}/icon.png`);
const _filename = randomString(10);
const filename = `${_filename}.png`;
let objects = 0;
test("puts an object", async () => {
objects = (await adapter.listObjects()).length;
expect(await adapter.putObject(filename, file)).toBeString();
});
test("lists objects", async () => {
expect((await adapter.listObjects()).length).toBe(objects + 1);
});
test("file exists", async () => {
expect(await adapter.objectExists(filename)).toBeTrue();
});
test("gets an object", async () => {
const res = await adapter.getObject(filename, new Headers());
expect(res.ok).toBeTrue();
// @todo: check the content
});
test("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
size: file.size
});
});
test("deletes an object", async () => {
expect(await adapter.deleteObject(filename)).toBeUndefined();
expect(await adapter.objectExists(filename)).toBeFalse();
});
});
@@ -1,96 +0,0 @@
import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageS3Adapter } from "../../../src/media";
import { config } from "dotenv";
const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` });
const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } =
dotenvOutput.parsed!;
// @todo: mock r2/s3 responses for faster tests
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
console.log("ALL_TESTS", process.env.ALL_TESTS);
const versions = [
[
"r2",
new StorageS3Adapter({
access_key: R2_ACCESS_KEY as string,
secret_access_key: R2_SECRET_ACCESS_KEY as string,
url: R2_URL as string
})
],
[
"s3",
new StorageS3Adapter({
access_key: AWS_ACCESS_KEY as string,
secret_access_key: AWS_SECRET_KEY as string,
url: AWS_S3_URL as string
})
]
] as const;
const _conf = {
adapters: ["r2", "s3"],
tests: [
"listObjects",
"putObject",
"objectExists",
"getObject",
"deleteObject",
"getObjectMeta"
]
};
const file = Bun.file(`${import.meta.dir}/icon.png`);
const filename = `${randomString(10)}.png`;
// single (dev)
//_conf = { adapters: [/*"r2",*/ "s3"], tests: [/*"putObject",*/ "listObjects"] };
function disabled(test: (typeof _conf.tests)[number]) {
return !_conf.tests.includes(test);
}
// @todo: add mocked fetch for faster tests
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
if (!_conf.adapters.includes(name)) {
console.log("Skipping", name);
return;
}
let objects = 0;
test.skipIf(disabled("putObject"))("puts an object", async () => {
objects = (await adapter.listObjects()).length;
expect(await adapter.putObject(filename, file)).toBeString();
});
test.skipIf(disabled("listObjects"))("lists objects", async () => {
expect((await adapter.listObjects()).length).toBe(objects + 1);
});
test.skipIf(disabled("objectExists"))("file exists", async () => {
expect(await adapter.objectExists(filename)).toBeTrue();
});
test.skipIf(disabled("getObject"))("gets an object", async () => {
const res = await adapter.getObject(filename, new Headers());
expect(res.ok).toBeTrue();
// @todo: check the content
});
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
size: file.size
});
});
test.skipIf(disabled("deleteObject"))("deletes an object", async () => {
expect(await adapter.deleteObject(filename)).toBeUndefined();
expect(await adapter.objectExists(filename)).toBeFalse();
});
});
});
+70 -2
View File
@@ -1,10 +1,13 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import * as large from "../../src/media/storage/mime-types"; import * as large from "../../src/media/storage/mime-types";
import * as tiny from "../../src/media/storage/mime-types-tiny"; import * as tiny from "../../src/media/storage/mime-types-tiny";
import { getRandomizedFilename } from "../../src/media/utils";
describe("media/mime-types", () => { describe("media/mime-types", () => {
test("tiny resolves", () => { test("tiny resolves", () => {
const tests = [[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"]]; const tests = [
[".mp4", "video/mp4", ".jpg", "image/jpeg", ".zip", "application/zip"],
] as const;
for (const [ext, mime] of tests) { for (const [ext, mime] of tests) {
expect(tiny.guess(ext)).toBe(mime); expect(tiny.guess(ext)).toBe(mime);
@@ -27,11 +30,76 @@ describe("media/mime-types", () => {
exts, exts,
ext, ext,
expected: ex, expected: ex,
actual: large.guessMimeType("." + ext) actual: large.guessMimeType("." + ext),
}); });
throw new Error(`Failed for ${ext}`); throw new Error(`Failed for ${ext}`);
} }
} }
} }
}); });
test("isMimeType", () => {
const tests = [
["image/avif", true],
["image/AVIF", true],
["image/jpeg", true],
["image/jpeg", true, ["image/png"]],
["image/png", false, ["image/png"]],
["image/png", true],
["image/heif", true],
["image/heic", true],
["image/gif", true],
["whatever", false],
["text/tab-separated-values", true],
["application/zip", true],
];
for (const [mime, expected, exclude] of tests) {
expect(
tiny.isMimeType(mime, exclude as any),
`isMimeType(): ${mime} should be ${expected}`,
).toBe(expected as any);
}
});
test("extension", () => {
const tests = [
["image/avif", "avif"],
["image/png", "png"],
["image/PNG", "png"],
["image/jpeg", "jpeg"],
["application/zip", "zip"],
["text/tab-separated-values", "tsv"],
["application/zip", "zip"],
] as const;
for (const [mime, ext] of tests) {
expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
}
});
test("getRandomizedFilename", () => {
const tests = [
["file.txt", "txt"],
["file.TXT", "txt"],
["image.jpg", "jpg"],
["image.avif", "avif"],
["image.heic", "heic"],
["image.jpeg", "jpeg"],
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
["-473Wx593H-466453554-black-MODEL.avif", "avif"],
] as const;
for (const [filename, ext] of tests) {
expect(
getRandomizedFilename(filename).split(".").pop(),
`getRandomizedFilename(): ${filename} should end with ${ext}`,
).toBe(ext);
}
// make sure it keeps the extension, even if the file has a different type
const file = new File([""], "image.jpg", { type: "text/plain" });
const [, ext] = getRandomizedFilename(file).split(".");
expect(ext).toBe("jpg");
});
}); });
+98 -26
View File
@@ -1,12 +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 "../../src"; 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 "../../src/data"; import { em, entity, make, text } from "../../src/data";
import { AppAuth, type ModuleBuildContext } from "../../src/modules"; import { AppAuth, type ModuleBuildContext } from "../../src/modules";
import { disableConsoleLog, enableConsoleLog } from "../helper"; import { disableConsoleLog, enableConsoleLog } from "../helper";
// @ts-ignore
import { makeCtx, moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";
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;
@@ -22,7 +29,7 @@ describe("AppAuth", () => {
const config = auth.toJSON(); const config = auth.toJSON();
expect(config.jwt).toBeUndefined(); expect(config.jwt).toBeUndefined();
expect(config.strategies.password.config).toBeUndefined(); expect(config.strategies?.password?.config).toBeUndefined();
}); });
test("enabling auth: generate secret", async () => { test("enabling auth: generate secret", async () => {
@@ -42,11 +49,12 @@ describe("AppAuth", () => {
const auth = new AppAuth( const auth = new AppAuth(
{ {
enabled: true, enabled: true,
// @ts-ignore
jwt: { jwt: {
secret: "123456" secret: "123456",
} },
}, },
ctx ctx,
); );
await auth.build(); await auth.build();
@@ -63,19 +71,78 @@ describe("AppAuth", () => {
const res = await app.request("/password/register", { const res = await app.request("/password/register", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: "some@body.com", email: "some@body.com",
password: "123456" password: "12345678",
}) }),
}); });
enableConsoleLog(); 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();
expect(users.length).toBe(1); expect(users.length).toBe(1);
expect(users[0].email).toBe("some@body.com"); expect(users[0]?.email).toBe("some@body.com");
}
});
test("creates user on register (bcrypt)", async () => {
const auth = new AppAuth(
{
enabled: true,
strategies: {
password: {
type: "password",
config: {
hashing: "bcrypt",
},
},
},
// @ts-ignore
jwt: {
secret: "123456",
},
},
ctx,
);
await auth.build();
await ctx.em.schema().sync({ force: true });
// expect no users, but the query to pass
const res = await ctx.em.repository("users").findMany();
expect(res.data.length).toBe(0);
const app = new AuthController(auth).getController();
{
disableConsoleLog();
const res = await app.request("/password/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "some@body.com",
password: "12345678",
}),
});
enableConsoleLog();
expect(res.status).toBe(200);
const { data: users } = await ctx.em.repository("users").findMany();
expect(users.length).toBe(1);
expect(users[0]?.email).toBe("some@body.com");
}
{
// check user in database
const rawUser = await ctx.connection.kysely
.selectFrom("users")
.selectAll()
.executeTakeFirstOrThrow();
expect(rawUser.strategy_value).toStartWith("$");
} }
}); });
@@ -85,13 +152,14 @@ describe("AppAuth", () => {
auth: { auth: {
enabled: true, enabled: true,
jwt: { jwt: {
secret: "123456" secret: "123456",
} },
} },
} },
}); });
await app.build(); await app.build();
app.registerAdminController();
const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh"); const spy = spyOn(app.module.auth.authenticator, "requestCookieRefresh");
// register custom route // register custom route
@@ -101,6 +169,10 @@ describe("AppAuth", () => {
await app.server.request("/api/system/ping"); await app.server.request("/api/system/ping");
await app.server.request("/test"); await app.server.request("/test");
expect(spy.mock.calls.length).toBe(0);
// admin route
await app.server.request("/");
expect(spy.mock.calls.length).toBe(1); expect(spy.mock.calls.length).toBe(1);
}); });
@@ -109,14 +181,14 @@ describe("AppAuth", () => {
initialConfig: { initialConfig: {
auth: { auth: {
entity_name: "users", entity_name: "users",
enabled: true enabled: true,
}, },
data: em({ data: em({
users: entity("users", { users: entity("users", {
additional: text() additional: text(),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
await app.build(); await app.build();
@@ -132,21 +204,21 @@ describe("AppAuth", () => {
const app = createApp({ const app = createApp({
initialConfig: { initialConfig: {
auth: { auth: {
enabled: true enabled: true,
}, },
data: em({ data: em({
users: entity("users", { users: entity("users", {
strategy: text({ strategy: text({
fillable: true, fillable: true,
hidden: false hidden: false,
}), }),
strategy_value: text({ strategy_value: text({
fillable: true, fillable: true,
hidden: false hidden: false,
}) }),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
await app.build(); await app.build();
@@ -157,7 +229,7 @@ describe("AppAuth", () => {
const authField = make(name, _authFieldProto as any); const authField = make(name, _authFieldProto as any);
const field = users.field(name)!; const field = users.field(name)!;
for (const prop of props) { for (const prop of props) {
expect(field.config[prop]).toBe(authField.config[prop]); expect(field.config[prop]).toEqual(authField.config[prop]);
} }
} }
}); });
+42 -4
View File
@@ -1,13 +1,51 @@
import { describe, expect, test } from "bun:test"; import { beforeEach, describe, expect, test } from "bun:test";
import { parse } from "../../src/core/utils"; import { parse } from "core/object/schema";
import { fieldsSchema } from "../../src/data/data-schema"; import { fieldsSchema } from "../../src/data/data-schema";
import { AppData } from "../../src/modules"; import { AppData, type ModuleBuildContext } from "../../src/modules";
import { moduleTestSuite } from "./module-test-suite"; import { makeCtx, moduleTestSuite } from "./module-test-suite";
import * as proto from "data/prototype";
describe("AppData", () => { describe("AppData", () => {
moduleTestSuite(AppData); moduleTestSuite(AppData);
let ctx: ModuleBuildContext;
beforeEach(() => {
ctx = makeCtx();
});
test("field config construction", () => { test("field config construction", () => {
expect(parse(fieldsSchema, { type: "text" })).toBeDefined(); expect(parse(fieldsSchema, { type: "text" })).toBeDefined();
}); });
test("should prevent multi-deletion of entities in single request", async () => {
const schema = proto.em({
one: proto.entity("one", {
text: proto.text(),
}),
two: proto.entity("two", {
text: proto.text(),
}),
three: proto.entity("three", {
text: proto.text(),
}),
});
const check = () => {
const expected = ["one", "two", "three"];
const fromConfig = Object.keys(data.config.entities ?? {});
const fromEm = data.em.entities.map((e) => e.name);
expect(fromConfig).toEqual(expected);
expect(fromEm).toEqual(expected);
};
// auth must be enabled, otherwise default config is returned
const data = new AppData(schema.toJSON(), ctx);
await data.build();
check();
expect(data.schema().remove("entities")).rejects.toThrow(/more than one entity/);
check();
await data.setContext(makeCtx()).build();
check();
});
}); });
+13 -12
View File
@@ -1,15 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { createApp, registries } from "../../src"; import { createApp } from "core/test/utils";
import { em, entity, text } from "../../src/data"; import { em, entity, text } from "../../src/data";
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/modules"; import { AppMedia } from "../../src/media/AppMedia";
import { moduleTestSuite } from "./module-test-suite"; import { moduleTestSuite } from "./module-test-suite";
describe("AppMedia", () => { describe("AppMedia", () => {
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({
initialConfig: { initialConfig: {
@@ -19,17 +19,18 @@ describe("AppMedia", () => {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./" path: "./",
} },
} },
}, },
data: em({ data: em({
media: entity("media", { media: entity("media", {
additional: text() additional: text(),
}) }),
}).toJSON() }).toJSON(),
} },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
@@ -49,7 +50,7 @@ describe("AppMedia", () => {
"modified_at", "modified_at",
"reference", "reference",
"entity_id", "entity_id",
"metadata" "metadata",
]); ]);
}); });
}); });
+48 -51
View File
@@ -1,23 +1,22 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { type TSchema, Type, stripMark } from "../../src/core/utils"; import { s, stripMark } from "core/object/schema";
import { EntityManager, em, entity, index, text } from "../../src/data"; import { EntityManager, em, entity, index, text } from "../../src/data";
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";
function createModule<Schema extends TSchema>(schema: Schema) { function createModule<Schema extends s.Schema>(schema: Schema) {
class TestModule extends Module<typeof schema> { return class TestModule extends Module<Schema> {
getSchema() { getSchema() {
return schema; return schema;
} }
toJSON() { override toJSON() {
return this.config; return this.config;
} }
useForceParse() { override useForceParse() {
return true; return true;
} }
} };
return TestModule;
} }
describe("Module", async () => { describe("Module", async () => {
@@ -25,7 +24,7 @@ describe("Module", async () => {
test("listener", async () => { test("listener", async () => {
let result: any; let result: any;
const module = createModule(Type.Object({ a: Type.String() })); const module = createModule(s.object({ a: s.string() }));
const m = new module({ a: "test" }); const m = new module({ a: "test" });
await m.schema().set({ a: "test2" }); await m.schema().set({ a: "test2" });
@@ -41,13 +40,13 @@ describe("Module", async () => {
describe("db schema", () => { describe("db schema", () => {
class M extends Module { class M extends Module {
override getSchema() { override getSchema() {
return Type.Object({}); return s.object({});
} }
prt = { prt = {
ensureEntity: this.ensureEntity.bind(this), ensureEntity: this.ctx.helper.ensureEntity.bind(this.ctx.helper),
ensureIndex: this.ensureIndex.bind(this), ensureIndex: this.ctx.helper.ensureIndex.bind(this.ctx.helper),
ensureSchema: this.ensureSchema.bind(this) ensureSchema: this.ctx.helper.ensureSchema.bind(this.ctx.helper),
}; };
get em() { get em() {
@@ -60,23 +59,27 @@ describe("Module", async () => {
Object.values(_em.entities), Object.values(_em.entities),
new DummyConnection(), new DummyConnection(),
_em.relations, _em.relations,
_em.indices _em.indices,
); );
return new M({} as any, { em, flags: Module.ctx_flags } as any); const ctx = {
em,
flags: Module.ctx_flags,
};
return new M({} as any, { ...ctx, helper: new ModuleHelper(ctx as any) } as any);
} }
function flat(_em: EntityManager) { function flat(_em: EntityManager) {
return { return {
entities: _em.entities.map((e) => ({ entities: _em.entities.map((e) => ({
name: e.name, name: e.name,
fields: e.fields.map((f) => f.name), fields: e.fields.map((f) => f.name),
type: e.type type: e.type,
})), })),
indices: _em.indices.map((i) => ({ indices: _em.indices.map((i) => ({
name: i.name, name: i.name,
entity: i.entity.name, entity: i.entity.name,
fields: i.fields.map((f) => f.name), fields: i.fields.map((f) => f.name),
unique: i.unique unique: i.unique,
})) })),
}; };
} }
@@ -88,15 +91,15 @@ describe("Module", async () => {
expect(flat(make(initial).em)).toEqual({ expect(flat(make(initial).em)).toEqual({
entities: [], entities: [],
indices: [] indices: [],
}); });
}); });
test("init", () => { test("init", () => {
const initial = em({ const initial = em({
users: entity("u", { users: entity("u", {
name: text() name: text(),
}) }),
}); });
const m = make(initial); const m = make(initial);
@@ -107,18 +110,18 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name"], fields: ["id", "name"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
}); });
test("ensure entity", () => { test("ensure entity", () => {
const initial = em({ const initial = em({
users: entity("u", { users: entity("u", {
name: text() name: text(),
}) }),
}); });
const m = make(initial); const m = make(initial);
@@ -127,29 +130,24 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name"], fields: ["id", "name"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
// this should add a new entity // this should add a new entity
m.prt.ensureEntity( m.prt.ensureEntity(
entity("p", { entity("p", {
title: text() title: text(),
}) }),
); );
// this should only add the field "important" // this should only add the field "important"
m.prt.ensureEntity( m.prt.ensureEntity(
entity( entity("u", {
"u", important: text(),
{ }),
important: text()
},
undefined,
"system"
)
); );
expect(m.ctx.flags.sync_required).toBe(true); expect(m.ctx.flags.sync_required).toBe(true);
@@ -158,23 +156,22 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name", "important"], fields: ["id", "name", "important"],
// ensured type must be present type: "regular",
type: "system"
}, },
{ {
name: "p", name: "p",
fields: ["id", "title"], fields: ["id", "title"],
type: "regular" type: "regular",
} },
], ],
indices: [] indices: [],
}); });
}); });
test("ensure index", () => { test("ensure index", () => {
const users = entity("u", { const users = entity("u", {
name: text(), name: text(),
title: text() title: text(),
}); });
const initial = em({ users }, ({ index }, { users }) => { const initial = em({ users }, ({ index }, { users }) => {
index(users).on(["title"]); index(users).on(["title"]);
@@ -189,23 +186,23 @@ describe("Module", async () => {
{ {
name: "u", name: "u",
fields: ["id", "name", "title"], fields: ["id", "name", "title"],
type: "regular" type: "regular",
} },
], ],
indices: [ indices: [
{ {
name: "idx_u_title", name: "idx_u_title",
entity: "u", entity: "u",
fields: ["title"], fields: ["title"],
unique: false unique: false,
}, },
{ {
name: "idx_u_name", name: "idx_u_name",
entity: "u", entity: "u",
fields: ["name"], fields: ["name"],
unique: false unique: false,
} },
] ],
}); });
}); });
}); });
+299 -42
View File
@@ -1,9 +1,11 @@
import { describe, expect, test } from "bun:test"; import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { stripMark } from "../../src/core/utils"; import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { entity, text } from "../../src/data"; import { Connection, entity, text } from "data";
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager"; import { Module } from "modules/Module";
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations"; import { type ConfigTable, getDefaultConfig, ModuleManager } from "modules/ModuleManager";
import { CURRENT_VERSION, TABLE_NAME } from "modules/migrations";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { s, stripMark } from "core/object/schema";
describe("ModuleManager", async () => { describe("ModuleManager", async () => {
test("s1: no config, no build", async () => { test("s1: no config, no build", async () => {
@@ -38,11 +40,11 @@ describe("ModuleManager", async () => {
basepath: "/api/data2", basepath: "/api/data2",
entities: { entities: {
test: entity("test", { test: entity("test", {
content: text() content: text(),
}).toJSON() }).toJSON(),
} },
} },
}); }) as any;
//const { version, ...json } = mm.toJSON() as any; //const { version, ...json } = mm.toJSON() as any;
const c2 = getDummyConnection(); const c2 = getDummyConnection();
@@ -68,11 +70,11 @@ describe("ModuleManager", async () => {
basepath: "/api/data2", basepath: "/api/data2",
entities: { entities: {
test: entity("test", { test: entity("test", {
content: text() content: text(),
}).toJSON() }).toJSON(),
} },
} },
}; } as any;
//const { version, ...json } = mm.toJSON() as any; //const { version, ...json } = mm.toJSON() as any;
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
@@ -88,24 +90,25 @@ describe("ModuleManager", async () => {
await mm2.build(); await mm2.build();
expect(stripMark(json)).toEqual(stripMark(mm2.configs())); /* console.log({
expect(mm2.configs().data.entities.test).toBeDefined(); json,
expect(mm2.configs().data.entities.test.fields.content).toBeDefined(); configs: mm2.configs(),
expect(mm2.get("data").toJSON().entities.test.fields.content).toBeDefined(); }); */
//expect(stripMark(json)).toEqual(stripMark(mm2.configs()));
expect(mm2.configs().data.entities?.test).toBeDefined();
expect(mm2.configs().data.entities?.test?.fields?.content).toBeDefined();
expect(mm2.get("data").toJSON().entities?.test?.fields?.content).toBeDefined();
}); });
test("s4: config given, table exists, version outdated, migrate", async () => { test("s4: config given, table exists, version outdated, migrate", 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();
const version = mm.version();
const json = mm.configs(); const json = mm.configs();
const c2 = getDummyConnection(); const c2 = getDummyConnection();
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 }
});
await mm2.syncConfigTable(); await mm2.syncConfigTable();
await db await db
@@ -128,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 } initial: { version: version - 1, ...json },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
await db await db
@@ -156,8 +159,8 @@ describe("ModuleManager", async () => {
...json, ...json,
data: { data: {
...json.data, ...json.data,
basepath: "/api/data2" basepath: "/api/data2",
} },
}; };
await db await db
.insertInto(TABLE_NAME) .insertInto(TABLE_NAME)
@@ -170,14 +173,14 @@ describe("ModuleManager", async () => {
expect(mm2.configs().data.basepath).toBe("/api/data2"); expect(mm2.configs().data.basepath).toBe("/api/data2");
}); });
test("blank app, modify config", async () => { /*test("blank app, modify config", async () => {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
const mm = new ModuleManager(dummyConnection); const mm = new ModuleManager(dummyConnection);
await mm.build(); await mm.build();
const configs = stripMark(mm.configs()); const configs = stripMark(mm.configs());
expect(mm.configs().server.admin.color_scheme).toBe("light"); expect(mm.configs().server.admin.color_scheme).toBeUndefined();
expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow(); expect(() => mm.get("server").schema().patch("admin", { color_scheme: "violet" })).toThrow();
await mm.get("server").schema().patch("admin", { color_scheme: "dark" }); await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
await mm.save(); await mm.save();
@@ -189,35 +192,37 @@ describe("ModuleManager", async () => {
...configs.server, ...configs.server,
admin: { admin: {
...configs.server.admin, ...configs.server.admin,
color_scheme: "dark" color_scheme: "dark",
} },
} },
}); });
}); });*/
test("partial config given", async () => { test("partial config given", async () => {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
const partial = { const partial = {
auth: { auth: {
enabled: true enabled: true,
} },
}; };
const mm = new ModuleManager(dummyConnection, { const mm = new ModuleManager(dummyConnection, {
initial: partial initial: partial,
}); });
await mm.build(); await mm.build();
expect(mm.version()).toBe(CURRENT_VERSION); expect(mm.version()).toBe(CURRENT_VERSION);
expect(mm.built()).toBe(true); expect(mm.built()).toBe(true);
expect(mm.configs().auth.enabled).toBe(true); expect(mm.configs().auth.enabled).toBe(true);
expect(mm.configs().data.entities.users).toBeDefined(); expect(mm.configs().data.entities?.users).toBeDefined();
}); });
test("partial config given, but db version exists", async () => { test("partial config given, but db version exists", 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();
@@ -226,9 +231,9 @@ describe("ModuleManager", async () => {
const mm2 = new ModuleManager(c2.dummyConnection, { const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { initial: {
auth: { auth: {
basepath: "/shouldnt/take/this" basepath: "/shouldnt/take/this",
} },
} },
}); });
await mm2.syncConfigTable(); await mm2.syncConfigTable();
const payload = { const payload = {
@@ -236,15 +241,15 @@ describe("ModuleManager", async () => {
auth: { auth: {
...json.auth, ...json.auth,
enabled: true, enabled: true,
basepath: "/api/auth2" basepath: "/api/auth2",
} },
}; };
await db await db
.insertInto(TABLE_NAME) .insertInto(TABLE_NAME)
.values({ .values({
type: "config", type: "config",
json: JSON.stringify(payload), json: JSON.stringify(payload),
version: CURRENT_VERSION version: CURRENT_VERSION,
}) })
.execute(); .execute();
await mm2.build(); await mm2.build();
@@ -252,4 +257,256 @@ describe("ModuleManager", async () => {
}); });
// @todo: add tests for migrations (check "backup" and new version) // @todo: add tests for migrations (check "backup" and new version)
describe("revert", async () => {
const failingModuleSchema = s.partialObject({
value: s.number(),
});
class FailingModule extends Module<s.Static<typeof failingModuleSchema>> {
getSchema() {
return failingModuleSchema;
}
override async build() {
//console.log("building FailingModule", this.config);
if (this.config.value && this.config.value < 0) {
throw new Error("value must be positive, given: " + this.config.value);
}
this.setBuilt();
}
}
class TestModuleManager extends ModuleManager {
constructor(...args: ConstructorParameters<typeof ModuleManager>) {
super(...args);
const [, options] = args;
// @ts-ignore
const initial = options?.initial?.failing ?? {};
this.modules["failing"] = new FailingModule(initial, this.ctx());
this.modules["failing"].setListener(async (c) => {
// @ts-ignore
await this.onModuleConfigUpdated("failing", c);
});
}
}
beforeEach(() => disableConsoleLog(["log", "warn", "error"]));
afterEach(enableConsoleLog);
test("it builds", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection);
expect(mm).toBeDefined();
await mm.build();
expect(mm.toJSON()).toBeDefined();
});
test("it accepts config", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
failing: { value: 2 },
},
});
await mm.build();
expect(mm.configs()["failing"].value).toBe(2);
});
test("it crashes on invalid", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
failing: { value: -1 },
},
});
expect(mm.build()).rejects.toThrow(/value must be positive/);
expect(mm.configs()["failing"].value).toBe(-1);
});
test("it correctly accepts valid", async () => {
const mockOnUpdated = mock(() => null);
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection, {
onUpdated: async () => {
mockOnUpdated();
},
});
await mm.build();
// @ts-ignore
const f = mm.mutateConfigSafe("failing");
// @ts-ignore
expect(f.set({ value: 2 })).resolves.toBeDefined();
expect(mockOnUpdated).toHaveBeenCalled();
});
test("it reverts on safe mutate", async () => {
const mockOnUpdated = mock(() => null);
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection, {
initial: {
// @ts-ignore
failing: { value: 1 },
},
onUpdated: async () => {
mockOnUpdated();
},
});
await mm.build();
expect(mm.configs()["failing"].value).toBe(1);
// now safe mutate
// @ts-ignore
expect(mm.mutateConfigSafe("failing").set({ value: -2 })).rejects.toThrow(
/value must be positive/,
);
expect(mm.configs()["failing"].value).toBe(1);
expect(mockOnUpdated).toHaveBeenCalled();
});
test("it only accepts schema mutating methods", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new TestModuleManager(dummyConnection);
await mm.build();
// @ts-ignore
const f = mm.mutateConfigSafe("failing");
// @ts-expect-error
expect(() => f.has("value")).toThrow();
// @ts-expect-error
expect(() => f.bypass()).toThrow();
// @ts-expect-error
expect(() => f.clone()).toThrow();
// @ts-expect-error
expect(() => f.get()).toThrow();
// @ts-expect-error
expect(() => f.default()).toThrow();
});
});
async function getRawConfig(c: Connection) {
return (await c.kysely
.selectFrom(TABLE_NAME)
.selectAll()
.where("type", "=", "config")
.orderBy("version", "desc")
.executeTakeFirstOrThrow()) as unknown as ConfigTable;
}
async function getDiffs(c: Connection, opts?: { dir?: "asc" | "desc"; limit?: number }) {
return await c.kysely
.selectFrom(TABLE_NAME)
.selectAll()
.where("type", "=", "diff")
.orderBy("version", opts?.dir ?? "desc")
.$if(!!opts?.limit, (b) => b.limit(opts!.limit!))
.execute();
}
describe("diffs", () => {
test("never empty", async () => {
const { dummyConnection: c } = getDummyConnection();
const mm = new ModuleManager(c);
await mm.build();
await mm.save();
expect(await getDiffs(c)).toHaveLength(0);
});
test("has timestamps", async () => {
const { dummyConnection: c } = getDummyConnection();
const mm = new ModuleManager(c);
await mm.build();
await mm.get("data").schema().patch("basepath", "/api/data2");
await mm.save();
const config = await getRawConfig(c);
const diffs = await getDiffs(c);
expect(config.json.data.basepath).toBe("/api/data2");
expect(diffs).toHaveLength(1);
expect(diffs[0]!.created_at).toBeDefined();
expect(diffs[0]!.updated_at).toBeDefined();
});
});
describe("validate & revert", () => {
const schema = s.object({
value: s.array(s.number()),
});
type SampleSchema = s.Static<typeof schema>;
class Sample extends Module<SampleSchema> {
getSchema() {
return schema;
}
override async build() {
this.setBuilt();
}
override async onBeforeUpdate(from: SampleSchema, to: SampleSchema) {
if (to.value.length > 3) {
throw new Error("too many values");
}
if (to.value.includes(7)) {
throw new Error("contains 7");
}
return to;
}
}
class TestModuleManager extends ModuleManager {
constructor(...args: ConstructorParameters<typeof ModuleManager>) {
super(...args);
this.modules["module1"] = new Sample({}, this.ctx());
}
}
test("respects module onBeforeUpdate", async () => {
const { dummyConnection: c } = getDummyConnection();
const mm = new TestModuleManager(c);
await mm.build();
const m = mm.get("module1" as any) as Sample;
{
expect(async () => {
await m.schema().set({ value: [1, 2, 3, 4, 5] });
return mm.save();
}).toThrow(/too many values/);
expect(m.config.value).toHaveLength(0);
expect((mm.configs() as any).module1.value).toHaveLength(0);
}
{
expect(async () => {
await mm.mutateConfigSafe("module1" as any).set({ value: [1, 2, 3, 4, 5] });
return mm.save();
}).toThrow(/too many values/);
expect(m.config.value).toHaveLength(0);
expect((mm.configs() as any).module1.value).toHaveLength(0);
}
{
expect(async () => {
await m.schema().set({ value: [1, 7, 5] });
return mm.save();
}).toThrow(/contains 7/);
expect(m.config.value).toHaveLength(0);
expect((mm.configs() as any).module1.value).toHaveLength(0);
}
{
expect(async () => {
await mm.mutateConfigSafe("module1" as any).set({ value: [1, 7, 5] });
return mm.save();
}).toThrow(/contains 7/);
expect(m.config.value).toHaveLength(0);
expect((mm.configs() as any).module1.value).toHaveLength(0);
}
});
});
}); });
@@ -0,0 +1,81 @@
import { describe, expect, test } from "bun:test";
import { type InitialModuleConfigs, createApp } from "../../../src";
import { type Kysely, sql } from "kysely";
import { getDummyConnection } from "../../helper";
import v7 from "./samples/v7.json";
import v8 from "./samples/v8.json";
import v8_2 from "./samples/v8-2.json";
// app expects migratable config to be present in database
async function createVersionedApp(config: InitialModuleConfigs | any) {
const { dummyConnection } = getDummyConnection();
if (!("version" in config)) throw new Error("config must have a version");
const { version, ...rest } = config;
const db = dummyConnection.kysely as Kysely<any>;
await sql`CREATE TABLE "__bknd" (
"id" integer not null primary key autoincrement,
"version" integer,
"type" text,
"json" text,
"created_at" datetime,
"updated_at" datetime
)`.execute(db);
await db
.insertInto("__bknd")
.values({
version,
type: "config",
created_at: new Date().toISOString(),
json: JSON.stringify(rest),
})
.execute();
const app = createApp({
connection: dummyConnection,
});
await app.build();
return app;
}
describe("Migrations", () => {
/**
* updated auth strategies to have "enabled" prop
* by default, migration should make all available strategies enabled
*/
test("migration from 7 to 8", async () => {
expect(v7.version).toBe(7);
const app = await createVersionedApp(v7);
expect(app.version()).toBeGreaterThan(7);
expect(app.toJSON(true).auth.strategies?.password?.enabled).toBe(true);
const req = await app.server.request("/api/auth/password/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "test@test.com",
password: "12345678",
}),
});
expect(req.ok).toBe(true);
const res = (await req.json()) as any;
expect(res.user.email).toBe("test@test.com");
});
test("migration from 8 to 9", async () => {
expect(v8.version).toBe(8);
const app = await createVersionedApp(v8);
expect(app.version()).toBeGreaterThan(8);
// @ts-expect-error
expect(app.toJSON(true).server.admin).toBeUndefined();
});
});
@@ -0,0 +1,638 @@
{
"version": 7,
"server": {
"admin": {
"basepath": "",
"logo_return_path": "/",
"color_scheme": "light"
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
]
}
},
"data": {
"basepath": "/api/data",
"entities": {
"products": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"brand": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"currency": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"price": {
"type": "number",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"price_compare": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"url": {
"type": "text",
"config": {
"html_config": {
"element": "input"
},
"required": true,
"fillable": true,
"hidden": ["table"]
}
},
"created_at": {
"type": "date",
"config": {
"type": "date",
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"description": {
"type": "text",
"config": {
"html_config": {
"element": "textarea",
"props": {
"rows": 4
}
},
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"images": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "products"
}
},
"identifier": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"metadata": {
"type": "jsonschema",
"config": {
"schema": {
"type": "object",
"properties": {
"size": {
"type": "string"
}
}
},
"required": false,
"fillable": true,
"hidden": ["table"]
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"path": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"size": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"modified_at": {
"type": "date",
"config": {
"type": "datetime",
"required": false,
"fillable": true,
"hidden": false
}
},
"reference": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"entity_id": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"metadata": {
"type": "json",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"email": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"strategy": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["password"]
},
"required": true,
"fillable": ["create"],
"hidden": ["update", "form"]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["guest", "admin"]
},
"required": false,
"fillable": true,
"hidden": false
}
},
"username": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"name": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"product_likes": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"created_at": {
"type": "date",
"config": {
"type": "date",
"required": false,
"fillable": true,
"hidden": false
}
},
"users_id": {
"type": "relation",
"config": {
"label": "User",
"required": true,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"products_id": {
"type": "relation",
"config": {
"label": "Product",
"required": true,
"reference": "products",
"target": "products",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
}
},
"config": {
"name": "Product Likes",
"sort_field": "id",
"sort_dir": "asc"
}
},
"boards": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"private": {
"type": "boolean",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": true,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"images": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "boards",
"max_items": 5
}
},
"cover": {
"type": "number",
"config": {
"default_value": 0,
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"boards_products": {
"type": "generated",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"boards_id": {
"type": "relation",
"config": {
"required": false,
"reference": "boards",
"target": "boards",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"products_id": {
"type": "relation",
"config": {
"required": false,
"reference": "products",
"target": "products",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
}
},
"relations": {
"poly_products_media_images": {
"type": "poly",
"source": "products",
"target": "media",
"config": {
"mappedBy": "images"
}
},
"n1_product_likes_users": {
"type": "n:1",
"source": "product_likes",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"n1_product_likes_products": {
"type": "n:1",
"source": "product_likes",
"target": "products",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"n1_boards_users": {
"type": "n:1",
"source": "boards",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"poly_boards_media_images": {
"type": "poly",
"source": "boards",
"target": "media",
"config": {
"mappedBy": "images",
"targetCardinality": 5
}
},
"mn_boards_products_boards_products,boards_products": {
"type": "m:n",
"source": "boards",
"target": "products",
"config": {}
}
},
"indices": {
"idx_unique_media_path": {
"entity": "media",
"fields": ["path"],
"unique": true
},
"idx_media_reference": {
"entity": "media",
"fields": ["reference"],
"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_product_likes_unique_products_id_users_id": {
"entity": "product_likes",
"fields": ["products_id", "users_id"],
"unique": true
},
"idx_media_entity_id": {
"entity": "media",
"fields": ["entity_id"],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "...",
"alg": "HS256",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"type": "password",
"config": {
"hashing": "sha256"
}
}
},
"roles": {
"guest": {
"is_default": true,
"permissions": ["system.access.api", "data.entity.read"]
},
"admin": {
"implicit_allow": true
}
},
"guard": {
"enabled": true
}
},
"media": {
"enabled": true,
"basepath": "/api/media",
"entity_name": "media",
"storage": {},
"adapter": {
"type": "s3",
"config": {
"access_key": "...",
"secret_access_key": "...",
"url": "https://some.r2.cloudflarestorage.com/some"
}
}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
@@ -0,0 +1,709 @@
{
"version": 8,
"server": {
"admin": {
"basepath": "",
"color_scheme": "light",
"logo_return_path": "/"
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
]
}
},
"data": {
"basepath": "/api/data",
"entities": {
"products": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"brand": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"currency": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"price": {
"type": "number",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"price_compare": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"url": {
"type": "text",
"config": {
"html_config": {
"element": "input"
},
"required": true,
"fillable": true,
"hidden": ["table"]
}
},
"created_at": {
"type": "date",
"config": {
"type": "date",
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"description": {
"type": "text",
"config": {
"html_config": {
"element": "textarea",
"props": {
"rows": 4
}
},
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"images": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "products"
}
},
"identifier": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"metadata": {
"type": "jsonschema",
"config": {
"schema": {
"type": "object",
"properties": {
"size": {
"type": "string"
},
"gender": {
"type": "string"
},
"ai_description": {
"type": "string"
}
},
"additionalProperties": {
"type": ["string", "number"]
}
},
"required": false,
"fillable": true,
"hidden": ["table"]
}
},
"_likes": {
"type": "number",
"config": {
"default_value": 0,
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc"
}
},
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"path": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"size": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"modified_at": {
"type": "date",
"config": {
"type": "datetime",
"required": false,
"fillable": true,
"hidden": false
}
},
"reference": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"entity_id": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"metadata": {
"type": "json",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"temporary": {
"type": "boolean",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc"
}
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"email": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"strategy": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["password"]
},
"required": true,
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["guest", "admin"]
},
"required": false,
"fillable": true,
"hidden": false
}
},
"username": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"name": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"_boards": {
"type": "number",
"config": {
"default_value": 0,
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc"
}
},
"product_likes": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"created_at": {
"type": "date",
"config": {
"type": "datetime",
"required": false,
"fillable": true,
"hidden": false
}
},
"users_id": {
"type": "relation",
"config": {
"label": "User",
"required": true,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"products_id": {
"type": "relation",
"config": {
"label": "Product",
"required": true,
"reference": "products",
"target": "products",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc",
"name": "Product Likes"
}
},
"boards": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"private": {
"type": "boolean",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"description": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": true,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"images": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "boards",
"max_items": 5
}
},
"cover": {
"type": "number",
"config": {
"default_value": 0,
"required": false,
"fillable": true,
"hidden": false
}
},
"_products": {
"type": "number",
"config": {
"default_value": 0,
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc"
}
},
"boards_products": {
"type": "generated",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"boards_id": {
"type": "relation",
"config": {
"required": true,
"reference": "boards",
"target": "boards",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"products_id": {
"type": "relation",
"config": {
"required": true,
"reference": "products",
"target": "products",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"manual": {
"type": "boolean",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "desc"
}
}
},
"relations": {
"poly_products_media_images": {
"type": "poly",
"source": "products",
"target": "media",
"config": {
"mappedBy": "images"
}
},
"n1_product_likes_users": {
"type": "n:1",
"source": "product_likes",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"n1_product_likes_products": {
"type": "n:1",
"source": "product_likes",
"target": "products",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"n1_boards_users": {
"type": "n:1",
"source": "boards",
"target": "users",
"config": {
"mappedBy": "",
"inversedBy": "",
"required": true,
"with_limit": 5
}
},
"poly_boards_media_images": {
"type": "poly",
"source": "boards",
"target": "media",
"config": {
"mappedBy": "images",
"targetCardinality": 5
}
},
"mn_boards_products_boards_products,boards_products": {
"type": "m:n",
"source": "boards",
"target": "products",
"config": {}
}
},
"indices": {
"idx_unique_media_path": {
"entity": "media",
"fields": ["path"],
"unique": true
},
"idx_media_reference": {
"entity": "media",
"fields": ["reference"],
"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_product_likes_unique_products_id_users_id": {
"entity": "product_likes",
"fields": ["products_id", "users_id"],
"unique": true
},
"idx_boards_products_unique_boards_id_products_id": {
"entity": "boards_products",
"fields": ["boards_id", "products_id"],
"unique": true
},
"idx_media_entity_id": {
"entity": "media",
"fields": ["entity_id"],
"unique": false
},
"idx_products_identifier": {
"entity": "products",
"fields": ["identifier"],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "2wY76Z$JQg(3t?Wn8g,^ZqZhlmjNx<@uQjI!7i^XZBF11Xa1>zZK2??Y[D|]cc%k",
"alg": "HS256",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"enabled": true,
"type": "password",
"config": {
"hashing": "sha256"
}
}
},
"roles": {
"guest": {
"is_default": true,
"permissions": ["system.access.api", "data.entity.read"]
},
"admin": {
"implicit_allow": true
}
},
"guard": {
"enabled": true
}
},
"media": {
"enabled": true,
"basepath": "/api/media",
"entity_name": "media",
"storage": {},
"adapter": {
"type": "s3",
"config": {
"access_key": "123",
"secret_access_key": "123",
"url": "https://123.r2.cloudflarestorage.com/enly"
}
}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
@@ -0,0 +1,598 @@
{
"version": 8,
"server": {
"admin": {
"basepath": "",
"logo_return_path": "/",
"color_scheme": "dark"
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
]
}
},
"data": {
"basepath": "/api/data",
"entities": {
"posts": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"slug": {
"type": "text",
"config": {
"html_config": {
"element": "input"
},
"pattern": "^[a-z\\-\\_0-9]+$",
"required": false,
"fillable": true,
"hidden": false,
"label": "Slug"
}
},
"content": {
"type": "text",
"config": {
"html_config": {
"element": "textarea",
"props": {
"rows": 5
}
},
"required": false,
"fillable": true,
"hidden": ["form"]
}
},
"active": {
"type": "boolean",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"images": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "posts"
}
},
"tags": {
"type": "jsonschema",
"config": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
},
"ui_schema": {
"ui:options": {
"orderable": false
}
},
"required": false,
"fillable": true,
"hidden": false
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": false,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
}
},
"config": {
"sort_field": "title",
"sort_dir": "desc"
}
},
"comments": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"content": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"created_at": {
"type": "date",
"config": {
"type": "date",
"required": false,
"fillable": true,
"hidden": false
}
},
"posts_id": {
"type": "relation",
"config": {
"label": "Posts",
"required": true,
"reference": "posts",
"target": "posts",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
},
"users_id": {
"type": "relation",
"config": {
"label": "Users",
"required": true,
"reference": "users",
"target": "users",
"target_field": "id",
"fillable": true,
"hidden": false,
"on_delete": "set null"
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"media": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"path": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"folder": {
"type": "boolean",
"config": {
"default_value": false,
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"mime_type": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"size": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"scope": {
"type": "text",
"config": {
"hidden": true,
"fillable": ["create"],
"required": false
}
},
"etag": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"modified_at": {
"type": "date",
"config": {
"type": "datetime",
"required": false,
"fillable": true,
"hidden": false
}
},
"reference": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"entity_id": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"metadata": {
"type": "json",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"email": {
"type": "text",
"config": {
"required": true,
"fillable": true,
"hidden": false
}
},
"strategy": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["password", "google"]
},
"required": true,
"fillable": ["create"],
"hidden": ["update", "form"]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": ["create"],
"hidden": ["read", "table", "update", "form"],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": ["guest", "admin", "editor"]
},
"required": false,
"fillable": true,
"hidden": false
}
},
"avatar": {
"type": "media",
"config": {
"required": false,
"fillable": ["update"],
"hidden": false,
"mime_types": [],
"virtual": true,
"entity": "users",
"max_items": 1
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"test": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"fillable": false,
"required": false,
"hidden": false
}
},
"title": {
"type": "text",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
},
"number": {
"type": "number",
"config": {
"required": false,
"fillable": true,
"hidden": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
}
},
"relations": {
"n1_comments_posts": {
"type": "n:1",
"source": "comments",
"target": "posts",
"config": {
"required": true,
"with_limit": 5
}
},
"poly_posts_media_images": {
"type": "poly",
"source": "posts",
"target": "media",
"config": {
"mappedBy": "images"
}
},
"n1_posts_users": {
"type": "n:1",
"source": "posts",
"target": "users",
"config": {
"with_limit": 5
}
},
"n1_comments_users": {
"type": "n:1",
"source": "comments",
"target": "users",
"config": {
"required": true,
"with_limit": 5
}
},
"poly_users_media_avatar": {
"type": "poly",
"source": "users",
"target": "media",
"config": {
"mappedBy": "avatar",
"targetCardinality": 1
}
}
},
"indices": {
"idx_unique_media_path": {
"entity": "media",
"fields": ["path"],
"unique": true
},
"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_media_reference": {
"entity": "media",
"fields": ["reference"],
"unique": false
},
"idx_media_entity_id": {
"entity": "media",
"fields": ["entity_id"],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"jwt": {
"secret": "A%3jk*wD!Zruj123123123j$Wm8qS8m8qS8",
"alg": "HS256",
"fields": ["id", "email", "role"],
"issuer": "showoff"
},
"guard": {
"enabled": true
},
"strategies": {
"password": {
"enabled": true,
"type": "password",
"config": {
"hashing": "sha256"
}
},
"google": {
"enabled": true,
"type": "oauth",
"config": {
"type": "oidc",
"client": {
"client_id": "545948917277-123ieuifrag.apps.googleusercontent.com",
"client_secret": "123-123hTTZfDDGPDPp"
},
"name": "google"
}
}
},
"roles": {
"guest": {
"permissions": [
"data.entity.read",
"system.access.api",
"system.config.read"
],
"is_default": true
},
"admin": {
"is_default": false,
"implicit_allow": true
},
"editor": {
"permissions": [
"system.access.admin",
"system.config.read",
"system.schema.read",
"system.config.read.secrets",
"system.access.api",
"data.entity.read",
"data.entity.update",
"data.entity.delete",
"data.entity.create"
]
}
},
"allow_register": true,
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
}
},
"media": {
"enabled": true,
"basepath": "/api/media",
"entity_name": "media",
"storage": {},
"adapter": {
"type": "s3",
"config": {
"access_key": "123",
"secret_access_key": "123",
"url": "https://123.r2.cloudflarestorage.com/bknd-123"
}
}
},
"flows": {
"basepath": "/api/flows",
"flows": {
"test": {
"trigger": {
"type": "http",
"config": {
"mode": "sync",
"method": "GET",
"response_type": "html",
"path": "/json-posts"
}
},
"tasks": {
"fetching": {
"type": "fetch",
"params": {
"method": "GET",
"headers": [],
"url": "https://jsonplaceholder.typicode.com/posts"
}
},
"render": {
"type": "render",
"params": {
"render": "<h1>Posts</h1>\n<ul>\n {% for post in fetching.output %}\n <li>{{ post.title }}</li>\n {% endfor %}\n</ul>"
}
}
},
"connections": {
"5cce66b5-57c6-4541-88ac-b298794c6c52": {
"source": "fetching",
"target": "render",
"config": {
"condition": {
"type": "success"
}
}
}
},
"start_task": "fetching",
"responding_task": "render"
}
}
}
}
+9 -4
View File
@@ -4,14 +4,14 @@ import { Hono } from "hono";
import { Guard } from "../../src/auth"; import { Guard } from "../../src/auth";
import { DebugLogger } from "../../src/core"; import { DebugLogger } from "../../src/core";
import { EventManager } from "../../src/core/events"; import { EventManager } from "../../src/core/events";
import { Default, stripMark } from "../../src/core/utils";
import { EntityManager } from "../../src/data"; import { EntityManager } from "../../src/data";
import { Module, type ModuleBuildContext } from "../../src/modules/Module"; import { Module, type ModuleBuildContext } from "../../src/modules/Module";
import { getDummyConnection } from "../helper"; import { getDummyConnection } from "../helper";
import { ModuleHelper } from "modules/ModuleHelper";
export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext { export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildContext {
const { dummyConnection } = getDummyConnection(); const { dummyConnection } = getDummyConnection();
return { const ctx = {
connection: dummyConnection, connection: dummyConnection,
server: new Hono(), server: new Hono(),
em: new EntityManager([], dummyConnection), em: new EntityManager([], dummyConnection),
@@ -19,8 +19,12 @@ 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),
...overrides ...overrides,
}; };
return {
...ctx,
helper: new ModuleHelper(ctx as any),
} as any;
} }
export function moduleTestSuite(module: { new (): Module }) { export function moduleTestSuite(module: { new (): Module }) {
@@ -40,7 +44,8 @@ export function moduleTestSuite(module: { new (): Module }) {
it("uses the default config", async () => { it("uses the default config", async () => {
const m = new module(); const m = new module();
await m.setContext(ctx).build(); await m.setContext(ctx).build();
expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {})); expect(m.toJSON()).toEqual(m.getSchema().template({}, { withOptional: true }));
//expect(stripMark(m.toJSON())).toEqual(Default(m.getSchema(), {}));
}); });
}); });
} }
+17 -17
View File
@@ -16,7 +16,7 @@ describe("json form", () => {
["0", { type: "boolean" }, false], ["0", { type: "boolean" }, false],
["on", { type: "boolean" }, true], ["on", { type: "boolean" }, true],
["off", { type: "boolean" }, false], ["off", { type: "boolean" }, false],
["null", { type: "null" }, null] ["null", { type: "null" }, null],
] satisfies [string, Exclude<JSONSchema, boolean>, any][]; ] satisfies [string, Exclude<JSONSchema, boolean>, any][];
for (const [input, schema, output] of examples) { for (const [input, schema, output] of examples) {
@@ -35,7 +35,7 @@ describe("json form", () => {
["array", "array", true], ["array", "array", true],
["object", "array", false], ["object", "array", false],
[["string", "number"], "number", true], [["string", "number"], "number", true],
["number", ["string", "number"], true] ["number", ["string", "number"], true],
] satisfies [IsTypeType, IsTypeType, boolean][]; ] satisfies [IsTypeType, IsTypeType, boolean][];
for (const [type, schemaType, output] of examples) { for (const [type, schemaType, output] of examples) {
@@ -48,7 +48,7 @@ describe("json form", () => {
["#/nested/property/0/name", "#/nested/property/0"], ["#/nested/property/0/name", "#/nested/property/0"],
["#/nested/property/0", "#/nested/property"], ["#/nested/property/0", "#/nested/property"],
["#/nested/property", "#/nested"], ["#/nested/property", "#/nested"],
["#/nested", "#"] ["#/nested", "#"],
]; ];
for (const [input, output] of examples) { for (const [input, output] of examples) {
@@ -61,16 +61,16 @@ describe("json form", () => {
[ [
"#/description", "#/description",
{ type: "object", properties: { description: { type: "string" } } }, { type: "object", properties: { description: { type: "string" } } },
false false,
], ],
[ [
"#/description", "#/description",
{ {
type: "object", type: "object",
required: ["description"], required: ["description"],
properties: { description: { type: "string" } } properties: { description: { type: "string" } },
}, },
true true,
], ],
[ [
"#/nested/property", "#/nested/property",
@@ -79,11 +79,11 @@ describe("json form", () => {
properties: { properties: {
nested: { nested: {
type: "object", type: "object",
properties: { property: { type: "string" } } properties: { property: { type: "string" } },
} },
} },
}, },
false false,
], ],
[ [
"#/nested/property", "#/nested/property",
@@ -93,12 +93,12 @@ describe("json form", () => {
nested: { nested: {
type: "object", type: "object",
required: ["property"], required: ["property"],
properties: { property: { type: "string" } } properties: { property: { type: "string" } },
} },
} },
}, },
true true,
] ],
] 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) {
@@ -113,7 +113,7 @@ describe("json form", () => {
["tags", "0", "0.tags"], ["tags", "0", "0.tags"],
["tags", 0, "0.tags"], ["tags", 0, "0.tags"],
["nested.property", "prefix", "prefix.nested.property"], ["nested.property", "prefix", "prefix.nested.property"],
["nested.property", "", "nested.property"] ["nested.property", "", "nested.property"],
] satisfies [string, any, string][]; ] satisfies [string, any, string][];
for (const [path, prefix, output] of examples) { for (const [path, prefix, output] of examples) {
@@ -128,7 +128,7 @@ describe("json form", () => {
["tags", "0", "tags.0"], ["tags", "0", "tags.0"],
["tags", 0, "tags.0"], ["tags", 0, "tags.0"],
["nested.property", "suffix", "nested.property.suffix"], ["nested.property", "suffix", "nested.property.suffix"],
["nested.property", "", "nested.property"] ["nested.property", "", "nested.property"],
] satisfies [string, any, string][]; ] satisfies [string, any, string][];
for (const [path, suffix, output] of examples) { for (const [path, suffix, output] of examples) {
+12
View File
@@ -0,0 +1,12 @@
import { describe, it, expect } from "vitest";
describe("Example Test Suite", () => {
it("should pass basic arithmetic", () => {
expect(1 + 1).toBe(2);
});
it("should handle async operations", async () => {
const result = await Promise.resolve(42);
expect(result).toBe(42);
});
});
+8
View File
@@ -0,0 +1,8 @@
import "@testing-library/jest-dom";
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
// Automatically cleanup after each test
afterEach(() => {
cleanup();
});
-13
View File
@@ -1,13 +0,0 @@
//import type { BkndConfig } from "./src";
export default {
app: {
connection: {
type: "libsql",
config: {
//url: "http://localhost:8080"
url: ":memory:"
}
}
}
};
+23
View File
@@ -0,0 +1,23 @@
import pkg from "./package.json" with { type: "json" };
import c from "picocolors";
import { formatNumber } from "core/utils";
const result = await Bun.build({
entrypoints: ["./src/cli/index.ts"],
target: "node",
outdir: "./dist/cli",
env: "PUBLIC_*",
minify: true,
define: {
__isDev: "0",
__version: JSON.stringify(pkg.version),
},
});
for (const output of result.outputs) {
const size_ = await output.text();
console.info(
c.cyan(formatNumber.fileSize(size_.length)),
c.dim(output.path.replace(import.meta.dir + "/", "")),
);
}
-257
View File
@@ -1,257 +0,0 @@
import { $, type Subprocess } from "bun";
import * as esbuild from "esbuild";
import postcss from "esbuild-postcss";
import { entryOutputMeta } from "./internal/esbuild.entry-output-meta.plugin";
import { guessMimeType } from "./src/media/storage/mime-types";
const args = process.argv.slice(2);
const watch = args.includes("--watch");
const minify = args.includes("--minify");
const types = args.includes("--types");
const sourcemap = args.includes("--sourcemap");
type BuildOptions = esbuild.BuildOptions & { name: string };
const baseOptions: Partial<Omit<esbuild.BuildOptions, "plugins">> & { plugins?: any[] } = {
minify,
sourcemap,
metafile: true,
format: "esm",
drop: ["console", "debugger"],
loader: {
".svg": "dataurl"
},
define: {
__isDev: "0"
}
};
// @ts-ignore
type BuildFn = (format?: "esm" | "cjs") => BuildOptions;
// build BE
const builds: Record<string, BuildFn> = {
backend: (format = "esm") => ({
...baseOptions,
name: `backend ${format}`,
entryPoints: [
"src/index.ts",
"src/data/index.ts",
"src/core/index.ts",
"src/core/utils/index.ts",
"src/ui/index.ts",
"src/ui/main.css"
],
outdir: "dist",
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
platform: "browser",
splitting: false,
bundle: true,
plugins: [postcss()],
//target: "es2022",
format
}),
/*components: (format = "esm") => ({
...baseOptions,
name: `components ${format}`,
entryPoints: ["src/ui/index.ts", "src/ui/main.css"],
outdir: "dist/ui",
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
format,
platform: "browser",
splitting: false,
//target: "es2022",
bundle: true,
//external: ["react", "react-dom", "@tanstack/react-query-devtools"],
plugins: [postcss()],
loader: {
".svg": "dataurl",
".js": "jsx"
}
}),*/
static: (format = "esm") => ({
...baseOptions,
name: `static ${format}`,
entryPoints: ["src/ui/main.tsx", "src/ui/main.css"],
entryNames: "[dir]/[name]-[hash]",
outdir: "dist/static",
outExtension: { ".js": format === "esm" ? ".js" : ".cjs" },
platform: "browser",
bundle: true,
splitting: true,
inject: ["src/ui/inject.js"],
target: "es2022",
format,
loader: {
".svg": "dataurl",
".js": "jsx"
},
define: {
__isDev: "0",
"process.env.NODE_ENV": '"production"'
},
chunkNames: "chunks/[name]-[hash]",
plugins: [
postcss(),
entryOutputMeta(async (info) => {
const manifest: Record<string, object> = {};
const toAsset = (output: string) => {
const name = output.split("/").pop()!;
return {
name,
path: output,
mime: guessMimeType(name)
};
};
for (const { output, meta } of info) {
manifest[meta.entryPoint as string] = toAsset(output);
if (meta.cssBundle) {
manifest["src/ui/main.css"] = toAsset(meta.cssBundle);
}
}
const manifest_file = "dist/static/manifest.json";
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
console.log(`Manifest written to ${manifest_file}`, manifest);
})
]
})
};
function adapter(adapter: string, overrides: Partial<esbuild.BuildOptions> = {}): BuildOptions {
return {
...baseOptions,
name: `adapter ${adapter} ${overrides?.format === "cjs" ? "cjs" : "esm"}`,
entryPoints: [`src/adapter/${adapter}`],
platform: "neutral",
outfile: `dist/adapter/${adapter}/index.${overrides?.format === "cjs" ? "cjs" : "js"}`,
external: [
"cloudflare:workers",
"@hono*",
"hono*",
"bknd*",
"*.html",
"node*",
"react*",
"next*",
"libsql",
"@libsql*"
],
splitting: false,
treeShaking: true,
bundle: true,
...overrides
};
}
const adapters = [
adapter("vite", { platform: "node" }),
adapter("cloudflare"),
adapter("nextjs", { platform: "node", format: "esm" }),
adapter("nextjs", { platform: "node", format: "cjs" }),
adapter("remix", { format: "esm" }),
adapter("remix", { format: "cjs" }),
adapter("bun"),
adapter("node", { platform: "node", format: "esm" }),
adapter("node", { platform: "node", format: "cjs" })
];
const collect = [
builds.static(),
builds.backend(),
//builds.components(),
builds.backend("cjs"),
//builds.components("cjs"),
...adapters
];
if (watch) {
const _state: {
timeout: Timer | undefined;
cleanup: Subprocess | undefined;
building: Subprocess | undefined;
} = {
timeout: undefined,
cleanup: undefined,
building: undefined
};
async function rebuildTypes() {
if (!types) return;
if (_state.timeout) {
clearTimeout(_state.timeout);
if (_state.cleanup) _state.cleanup.kill();
if (_state.building) _state.building.kill();
}
_state.timeout = setTimeout(async () => {
_state.cleanup = Bun.spawn(["bun", "clean:types"], {
onExit: () => {
_state.cleanup = undefined;
_state.building = Bun.spawn(["bun", "build:types"], {
onExit: () => {
_state.building = undefined;
console.log("Types rebuilt");
}
});
}
});
}, 1000);
}
for (const { name, ...build } of collect) {
const ctx = await esbuild.context({
...build,
plugins: [
...(build.plugins ?? []),
{
name: "rebuild-notify",
setup(build) {
build.onEnd((result) => {
console.log(`rebuilt ${name} with ${result.errors.length} errors`);
rebuildTypes();
});
}
}
]
});
ctx.watch();
}
} else {
await $`rm -rf dist`;
async function _build() {
let i = 0;
const count = collect.length;
for await (const { name, ...build } of collect) {
await esbuild.build({
...build,
plugins: [
...(build.plugins || []),
{
name: "progress",
setup(build) {
i++;
build.onEnd((result) => {
const errors = result.errors.length;
const from = String(i).padStart(String(count).length);
console.log(`[${from}/${count}] built ${name} with ${errors} errors`);
});
}
}
]
});
}
console.log("All builds complete");
}
async function _buildtypes() {
if (!types) return;
Bun.spawn(["bun", "build:types"], {
onExit: () => {
console.log("Types rebuilt");
}
});
}
await Promise.all([_build(), _buildtypes()]);
}
+228 -129
View File
@@ -1,5 +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" };
const args = process.argv.slice(2); const args = process.argv.slice(2);
const watch = args.includes("--watch"); const watch = args.includes("--watch");
@@ -8,8 +9,13 @@ 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");
const define = {
__isDev: "0",
__version: JSON.stringify(pkg.version),
};
if (clean) { if (clean) {
console.log("Cleaning dist (w/o static)"); console.info("Cleaning dist (w/o static)");
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`; await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
} }
@@ -21,15 +27,15 @@ function buildTypes() {
Bun.spawn(["bun", "build:types"], { Bun.spawn(["bun", "build:types"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
console.log("Types built"); console.info("Types built");
Bun.spawn(["bun", "tsc-alias"], { Bun.spawn(["bun", "tsc-alias"], {
stdout: "inherit", stdout: "inherit",
onExit: () => { onExit: () => {
console.log("Types aliased"); console.info("Types aliased");
types_running = false; types_running = false;
} },
}); });
} },
}); });
} }
@@ -46,114 +52,167 @@ if (types && !watch) {
buildTypes(); buildTypes();
} }
function banner(title: string) {
console.info("");
console.info("=".repeat(40));
console.info(title.toUpperCase());
console.info("-".repeat(40));
}
// collection of always-external packages
const external = [
"bun:test",
"node:test",
"node:assert/strict",
"@libsql/client",
"bknd",
/^bknd\/.*/,
] as const;
/** /**
* Building backend and general API * Building backend and general API
*/ */
await tsup.build({ async function buildApi() {
minify, banner("Building API");
sourcemap, await tsup.build({
watch, minify,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"], sourcemap,
outDir: "dist", watch,
external: ["bun:test", "@libsql/client"], define,
metafile: true, entry: [
platform: "browser", "src/index.ts",
format: ["esm"], "src/core/index.ts",
splitting: false, "src/core/utils/index.ts",
treeshake: true, "src/data/index.ts",
loader: { "src/media/index.ts",
".svg": "dataurl" "src/plugins/index.ts",
}, ],
onSuccess: async () => { outDir: "dist",
delayTypes(); external: [...external],
} metafile: true,
}); platform: "browser",
format: ["esm"],
splitting: false,
treeshake: true,
loader: {
".svg": "dataurl",
},
onSuccess: async () => {
delayTypes();
},
});
}
async function rewriteClient(path: string) {
const bundle = await Bun.file(path).text();
await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
}
/** /**
* Building UI for direct imports * Building UI for direct imports
*/ */
await tsup.build({ async function buildUi() {
minify, const base = {
sourcemap, minify,
watch, sourcemap,
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"], watch,
outDir: "dist/ui", define,
external: [ external: [
"bun:test", ...external,
"react", "react",
"react-dom", "react-dom",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"use-sync-external-store", "use-sync-external-store",
/codemirror/, /codemirror/,
"@xyflow/react", "@xyflow/react",
"@mantine/core" "@mantine/core",
], ],
metafile: true, metafile: true,
platform: "browser", platform: "browser",
format: ["esm"], format: ["esm"],
splitting: false, splitting: false,
bundle: true, bundle: true,
treeshake: true, treeshake: true,
loader: { loader: {
".svg": "dataurl" ".svg": "dataurl",
}, },
esbuildOptions: (options) => { esbuildOptions: (options) => {
options.logLevel = "silent"; options.logLevel = "silent";
}, },
onSuccess: async () => { } satisfies tsup.Options;
delayTypes();
} banner("Building UI");
}); await tsup.build({
...base,
entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
onSuccess: async () => {
await rewriteClient("./dist/ui/index.js");
delayTypes();
},
});
banner("Building Client");
await tsup.build({
...base,
entry: ["src/ui/client/index.ts"],
outDir: "dist/ui/client",
onSuccess: async () => {
await rewriteClient("./dist/ui/client/index.js");
delayTypes();
},
});
}
/** /**
* Building UI Elements * Building UI Elements
* - tailwind-merge is mocked, no exclude * - tailwind-merge is mocked, no exclude
* - ui/client is external, and after built replaced with "bknd/client" * - ui/client is external, and after built replaced with "bknd/client"
*/ */
await tsup.build({ async function buildUiElements() {
minify, banner("Building UI Elements");
sourcemap, await tsup.build({
watch, minify,
entry: ["src/ui/elements/index.ts"], sourcemap,
outDir: "dist/ui/elements", watch,
external: [ define,
"ui/client", entry: ["src/ui/elements/index.ts"],
"react", outDir: "dist/ui/elements",
"react-dom", external: [
"react/jsx-runtime", "ui/client",
"react/jsx-dev-runtime", "react",
"use-sync-external-store" "react-dom",
], "react/jsx-runtime",
metafile: true, "react/jsx-dev-runtime",
platform: "browser", "use-sync-external-store",
format: ["esm"], ],
splitting: false, metafile: true,
bundle: true, platform: "browser",
treeshake: true, format: ["esm"],
loader: { splitting: false,
".svg": "dataurl" bundle: true,
}, treeshake: true,
esbuildOptions: (options) => { loader: {
options.alias = { ".svg": "dataurl",
// not important for elements, mock to reduce bundle },
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts" esbuildOptions: (options) => {
}; options.alias = {
}, // not important for elements, mock to reduce bundle
onSuccess: async () => { "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
// manually replace ui/client with bknd/client };
const path = "./dist/ui/elements/index.js"; },
const bundle = await Bun.file(path).text(); onSuccess: async () => {
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client")); await rewriteClient("./dist/ui/elements/index.js");
delayTypes();
delayTypes(); },
} });
}); }
/** /**
* Building adapters * Building adapters
*/ */
function baseConfig(adapter: string): tsup.Options { function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
return { return {
minify, minify,
sourcemap, sourcemap,
@@ -162,47 +221,87 @@ function baseConfig(adapter: string): tsup.Options {
format: ["esm"], format: ["esm"],
platform: "neutral", platform: "neutral",
outDir: `dist/adapter/${adapter}`, outDir: `dist/adapter/${adapter}`,
define: {
__isDev: "0"
},
external: [
/^cloudflare*/,
/^@?(hono|libsql).*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/
],
metafile: true, metafile: true,
splitting: false, splitting: false,
onSuccess: async () => { onSuccess: async () => {
delayTypes(); delayTypes();
} },
...overrides,
define: {
...define,
...overrides.define,
},
external: [
/^cloudflare*/,
/^@?hono.*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/,
...external,
...(Array.isArray(overrides.external) ? overrides.external : []),
],
}; };
} }
// base adapter handles async function buildAdapters() {
await tsup.build({ banner("Building Adapters");
...baseConfig(""), // base adapter handles
entry: ["src/adapter/index.ts"], await tsup.build({
outDir: "dist/adapter" ...baseConfig(""),
}); entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
});
// specific adatpers // specific adatpers
await tsup.build(baseConfig("remix")); await tsup.build(baseConfig("react-router"));
await tsup.build(baseConfig("bun")); await tsup.build(
await tsup.build(baseConfig("astro")); baseConfig("bun", {
await tsup.build(baseConfig("cloudflare")); external: [/^bun\:.*/],
}),
);
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build(baseConfig("cloudflare"));
await tsup.build({ await tsup.build({
...baseConfig("vite"), ...baseConfig("vite"),
platform: "node" platform: "node",
}); });
await tsup.build({ await tsup.build({
...baseConfig("nextjs"), ...baseConfig("nextjs"),
platform: "node" platform: "node",
}); });
await tsup.build({ await tsup.build({
...baseConfig("node"), ...baseConfig("node"),
platform: "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();
+218
View File
@@ -0,0 +1,218 @@
import { $ } from "bun";
import path from "node:path";
import c from "picocolors";
const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1);
async function run(
cmd: string[] | string,
opts: Bun.SpawnOptions.OptionsObject & {},
onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void,
): Promise<{ proc: Bun.Subprocess; data: any }> {
return new Promise((resolve, reject) => {
const proc = Bun.spawn(Array.isArray(cmd) ? cmd : cmd.split(" "), {
...opts,
stdout: "pipe",
stderr: "pipe",
});
// Read from stdout
const reader = proc.stdout.getReader();
const decoder = new TextDecoder();
// Function to read chunks
let resolveCalled = false;
(async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
if (!resolveCalled) {
console.log(c.dim(text.replace(/\n$/, "")));
}
onChunk(
text,
(data) => {
resolve({ proc, data });
resolveCalled = true;
},
reject,
);
}
} catch (err) {
reject(err);
}
})();
proc.exited.then((code) => {
if (code !== 0 && code !== 130) {
throw new Error(`Process exited with code ${code}`);
}
});
});
}
const adapters = {
node: {
dir: path.join(basePath, "examples/node"),
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
},
start: async function () {
return await run(
"npm run start",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /running on (http:\/\/.*)\n/;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
bun: {
dir: path.join(basePath, "examples/bun"),
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`;
},
start: async function () {
return await run(
"npm run start",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /running on (http:\/\/.*)\n/;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
cloudflare: {
dir: path.join(basePath, "examples/cloudflare-worker"),
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf .wrangler node_modules/.cache node_modules/.mf`;
},
start: async function () {
return await run(
"npm run dev",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /Ready on (http:\/\/.*)/;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
"react-router": {
dir: path.join(basePath, "examples/react-router"),
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf .react-router data.db`;
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
},
start: async function () {
return await run(
"npm run dev",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /Local.*?(http:\/\/.*)\//;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
nextjs: {
dir: path.join(basePath, "examples/nextjs"),
env: "TEST_TIMEOUT=20000",
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf .nextjs data.db`;
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
},
start: async function () {
return await run(
"npm run dev",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /Local.*?(http:\/\/.*)\n/;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
astro: {
dir: path.join(basePath, "examples/astro"),
clean: async function () {
const cwd = path.relative(process.cwd(), this.dir);
await $`cd ${cwd} && rm -rf .astro data.db`;
await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`;
},
start: async function () {
return await run(
"npm run dev",
{
cwd: this.dir,
},
(chunk, resolve, reject) => {
const regex = /Local.*?(http:\/\/.*)\//;
if (regex.test(chunk)) {
resolve(chunk.match(regex)?.[1]);
}
},
);
},
},
} as const;
async function testAdapter(name: keyof typeof adapters) {
const config = adapters[name];
console.log("adapter", c.cyan(name));
await config.clean();
const { proc, data } = await config.start();
console.log("proc:", proc.pid, "data:", c.cyan(data));
//proc.kill();process.exit(0);
const add_env = "env" in config && config.env ? config.env : "";
await $`TEST_URL=${data} TEST_ADAPTER=${name} ${add_env} bun run test:e2e`;
console.log("DONE!");
while (!proc.killed) {
proc.kill("SIGINT");
await Bun.sleep(250);
console.log("Waiting for process to exit...");
}
}
// run with: TEST_ADAPTER=astro bun run e2e/adapters.ts
// (modify `test:e2e` to `test:e2e:ui` to see the UI)
if (process.env.TEST_ADAPTER) {
await testAdapter(process.env.TEST_ADAPTER as any);
} else {
for (const [name] of Object.entries(adapters)) {
await testAdapter(name as any);
}
}

Some files were not shown because too many files have changed in this diff Show More