Compare commits

..

228 Commits

Author SHA1 Message Date
dswbx 3180037b67 admin ui: store data canvas positions 2025-03-18 12:39:23 +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
dswbx 09d35d3cc0 Merge pull request #60 from bknd-io/release/0.7
Release 0.7
2025-02-08 16:38:07 +01:00
dswbx f7e6928dba form: fix popover, improve form types 2025-02-08 15:43:02 +01:00
dswbx 3fa682bfe1 form: fix switch required, add root form error to media settings 2025-02-08 13:27:47 +01:00
dswbx 97b0ff24c8 Merge pull request #71 from bknd-io/feat/elements-native-form
feat/elements-native-form
2025-02-08 12:01:35 +01:00
dswbx bb964dc835 added native reactive form to elements 2025-02-08 11:59:43 +01:00
dswbx 5840ec6b89 Merge pull request #70 from bknd-io/feat/optimize-form-renders
Feat/optimize form renders
2025-02-08 11:59:13 +01:00
dswbx f29641c702 using path instead of pointer, replaced lodash usage 2025-02-08 09:18:24 +01:00
dswbx 2e3ee65aa7 added more input field types and improved typing 2025-02-07 17:28:01 +01:00
dswbx 11a6f5c9b9 hide rerender scanner 2025-02-07 16:12:39 +01:00
dswbx 324d641410 form rerenders optimized 2025-02-07 16:11:21 +01:00
dswbx 02e7e1ca95 initially reduced form rerenders 2025-02-06 17:12:43 +01:00
dswbx 78d4f928ba fix nextjs node adapter import 2025-02-06 15:19:25 +01:00
dswbx 0a5cc2ad98 fix bknd theme 2025-02-06 15:00:54 +01:00
dswbx e14d528386 Merge pull request #68 from bknd-io/feat/media-settings-ui
Improved media settings UI/UX
2025-02-06 13:59:24 +01:00
dswbx 15003b004b Merge pull request #67 from bknd-io/fix/api-query-with-string-object
fix/api-query-with-string-object
2025-02-06 13:58:50 +01:00
dswbx 9a4c2bd530 finalize new media settings ui 2025-02-06 13:58:29 +01:00
dswbx ab61a1392c Merge pull request #69 from bknd-io/fix/admin-config-secrets-missing
fix clearing schema on unauthorized secrets fetching
2025-02-06 09:01:47 +01:00
dswbx 53d55448e6 fix clearing schema on unauthorized secrets fetching 2025-02-06 09:01:11 +01:00
dswbx 46cf310ad6 finalize new media settings 2025-02-05 17:54:48 +01:00
dswbx 8418231c43 improved media settings implementation 2025-02-05 16:11:53 +01:00
dswbx 4b3493a6f5 changed media settings to new form 2025-02-05 11:04:37 +01:00
dswbx f432473ed9 initial json schema form implementation 2025-02-05 10:31:08 +01:00
dswbx 8f0cef1f37 clear console log 2025-02-01 12:15:48 +01:00
dswbx a8e78f984d fix repo stringified string parsing 2025-02-01 12:12:22 +01:00
dswbx babeaae8d1 Merge pull request #66 from bknd-io/fix/auth-api-include-cookie
also sending cookies on json auth requests
2025-02-01 09:15:16 +01:00
dswbx 45d44f8177 Merge remote-tracking branch 'origin/release/0.7' into fix/auth-api-include-cookie
# Conflicts:
#	app/src/auth/authenticate/Authenticator.ts
2025-02-01 09:15:01 +01:00
dswbx db476080ad also sending cookies on json auth requests 2025-02-01 09:13:42 +01:00
dswbx 158818520b Merge pull request #65 from bknd-io/feat/auth-redirect-param
feat/auth-redirect-param
2025-01-29 20:30:52 +01:00
dswbx 4755288fed improve fallbacks for redirection 2025-01-29 20:30:27 +01:00
dswbx 10db267164 added a redirect param to password strategy 2025-01-29 20:26:06 +01:00
dswbx dfe7d55057 Merge pull request #64 from bknd-io/feat/improved-api-construction
updated API instantiation, and update user on verify
2025-01-29 14:49:00 +01:00
dswbx c2b3316fcb updated API instantiation, and update user on verify 2025-01-29 14:44:32 +01:00
dswbx 86ba055f5e fix entity table select 2025-01-28 09:10:38 +01:00
dswbx 9d6ccda20d Merge pull request #63 from bknd-io/fix/api-and-adapter
fix/api-and-adapter
2025-01-28 08:16:56 +01:00
dswbx 39c419c320 add built state and skip auth cookie if not signed in for local calls 2025-01-25 19:30:03 +01:00
dswbx 2d6d83ccb2 add fetcher option to api to allow local calls 2025-01-25 19:29:36 +01:00
dswbx ba1eb5fe08 remove adapter re-exports 2025-01-25 19:28:39 +01:00
dswbx 123389786d bump 0.7 rc version 2025-01-25 14:19:23 +01:00
dswbx b65bb3b826 Merge pull request #62 from bknd-io/feat/adapter-optimize-imports
optimize adapter imports to deduplicate
2025-01-25 14:14:25 +01:00
dswbx 5b40d5eaf4 optimize adapter imports to deduplicate 2025-01-25 14:13:34 +01:00
dswbx 89b1fd3909 Merge pull request #61 from bknd-io/feat/cf-static-assets
adapter(cloudflare): add supports for the new assets feature
2025-01-25 13:48:52 +01:00
dswbx aa7eac258b adapter(cloudflare): add supports for the new assets feature 2025-01-25 13:40:28 +01:00
dswbx 392a06c486 Merge pull request #59 from bknd-io/feat/adapter-auth-env
Adapter: verified auth & env-specific construction
2025-01-25 13:36:59 +01:00
dswbx 1beac5043b updated cloudflare adapter docs to match new args 2025-01-25 12:33:05 +01:00
dswbx 475488c910 Merge remote-tracking branch 'origin/feat/adapter-api-async' into feat/adapter-auth-env 2025-01-25 11:55:15 +01:00
dswbx 9ddacb7ae5 added adapter exports, generalized env-depending config construction 2025-01-25 11:54:19 +01:00
dswbx 3d1d52cbf8 Merge pull request #58 from bknd-io/release/0.6.2
Release 0.6.2
2025-01-25 09:40:44 +01:00
dswbx acc504cdc9 release version 0.6.2 2025-01-25 09:40:15 +01:00
dswbx d98d3351c0 Merge pull request #57 from bknd-io/feat/elements-docs-and-custom
restructured elements for better customization
2025-01-25 09:17:39 +01:00
dswbx 3e606cb159 Merge pull request #54 from bknd-io/fix/nextjs-schema-update
fixed schema updates to fail in nextjs envs due to lodash's merge
2025-01-25 09:17:23 +01:00
dswbx cc76964d03 restructured elements slightly for better customization, updated readme and docs 2025-01-25 09:14:02 +01:00
dswbx b0c5f6307a updated adapters to automatically verify auth 2025-01-25 09:09:09 +01:00
dswbx b3019d7333 Merge pull request #55 from bknd-io/feat/nextjs-plasmic-catchall
bknd/plasmic: added helper functions for integration
2025-01-25 09:03:30 +01:00
dswbx 2265451d4b added handy nextjs-plasmic-bknd functions 2025-01-23 10:59:33 +01:00
dswbx ba936604a3 fix overwrite of entity config (e.g. empty label) 2025-01-23 09:02:58 +01:00
dswbx 7dd42fbff3 fixed schema updates to fail in nextjs envs due to lodash's merge 2025-01-23 08:34:11 +01:00
dswbx f64e5dac03 updated README and documentation (first part) to match 0.6 2025-01-21 18:30:01 +01:00
dswbx 24542f30cb Merge pull request #53 from bknd-io/fix/release-0.6.1
fix auth screens' missing config permission, and media URL resolution
2025-01-21 17:13:57 +01:00
dswbx 51715158c2 fix auth screens' missing config permission, and media URL resolution 2025-01-21 17:10:45 +01:00
638 changed files with 26275 additions and 7919 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Run Tests
on:
pull_request:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
working-directory: ./app
run: bun install
- name: Run tests
working-directory: ./app
run: bun run test
+1
View File
@@ -19,6 +19,7 @@ packages/media/.env
.history
**/*/.db/*
**/*/.configs/*
**/*/.template/*
**/*/*.db
**/*/*.db-shm
**/*/*.db-wal
+117 -30
View File
@@ -1,13 +1,15 @@
[![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd
"View this project on NPM")
[![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 simplifies app development by providing fully functional backend for data management,
authentication, workflows and media. Since it's 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!
<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" target="_blank">
<strong>⭐ Live Demo</strong>
</a>
</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!
**For documentation and examples, please visit https://docs.bknd.io.**
@@ -15,33 +17,118 @@ deploying multiple separate services!
> Please keep in mind that **bknd** is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
## ✨ Features
**📊 Data**: Define, query, and control your data with ease.
- Define entities with fields and relationships, synced directly to your database.
- Supported field types: `primary`, `text`, `number`, `date`, `boolean`, `enum`, `json`, `jsonschema`.
- Relationship types: `one-to-one`, `many-to-one`, `many-to-many`, and `polymorphic`.
- Advanced querying with the **Repository**: filtering, sorting, pagination, and relational data handling.
- Seamlessly manage data with mutators and a robust event system.
- Extend database functionality with batching, introspection, and support for multiple SQL dialects.
## Size
![gzipped size of bknd](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/index.js?compression=gzip&label=bknd)
![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@latest/dist/ui/elements/index.js?compression=gzip&label=bknd/elements)
![gzipped size of bknd/ui](https://img.badgesize.io/https://unpkg.com/bknd@latest/dist/ui/index.js?compression=gzip&label=bknd/ui)
**🔐 Auth**: Easily implement reliable authentication strategies.
- Built-in `user` entity with customizable fields.
- Supports multiple authentication strategies:
- Email/password (with hashed storage).
- OAuth/OIDC (Google, GitHub, and more).
- Secure JWT generation and session management.
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.
**🖼️ Media**: Effortlessly manage and serve all your media files.
- Upload files with ease.
- Adapter-based support for S3, S3-compatible storage (e.g., R2, Tigris), and Cloudinary.
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 212 kB gzipped (e.g. deployed as Cloudflare Worker).
**🔄 Flows** (UI integration coming soon!): Design and run workflows with seamless automation.
- Create and run workflows with trigger-based automation:
- Manual triggers or events from data, auth, media, or server actions.
- HTTP triggers for external integrations.
- Define tasks in sequence, parallel, or loops, with conditional execution.
- Use reusable sub-workflows to organize complex processes.
- Leverage OpenAPI specifications for API-based tasks.
## 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.
**The solution:** A backend system that only assumes and implements primitive details, integrates into multiple environments, and adheres to industry standards.
## Features
* ⚡ Instant backend with full REST API:
* **Data**: Define, query, and control your data with ease.
* **Auth**: Easily implement reliable authentication strategies.
* **Media**: Effortlessly manage and serve all your media files.
* **Flows**: Design and run workflows with seamless automation. (UI integration coming soon!)
* 🌐 Built on Web Standards for maximum compatibility
* 🏃‍♂️ Multiple run modes
* standalone using the CLI
* using a JavaScript runtime (Node, Bun, workerd)
* using a React framework (Next.js, React Router, Astro)
* 📦 Official API and React SDK with type-safety
* ⚛️ React elements for auto-configured authentication and media components
## Structure
The package is mainly split into 4 parts, each serving a specific purpose:
| Import | Purpose |
|-----------------------------|------------------------------------------------------|
| `bknd`<br/>`bknd/adapter/*` | Backend including APIs and adapters |
| `bknd/ui` | Admin UI components for react frameworks |
| `bknd/client` | TypeScript SDK and React hooks for the API endpoints |
| `bknd/elements` | React components for authentication and media |
### The backend (`bknd`)
Serve the backend as an API for any JS runtime or framework. The latter is especially handy, as it allows you to deploy your frontend and backend bundled together. Furthermore it allows adding additional logic in a way you're already familar with. Just add another route and you're good to go.
Here is an example of serving the API using node:
```js index.js
import { serve } from "bknd/adapter/node"
serve();
```
### Integrated admin UI (`bknd/ui`)
The admin UI allows to manage your data including full configuration of your backend using a graphical user interface. Using `vite`, your admin route looks like this:
```tsx
import { Admin } from "bknd/ui"
import "bknd/dist/styles.css";
export default function AdminPage() {
return <Admin />
}
```
### 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:
```bash
curl -XGET <your-endpoint>/api/data/entity/<entity>
{
"data": [
{ "id": 1, ... },
{ "id": 2, ... }
],
"meta": { /* ... */ }
}
```
In a JavaScript environment, you can use the TypeScript SDK with type-safety. The above example would look like this:
```ts
import { Api } from "bknd/client";
const api = new Api({ host: "<endpoint>" });
const { data } = await api.data.readMany("<entity>");
```
If you're using React, there are 2 hooks exposed (`useApi`, `useEntity`), as well as an `swr` wrapper around each (`useApiQuery`, `useEntityQuery`). The `swr` wrapped hooks automatically handled query invalidation:
```tsx
import { useState } from "react";
import { useEntityQuery } from "bknd/client";
export default function App() {
const { data } = useEntityQuery("todos");
return <ul>
{data?.map(todo => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
}
```
### React elements (`bknd/elements`)
You don't have to figure out API details to include media uploads to your app. For an user avatar upload, this is all you need:
```tsx
import { Media } from "bknd/elements"
import "bknd/dist/main.css"
export function UserAvatar() {
return <Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
/>
}
```
The import path also exports components for login and registration forms which are automatically pointed to the `bknd` defaults.
## 🚀 Quick start
+14 -2
View File
@@ -1,9 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test";
import { afterAll, afterEach, describe, expect, test } from "bun:test";
import { App } from "../src";
import { getDummyConnection } from "./helper";
const { dummyConnection, afterAllCleanup } = getDummyConnection();
afterAll(afterAllCleanup);
afterEach(afterAllCleanup);
describe("App tests", async () => {
test("boots and pongs", async () => {
@@ -12,4 +12,16 @@ describe("App tests", async () => {
//expect(await app.data?.em.ping()).toBeTrue();
});
/*test.only("what", async () => {
const app = new App(dummyConnection, {
auth: {
enabled: true,
},
});
await app.module.auth.build();
await app.module.data.build();
console.log(app.em.entities.map((e) => e.name));
console.log(await app.em.schema().getDiff());
});*/
});
+1
View File
@@ -0,0 +1 @@
tmp/*
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+71
View File
@@ -0,0 +1,71 @@
import { describe, expect, it } from "bun:test";
import { sign } from "hono/jwt";
import { Api } from "../../src/Api";
describe("Api", async () => {
it("should construct without options", () => {
const api = new Api();
expect(api.baseUrl).toBe("http://localhost");
expect(api.isAuthVerified()).toBe(false);
});
it("should ignore force verify if no claims given", () => {
const api = new Api({ verified: true });
expect(api.baseUrl).toBe("http://localhost");
expect(api.isAuthVerified()).toBe(false);
});
it("should construct from request (token)", async () => {
const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false);
const params = api.getParams();
expect(params.token).toBe(token);
expect(params.token_transport).toBe("header");
expect(params.host).toBe("http://example.com");
});
it("should construct from request (cookie)", async () => {
const token = await sign({ foo: "bar" }, "1234");
const request = new Request("http://example.com/test", {
headers: {
Cookie: `auth=${token}`,
},
});
const api = new Api({ request });
expect(api.isAuthVerified()).toBe(false);
const params = api.getParams();
console.log(params);
expect(params.token).toBe(token);
expect(params.token_transport).toBe("cookie");
expect(params.host).toBe("http://example.com");
});
it("should construct from token", async () => {
const token = await sign({ foo: "bar" }, "1234");
const api = new Api({ token });
expect(api.isAuthVerified()).toBe(false);
const params = api.getParams();
expect(params.token).toBe(token);
expect(params.token_transport).toBe("header");
expect(params.host).toBe("http://localhost");
});
it("should prefer host when request is given", async () => {
const request = new Request("http://example.com/test");
const api = new Api({ request, host: "http://another.com" });
const params = api.getParams();
expect(params.token).toBeUndefined();
expect(params.token_transport).toBe("header");
expect(params.host).toBe("http://another.com");
});
});
+153 -11
View File
@@ -17,16 +17,16 @@ describe("DataApi", () => {
const get = api.readMany("a".repeat(300), { select: ["id", "name"] });
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"] });
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 () => {
const schema = proto.em({
posts: proto.entity("posts", { title: proto.text() })
posts: proto.entity("posts", { title: proto.text() }),
});
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
@@ -39,20 +39,18 @@ describe("DataApi", () => {
const app = controller.getController();
{
const res = (await app.request("/posts")) as Response;
const { data } = await res.json();
const res = (await app.request("/entity/posts")) as Response;
const { data } = (await res.json()) as any;
expect(data.length).toEqual(3);
}
// @ts-ignore tests
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 });
// @ts-ignore protected
api.fetcher = app.request as typeof fetch;
const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }, app.request as typeof fetch);
{
const req = api.readMany("posts", { select: ["title"] });
expect(req.request.method).toBe("GET");
const res = await req;
expect(res.data).toEqual(payload);
expect(res.data).toEqual(payload as any);
}
{
@@ -60,11 +58,155 @@ describe("DataApi", () => {
select: ["title"],
limit: 100000,
offset: 0,
sort: "id"
sort: "id",
});
expect(req.request.method).toBe("POST");
const res = await req;
expect(res.data).toEqual(payload);
expect(res.data).toEqual(payload 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.count).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();
}
});
});
+146
View File
@@ -0,0 +1,146 @@
/// <reference types="@types/bun" />
import { describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { getFileFromContext, isFile, isReadableStream } from "../../src/core/utils";
import { MediaApi } from "../../src/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",
});
expect(api.getUploadHeaders().get("Authorization")).toBe("Bearer token");
});
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 () => {
// @ts-ignore tests
const api = new MediaApi({}, 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",
);
}
});
});
+12 -10
View File
@@ -27,10 +27,7 @@ describe("ModuleApi", () => {
it("fetches endpoint", async () => {
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
const api = new Api({ host });
// @ts-expect-error it's protected
api.fetcher = app.request as typeof fetch;
const api = new Api({ host }, app.request as typeof fetch);
const res = await api.get("/endpoint");
expect(res.res.ok).toEqual(true);
@@ -41,10 +38,7 @@ describe("ModuleApi", () => {
it("has accessible request", async () => {
const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" }));
const api = new Api({ host });
// @ts-expect-error it's protected
api.fetcher = app.request as typeof fetch;
const api = new Api({ host }, app.request as typeof fetch);
const promise = api.get("/endpoint");
expect(promise.request).toBeDefined();
@@ -67,7 +61,7 @@ describe("ModuleApi", () => {
it("adds additional headers from options", () => {
const headers = new Headers({
"X-Test": "123"
"X-Test": "123",
});
const api = new Api({ host, headers });
expect(api.get("/").request.headers.get("X-Test")).toEqual("123");
@@ -81,7 +75,7 @@ describe("ModuleApi", () => {
it("uses search params", () => {
const api = new Api({ host });
const search = new URLSearchParams({
foo: "bar"
foo: "bar",
});
expect(api.get("/", search).request.url).toEqual("http://localhost/?" + search.toString());
});
@@ -95,6 +89,14 @@ describe("ModuleApi", () => {
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 method shortcut functions
});
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, mock, test } from "bun:test";
import type { ModuleBuildContext } from "../../src";
import { type App, createApp } from "../../src/App";
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",
]);
},
},
}).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");
});
});
+39 -9
View File
@@ -24,9 +24,9 @@ describe("repros", async () => {
adapter: {
type: "local",
config: {
path: "./"
}
}
path: "./",
},
},
});
expect(config.enabled).toBe(true);
@@ -38,9 +38,9 @@ describe("repros", async () => {
"entities.test",
proto
.entity("test", {
content: proto.text()
content: proto.text(),
})
.toJSON()
.toJSON(),
);
expect(app.em.entities.map((e) => e.name)).toContain("test");
}
@@ -54,8 +54,8 @@ describe("repros", async () => {
hidden: false,
mime_types: [],
virtual: true,
entity: "test"
}
entity: "test",
},
});
expect(
@@ -63,11 +63,41 @@ describe("repros", async () => {
type: "poly",
source: "test",
target: "media",
config: { mappedBy: "files" }
})
config: { mappedBy: "files" },
}),
).resolves.toBeDefined();
}
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",
},
]);
});
});
+22 -26
View File
@@ -7,19 +7,17 @@ describe("authorize", () => {
["read", "write"],
{
admin: {
permissions: ["read", "write"]
}
permissions: ["read", "write"],
},
},
{ enabled: true }
{ enabled: true },
);
const user = {
role: "admin"
role: "admin",
};
guard.setUserContext(user);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(true);
expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write", user)).toBe(true);
expect(() => guard.granted("something")).toThrow();
});
@@ -29,27 +27,25 @@ describe("authorize", () => {
["read", "write"],
{
admin: {
permissions: ["read", "write"]
permissions: ["read", "write"],
},
guest: {
permissions: ["read"],
is_default: true
}
is_default: true,
},
},
{ enabled: true }
{ enabled: true },
);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(false);
const user = {
role: "admin"
role: "admin",
};
guard.setUserContext(user);
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(true);
expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write", user)).toBe(true);
});
test("guard implicit allow", async () => {
@@ -62,24 +58,24 @@ describe("authorize", () => {
test("role implicit allow", async () => {
const guard = Guard.create(["read", "write"], {
admin: {
implicit_allow: true
}
implicit_allow: true,
},
});
guard.setUserContext({
role: "admin"
});
const user = {
role: "admin",
};
expect(guard.granted("read")).toBe(true);
expect(guard.granted("write")).toBe(true);
expect(guard.granted("read", user)).toBe(true);
expect(guard.granted("write", user)).toBe(true);
});
test("guard with guest role implicit allow", async () => {
const guard = Guard.create(["read", "write"], {
guest: {
implicit_allow: true,
is_default: true
}
is_default: true,
},
});
expect(guard.getUserRole()?.name).toBe("guest");
@@ -7,10 +7,10 @@ describe("OAuthStrategy", async () => {
const strategy = new OAuthStrategy({
type: "oidc",
client: {
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET
client_id: process.env.OAUTH_CLIENT_ID!,
client_secret: process.env.OAUTH_CLIENT_SECRET!,
},
name: "google"
name: "google",
});
const state = "---";
const redirect_uri = "http://localhost:3000/auth/google/callback";
@@ -19,11 +19,6 @@ describe("OAuthStrategy", async () => {
const config = await strategy.getConfig();
console.log("config", JSON.stringify(config, null, 2));
const request = await strategy.request({
redirect_uri,
state
});
const server = Bun.serve({
fetch: async (req) => {
const url = new URL(req.url);
@@ -31,13 +26,18 @@ describe("OAuthStrategy", async () => {
console.log("req", req);
const user = await strategy.callback(url, {
redirect_uri,
state
state,
});
console.log("---user", user);
}
return new Response("Bun!");
}
},
});
const request = await strategy.request({
redirect_uri,
state,
});
console.log("request", request);
+8 -7
View File
@@ -26,7 +26,7 @@ class ReturnEvent extends Event<{ foo: string }, string> {
}
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));
delayed();
},
"sync"
"sync",
);
// don't allow unknown
@@ -83,8 +83,8 @@ describe("EventManager", async () => {
const emgr = new EventManager(
{ InformationalEvent },
{
asyncExecutor
}
asyncExecutor,
},
);
emgr.onEvent(InformationalEvent, async () => {});
@@ -98,8 +98,8 @@ describe("EventManager", async () => {
const emgr = new EventManager(
{ ReturnEvent, InformationalEvent },
{
onInvalidReturn
}
onInvalidReturn,
},
);
// @ts-expect-error InformationalEvent has no return value
@@ -111,6 +111,7 @@ describe("EventManager", async () => {
emgr.onEvent(ReturnEvent, async () => "1", "sync");
emgr.onEvent(ReturnEvent, async () => "0", "sync");
// @todo: fix this
// @ts-expect-error must be string
emgr.onEvent(ReturnEvent, async () => 0, "sync");
@@ -139,7 +140,7 @@ describe("EventManager", async () => {
expect(slug).toBe("informational-event");
call();
},
{ mode: "sync", once: true }
{ mode: "sync", once: true },
);
expect(emgr.getListeners().length).toBe(1);
+6 -6
View File
@@ -30,8 +30,8 @@ describe("Registry", () => {
first: {
cls: What,
schema: Type.Object({ type: Type.String(), what: Type.String() }),
enabled: true
}
enabled: true,
},
} satisfies Record<string, Test1>);
const item = registry.get("first");
@@ -42,7 +42,7 @@ describe("Registry", () => {
registry.add("second", {
cls: What2,
schema: second,
enabled: true
enabled: true,
});
// @ts-ignore
expect(registry.get("second").schema).toEqual(second);
@@ -52,7 +52,7 @@ describe("Registry", () => {
// @ts-expect-error
cls: NotAllowed,
schema: third,
enabled: true
enabled: true,
});
// @ts-ignore
expect(registry.get("third").schema).toEqual(third);
@@ -62,7 +62,7 @@ describe("Registry", () => {
cls: What,
// @ts-expect-error
schema: fourth,
enabled: true
enabled: true,
});
// @ts-ignore
expect(registry.get("fourth").schema).toEqual(fourth);
@@ -75,7 +75,7 @@ describe("Registry", () => {
return {
cls: a,
schema: a.prototype.getType(),
enabled: true
enabled: true,
};
});
+5 -5
View File
@@ -4,7 +4,7 @@ import { after, beforeEach, describe, test } from "node:test";
import { Miniflare } from "miniflare";
import {
CloudflareKVCacheItem,
CloudflareKVCachePool
CloudflareKVCachePool,
} from "../../../src/core/cache/adapters/CloudflareKvCache";
import { runTests } from "./cache-test-suite";
@@ -26,7 +26,7 @@ describe("CloudflareKv", async () => {
mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
kvNamespaces: ["TEST"]
kvNamespaces: ["TEST"],
});
const kv = await mf.getKVNamespace("TEST");
return new CloudflareKVCachePool(kv as any);
@@ -45,10 +45,10 @@ describe("CloudflareKv", async () => {
},
toBeUndefined() {
assert.equal(actual, undefined);
}
},
};
}
}
},
},
});
after(async () => {
+2 -2
View File
@@ -9,7 +9,7 @@ describe("MemoryCache", () => {
tester: {
test,
beforeEach,
expect
}
expect,
},
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { checksum, hash } from "../../src/core/utils";
describe("crypto", async () => {
test("sha256", async () => {
expect(await hash.sha256("test")).toBe(
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
);
});
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();
});
});
+83 -83
View File
@@ -8,8 +8,8 @@ describe("SchemaObject", async () => {
Type.Object({ a: Type.String({ default: "b" }) }),
{ a: "test" },
{
forceParse: true
}
forceParse: true,
},
);
expect(m.get()).toEqual({ a: "test" });
@@ -30,14 +30,14 @@ describe("SchemaObject", async () => {
b: Type.Object(
{
c: Type.String({ default: "d" }),
e: Type.String({ default: "f" })
e: Type.String({ default: "f" }),
},
{ default: {} }
)
{ default: {} },
),
},
{ default: {}, additionalProperties: false }
)
})
{ default: {}, additionalProperties: false },
),
}),
);
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d", e: "f" } } });
@@ -59,8 +59,8 @@ describe("SchemaObject", async () => {
test("patch array", async () => {
const m = new SchemaObject(
Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
})
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
);
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -81,14 +81,14 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }),
b: Type.Object(
{
c: Type.String({ default: "d" })
c: Type.String({ default: "d" }),
},
{ default: {} }
)
{ default: {} },
),
},
{ default: {} }
)
})
{ default: {} },
),
}),
);
expect(m.get()).toEqual({ s: { a: "b", b: { c: "d" } } });
@@ -108,8 +108,8 @@ describe("SchemaObject", async () => {
test("set", async () => {
const m = new SchemaObject(
Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
})
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
);
expect(m.get()).toEqual({ methods: ["GET", "PATCH"] });
@@ -125,7 +125,7 @@ describe("SchemaObject", async () => {
let result: any;
const m = new SchemaObject(
Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
undefined,
{
@@ -133,8 +133,8 @@ describe("SchemaObject", async () => {
await new Promise((r) => setTimeout(r, 10));
called = true;
result = config;
}
}
},
},
);
await m.set({ methods: ["GET", "POST"] });
@@ -146,7 +146,7 @@ describe("SchemaObject", async () => {
let called = false;
const m = new SchemaObject(
Type.Object({
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] })
methods: Type.Array(Type.String(), { default: ["GET", "PATCH"] }),
}),
undefined,
{
@@ -155,8 +155,8 @@ describe("SchemaObject", async () => {
called = true;
to.methods.push("OPTIONS");
return to;
}
}
},
},
);
const result = await m.set({ methods: ["GET", "POST"] });
@@ -168,7 +168,7 @@ describe("SchemaObject", async () => {
test("throwIfRestricted", async () => {
const m = new SchemaObject(Type.Object({}), undefined, {
restrictPaths: ["a.b"]
restrictPaths: ["a.b"],
});
expect(() => m.throwIfRestricted("a.b")).toThrow();
@@ -185,18 +185,18 @@ describe("SchemaObject", async () => {
a: Type.String({ default: "b" }),
b: Type.Object(
{
c: Type.String({ default: "d" })
c: Type.String({ default: "d" }),
},
{ default: {} }
)
{ default: {} },
),
},
{ default: {} }
)
{ default: {} },
),
}),
undefined,
{
restrictPaths: ["s.b"]
}
restrictPaths: ["s.b"],
},
);
expect(m.patch("s.b.c", "e")).rejects.toThrow();
@@ -217,33 +217,33 @@ describe("SchemaObject", async () => {
additionalProperties: Type.Object({
type: Type.String(),
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 () => {
const data = {
entities: {
some: {
fields: {
a: { type: "string", config: { some: "thing" } }
}
}
}
a: { type: "string", config: { some: "thing" } },
},
},
},
};
const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config/]
overwritePaths: [/^entities\..*\.fields\..*\.config/],
});
await m.patch("entities.some.fields.a", { type: "string", config: { another: "one" } });
@@ -252,10 +252,10 @@ describe("SchemaObject", async () => {
entities: {
some: {
fields: {
a: { type: "string", config: { another: "one" } }
}
}
}
a: { type: "string", config: { another: "one" } },
},
},
},
});
});
@@ -265,22 +265,22 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
password: { type: "string" }
}
}
}
password: { type: "string" },
},
},
},
};
const m = new SchemaObject(dataEntitiesSchema, data, {
forceParse: true,
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/]
overwritePaths: [/^entities\..*\.fields\..*\.config\.html_config$/],
});
await m.patch("entities.test", {
fields: {
content: {
type: "text"
}
}
type: "text",
},
},
});
expect(m.get()).toEqual({
@@ -288,17 +288,17 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
password: { type: "string" }
}
password: { type: "string" },
},
},
test: {
fields: {
content: {
type: "text"
}
}
}
}
type: "text",
},
},
},
},
});
});
@@ -308,14 +308,14 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
password: { type: "string" }
}
}
}
password: { type: "string" },
},
},
},
};
const m = new SchemaObject(dataEntitiesSchema, data, {
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();
@@ -323,13 +323,13 @@ describe("SchemaObject", async () => {
await m.patch("entities.test", {
fields: {
content: {
type: "text"
}
}
type: "text",
},
},
});
await m.patch("entities.users.config", {
sort_dir: "desc"
sort_dir: "desc",
});
expect(m.get()).toEqual({
@@ -337,20 +337,20 @@ describe("SchemaObject", async () => {
users: {
fields: {
email: { type: "string" },
password: { type: "string" }
password: { type: "string" },
},
config: {
sort_dir: "desc"
}
sort_dir: "desc",
},
},
test: {
fields: {
content: {
type: "text"
}
}
}
}
type: "text",
},
},
},
},
});
});
});
+44 -44
View File
@@ -13,8 +13,8 @@ describe("diff", () => {
t: "a",
p: ["b"],
o: undefined,
n: 2
}
n: 2,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -35,8 +35,8 @@ describe("diff", () => {
t: "r",
p: ["b"],
o: 2,
n: undefined
}
n: undefined,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -57,8 +57,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: 1,
n: 2
}
n: 2,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -79,8 +79,8 @@ describe("diff", () => {
t: "e",
p: ["a", "b"],
o: 1,
n: 2
}
n: 2,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -101,14 +101,14 @@ describe("diff", () => {
t: "e",
p: ["a", 1],
o: 2,
n: 4
n: 4,
},
{
t: "a",
p: ["a", 3],
o: undefined,
n: 5
}
n: 5,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -129,20 +129,20 @@ describe("diff", () => {
t: "a",
p: ["a", 0],
o: undefined,
n: 1
n: 1,
},
{
t: "a",
p: ["a", 1],
o: undefined,
n: 2
n: 2,
},
{
t: "a",
p: ["a", 2],
o: undefined,
n: 3
}
n: 3,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -163,14 +163,14 @@ describe("diff", () => {
t: "e",
p: ["a", 1],
o: 2,
n: 3
n: 3,
},
{
t: "r",
p: ["a", 2],
o: 3,
n: undefined
}
n: undefined,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -183,14 +183,14 @@ describe("diff", () => {
it("should handle complex nested changes", () => {
const oldObj = {
a: {
b: [1, 2, { c: 3 }]
}
b: [1, 2, { c: 3 }],
},
};
const newObj = {
a: {
b: [1, 2, { c: 4 }, 5]
}
b: [1, 2, { c: 4 }, 5],
},
};
const diffs = diff(oldObj, newObj);
@@ -200,14 +200,14 @@ describe("diff", () => {
t: "e",
p: ["a", "b", 2, "c"],
o: 3,
n: 4
n: 4,
},
{
t: "a",
p: ["a", "b", 3],
o: undefined,
n: 5
}
n: 5,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -228,14 +228,14 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: undefined,
n: null
n: null,
},
{
t: "e",
p: ["b"],
o: null,
n: undefined
}
n: undefined,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -256,8 +256,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: 1,
n: "1"
}
n: "1",
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -278,14 +278,14 @@ describe("diff", () => {
t: "r",
p: ["b"],
o: 2,
n: undefined
n: undefined,
},
{
t: "a",
p: ["c"],
o: undefined,
n: 3
}
n: 3,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -306,8 +306,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: [1, 2, 3],
n: { b: 4 }
}
n: { b: 4 },
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -328,8 +328,8 @@ describe("diff", () => {
t: "e",
p: ["a"],
o: { b: 1 },
n: 2
}
n: 2,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -350,14 +350,14 @@ describe("diff", () => {
t: "r",
p: ["a"],
o: 1,
n: undefined
n: undefined,
},
{
t: "a",
p: ["b"],
o: undefined,
n: 2
}
n: 2,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -408,8 +408,8 @@ describe("diff", () => {
t: "a",
p: ["a"],
o: undefined,
n: 1
}
n: 1,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -430,8 +430,8 @@ describe("diff", () => {
t: "r",
p: ["a"],
o: 1,
n: undefined
}
n: undefined,
},
]);
const appliedObj = apply(oldObj, diffs);
@@ -9,7 +9,7 @@ describe("object-query", () => {
test("validates", async () => {
const converted = convert({
name: { $eq: "ch" }
name: { $eq: "ch" },
});
validate(converted, { name: "Michael" });
});
@@ -31,7 +31,7 @@ describe("object-query", () => {
[{ val: { $notnull: 1 } }, { val: null }, false],
[{ val: { $regex: ".*" } }, { 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) {
@@ -55,10 +55,10 @@ describe("object-query", () => {
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "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: 3 }, false]
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, { val1: 3 }, false],
];
for (const [query, object, expected] of tests) {
+162 -15
View File
@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test";
import { Perf } from "../../src/core/utils";
import * as reqres from "../../src/core/utils/reqres";
import * as strings from "../../src/core/utils/strings";
import { Perf, datetimeStringUTC, isBlob, ucFirst } from "../../src/core/utils";
import * as utils from "../../src/core/utils";
async function wait(ms: number) {
return new Promise((resolve) => {
@@ -13,33 +12,33 @@ describe("Core Utils", async () => {
describe("[core] strings", async () => {
test("objectToKeyValueArray", async () => {
const obj = { a: 1, b: 2, c: 3 };
const result = strings.objectToKeyValueArray(obj);
const result = utils.objectToKeyValueArray(obj);
expect(result).toEqual([
{ key: "a", value: 1 },
{ key: "b", value: 2 },
{ key: "c", value: 3 }
{ key: "c", value: 3 },
]);
});
test("snakeToPascalWithSpaces", async () => {
const result = strings.snakeToPascalWithSpaces("snake_to_pascal");
const result = utils.snakeToPascalWithSpaces("snake_to_pascal");
expect(result).toBe("Snake To Pascal");
});
test("randomString", async () => {
const result = strings.randomString(10);
const result = utils.randomString(10);
expect(result).toHaveLength(10);
});
test("pascalToKebab", async () => {
const result = strings.pascalToKebab("PascalCase");
const result = utils.pascalToKebab("PascalCase");
expect(result).toBe("pascal-case");
});
test("replaceSimplePlaceholders", async () => {
const str = "Hello, {$name}!";
const vars = { name: "John" };
const result = strings.replaceSimplePlaceholders(str, vars);
const result = utils.replaceSimplePlaceholders(str, vars);
expect(result).toBe("Hello, John!");
});
});
@@ -49,33 +48,84 @@ describe("Core Utils", async () => {
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("Authorization", "Bearer 123");
const obj = reqres.headersToObject(headers);
const obj = utils.headersToObject(headers);
expect(obj).toEqual({
"content-type": "application/json",
authorization: "Bearer 123"
authorization: "Bearer 123",
});
});
test("replaceUrlParam", () => {
const url = "/api/:id/:name";
const params = { id: "123", name: "test" };
const result = reqres.replaceUrlParam(url, params);
const result = utils.replaceUrlParam(url, params);
expect(result).toBe("/api/123/test");
});
test("encode", () => {
const obj = { id: "123", name: "test" };
const result = reqres.encodeSearch(obj);
const result = utils.encodeSearch(obj);
expect(result).toBe("id=123&name=test");
const obj2 = { id: "123", name: ["test1", "test2"] };
const result2 = reqres.encodeSearch(obj2);
const result2 = utils.encodeSearch(obj2);
expect(result2).toBe("id=123&name=test1&name=test2");
const obj3 = { id: "123", name: { test: "test" } };
const result3 = reqres.encodeSearch(obj3, { encode: true });
const result3 = utils.encodeSearch(obj3, { encode: true });
expect(result3).toBe("id=123&name=%7B%22test%22%3A%22test%22%7D");
});
describe("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);
});
});
describe("perf", async () => {
@@ -108,4 +158,101 @@ describe("Core Utils", async () => {
expect(count).toBe(2);
});
});
describe("objects", () => {
test("omitKeys", () => {
const objects = [
[{ a: 1, b: 2, c: 3 }, ["a"], { b: 2, 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 }, ["a", "b"], { c: 3 }],
[{ a: 1, b: 2, c: 3 }, ["a", "b", "c"], {}],
] as [object, string[], object][];
for (const [obj, keys, expected] of objects) {
const result = utils.omitKeys(obj, keys as any);
expect(result).toEqual(expected);
}
});
test("isEqual", () => {
const objects = [
[1, 1, true],
[1, "1", false],
[1, 2, false],
["1", "1", true],
["1", "2", false],
[true, true, true],
[true, false, false],
[false, false, true],
[1, Number.NaN, false],
[Number.NaN, Number.NaN, true],
[null, null, true],
[null, undefined, false],
[undefined, undefined, true],
[new Map([["a", 1]]), new Map([["a", 1]]), true],
[new Map([["a", 1]]), new Map([["a", 2]]), false],
[new Map([["a", 1]]), new Map([["b", 1]]), false],
[
new Map([["a", 1]]),
new Map([
["a", 1],
["b", 2],
]),
false,
],
[{ a: 1 }, { a: 1 }, true],
[{ a: 1 }, { a: 2 }, false],
[{ a: 1 }, { b: 1 }, false],
[{ a: "1" }, { a: "1" }, true],
[{ a: "1" }, { a: "2" }, false],
[{ a: "1" }, { b: "1" }, false],
[{ a: 1 }, { a: 1, b: 2 }, false],
[{ a: [1, 2, 3] }, { a: [1, 2, 3] }, true],
[{ a: [1, 2, 3] }, { a: [1, 2, 4] }, false],
[{ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }, false],
[{ a: { b: 1 } }, { a: { b: 1 } }, true],
[{ a: { b: 1 } }, { a: { b: 2 } }, false],
[{ a: { b: 1 } }, { a: { c: 1 } }, false],
[{ a: { b: 1 } }, { a: { b: 1, c: 2 } }, false],
[[1, 2, 3], [1, 2, 3], true],
[[1, 2, 3], [1, 2, 4], false],
[[1, 2, 3], [1, 2, 3, 4], false],
[[{ a: 1 }], [{ a: 1 }], true],
[[{ a: 1 }], [{ a: 2 }], false],
[[{ a: 1 }], [{ b: 1 }], false],
] as [any, any, boolean][];
for (const [a, b, expected] of objects) {
const result = utils.isEqual(a, b);
expect(result).toEqual(expected);
}
});
test("getPath", () => {
const tests = [
[{ a: 1, b: 2, c: 3 }, "a", 1],
[{ a: 1, b: 2, c: 3 }, "b", 2],
[{ a: { b: 1 } }, "a.b", 1],
[{ a: { b: 1 } }, "a.b.c", null, null],
[{ a: { b: 1 } }, "a.b.c", 1, 1],
[[[1]], "0.0", 1],
] as [object, string, any, any][];
for (const [obj, path, expected, defaultValue] of tests) {
const result = utils.getPath(obj, path, defaultValue);
expect(result).toEqual(expected);
}
});
});
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);
});
});
});
+29 -29
View File
@@ -9,7 +9,7 @@ import {
ManyToOneRelation,
type MutatorResponse,
type RepositoryResponse,
TextField
TextField,
} from "../../src/data";
import { DataController } from "../../src/data/api/DataController";
import { dataConfigSchema } from "../../src/data/data-schema";
@@ -35,17 +35,17 @@ describe("[data] DataController", async () => {
meta: {
total: 0,
count: 0,
items: 0
}
items: 0,
},
});
expect(res).toEqual({
meta: {
total: 0,
count: 0,
items: 0
items: 0,
},
data: []
data: [],
});
});
@@ -59,22 +59,22 @@ describe("[data] DataController", async () => {
data: [] as any,
sql: "",
parameters: [] as any,
result: [] as any
result: [] as any,
});
expect(res).toEqual({
data: []
data: [],
});
});
describe("getController", async () => {
const users = new Entity("users", [
new TextField("name", { required: true }),
new TextField("bio")
new TextField("bio"),
]);
const posts = new Entity("posts", [new TextField("content")]);
const em = new EntityManager([users, posts], dummyConnection, [
new ManyToOneRelation(posts, users)
new ManyToOneRelation(posts, users),
]);
await em.schema().sync({ force: true });
@@ -83,12 +83,12 @@ describe("[data] DataController", async () => {
users: [
{ name: "foo", bio: "bar" },
{ name: "bar", bio: null },
{ name: "baz", bio: "!!!" }
{ name: "baz", bio: "!!!" },
],
posts: [
{ 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() };
@@ -116,9 +116,9 @@ describe("[data] DataController", async () => {
//console.log("app.routes", app.routes);
// create users
for await (const _user of fixtures.users) {
const res = await app.request("/users", {
const res = await app.request("/entity/users", {
method: "POST",
body: JSON.stringify(_user)
body: JSON.stringify(_user),
});
//console.log("res", { _user }, res);
const result = (await res.json()) as MutatorResponse;
@@ -131,9 +131,9 @@ describe("[data] DataController", async () => {
// create posts
for await (const _post of fixtures.posts) {
const res = await app.request("/posts", {
const res = await app.request("/entity/posts", {
method: "POST",
body: JSON.stringify(_post)
body: JSON.stringify(_post),
});
const result = (await res.json()) as MutatorResponse;
const { id, ...data } = result.data as any;
@@ -145,7 +145,7 @@ describe("[data] DataController", 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;
expect(data.meta.total).toBe(3);
@@ -156,14 +156,14 @@ describe("[data] DataController", async () => {
});
test("/:entity/query (func query)", async () => {
const res = await app.request("/users/query", {
const res = await app.request("/entity/users/query", {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify({
where: { bio: { $isnull: 1 } }
})
where: { bio: { $isnull: 1 } },
}),
});
const data = (await res.json()) as RepositoryResponse;
@@ -175,7 +175,7 @@ describe("[data] DataController", 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;
expect(data.meta.total).toBe(3);
@@ -186,7 +186,7 @@ describe("[data] DataController", 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>;
console.log("data", data);
@@ -197,9 +197,9 @@ describe("[data] DataController", async () => {
});
test("/:entity (update one)", async () => {
const res = await app.request("/users/3", {
const res = await app.request("/entity/users/3", {
method: "PATCH",
body: JSON.stringify({ name: "new name" })
body: JSON.stringify({ name: "new name" }),
});
const { data } = (await res.json()) as MutatorResponse;
@@ -208,7 +208,7 @@ describe("[data] DataController", 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;
console.log("data", data);
@@ -220,14 +220,14 @@ describe("[data] DataController", async () => {
});
test("/:entity/:id (delete one)", async () => {
const res = await app.request("/posts/2", {
method: "DELETE"
const res = await app.request("/entity/posts/2", {
method: "DELETE",
});
const { data } = (await res.json()) as RepositoryResponse<EntityData>;
expect(data).toEqual({ id: 2, ...fixtures.posts[1] });
// verify
const res2 = await app.request("/posts");
const res2 = await app.request("/entity/posts");
const data2 = (await res2.json()) as RepositoryResponse;
expect(data2.meta.total).toBe(1);
});
+34 -17
View File
@@ -30,7 +30,7 @@ describe("data-query-impl", () => {
[{ 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"]]
[{ val: { $like: "w*t" } }, '"val" like ?', ["w%t"]],
];
for (const [query, expectedSql, expectedParams] of tests) {
@@ -51,22 +51,22 @@ describe("data-query-impl", () => {
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"]
["foo", "bar"],
],
[
{ val1: { $eq: "foo" }, val2: { $eq: "bar" } },
'("val1" = ? and "val2" = ?)',
["foo", "bar"]
["foo", "bar"],
],
// or constructs
[
{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } },
'("val1" = ? or "val2" = ?)',
["foo", "bar"]
["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]]
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, '("val1" = ? or "val1" = ?)', [1, 2]],
];
for (const [query, expectedSql, expectedParams] of tests) {
@@ -86,7 +86,7 @@ describe("data-query-impl", () => {
// or constructs
[{ $or: { val1: { $eq: "foo" }, val2: { $eq: "bar" } } }, ["val1", "val2"]],
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]]
[{ val1: { $eq: 1 }, $or: { val1: { $eq: 2 } } }, ["val1"]],
];
for (const [query, expectedKeys] of tests) {
@@ -105,24 +105,41 @@ describe("data-query-impl", () => {
posts: {
with: {
images: {
select: ["id"]
}
}
}
}
select: ["id"],
},
},
},
},
},
{
with: {
posts: {
with: {
images: {
select: ["id"]
}
}
}
}
}
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);
}
});
});
+9 -9
View File
@@ -5,7 +5,7 @@ import {
NumberField,
PrimaryField,
Repository,
TextField
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper";
@@ -18,14 +18,14 @@ describe("some tests", async () => {
const users = new Entity("users", [
new TextField("username", { required: true, default_value: "nobody" }),
new TextField("email", { maxLength: 3 })
new TextField("email", { maxLength: 3 }),
]);
const posts = new Entity("posts", [
new TextField("title"),
new TextField("content"),
new TextField("created_at"),
new NumberField("likes", { default_value: 0 })
new NumberField("likes", { default_value: 0 }),
]);
const em = new EntityManager([users, posts], connection);
@@ -43,7 +43,7 @@ describe("some tests", async () => {
});*/
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.result).toEqual([]);
@@ -53,7 +53,7 @@ describe("some tests", async () => {
const query = await em.repository(users).findMany();
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.result).toEqual([]);
@@ -63,7 +63,7 @@ describe("some tests", async () => {
const query = await em.repository(posts).findMany();
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.result).toEqual([]);
@@ -74,7 +74,7 @@ describe("some tests", async () => {
new Entity("users", [
new TextField("username"),
new TextField("email"),
new TextField("email") // not throwing, it's just being ignored
new TextField("email"), // not throwing, it's just being ignored
]);
}).toBeDefined();
@@ -83,7 +83,7 @@ describe("some tests", async () => {
new TextField("username"),
new TextField("email"),
// field config differs, will throw
new TextField("email", { required: true })
new TextField("email", { required: true }),
]);
}).toThrow();
@@ -91,7 +91,7 @@ describe("some tests", async () => {
new Entity("users", [
new PrimaryField(),
new TextField("username"),
new TextField("email")
new TextField("email"),
]);
}).toBeDefined();
});
+2 -2
View File
@@ -16,7 +16,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME);
return true;
}
},
};
}
@@ -26,7 +26,7 @@ export function getDummyConnection(memory: boolean = true) {
return {
dummyConnection,
afterAllCleanup
afterAllCleanup,
};
}
+3 -3
View File
@@ -6,7 +6,7 @@ import {
ManyToOneRelation,
NumberField,
SchemaManager,
TextField
TextField,
} from "../../src/data";
import { getDummyConnection } from "./helper";
@@ -21,7 +21,7 @@ describe("Mutator relation", async () => {
const posts = new Entity("posts", [
new TextField("title"),
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")]);
@@ -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: data.id })
em.mutator(posts).insertOne({ title: "post2", users_id: data.id }),
).resolves.toBeDefined();
});
});
+22 -22
View File
@@ -14,7 +14,7 @@ describe("Mutator simple", async () => {
const items = new Entity("items", [
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);
@@ -29,11 +29,11 @@ describe("Mutator simple", async () => {
test("insert single row", async () => {
const mutation = await em.mutator(items).insertOne({
label: "test",
count: 1
count: 1,
});
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 });
@@ -41,8 +41,8 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
dir: "desc"
}
dir: "desc",
},
});
expect(query.result).toEqual([{ id: 1, label: "test", count: 1 }]);
@@ -53,18 +53,18 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
dir: "desc"
}
dir: "desc",
},
});
const id = query.data![0].id as number;
const mutation = await em.mutator(items).updateOne(id, {
label: "new label",
count: 100
count: 100,
});
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 });
});
@@ -74,15 +74,15 @@ describe("Mutator simple", async () => {
limit: 1,
sort: {
by: "id",
dir: "desc"
}
dir: "desc",
},
});
const id = query.data![0].id as number;
const mutation = await em.mutator(items).deleteOne(id);
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 });
@@ -94,7 +94,7 @@ describe("Mutator simple", async () => {
const incompleteCreate = async () =>
await em.mutator(items).insertOne({
//label: "test",
count: 1
count: 1,
});
expect(incompleteCreate()).rejects.toThrow();
@@ -104,7 +104,7 @@ describe("Mutator simple", async () => {
const invalidCreate1 = async () =>
await em.mutator(items).insertOne({
label: 111, // this should work
count: "1" // this should fail
count: "1", // this should fail
});
expect(invalidCreate1()).rejects.toThrow(TransformPersistFailedException);
@@ -112,7 +112,7 @@ describe("Mutator simple", async () => {
const invalidCreate2 = async () =>
await em.mutator(items).insertOne({
label: "", // this should fail
count: 1
count: 1,
});
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);
//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(res.data.count).toBe(0);
@@ -152,27 +152,27 @@ describe("Mutator simple", async () => {
await em.mutator(items).updateWhere(
{ count: 2 },
{
count: 10
}
count: 10,
},
);
expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", 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
await em.mutator(items).updateWhere(
{ count: 2 },
{
count: 1
}
count: 1,
},
);
expect((await em.repository(items).findMany()).data).toEqual([
{ id: 6, label: "update", count: 2 },
{ id: 7, label: "update too", count: 2 },
{ id: 8, label: "keep", count: 0 }
{ id: 8, label: "keep", count: 0 },
]);
});
+11 -11
View File
@@ -23,23 +23,23 @@ describe("Polymorphic", async () => {
source: "categories",
target: "media",
config: {
mappedBy: "image"
}
mappedBy: "image",
},
});
// media should not see categories
expect(em.relationsOf(media.name).map((r) => r.toJSON())).toEqual([]);
// it's important that media cannot access categories
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.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media"
"media",
]);
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([]);
@@ -48,7 +48,7 @@ describe("Polymorphic", async () => {
"id",
"path",
"reference",
"entity_id"
"entity_id",
]);
expect(media.getSelect()).toEqual(["id", "path"]);
});
@@ -60,7 +60,7 @@ describe("Polymorphic", async () => {
const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, {
mappedBy: "single",
targetCardinality: 1
targetCardinality: 1,
});
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -71,17 +71,17 @@ describe("Polymorphic", async () => {
// it's important that media cannot access categories
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.sourceRelationsOf(categories).map((r) => r.target.entity.name)).toEqual([
"media",
"media"
"media",
]);
expect(em.relations.sourceRelationsOf(categories).map((r) => r.target.reference)).toEqual([
"single",
"multiple"
"multiple",
]);
expect(em.relations.sourceRelationsOf(media).map((r) => r.target.entity.name)).toEqual([]);
@@ -90,7 +90,7 @@ describe("Polymorphic", async () => {
"id",
"path",
"reference",
"entity_id"
"entity_id",
]);
});
});
+36 -36
View File
@@ -12,7 +12,7 @@ import {
NumberField,
OneToOneRelation,
PolymorphicRelation,
TextField
TextField,
} from "../../src/data";
import { DummyConnection } from "../../src/data/connection/DummyConnection";
import {
@@ -31,7 +31,7 @@ import {
medium,
number,
relation,
text
text,
} from "../../src/data/prototype";
import { MediaField } from "../../src/media/MediaField";
@@ -54,7 +54,7 @@ describe("prototype", () => {
name: text(),
bio: text(),
age: number(),
some: number()
some: number(),
});
type db = {
users: Schema<typeof users>;
@@ -70,7 +70,7 @@ describe("prototype", () => {
name: text({ default_value: "hello" }).required(),
bio: text(),
age: number(),
some: number().required()
some: number().required(),
});
const obj: InsertSchema<typeof user> = { name: "yo", some: 1 };
@@ -83,12 +83,12 @@ describe("prototype", () => {
new TextField("title", { required: true }),
new TextField("content"),
new DateField("created_at", {
type: "datetime"
type: "datetime",
}),
// @ts-ignore
new MediaField("images", { entity: "posts" }),
// @ts-ignore
new MediaField("cover", { entity: "posts", max_items: 1 })
new MediaField("cover", { entity: "posts", max_items: 1 }),
]);
const posts2 = entity("posts", {
@@ -96,7 +96,7 @@ describe("prototype", () => {
content: text(),
created_at: datetime(),
images: media(),
cover: medium()
cover: medium(),
});
type Posts = Schema<typeof posts2>;
@@ -117,11 +117,11 @@ describe("prototype", () => {
type: "objects",
values: [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Not active" }
]
}
{ value: "inactive", label: "Not active" },
],
},
}),
new JsonField("json")
new JsonField("json"),
]);
const test2 = entity("test", {
@@ -134,10 +134,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({
enum: [
{ 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());
@@ -161,12 +161,12 @@ describe("prototype", () => {
// category has single image
new PolymorphicRelation(categories, _media, {
mappedBy: "image",
targetCardinality: 1
targetCardinality: 1,
}),
// post has multiple 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 = [
@@ -180,7 +180,7 @@ describe("prototype", () => {
relation(categories).polyToOne(_media, { mappedBy: "image" }),
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()));
@@ -194,21 +194,21 @@ describe("prototype", () => {
posts,
categories,
{
connectionTableMappedName: "custom"
connectionTableMappedName: "custom",
},
[new TextField("description")]
[new TextField("description")],
);
const fields = {
description: text()
description: text(),
};
let o: FieldSchema<typeof fields>;
const rel2 = relation(posts).manyToMany(
categories,
{
connectionTableMappedName: "custom"
connectionTableMappedName: "custom",
},
fields
fields,
);
expect(rel.toJSON()).toEqual(rel2.toJSON());
@@ -216,11 +216,11 @@ describe("prototype", () => {
test("devexample", async () => {
const users = entity("users", {
username: text()
username: text(),
});
const comments = entity("comments", {
content: text()
content: text(),
});
const posts = entity("posts", {
@@ -228,17 +228,17 @@ describe("prototype", () => {
content: text(),
created_at: datetime(),
images: media(),
cover: medium()
cover: medium(),
});
const categories = entity("categories", {
name: text(),
description: text(),
image: medium()
image: medium(),
});
const settings = entity("settings", {
theme: text()
theme: text(),
});
const test = entity("test", {
@@ -251,10 +251,10 @@ describe("prototype", () => {
status: enumm<"active" | "inactive">({
enum: [
{ 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", {});
@@ -270,7 +270,7 @@ describe("prototype", () => {
relation(users).oneToOne(settings),
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;
@@ -281,12 +281,12 @@ describe("prototype", () => {
{
posts: entity("posts", { name: text(), slug: text().required() }),
comments: entity("comments", { some: text() }),
users: entity("users", { email: text() })
users: entity("users", { email: text() }),
},
({ relation, index }, { posts, comments, users }) => {
relation(posts).manyToOne(comments).manyToOne(users);
index(posts).on(["name"]).on(["slug"], true);
}
},
);
type LocalDb = (typeof _em)["DB"];
@@ -294,7 +294,7 @@ describe("prototype", () => {
const es = [
new Entity("posts", [new TextField("name"), new TextField("slug", { required: true })]),
new Entity("comments", [new TextField("some")]),
new Entity("users", [new TextField("email")])
new Entity("users", [new TextField("email")]),
];
const _em2 = new EntityManager(
es,
@@ -302,8 +302,8 @@ describe("prototype", () => {
[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("slug")!], true)
]
new EntityIndex(es[0], [es[0].field("slug")!], true),
],
);
// @ts-ignore
+22 -22
View File
@@ -6,7 +6,7 @@ import {
ManyToOneRelation,
OneToOneRelation,
PolymorphicRelation,
RelationField
RelationField,
} from "../../src/data/relations";
import { getDummyConnection } from "./helper";
@@ -22,32 +22,32 @@ describe("Relations", async () => {
const r1 = new RelationField("users_id", {
reference: "users",
target: "users",
target_field: "id"
target_field: "id",
});
const sql1 = schema
.createTable("posts")
.addColumn(...r1.schema()!)
.addColumn(...em.connection.getFieldSchema(r1.schema())!)
.compile().sql;
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("author_id", {
reference: "author",
target: "users",
target_field: "id"
target_field: "id",
});
const sql2 = schema
.createTable("posts")
.addColumn(...r2.schema()!)
.addColumn(...em.connection.getFieldSchema(r2.schema())!)
.compile().sql;
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",
target: "users",
target_field: "id",
required: true
required: true,
});
expect(r1.isRequired()).toBeTrue();
});
@@ -66,8 +66,8 @@ describe("Relations", async () => {
const users = new Entity("users", [new TextField("username")]);
const posts = new Entity("posts", [
new TextField("title", {
maxLength: 2
})
maxLength: 2,
}),
]);
const entities = [users, posts];
@@ -122,7 +122,7 @@ describe("Relations", async () => {
.selectFrom(users.name)
.select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts"));
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);
const userObj = { id: 1, username: "test" };
@@ -142,7 +142,7 @@ describe("Relations", async () => {
.select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author"));
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);
const postObj = { id: 1, title: "test" };
@@ -158,7 +158,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: undefined,
relation_type: "n:1"
relation_type: "n:1",
});
expect(postAuthorRel!.helper(posts.name)!.getMutationInfo()).toEqual({
@@ -170,7 +170,7 @@ describe("Relations", async () => {
$detach: false,
primary: "id",
cardinality: 1,
relation_type: "n:1"
relation_type: "n:1",
});
/*console.log("ManyToOne (source=posts, target=users)");
@@ -225,7 +225,7 @@ describe("Relations", async () => {
$detach: false,
primary: "id",
cardinality: 1,
relation_type: "1:1"
relation_type: "1:1",
});
expect(userSettingRel!.helper(settings.name)!.getMutationInfo()).toEqual({
reference: "users",
@@ -236,7 +236,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: 1,
relation_type: "1:1"
relation_type: "1:1",
});
/*console.log("");
@@ -310,16 +310,16 @@ describe("Relations", async () => {
*/
const selectCategoriesFromPosts = kysely
.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(
'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
.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(
'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
@@ -332,7 +332,7 @@ describe("Relations", async () => {
$detach: true,
primary: "id",
cardinality: undefined,
relation_type: "m:n"
relation_type: "m:n",
});
expect(relations[0].helper(categories.name)!.getMutationInfo()).toEqual({
reference: "posts",
@@ -343,7 +343,7 @@ describe("Relations", async () => {
$detach: false,
primary: undefined,
cardinality: undefined,
relation_type: "m:n"
relation_type: "m:n",
});
/*console.log("");
+3 -3
View File
@@ -6,7 +6,7 @@ describe("[data] Entity", async () => {
new TextField("name", { required: true }),
new TextField("description"),
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 () => {
@@ -17,7 +17,7 @@ describe("[data] Entity", async () => {
expect(entity.getFillableFields().map((f) => f.name)).toEqual([
"name",
"description",
"hidden"
"hidden",
]);
});
@@ -28,7 +28,7 @@ describe("[data] Entity", async () => {
test("getDefaultObject", async () => {
expect(entity.getDefaultObject()).toEqual({
age: 18,
hidden: "secret"
hidden: "secret",
});
});
@@ -4,7 +4,7 @@ import {
EntityManager,
ManyToManyRelation,
ManyToOneRelation,
SchemaManager
SchemaManager,
} from "../../../src/data";
import { UnableToConnectException } from "../../../src/data/errors";
import { getDummyConnection } from "../helper";
@@ -25,7 +25,7 @@ describe("[data] EntityManager", async () => {
expect(await em.ping()).toBe(true);
expect(() => em.entity("...")).toThrow();
expect(() =>
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2")))
em.addRelation(new ManyToOneRelation(new Entity("1"), new Entity("2"))),
).toThrow();
expect(em.schema()).toBeInstanceOf(SchemaManager);
@@ -98,7 +98,7 @@ describe("[data] EntityManager", async () => {
expect(userTargetRel.map((r) => r.other(users).entity.name)).toEqual(["posts", "comments"]);
expect(postTargetRel.map((r) => r.other(posts).entity.name)).toEqual([
"comments",
"categories"
"categories",
]);
expect(commentTargetRel.map((r) => r.other(comments).entity.name)).toEqual([]);
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);
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');
});
@@ -23,7 +23,7 @@ describe("[data] JoinBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [
"posts"
"posts",
]);
const res = qb.compile();
@@ -34,7 +34,7 @@ describe("[data] JoinBuilder", async () => {
);*/
const qb2 = JoinBuilder.addClause(em, em.connection.kysely.selectFrom("posts"), posts, [
"author"
"author",
]);
const res2 = qb2.compile();
+33 -33
View File
@@ -9,7 +9,7 @@ import {
OneToOneRelation,
type RelationField,
RelationMutator,
TextField
TextField,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { getDummyConnection } from "../helper";
@@ -22,7 +22,7 @@ describe("[data] Mutator (base)", async () => {
new TextField("label", { required: true }),
new NumberField("count"),
new TextField("hidden", { hidden: true }),
new TextField("not_fillable", { fillable: false })
new TextField("not_fillable", { fillable: false }),
]);
const em = new EntityManager<any>([entity], dummyConnection);
await em.schema().sync({ force: true });
@@ -44,7 +44,7 @@ describe("[data] Mutator (base)", async () => {
test("updateOne", async () => {
const { data } = await em.mutator(entity).insertOne(payload);
const updated = await em.mutator(entity).updateOne(data.id, {
count: 2
count: 2,
});
expect(updated.parameters).toEqual([2, data.id]);
@@ -77,7 +77,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting relational field should just return key value to be added
expect(
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id)
postRelMutator.persistRelationField(postRelField, "users_id", userData.data.id),
).resolves.toEqual(["users_id", userData.data.id]);
// persisting invalid value should throw
@@ -86,8 +86,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
// persisting reference should ...
expect(
postRelMutator.persistReference(relations[0]!, "users", {
$set: { id: userData.data.id }
})
$set: { 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
@@ -99,8 +99,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect(
em.mutator(posts).insertOne({
title: "post1",
users_id: 100 // user does not exist yet
})
users_id: 100, // user does not exist yet
}),
).rejects.toThrow();
});
@@ -111,7 +111,7 @@ describe("[data] Mutator (ManyToOne)", async () => {
const em = new EntityManager([items, cats], dummyConnection, relations);
expect(em.mutator(items).insertOne({ label: "test" })).rejects.toThrow(
'Field "cats_id" is required'
'Field "cats_id" is required',
);
});
@@ -119,14 +119,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({
title: "post1",
users_id: data.id
users_id: data.id,
});
expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({
title: "post1",
users_id: null
users_id: null,
});
expect(res2.data.users_id).toBe(null);
});
@@ -135,14 +135,14 @@ describe("[data] Mutator (ManyToOne)", async () => {
const { data } = await em.mutator(users).insertOne({ username: "user1" });
const res = await em.mutator(posts).insertOne({
title: "post1",
users: { $set: { id: data.id } }
users: { $set: { id: data.id } },
});
expect(res.data.users_id).toBe(data.id);
// setting "null" should be allowed
const res2 = await em.mutator(posts).insertOne({
title: "post1",
users: { $set: { id: null } }
users: { $set: { id: null } },
});
expect(res2.data.users_id).toBe(null);
});
@@ -151,8 +151,8 @@ describe("[data] Mutator (ManyToOne)", async () => {
expect(
em.mutator(posts).insertOne({
title: "test",
users: { $create: { username: "test" } }
})
users: { $create: { username: "test" } },
}),
).rejects.toThrow();
});
@@ -162,27 +162,27 @@ describe("[data] Mutator (ManyToOne)", async () => {
const res2 = await em.mutator(posts).insertOne({ title: "post1" });
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);
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);
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);
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);
const up4 = await em.mutator(posts).updateOne(res2.data.id, {
users_id: null
users_id: null,
});
expect(up4.data.users_id).toBe(null);
});
@@ -199,8 +199,8 @@ describe("[data] Mutator (OneToOne)", async () => {
expect(
em.mutator(users).insertOne({
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();
});
@@ -210,15 +210,15 @@ describe("[data] Mutator (OneToOne)", async () => {
expect(
em.mutator(users).insertOne({
username: "test",
settings: { $set: { id: data.id } }
})
settings: { $set: { id: data.id } },
}),
).rejects.toThrow();
});
test("insertOne: using $create", async () => {
const res = await em.mutator(users).insertOne({
username: "test",
settings: { $create: { theme: "dark" } }
settings: { $create: { theme: "dark" } },
});
expect(res.data.settings_id).toBeDefined();
});
@@ -303,7 +303,7 @@ describe("[data] Mutator (Events)", async () => {
test("insertOne event return is respected", async () => {
const posts = proto.entity("posts", {
title: proto.text(),
views: proto.number()
views: proto.number(),
});
const conn = getDummyConnection();
@@ -318,10 +318,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => {
return {
...event.params.data,
views: 2
views: 2,
};
},
"sync"
"sync",
);
const mutator = em.mutator("posts");
@@ -329,14 +329,14 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({
id: 1,
title: "test",
views: 2
views: 2,
});
});
test("updateOne event return is respected", async () => {
const posts = proto.entity("posts", {
title: proto.text(),
views: proto.number()
views: proto.number(),
});
const conn = getDummyConnection();
@@ -351,10 +351,10 @@ describe("[data] Mutator (Events)", async () => {
async (event) => {
return {
...event.params.data,
views: event.params.data.views + 1
views: event.params.data.views + 1,
};
},
"sync"
"sync",
);
const mutator = em.mutator("posts");
@@ -363,7 +363,7 @@ describe("[data] Mutator (Events)", async () => {
expect(result.data).toEqual({
id: 1,
title: "test",
views: 3
views: 3,
});
});
});
+6 -6
View File
@@ -7,7 +7,7 @@ import {
LibsqlConnection,
ManyToOneRelation,
RepositoryEvents,
TextField
TextField,
} from "../../../src/data";
import { getDummyConnection } from "../helper";
@@ -70,13 +70,13 @@ describe("[Repository]", async () => {
const q1 = selectQ(conn).compile();
const res = await client.execute({
sql: q1.sql,
args: q1.parameters as any
args: q1.parameters as any,
});
const q2 = countQ(conn).compile();
const count = await client.execute({
sql: q2.sql,
args: q2.parameters as any
args: q2.parameters as any,
});
return [res, count];
}
@@ -93,7 +93,7 @@ describe("[Repository]", async () => {
const exec = async (
name: string,
fn: (em: EntityManager<any>) => Promise<any>,
em: EntityManager<any>
em: EntityManager<any>,
) => {
const res = await Perf.execute(() => fn(em), times);
await sleep(1000);
@@ -102,7 +102,7 @@ describe("[Repository]", async () => {
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)
last: res.marks[res.marks.length - 1].time.toFixed(2),
};
console.log(info.name, info, res.marks);
return info;
@@ -183,7 +183,7 @@ describe("[data] Repository (Events)", async () => {
const items = new Entity("items", [new TextField("label")]);
const categories = new Entity("categories", [new TextField("label")]);
const em = new EntityManager([items, categories], dummyConnection, [
new ManyToOneRelation(categories, items)
new ManyToOneRelation(categories, items),
]);
await em.schema().sync({ force: true });
const events = new Map<string, any>();
+33 -33
View File
@@ -15,7 +15,7 @@ describe("SchemaManager tests", async () => {
const em = new EntityManager([entity], dummyConnection, [], [index]);
const schema = new SchemaManager(em);
const introspection = schema.getIntrospectionFromEntity(em.entities[0]);
const introspection = schema.getIntrospectionFromEntity(em.entities[0]!);
expect(introspection).toEqual({
name: "test",
isView: false,
@@ -26,7 +26,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: true,
hasDefaultValue: false,
comment: undefined
comment: undefined,
},
{
name: "username",
@@ -34,7 +34,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined
comment: undefined,
},
{
name: "email",
@@ -42,7 +42,7 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined
comment: undefined,
},
{
name: "bio",
@@ -50,8 +50,8 @@ describe("SchemaManager tests", async () => {
isNullable: true,
isAutoIncrementing: false,
hasDefaultValue: false,
comment: undefined
}
comment: undefined,
},
],
indices: [
{
@@ -61,11 +61,11 @@ describe("SchemaManager tests", async () => {
columns: [
{
name: "email",
order: 0
}
]
}
]
order: 0,
},
],
},
],
});
});
@@ -77,10 +77,10 @@ describe("SchemaManager tests", async () => {
new Entity(table, [
new TextField("username"),
new TextField("email"),
new TextField("bio")
])
new TextField("bio"),
]),
],
dummyConnection
dummyConnection,
);
const kysely = em.connection.kysely;
@@ -101,15 +101,15 @@ describe("SchemaManager tests", async () => {
name: table,
isNew: false,
columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [index] }
}
indices: { add: [], drop: [index] },
},
]);
// now sync
await schema.sync({ force: true, drop: true });
const diffAfter = await schema.getDiff();
console.log("diffAfter", diffAfter);
//console.log("diffAfter", diffAfter);
expect(diffAfter.length).toBe(0);
await kysely.schema.dropTable(table).execute();
@@ -119,7 +119,7 @@ describe("SchemaManager tests", async () => {
const table = "drop_column";
const em = new EntityManager(
[new Entity(table, [new TextField("username")])],
dummyConnection
dummyConnection,
);
const kysely = em.connection.kysely;
@@ -141,10 +141,10 @@ describe("SchemaManager tests", async () => {
columns: {
add: [],
drop: ["email"],
change: []
change: [],
},
indices: { add: [], drop: [] }
}
indices: { add: [], drop: [] },
},
]);
// now sync
@@ -165,15 +165,15 @@ describe("SchemaManager tests", async () => {
new Entity(usersTable, [
new TextField("username"),
new TextField("email"),
new TextField("bio")
new TextField("bio"),
]),
new Entity(postsTable, [
new TextField("title"),
new TextField("content"),
new TextField("created_at")
])
new TextField("created_at"),
]),
],
dummyConnection
dummyConnection,
);
const kysely = em.connection.kysely;
@@ -192,7 +192,7 @@ describe("SchemaManager tests", async () => {
name: usersTable,
isNew: false,
columns: { add: ["bio"], drop: [], change: [] },
indices: { add: [], drop: [] }
indices: { add: [], drop: [] },
},
{
name: postsTable,
@@ -200,10 +200,10 @@ describe("SchemaManager tests", async () => {
columns: {
add: ["id", "title", "content", "created_at"],
drop: [],
change: []
change: [],
},
indices: { add: [], drop: [] }
}
indices: { add: [], drop: [] },
},
]);
// now sync
@@ -228,8 +228,8 @@ describe("SchemaManager tests", async () => {
name: entity.name,
isNew: true,
columns: { add: ["id", "email"], drop: [], change: [] },
indices: { add: [index.name!], drop: [] }
}
indices: { add: [index.name!], drop: [] },
},
]);
// sync and then check again
@@ -256,8 +256,8 @@ describe("SchemaManager tests", async () => {
name: entity.name,
isNew: false,
columns: { add: [], drop: [], change: [] },
indices: { add: [index.name!], drop: [] }
}
indices: { add: [index.name!], drop: [] },
},
]);
// sync and then check again
+86 -87
View File
@@ -1,5 +1,4 @@
import { afterAll, describe, expect, test } from "bun:test";
import { _jsonp } from "../../../src/core/utils";
import { describe, expect, test } from "bun:test";
import {
Entity,
EntityManager,
@@ -7,10 +6,10 @@ import {
ManyToOneRelation,
PolymorphicRelation,
TextField,
WithBuilder
WithBuilder,
} from "../../../src/data";
import * as proto from "../../../src/data/prototype";
import { compileQb, prettyPrintQb, schemaToEm } from "../../helper";
import { schemaToEm } from "../../helper";
import { getDummyConnection } from "../helper";
const { dummyConnection } = getDummyConnection();
@@ -21,32 +20,32 @@ describe("[data] WithBuilder", async () => {
{
posts: proto.entity("posts", {}),
users: proto.entity("users", {}),
media: proto.entity("media", {})
media: proto.entity("media", {}),
},
({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" });
}
},
);
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", { users: {} })).toBe(1);
expect(
WithBuilder.validateWiths(em, "posts", {
users: {
with: { avatar: {} }
}
})
with: { avatar: {} },
},
}),
).toBe(2);
expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow();
expect(() =>
WithBuilder.validateWiths(em, "posts", {
users: {
with: { glibberish: {} }
}
})
with: { glibberish: {} },
},
}),
).toThrow();
});
@@ -56,8 +55,8 @@ describe("[data] WithBuilder", async () => {
expect(() =>
WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {}
})
posts: {},
}),
).toThrow('Relation "users<>posts" not found');
});
@@ -68,13 +67,13 @@ describe("[data] WithBuilder", async () => {
const em = new EntityManager([users, posts], dummyConnection, relations);
const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, {
posts: {}
posts: {},
});
const res = qb.compile();
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]);
@@ -83,14 +82,14 @@ describe("[data] WithBuilder", async () => {
em.connection.kysely.selectFrom("posts"),
posts, // @todo: try with "users", it gives output!
{
author: {}
}
author: {},
},
);
const res2 = qb2.compile();
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 ? offset ?) as obj) as "author" from "posts"',
);
expect(res2.parameters).toEqual([1, 0]);
});
@@ -124,7 +123,7 @@ describe("[data] WithBuilder", async () => {
.values([
{ posts_id: 1, categories_id: 1 },
{ posts_id: 2, categories_id: 2 },
{ posts_id: 1, categories_id: 2 }
{ posts_id: 1, categories_id: 2 },
])
.execute();
@@ -138,14 +137,14 @@ describe("[data] WithBuilder", async () => {
title: "fashion post",
categories: [
{ id: 1, label: "fashion" },
{ id: 2, label: "beauty" }
]
{ id: 2, label: "beauty" },
],
},
{
id: 2,
title: "beauty post",
categories: [{ id: 2, label: "beauty" }]
}
categories: [{ id: 2, label: "beauty" }],
},
]);
const res2 = await em.repository(categories).findMany({ with: { posts: {} } });
@@ -156,21 +155,21 @@ describe("[data] WithBuilder", async () => {
{
id: 1,
label: "fashion",
posts: [{ id: 1, title: "fashion post" }]
posts: [{ id: 1, title: "fashion post" }],
},
{
id: 2,
label: "beauty",
posts: [
{ id: 1, title: "fashion post" },
{ id: 2, title: "beauty post" }
]
{ id: 2, title: "beauty post" },
],
},
{
id: 3,
label: "tech",
posts: []
}
posts: [],
},
]);
});
@@ -181,7 +180,7 @@ describe("[data] WithBuilder", async () => {
const entities = [media, categories];
const single = new PolymorphicRelation(categories, media, {
mappedBy: "single",
targetCardinality: 1
targetCardinality: 1,
});
const multiple = new PolymorphicRelation(categories, media, { mappedBy: "multiple" });
@@ -191,11 +190,11 @@ describe("[data] WithBuilder", async () => {
em,
em.connection.kysely.selectFrom("categories"),
categories,
{ single: {} }
{ single: {} },
);
const res = qb.compile();
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 ? offset ?) as obj) as "single" from "categories"',
);
expect(res.parameters).toEqual(["categories.single", 1, 0]);
@@ -203,11 +202,11 @@ describe("[data] WithBuilder", async () => {
em,
em.connection.kysely.selectFrom("categories"),
categories,
{ multiple: {} }
{ multiple: {} },
);
const res2 = qb2.compile();
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]);
});
@@ -240,16 +239,16 @@ describe("[data] WithBuilder", async () => {
{
posts: proto.entity("posts", {}),
users: proto.entity("users", {
username: proto.text()
username: proto.text(),
}),
media: proto.entity("media", {
path: proto.text()
})
path: proto.text(),
}),
},
({ relation }, { posts, users, media }) => {
relation(posts).manyToOne(users);
relation(users).polyToOne(media, { mappedBy: "avatar" });
}
},
);
const em = schemaToEm(schema);
@@ -265,16 +264,16 @@ describe("[data] WithBuilder", async () => {
with: {
avatar: {
select: ["id", "path"],
limit: 2 // ignored
}
}
}
}
limit: 2, // ignored
},
},
},
},
);
//prettyPrintQb(qb);
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 ? 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"',
);
expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]);
});
@@ -285,17 +284,17 @@ describe("[data] WithBuilder", async () => {
posts: proto.entity("posts", {}),
comments: proto.entity("comments", {}),
users: proto.entity("users", {
username: proto.text()
username: proto.text(),
}),
media: proto.entity("media", {
path: proto.text()
})
path: proto.text(),
}),
},
({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users);
}
},
);
const em = schemaToEm(schema);
@@ -308,15 +307,15 @@ describe("[data] WithBuilder", async () => {
limit: 12,
with: {
users: {
select: ["username"]
}
}
}
}
select: ["username"],
},
},
},
},
);
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 ? 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"',
);
expect(qb.compile().parameters).toEqual([1, 0, 12, 0]);
});
@@ -325,23 +324,23 @@ describe("[data] WithBuilder", async () => {
const schema = proto.em(
{
posts: proto.entity("posts", {
title: proto.text()
title: proto.text(),
}),
comments: proto.entity("comments", {
content: proto.text()
content: proto.text(),
}),
users: proto.entity("users", {
username: proto.text()
username: proto.text(),
}),
media: proto.entity("media", {
path: proto.text()
})
path: proto.text(),
}),
},
({ relation }, { posts, comments, users, media }) => {
relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" });
relation(users).polyToOne(media, { mappedBy: "avatar" });
relation(comments).manyToOne(posts).manyToOne(users);
}
},
);
const em = schemaToEm(schema);
await em.schema().sync({ force: true });
@@ -351,7 +350,7 @@ describe("[data] WithBuilder", async () => {
await em.mutator("posts").insertMany([
{ title: "post1", users_id: 1 },
{ title: "post2", users_id: 1 },
{ title: "post3", users_id: 2 }
{ title: "post3", users_id: 2 },
]);
await em.mutator("comments").insertMany([
{ 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: "comment4", posts_id: 2, users_id: 2 },
{ 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({
@@ -371,11 +370,11 @@ describe("[data] WithBuilder", async () => {
select: ["content"],
with: {
users: {
select: ["username"]
}
}
}
}
select: ["username"],
},
},
},
},
});
expect(result.data).toEqual([
@@ -385,16 +384,16 @@ describe("[data] WithBuilder", async () => {
{
content: "comment1",
users: {
username: "user1"
}
username: "user1",
},
},
{
content: "comment1-1",
users: {
username: "user1"
}
}
]
username: "user1",
},
},
],
},
{
title: "post2",
@@ -402,16 +401,16 @@ describe("[data] WithBuilder", async () => {
{
content: "comment3",
users: {
username: "user1"
}
username: "user1",
},
},
{
content: "comment4",
users: {
username: "user2"
}
}
]
username: "user2",
},
},
],
},
{
title: "post3",
@@ -419,17 +418,17 @@ describe("[data] WithBuilder", async () => {
{
content: "comment5",
users: {
username: "user1"
}
username: "user1",
},
},
{
content: "comment6",
users: {
username: "user2"
}
}
]
}
username: "user2",
},
},
],
},
]);
//console.log(_jsonp(result.data));
});
@@ -22,10 +22,10 @@ describe("Connection", async () => {
columns: [
{
name: "name",
order: 0
}
]
}
order: 0,
},
],
},
]);
});
@@ -54,14 +54,14 @@ describe("Connection", async () => {
columns: [
{
name: "name",
order: 0
order: 0,
},
{
name: "desc",
order: 1
}
]
}
order: 1,
},
],
},
]);
});
@@ -83,10 +83,10 @@ describe("Connection", async () => {
columns: [
{
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,
},
],
},
]);
});
});
@@ -10,12 +10,12 @@ describe("[data] EnumField", async () => {
runBaseFieldTests(
EnumField,
{ 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 () => {
expect(
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" })
() => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }),
).toThrow();
});
@@ -31,7 +31,7 @@ describe("[data] EnumField", async () => {
const field = new EnumField("test", {
options: options(["a", "b", "c"]),
default_value: "a",
required: true
required: true,
});
expect(field.transformRetrieve(null)).toBe("a");
+17 -11
View File
@@ -1,43 +1,49 @@
import { describe, expect, test } from "bun:test";
import { Default, parse, stripMark } from "../../../../src/core/utils";
import { Field, type SchemaResponse, TextField, baseFieldConfigSchema } from "../../../../src/data";
import { runBaseFieldTests, transformPersist } from "./inc";
import { Default, stripMark } from "../../../../src/core/utils";
import { baseFieldConfigSchema, Field } from "../../../../src/data/fields/Field";
import { runBaseFieldTests } from "./inc";
describe("[data] Field", async () => {
class FieldSpec extends Field {
schema(): SchemaResponse {
return this.useSchemaHelper("text");
}
getSchema() {
return baseFieldConfigSchema;
}
}
test("fieldSpec", () => {
expect(new FieldSpec("test").schema()).toEqual({
name: "test",
type: "text",
nullable: true, // always true
dflt: undefined, // never using default value
});
});
runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" });
test("default config", async () => {
const config = Default(baseFieldConfigSchema, {});
expect(stripMark(new FieldSpec("test").config)).toEqual(config);
expect(stripMark(new FieldSpec("test").config)).toEqual(config as any);
});
test("transformPersist (specific)", async () => {
const required = new FieldSpec("test", { required: true });
const requiredDefault = new FieldSpec("test", {
required: true,
default_value: "test"
default_value: "test",
});
expect(required.transformPersist(null, undefined as any, undefined as any)).rejects.toThrow();
expect(
required.transformPersist(undefined, undefined as any, undefined as any)
required.transformPersist(undefined, undefined as any, undefined as any),
).rejects.toThrow();
// works because it has a default value
expect(
requiredDefault.transformPersist(null, undefined as any, undefined as any)
requiredDefault.transformPersist(null, undefined as any, undefined as any),
).resolves.toBeDefined();
expect(
requiredDefault.transformPersist(undefined, undefined as any, undefined as any)
requiredDefault.transformPersist(undefined, undefined as any, undefined as any),
).resolves.toBeDefined();
});
});
@@ -5,7 +5,7 @@ import {
EntityIndex,
type EntityManager,
Field,
type SchemaResponse
type SchemaResponse,
} from "../../../../src/data";
class TestField extends Field {
@@ -7,7 +7,7 @@ describe("[data] JsonField", async () => {
runBaseFieldTests(JsonField, {
defaultValue: { a: 1 },
sampleValues: ["string", { test: 1 }, 1],
schemaType: "text"
schemaType: "text",
});
test("transformPersist (no config)", async () => {
@@ -10,7 +10,12 @@ describe("[data] PrimaryField", async () => {
test("schema", () => {
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 () => {
+22 -19
View File
@@ -18,7 +18,7 @@ export function transformPersist(field: Field, value: any, context?: TActionCont
export function runBaseFieldTests(
fieldClass: ConstructableField,
config: FieldTestConfig,
_requiredConfig: any = {}
_requiredConfig: any = {},
) {
const noConfigField = new fieldClass("no_config", _requiredConfig);
const fillable = new fieldClass("fillable", { ..._requiredConfig, fillable: true });
@@ -29,16 +29,19 @@ export function runBaseFieldTests(
..._requiredConfig,
fillable: true,
required: true,
default_value: config.defaultValue
default_value: config.defaultValue,
});
test("schema", () => {
expect(noConfigField.name).toBe("no_config");
expect(noConfigField.schema(null as any)).toEqual([
"no_config",
config.schemaType,
expect.any(Function)
]);
const { type, name, nullable, dflt } = noConfigField.schema()!;
expect({ type, name, nullable, dflt }).toEqual({
type: config.schemaType as any,
name: "no_config",
nullable: true, // always true
dflt: undefined, // never using default value
});
});
test("hasDefault", async () => {
@@ -96,7 +99,7 @@ export function runBaseFieldTests(
//order: 1,
fillable: true,
required: false,
hidden: false
hidden: false,
//virtual: false,
//default_value: undefined
};
@@ -105,20 +108,20 @@ export function runBaseFieldTests(
const json = field.toJSON();
return {
...json,
config: omit(json.config, ["html"])
config: omit(json.config, ["html"]),
};
}
expect(fieldJson(noConfigField)).toEqual({
//name: "no_config",
type: noConfigField.type,
config: _config
config: _config,
});
expect(fieldJson(fillable)).toEqual({
//name: "fillable",
type: noConfigField.type,
config: _config
config: _config,
});
expect(fieldJson(required)).toEqual({
@@ -126,8 +129,8 @@ export function runBaseFieldTests(
type: required.type,
config: {
..._config,
required: true
}
required: true,
},
});
expect(fieldJson(hidden)).toEqual({
@@ -135,8 +138,8 @@ export function runBaseFieldTests(
type: required.type,
config: {
..._config,
hidden: true
}
hidden: true,
},
});
expect(fieldJson(dflt)).toEqual({
@@ -144,8 +147,8 @@ export function runBaseFieldTests(
type: dflt.type,
config: {
..._config,
default_value: config.defaultValue
}
default_value: config.defaultValue,
},
});
expect(fieldJson(requiredAndDefault)).toEqual({
@@ -155,8 +158,8 @@ export function runBaseFieldTests(
..._config,
fillable: true,
required: true,
default_value: config.defaultValue
}
default_value: config.defaultValue,
},
});
});
}
@@ -4,7 +4,7 @@ import {
type BaseRelationConfig,
EntityRelation,
EntityRelationAnchor,
RelationTypes
RelationTypes,
} from "../../../../src/data/relations";
class TestEntityRelation extends EntityRelation {
@@ -12,7 +12,7 @@ class TestEntityRelation extends EntityRelation {
super(
new EntityRelationAnchor(new Entity("source"), "source"),
new EntityRelationAnchor(new Entity("target"), "target"),
config
config,
);
}
initialize(em: EntityManager<any>) {}
+12 -12
View File
@@ -30,14 +30,14 @@ beforeAll(() =>
method: init?.method ?? "GET",
// @ts-ignore
headers: Object.fromEntries(init?.headers?.entries() ?? []),
body: init?.body
body: init?.body,
};
return new Response(JSON.stringify({ todos: [1, 2], request }), {
status: 200,
headers: { "Content-Type": "application/json" }
headers: { "Content-Type": "application/json" },
});
})
}),
);
afterAll(unmockFetch);
@@ -46,7 +46,7 @@ describe("FetchTask", async () => {
const task = new FetchTask("Fetch Something", {
url: "https://jsonplaceholder.typicode.com/todos/1",
method: "GET",
headers: [{ key: "Content-Type", value: "application/json" }]
headers: [{ key: "Content-Type", value: "application/json" }],
});
const result = await task.run();
@@ -62,18 +62,18 @@ describe("FetchTask", async () => {
expect(
// // @ts-expect-error
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 })
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: 1 }),
).toThrow();
expect(
new FetchTask("", {
url: "https://jsonplaceholder.typicode.com",
method: "invalid"
}).execute()
method: "invalid",
}).execute(),
).rejects.toThrow(/^Invalid method/);
expect(
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" })
() => new FetchTask("", { url: "https://jsonplaceholder.typicode.com", method: "GET" }),
).toBeDefined();
expect(() => new FetchTask("", { url: "", method: "Invalid" })).toThrow();
@@ -85,17 +85,17 @@ describe("FetchTask", async () => {
method: "{{ flow.output.method }}",
headers: [
{ 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({
email: "{{ flow.output.email }}"
})
email: "{{ flow.output.email }}",
}),
});
const inputs = {
headerKey: "Type",
apiKey: 123,
email: "what@else.com",
method: "PATCH"
method: "PATCH",
};
const flow = new Flow("", [task]);
+9 -9
View File
@@ -4,16 +4,16 @@ import { Flow, LogTask, RenderTask, SubFlowTask } from "../../src/flows";
describe("SubFlowTask", async () => {
test("Simple Subflow", async () => {
const subTask = new RenderTask("render", {
render: "subflow"
render: "subflow",
});
const subflow = new Flow("subflow", [subTask]);
const task = new LogTask("log");
const task2 = new SubFlowTask("sub", {
flow: subflow
flow: subflow,
});
const task3 = new RenderTask("render2", {
render: "Subflow output: {{ sub.output }}"
render: "Subflow output: {{ sub.output }}",
});
const flow = new Flow("test", [task, task2, task3], []);
@@ -30,7 +30,7 @@ describe("SubFlowTask", async () => {
test("Simple loop", async () => {
const subTask = new RenderTask("render", {
render: "run {{ flow.output }}"
render: "run {{ flow.output }}",
});
const subflow = new Flow("subflow", [subTask]);
@@ -38,10 +38,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", {
flow: subflow,
loop: true,
input: [1, 2, 3]
input: [1, 2, 3],
});
const task3 = new RenderTask("render2", {
render: `Subflow output: {{ sub.output | join: ", " }}`
render: `Subflow output: {{ sub.output | join: ", " }}`,
});
const flow = new Flow("test", [task, task2, task3], []);
@@ -61,7 +61,7 @@ describe("SubFlowTask", async () => {
test("Simple loop from flow input", async () => {
const subTask = new RenderTask("render", {
render: "run {{ flow.output }}"
render: "run {{ flow.output }}",
});
const subflow = new Flow("subflow", [subTask]);
@@ -70,10 +70,10 @@ describe("SubFlowTask", async () => {
const task2 = new SubFlowTask("sub", {
flow: subflow,
loop: true,
input: "{{ flow.output | json }}"
input: "{{ flow.output | json }}",
});
const task3 = new RenderTask("render2", {
render: `Subflow output: {{ sub.output | join: ", " }}`
render: `Subflow output: {{ sub.output | join: ", " }}`,
});
const flow = new Flow("test", [task, task2, task3], []);
+26 -26
View File
@@ -8,13 +8,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Number()) }),
{
test: "{{ some.path }}"
test: "{{ some.path }}",
},
{
some: {
path: 1
}
}
path: 1,
},
},
);
expect(result.test).toBe(1);
@@ -24,13 +24,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: Type.String() }),
{
test: "{{ some.path }}"
test: "{{ some.path }}",
},
{
some: {
path: "1/1"
}
}
path: "1/1",
},
},
);
expect(result.test).toBe("1/1");
@@ -40,13 +40,13 @@ describe("Task", async () => {
const result = await Task.resolveParams(
Type.Object({ test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })) }),
{
test: { key: "path", value: "{{ some.path }}" }
test: { key: "path", value: "{{ some.path }}" },
},
{
some: {
path: "1/1"
}
}
path: "1/1",
},
},
);
expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -55,17 +55,17 @@ describe("Task", async () => {
test("resolveParams: with json", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() }))
test: dynamic(Type.Object({ key: Type.String(), value: Type.String() })),
}),
{
test: "{{ some | json }}"
test: "{{ some | json }}",
},
{
some: {
key: "path",
value: "1/1"
}
}
value: "1/1",
},
},
);
expect(result.test).toEqual({ key: "path", value: "1/1" });
@@ -74,11 +74,11 @@ describe("Task", async () => {
test("resolveParams: with array", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Array(Type.String()))
test: dynamic(Type.Array(Type.String())),
}),
{
test: '{{ "1,2,3" | split: "," | json }}'
}
test: '{{ "1,2,3" | split: "," | json }}',
},
);
expect(result.test).toEqual(["1", "2", "3"]);
@@ -87,11 +87,11 @@ describe("Task", async () => {
test("resolveParams: boolean", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Boolean())
test: dynamic(Type.Boolean()),
}),
{
test: "{{ true }}"
}
test: "{{ true }}",
},
);
expect(result.test).toEqual(true);
@@ -100,11 +100,11 @@ describe("Task", async () => {
test("resolveParams: float", async () => {
const result = await Task.resolveParams(
Type.Object({
test: dynamic(Type.Number(), Number.parseFloat)
test: dynamic(Type.Number(), Number.parseFloat),
}),
{
test: "{{ 3.14 }}"
}
test: "{{ 3.14 }}",
},
);
expect(result.test).toEqual(3.14);
+3 -3
View File
@@ -7,11 +7,11 @@ const first = getNamedTask(
//throw new Error("Error");
return {
inner: {
result: 2
}
result: 2,
},
};
},
1000
1000,
);
const second = getNamedTask("second (if match)");
const third = getNamedTask("third (if error)");
+3 -3
View File
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor(
name: string,
params: any,
private fn: () => any
private fn: () => any,
) {
super(name, params);
}
@@ -54,8 +54,8 @@ export function getNamedTask(name: string, _func?: () => Promise<any>, delay?: n
return new ExecTask(
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 third = new LogTask("Long Third", { delay: 2500 });
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
+11 -11
View File
@@ -23,10 +23,10 @@ class OutputParamTask extends Task<typeof OutputParamTask.schema> {
static override schema = Type.Object({
number: dynamic(
Type.Number({
title: "Output number"
title: "Output number",
}),
Number.parseInt
)
Number.parseInt,
),
});
async execute(inputs: InputsMap) {
@@ -75,7 +75,7 @@ describe("Flow task inputs", async () => {
test("output/input", async () => {
const task = new OutputParamTask("task1", { number: 111 });
const task2 = new OutputParamTask("task2", {
number: "{{ task1.output }}"
number: "{{ task1.output }}",
});
const flow = new Flow("test", [task, task2]);
@@ -94,10 +94,10 @@ describe("Flow task inputs", async () => {
test("input from flow", async () => {
const task = new OutputParamTask("task1", {
number: "{{flow.output.someFancyParam}}"
number: "{{flow.output.someFancyParam}}",
});
const task2 = new OutputParamTask("task2", {
number: "{{task1.output}}"
number: "{{task1.output}}",
});
const flow = new Flow("test", [task, task2]);
@@ -126,7 +126,7 @@ describe("Flow task inputs", async () => {
const emgr = new EventManager({ EventTriggerClass });
const task = new OutputParamTask("event", {
number: "{{flow.output.number}}"
number: "{{flow.output.number}}",
});
const flow = new Flow(
"test",
@@ -134,8 +134,8 @@ describe("Flow task inputs", async () => {
[],
new EventTrigger({
event: "test-event",
mode: "sync"
})
mode: "sync",
}),
);
flow.setRespondingTask(task);
flow.trigger.register(flow, emgr);
@@ -155,8 +155,8 @@ describe("Flow task inputs", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
mode: "sync"
})
mode: "sync",
}),
);
flow.setRespondingTask(task);
+4 -4
View File
@@ -10,7 +10,7 @@ const flows = {
back,
fanout,
parallel,
simpleFetch
simpleFetch,
};
const arg = process.argv[2];
@@ -32,7 +32,7 @@ const colors = [
"#F78F1E", // Saffron
"#BD10E0", // Vivid Purple
"#50E3C2", // Turquoise
"#9013FE" // Grape
"#9013FE", // Grape
];
const colorsCache: Record<string, string> = {};
@@ -82,7 +82,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
}
return t;
})
}),
);
}
});
@@ -92,7 +92,7 @@ function TerminalFlow({ flow }: { flow: Flow }) {
console.log("done", response ? response : "(no response)");
console.log(
"Executed tasks:",
execution.logs.map((l) => l.task.name)
execution.logs.map((l) => l.task.name),
);
console.log("Executed count:", execution.logs.length);
});
+7 -7
View File
@@ -11,7 +11,7 @@ class ExecTask extends Task {
constructor(
name: string,
params: any,
private fn: () => any
private fn: () => any,
) {
super(name, params);
}
@@ -60,7 +60,7 @@ describe("Flow trigger", async () => {
"test",
[task],
[],
new EventTrigger({ event: "test-event", mode: "sync" })
new EventTrigger({ event: "test-event", mode: "sync" }),
);
flow.trigger.register(flow, emgr);
@@ -107,8 +107,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
mode: "sync"
})
mode: "sync",
}),
);
const hono = new Hono();
@@ -123,7 +123,7 @@ describe("Flow trigger", async () => {
test("http trigger with response", async () => {
const task = ExecTask.create("http", () => ({
called: true
called: true,
}));
const flow = new Flow(
"test",
@@ -132,8 +132,8 @@ describe("Flow trigger", async () => {
new HttpTrigger({
path: "/test",
method: "GET",
mode: "sync"
})
mode: "sync",
}),
);
flow.setRespondingTask(task);
+14 -14
View File
@@ -11,13 +11,13 @@ class ExecTask extends Task<typeof ExecTask.schema> {
type = "exec";
static override schema = Type.Object({
delay: Type.Number({ default: 10 })
delay: Type.Number({ default: 10 }),
});
constructor(
name: string,
params: Static<typeof ExecTask.schema>,
private func: () => Promise<any>
private func: () => Promise<any>,
) {
super(name, params);
}
@@ -36,12 +36,12 @@ function getTask(num: number = 0, delay: number = 5) {
return new ExecTask(
`Task ${num}`,
{
delay
delay,
},
async () => {
//console.log(`[DONE] Task: ${num}`);
return true;
}
},
);
//return new LogTask(`Log ${num}`, { delay });
}
@@ -56,9 +56,9 @@ function getNamedTask(name: string, _func?: () => Promise<any>, delay?: number)
return new ExecTask(
name,
{
delay: delay ?? 0
delay: delay ?? 0,
},
func
func,
);
}
@@ -228,7 +228,7 @@ describe("Flow tests", async () => {
back
.task(third)
.getOutTasks()
.map((t) => t.name)
.map((t) => t.name),
).toEqual(["second", "fourth"]);
const execution = back.createExecution();
@@ -263,7 +263,7 @@ describe("Flow tests", async () => {
back
.task(third)
.getOutTasks()
.map((t) => t.name)
.map((t) => t.name),
).toEqual(["second", "fourth"]);
const execution = back.createExecution();
@@ -324,8 +324,8 @@ describe("Flow tests", async () => {
const first = getNamedTask("first", async () => {
return {
inner: {
result: 2
}
result: 2,
},
};
});
const second = getNamedTask("second");
@@ -361,7 +361,7 @@ describe("Flow tests", async () => {
"[event]",
event.isStart() ? "start" : "end",
event.task().name,
event.isStart() ? undefined : event.succeeded()
event.isStart() ? undefined : event.succeeded(),
);
}
});
@@ -389,7 +389,7 @@ describe("Flow tests", async () => {
const second = new LogTask("Task 1");
const third = new LogTask("Task 2", { delay: 50 });
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
@@ -405,7 +405,7 @@ describe("Flow tests", async () => {
// @todo: fix
const deserialized = Flow.fromObject("", original, {
fetch: { cls: FetchTask },
log: { cls: LogTask }
log: { cls: LogTask },
} as any);
const diffdeep = getObjectDiff(original, deserialized.toJSON());
@@ -414,7 +414,7 @@ describe("Flow tests", async () => {
expect(flow.startTask.name).toEqual(deserialized.startTask.name);
expect(flow.respondingTask?.name).toEqual(
// @ts-ignore
deserialized.respondingTask?.name
deserialized.respondingTask?.name,
);
//console.log("--- creating original sequence");
+52 -3
View File
@@ -4,6 +4,9 @@ import Database from "libsql";
import { format as sqlFormat } from "sql-formatter";
import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data";
import type { em as protoEm } from "../src/data/prototype";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { slugify } from "core/utils/strings";
export function getDummyDatabase(memory: boolean = true): {
dummyDb: SqliteDatabase;
@@ -17,7 +20,7 @@ export function getDummyDatabase(memory: boolean = true): {
afterAllCleanup: async () => {
if (!memory) await unlink(DB_NAME);
return true;
}
},
};
}
@@ -27,7 +30,7 @@ export function getDummyConnection(memory: boolean = true) {
return {
dummyConnection,
afterAllCleanup
afterAllCleanup,
};
}
@@ -39,7 +42,7 @@ type ConsoleSeverity = "log" | "warn" | "error";
const _oldConsoles = {
log: console.log,
warn: console.warn,
error: console.error
error: console.error,
};
export function disableConsoleLog(severities: ConsoleSeverity[] = ["log", "warn"]) {
@@ -68,3 +71,49 @@ export function schemaToEm(s: ReturnType<typeof protoEm>, conn?: Connection): En
const connection = conn ? conn : getDummyConnection().dummyConnection;
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;
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 type { AuthResponse } from "../../src/auth";
import { auth } from "../../src/auth/middlewares";
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);
afterAll(enableConsoleLog);
@@ -15,25 +19,25 @@ const roles = {
"system.schema.read",
"system.access.api",
"system.config.read",
"data.entity.read"
"data.entity.read",
],
is_default: true
is_default: true,
},
admin: {
is_default: true,
implicit_allow: true
}
implicit_allow: true,
},
},
strict: {
guest: {
permissions: ["system.access.api", "system.config.read", "data.entity.read"],
is_default: true
is_default: true,
},
admin: {
is_default: true,
implicit_allow: true
}
}
implicit_allow: true,
},
},
};
const configs = {
auth: {
@@ -41,31 +45,32 @@ const configs = {
entity_name: "users",
jwt: {
secret: secureRandomString(20),
issuer: randomString(10)
issuer: randomString(10),
},
roles: roles.strict,
guard: {
enabled: true
}
enabled: true,
},
},
users: {
normal: {
email: "normal@bknd.io",
password: "12345678"
password: "12345678",
},
admin: {
email: "admin@bknd.io",
password: "12345678",
role: "admin"
}
}
role: "admin",
},
},
};
function createAuthApp() {
const app = createApp({
connection: dummyConnection,
initialConfig: {
auth: configs.auth
}
auth: configs.auth,
},
});
app.emgr.onEvent(
@@ -74,7 +79,7 @@ function createAuthApp() {
await app.createUser(configs.users.normal);
await app.createUser(configs.users.admin);
},
"sync"
"sync",
);
return app;
@@ -93,14 +98,14 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
if (mode === "cookie") {
return {
cookie: `auth=${token};`,
...additional
...additional,
};
}
return {
Authorization: `Bearer ${token}`,
Authorization: token ? `Bearer ${token}` : "",
"Content-Type": "application/json",
...additional
...additional,
};
}
function body(obj?: Record<string, any>) {
@@ -117,12 +122,12 @@ const fns = <Mode extends "cookie" | "token" = "token">(app: App, mode?: Mode) =
return {
login: async (
user: any
user: any,
): Promise<{ res: Response; data: Mode extends "token" ? AuthResponse : string }> => {
const res = (await app.server.request("/api/auth/password/login", {
method: "POST",
headers: headers(),
body: body(user)
body: body(user),
})) as Response;
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">> => {
const res = (await app.server.request("/api/auth/me", {
method: "GET",
headers: headers(token)
headers: headers(token),
})) as Response;
return await res.json();
}
},
};
};
@@ -210,4 +215,36 @@ describe("integration auth", () => {
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,24 @@
import { describe, expect, it } from "bun:test";
import { createApp } from "../../src";
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", {
name: "posts",
config: { sort_field: "id", sort_dir: "asc" },
fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } },
type: "regular",
});
expect(app.em.entities.map((e) => e.name)).toContain("posts");
});
});
+86 -45
View File
@@ -1,56 +1,97 @@
import { describe, test } from "bun:test";
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";
/// <reference types="@types/bun" />
const { dummyConnection, afterAllCleanup } = getDummyConnection();
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createApp, registries } from "../../src";
import { mergeObject, randomString } from "../../src/core/utils";
import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter";
import { assetsPath, assetsTmpPath, disableConsoleLog, enableConsoleLog } from "../helper";
/**
* 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()
};
beforeAll(() => {
registries.media.register("local", StorageLocalAdapter);
});
const media = new AppMedia(
// @ts-ignore
{
enabled: true,
adapter: {
type: "s3",
config: {
access_key: process.env.R2_ACCESS_KEY as string,
secret_access_key: process.env.R2_SECRET_ACCESS_KEY as string,
url: process.env.R2_URL as string
}
}
},
ctx
);
await media.build();
const app = new MediaController(media).getController();
const path = `${assetsPath}/image.png`;
const file = Bun.file(`${import.meta.dir}/adapters/icon.png`);
console.log("file", file);
async function makeApp(mediaOverride: Partial<TAppMediaConfig> = {}) {
const app = createApp({
initialConfig: {
media: mergeObject(
{
enabled: true,
adapter: {
type: "local",
config: {
path: assetsTmpPath,
},
},
},
mediaOverride,
),
},
});
await app.build();
return app;
}
function makeName(ext: string) {
return randomString(10) + "." + ext;
}
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("MediaController", () => {
test.only("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();
form.append("file", file);
await app.request("/upload/test.png", {
const res = await app.server.request("/api/media/upload/" + name, {
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);
});
});
+1 -1
View File
@@ -60,7 +60,7 @@ describe("Storage", async () => {
test("uploads a file", async () => {
const {
meta: { type, size }
meta: { type, size },
} = await storage.uploadFile("hello", "world.txt");
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
});
@@ -14,7 +14,7 @@ test("what", async () => {
const mf = new Miniflare({
modules: true,
script: "export default { async fetch() { return new Response(null); } }",
r2Buckets: ["BUCKET"]
r2Buckets: ["BUCKET"],
});
const bucket = await mf.getR2Bucket("BUCKET");
@@ -8,17 +8,19 @@ const {
CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET,
CLOUDINARY_UPLOAD_PRESET
CLOUDINARY_UPLOAD_PRESET,
} = dotenvOutput.parsed!;
const ALL_TESTS = !!process.env.ALL_TESTS;
describe.skipIf(ALL_TESTS)("StorageCloudinaryAdapter", () => {
if (ALL_TESTS) return;
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
upload_preset: CLOUDINARY_UPLOAD_PRESET as string,
});
const file = Bun.file(`${import.meta.dir}/icon.png`);
@@ -1,13 +1,14 @@
import { describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageLocalAdapter } from "../../../src/media/storage/adapters/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../../helper";
describe("StorageLocalAdapter", () => {
const adapter = new StorageLocalAdapter({
path: `${import.meta.dir}/local`
path: assetsTmpPath,
});
const file = Bun.file(`${import.meta.dir}/icon.png`);
const file = Bun.file(`${assetsPath}/image.png`);
const _filename = randomString(10);
const filename = `${_filename}.png`;
@@ -15,7 +16,7 @@ describe("StorageLocalAdapter", () => {
test("puts an object", async () => {
objects = (await adapter.listObjects()).length;
expect(await adapter.putObject(filename, file)).toBeString();
expect(await adapter.putObject(filename, file as unknown as File)).toBeString();
});
test("lists objects", async () => {
@@ -35,7 +36,7 @@ describe("StorageLocalAdapter", () => {
test("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
size: file.size
size: file.size,
});
});
@@ -1,34 +1,47 @@
import { describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { randomString } from "../../../src/core/utils";
import { StorageS3Adapter } from "../../../src/media";
import { config } from "dotenv";
//import { enableFetchLogging } from "../../helper";
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;
console.log("ALL_TESTS?", ALL_TESTS);
/*
// @todo: preparation to mock s3 calls + replace fast-xml-parser
let cleanup: () => void;
beforeAll(async () => {
cleanup = await enableFetchLogging();
});
afterAll(() => {
cleanup();
}); */
describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
console.log("ALL_TESTS", process.env.ALL_TESTS);
if (ALL_TESTS) return;
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
})
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
})
]
url: AWS_S3_URL as string,
}),
],
] as const;
const _conf = {
@@ -39,8 +52,8 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
"objectExists",
"getObject",
"deleteObject",
"getObjectMeta"
]
"getObjectMeta",
],
};
const file = Bun.file(`${import.meta.dir}/icon.png`);
@@ -55,7 +68,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
// @todo: add mocked fetch for faster tests
describe.each(versions)("StorageS3Adapter for %s", async (name, adapter) => {
if (!_conf.adapters.includes(name)) {
if (!_conf.adapters.includes(name) || ALL_TESTS) {
console.log("Skipping", name);
return;
}
@@ -64,7 +77,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
test.skipIf(disabled("putObject"))("puts an object", async () => {
objects = (await adapter.listObjects()).length;
expect(await adapter.putObject(filename, file)).toBeString();
expect(await adapter.putObject(filename, file as any)).toBeString();
});
test.skipIf(disabled("listObjects"))("lists objects", async () => {
@@ -84,7 +97,7 @@ describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => {
test.skipIf(disabled("getObjectMeta"))("gets object meta", async () => {
expect(await adapter.getObjectMeta(filename)).toEqual({
type: file.type, // image/png
size: file.size
size: file.size,
});
});
+62 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import * as large from "../../src/media/storage/mime-types";
import * as tiny from "../../src/media/storage/mime-types-tiny";
import { getRandomizedFilename } from "../../src/media/utils";
describe("media/mime-types", () => {
test("tiny resolves", () => {
@@ -27,11 +28,71 @@ describe("media/mime-types", () => {
exts,
ext,
expected: ex,
actual: large.guessMimeType("." + ext)
actual: large.guessMimeType("." + 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"],
];
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"],
];
for (const [filename, ext] of tests) {
expect(
getRandomizedFilename(filename).split(".").pop(),
`getRandomizedFilename(): ${filename} should end with ${ext}`,
).toBe(ext);
}
});
});
+27 -25
View File
@@ -4,6 +4,7 @@ import { AuthController } from "../../src/auth/api/AuthController";
import { em, entity, make, text } from "../../src/data";
import { AppAuth, type ModuleBuildContext } from "../../src/modules";
import { disableConsoleLog, enableConsoleLog } from "../helper";
// @ts-ignore
import { makeCtx, moduleTestSuite } from "./module-test-suite";
describe("AppAuth", () => {
@@ -22,7 +23,7 @@ describe("AppAuth", () => {
const config = auth.toJSON();
expect(config.jwt).toBeUndefined();
expect(config.strategies.password.config).toBeUndefined();
expect(config.strategies?.password?.config).toBeUndefined();
});
test("enabling auth: generate secret", async () => {
@@ -42,11 +43,12 @@ describe("AppAuth", () => {
const auth = new AppAuth(
{
enabled: true,
// @ts-ignore
jwt: {
secret: "123456"
}
secret: "123456",
},
},
ctx
ctx,
);
await auth.build();
@@ -63,19 +65,19 @@ describe("AppAuth", () => {
const res = await app.request("/password/register", {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "some@body.com",
password: "123456"
})
password: "123456",
}),
});
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");
expect(users[0]?.email).toBe("some@body.com");
}
});
@@ -85,10 +87,10 @@ describe("AppAuth", () => {
auth: {
enabled: true,
jwt: {
secret: "123456"
}
}
}
secret: "123456",
},
},
},
});
await app.build();
@@ -109,14 +111,14 @@ describe("AppAuth", () => {
initialConfig: {
auth: {
entity_name: "users",
enabled: true
enabled: true,
},
data: em({
users: entity("users", {
additional: text()
})
}).toJSON()
}
additional: text(),
}),
}).toJSON(),
},
});
await app.build();
@@ -132,21 +134,21 @@ describe("AppAuth", () => {
const app = createApp({
initialConfig: {
auth: {
enabled: true
enabled: true,
},
data: em({
users: entity("users", {
strategy: text({
fillable: true,
hidden: false
hidden: false,
}),
strategy_value: text({
fillable: true,
hidden: false
})
})
}).toJSON()
}
hidden: false,
}),
}),
}).toJSON(),
},
});
await app.build();
@@ -157,7 +159,7 @@ describe("AppAuth", () => {
const authField = make(name, _authFieldProto as any);
const field = users.field(name)!;
for (const prop of props) {
expect(field.config[prop]).toBe(authField.config[prop]);
expect(field.config[prop]).toEqual(authField.config[prop]);
}
}
});
+8 -8
View File
@@ -19,16 +19,16 @@ describe("AppMedia", () => {
adapter: {
type: "local",
config: {
path: "./"
}
}
path: "./",
},
},
},
data: em({
media: entity("media", {
additional: text()
})
}).toJSON()
}
additional: text(),
}),
}).toJSON(),
},
});
await app.build();
@@ -49,7 +49,7 @@ describe("AppMedia", () => {
"modified_at",
"reference",
"entity_id",
"metadata"
"metadata",
]);
});
});
+32 -32
View File
@@ -47,7 +47,7 @@ describe("Module", async () => {
prt = {
ensureEntity: this.ensureEntity.bind(this),
ensureIndex: this.ensureIndex.bind(this),
ensureSchema: this.ensureSchema.bind(this)
ensureSchema: this.ensureSchema.bind(this),
};
get em() {
@@ -60,7 +60,7 @@ describe("Module", async () => {
Object.values(_em.entities),
new DummyConnection(),
_em.relations,
_em.indices
_em.indices,
);
return new M({} as any, { em, flags: Module.ctx_flags } as any);
}
@@ -69,14 +69,14 @@ describe("Module", async () => {
entities: _em.entities.map((e) => ({
name: e.name,
fields: e.fields.map((f) => f.name),
type: e.type
type: e.type,
})),
indices: _em.indices.map((i) => ({
name: i.name,
entity: i.entity.name,
fields: i.fields.map((f) => f.name),
unique: i.unique
}))
unique: i.unique,
})),
};
}
@@ -88,15 +88,15 @@ describe("Module", async () => {
expect(flat(make(initial).em)).toEqual({
entities: [],
indices: []
indices: [],
});
});
test("init", () => {
const initial = em({
users: entity("u", {
name: text()
})
name: text(),
}),
});
const m = make(initial);
@@ -107,18 +107,18 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name"],
type: "regular"
}
type: "regular",
},
],
indices: []
indices: [],
});
});
test("ensure entity", () => {
const initial = em({
users: entity("u", {
name: text()
})
name: text(),
}),
});
const m = make(initial);
@@ -127,17 +127,17 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name"],
type: "regular"
}
type: "regular",
},
],
indices: []
indices: [],
});
// this should add a new entity
m.prt.ensureEntity(
entity("p", {
title: text()
})
title: text(),
}),
);
// this should only add the field "important"
@@ -145,11 +145,11 @@ describe("Module", async () => {
entity(
"u",
{
important: text()
important: text(),
},
undefined,
"system"
)
"system",
),
);
expect(m.ctx.flags.sync_required).toBe(true);
@@ -159,22 +159,22 @@ describe("Module", async () => {
name: "u",
fields: ["id", "name", "important"],
// ensured type must be present
type: "system"
type: "system",
},
{
name: "p",
fields: ["id", "title"],
type: "regular"
}
type: "regular",
},
],
indices: []
indices: [],
});
});
test("ensure index", () => {
const users = entity("u", {
name: text(),
title: text()
title: text(),
});
const initial = em({ users }, ({ index }, { users }) => {
index(users).on(["title"]);
@@ -189,23 +189,23 @@ describe("Module", async () => {
{
name: "u",
fields: ["id", "name", "title"],
type: "regular"
}
type: "regular",
},
],
indices: [
{
name: "idx_u_title",
entity: "u",
fields: ["title"],
unique: false
unique: false,
},
{
name: "idx_u_name",
entity: "u",
fields: ["name"],
unique: false
}
]
unique: false,
},
],
});
});
});
+166 -38
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import { stripMark } from "../../src/core/utils";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { Type, disableConsoleLog, enableConsoleLog, stripMark } from "../../src/core/utils";
import { entity, text } from "../../src/data";
import { Module } from "../../src/modules/Module";
import { ModuleManager, getDefaultConfig } from "../../src/modules/ModuleManager";
import { CURRENT_VERSION, TABLE_NAME } from "../../src/modules/migrations";
import { getDummyConnection } from "../helper";
@@ -38,11 +39,11 @@ describe("ModuleManager", async () => {
basepath: "/api/data2",
entities: {
test: entity("test", {
content: text()
}).toJSON()
}
}
});
content: text(),
}).toJSON(),
},
},
}) as any;
//const { version, ...json } = mm.toJSON() as any;
const c2 = getDummyConnection();
@@ -68,11 +69,11 @@ describe("ModuleManager", async () => {
basepath: "/api/data2",
entities: {
test: entity("test", {
content: text()
}).toJSON()
}
}
};
content: text(),
}).toJSON(),
},
},
} as any;
//const { version, ...json } = mm.toJSON() as any;
const { dummyConnection } = getDummyConnection();
@@ -89,23 +90,20 @@ describe("ModuleManager", async () => {
await mm2.build();
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();
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 () => {
const c = getDummyConnection();
const mm = new ModuleManager(c.dummyConnection);
await mm.build();
const version = mm.version();
const json = mm.configs();
const c2 = getDummyConnection();
const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { version: version - 1, ...json }
});
const mm2 = new ModuleManager(c2.dummyConnection);
await mm2.syncConfigTable();
await db
@@ -128,7 +126,7 @@ describe("ModuleManager", async () => {
const db = c2.dummyConnection.kysely;
const mm2 = new ModuleManager(c2.dummyConnection, {
initial: { version: version - 1, ...json }
initial: { version: version - 1, ...json },
});
await mm2.syncConfigTable();
await db
@@ -156,8 +154,8 @@ describe("ModuleManager", async () => {
...json,
data: {
...json.data,
basepath: "/api/data2"
}
basepath: "/api/data2",
},
};
await db
.insertInto(TABLE_NAME)
@@ -170,14 +168,14 @@ describe("ModuleManager", async () => {
expect(mm2.configs().data.basepath).toBe("/api/data2");
});
test("blank app, modify config", async () => {
/*test("blank app, modify config", async () => {
const { dummyConnection } = getDummyConnection();
const mm = new ModuleManager(dummyConnection);
await mm.build();
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();
await mm.get("server").schema().patch("admin", { color_scheme: "dark" });
await mm.save();
@@ -189,35 +187,37 @@ describe("ModuleManager", async () => {
...configs.server,
admin: {
...configs.server.admin,
color_scheme: "dark"
}
}
color_scheme: "dark",
},
},
});
});
});*/
test("partial config given", async () => {
const { dummyConnection } = getDummyConnection();
const partial = {
auth: {
enabled: true
}
enabled: true,
},
};
const mm = new ModuleManager(dummyConnection, {
initial: partial
initial: partial,
});
await mm.build();
expect(mm.version()).toBe(CURRENT_VERSION);
expect(mm.built()).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 () => {
const c = getDummyConnection();
const mm = new ModuleManager(c.dummyConnection);
await mm.build();
console.log("==".repeat(30));
console.log("");
const json = mm.configs();
const c2 = getDummyConnection();
@@ -226,9 +226,9 @@ describe("ModuleManager", async () => {
const mm2 = new ModuleManager(c2.dummyConnection, {
initial: {
auth: {
basepath: "/shouldnt/take/this"
}
}
basepath: "/shouldnt/take/this",
},
},
});
await mm2.syncConfigTable();
const payload = {
@@ -236,15 +236,15 @@ describe("ModuleManager", async () => {
auth: {
...json.auth,
enabled: true,
basepath: "/api/auth2"
}
basepath: "/api/auth2",
},
};
await db
.insertInto(TABLE_NAME)
.values({
type: "config",
json: JSON.stringify(payload),
version: CURRENT_VERSION
version: CURRENT_VERSION,
})
.execute();
await mm2.build();
@@ -252,4 +252,132 @@ describe("ModuleManager", async () => {
});
// @todo: add tests for migrations (check "backup" and new version)
describe("revert", async () => {
const failingModuleSchema = Type.Object({
value: Type.Optional(Type.Number()),
});
class FailingModule extends Module<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();
});
});
});
@@ -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"
}
}
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { Guard } from "../../src/auth";
import { DebugLogger } from "../../src/core";
import { EventManager } from "../../src/core/events";
import { Default, stripMark } from "../../src/core/utils";
import { EntityManager } from "../../src/data";
@@ -17,7 +18,8 @@ export function makeCtx(overrides?: Partial<ModuleBuildContext>): ModuleBuildCon
emgr: new EventManager(),
guard: new Guard(),
flags: Module.ctx_flags,
...overrides
logger: new DebugLogger(false),
...overrides,
};
}
+138
View File
@@ -0,0 +1,138 @@
import { describe, expect, test } from "bun:test";
import { Draft2019 } from "json-schema-library";
import type { JSONSchema } from "json-schema-to-ts";
import * as utils from "../../src/ui/components/form/json-schema-form/utils";
import type { IsTypeType } from "../../src/ui/components/form/json-schema-form/utils";
describe("json form", () => {
test("coerse", () => {
const examples = [
["test", { type: "string" }, "test"],
["1", { type: "integer" }, 1],
["1", { type: "number" }, 1],
["true", { type: "boolean" }, true],
["false", { type: "boolean" }, false],
["1", { type: "boolean" }, true],
["0", { type: "boolean" }, false],
["on", { type: "boolean" }, true],
["off", { type: "boolean" }, false],
["null", { type: "null" }, null],
] satisfies [string, Exclude<JSONSchema, boolean>, any][];
for (const [input, schema, output] of examples) {
expect(utils.coerce(input, schema)).toBe(output);
}
});
test("isType", () => {
const examples = [
["string", "string", true],
["integer", "number", false],
["number", "number", true],
["boolean", "boolean", true],
["null", "null", true],
["object", "object", true],
["array", "array", true],
["object", "array", false],
[["string", "number"], "number", true],
["number", ["string", "number"], true],
] satisfies [IsTypeType, IsTypeType, boolean][];
for (const [type, schemaType, output] of examples) {
expect(utils.isType(type, schemaType)).toBe(output);
}
});
test("getParentPointer", () => {
const examples = [
["#/nested/property/0/name", "#/nested/property/0"],
["#/nested/property/0", "#/nested/property"],
["#/nested/property", "#/nested"],
["#/nested", "#"],
];
for (const [input, output] of examples) {
expect(utils.getParentPointer(input)).toBe(output);
}
});
test("isRequired", () => {
const examples = [
[
"#/description",
{ type: "object", properties: { description: { type: "string" } } },
false,
],
[
"#/description",
{
type: "object",
required: ["description"],
properties: { description: { type: "string" } },
},
true,
],
[
"#/nested/property",
{
type: "object",
properties: {
nested: {
type: "object",
properties: { property: { type: "string" } },
},
},
},
false,
],
[
"#/nested/property",
{
type: "object",
properties: {
nested: {
type: "object",
required: ["property"],
properties: { property: { type: "string" } },
},
},
},
true,
],
] satisfies [string, Exclude<JSONSchema, boolean>, boolean][];
for (const [pointer, schema, output] of examples) {
expect(utils.isRequired(new Draft2019(schema), pointer, schema)).toBe(output);
}
});
test("prefixPath", () => {
const examples = [
["normal", "", "normal"],
["", "prefix", "prefix"],
["tags", "0", "0.tags"],
["tags", 0, "0.tags"],
["nested.property", "prefix", "prefix.nested.property"],
["nested.property", "", "nested.property"],
] satisfies [string, any, string][];
for (const [path, prefix, output] of examples) {
expect(utils.prefixPath(path, prefix)).toBe(output);
}
});
test("suffixPath", () => {
const examples = [
["normal", "", "normal"],
["", "suffix", "suffix"],
["tags", "0", "tags.0"],
["tags", 0, "tags.0"],
["nested.property", "suffix", "nested.property.suffix"],
["nested.property", "", "nested.property"],
] satisfies [string, any, string][];
for (const [path, suffix, output] of examples) {
expect(utils.suffixPath(path, suffix)).toBe(output);
}
});
});
-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:"
}
}
}
};
-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()]);
}
+176 -117
View File
@@ -27,9 +27,9 @@ function buildTypes() {
onExit: () => {
console.log("Types aliased");
types_running = false;
}
},
});
}
},
});
}
@@ -46,114 +46,150 @@ if (types && !watch) {
buildTypes();
}
function banner(title: string) {
console.log("");
console.log("=".repeat(40));
console.log(title.toUpperCase());
console.log("-".repeat(40));
}
// collection of always-external packages
const external = ["bun:test", "@libsql/client"] as const;
/**
* Building backend and general API
*/
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
outDir: "dist",
external: ["bun:test", "@libsql/client"],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
treeshake: true,
loader: {
".svg": "dataurl"
},
onSuccess: async () => {
delayTypes();
}
});
async function buildApi() {
banner("Building API");
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/index.ts", "src/data/index.ts", "src/core/index.ts", "src/core/utils/index.ts"],
outDir: "dist",
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
*/
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"],
outDir: "dist/ui",
external: [
"bun:test",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
/codemirror/,
"@xyflow/react",
"@mantine/core"
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl"
},
esbuildOptions: (options) => {
options.logLevel = "silent";
},
onSuccess: async () => {
delayTypes();
}
});
async function buildUi() {
const base = {
minify,
sourcemap,
watch,
external: [
...external,
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
/codemirror/,
"@xyflow/react",
"@mantine/core",
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl",
},
esbuildOptions: (options) => {
options.logLevel = "silent";
},
} satisfies tsup.Options;
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
* - tailwind-merge is mocked, no exclude
* - ui/client is external, and after built replaced with "bknd/client"
*/
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements",
external: [
"ui/client",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store"
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl"
},
esbuildOptions: (options) => {
options.alias = {
// not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts"
};
},
onSuccess: async () => {
// manually replace ui/client with bknd/client
const path = "./dist/ui/elements/index.js";
const bundle = await Bun.file(path).text();
await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client"));
delayTypes();
}
});
async function buildUiElements() {
banner("Building UI Elements");
await tsup.build({
minify,
sourcemap,
watch,
entry: ["src/ui/elements/index.ts"],
outDir: "dist/ui/elements",
external: [
"ui/client",
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
"use-sync-external-store",
],
metafile: true,
platform: "browser",
format: ["esm"],
splitting: false,
bundle: true,
treeshake: true,
loader: {
".svg": "dataurl",
},
esbuildOptions: (options) => {
options.alias = {
// not important for elements, mock to reduce bundle
"tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
};
},
onSuccess: async () => {
await rewriteClient("./dist/ui/elements/index.js");
delayTypes();
},
});
}
/**
* Building adapters
*/
function baseConfig(adapter: string): tsup.Options {
function baseConfig(adapter: string, overrides: Partial<tsup.Options> = {}): tsup.Options {
return {
minify,
sourcemap,
@@ -162,40 +198,63 @@ function baseConfig(adapter: string): tsup.Options {
format: ["esm"],
platform: "neutral",
outDir: `dist/adapter/${adapter}`,
metafile: true,
splitting: false,
onSuccess: async () => {
delayTypes();
},
...overrides,
define: {
__isDev: "0"
__isDev: "0",
...overrides.define,
},
external: [
/^cloudflare*/,
/^@?(hono|libsql).*?/,
/^(bknd|react|next|node).*?/,
/.*\.(html)$/
/.*\.(html)$/,
...(Array.isArray(overrides.external) ? overrides.external : []),
],
metafile: true,
splitting: false,
treeshake: true,
onSuccess: async () => {
delayTypes();
}
};
}
await tsup.build(baseConfig("remix"));
await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("cloudflare"));
async function buildAdapters() {
banner("Building Adapters");
// base adapter handles
await tsup.build({
...baseConfig(""),
entry: ["src/adapter/index.ts"],
outDir: "dist/adapter",
});
await tsup.build({
...baseConfig("vite"),
platform: "node"
});
// specific adatpers
await tsup.build(baseConfig("react-router"));
await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build(
baseConfig("cloudflare", {
external: [/^kysely/],
}),
);
await tsup.build({
...baseConfig("nextjs"),
platform: "node"
});
await tsup.build({
...baseConfig("vite"),
platform: "node",
});
await tsup.build({
...baseConfig("node"),
platform: "node"
});
await tsup.build({
...baseConfig("nextjs"),
platform: "node",
});
await tsup.build({
...baseConfig("node"),
platform: "node",
});
}
await buildApi();
await buildUi();
await buildUiElements();
await buildAdapters();
@@ -5,8 +5,8 @@ export const entryOutputMeta = (
outputs: {
output: string;
meta: Metafile["outputs"][string];
}[]
) => void | Promise<void>
}[],
) => void | Promise<void>,
): Plugin => ({
name: "report-entry-output-plugin",
setup(build) {
@@ -29,5 +29,5 @@ export const entryOutputMeta = (
await onComplete?.(outputs);
}
});
}
},
});
+79 -57
View File
@@ -3,8 +3,8 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.6.0",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"version": "0.10.0-rc.5",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {
"type": "git",
@@ -19,12 +19,12 @@
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
"build": "NODE_ENV=production bun run build.ts --minify --types",
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --env PUBLIC_* --minify",
"build:static": "vite build",
"watch": "bun run build.ts --types --watch",
"types": "bun tsc --noEmit",
"types": "bun tsc -p tsconfig.build.json --noEmit",
"clean:types": "find ./dist -name '*.d.ts' -delete && rm -f ./dist/tsconfig.tsbuildinfo",
"build:types": "tsc --emitDeclarationOnly && tsc-alias",
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias",
"updater": "bun x npm-check-updates -ui",
"cli": "LOCAL=1 bun src/cli/index.ts",
"prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./",
@@ -32,72 +32,84 @@
},
"license": "FSL-1.1-MIT",
"dependencies": {
"@cfworker/json-schema": "^2.0.1",
"@libsql/client": "^0.14.0",
"@sinclair/typebox": "^0.32.34",
"@tanstack/react-form": "0.19.2",
"aws4fetch": "^1.0.18",
"dayjs": "^1.11.13",
"fast-xml-parser": "^4.4.0",
"hono": "^4.6.12",
"kysely": "^0.27.4",
"liquidjs": "^10.15.0",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"swr": "^2.2.5",
"json-schema-form-react": "^0.0.2",
"@uiw/react-codemirror": "^4.23.6",
"@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-liquid": "^6.2.1",
"@xyflow/react": "^12.3.2",
"@mantine/core": "^7.13.4",
"@hello-pangea/dnd": "^17.0.0"
"@codemirror/lang-liquid": "^6.2.2",
"@hello-pangea/dnd": "^18.0.1",
"@libsql/client": "^0.14.0",
"@mantine/core": "^7.17.1",
"@mantine/hooks": "^7.17.1",
"@sinclair/typebox": "^0.34.30",
"@tanstack/react-form": "^1.0.5",
"@uiw/react-codemirror": "^4.23.10",
"@xyflow/react": "^12.4.4",
"aws4fetch": "^1.0.20",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.0.8",
"hono": "^4.7.4",
"json-schema-form-react": "^0.0.2",
"json-schema-library": "^10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
"kysely": "^0.27.6",
"liquidjs": "^10.21.0",
"lodash-es": "^4.17.21",
"oauth4webapi": "^2.11.1",
"object-path-immutable": "^4.1.2",
"picocolors": "^1.1.1",
"radix-ui": "^1.1.3",
"swr": "^2.3.3"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.613.0",
"@aws-sdk/client-s3": "^3.758.0",
"@bluwy/giget-core": "^0.1.2",
"@dagrejs/dagre": "^1.1.4",
"@hono/typebox-validator": "^0.2.6",
"@hono/vite-dev-server": "^0.17.0",
"@hono/zod-validator": "^0.4.1",
"@hookform/resolvers": "^3.9.1",
"@hono/typebox-validator": "^0.3.2",
"@hono/vite-dev-server": "^0.19.0",
"@hookform/resolvers": "^4.1.3",
"@libsql/kysely-libsql": "^0.4.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@rjsf/core": "^5.22.2",
"@mantine/modals": "^7.17.1",
"@mantine/notifications": "^7.17.1",
"@rjsf/core": "5.22.2",
"@tabler/icons-react": "3.18.0",
"@types/node": "^22.10.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20",
"@tailwindcss/postcss": "^4.0.12",
"@tailwindcss/vite": "^4.0.12",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"clsx": "^2.1.1",
"esbuild-postcss": "^0.0.4",
"jotai": "^2.10.1",
"dotenv": "^16.4.7",
"jotai": "^2.12.2",
"kysely-d1": "^0.3.0",
"open": "^10.1.0",
"openapi-types": "^12.1.3",
"postcss": "^8.4.47",
"postcss": "^8.5.3",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"react-hook-form": "^7.53.1",
"posthog-js-lite": "^3.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-icons": "5.2.1",
"react-json-view-lite": "^2.0.1",
"sql-formatter": "^15.4.9",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.14",
"react-json-view-lite": "^2.4.1",
"sql-formatter": "^15.4.11",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.12",
"tailwindcss-animate": "^1.0.7",
"tsc-alias": "^1.8.10",
"tsup": "^8.3.5",
"vite": "^5.4.10",
"vite-plugin-static-copy": "^2.0.0",
"vite-tsconfig-paths": "^5.0.1",
"wouter": "^3.3.5"
"tsc-alias": "^1.8.11",
"tsup": "^8.4.0",
"vite": "^6.2.1",
"vite-tsconfig-paths": "^5.1.4",
"wouter": "^3.6.0"
},
"optionalDependencies": {
"@hono/node-server": "^1.13.7"
"@hono/node-server": "^1.13.8"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
"react": ">=19",
"react-dom": ">=19"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -148,6 +160,10 @@
"import": "./dist/adapter/cloudflare/index.js",
"require": "./dist/adapter/cloudflare/index.cjs"
},
"./adapter": {
"types": "./dist/types/adapter/index.d.ts",
"import": "./dist/adapter/index.js"
},
"./adapter/vite": {
"types": "./dist/types/adapter/vite/index.d.ts",
"import": "./dist/adapter/vite/index.js",
@@ -158,10 +174,10 @@
"import": "./dist/adapter/nextjs/index.js",
"require": "./dist/adapter/nextjs/index.cjs"
},
"./adapter/remix": {
"types": "./dist/types/adapter/remix/index.d.ts",
"import": "./dist/adapter/remix/index.js",
"require": "./dist/adapter/remix/index.cjs"
"./adapter/react-router": {
"types": "./dist/types/adapter/react-router/index.d.ts",
"import": "./dist/adapter/react-router/index.js",
"require": "./dist/adapter/react-router/index.cjs"
},
"./adapter/bun": {
"types": "./dist/types/adapter/bun/index.d.ts",
@@ -178,6 +194,11 @@
"import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs"
},
"./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js",
"require": "./dist/adapter/aws/index.cjs"
},
"./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json"
@@ -206,6 +227,7 @@
"cloudflare",
"nextjs",
"remix",
"react-router",
"astro",
"bun",
"node"
+5 -8
View File
@@ -1,9 +1,6 @@
export default {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
@@ -11,8 +8,8 @@ export default {
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em"
}
}
}
"mantine-breakpoint-xl": "88em",
},
},
},
};
+76 -31
View File
@@ -2,9 +2,9 @@ import type { SafeUser } from "auth";
import { AuthApi } from "auth/api/AuthApi";
import { DataApi } from "data/api/DataApi";
import { decode } from "hono/jwt";
import { omit } from "lodash-es";
import { MediaApi } from "media/api/MediaApi";
import { SystemApi } from "modules/SystemApi";
import { omitKeys } from "core/utils";
export type TApiUser = SafeUser;
@@ -17,13 +17,22 @@ declare global {
}
export type ApiOptions = {
host: string;
user?: TApiUser;
token?: string;
host?: string;
headers?: Headers;
key?: string;
localStorage?: boolean;
};
fetcher?: typeof fetch;
verbose?: boolean;
verified?: boolean;
} & (
| {
token?: string;
user?: TApiUser;
}
| {
request: Request;
}
);
export type AuthState = {
token?: string;
@@ -42,14 +51,26 @@ export class Api {
public auth!: AuthApi;
public media!: MediaApi;
constructor(private readonly options: ApiOptions) {
if (options.user) {
this.user = options.user;
this.token_transport = "none";
this.verified = true;
} else if (options.token) {
constructor(private options: ApiOptions = {}) {
// only mark verified if forced
this.verified = options.verified === true;
// prefer request if given
if ("request" in options) {
this.options.host = options.host ?? new URL(options.request.url).origin;
this.options.headers = options.headers ?? options.request.headers;
this.extractToken();
// then check for a token
} else if ("token" in options) {
this.token_transport = "header";
this.updateToken(options.token);
// then check for an user object
} else if ("user" in options) {
this.token_transport = "none";
this.user = options.user;
this.verified = options.verified !== false;
} else {
this.extractToken();
}
@@ -58,7 +79,7 @@ export class Api {
}
get baseUrl() {
return this.options.host;
return this.options.host ?? "http://localhost";
}
get tokenKey() {
@@ -66,13 +87,15 @@ export class Api {
}
private extractToken() {
// if token has to be extracted, it's never verified
this.verified = false;
if (this.options.headers) {
// try cookies
const cookieToken = getCookieValue(this.options.headers.get("cookie"), "auth");
if (cookieToken) {
this.updateToken(cookieToken);
this.token_transport = "cookie";
this.verified = true;
this.updateToken(cookieToken);
return;
}
@@ -96,8 +119,10 @@ export class Api {
updateToken(token?: string, rebuild?: boolean) {
this.token = token;
this.verified = false;
if (token) {
this.user = omit(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
this.user = omitKeys(decode(token).payload as any, ["iat", "iss", "exp"]) as any;
} else {
this.user = undefined;
}
@@ -115,19 +140,28 @@ export class Api {
if (rebuild) this.buildApis();
}
markAuthVerified(verfied: boolean) {
private markAuthVerified(verfied: boolean) {
this.verified = verfied;
return this;
}
isAuthVerified(): boolean {
return this.verified;
}
getAuthState(): AuthState {
return {
token: this.token,
user: this.user,
verified: this.verified
verified: this.verified,
};
}
isAuthenticated(): boolean {
const { token, user } = this.getAuthState();
return !!token && !!user;
}
async getVerifiedAuthState(): Promise<AuthState> {
await this.verifyAuth();
return this.getAuthState();
@@ -140,11 +174,13 @@ export class Api {
}
try {
const res = await this.auth.me();
if (!res.ok || !res.body.user) {
const { ok, data } = await this.auth.me();
const user = data?.user;
if (!ok || !user) {
throw new Error();
}
this.user = user;
this.markAuthVerified(true);
} catch (e) {
this.markAuthVerified(false);
@@ -156,21 +192,30 @@ export class Api {
return this.user || null;
}
private buildApis() {
const baseParams = {
host: this.options.host,
getParams() {
return Object.freeze({
host: this.baseUrl,
token: this.token,
headers: this.options.headers,
token_transport: this.token_transport
};
this.system = new SystemApi(baseParams);
this.data = new DataApi(baseParams);
this.auth = new AuthApi({
...baseParams,
onTokenUpdate: (token) => this.updateToken(token, true)
token_transport: this.token_transport,
verbose: this.options.verbose,
});
this.media = new MediaApi(baseParams);
}
private buildApis() {
const baseParams = this.getParams();
const fetcher = this.options.fetcher;
this.system = new SystemApi(baseParams, fetcher);
this.data = new DataApi(baseParams, fetcher);
this.auth = new AuthApi(
{
...baseParams,
onTokenUpdate: (token) => this.updateToken(token, true),
},
fetcher,
);
this.media = new MediaApi(baseParams, fetcher);
}
}
+81 -34
View File
@@ -1,16 +1,22 @@
import type { CreateUserPayload } from "auth/AppAuth";
import { $console } from "core";
import { Event } from "core/events";
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
import type { Hono } from "hono";
import {
type InitialModuleConfigs,
type ModuleBuildContext,
ModuleManager,
type ModuleManagerOptions,
type Modules
type Modules,
} from "modules/ModuleManager";
import * as SystemPermissions from "modules/permissions";
import { AdminController, type AdminControllerOptions } from "modules/server/AdminController";
import { SystemController } from "modules/server/SystemController";
// biome-ignore format: must be there
import { Api, type ApiOptions } from "Api";
export type AppPlugin = (app: App) => Promise<void> | void;
abstract class AppEvent<A = {}> extends Event<{ app: App } & A> {}
@@ -25,57 +31,74 @@ export class AppFirstBoot extends AppEvent {
}
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
export type AppOptions = {
plugins?: AppPlugin[];
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
};
export type CreateAppConfig = {
connection?:
| Connection
| {
// @deprecated
type: "libsql";
config: LibSqlCredentials;
};
}
| LibSqlCredentials;
initialConfig?: InitialModuleConfigs;
plugins?: AppPlugin[];
options?: Omit<ModuleManagerOptions, "initial" | "onUpdated">;
options?: AppOptions;
};
export type AppConfig = InitialModuleConfigs;
export type LocalApiOptions = Request | ApiOptions;
export class App {
modules: ModuleManager;
static readonly Events = AppEvents;
adminController?: AdminController;
private trigger_first_boot = false;
private plugins: AppPlugin[];
private _id: string = crypto.randomUUID();
private _building: boolean = false;
constructor(
private connection: Connection,
_initialConfig?: InitialModuleConfigs,
private plugins: AppPlugin[] = [],
moduleManagerOptions?: ModuleManagerOptions
private options?: AppOptions,
) {
this.plugins = options?.plugins ?? [];
this.modules = new ModuleManager(connection, {
...moduleManagerOptions,
...(options?.manager ?? {}),
initial: _initialConfig,
onUpdated: async (key, config) => {
// if the EventManager was disabled, we assume we shouldn't
// respond to events, such as "onUpdated".
// this is important if multiple changes are done, and then build() is called manually
if (!this.emgr.enabled) {
console.warn("[APP] config updated, but event manager is disabled, skip.");
$console.warn("App config updated, but event manager is disabled, skip.");
return;
}
console.log("[APP] config updated", key);
await this.build({ sync: true, save: true });
$console.log("App config updated", key);
// @todo: potentially double syncing
await this.build({ sync: true });
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
},
onFirstBoot: async () => {
console.log("[APP] first boot");
$console.log("App first boot");
this.trigger_first_boot = true;
},
onServerInit: async (server) => {
server.use(async (c, next) => {
c.set("app", this);
await next();
try {
// gracefully add the app id
c.res.headers.set("X-bknd-id", this._id);
} catch (e) {}
});
}
},
});
this.modules.ctx().emgr.registerEvents(AppEvents);
}
@@ -84,15 +107,18 @@ export class App {
return this.modules.ctx().emgr;
}
async build(options?: { sync?: boolean; drop?: boolean; save?: boolean }) {
await this.modules.build();
if (options?.sync) {
const syncResult = await this.module.data.em
.schema()
.sync({ force: true, drop: options.drop });
//console.log("syncing", syncResult);
async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
// prevent multiple concurrent builds
if (this._building) {
while (this._building) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
if (!options?.forceBuild) return;
}
this._building = true;
if (options?.sync) this.modules.ctx().flags.sync_required = true;
await this.modules.build({ fetch: options?.fetch });
const { guard, server } = this.modules.ctx();
@@ -105,23 +131,24 @@ export class App {
await Promise.all(this.plugins.map((plugin) => plugin(this)));
}
$console.log("App built");
await this.emgr.emit(new AppBuiltEvent({ app: this }));
server.all("/api/*", async (c) => c.notFound());
if (options?.save) {
await this.modules.save();
}
// first boot is set from ModuleManager when there wasn't a config table
if (this.trigger_first_boot) {
this.trigger_first_boot = false;
await this.emgr.emit(new AppFirstBoot({ app: this }));
await this.options?.seed?.({
...this.modules.ctx(),
app: this,
});
}
this._building = false;
}
mutateConfig<Module extends keyof Modules>(module: Module) {
return this.modules.get(module).schema();
return this.modules.mutateConfigSafe(module);
}
get server() {
@@ -132,7 +159,7 @@ export class App {
return this.modules.ctx().em;
}
get fetch(): any {
get fetch(): Hono["fetch"] {
return this.server.fetch;
}
@@ -142,8 +169,8 @@ export class App {
{
get: (_, module: keyof Modules) => {
return this.modules.get(module);
}
}
},
},
) as Modules;
}
@@ -155,6 +182,10 @@ export class App {
return this.modules.version();
}
isBuilt(): boolean {
return this.modules.isBuilt();
}
registerAdminController(config?: AdminControllerOptions) {
// register admin
this.adminController = new AdminController(this, config);
@@ -173,6 +204,15 @@ export class App {
async createUser(p: CreateUserPayload) {
return this.module.auth.createUser(p);
}
getApi(options?: LocalApiOptions) {
const fetcher = this.server.request as typeof fetch;
if (options && options instanceof Request) {
return new Api({ request: options, headers: options.headers, fetcher });
}
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
}
}
export function createApp(config: CreateAppConfig = {}) {
@@ -182,18 +222,25 @@ export function createApp(config: CreateAppConfig = {}) {
if (Connection.isConnection(config.connection)) {
connection = config.connection;
} else if (typeof config.connection === "object") {
connection = new LibsqlConnection(config.connection.config);
if ("type" in config.connection) {
$console.warn(
"Using deprecated connection type 'libsql', use the 'config' object directly.",
);
connection = new LibsqlConnection(config.connection.config);
} else {
connection = new LibsqlConnection(config.connection);
}
} else {
connection = new LibsqlConnection({ url: ":memory:" });
console.warn("[!] No connection provided, using in-memory database");
$console.warn("No connection provided, using in-memory database");
}
} catch (e) {
console.error("Could not create connection", e);
$console.error("Could not create connection", e);
}
if (!connection) {
throw new Error("Invalid connection");
}
return new App(connection, config.initialConfig, config.plugins, config.options);
return new App(connection, config.initialConfig, config.options);
}
+12 -9
View File
@@ -1,7 +1,8 @@
import { type FrameworkBkndConfig, createFrameworkApp } from "adapter";
import { Api, type ApiOptions, type App } from "bknd";
import type { App } from "bknd";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
import { Api, type ApiOptions } from "bknd/client";
export type AstroBkndConfig = FrameworkBkndConfig;
export type AstroBkndConfig<Args = TAstro> = FrameworkBkndConfig<Args>;
type TAstro = {
request: Request;
@@ -13,18 +14,20 @@ export type Options = {
host?: string;
};
export function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
return new Api({
export async function getApi(Astro: TAstro, options: Options = { mode: "static" }) {
const api = new Api({
host: new URL(Astro.request.url).origin,
headers: options.mode === "dynamic" ? Astro.request.headers : undefined
headers: options.mode === "dynamic" ? Astro.request.headers : undefined,
});
await api.verifyAuth();
return api;
}
let app: App;
export function serve(config: AstroBkndConfig = {}) {
return async (args: TAstro) => {
export function serve<Context extends TAstro = TAstro>(config: AstroBkndConfig<Context> = {}) {
return async (args: Context) => {
if (!app) {
app = await createFrameworkApp(config);
app = await createFrameworkApp(config, args);
}
return app.fetch(args.request);
};
+68
View File
@@ -0,0 +1,68 @@
import type { App } from "bknd";
import { handle } from "hono/aws-lambda";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
assets?:
| {
mode: "local";
root: string;
}
| {
mode: "url";
url: string;
};
};
let app: App;
export async function createApp({
adminOptions = false,
assets,
...config
}: AwsLambdaBkndConfig = {}) {
if (!app) {
let additional: Partial<RuntimeBkndConfig> = {
adminOptions,
};
if (assets?.mode) {
switch (assets.mode) {
case "local":
// @todo: serve static outside app context
additional = {
adminOptions: adminOptions === false ? undefined : adminOptions,
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
root: assets.root,
onFound: (path, c) => {
c.res.headers.set("Cache-Control", "public, max-age=31536000");
},
}),
};
break;
case "url":
additional.adminOptions = {
...(typeof adminOptions === "object" ? adminOptions : {}),
assets_path: assets.url,
};
break;
default:
throw new Error("Invalid assets mode");
}
}
app = await createRuntimeApp({
...config,
...additional,
});
}
return app;
}
export function serveLambda(config: AwsLambdaBkndConfig = {}) {
console.log("serving lambda");
return async (event) => {
const app = await createApp(config);
return await handle(app.server)(event);
};
}
+1
View File
@@ -0,0 +1 @@
export * from "./aws-lambda.adapter";
+9 -8
View File
@@ -2,10 +2,11 @@
import path from "node:path";
import type { App } from "bknd";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { config } from "bknd/core";
import type { ServeOptions } from "bun";
import { config } from "core";
import { serveStatic } from "hono/bun";
import { type RuntimeBkndConfig, createRuntimeApp } from "../index";
let app: App;
@@ -15,10 +16,10 @@ export async function createApp({ distPath, ...config }: RuntimeBkndConfig = {})
const root = path.resolve(distPath ?? "./node_modules/bknd/dist", "static");
if (!app) {
registerLocalMediaAdapter();
app = await createRuntimeApp({
...config,
registerLocalMedia: true,
serveStatic: serveStatic({ root })
serveStatic: serveStatic({ root }),
});
}
@@ -29,11 +30,11 @@ export function serve({
distPath,
connection,
initialConfig,
plugins,
options,
port = config.server.default_port,
onBuilt,
buildConfig,
adminOptions,
...serveOptions
}: BunBkndConfig = {}) {
Bun.serve({
@@ -43,14 +44,14 @@ export function serve({
const app = await createApp({
connection,
initialConfig,
plugins,
options,
onBuilt,
buildConfig,
distPath
adminOptions,
distPath,
});
return app.fetch(request);
}
},
});
console.log(`Server is running on http://localhost:${port}`);
@@ -0,0 +1,59 @@
/// <reference types="@cloudflare/workers-types" />
import { KyselyPluginRunner, SqliteConnection, SqliteIntrospector } from "bknd/data";
import type { QB } from "data/connection/Connection";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin } from "kysely";
import { D1Dialect } from "kysely-d1";
export type D1ConnectionConfig = {
binding: D1Database;
};
class CustomD1Dialect extends D1Dialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, {
excludeTables: ["_cf_KV"],
});
}
}
export class D1Connection extends SqliteConnection {
protected override readonly supported = {
batching: true,
};
constructor(private config: D1ConnectionConfig) {
const plugins = [new ParseJSONResultsPlugin()];
const kysely = new Kysely({
dialect: new CustomD1Dialect({ database: config.binding }),
plugins,
});
super(kysely, {}, plugins);
}
protected override async batch<Queries extends QB[]>(
queries: [...Queries],
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
const db = this.config.binding;
const res = await db.batch(
queries.map((q) => {
const { sql, parameters } = q.compile();
return db.prepare(sql).bind(...parameters);
}),
);
// let it run through plugins
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
const data: any = [];
for (const r of res) {
const rows = await kyselyPlugins.transformResultRows(r.results);
data.push(rows);
}
return data;
}
}
@@ -1,6 +1,47 @@
import { isDebug } from "core";
import type { FileBody, StorageAdapter } from "../Storage";
import { guessMimeType } from "../mime-types";
import { registries } from "bknd";
import { isDebug } from "bknd/core";
import { StringEnum, Type } from "bknd/utils";
import type { FileBody, StorageAdapter } from "media/storage/Storage";
import { guess } from "media/storage/mime-types-tiny";
import { getBindings } from "./bindings";
export function makeSchema(bindings: string[] = []) {
return Type.Object(
{
binding: bindings.length > 0 ? StringEnum(bindings) : Type.Optional(Type.String()),
},
{ title: "R2", description: "Cloudflare R2 storage" },
);
}
export function registerMedia(env: Record<string, any>) {
const r2_bindings = getBindings(env, "R2Bucket");
registries.media.register(
"r2",
class extends StorageR2Adapter {
constructor(private config: any) {
const binding = r2_bindings.find((b) => b.key === config.binding);
if (!binding) {
throw new Error(`No R2Bucket found with key ${config.binding}`);
}
super(binding?.value);
}
override getSchema() {
return makeSchema(r2_bindings.map((b) => b.key));
}
override toJSON() {
return {
...super.toJSON(),
config: this.config,
};
}
},
);
}
/**
* Adapter for R2 storage
@@ -14,7 +55,7 @@ export class StorageR2Adapter implements StorageAdapter {
}
getSchema() {
return undefined;
return makeSchema();
}
async putObject(key: string, body: FileBody) {
@@ -26,13 +67,13 @@ export class StorageR2Adapter implements StorageAdapter {
}
}
async listObjects(
prefix?: string
prefix?: string,
): Promise<{ key: string; last_modified: Date; size: number }[]> {
const list = await this.bucket.list({ limit: 50 });
return list.objects.map((item) => ({
key: item.key,
size: item.size,
last_modified: item.uploaded
last_modified: item.uploaded,
}));
}
@@ -47,7 +88,8 @@ export class StorageR2Adapter implements StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> {
let object: R2ObjectBody | null;
const responseHeaders = new Headers({
"Accept-Ranges": "bytes"
"Accept-Ranges": "bytes",
"Content-Type": guess(key),
});
//console.log("getObject:headers", headersToObject(headers));
@@ -56,7 +98,7 @@ export class StorageR2Adapter implements StorageAdapter {
? {} // miniflare doesn't support range requests
: {
range: headers,
onlyIf: headers
onlyIf: headers,
};
object = (await this.bucket.get(key, options)) as R2ObjectBody;
@@ -88,7 +130,7 @@ export class StorageR2Adapter implements StorageAdapter {
return new Response(object.body, {
status: object.range ? 206 : 200,
headers: responseHeaders
headers: responseHeaders,
});
}
@@ -97,10 +139,9 @@ export class StorageR2Adapter implements StorageAdapter {
if (!metadata || Object.keys(metadata).length === 0) {
// guessing is especially required for dev environment (miniflare)
metadata = {
contentType: guessMimeType(object.key)
contentType: guess(object.key),
};
}
//console.log("writeHttpMetadata", object.httpMetadata, metadata);
for (const [key, value] of Object.entries(metadata)) {
const camelToDash = key.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -115,8 +156,8 @@ export class StorageR2Adapter implements StorageAdapter {
}
return {
type: String(head.httpMetadata?.contentType ?? "application/octet-stream"),
size: head.size
type: String(head.httpMetadata?.contentType ?? guess(key)),
size: head.size,
};
}
@@ -131,7 +172,7 @@ export class StorageR2Adapter implements StorageAdapter {
toJSON(secrets?: boolean) {
return {
type: this.getName(),
config: {}
config: {},
};
}
}
+32
View File
@@ -0,0 +1,32 @@
export type BindingTypeMap = {
D1Database: D1Database;
KVNamespace: KVNamespace;
DurableObjectNamespace: DurableObjectNamespace;
R2Bucket: R2Bucket;
};
export type GetBindingType = keyof BindingTypeMap;
export type BindingMap<T extends GetBindingType> = { key: string; value: BindingTypeMap[T] };
export function getBindings<T extends GetBindingType>(env: any, type: T): BindingMap<T>[] {
const bindings: BindingMap<T>[] = [];
for (const key in env) {
try {
if (env[key] && (env[key] as any).constructor.name === type) {
bindings.push({
key,
value: env[key] as BindingTypeMap[T],
});
}
} catch (e) {}
}
return bindings;
}
export function getBinding<T extends GetBindingType>(env: any, type: T): BindingMap<T> {
const bindings = getBindings(env, type);
if (bindings.length === 0) {
throw new Error(`No ${type} found in bindings`);
}
return bindings[0] as BindingMap<T>;
}
@@ -1,18 +1,23 @@
import type { CreateAppConfig } from "bknd";
/// <reference types="@cloudflare/workers-types" />
import { type FrameworkBkndConfig, makeConfig } from "bknd/adapter";
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import type { FrameworkBkndConfig } from "../index";
import { D1Connection } from "./D1Connection";
import { registerMedia } from "./StorageR2Adapter";
import { getBinding } from "./bindings";
import { getCached } from "./modes/cached";
import { getDurable } from "./modes/durable";
import { getFresh, getWarm } from "./modes/fresh";
export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> & {
app: CreateAppConfig | ((env: Env) => CreateAppConfig);
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & {
mode?: "warm" | "fresh" | "cache" | "durable";
bindings?: (env: Env) => {
bindings?: (args: Context<Env>) => {
kv?: KVNamespace;
dobj?: DurableObjectNamespace;
db?: D1Database;
};
static?: "kv" | "assets";
key?: string;
keepAliveSeconds?: number;
forceHttps?: boolean;
@@ -21,28 +26,65 @@ export type CloudflareBkndConfig<Env = any> = Omit<FrameworkBkndConfig, "app"> &
html?: string;
};
export type Context = {
export type Context<Env = any> = {
request: Request;
env: any;
env: Env;
ctx: ExecutionContext;
};
export function serve(config: CloudflareBkndConfig) {
return {
async fetch(request: Request, env: any, ctx: ExecutionContext) {
const url = new URL(request.url);
const manifest = config.manifest;
let media_registered: boolean = false;
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
if (!media_registered) {
registerMedia(context.env as any);
media_registered = true;
}
if (manifest) {
const appConfig = makeConfig(config, context);
const bindings = config.bindings?.(context);
if (!appConfig.connection) {
let db: D1Database | undefined;
if (bindings?.db) {
console.log("Using database from bindings");
db = bindings.db;
} else if (Object.keys(context.env ?? {}).length > 0) {
const binding = getBinding(context.env, "D1Database");
if (binding) {
console.log(`Using database from env "${binding.key}"`);
db = binding.value;
}
}
if (db) {
appConfig.connection = new D1Connection({ binding: db });
} else {
throw new Error("No database connection given");
}
}
return appConfig;
}
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
return {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (config.manifest && config.static === "assets") {
console.warn("manifest is not useful with static 'assets'");
} else if (!config.manifest && config.static === "kv") {
throw new Error("manifest is required with static 'kv'");
}
if (config.manifest && config.static !== "assets") {
const pathname = url.pathname.slice(1);
const assetManifest = JSON.parse(manifest);
const assetManifest = JSON.parse(config.manifest);
if (pathname && pathname in assetManifest) {
const hono = new Hono();
hono.all("*", async (c, next) => {
const res = await serveStatic({
path: `./${pathname}`,
manifest
manifest: config.manifest!,
})(c as any, next);
if (res instanceof Response) {
const ttl = 60 * 60 * 24 * 365;
@@ -57,8 +99,6 @@ export function serve(config: CloudflareBkndConfig) {
}
}
config.setAdminHtml = config.setAdminHtml && !!config.manifest;
const context = { request, env, ctx } as Context;
const mode = config.mode ?? "warm";
@@ -74,6 +114,6 @@ export function serve(config: CloudflareBkndConfig) {
default:
throw new Error(`Unknown mode ${mode}`);
}
}
},
};
}
+14
View File
@@ -1,4 +1,18 @@
import { D1Connection, type D1ConnectionConfig } from "./D1Connection";
export * from "./cloudflare-workers.adapter";
export { makeApp, getFresh, getWarm } from "./modes/fresh";
export { getCached } from "./modes/cached";
export { DurableBkndApp, getDurable } from "./modes/durable";
export { D1Connection, type D1ConnectionConfig };
export {
getBinding,
getBindings,
type BindingTypeMap,
type GetBindingType,
type BindingMap,
} from "./bindings";
export function d1(config: D1ConnectionConfig) {
return new D1Connection(config);
}
+7 -7
View File
@@ -1,8 +1,8 @@
import { createRuntimeApp } from "adapter";
import { App } from "bknd";
import type { CloudflareBkndConfig, Context } from "../index";
import { createRuntimeApp } from "bknd/adapter";
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Context) {
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
const { kv } = config.bindings?.(env)!;
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
const key = config.key ?? "app";
@@ -16,7 +16,7 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
const app = await createRuntimeApp(
{
...config,
...makeCfConfig(config, { env, ctx, ...args }),
initialConfig,
onBuilt: async (app) => {
app.module.server.client.get("/__bknd/cache", async (c) => {
@@ -31,13 +31,13 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx }: Cont
async ({ params: { app } }) => {
saveConfig(app.toJSON(true));
},
"sync"
"sync",
);
await config.beforeBuild?.(app);
},
adminOptions: { html: config.html }
adminOptions: { html: config.html },
},
env
{ env, ctx, ...args },
);
if (!cachedConfig) {
+9 -9
View File
@@ -1,7 +1,7 @@
import { DurableObject } from "cloudflare:workers";
import { createRuntimeApp } from "adapter";
import type { CloudflareBkndConfig, Context } from "adapter/cloudflare";
import type { App, CreateAppConfig } from "bknd";
import { createRuntimeApp, makeConfig } from "bknd/adapter";
import type { CloudflareBkndConfig, Context } from "../index";
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
const { dobj } = config.bindings?.(ctx.env)!;
@@ -17,13 +17,13 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
const id = dobj.idFromName(key);
const stub = dobj.get(id) as unknown as DurableBkndApp;
const create_config = typeof config.app === "function" ? config.app(ctx.env) : config.app;
const create_config = makeConfig(config, ctx);
const res = await stub.fire(ctx.request, {
config: create_config,
html: config.html,
keepAliveSeconds: config.keepAliveSeconds,
setAdminHtml: config.setAdminHtml
setAdminHtml: config.setAdminHtml,
});
const headers = new Headers(res.headers);
@@ -32,7 +32,7 @@ export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers
headers,
});
}
@@ -48,7 +48,7 @@ export class DurableBkndApp extends DurableObject {
html?: string;
keepAliveSeconds?: number;
setAdminHtml?: boolean;
}
},
) {
let buildtime = 0;
if (!this.app) {
@@ -73,7 +73,7 @@ export class DurableBkndApp extends DurableObject {
return c.json({
id: this.id,
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
colo: context.colo
colo: context.colo,
});
});
@@ -82,7 +82,7 @@ export class DurableBkndApp extends DurableObject {
adminOptions: { html: options.html },
beforeBuild: async (app) => {
await this.beforeBuild(app);
}
},
});
buildtime = performance.now() - start;
@@ -101,7 +101,7 @@ export class DurableBkndApp extends DurableObject {
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers
headers,
});
}
+6 -6
View File
@@ -1,14 +1,14 @@
import { createRuntimeApp } from "adapter";
import type { App } from "bknd";
import type { CloudflareBkndConfig, Context } from "../index";
import { createRuntimeApp } from "bknd/adapter";
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
export async function makeApp(config: CloudflareBkndConfig, { env }: Context) {
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
return await createRuntimeApp(
{
...config,
adminOptions: config.html ? { html: config.html } : undefined
...makeCfConfig(config, ctx),
adminOptions: config.html ? { html: config.html } : undefined,
},
env
ctx,
);
}
+19 -60
View File
@@ -1,59 +1,31 @@
import type { IncomingMessage } from "node:http";
import { App, type CreateAppConfig, registries } from "bknd";
import { config as $config } from "core";
import { App, type CreateAppConfig } from "bknd";
import { config as $config } from "bknd/core";
import type { MiddlewareHandler } from "hono";
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
import type { AdminControllerOptions } from "modules/server/AdminController";
export type BkndConfig<Env = any> = CreateAppConfig & {
app?: CreateAppConfig | ((env: Env) => CreateAppConfig);
export type BkndConfig<Args = any> = CreateAppConfig & {
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
onBuilt?: (app: App) => Promise<void>;
beforeBuild?: (app: App) => Promise<void>;
buildConfig?: Parameters<App["build"]>[0];
};
export type FrameworkBkndConfig<Env = any> = BkndConfig<Env>;
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
export type RuntimeBkndConfig<Env = any> = BkndConfig<Env> & {
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
distPath?: string;
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
};
export function nodeRequestToRequest(req: IncomingMessage): Request {
let protocol = "http";
try {
protocol = req.headers["x-forwarded-proto"] as string;
} catch (e) {}
const host = req.headers.host;
const url = `${protocol}://${host}${req.url}`;
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (Array.isArray(value)) {
headers.append(key, value.join(", "));
} else if (value) {
headers.append(key, value);
}
}
const method = req.method || "GET";
return new Request(url, {
method,
headers
});
}
export function registerLocalMediaAdapter() {
registries.media.register("local", StorageLocalAdapter);
}
export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): CreateAppConfig {
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
let additionalConfig: CreateAppConfig = {};
if ("app" in config && config.app) {
if (typeof config.app === "function") {
if (!env) {
throw new Error("env is required when config.app is a function");
if (!args) {
throw new Error("args is required when config.app is a function");
}
additionalConfig = config.app(env);
additionalConfig = config.app(args);
} else {
additionalConfig = config.app;
}
@@ -62,11 +34,11 @@ export function makeConfig<Env = any>(config: BkndConfig<Env>, env?: Env): Creat
return { ...config, ...additionalConfig };
}
export async function createFrameworkApp<Env = any>(
export async function createFrameworkApp<Args = any>(
config: FrameworkBkndConfig,
env?: Env
args?: Args,
): Promise<App> {
const app = App.create(makeConfig(config, env));
const app = App.create(makeConfig(config, args));
if (config.onBuilt) {
app.emgr.onEvent(
@@ -74,7 +46,7 @@ export async function createFrameworkApp<Env = any>(
async () => {
await config.onBuilt?.(app);
},
"sync"
"sync",
);
}
@@ -85,22 +57,9 @@ export async function createFrameworkApp<Env = any>(
}
export async function createRuntimeApp<Env = any>(
{
serveStatic,
registerLocalMedia,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
registerLocalMedia?: boolean;
adminOptions?: AdminControllerOptions | false;
},
env?: Env
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
env?: Env,
): Promise<App> {
if (registerLocalMedia) {
registerLocalMediaAdapter();
}
const app = App.create(makeConfig(config, env));
app.emgr.onEvent(
@@ -118,7 +77,7 @@ export async function createRuntimeApp<Env = any>(
app.registerAdminController(adminOptions);
}
},
"sync"
"sync",
);
await config.beforeBuild?.(app);

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