├── .github └── workflows │ └── test.yml ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── app ├── .env.example ├── .gitignore ├── .ncurc.json ├── __test__ │ ├── App.spec.ts │ ├── _assets │ │ ├── .gitignore │ │ ├── image.jpg │ │ └── image.png │ ├── adapter │ │ └── adapter.test.ts │ ├── api │ │ ├── Api.spec.ts │ │ ├── DataApi.spec.ts │ │ ├── MediaApi.spec.ts │ │ └── ModuleApi.spec.ts │ ├── app │ │ ├── App.spec.ts │ │ └── repro.spec.ts │ ├── auth │ │ ├── Authenticator.spec.ts │ │ ├── authorize │ │ │ └── authorize.spec.ts │ │ ├── middleware.spec.ts │ │ └── strategies │ │ │ └── OAuthStrategy.spec.ts │ ├── core │ │ ├── EventManager.spec.ts │ │ ├── Registry.spec.ts │ │ ├── benchmarks │ │ │ └── crypto.bm.ts │ │ ├── crypto.spec.ts │ │ ├── env.spec.ts │ │ ├── helper.ts │ │ ├── object │ │ │ ├── SchemaObject.spec.ts │ │ │ ├── diff.test.ts │ │ │ └── object-query.spec.ts │ │ └── utils.spec.ts │ ├── data │ │ ├── DataController.spec.ts │ │ ├── data.test.ts │ │ ├── helper.ts │ │ ├── mutation.relation.test.ts │ │ ├── mutation.simple.test.ts │ │ ├── polymorphic.test.ts │ │ ├── prototype.test.ts │ │ ├── relations.test.ts │ │ └── specs │ │ │ ├── Entity.spec.ts │ │ │ ├── EntityManager.spec.ts │ │ │ ├── JoinBuilder.spec.ts │ │ │ ├── Mutator.spec.ts │ │ │ ├── Repository.spec.ts │ │ │ ├── SchemaManager.spec.ts │ │ │ ├── WhereBuilder.spec.ts │ │ │ ├── WithBuilder.spec.ts │ │ │ ├── connection │ │ │ ├── Connection.spec.ts │ │ │ └── SqliteIntrospector.spec.ts │ │ │ ├── fields │ │ │ ├── BooleanField.spec.ts │ │ │ ├── DateField.spec.ts │ │ │ ├── EnumField.spec.ts │ │ │ ├── Field.spec.ts │ │ │ ├── FieldIndex.spec.ts │ │ │ ├── JsonField.spec.ts │ │ │ ├── JsonSchemaField.spec.ts │ │ │ ├── NumberField.spec.ts │ │ │ ├── PrimaryField.spec.ts │ │ │ └── TextField.spec.ts │ │ │ └── relations │ │ │ └── EntityRelation.spec.ts │ ├── flows │ │ ├── FetchTask.spec.ts │ │ ├── SubWorkflowTask.spec.ts │ │ ├── Task.spec.ts │ │ ├── inc │ │ │ ├── back.ts │ │ │ ├── fanout-condition.ts │ │ │ ├── helper.tsx │ │ │ ├── parallel.ts │ │ │ └── simple-fetch.ts │ │ ├── inputs.test.ts │ │ ├── render.tsx │ │ ├── trigger.test.ts │ │ └── workflow-basic.test.ts │ ├── helper.ts │ ├── integration │ │ ├── auth.integration.test.ts │ │ └── config.integration.test.ts │ ├── media │ │ ├── MediaController.spec.ts │ │ ├── Storage.spec.ts │ │ ├── adapters │ │ │ └── icon.png │ │ └── mime-types.spec.ts │ ├── modules │ │ ├── AppAuth.spec.ts │ │ ├── AppData.spec.ts │ │ ├── AppMedia.spec.ts │ │ ├── Module.spec.ts │ │ ├── ModuleManager.spec.ts │ │ ├── migrations │ │ │ ├── migrations.spec.ts │ │ │ └── samples │ │ │ │ ├── v7.json │ │ │ │ ├── v8-2.json │ │ │ │ └── v8.json │ │ └── module-test-suite.ts │ ├── ui │ │ └── json-form.spec.ts │ └── vitest │ │ ├── base.vi-test.ts │ │ └── setup.ts ├── build.cli.ts ├── build.ts ├── bunfig.toml ├── e2e │ ├── adapters.ts │ ├── assets │ │ └── image.jpg │ ├── base.e2e-spec.ts │ ├── inc │ │ └── adapters.ts │ └── media.e2e-spec.ts ├── env.d.ts ├── internal │ └── esbuild.entry-output-meta.plugin.ts ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── src │ ├── Api.ts │ ├── App.ts │ ├── adapter │ │ ├── adapter-test-suite.ts │ │ ├── astro │ │ │ ├── astro.adapter.spec.ts │ │ │ ├── astro.adapter.ts │ │ │ └── index.ts │ │ ├── aws │ │ │ ├── aws-lambda.adapter.ts │ │ │ ├── aws.adapter.spec.ts │ │ │ └── index.ts │ │ ├── bun │ │ │ ├── bun.adapter.spec.ts │ │ │ ├── bun.adapter.ts │ │ │ ├── index.ts │ │ │ └── test.ts │ │ ├── cloudflare │ │ │ ├── D1Connection.ts │ │ │ ├── bindings.ts │ │ │ ├── cloudflare-workers.adapter.spec.ts │ │ │ ├── cloudflare-workers.adapter.ts │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── modes │ │ │ │ ├── cached.ts │ │ │ │ ├── durable.ts │ │ │ │ └── fresh.ts │ │ │ └── storage │ │ │ │ ├── StorageR2Adapter.native-spec.ts │ │ │ │ └── StorageR2Adapter.ts │ │ ├── index.ts │ │ ├── nextjs │ │ │ ├── index.ts │ │ │ ├── nextjs.adapter.spec.ts │ │ │ └── nextjs.adapter.ts │ │ ├── node │ │ │ ├── index.ts │ │ │ ├── node.adapter.native-spec.ts │ │ │ ├── node.adapter.spec.ts │ │ │ ├── node.adapter.ts │ │ │ ├── storage │ │ │ │ ├── StorageLocalAdapter.native-spec.ts │ │ │ │ ├── StorageLocalAdapter.spec.ts │ │ │ │ └── StorageLocalAdapter.ts │ │ │ └── test.ts │ │ ├── react-router │ │ │ ├── index.ts │ │ │ ├── react-router.adapter.spec.ts │ │ │ └── react-router.adapter.ts │ │ ├── utils.ts │ │ └── vite │ │ │ ├── dev-server-config.ts │ │ │ ├── index.ts │ │ │ └── vite.adapter.ts │ ├── auth │ │ ├── AppAuth.ts │ │ ├── AppUserPool.ts │ │ ├── api │ │ │ ├── AuthApi.ts │ │ │ └── AuthController.ts │ │ ├── auth-permissions.ts │ │ ├── auth-schema.ts │ │ ├── authenticate │ │ │ ├── Authenticator.ts │ │ │ └── strategies │ │ │ │ ├── PasswordStrategy.ts │ │ │ │ ├── Strategy.ts │ │ │ │ ├── index.ts │ │ │ │ └── oauth │ │ │ │ ├── CustomOAuthStrategy.ts │ │ │ │ ├── OAuthStrategy.ts │ │ │ │ └── issuers │ │ │ │ ├── github.ts │ │ │ │ ├── google.ts │ │ │ │ └── index.ts │ │ ├── authorize │ │ │ ├── Guard.ts │ │ │ └── Role.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ └── middlewares.ts │ ├── cli │ │ ├── commands │ │ │ ├── config.ts │ │ │ ├── copy-assets.ts │ │ │ ├── create │ │ │ │ ├── create.ts │ │ │ │ ├── index.ts │ │ │ │ ├── npm.ts │ │ │ │ └── templates │ │ │ │ │ ├── aws.ts │ │ │ │ │ ├── cloudflare.ts │ │ │ │ │ └── index.ts │ │ │ ├── debug.ts │ │ │ ├── index.ts │ │ │ ├── run │ │ │ │ ├── index.ts │ │ │ │ ├── platform.ts │ │ │ │ └── run.ts │ │ │ ├── schema.ts │ │ │ ├── types │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── user.ts │ │ ├── index.ts │ │ ├── types.d.ts │ │ └── utils │ │ │ ├── cli.ts │ │ │ ├── sys.ts │ │ │ └── telemetry.ts │ ├── core │ │ ├── clients │ │ │ └── aws │ │ │ │ └── AwsClient.ts │ │ ├── config.ts │ │ ├── console.ts │ │ ├── env.ts │ │ ├── errors.ts │ │ ├── events │ │ │ ├── Event.ts │ │ │ ├── EventListener.ts │ │ │ ├── EventManager.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── object │ │ │ ├── SchemaObject.ts │ │ │ ├── diff.ts │ │ │ ├── query │ │ │ │ ├── object-query.ts │ │ │ │ └── query.ts │ │ │ └── schema │ │ │ │ ├── index.ts │ │ │ │ └── validator.ts │ │ ├── registry │ │ │ └── Registry.ts │ │ ├── security │ │ │ └── Permission.ts │ │ ├── server │ │ │ ├── ContextHelper.ts │ │ │ ├── flash.ts │ │ │ └── lib │ │ │ │ ├── index.ts │ │ │ │ └── tbValidator.ts │ │ ├── template │ │ │ ├── SimpleRenderer.spec.ts │ │ │ └── SimpleRenderer.ts │ │ ├── test │ │ │ └── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── DebugLogger.ts │ │ │ ├── browser.ts │ │ │ ├── crypto.ts │ │ │ ├── dates.ts │ │ │ ├── file.ts │ │ │ ├── index.ts │ │ │ ├── numbers.ts │ │ │ ├── objects.ts │ │ │ ├── perf.ts │ │ │ ├── reqres.ts │ │ │ ├── runtime.ts │ │ │ ├── sql.ts │ │ │ ├── strings.ts │ │ │ ├── test.ts │ │ │ ├── typebox │ │ │ ├── from-schema.ts │ │ │ └── index.ts │ │ │ ├── types.d.ts │ │ │ ├── uuid.ts │ │ │ └── xml.ts │ ├── data │ │ ├── AppData.ts │ │ ├── api │ │ │ ├── DataApi.ts │ │ │ └── DataController.ts │ │ ├── connection │ │ │ ├── BaseIntrospector.ts │ │ │ ├── Connection.ts │ │ │ ├── DummyConnection.ts │ │ │ ├── index.ts │ │ │ └── sqlite │ │ │ │ ├── LibsqlConnection.ts │ │ │ │ ├── SqliteConnection.ts │ │ │ │ ├── SqliteIntrospector.ts │ │ │ │ └── SqliteLocalConnection.ts │ │ ├── data-schema.ts │ │ ├── entities │ │ │ ├── Entity.ts │ │ │ ├── EntityManager.ts │ │ │ ├── EntityTypescript.ts │ │ │ ├── Mutator.ts │ │ │ ├── index.ts │ │ │ └── query │ │ │ │ ├── JoinBuilder.ts │ │ │ │ ├── Repository.ts │ │ │ │ ├── WhereBuilder.ts │ │ │ │ └── WithBuilder.ts │ │ ├── errors.ts │ │ ├── events │ │ │ └── index.ts │ │ ├── fields │ │ │ ├── BooleanField.ts │ │ │ ├── DateField.ts │ │ │ ├── EnumField.ts │ │ │ ├── Field.ts │ │ │ ├── JsonField.ts │ │ │ ├── JsonSchemaField.ts │ │ │ ├── NumberField.ts │ │ │ ├── PrimaryField.ts │ │ │ ├── TextField.ts │ │ │ ├── VirtualField.ts │ │ │ ├── field-test-suite.ts │ │ │ ├── index.ts │ │ │ └── indices │ │ │ │ └── EntityIndex.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── permissions │ │ │ └── index.ts │ │ ├── plugins │ │ │ ├── DeserializeJsonValuesPlugin.ts │ │ │ ├── FilterNumericKeysPlugin.ts │ │ │ └── KyselyPluginRunner.ts │ │ ├── prototype │ │ │ └── index.ts │ │ ├── relations │ │ │ ├── EntityRelation.ts │ │ │ ├── EntityRelationAnchor.ts │ │ │ ├── ManyToManyRelation.ts │ │ │ ├── ManyToOneRelation.ts │ │ │ ├── OneToOneRelation.ts │ │ │ ├── PolymorphicRelation.ts │ │ │ ├── RelationAccessor.ts │ │ │ ├── RelationField.ts │ │ │ ├── RelationHelper.ts │ │ │ ├── RelationMutator.ts │ │ │ ├── index.ts │ │ │ └── relation-types.ts │ │ ├── schema │ │ │ ├── SchemaManager.ts │ │ │ └── constructor.ts │ │ └── server │ │ │ ├── query.spec.ts │ │ │ └── query.ts │ ├── flows │ │ ├── AppFlows.ts │ │ ├── examples │ │ │ └── simple-fetch.ts │ │ ├── flows-schema.ts │ │ ├── flows │ │ │ ├── Execution.ts │ │ │ ├── Flow.ts │ │ │ ├── FlowTaskConnector.ts │ │ │ ├── executors │ │ │ │ └── RuntimeExecutor.ts │ │ │ └── triggers │ │ │ │ ├── EventTrigger.ts │ │ │ │ ├── HttpTrigger.ts │ │ │ │ ├── Trigger.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── tasks │ │ │ ├── Task.tsx │ │ │ ├── TaskConnection.ts │ │ │ └── presets │ │ │ ├── FetchTask.ts │ │ │ ├── LogTask.ts │ │ │ ├── RenderTask.ts │ │ │ └── SubFlowTask.ts │ ├── index.ts │ ├── media │ │ ├── AppMedia.ts │ │ ├── MediaField.ts │ │ ├── api │ │ │ ├── MediaApi.ts │ │ │ └── MediaController.ts │ │ ├── index.ts │ │ ├── media-permissions.ts │ │ ├── media-schema.ts │ │ ├── storage │ │ │ ├── Storage.ts │ │ │ ├── StorageAdapter.ts │ │ │ ├── adapters │ │ │ │ ├── adapter-test-suite.ts │ │ │ │ ├── cloudinary │ │ │ │ │ ├── StorageCloudinaryAdapter.spec.ts │ │ │ │ │ └── StorageCloudinaryAdapter.ts │ │ │ │ └── s3 │ │ │ │ │ ├── StorageS3Adapter.spec.ts │ │ │ │ │ └── StorageS3Adapter.ts │ │ │ ├── events │ │ │ │ └── index.ts │ │ │ ├── mime-types-tiny.ts │ │ │ └── mime-types.ts │ │ └── utils │ │ │ └── index.ts │ ├── modules │ │ ├── Controller.ts │ │ ├── Module.ts │ │ ├── ModuleApi.ts │ │ ├── ModuleManager.ts │ │ ├── SystemApi.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ └── index.ts │ │ ├── migrations.ts │ │ ├── permissions │ │ │ └── index.ts │ │ ├── registries.ts │ │ └── server │ │ │ ├── AdminController.tsx │ │ │ ├── AppServer.ts │ │ │ ├── SystemController.ts │ │ │ └── openapi.ts │ ├── plugins │ │ └── cloudflare │ │ │ └── image-optimization-plugin.ts │ └── ui │ │ ├── Admin.tsx │ │ ├── assets │ │ └── favicon.ico │ │ ├── client │ │ ├── BkndProvider.tsx │ │ ├── ClientProvider.tsx │ │ ├── api │ │ │ ├── use-api.ts │ │ │ └── use-entity.ts │ │ ├── bknd.ts │ │ ├── index.ts │ │ ├── schema │ │ │ ├── actions.ts │ │ │ ├── auth │ │ │ │ ├── use-auth.ts │ │ │ │ └── use-bknd-auth.ts │ │ │ ├── data │ │ │ │ └── use-bknd-data.ts │ │ │ ├── flows │ │ │ │ └── use-flows.ts │ │ │ ├── media │ │ │ │ └── use-bknd-media.ts │ │ │ └── system │ │ │ │ └── use-bknd-system.ts │ │ ├── use-theme.ts │ │ └── utils │ │ │ └── AppReduced.ts │ │ ├── components │ │ ├── Context.tsx │ │ ├── buttons │ │ │ ├── Button.tsx │ │ │ └── IconButton.tsx │ │ ├── canvas │ │ │ ├── Canvas.tsx │ │ │ ├── components │ │ │ │ └── nodes │ │ │ │ │ └── DefaultNode.tsx │ │ │ ├── layouts │ │ │ │ └── index.ts │ │ │ └── panels │ │ │ │ ├── Panel.tsx │ │ │ │ └── index.tsx │ │ ├── code │ │ │ ├── CodeEditor.tsx │ │ │ ├── HtmlEditor.tsx │ │ │ ├── JsonEditor.tsx │ │ │ └── JsonViewer.tsx │ │ ├── display │ │ │ ├── Alert.tsx │ │ │ ├── Empty.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── Icon.tsx │ │ │ ├── Logo.tsx │ │ │ └── Message.tsx │ │ ├── form │ │ │ ├── FloatingSelect │ │ │ │ └── FloatingSelect.tsx │ │ │ ├── Formy │ │ │ │ ├── BooleanInputMantine.tsx │ │ │ │ ├── components.tsx │ │ │ │ └── index.ts │ │ │ ├── SearchInput.tsx │ │ │ ├── SegmentedControl.tsx │ │ │ ├── hook-form-mantine │ │ │ │ ├── MantineNumberInput.tsx │ │ │ │ ├── MantineRadio.tsx │ │ │ │ ├── MantineSegmentedControl.tsx │ │ │ │ ├── MantineSelect.tsx │ │ │ │ └── MantineSwitch.tsx │ │ │ ├── json-schema-form │ │ │ │ ├── AnyOfField.tsx │ │ │ │ ├── ArrayField.tsx │ │ │ │ ├── Field.tsx │ │ │ │ ├── FieldWrapper.tsx │ │ │ │ ├── Form.tsx │ │ │ │ ├── ObjectField.tsx │ │ │ │ ├── index.ts │ │ │ │ └── utils.ts │ │ │ ├── json-schema │ │ │ │ ├── JsonSchemaForm.tsx │ │ │ │ ├── JsonSchemaValidator.ts │ │ │ │ ├── fields │ │ │ │ │ ├── HtmlField.tsx │ │ │ │ │ ├── JsonField.tsx │ │ │ │ │ ├── MultiSchemaField.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.css │ │ │ │ ├── templates │ │ │ │ │ ├── ArrayFieldItemTemplate.tsx │ │ │ │ │ ├── ArrayFieldTemplate.tsx │ │ │ │ │ ├── BaseInputTemplate.tsx │ │ │ │ │ ├── ButtonTemplates.tsx │ │ │ │ │ ├── FieldTemplate.tsx │ │ │ │ │ ├── ObjectFieldTemplate.tsx │ │ │ │ │ ├── TitleFieldTemplate.tsx │ │ │ │ │ ├── WrapIfAdditionalTemplate.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── typebox │ │ │ │ │ ├── RJSFTypeboxValidator.ts │ │ │ │ │ └── from-schema.ts │ │ │ │ └── widgets │ │ │ │ │ ├── CheckboxWidget.tsx │ │ │ │ │ ├── CheckboxesWidget.tsx │ │ │ │ │ ├── JsonWidget.tsx │ │ │ │ │ ├── RadioWidget.tsx │ │ │ │ │ ├── SelectWidget.tsx │ │ │ │ │ └── index.tsx │ │ │ └── native-form │ │ │ │ ├── NativeForm.tsx │ │ │ │ └── utils.ts │ │ ├── list │ │ │ ├── CollapsibleList.tsx │ │ │ └── SortableList.tsx │ │ ├── menu │ │ │ └── Dropdown.tsx │ │ ├── modal │ │ │ ├── Modal.tsx │ │ │ └── Modal2.tsx │ │ ├── overlay │ │ │ ├── Dropdown.tsx │ │ │ ├── Modal.tsx │ │ │ └── Popover.tsx │ │ ├── steps │ │ │ └── Steps.tsx │ │ ├── table │ │ │ └── DataTable.tsx │ │ └── wouter │ │ │ └── Link.tsx │ │ ├── container │ │ └── use-flows.ts │ │ ├── elements │ │ ├── auth │ │ │ ├── AuthForm.tsx │ │ │ ├── AuthScreen.tsx │ │ │ ├── SocialLink.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ └── use-auth.ts │ │ ├── index.ts │ │ ├── media │ │ │ ├── Dropzone.tsx │ │ │ ├── DropzoneContainer.tsx │ │ │ ├── DropzoneInner.tsx │ │ │ ├── dropzone-state.ts │ │ │ ├── file-selector.ts │ │ │ ├── helper.spec.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ └── use-dropzone.ts │ │ └── mocks │ │ │ └── tailwind-merge.ts │ │ ├── hooks │ │ ├── use-browser-title.ts │ │ ├── use-effect.ts │ │ ├── use-event.ts │ │ ├── use-render-count.ts │ │ ├── use-route-path-state.tsx │ │ └── use-search.ts │ │ ├── index.ts │ │ ├── inject.js │ │ ├── layouts │ │ └── AppShell │ │ │ ├── AppShell.tsx │ │ │ ├── Breadcrumbs.tsx │ │ │ ├── Breadcrumbs2.tsx │ │ │ ├── Header.tsx │ │ │ ├── index.ts │ │ │ └── use-appshell.tsx │ │ ├── lib │ │ ├── config.ts │ │ ├── mantine │ │ │ └── theme.ts │ │ ├── routes.ts │ │ └── utils.ts │ │ ├── main.css │ │ ├── main.tsx │ │ ├── modals │ │ ├── debug │ │ │ ├── DebugModal.tsx │ │ │ ├── OverlayModal.tsx │ │ │ ├── SchemaFormModal.tsx │ │ │ └── TestModal.tsx │ │ ├── index.tsx │ │ ├── media │ │ │ └── MediaInfoModal.tsx │ │ └── transitions.ts │ │ ├── modules │ │ ├── auth │ │ │ └── hooks │ │ │ │ └── use-create-user-modal.ts │ │ ├── data │ │ │ ├── components │ │ │ │ ├── EntityForm.tsx │ │ │ │ ├── EntityTable.tsx │ │ │ │ ├── EntityTable2.tsx │ │ │ │ ├── canvas │ │ │ │ │ ├── DataSchemaCanvas.tsx │ │ │ │ │ └── EntityTableNode.tsx │ │ │ │ ├── fields-specs.ts │ │ │ │ ├── fields │ │ │ │ │ ├── EntityJsonSchemaFormField.tsx │ │ │ │ │ └── EntityRelationalFormField.tsx │ │ │ │ └── schema │ │ │ │ │ └── create-modal │ │ │ │ │ ├── CreateModal.tsx │ │ │ │ │ ├── step.create.tsx │ │ │ │ │ ├── step.entity.fields.tsx │ │ │ │ │ ├── step.entity.tsx │ │ │ │ │ ├── step.relation.tsx │ │ │ │ │ ├── step.select.tsx │ │ │ │ │ └── templates │ │ │ │ │ ├── media │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── template.media.component.tsx │ │ │ │ │ └── template.media.meta.ts │ │ │ │ │ └── register.ts │ │ │ └── hooks │ │ │ │ └── useEntityForm.tsx │ │ ├── flows │ │ │ ├── components │ │ │ │ ├── FlowCanvas.tsx │ │ │ │ ├── TriggerComponent.tsx │ │ │ │ ├── form │ │ │ │ │ ├── TaskForm.tsx │ │ │ │ │ └── TemplateField.tsx │ │ │ │ └── tasks │ │ │ │ │ ├── FetchTaskComponent.tsx │ │ │ │ │ ├── RenderTaskComponent.tsx │ │ │ │ │ ├── TaskComponent.tsx │ │ │ │ │ └── index.ts │ │ │ ├── components2 │ │ │ │ ├── form │ │ │ │ │ └── KeyValueInput.tsx │ │ │ │ ├── hud │ │ │ │ │ └── FlowPanel.tsx │ │ │ │ └── nodes │ │ │ │ │ ├── BaseNode.tsx │ │ │ │ │ ├── Handle.tsx │ │ │ │ │ ├── SelectNode.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tasks │ │ │ │ │ ├── FetchTaskNode.tsx │ │ │ │ │ ├── RenderNode.tsx │ │ │ │ │ └── TaskNode.tsx │ │ │ │ │ └── triggers │ │ │ │ │ └── TriggerNode.tsx │ │ │ ├── hooks │ │ │ │ └── use-flow │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── state.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ └── server │ │ │ └── FlashMessage.tsx │ │ ├── routes │ │ ├── auth │ │ │ ├── _auth.root.tsx │ │ │ ├── auth.index.tsx │ │ │ ├── auth.login.tsx │ │ │ ├── auth.register.tsx │ │ │ ├── auth.roles.edit.$role.tsx │ │ │ ├── auth.roles.tsx │ │ │ ├── auth.settings.tsx │ │ │ ├── auth.strategies.tsx │ │ │ ├── auth.users.tsx │ │ │ ├── forms │ │ │ │ └── role.form.tsx │ │ │ └── index.tsx │ │ ├── data │ │ │ ├── _data.root.tsx │ │ │ ├── data.$entity.$id.tsx │ │ │ ├── data.$entity.create.tsx │ │ │ ├── data.$entity.index.tsx │ │ │ ├── data.schema.$entity.tsx │ │ │ ├── data.schema.index.tsx │ │ │ ├── forms │ │ │ │ └── entity.fields.form.tsx │ │ │ └── index.tsx │ │ ├── flows │ │ │ ├── _flows.root.tsx │ │ │ ├── components │ │ │ │ └── FlowCreateModal.tsx │ │ │ ├── flows.edit.$name.tsx │ │ │ ├── flows.list.tsx │ │ │ └── index.tsx │ │ ├── flows_old │ │ │ ├── _flows.root.tsx │ │ │ ├── flow.$key.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── media │ │ │ ├── _media.root.tsx │ │ │ ├── index.tsx │ │ │ ├── media.index.tsx │ │ │ └── media.settings.tsx │ │ ├── root.tsx │ │ ├── settings │ │ │ ├── components │ │ │ │ ├── Setting.tsx │ │ │ │ ├── SettingNewModal.tsx │ │ │ │ └── SettingSchemaModal.tsx │ │ │ ├── index.tsx │ │ │ ├── routes │ │ │ │ ├── auth.settings.tsx │ │ │ │ ├── data.settings.tsx │ │ │ │ ├── flows.settings.tsx │ │ │ │ └── server.settings.tsx │ │ │ └── utils │ │ │ │ └── schema.ts │ │ └── test │ │ │ ├── index.tsx │ │ │ └── tests │ │ │ ├── appshell-accordions-test.tsx │ │ │ ├── dropdown-test.tsx │ │ │ ├── dropzone-element-test.tsx │ │ │ ├── flow-create-schema-test.tsx │ │ │ ├── flow-form-test.tsx │ │ │ ├── flows-test.tsx │ │ │ ├── formy-test.tsx │ │ │ ├── html-form-test.tsx │ │ │ ├── json-schema-form-react-test.tsx │ │ │ ├── json-schema-form3.tsx │ │ │ ├── jsonform-test │ │ │ └── index.tsx │ │ │ ├── liquid-js-test.tsx │ │ │ ├── mantine-test.tsx │ │ │ ├── modal-test.tsx │ │ │ ├── query-jsonform.tsx │ │ │ ├── react-hook-errors.tsx │ │ │ ├── reactflow-test.tsx │ │ │ ├── schema-test.tsx │ │ │ ├── sortable-test.tsx │ │ │ ├── sql-ai-test.tsx │ │ │ ├── swagger-test.tsx │ │ │ ├── swr-and-api.tsx │ │ │ ├── swr-and-data-api.tsx │ │ │ └── themes.tsx │ │ ├── store │ │ ├── appshell.ts │ │ ├── index.ts │ │ └── utils.ts │ │ └── styles.css ├── tailwind.config.js ├── tsconfig.build.json ├── tsconfig.json ├── vite.config.ts ├── vite.dev.ts └── vitest.config.ts ├── biome.json ├── bun.lock ├── docker ├── Dockerfile └── README.md ├── docs ├── _assets │ ├── Favicon.svg │ ├── favicon.ico │ ├── images │ │ ├── checks-passed.png │ │ ├── hero-dark.svg │ │ └── hero-light.svg │ ├── logo │ │ ├── bknd_logo_black.svg │ │ ├── bknd_logo_white.svg │ │ ├── dark.svg │ │ └── light.svg │ └── poster.png ├── api-reference │ ├── auth │ │ ├── login.mdx │ │ ├── me.mdx │ │ ├── register.mdx │ │ └── strategies.mdx │ ├── data │ │ ├── create.mdx │ │ ├── delete.mdx │ │ ├── get.mdx │ │ ├── list.mdx │ │ └── update.mdx │ ├── endpoint │ │ ├── create.mdx │ │ ├── delete.mdx │ │ └── get.mdx │ ├── introduction.mdx │ ├── openapi.json │ └── system │ │ ├── config.mdx │ │ ├── ping.mdx │ │ └── schema.mdx ├── concepts.mdx ├── guide │ └── introduction.mdx ├── integration │ ├── astro.mdx │ ├── aws.mdx │ ├── bun.mdx │ ├── cloudflare.mdx │ ├── docker.mdx │ ├── introduction.mdx │ ├── nextjs.mdx │ ├── node.mdx │ ├── react-router.mdx │ └── vite.mdx ├── mint.json ├── modules │ ├── auth.mdx │ ├── data.mdx │ ├── flows.mdx │ ├── media.mdx │ └── overview.mdx ├── motivation.mdx ├── package-lock.json ├── package.json ├── setup.mdx ├── snippets │ ├── install-bknd.mdx │ ├── integration-icons.mdx │ └── stackblitz.mdx ├── start.mdx └── usage │ ├── cli.mdx │ ├── database.mdx │ ├── elements.mdx │ ├── introduction.mdx │ ├── react.mdx │ └── sdk.mdx ├── examples ├── .gitignore ├── astro │ ├── .gitignore │ ├── README.md │ ├── astro.config.mjs │ ├── bknd.config.ts │ ├── package.json │ ├── public │ │ └── favicon.svg │ ├── src │ │ ├── bknd.ts │ │ ├── components │ │ │ └── Card.astro │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ └── pages │ │ │ ├── admin │ │ │ └── [...admin].astro │ │ │ ├── api │ │ │ └── [...api].ts │ │ │ ├── index.astro │ │ │ └── ssr.astro │ └── tsconfig.json ├── aws-lambda │ ├── clean.sh │ ├── deploy.sh │ ├── index.mjs │ ├── package.json │ ├── test.js │ └── trust-policy.json ├── bun │ ├── .gitignore │ ├── README.md │ ├── bun.lock │ ├── index.ts │ ├── package.json │ ├── static.ts │ └── tsconfig.json ├── cloudflare-worker │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── worker-configuration.d.ts │ └── wrangler.json ├── nextjs │ ├── .gitignore │ ├── README.md │ ├── bknd.config.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── bknd.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ ├── src │ │ ├── app │ │ │ ├── (main) │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── ssr │ │ │ │ │ └── page.tsx │ │ │ ├── admin │ │ │ │ └── [[...admin]] │ │ │ │ │ ├── Admin.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── [[...bknd]] │ │ │ │ │ └── route.ts │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ └── layout.tsx │ │ ├── bknd.ts │ │ └── components │ │ │ ├── Buttons.tsx │ │ │ ├── Footer.tsx │ │ │ └── List.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── node │ ├── .gitignore │ ├── README.md │ ├── index.js │ └── package.json ├── plasmic │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── admin.tsx │ │ ├── main.tsx │ │ └── server.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react-router │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── app.css │ │ ├── bknd.ts │ │ ├── root.tsx │ │ ├── routes.ts │ │ └── routes │ │ │ ├── _index.tsx │ │ │ ├── admin.$.tsx │ │ │ └── api.$.ts │ ├── bknd.config.ts │ ├── package.json │ ├── public │ │ ├── bknd.svg │ │ ├── favicon.ico │ │ ├── logo-dark.svg │ │ └── logo-light.svg │ ├── react-router.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ └── bknd.svg │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ └── Center.tsx │ │ ├── main.tsx │ │ ├── routes │ │ │ ├── _index.tsx │ │ │ └── admin.tsx │ │ ├── styles.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts └── sw │ ├── index.html │ ├── main.ts │ ├── package.json │ ├── sw.ts │ └── tsconfig.json ├── package.json ├── packages ├── cli │ └── package.json ├── plasmic │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Image.tsx │ │ │ ├── LazyRender.tsx │ │ │ ├── data │ │ │ │ └── BkndData.tsx │ │ │ └── index.ts │ │ ├── contexts │ │ │ ├── BkndContext.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── nextjs │ │ │ └── index.tsx │ └── tsconfig.json ├── postgres │ ├── README.md │ ├── package.json │ ├── src │ │ ├── PostgresConnection.ts │ │ ├── PostgresIntrospector.ts │ │ └── index.ts │ ├── test │ │ ├── base.test.ts │ │ ├── integration.test.ts │ │ └── setup.ts │ └── tsconfig.json └── sqlocal │ ├── README.md │ ├── package.json │ ├── src │ ├── SQLocalConnection.ts │ └── index.ts │ ├── test │ ├── base.test.ts │ ├── connection.test.ts │ └── integration.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── tsconfig.json └── verdaccio.yml /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ main ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Bun 16 | uses: oven-sh/setup-bun@v1 17 | with: 18 | bun-version: "1.2.14" 19 | 20 | - name: Install dependencies 21 | working-directory: ./app 22 | run: bun install 23 | 24 | - name: Build 25 | working-directory: ./app 26 | run: bun run build:ci 27 | 28 | - name: Run Bun tests 29 | working-directory: ./app 30 | run: bun run test:bun 31 | 32 | - name: Run Node tests 33 | working-directory: ./app 34 | run: npm run test:node -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.cache 4 | /.wrangler 5 | /build 6 | /public/build 7 | packages/media/.env 8 | *.sqlite 9 | /data.sqld/ 10 | /dist 11 | **/*/dist 12 | **/*/build 13 | **/*/.cache 14 | **/*/.env 15 | **/*/.dev.vars 16 | **/*/.wrangler 17 | **/*/*.tgz 18 | **/*/vite.config.ts.timestamp* 19 | .history 20 | **/*/.db/* 21 | **/*/.configs/* 22 | **/*/.template/* 23 | **/*/*.db 24 | **/*/*.db-shm 25 | **/*/*.db-wal 26 | **/*/.tmp 27 | .npmrc 28 | /.verdaccio 29 | .idea 30 | .vscode 31 | .git_old 32 | docker/tmp 33 | .debug 34 | .history -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /app/.env.example: -------------------------------------------------------------------------------- 1 | # ===== DB Settings ===== 2 | VITE_DB_URL=:memory: 3 | # you can set a location for a database here, it'll overwrite the previous setting 4 | # ideally use the ".db" folder (create it first), it's git ignored 5 | VITE_DB_URL=file:.db/dev.db 6 | 7 | # alternatively, you can use url/token combination 8 | #VITE_DB_URL= 9 | #VITE_DB_TOKEN= 10 | 11 | 12 | # ===== DEV Server ===== 13 | # restart the dev server on every change (enable with "1") 14 | VITE_APP_FRESH= 15 | # displays react-scan widget (enable with "1") 16 | VITE_DEBUG_RERENDERS= 17 | # console logs registered routes on start (enable with "1") 18 | VITE_SHOW_ROUTES= 19 | 20 | 21 | # ===== Test Credentials ===== 22 | RESEND_API_KEY= 23 | R2_TOKEN= 24 | 25 | R2_ACCESS_KEY= 26 | R2_SECRET_ACCESS_KEY= 27 | R2_URL= 28 | 29 | AWS_ACCESS_KEY= 30 | AWS_SECRET_KEY= 31 | AWS_S3_URL= 32 | 33 | OAUTH_CLIENT_ID= 34 | OAUTH_CLIENT_SECRET= 35 | 36 | PUBLIC_POSTHOG_KEY= 37 | PUBLIC_POSTHOG_HOST= 38 | 39 | # ===== Internals ===== 40 | BKND_CLI_CREATE_REF=main 41 | BKND_CLI_LOG_LEVEL=debug 42 | BKND_MODULES_DEBUG=1 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | playwright-report 2 | test-results 3 | bknd.config.* 4 | __test__/helper.d.ts -------------------------------------------------------------------------------- /app/.ncurc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reject": ["react-icons", "@tabler/icons-react", "@tanstack/react-form"] 3 | } 4 | -------------------------------------------------------------------------------- /app/__test__/App.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, afterEach, describe, expect, test } from "bun:test"; 2 | import { App } from "../src"; 3 | import { getDummyConnection } from "./helper"; 4 | 5 | const { dummyConnection, afterAllCleanup } = getDummyConnection(); 6 | afterEach(afterAllCleanup); 7 | 8 | describe("App tests", async () => { 9 | test("boots and pongs", async () => { 10 | const app = new App(dummyConnection); 11 | await app.build(); 12 | 13 | //expect(await app.data?.em.ping()).toBeTrue(); 14 | }); 15 | 16 | /*test.only("what", async () => { 17 | const app = new App(dummyConnection, { 18 | auth: { 19 | enabled: true, 20 | }, 21 | }); 22 | await app.module.auth.build(); 23 | await app.module.data.build(); 24 | console.log(app.em.entities.map((e) => e.name)); 25 | console.log(await app.em.schema().getDiff()); 26 | });*/ 27 | }); 28 | -------------------------------------------------------------------------------- /app/__test__/_assets/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/* -------------------------------------------------------------------------------- /app/__test__/_assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bknd-io/bknd/7b128c97012e598b2667a4212691d0f019becd14/app/__test__/_assets/image.jpg -------------------------------------------------------------------------------- /app/__test__/_assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bknd-io/bknd/7b128c97012e598b2667a4212691d0f019becd14/app/__test__/_assets/image.png -------------------------------------------------------------------------------- /app/__test__/auth/middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "bun:test"; 2 | -------------------------------------------------------------------------------- /app/__test__/core/benchmarks/crypto.bm.ts: -------------------------------------------------------------------------------- 1 | import { baseline, bench, group, run } from "mitata"; 2 | import * as crypt from "../../../src/core/utils/crypto"; 3 | 4 | // deno 5 | // import { ... } from 'npm:mitata'; 6 | 7 | // d8/jsc 8 | // import { ... } from '/src/cli.mjs'; 9 | 10 | const small = "hello"; 11 | const big = "hello".repeat(1000); 12 | 13 | group("hashing (small)", () => { 14 | baseline("baseline", () => JSON.parse(JSON.stringify({ small }))); 15 | bench("sha-1", async () => await crypt.hash.sha256(small)); 16 | bench("sha-256", async () => await crypt.hash.sha256(small)); 17 | }); 18 | 19 | group("hashing (big)", () => { 20 | baseline("baseline", () => JSON.parse(JSON.stringify({ big }))); 21 | bench("sha-1", async () => await crypt.hash.sha256(big)); 22 | bench("sha-256", async () => await crypt.hash.sha256(big)); 23 | }); 24 | 25 | /*group({ name: 'group2', summary: false }, () => { 26 | bench('new Array(0)', () => new Array(0)); 27 | bench('new Array(1024)', () => new Array(1024)); 28 | });*/ 29 | 30 | // @ts-ignore 31 | await run(); 32 | -------------------------------------------------------------------------------- /app/__test__/core/crypto.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { checksum, hash } from "../../src/core/utils"; 3 | 4 | describe("crypto", async () => { 5 | test("sha256", async () => { 6 | expect(await hash.sha256("test")).toBe( 7 | "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", 8 | ); 9 | }); 10 | test("sha1", async () => { 11 | expect(await hash.sha1("test")).toBe("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"); 12 | }); 13 | test("checksum", async () => { 14 | expect(await checksum("hello world")).toBe("2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/__test__/core/helper.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "bun:test"; 2 | 3 | let _oldFetch: typeof fetch; 4 | export function mockFetch(responseMethods: Partial) { 5 | _oldFetch = global.fetch; 6 | // @ts-ignore 7 | global.fetch = jest.fn(() => Promise.resolve(responseMethods)); 8 | } 9 | 10 | export function mockFetch2(newFetch: (input: RequestInfo, init: RequestInit) => Promise) { 11 | _oldFetch = global.fetch; 12 | // @ts-ignore 13 | global.fetch = jest.fn(newFetch); 14 | } 15 | 16 | export function unmockFetch() { 17 | global.fetch = _oldFetch; 18 | } 19 | -------------------------------------------------------------------------------- /app/__test__/data/helper.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import type { SqliteDatabase } from "kysely"; 3 | // @ts-ignore 4 | import Database from "libsql"; 5 | import { SqliteLocalConnection } from "../../src/data"; 6 | 7 | export function getDummyDatabase(memory: boolean = true): { 8 | dummyDb: SqliteDatabase; 9 | afterAllCleanup: () => Promise; 10 | } { 11 | const DB_NAME = memory ? ":memory:" : `${Math.random().toString(36).substring(7)}.db`; 12 | const dummyDb = new Database(DB_NAME); 13 | 14 | return { 15 | dummyDb, 16 | afterAllCleanup: async () => { 17 | if (!memory) await unlink(DB_NAME); 18 | return true; 19 | }, 20 | }; 21 | } 22 | 23 | export function getDummyConnection(memory: boolean = true) { 24 | const { dummyDb, afterAllCleanup } = getDummyDatabase(memory); 25 | const dummyConnection = new SqliteLocalConnection(dummyDb); 26 | 27 | return { 28 | dummyConnection, 29 | afterAllCleanup, 30 | }; 31 | } 32 | 33 | export function getLocalLibsqlConnection() { 34 | return { url: "http://127.0.0.1:8080" }; 35 | } 36 | -------------------------------------------------------------------------------- /app/__test__/data/specs/fields/DateField.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { DateField } from "../../../../src/data"; 3 | import { fieldTestSuite } from "data/fields/field-test-suite"; 4 | 5 | describe("[data] DateField", async () => { 6 | fieldTestSuite({ expect, test }, DateField, { defaultValue: new Date(), schemaType: "date" }); 7 | 8 | // @todo: add datefield tests 9 | test("week", async () => { 10 | const field = new DateField("test", { type: "week" }); 11 | console.log(field.getValue("2021-W01", "submit")); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /app/__test__/data/specs/fields/FieldIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { Type } from "@sinclair/typebox"; 3 | import { Entity, EntityIndex, Field } from "../../../../src/data"; 4 | 5 | class TestField extends Field { 6 | protected getSchema(): any { 7 | return Type.Any(); 8 | } 9 | 10 | override schema() { 11 | return undefined as any; 12 | } 13 | } 14 | 15 | describe("FieldIndex", async () => { 16 | const entity = new Entity("test", []); 17 | test("it constructs", async () => { 18 | const field = new TestField("name"); 19 | const index = new EntityIndex(entity, [field]); 20 | 21 | expect(index.fields).toEqual([field]); 22 | expect(index.name).toEqual("idx_test_name"); 23 | expect(index.unique).toEqual(false); 24 | }); 25 | 26 | test("it fails on non-unique", async () => { 27 | const field = new TestField("name", { required: false }); 28 | 29 | expect(() => new EntityIndex(entity, [field], true)).toThrowError(); 30 | expect(() => new EntityIndex(entity, [field])).toBeDefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/__test__/data/specs/fields/JsonSchemaField.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { JsonSchemaField } from "../../../../src/data"; 3 | import { fieldTestSuite } from "data/fields/field-test-suite"; 4 | 5 | describe("[data] JsonSchemaField", async () => { 6 | // @ts-ignore 7 | fieldTestSuite({ expect, test }, JsonSchemaField, { defaultValue: {}, schemaType: "text" }); 8 | 9 | // @todo: add JsonSchemaField tests 10 | }); 11 | -------------------------------------------------------------------------------- /app/__test__/data/specs/fields/NumberField.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { NumberField } from "../../../../src/data"; 3 | import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; 4 | 5 | describe("[data] NumberField", async () => { 6 | test("transformPersist (config)", async () => { 7 | const field = new NumberField("test", { minimum: 3, maximum: 5 }); 8 | 9 | expect(transformPersist(field, 2)).rejects.toThrow(); 10 | expect(transformPersist(field, 6)).rejects.toThrow(); 11 | expect(transformPersist(field, 4)).resolves.toBe(4); 12 | 13 | const field2 = new NumberField("test"); 14 | expect(transformPersist(field2, 0)).resolves.toBe(0); 15 | expect(transformPersist(field2, 10000)).resolves.toBe(10000); 16 | }); 17 | 18 | fieldTestSuite({ expect, test }, NumberField, { defaultValue: 12, schemaType: "integer" }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/__test__/data/specs/fields/TextField.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { TextField } from "../../../../src/data"; 3 | import { fieldTestSuite, transformPersist } from "data/fields/field-test-suite"; 4 | 5 | describe("[data] TextField", async () => { 6 | test("transformPersist (config)", async () => { 7 | const field = new TextField("test", { minLength: 3, maxLength: 5 }); 8 | 9 | expect(transformPersist(field, "a")).rejects.toThrow(); 10 | expect(transformPersist(field, "abcdefghijklmn")).rejects.toThrow(); 11 | expect(transformPersist(field, "abc")).resolves.toBe("abc"); 12 | }); 13 | 14 | fieldTestSuite({ expect, test }, TextField, { defaultValue: "abc", schemaType: "text" }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/__test__/flows/inc/back.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Flow } from "../../../src/flows"; 2 | import { getNamedTask } from "./helper"; 3 | 4 | const first = getNamedTask("first"); 5 | const second = getNamedTask("second"); 6 | const fourth = getNamedTask("fourth"); 7 | 8 | let thirdRuns = 0; 9 | const third = getNamedTask("third", async () => { 10 | thirdRuns++; 11 | if (thirdRuns === 3) { 12 | return true; 13 | } 14 | 15 | throw new Error("Third failed"); 16 | }); 17 | 18 | const back = new Flow("back", [first, second, third, fourth]); 19 | back.task(first).asInputFor(second); 20 | back.task(second).asInputFor(third); 21 | back.task(third).asInputFor(second, Condition.error(), 2); 22 | back.task(third).asInputFor(fourth, Condition.success()); 23 | 24 | export { back }; 25 | -------------------------------------------------------------------------------- /app/__test__/flows/inc/fanout-condition.ts: -------------------------------------------------------------------------------- 1 | import { Condition, Flow } from "../../../src/flows"; 2 | import { getNamedTask } from "./helper"; 3 | 4 | const first = getNamedTask( 5 | "first", 6 | async () => { 7 | //throw new Error("Error"); 8 | return { 9 | inner: { 10 | result: 2, 11 | }, 12 | }; 13 | }, 14 | 1000, 15 | ); 16 | const second = getNamedTask("second (if match)"); 17 | const third = getNamedTask("third (if error)"); 18 | 19 | const fanout = new Flow("fanout", [first, second, third]); 20 | fanout.task(first).asInputFor(third, Condition.error(), 2); 21 | fanout.task(first).asInputFor(second, Condition.matches("inner.result", 2)); 22 | 23 | export { fanout }; 24 | -------------------------------------------------------------------------------- /app/__test__/flows/inc/parallel.ts: -------------------------------------------------------------------------------- 1 | import { Flow } from "../../../src/flows"; 2 | import { getNamedTask } from "./helper"; 3 | 4 | const first = getNamedTask("first"); 5 | const second = getNamedTask("second", undefined, 1000); 6 | const third = getNamedTask("third"); 7 | const fourth = getNamedTask("fourth"); 8 | const fifth = getNamedTask("fifth"); // without connection 9 | 10 | const parallel = new Flow("Parallel", [first, second, third, fourth, fifth]); 11 | parallel.task(first).asInputFor(second); 12 | parallel.task(first).asInputFor(third); 13 | parallel.task(third).asInputFor(fourth); 14 | 15 | export { parallel }; 16 | -------------------------------------------------------------------------------- /app/__test__/flows/inc/simple-fetch.ts: -------------------------------------------------------------------------------- 1 | import { FetchTask, Flow, LogTask } from "../../../src/flows"; 2 | 3 | const first = new LogTask("First", { delay: 1000 }); 4 | const second = new LogTask("Second", { delay: 1000 }); 5 | const third = new LogTask("Long Third", { delay: 2500 }); 6 | const fourth = new FetchTask("Fetch Something", { 7 | url: "https://jsonplaceholder.typicode.com/todos/1", 8 | }); 9 | const fifth = new LogTask("Task 4", { delay: 500 }); // without connection 10 | 11 | const simpleFetch = new Flow("simpleFetch", [first, second, third, fourth, fifth]); 12 | simpleFetch.task(first).asInputFor(second); 13 | simpleFetch.task(first).asInputFor(third); 14 | simpleFetch.task(fourth).asOutputFor(third); 15 | 16 | simpleFetch.setRespondingTask(fourth); 17 | 18 | export { simpleFetch }; 19 | -------------------------------------------------------------------------------- /app/__test__/integration/config.integration.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "bun:test"; 2 | import { createApp } from "../../src"; 3 | import { Api } from "../../src/Api"; 4 | 5 | describe("integration config", () => { 6 | it("should create an entity", async () => { 7 | const app = createApp(); 8 | await app.build(); 9 | const api = new Api({ 10 | host: "http://localhost", 11 | fetcher: app.server.request as typeof fetch, 12 | }); 13 | 14 | // create entity 15 | await api.system.addConfig("data", "entities.posts", { 16 | name: "posts", 17 | config: { sort_field: "id", sort_dir: "asc" }, 18 | fields: { id: { type: "primary", name: "id" }, asdf: { type: "text" } }, 19 | type: "regular", 20 | }); 21 | 22 | expect(app.em.entities.map((e) => e.name)).toContain("posts"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /app/__test__/media/adapters/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bknd-io/bknd/7b128c97012e598b2667a4212691d0f019becd14/app/__test__/media/adapters/icon.png -------------------------------------------------------------------------------- /app/__test__/vitest/base.vi-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | 3 | describe("Example Test Suite", () => { 4 | it("should pass basic arithmetic", () => { 5 | expect(1 + 1).toBe(2); 6 | }); 7 | 8 | it("should handle async operations", async () => { 9 | const result = await Promise.resolve(42); 10 | expect(result).toBe(42); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/__test__/vitest/setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { afterEach } from "vitest"; 3 | import { cleanup } from "@testing-library/react"; 4 | 5 | // Automatically cleanup after each test 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | -------------------------------------------------------------------------------- /app/build.cli.ts: -------------------------------------------------------------------------------- 1 | import pkg from "./package.json" with { type: "json" }; 2 | import c from "picocolors"; 3 | import { formatNumber } from "core/utils"; 4 | 5 | const result = await Bun.build({ 6 | entrypoints: ["./src/cli/index.ts"], 7 | target: "node", 8 | outdir: "./dist/cli", 9 | env: "PUBLIC_*", 10 | minify: true, 11 | define: { 12 | __isDev: "0", 13 | __version: JSON.stringify(pkg.version), 14 | }, 15 | }); 16 | 17 | for (const output of result.outputs) { 18 | const size_ = await output.text(); 19 | console.info( 20 | c.cyan(formatNumber.fileSize(size_.length)), 21 | c.dim(output.path.replace(import.meta.dir + "/", "")), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | #registry = "http://localhost:4873" 3 | 4 | [test] 5 | coverageSkipTestFiles = true -------------------------------------------------------------------------------- /app/e2e/assets/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bknd-io/bknd/7b128c97012e598b2667a4212691d0f019becd14/app/e2e/assets/image.jpg -------------------------------------------------------------------------------- /app/e2e/base.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | import { testIds } from "../src/ui/lib/config"; 4 | 5 | import { getAdapterConfig } from "./inc/adapters"; 6 | const config = getAdapterConfig(); 7 | 8 | test("start page has expected title", async ({ page }) => { 9 | await page.goto(config.base_path); 10 | await expect(page).toHaveTitle(/BKND/); 11 | }); 12 | 13 | test("start page has expected heading", async ({ page }) => { 14 | await page.goto(config.base_path); 15 | 16 | // Example of checking if a heading with "No entity selected" exists and is visible 17 | const heading = page.getByRole("heading", { name: /No entity selected/i }); 18 | await expect(heading).toBeVisible(); 19 | }); 20 | 21 | test("modal opens on button click", async ({ page }) => { 22 | await page.goto(config.base_path); 23 | await page.getByTestId(testIds.data.btnCreateEntity).click(); 24 | await expect(page.getByRole("dialog")).toBeVisible(); 25 | }); 26 | -------------------------------------------------------------------------------- /app/e2e/inc/adapters.ts: -------------------------------------------------------------------------------- 1 | const adapter = process.env.TEST_ADAPTER; 2 | 3 | const default_config = { 4 | media_adapter: "local", 5 | base_path: "", 6 | } as const; 7 | 8 | const configs = { 9 | cloudflare: { 10 | media_adapter: "r2", 11 | }, 12 | "react-router": { 13 | base_path: "/admin", 14 | }, 15 | nextjs: { 16 | base_path: "/admin", 17 | }, 18 | astro: { 19 | base_path: "/admin", 20 | }, 21 | node: { 22 | base_path: "", 23 | }, 24 | bun: { 25 | base_path: "", 26 | }, 27 | }; 28 | 29 | export function getAdapterConfig(): typeof default_config { 30 | if (adapter) { 31 | if (!configs[adapter]) { 32 | console.warn( 33 | `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`, 34 | ); 35 | } else { 36 | return { 37 | ...default_config, 38 | ...configs[adapter], 39 | }; 40 | } 41 | } 42 | 43 | return default_config; 44 | } 45 | -------------------------------------------------------------------------------- /app/internal/esbuild.entry-output-meta.plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Metafile, Plugin } from "esbuild"; 2 | 3 | export const entryOutputMeta = ( 4 | onComplete?: ( 5 | outputs: { 6 | output: string; 7 | meta: Metafile["outputs"][string]; 8 | }[], 9 | ) => void | Promise, 10 | ): Plugin => ({ 11 | name: "report-entry-output-plugin", 12 | setup(build) { 13 | build.initialOptions.metafile = true; // Ensure metafile is enabled 14 | 15 | build.onEnd(async (result) => { 16 | console.log("result", result); 17 | if (result?.metafile?.outputs) { 18 | const entries = build.initialOptions.entryPoints! as string[]; 19 | 20 | const outputs = Object.entries(result.metafile.outputs) 21 | .filter(([, meta]) => { 22 | return meta.entryPoint && entries.includes(meta.entryPoint); 23 | }) 24 | .map(([output, meta]) => ({ output, meta })); 25 | if (outputs.length === 0) { 26 | return; 27 | } 28 | 29 | await onComplete?.(outputs); 30 | } 31 | }); 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | "postcss-preset-mantine": {}, 5 | "postcss-simple-vars": { 6 | variables: { 7 | "mantine-breakpoint-xs": "36em", 8 | "mantine-breakpoint-sm": "48em", 9 | "mantine-breakpoint-md": "62em", 10 | "mantine-breakpoint-lg": "75em", 11 | "mantine-breakpoint-xl": "88em", 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /app/src/adapter/astro/astro.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as astro from "./astro.adapter"; 3 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 4 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 5 | import { bunTestRunner } from "adapter/bun/test"; 6 | 7 | beforeAll(disableConsoleLog); 8 | afterAll(enableConsoleLog); 9 | 10 | describe("astro adapter", () => { 11 | adapterTestSuite(bunTestRunner, { 12 | makeApp: astro.getApp, 13 | makeHandler: (c, a, o) => (request: Request) => astro.serve(c, a, o)({ request }), 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/astro/astro.adapter.ts: -------------------------------------------------------------------------------- 1 | import { type FrameworkBkndConfig, createFrameworkApp, type FrameworkOptions } from "bknd/adapter"; 2 | 3 | type AstroEnv = NodeJS.ProcessEnv; 4 | type TAstro = { 5 | request: Request; 6 | }; 7 | export type AstroBkndConfig = FrameworkBkndConfig; 8 | 9 | export async function getApp( 10 | config: AstroBkndConfig = {}, 11 | args: Env = {} as Env, 12 | opts: FrameworkOptions = {}, 13 | ) { 14 | return await createFrameworkApp(config, args ?? import.meta.env, opts); 15 | } 16 | 17 | export function serve( 18 | config: AstroBkndConfig = {}, 19 | args: Env = {} as Env, 20 | opts?: FrameworkOptions, 21 | ) { 22 | return async (fnArgs: TAstro) => { 23 | return (await getApp(config, args, opts)).fetch(fnArgs.request); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /app/src/adapter/astro/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./astro.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/adapter/aws/aws.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as awsLambda from "./aws-lambda.adapter"; 3 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 4 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 5 | import { bunTestRunner } from "adapter/bun/test"; 6 | 7 | beforeAll(disableConsoleLog); 8 | afterAll(enableConsoleLog); 9 | 10 | describe("aws adapter", () => { 11 | adapterTestSuite(bunTestRunner, { 12 | makeApp: awsLambda.createApp, 13 | // @todo: add a request to lambda event translator? 14 | makeHandler: (c, a, o) => async (request: Request) => { 15 | const app = await awsLambda.createApp(c, a, o); 16 | return app.fetch(request); 17 | }, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/src/adapter/aws/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./aws-lambda.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/adapter/bun/bun.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as bun from "./bun.adapter"; 3 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 4 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 5 | import { bunTestRunner } from "adapter/bun/test"; 6 | 7 | beforeAll(disableConsoleLog); 8 | afterAll(enableConsoleLog); 9 | 10 | describe("bun adapter", () => { 11 | adapterTestSuite(bunTestRunner, { 12 | makeApp: bun.createApp, 13 | makeHandler: bun.createHandler, 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/bun/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./bun.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/adapter/bun/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, mock } from "bun:test"; 2 | 3 | export const bunTestRunner = { 4 | expect, 5 | test, 6 | mock, 7 | }; 8 | -------------------------------------------------------------------------------- /app/src/adapter/cloudflare/bindings.ts: -------------------------------------------------------------------------------- 1 | export type BindingTypeMap = { 2 | D1Database: D1Database; 3 | KVNamespace: KVNamespace; 4 | DurableObjectNamespace: DurableObjectNamespace; 5 | R2Bucket: R2Bucket; 6 | }; 7 | 8 | export type GetBindingType = keyof BindingTypeMap; 9 | export type BindingMap = { key: string; value: BindingTypeMap[T] }; 10 | 11 | export function getBindings(env: any, type: T): BindingMap[] { 12 | const bindings: BindingMap[] = []; 13 | for (const key in env) { 14 | try { 15 | if (env[key] && (env[key] as any).constructor.name === type) { 16 | bindings.push({ 17 | key, 18 | value: env[key] as BindingTypeMap[T], 19 | }); 20 | } 21 | } catch (e) {} 22 | } 23 | return bindings; 24 | } 25 | 26 | export function getBinding(env: any, type: T): BindingMap { 27 | const bindings = getBindings(env, type); 28 | if (bindings.length === 0) { 29 | throw new Error(`No ${type} found in bindings`); 30 | } 31 | return bindings[0] as BindingMap; 32 | } 33 | -------------------------------------------------------------------------------- /app/src/adapter/cloudflare/index.ts: -------------------------------------------------------------------------------- 1 | import { D1Connection, type D1ConnectionConfig } from "./D1Connection"; 2 | 3 | export * from "./cloudflare-workers.adapter"; 4 | export { makeApp, getFresh } from "./modes/fresh"; 5 | export { getCached } from "./modes/cached"; 6 | export { DurableBkndApp, getDurable } from "./modes/durable"; 7 | export { D1Connection, type D1ConnectionConfig }; 8 | export { 9 | getBinding, 10 | getBindings, 11 | type BindingTypeMap, 12 | type GetBindingType, 13 | type BindingMap, 14 | } from "./bindings"; 15 | 16 | export function d1(config: D1ConnectionConfig) { 17 | return new D1Connection(config); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/adapter/cloudflare/modes/fresh.ts: -------------------------------------------------------------------------------- 1 | import { createRuntimeApp, type RuntimeOptions } from "bknd/adapter"; 2 | import type { CloudflareBkndConfig, Context, CloudflareEnv } from "../index"; 3 | import { makeConfig, registerAsyncsExecutionContext } from "../config"; 4 | 5 | export async function makeApp( 6 | config: CloudflareBkndConfig, 7 | args: Env = {} as Env, 8 | opts?: RuntimeOptions, 9 | ) { 10 | return await createRuntimeApp(makeConfig(config, args), args, opts); 11 | } 12 | 13 | export async function getFresh( 14 | config: CloudflareBkndConfig, 15 | ctx: Context, 16 | opts: RuntimeOptions = {}, 17 | ) { 18 | return await makeApp( 19 | { 20 | ...config, 21 | onBuilt: async (app) => { 22 | registerAsyncsExecutionContext(app, ctx.ctx); 23 | await config.onBuilt?.(app); 24 | }, 25 | }, 26 | ctx.env, 27 | opts, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/src/adapter/nextjs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./nextjs.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/adapter/nextjs/nextjs.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as nextjs from "./nextjs.adapter"; 3 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 4 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 5 | import { bunTestRunner } from "adapter/bun/test"; 6 | import type { NextjsBkndConfig } from "./nextjs.adapter"; 7 | 8 | beforeAll(disableConsoleLog); 9 | afterAll(enableConsoleLog); 10 | 11 | describe("nextjs adapter", () => { 12 | adapterTestSuite(bunTestRunner, { 13 | makeApp: nextjs.getApp, 14 | makeHandler: nextjs.serve, 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/src/adapter/node/index.ts: -------------------------------------------------------------------------------- 1 | import { registries } from "bknd"; 2 | import { type LocalAdapterConfig, StorageLocalAdapter } from "./storage/StorageLocalAdapter"; 3 | 4 | export * from "./node.adapter"; 5 | export { StorageLocalAdapter, type LocalAdapterConfig }; 6 | 7 | let registered = false; 8 | export function registerLocalMediaAdapter() { 9 | if (!registered) { 10 | registries.media.register("local", StorageLocalAdapter); 11 | registered = true; 12 | } 13 | 14 | return (config: Partial = {}) => { 15 | const adapter = new StorageLocalAdapter(config); 16 | return adapter.toJSON(true); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /app/src/adapter/node/node.adapter.native-spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, before, after } from "node:test"; 2 | import * as node from "./node.adapter"; 3 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 4 | import { nodeTestRunner } from "adapter/node/test"; 5 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 6 | 7 | before(() => disableConsoleLog()); 8 | after(enableConsoleLog); 9 | 10 | describe("node adapter", () => { 11 | adapterTestSuite(nodeTestRunner, { 12 | makeApp: node.createApp, 13 | makeHandler: node.createHandler, 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/node/node.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as node from "./node.adapter"; 3 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 4 | import { bunTestRunner } from "adapter/bun/test"; 5 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 6 | 7 | beforeAll(disableConsoleLog); 8 | afterAll(enableConsoleLog); 9 | 10 | describe("node adapter (bun)", () => { 11 | adapterTestSuite(bunTestRunner, { 12 | makeApp: node.createApp, 13 | makeHandler: node.createHandler, 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/node/storage/StorageLocalAdapter.native-spec.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "node:test"; 2 | import { nodeTestRunner } from "adapter/node/test"; 3 | import { StorageLocalAdapter } from "adapter/node"; 4 | import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite"; 5 | import { readFileSync } from "node:fs"; 6 | import path from "node:path"; 7 | 8 | describe("StorageLocalAdapter (node)", async () => { 9 | const basePath = path.resolve(import.meta.dirname, "../../../../__test__/_assets"); 10 | const buffer = readFileSync(path.join(basePath, "image.png")); 11 | const file = new File([buffer], "image.png", { type: "image/png" }); 12 | 13 | const adapter = new StorageLocalAdapter({ 14 | path: path.join(basePath, "tmp"), 15 | }); 16 | 17 | await adapterTestSuite(nodeTestRunner, adapter, file); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/adapter/node/storage/StorageLocalAdapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { StorageLocalAdapter } from "./StorageLocalAdapter"; 3 | // @ts-ignore 4 | import { assetsPath, assetsTmpPath } from "../../../../__test__/helper"; 5 | import { adapterTestSuite } from "media/storage/adapters/adapter-test-suite"; 6 | import { bunTestRunner } from "adapter/bun/test"; 7 | 8 | describe("StorageLocalAdapter (bun)", async () => { 9 | const adapter = new StorageLocalAdapter({ 10 | path: assetsTmpPath, 11 | }); 12 | 13 | const file = Bun.file(`${assetsPath}/image.png`); 14 | await adapterTestSuite(bunTestRunner, adapter, file); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/react-router/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./react-router.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/adapter/react-router/react-router.adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe } from "bun:test"; 2 | import * as rr from "./react-router.adapter"; 3 | import { disableConsoleLog, enableConsoleLog } from "core/utils"; 4 | import { adapterTestSuite } from "adapter/adapter-test-suite"; 5 | import { bunTestRunner } from "adapter/bun/test"; 6 | 7 | beforeAll(disableConsoleLog); 8 | afterAll(enableConsoleLog); 9 | 10 | describe("react-router adapter", () => { 11 | adapterTestSuite(bunTestRunner, { 12 | makeApp: rr.getApp, 13 | makeHandler: (c, a, o) => (request: Request) => rr.serve(c, a?.env, o)({ request }), 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/src/adapter/react-router/react-router.adapter.ts: -------------------------------------------------------------------------------- 1 | import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; 2 | import type { FrameworkOptions } from "adapter"; 3 | 4 | type ReactRouterEnv = NodeJS.ProcessEnv; 5 | type ReactRouterFunctionArgs = { 6 | request: Request; 7 | }; 8 | export type ReactRouterBkndConfig = FrameworkBkndConfig; 9 | 10 | export async function getApp( 11 | config: ReactRouterBkndConfig, 12 | args: Env = {} as Env, 13 | opts?: FrameworkOptions, 14 | ) { 15 | return await createFrameworkApp(config, args ?? process.env, opts); 16 | } 17 | 18 | export function serve( 19 | config: ReactRouterBkndConfig = {}, 20 | args: Env = {} as Env, 21 | opts?: FrameworkOptions, 22 | ) { 23 | return async (fnArgs: ReactRouterFunctionArgs) => { 24 | return (await getApp(config, args, opts)).fetch(fnArgs.request); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /app/src/adapter/utils.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage } from "node:http"; 2 | 3 | export function nodeRequestToRequest(req: IncomingMessage): Request { 4 | let protocol = "http"; 5 | try { 6 | protocol = req.headers["x-forwarded-proto"] as string; 7 | } catch (e) {} 8 | const host = req.headers.host; 9 | const url = `${protocol}://${host}${req.url}`; 10 | const headers = new Headers(); 11 | 12 | for (const [key, value] of Object.entries(req.headers)) { 13 | if (Array.isArray(value)) { 14 | headers.append(key, value.join(", ")); 15 | } else if (value) { 16 | headers.append(key, value); 17 | } 18 | } 19 | 20 | const method = req.method || "GET"; 21 | return new Request(url, { 22 | method, 23 | headers, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/adapter/vite/dev-server-config.ts: -------------------------------------------------------------------------------- 1 | export const devServerConfig = { 2 | entry: "./server.ts", 3 | exclude: [ 4 | /.*\.tsx?($|\?)/, 5 | /^(?!.*\/__admin).*\.(s?css|less)($|\?)/, 6 | // exclude except /api 7 | /^(?!.*\/api).*\.(ico|mp4|jpg|jpeg|svg|png|vtt|mp3|js)($|\?)/, 8 | /^\/@.+$/, 9 | /\/components.*?\.json.*/, // @todo: improve 10 | /^\/(public|assets|static)\/.+/, 11 | /^\/node_modules\/.*/, 12 | ] as any, 13 | injectClientScript: false, 14 | } as const; 15 | -------------------------------------------------------------------------------- /app/src/adapter/vite/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./vite.adapter"; 2 | -------------------------------------------------------------------------------- /app/src/auth/auth-permissions.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from "core"; 2 | 3 | export const createUser = new Permission("auth.user.create"); 4 | //export const updateUser = new Permission("auth.user.update"); 5 | -------------------------------------------------------------------------------- /app/src/auth/authenticate/strategies/index.ts: -------------------------------------------------------------------------------- 1 | import { CustomOAuthStrategy } from "auth/authenticate/strategies/oauth/CustomOAuthStrategy"; 2 | import { PasswordStrategy, type PasswordStrategyOptions } from "./PasswordStrategy"; 3 | import { OAuthCallbackException, OAuthStrategy } from "./oauth/OAuthStrategy"; 4 | 5 | export * as issuers from "./oauth/issuers"; 6 | 7 | export { 8 | type PasswordStrategyOptions, 9 | PasswordStrategy, 10 | OAuthStrategy, 11 | OAuthCallbackException, 12 | CustomOAuthStrategy, 13 | }; 14 | -------------------------------------------------------------------------------- /app/src/auth/authenticate/strategies/oauth/issuers/google.ts: -------------------------------------------------------------------------------- 1 | import type { IssuerConfig } from "../OAuthStrategy"; 2 | 3 | type GoogleUserInfo = { 4 | sub: string; 5 | name: string; 6 | given_name: string; 7 | family_name: string; 8 | picture: string; 9 | email: string; 10 | email_verified: boolean; 11 | locale: string; 12 | }; 13 | 14 | export const google: IssuerConfig = { 15 | type: "oidc", 16 | client: { 17 | token_endpoint_auth_method: "client_secret_basic", 18 | }, 19 | as: { 20 | issuer: "https://accounts.google.com", 21 | }, 22 | profile: async (info) => { 23 | return { 24 | ...info, 25 | sub: info.sub, 26 | email: info.email, 27 | }; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /app/src/auth/authenticate/strategies/oauth/issuers/index.ts: -------------------------------------------------------------------------------- 1 | export { google } from "./google"; 2 | export { github } from "./github"; 3 | -------------------------------------------------------------------------------- /app/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { UserExistsException, UserNotFoundException, InvalidCredentialsException } from "./errors"; 2 | export { 3 | type ProfileExchange, 4 | type Strategy, 5 | type User, 6 | type SafeUser, 7 | type CreateUser, 8 | type AuthResponse, 9 | type UserPool, 10 | type AuthAction, 11 | type AuthUserResolver, 12 | Authenticator, 13 | authenticatorConfig, 14 | jwtConfig, 15 | } from "./authenticate/Authenticator"; 16 | 17 | export { AppAuth, type UserFieldSchema } from "./AppAuth"; 18 | 19 | export { Guard, type GuardUserContext, type GuardConfig } from "./authorize/Guard"; 20 | export { Role } from "./authorize/Role"; 21 | 22 | export * as AuthPermissions from "./auth-permissions"; 23 | -------------------------------------------------------------------------------- /app/src/cli/commands/config.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultConfig } from "modules/ModuleManager"; 2 | import type { CliCommand } from "../types"; 3 | 4 | export const config: CliCommand = (program) => { 5 | program 6 | .command("config") 7 | .description("get default config") 8 | .option("--pretty", "pretty print") 9 | .action((options) => { 10 | const config = getDefaultConfig(); 11 | 12 | // biome-ignore lint/suspicious/noConsoleLog: 13 | console.log(options.pretty ? JSON.stringify(config, null, 2) : JSON.stringify(config)); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /app/src/cli/commands/create/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create"; 2 | -------------------------------------------------------------------------------- /app/src/cli/commands/index.ts: -------------------------------------------------------------------------------- 1 | export { config } from "./config"; 2 | export { schema } from "./schema"; 3 | export { run } from "./run"; 4 | export { debug } from "./debug"; 5 | export { user } from "./user"; 6 | export { create } from "./create"; 7 | export { copyAssets } from "./copy-assets"; 8 | export { types } from "./types"; 9 | -------------------------------------------------------------------------------- /app/src/cli/commands/run/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./run"; 2 | -------------------------------------------------------------------------------- /app/src/cli/commands/schema.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultSchema } from "modules/ModuleManager"; 2 | import type { CliCommand } from "../types"; 3 | 4 | export const schema: CliCommand = (program) => { 5 | program 6 | .command("schema") 7 | .description("get schema") 8 | .option("--pretty", "pretty print") 9 | .action((options) => { 10 | const schema = getDefaultSchema(); 11 | // biome-ignore lint/suspicious/noConsoleLog: 12 | console.log(options.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema)); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /app/src/cli/commands/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /app/src/cli/commands/types/types.ts: -------------------------------------------------------------------------------- 1 | import type { CliCommand } from "cli/types"; 2 | import { Option } from "commander"; 3 | import { makeAppFromEnv } from "cli/commands/run"; 4 | import { EntityTypescript } from "data/entities/EntityTypescript"; 5 | import { writeFile } from "cli/utils/sys"; 6 | import c from "picocolors"; 7 | 8 | export const types: CliCommand = (program) => { 9 | program 10 | .command("types") 11 | .description("generate types") 12 | .addOption(new Option("-o, --outfile ", "output file").default("bknd-types.d.ts")) 13 | .addOption(new Option("--no-write", "do not write to file").default(true)) 14 | .action(action); 15 | }; 16 | 17 | async function action({ 18 | outfile, 19 | write, 20 | }: { 21 | outfile: string; 22 | write: boolean; 23 | }) { 24 | const app = await makeAppFromEnv({ 25 | server: "node", 26 | }); 27 | await app.build(); 28 | 29 | const et = new EntityTypescript(app.em); 30 | 31 | if (write) { 32 | await writeFile(outfile, et.toString()); 33 | console.info(`\nTypes written to ${c.cyan(outfile)}`); 34 | } else { 35 | console.info(et.toString()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from "commander"; 4 | import color from "picocolors"; 5 | import * as commands from "./commands"; 6 | import { getVersion } from "./utils/sys"; 7 | import { capture, flush, init } from "cli/utils/telemetry"; 8 | const program = new Command(); 9 | 10 | export async function main() { 11 | await init(); 12 | capture("start"); 13 | 14 | const version = await getVersion(); 15 | program 16 | .name("bknd") 17 | .description(color.yellowBright("⚡") + " bknd cli " + color.bold(color.cyan(`v${version}`))) 18 | .version(version) 19 | .hook("preAction", (thisCommand, actionCommand) => { 20 | capture(`cmd_${actionCommand.name()}`); 21 | }) 22 | .hook("postAction", async () => { 23 | await flush(); 24 | }); 25 | 26 | // register commands 27 | for (const command of Object.values(commands)) { 28 | command(program); 29 | } 30 | 31 | await program.parseAsync(); 32 | } 33 | 34 | main() 35 | .then(null) 36 | .catch(async (e) => { 37 | await flush(); 38 | console.error(e); 39 | }); 40 | -------------------------------------------------------------------------------- /app/src/cli/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { BkndConfig } from "adapter"; 2 | import type { Command } from "commander"; 3 | 4 | export type CliCommand = (program: Command) => void; 5 | 6 | export type CliBkndConfig = BkndConfig & { 7 | server?: { 8 | port?: number; 9 | platform?: "node" | "bun"; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /app/src/core/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * These are package global defaults. 3 | */ 4 | import type { Generated } from "kysely"; 5 | 6 | export type PrimaryFieldType = IdType | Generated; 7 | 8 | export interface AppEntity { 9 | id: PrimaryFieldType; 10 | } 11 | 12 | export interface DB { 13 | // make sure to make unknown as "any" 14 | [key: string]: { 15 | id: PrimaryFieldType; 16 | [key: string]: any; 17 | }; 18 | } 19 | 20 | export const config = { 21 | server: { 22 | default_port: 1337, 23 | // resetted to root for now, bc bundling with vite 24 | assets_path: "/", 25 | }, 26 | data: { 27 | default_primary_field: "id", 28 | }, 29 | } as const; 30 | -------------------------------------------------------------------------------- /app/src/core/events/EventListener.ts: -------------------------------------------------------------------------------- 1 | import type { Event } from "./Event"; 2 | import type { EventClass } from "./EventManager"; 3 | 4 | export const ListenerModes = ["sync", "async"] as const; 5 | export type ListenerMode = (typeof ListenerModes)[number]; 6 | 7 | export type ListenerHandler> = ( 8 | event: E, 9 | slug: string, 10 | ) => E extends Event ? R | Promise : never; 11 | 12 | export class EventListener { 13 | mode: ListenerMode = "async"; 14 | event: EventClass; 15 | handler: ListenerHandler; 16 | once: boolean = false; 17 | id?: string; 18 | 19 | constructor( 20 | event: EventClass, 21 | handler: ListenerHandler, 22 | mode: ListenerMode = "async", 23 | id?: string, 24 | ) { 25 | this.event = event; 26 | this.handler = handler; 27 | this.mode = mode; 28 | this.id = id; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/core/events/index.ts: -------------------------------------------------------------------------------- 1 | export { Event, NoParamEvent, InvalidEventReturn } from "./Event"; 2 | export { 3 | EventListener, 4 | ListenerModes, 5 | type ListenerMode, 6 | type ListenerHandler, 7 | } from "./EventListener"; 8 | export { EventManager, type EmitsEvents, type EventClass } from "./EventManager"; 9 | -------------------------------------------------------------------------------- /app/src/core/security/Permission.ts: -------------------------------------------------------------------------------- 1 | export class Permission { 2 | constructor(public name: Name) { 3 | this.name = name; 4 | } 5 | 6 | toJSON() { 7 | return { 8 | name: this.name, 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/core/server/ContextHelper.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from "hono"; 2 | 3 | export class ContextHelper { 4 | constructor(protected c: Context) {} 5 | 6 | contentTypeMime(): string { 7 | const contentType = this.c.res.headers.get("Content-Type"); 8 | if (contentType) { 9 | return String(contentType.split(";")[0]); 10 | } 11 | return ""; 12 | } 13 | 14 | isHtml(): boolean { 15 | return this.contentTypeMime() === "text/html"; 16 | } 17 | 18 | url(): URL { 19 | return new URL(this.c.req.url); 20 | } 21 | 22 | headersObject() { 23 | const headers = {}; 24 | for (const [k, v] of this.c.res.headers.entries()) { 25 | headers[k] = v; 26 | } 27 | return headers; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/core/server/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { tbValidator } from "./tbValidator"; 2 | -------------------------------------------------------------------------------- /app/src/core/types.ts: -------------------------------------------------------------------------------- 1 | export interface Serializable { 2 | toJSON(): Json; 3 | fromJSON(json: Json): Class; 4 | } 5 | -------------------------------------------------------------------------------- /app/src/core/utils/DebugLogger.ts: -------------------------------------------------------------------------------- 1 | export class DebugLogger { 2 | public _context: string[] = []; 3 | _enabled: boolean = true; 4 | private readonly id = Math.random().toString(36).substr(2, 9); 5 | private last: number = 0; 6 | 7 | constructor(enabled: boolean = true) { 8 | this._enabled = enabled; 9 | } 10 | 11 | context(context: string) { 12 | this._context.push(context); 13 | return this; 14 | } 15 | 16 | clear() { 17 | this._context.pop(); 18 | return this; 19 | } 20 | 21 | reset() { 22 | this.last = 0; 23 | return this; 24 | } 25 | 26 | log(...args: any[]) { 27 | if (!this._enabled) return this; 28 | 29 | const now = performance.now(); 30 | const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last)); 31 | const indents = " ".repeat(Math.max(this._context.length - 1, 0)); 32 | const context = 33 | this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : ""; 34 | 35 | // biome-ignore lint/suspicious/noConsoleLog: 36 | console.log(indents, context, time, ...args); 37 | 38 | this.last = now; 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/core/utils/browser.ts: -------------------------------------------------------------------------------- 1 | export type TBrowser = "Opera" | "Edge" | "Chrome" | "Safari" | "Firefox" | "IE" | "unknown"; 2 | export function getBrowser(): TBrowser { 3 | if ((navigator.userAgent.indexOf("Opera") || navigator.userAgent.indexOf("OPR")) !== -1) { 4 | return "Opera"; 5 | } else if (navigator.userAgent.indexOf("Edg") !== -1) { 6 | return "Edge"; 7 | } else if (navigator.userAgent.indexOf("Chrome") !== -1) { 8 | return "Chrome"; 9 | } else if (navigator.userAgent.indexOf("Safari") !== -1) { 10 | return "Safari"; 11 | } else if (navigator.userAgent.indexOf("Firefox") !== -1) { 12 | return "Firefox"; 13 | // @ts-ignore 14 | } else if (navigator.userAgent.indexOf("MSIE") !== -1 || !!document.documentMode === true) { 15 | //IF IE > 10 16 | return "IE"; 17 | } else { 18 | return "unknown"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/core/utils/dates.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import weekOfYear from "dayjs/plugin/weekOfYear.js"; 3 | 4 | declare module "dayjs" { 5 | interface Dayjs { 6 | week(): number; 7 | week(value: number): dayjs.Dayjs; 8 | } 9 | } 10 | 11 | dayjs.extend(weekOfYear); 12 | 13 | export function datetimeStringLocal(dateOrString?: string | Date | undefined): string { 14 | return dayjs(dateOrString).format("YYYY-MM-DD HH:mm:ss"); 15 | } 16 | 17 | export function datetimeStringUTC(dateOrString?: string | Date | undefined): string { 18 | const date = dateOrString ? new Date(dateOrString) : new Date(); 19 | return date.toISOString().replace("T", " ").split(".")[0]!; 20 | } 21 | 22 | export function getTimezoneOffset(): number { 23 | return new Date().getTimezoneOffset(); 24 | } 25 | 26 | export function getTimezone(): string { 27 | return Intl.DateTimeFormat().resolvedOptions().timeZone; 28 | } 29 | 30 | export { dayjs }; 31 | -------------------------------------------------------------------------------- /app/src/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./browser"; 2 | export * from "./objects"; 3 | export * from "./strings"; 4 | export * from "./perf"; 5 | export * from "./file"; 6 | export * from "./reqres"; 7 | export * from "./xml"; 8 | export type { Prettify, PrettifyRec } from "./types"; 9 | export * from "./typebox"; 10 | export * from "./dates"; 11 | export * from "./crypto"; 12 | export * from "./uuid"; 13 | export { FromSchema } from "./typebox/from-schema"; 14 | export * from "./test"; 15 | export * from "./runtime"; 16 | export * from "./numbers"; 17 | -------------------------------------------------------------------------------- /app/src/core/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | export function clampNumber(value: number, min: number, max: number): number { 2 | const lower = Math.min(min, max); 3 | const upper = Math.max(min, max); 4 | return Math.max(lower, Math.min(value, upper)); 5 | } 6 | 7 | export function ensureInt(value?: string | number | null | undefined): number { 8 | if (value === undefined || value === null) { 9 | return 0; 10 | } 11 | 12 | return typeof value === "number" ? value : Number.parseInt(value, 10); 13 | } 14 | 15 | export const formatNumber = { 16 | fileSize: (bytes: number, decimals = 2): string => { 17 | if (bytes === 0) return "0 Bytes"; 18 | const k = 1024; 19 | const dm = decimals < 0 ? 0 : decimals; 20 | const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; 21 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 22 | return Number.parseFloat((bytes / k ** i).toFixed(dm)) + " " + sizes[i]; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /app/src/core/utils/sql.ts: -------------------------------------------------------------------------------- 1 | import { isDebug } from "../env"; 2 | 3 | export async function formatSql(sql: string): Promise { 4 | if (isDebug()) { 5 | const { format } = await import("sql-formatter"); 6 | return format(sql); 7 | } 8 | return ""; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/core/utils/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Prettify = { 2 | [K in keyof T]: T[K]; 3 | } & NonNullable; 4 | 5 | // prettify recursively 6 | export type PrettifyRec = { 7 | [K in keyof T]: T[K] extends object ? Prettify : T[K]; 8 | } & NonNullable; 9 | -------------------------------------------------------------------------------- /app/src/core/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | // generates v4 2 | export function uuid(): string { 3 | return crypto.randomUUID(); 4 | } 5 | -------------------------------------------------------------------------------- /app/src/core/utils/xml.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from "fast-xml-parser"; 2 | 3 | export function xmlToObject(xml: string) { 4 | const parser = new XMLParser(); 5 | return parser.parse(xml); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/data/connection/DummyConnection.ts: -------------------------------------------------------------------------------- 1 | import { Connection, type FieldSpec, type SchemaResponse } from "./Connection"; 2 | 3 | export class DummyConnection extends Connection { 4 | protected override readonly supported = { 5 | batching: true, 6 | }; 7 | 8 | constructor() { 9 | super(undefined as any); 10 | } 11 | 12 | override getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse { 13 | throw new Error("Method not implemented."); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/data/connection/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseIntrospector } from "./BaseIntrospector"; 2 | export { 3 | Connection, 4 | type FieldSpec, 5 | type IndexSpec, 6 | type DbFunctions, 7 | type SchemaResponse, 8 | } from "./Connection"; 9 | 10 | // sqlite 11 | export { LibsqlConnection, type LibSqlCredentials } from "./sqlite/LibsqlConnection"; 12 | export { SqliteConnection } from "./sqlite/SqliteConnection"; 13 | export { SqliteIntrospector } from "./sqlite/SqliteIntrospector"; 14 | export { SqliteLocalConnection } from "./sqlite/SqliteLocalConnection"; 15 | -------------------------------------------------------------------------------- /app/src/data/connection/sqlite/SqliteLocalConnection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DatabaseIntrospector, 3 | Kysely, 4 | ParseJSONResultsPlugin, 5 | type SqliteDatabase, 6 | SqliteDialect, 7 | } from "kysely"; 8 | import { SqliteConnection } from "./SqliteConnection"; 9 | import { SqliteIntrospector } from "./SqliteIntrospector"; 10 | 11 | const plugins = [new ParseJSONResultsPlugin()]; 12 | 13 | class CustomSqliteDialect extends SqliteDialect { 14 | override createIntrospector(db: Kysely): DatabaseIntrospector { 15 | return new SqliteIntrospector(db, { 16 | excludeTables: ["test_table"], 17 | plugins, 18 | }); 19 | } 20 | } 21 | 22 | export class SqliteLocalConnection extends SqliteConnection { 23 | constructor(private database: SqliteDatabase) { 24 | const kysely = new Kysely({ 25 | dialect: new CustomSqliteDialect({ database }), 26 | plugins, 27 | }); 28 | 29 | super(kysely, {}, plugins); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/data/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Entity"; 2 | export * from "./EntityManager"; 3 | export * from "./Mutator"; 4 | export * from "./query/Repository"; 5 | export * from "./query/WhereBuilder"; 6 | export * from "./query/WithBuilder"; 7 | -------------------------------------------------------------------------------- /app/src/data/fields/VirtualField.ts: -------------------------------------------------------------------------------- 1 | import type { Static } from "core/utils"; 2 | import { Field, baseFieldConfigSchema } from "./Field"; 3 | import * as tbbox from "@sinclair/typebox"; 4 | const { Type } = tbbox; 5 | 6 | export const virtualFieldConfigSchema = Type.Composite([baseFieldConfigSchema, Type.Object({})]); 7 | 8 | export type VirtualFieldConfig = Static; 9 | 10 | export class VirtualField extends Field { 11 | override readonly type = "virtual"; 12 | 13 | constructor(name: string, config?: Partial) { 14 | // field must be virtual, as it doesn't store a reference to the entity 15 | super(name, { ...config, fillable: false, virtual: true }); 16 | } 17 | 18 | protected getSchema() { 19 | return virtualFieldConfigSchema; 20 | } 21 | 22 | override schema() { 23 | return undefined; 24 | } 25 | 26 | override toJsonSchema() { 27 | return this.toSchemaWrapIfRequired( 28 | Type.Any({ 29 | default: this.getDefault(), 30 | readOnly: true, 31 | }), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/data/index.ts: -------------------------------------------------------------------------------- 1 | import { MutatorEvents, RepositoryEvents } from "./events"; 2 | 3 | export * from "./fields"; 4 | export * from "./entities"; 5 | export * from "./relations"; 6 | export * from "./schema/SchemaManager"; 7 | export * from "./prototype"; 8 | export * from "./connection"; 9 | 10 | export { 11 | type RepoQuery, 12 | type RepoQueryIn, 13 | getRepoQueryTemplate, 14 | repoQuery, 15 | } from "./server/query"; 16 | 17 | export type { WhereQuery } from "./entities/query/WhereBuilder"; 18 | 19 | export { KyselyPluginRunner } from "./plugins/KyselyPluginRunner"; 20 | 21 | export { constructEntity, constructRelation } from "./schema/constructor"; 22 | 23 | export const DatabaseEvents = { 24 | ...MutatorEvents, 25 | ...RepositoryEvents, 26 | }; 27 | export { MutatorEvents, RepositoryEvents }; 28 | 29 | export * as DataPermissions from "./permissions"; 30 | 31 | export { MediaField, type MediaFieldConfig, type MediaItem } from "media/MediaField"; 32 | -------------------------------------------------------------------------------- /app/src/data/permissions/index.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from "core"; 2 | 3 | export const entityRead = new Permission("data.entity.read"); 4 | export const entityCreate = new Permission("data.entity.create"); 5 | export const entityUpdate = new Permission("data.entity.update"); 6 | export const entityDelete = new Permission("data.entity.delete"); 7 | export const databaseSync = new Permission("data.database.sync"); 8 | export const rawQuery = new Permission("data.raw.query"); 9 | export const rawMutate = new Permission("data.raw.mutate"); 10 | -------------------------------------------------------------------------------- /app/src/data/plugins/DeserializeJsonValuesPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | KyselyPlugin, 3 | PluginTransformQueryArgs, 4 | PluginTransformResultArgs, 5 | QueryResult, 6 | RootOperationNode, 7 | UnknownRow, 8 | } from "kysely"; 9 | 10 | type KeyValueObject = { [key: string]: any }; 11 | 12 | export class DeserializeJsonValuesPlugin implements KyselyPlugin { 13 | transformQuery(args: PluginTransformQueryArgs): RootOperationNode { 14 | return args.node; 15 | } 16 | transformResult(args: PluginTransformResultArgs): Promise> { 17 | return Promise.resolve({ 18 | ...args.result, 19 | rows: args.result.rows.map((row: KeyValueObject) => { 20 | const result: KeyValueObject = {}; 21 | for (const key in row) { 22 | try { 23 | // Attempt to parse the value as JSON 24 | result[key] = JSON.parse(row[key]); 25 | } catch (error) { 26 | // If parsing fails, keep the original value 27 | result[key] = row[key]; 28 | } 29 | } 30 | return result; 31 | }), 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/data/plugins/FilterNumericKeysPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | KyselyPlugin, 3 | PluginTransformQueryArgs, 4 | PluginTransformResultArgs, 5 | QueryResult, 6 | RootOperationNode, 7 | UnknownRow, 8 | } from "kysely"; 9 | 10 | type KeyValueObject = { [key: string]: any }; 11 | 12 | export class FilterNumericKeysPlugin implements KyselyPlugin { 13 | transformQuery(args: PluginTransformQueryArgs): RootOperationNode { 14 | return args.node; 15 | } 16 | transformResult(args: PluginTransformResultArgs): Promise> { 17 | return Promise.resolve({ 18 | ...args.result, 19 | rows: args.result.rows.map((row: KeyValueObject) => { 20 | const filteredObj: KeyValueObject = {}; 21 | for (const key in row) { 22 | if (Number.isNaN(+key)) { 23 | // Check if the key is not a number 24 | filteredObj[key] = row[key]; 25 | } 26 | } 27 | return filteredObj; 28 | }), 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/data/plugins/KyselyPluginRunner.ts: -------------------------------------------------------------------------------- 1 | import type { KyselyPlugin, UnknownRow } from "kysely"; 2 | 3 | // @todo: add test 4 | export class KyselyPluginRunner { 5 | protected plugins: Set; 6 | 7 | constructor(plugins: KyselyPlugin[] = []) { 8 | this.plugins = new Set(plugins); 9 | } 10 | 11 | async transformResultRows(rows: T[]): Promise { 12 | let copy = rows; 13 | for (const plugin of this.plugins) { 14 | const res = await plugin.transformResult({ 15 | queryId: "1" as any, 16 | result: { rows: copy as UnknownRow[] }, 17 | }); 18 | copy = res.rows as T[]; 19 | } 20 | 21 | return copy as T[]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/data/relations/EntityRelationAnchor.ts: -------------------------------------------------------------------------------- 1 | import type { Entity } from "../entities"; 2 | 3 | export class EntityRelationAnchor { 4 | entity: Entity; 5 | cardinality?: number; 6 | 7 | /** 8 | * The name that the other entity will use to reference this entity 9 | */ 10 | reference: string; 11 | 12 | constructor(entity: Entity, name: string, cardinality?: number) { 13 | this.entity = entity; 14 | this.cardinality = cardinality; 15 | this.reference = name; 16 | } 17 | 18 | toJSON() { 19 | return { 20 | entity: this.entity.name, 21 | cardinality: this.cardinality, 22 | name: this.reference, 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/data/relations/relation-types.ts: -------------------------------------------------------------------------------- 1 | export const RelationTypes = { 2 | OneToOne: "1:1", 3 | ManyToOne: "n:1", 4 | ManyToMany: "m:n", 5 | Polymorphic: "poly", 6 | } as const; 7 | export type RelationType = (typeof RelationTypes)[keyof typeof RelationTypes]; 8 | -------------------------------------------------------------------------------- /app/src/data/schema/constructor.ts: -------------------------------------------------------------------------------- 1 | import { transformObject } from "core/utils"; 2 | import { Entity, type Field } from "data"; 3 | import { FIELDS, RELATIONS, type TAppDataEntity, type TAppDataRelation } from "data/data-schema"; 4 | 5 | export function constructEntity(name: string, entityConfig: TAppDataEntity) { 6 | const fields = transformObject(entityConfig.fields ?? {}, (fieldConfig, name) => { 7 | const { type } = fieldConfig; 8 | if (!(type in FIELDS)) { 9 | throw new Error(`Field type "${type}" not found`); 10 | } 11 | 12 | const { field } = FIELDS[type as any]; 13 | const returnal = new field(name, fieldConfig.config) as Field; 14 | return returnal; 15 | }); 16 | 17 | return new Entity( 18 | name, 19 | Object.values(fields), 20 | entityConfig.config as any, 21 | entityConfig.type as any, 22 | ); 23 | } 24 | 25 | export function constructRelation( 26 | relationConfig: TAppDataRelation, 27 | resolver: (name: Entity | string) => Entity, 28 | ) { 29 | return new RELATIONS[relationConfig.type].cls( 30 | resolver(relationConfig.source), 31 | resolver(relationConfig.target), 32 | relationConfig.config, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/src/flows/examples/simple-fetch.ts: -------------------------------------------------------------------------------- 1 | import { Flow } from "../flows/Flow"; 2 | import { FetchTask } from "../tasks/presets/FetchTask"; 3 | import { LogTask } from "../tasks/presets/LogTask"; 4 | 5 | const first = new LogTask("First", { delay: 1000 }); 6 | const second = new LogTask("Second", { delay: 1000 }); 7 | const third = new LogTask("Long Third", { delay: 2500 }); 8 | const fourth = new FetchTask("Fetch Something", { 9 | url: "https://jsonplaceholder.typicode.com/todos/1", 10 | }); 11 | const fifth = new LogTask("Task 4", { delay: 500 }); // without connection 12 | 13 | const simpleFetch = new Flow("simpleFetch", [first, second, third, fourth, fifth]); 14 | simpleFetch.task(first).asInputFor(second); 15 | simpleFetch.task(first).asInputFor(third); 16 | simpleFetch.task(fourth).asOutputFor(third); 17 | 18 | simpleFetch.setRespondingTask(fourth); 19 | 20 | export { simpleFetch }; 21 | -------------------------------------------------------------------------------- /app/src/flows/flows/executors/RuntimeExecutor.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from "../../tasks/Task"; 2 | import { $console } from "core"; 3 | 4 | export class RuntimeExecutor { 5 | async run( 6 | nextTasks: () => Task[], 7 | onDone?: (task: Task, result: Awaited>) => void, 8 | ) { 9 | const tasks = nextTasks(); 10 | if (tasks.length === 0) { 11 | return; 12 | } 13 | 14 | const promises = tasks.map(async (t) => { 15 | const result = await t.run(); 16 | onDone?.(t, result); 17 | return result; 18 | }); 19 | 20 | try { 21 | await Promise.all(promises); 22 | } catch (e) { 23 | $console.error("RuntimeExecutor: error", e); 24 | } 25 | 26 | return this.run(nextTasks, onDone); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/flows/flows/triggers/Trigger.ts: -------------------------------------------------------------------------------- 1 | import { type Static, StringEnum, parse } from "core/utils"; 2 | import type { Execution } from "../Execution"; 3 | import type { Flow } from "../Flow"; 4 | import * as tbbox from "@sinclair/typebox"; 5 | const { Type } = tbbox; 6 | 7 | export class Trigger { 8 | // @todo: remove this 9 | executions: Execution[] = []; 10 | type = "manual"; 11 | config: Static; 12 | 13 | static schema = Type.Object({ 14 | mode: StringEnum(["sync", "async"], { default: "async" }), 15 | }); 16 | 17 | constructor(config?: Partial>) { 18 | const schema = (this.constructor as typeof Trigger).schema; 19 | // @ts-ignore for now 20 | this.config = parse(schema, config ?? {}); 21 | } 22 | 23 | async register(flow: Flow, ...args: any[]): Promise { 24 | // @todo: remove this 25 | this.executions.push(await flow.start()); 26 | } 27 | 28 | toJSON() { 29 | return { 30 | type: this.type, 31 | config: this.config, 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/flows/flows/triggers/index.ts: -------------------------------------------------------------------------------- 1 | import { EventTrigger } from "./EventTrigger"; 2 | import { HttpTrigger } from "./HttpTrigger"; 3 | import { Trigger } from "./Trigger"; 4 | 5 | export { Trigger, EventTrigger, HttpTrigger }; 6 | 7 | export const TriggerMap = { 8 | manual: { cls: Trigger }, 9 | event: { cls: EventTrigger }, 10 | http: { cls: HttpTrigger }, 11 | } as const; 12 | export type TriggerMapType = typeof TriggerMap; 13 | -------------------------------------------------------------------------------- /app/src/flows/index.ts: -------------------------------------------------------------------------------- 1 | import { FetchTask } from "./tasks/presets/FetchTask"; 2 | import { LogTask } from "./tasks/presets/LogTask"; 3 | import { RenderTask } from "./tasks/presets/RenderTask"; 4 | import { SubFlowTask } from "./tasks/presets/SubFlowTask"; 5 | 6 | export { Flow } from "./flows/Flow"; 7 | export { 8 | Execution, 9 | type TaskLog, 10 | type InputsMap, 11 | ExecutionState, 12 | ExecutionEvent, 13 | } from "./flows/Execution"; 14 | export { RuntimeExecutor } from "./flows/executors/RuntimeExecutor"; 15 | export { FlowTaskConnector } from "./flows/FlowTaskConnector"; 16 | 17 | export { 18 | Trigger, 19 | EventTrigger, 20 | HttpTrigger, 21 | TriggerMap, 22 | type TriggerMapType, 23 | } from "./flows/triggers"; 24 | 25 | import { Task } from "./tasks/Task"; 26 | export type { TaskResult, TaskRenderProps } from "./tasks/Task"; 27 | export { TaskConnection, Condition } from "./tasks/TaskConnection"; 28 | 29 | export const TaskMap = { 30 | fetch: { cls: FetchTask }, 31 | log: { cls: LogTask }, 32 | render: { cls: RenderTask }, 33 | subflow: { cls: SubFlowTask }, 34 | } as const; 35 | export type TaskMapType = typeof TaskMap; 36 | 37 | export { Task, FetchTask, LogTask, RenderTask, SubFlowTask }; 38 | -------------------------------------------------------------------------------- /app/src/flows/tasks/presets/LogTask.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "../Task"; 2 | import { $console } from "core"; 3 | import * as tbbox from "@sinclair/typebox"; 4 | const { Type } = tbbox; 5 | 6 | export class LogTask extends Task { 7 | type = "log"; 8 | 9 | static override schema = Type.Object({ 10 | delay: Type.Number({ default: 10 }), 11 | }); 12 | 13 | async execute() { 14 | await new Promise((resolve) => setTimeout(resolve, this.params.delay)); 15 | $console.log(`[DONE] LogTask: ${this.name}`); 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/flows/tasks/presets/RenderTask.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "../Task"; 2 | import * as tbbox from "@sinclair/typebox"; 3 | const { Type } = tbbox; 4 | 5 | export class RenderTask> extends Task< 6 | typeof RenderTask.schema, 7 | Output 8 | > { 9 | type = "render"; 10 | 11 | static override schema = Type.Object({ 12 | render: Type.String(), 13 | }); 14 | 15 | async execute() { 16 | return this.params.render as unknown as Output; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | App, 3 | createApp, 4 | AppEvents, 5 | type AppConfig, 6 | type CreateAppConfig, 7 | type AppPlugin, 8 | type LocalApiOptions, 9 | } from "./App"; 10 | 11 | export { 12 | getDefaultConfig, 13 | getDefaultSchema, 14 | type ModuleConfigs, 15 | type ModuleSchemas, 16 | type ModuleManagerOptions, 17 | type ModuleBuildContext, 18 | type InitialModuleConfigs, 19 | } from "./modules/ModuleManager"; 20 | 21 | export * as middlewares from "modules/middlewares"; 22 | export { registries } from "modules/registries"; 23 | 24 | export type { MediaFieldSchema } from "media/AppMedia"; 25 | export type { UserFieldSchema } from "auth/AppAuth"; 26 | -------------------------------------------------------------------------------- /app/src/media/media-permissions.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from "core"; 2 | 3 | export const readFile = new Permission("media.file.read"); 4 | export const listFiles = new Permission("media.file.list"); 5 | export const uploadFile = new Permission("media.file.upload"); 6 | export const deleteFile = new Permission("media.file.delete"); 7 | -------------------------------------------------------------------------------- /app/src/media/storage/events/index.ts: -------------------------------------------------------------------------------- 1 | import { Event, InvalidEventReturn } from "core/events"; 2 | import type { FileBody, FileUploadPayload } from "../Storage"; 3 | 4 | export type FileUploadedEventData = FileUploadPayload & { 5 | file: FileBody; 6 | }; 7 | export class FileUploadedEvent extends Event { 8 | static override slug = "file-uploaded"; 9 | 10 | override validate(data: object) { 11 | if (typeof data !== "object") { 12 | throw new InvalidEventReturn("object", typeof data); 13 | } 14 | 15 | return this.clone({ 16 | // prepending result, so original is always kept 17 | ...data, 18 | ...this.params, 19 | }); 20 | } 21 | } 22 | 23 | export class FileDeletedEvent extends Event<{ name: string }> { 24 | static override slug = "file-deleted"; 25 | } 26 | 27 | export class FileAccessEvent extends Event<{ name: string }> { 28 | static override slug = "file-access"; 29 | } 30 | -------------------------------------------------------------------------------- /app/src/media/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { isFile, randomString } from "core/utils"; 2 | import { extension } from "media/storage/mime-types-tiny"; 3 | 4 | export function getExtensionFromName(filename: string): string | undefined { 5 | if (!filename.includes(".")) return; 6 | 7 | const parts = filename.split("."); 8 | return parts[parts.length - 1]?.toLowerCase(); 9 | } 10 | 11 | export function getRandomizedFilename(file: File, length?: number): string; 12 | export function getRandomizedFilename(file: string, length?: number): string; 13 | export function getRandomizedFilename(file: File | string, length = 16): string { 14 | const filename = typeof file === "string" ? file : file.name; 15 | 16 | if (typeof filename !== "string") { 17 | console.error("Couldn't extract filename from", file); 18 | throw new Error("Invalid file name"); 19 | } 20 | 21 | let ext = getExtensionFromName(filename); 22 | if (!ext && isFile(file) && file.type) { 23 | const _ext = extension(file.type); 24 | if (_ext.length > 0) ext = _ext; 25 | } 26 | 27 | // @todo: use uuid instead? 28 | return [randomString(length), ext].filter(Boolean).join("."); 29 | } 30 | -------------------------------------------------------------------------------- /app/src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import * as prototype from "data/prototype"; 2 | export { prototype }; 3 | 4 | export { AppAuth } from "auth/AppAuth"; 5 | export { AppData } from "data/AppData"; 6 | export { AppMedia, type MediaFieldSchema } from "media/AppMedia"; 7 | export { AppFlows, type AppFlowsSchema } from "flows/AppFlows"; 8 | export { 9 | type ModuleConfigs, 10 | type ModuleSchemas, 11 | MODULE_NAMES, 12 | type ModuleKey, 13 | } from "./ModuleManager"; 14 | export type { ModuleBuildContext } from "./Module"; 15 | 16 | export { 17 | type PrimaryFieldType, 18 | type BaseModuleApiOptions, 19 | type ApiResponse, 20 | ModuleApi, 21 | } from "./ModuleApi"; 22 | -------------------------------------------------------------------------------- /app/src/modules/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { auth, permission } from "auth/middlewares"; 2 | -------------------------------------------------------------------------------- /app/src/modules/permissions/index.ts: -------------------------------------------------------------------------------- 1 | import { Permission } from "core"; 2 | 3 | export const accessAdmin = new Permission("system.access.admin"); 4 | export const accessApi = new Permission("system.access.api"); 5 | export const configRead = new Permission("system.config.read"); 6 | export const configReadSecrets = new Permission("system.config.read.secrets"); 7 | export const configWrite = new Permission("system.config.write"); 8 | export const schemaRead = new Permission("system.schema.read"); 9 | export const build = new Permission("system.build"); 10 | -------------------------------------------------------------------------------- /app/src/modules/registries.ts: -------------------------------------------------------------------------------- 1 | import { MediaAdapterRegistry } from "media"; 2 | 3 | const registries = { 4 | media: MediaAdapterRegistry, 5 | } as const; 6 | 7 | export { registries }; 8 | -------------------------------------------------------------------------------- /app/src/ui/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bknd-io/bknd/7b128c97012e598b2667a4212691d0f019becd14/app/src/ui/assets/favicon.ico -------------------------------------------------------------------------------- /app/src/ui/client/bknd.ts: -------------------------------------------------------------------------------- 1 | export { BkndProvider, type BkndAdminOptions, useBknd } from "./BkndProvider"; 2 | -------------------------------------------------------------------------------- /app/src/ui/client/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ClientProvider, 3 | useBkndWindowContext, 4 | type ClientProviderProps, 5 | useApi, 6 | useBaseUrl, 7 | } from "./ClientProvider"; 8 | 9 | export * from "./api/use-api"; 10 | export * from "./api/use-entity"; 11 | export { useAuth } from "./schema/auth/use-auth"; 12 | export { Api, type TApiUser, type AuthState, type ApiOptions } from "../../Api"; 13 | export { FetchPromise } from "modules/ModuleApi"; 14 | export type { RepoQueryIn } from "data"; 15 | -------------------------------------------------------------------------------- /app/src/ui/client/schema/flows/use-flows.ts: -------------------------------------------------------------------------------- 1 | import { type Static, parse } from "core/utils"; 2 | import { type TAppFlowSchema, flowSchema } from "flows/flows-schema"; 3 | import { useBknd } from "../../BkndProvider"; 4 | 5 | export function useFlows() { 6 | const { config, app, actions: bkndActions } = useBknd(); 7 | 8 | const actions = { 9 | flow: { 10 | create: async (name: string, data: TAppFlowSchema) => { 11 | console.log("would create", name, data); 12 | const parsed = parse(flowSchema, data, { skipMark: true, forceParse: true }); 13 | console.log("parsed", parsed); 14 | const res = await bkndActions.add("flows", `flows.${name}`, parsed); 15 | console.log("res", res); 16 | }, 17 | }, 18 | }; 19 | 20 | return { flows: app.flows, config: config.flows, actions }; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/ui/client/schema/media/use-bknd-media.ts: -------------------------------------------------------------------------------- 1 | import type { TAppMediaConfig } from "media/media-schema"; 2 | import { useBknd } from "ui/client/BkndProvider"; 3 | 4 | export function useBkndMedia() { 5 | const { config, schema, actions: bkndActions } = useBknd(); 6 | 7 | const actions = { 8 | config: { 9 | patch: async (data: Partial) => { 10 | if (await bkndActions.set("media", data, true)) { 11 | await bkndActions.reload(); 12 | return true; 13 | } 14 | 15 | return false; 16 | }, 17 | }, 18 | }; 19 | const $media = {}; 20 | 21 | return { 22 | $media, 23 | config: config.media, 24 | schema: schema.media, 25 | actions, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /app/src/ui/client/schema/system/use-bknd-system.ts: -------------------------------------------------------------------------------- 1 | import { useBknd } from "ui/client/bknd"; 2 | import { useTheme } from "ui/client/use-theme"; 3 | 4 | export function useBkndSystem() { 5 | const { config, schema, actions: bkndActions } = useBknd(); 6 | const { theme } = useTheme(); 7 | 8 | const actions = { 9 | theme: { 10 | set: async (scheme: "light" | "dark") => { 11 | return await bkndActions.patch("server", "admin", { 12 | color_scheme: scheme, 13 | }); 14 | }, 15 | toggle: async () => { 16 | return await bkndActions.patch("server", "admin", { 17 | color_scheme: theme === "light" ? "dark" : "light", 18 | }); 19 | }, 20 | }, 21 | }; 22 | const $system = {}; 23 | 24 | return { 25 | $system, 26 | config: config.server, 27 | schema: schema.server, 28 | theme, 29 | actions, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /app/src/ui/components/Context.tsx: -------------------------------------------------------------------------------- 1 | import { useBaseUrl } from "../client/ClientProvider"; 2 | 3 | export function Context() { 4 | const baseurl = useBaseUrl(); 5 | 6 | return ( 7 |
8 | {JSON.stringify( 9 | { 10 | baseurl, 11 | }, 12 | null, 13 | 2, 14 | )} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/ui/components/code/HtmlEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | import type { CodeEditorProps } from "./CodeEditor"; 5 | const CodeEditor = lazy(() => import("./CodeEditor")); 6 | 7 | export function HtmlEditor({ editable, ...props }: CodeEditorProps) { 8 | return ( 9 | 10 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/ui/components/code/JsonEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | import type { CodeEditorProps } from "./CodeEditor"; 4 | const CodeEditor = lazy(() => import("./CodeEditor")); 5 | 6 | export function JsonEditor({ editable, className, ...props }: CodeEditorProps) { 7 | return ( 8 | 9 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/ui/components/display/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { TbAlertCircle } from "react-icons/tb"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export type IconProps = { 5 | className?: string; 6 | title?: string; 7 | }; 8 | 9 | const Warning = ({ className, ...props }: IconProps) => ( 10 | 14 | ); 15 | 16 | export const Icon = { 17 | Warning, 18 | }; 19 | -------------------------------------------------------------------------------- /app/src/ui/components/display/Message.tsx: -------------------------------------------------------------------------------- 1 | import { IconLockAccessOff } from "@tabler/icons-react"; 2 | import { Empty, type EmptyProps } from "./Empty"; 3 | 4 | const NotFound = (props: Partial) => ; 5 | const NotAllowed = (props: Partial) => ; 6 | const MissingPermission = ({ 7 | what, 8 | ...props 9 | }: Partial & { 10 | what?: string; 11 | }) => ( 12 | 18 | ); 19 | const NotEnabled = (props: Partial) => ; 20 | 21 | export const Message = { 22 | NotFound, 23 | NotAllowed, 24 | NotEnabled, 25 | MissingPermission, 26 | }; 27 | -------------------------------------------------------------------------------- /app/src/ui/components/form/Formy/BooleanInputMantine.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "@mantine/core"; 2 | import { forwardRef, useEffect, useState } from "react"; 3 | 4 | export const BooleanInputMantine = forwardRef>( 5 | (props, ref) => { 6 | const [checked, setChecked] = useState(Boolean(props.value ?? props.defaultValue)); 7 | 8 | useEffect(() => { 9 | console.log("value change", props.value); 10 | setChecked(Boolean(props.value)); 11 | }, [props.value]); 12 | 13 | function handleCheck(e) { 14 | setChecked(e.target.checked); 15 | props.onChange?.(e.target.checked); 16 | } 17 | 18 | return ( 19 |
20 | 27 |
28 | ); 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /app/src/ui/components/form/Formy/index.ts: -------------------------------------------------------------------------------- 1 | import { BooleanInputMantine } from "./BooleanInputMantine"; 2 | import { DateInput, Input, Textarea } from "./components"; 3 | 4 | export const formElementFactory = (element: string, props: any) => { 5 | switch (element) { 6 | case "date": 7 | return DateInput; 8 | case "boolean": 9 | return BooleanInputMantine; 10 | case "textarea": 11 | return Textarea; 12 | default: 13 | return Input; 14 | } 15 | }; 16 | 17 | export * from "./components"; 18 | -------------------------------------------------------------------------------- /app/src/ui/components/form/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import type { ElementProps } from "@mantine/core"; 2 | import { TbSearch } from "react-icons/tb"; 3 | 4 | export const SearchInput = (props: ElementProps<"input">) => ( 5 |
6 |
7 | 8 |
9 | 15 |
16 | ); 17 | -------------------------------------------------------------------------------- /app/src/ui/components/form/SegmentedControl.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Input, 3 | SegmentedControl as MantineSegmentedControl, 4 | type SegmentedControlProps as MantineSegmentedControlProps, 5 | } from "@mantine/core"; 6 | 7 | type SegmentedControlProps = MantineSegmentedControlProps & { 8 | label?: string; 9 | description?: string; 10 | }; 11 | 12 | export function SegmentedControl({ label, description, size, ...props }: SegmentedControlProps) { 13 | return ( 14 | 15 | {label && ( 16 |
17 | {label} 18 | {description && {description}} 19 |
20 | )} 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/ui/components/form/hook-form-mantine/MantineNumberInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | NumberInput as $NumberInput, 3 | type NumberInputProps as $NumberInputProps, 4 | } from "@mantine/core"; 5 | import { type FieldValues, type UseControllerProps, useController } from "react-hook-form"; 6 | 7 | export type MantineNumberInputProps = UseControllerProps & 8 | Omit<$NumberInputProps, "value" | "defaultValue">; 9 | 10 | export function MantineNumberInput({ 11 | name, 12 | control, 13 | defaultValue, 14 | rules, 15 | shouldUnregister, 16 | onChange, 17 | ...props 18 | }: MantineNumberInputProps) { 19 | const { 20 | field: { value, onChange: fieldOnChange, ...field }, 21 | fieldState, 22 | } = useController({ 23 | name, 24 | control, 25 | defaultValue, 26 | rules, 27 | shouldUnregister, 28 | }); 29 | 30 | return ( 31 | <$NumberInput 32 | value={value} 33 | onChange={(e) => { 34 | fieldOnChange(e); 35 | onChange?.(e); 36 | }} 37 | error={fieldState.error?.message} 38 | {...field} 39 | {...props} 40 | /> 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/src/ui/components/form/hook-form-mantine/MantineSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as $Switch, type SwitchProps as $SwitchProps } from "@mantine/core"; 2 | import { type FieldValues, type UseControllerProps, useController } from "react-hook-form"; 3 | 4 | export type SwitchProps = UseControllerProps & 5 | Omit<$SwitchProps, "value" | "checked" | "defaultValue">; 6 | 7 | export function MantineSwitch({ 8 | name, 9 | control, 10 | defaultValue, 11 | rules, 12 | shouldUnregister, 13 | onChange, 14 | ...props 15 | }: SwitchProps) { 16 | const { 17 | field: { value, onChange: fieldOnChange, ...field }, 18 | fieldState, 19 | } = useController({ 20 | name, 21 | control, 22 | defaultValue, 23 | rules, 24 | shouldUnregister, 25 | }); 26 | 27 | return ( 28 | <$Switch 29 | value={value} 30 | checked={value} 31 | onChange={(e) => { 32 | fieldOnChange(e); 33 | onChange?.(e); 34 | }} 35 | error={fieldState.error?.message} 36 | {...field} 37 | {...props} 38 | /> 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema-form/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Field"; 2 | export * from "./Form"; 3 | export * from "./ObjectField"; 4 | export * from "./ArrayField"; 5 | export * from "./AnyOfField"; 6 | export * from "./FieldWrapper"; 7 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/fields/HtmlField.tsx: -------------------------------------------------------------------------------- 1 | import type { FieldProps } from "@rjsf/utils"; 2 | import { Label } from "../templates/FieldTemplate"; 3 | import { HtmlEditor } from "ui/components/code/HtmlEditor"; 4 | 5 | // @todo: move editor to lazy loading component 6 | export default function HtmlField({ 7 | formData, 8 | onChange, 9 | disabled, 10 | readonly, 11 | ...props 12 | }: FieldProps) { 13 | function handleChange(data) { 14 | onChange(data); 15 | } 16 | 17 | const isDisabled = disabled || readonly; 18 | const id = props.idSchema.$id; 19 | 20 | return ( 21 |
22 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/fields/JsonField.tsx: -------------------------------------------------------------------------------- 1 | import type { FieldProps } from "@rjsf/utils"; 2 | import { JsonEditor } from "../../../code/JsonEditor"; 3 | import { Label } from "../templates/FieldTemplate"; 4 | 5 | // @todo: move editor to lazy loading component 6 | export default function JsonField({ 7 | formData, 8 | onChange, 9 | disabled, 10 | readonly, 11 | ...props 12 | }: FieldProps) { 13 | const value = JSON.stringify(formData, null, 2); 14 | 15 | function handleChange(data) { 16 | try { 17 | onChange(JSON.parse(data)); 18 | } catch (err) { 19 | console.error(err); 20 | } 21 | } 22 | 23 | const isDisabled = disabled || readonly; 24 | const id = props.idSchema.$id; 25 | 26 | return ( 27 |
28 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/fields/index.ts: -------------------------------------------------------------------------------- 1 | import JsonField from "./JsonField"; 2 | import MultiSchemaField from "./MultiSchemaField"; 3 | import HtmlField from "./HtmlField"; 4 | 5 | export const fields = { 6 | AnyOfField: MultiSchemaField, 7 | OneOfField: MultiSchemaField, 8 | JsonField, 9 | HtmlField, 10 | }; 11 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/index.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, forwardRef, lazy } from "react"; 2 | import type { JsonSchemaFormProps, JsonSchemaFormRef } from "./JsonSchemaForm"; 3 | 4 | export type { JsonSchemaFormProps, JsonSchemaFormRef }; 5 | 6 | const Module = lazy(() => 7 | import("./JsonSchemaForm").then((m) => ({ 8 | default: m.JsonSchemaForm, 9 | })), 10 | ); 11 | 12 | export const JsonSchemaForm = forwardRef((props, ref) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/templates/ButtonTemplates.tsx: -------------------------------------------------------------------------------- 1 | import { TbArrowDown, TbArrowUp, TbPlus, TbTrash } from "react-icons/tb"; 2 | import { Button } from "../../../buttons/Button"; 3 | import { IconButton } from "../../../buttons/IconButton"; 4 | 5 | export const AddButton = ({ onClick, disabled, ...rest }) => ( 6 |
7 | 10 |
11 | ); 12 | 13 | export const RemoveButton = ({ onClick, disabled, ...rest }) => ( 14 |
15 | 16 |
17 | ); 18 | 19 | export const MoveUpButton = ({ onClick, disabled, ...rest }) => ( 20 |
21 | 22 |
23 | ); 24 | 25 | export const MoveDownButton = ({ onClick, disabled, ...rest }) => ( 26 |
27 | 28 |
29 | ); 30 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/templates/TitleFieldTemplate.tsx: -------------------------------------------------------------------------------- 1 | import type { FormContextType, RJSFSchema, StrictRJSFSchema, TitleFieldProps } from "@rjsf/utils"; 2 | import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils"; 3 | 4 | const REQUIRED_FIELD_SYMBOL = "*"; 5 | 6 | /** The `TitleField` is the template to use to render the title of a field 7 | * 8 | * @param props - The `TitleFieldProps` for this component 9 | */ 10 | export default function TitleField< 11 | T = any, 12 | S extends StrictRJSFSchema = RJSFSchema, 13 | F extends FormContextType = any, 14 | >(props: TitleFieldProps) { 15 | const { id, title, required } = props; 16 | return ( 17 | 18 | {ucFirstAllSnakeToPascalWithSpaces(title)} 19 | {/*{required && {REQUIRED_FIELD_SYMBOL}}*/} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/templates/index.ts: -------------------------------------------------------------------------------- 1 | import ArrayFieldItemTemplate from "./ArrayFieldItemTemplate"; 2 | import ArrayFieldTemplate from "./ArrayFieldTemplate"; 3 | import BaseInputTemplate from "./BaseInputTemplate"; 4 | import * as ButtonTemplates from "./ButtonTemplates"; 5 | import { FieldTemplate } from "./FieldTemplate"; 6 | import ObjectFieldTemplate from "./ObjectFieldTemplate"; 7 | import TitleFieldTemplate from "./TitleFieldTemplate"; 8 | import WrapIfAdditionalTemplate from "./WrapIfAdditionalTemplate"; 9 | 10 | export const templates = { 11 | ButtonTemplates, 12 | ArrayFieldItemTemplate, 13 | ArrayFieldTemplate, 14 | FieldTemplate, 15 | TitleFieldTemplate, 16 | ObjectFieldTemplate, 17 | BaseInputTemplate, 18 | WrapIfAdditionalTemplate, 19 | }; 20 | -------------------------------------------------------------------------------- /app/src/ui/components/form/json-schema/widgets/JsonWidget.tsx: -------------------------------------------------------------------------------- 1 | import type { WidgetProps } from "@rjsf/utils"; 2 | import { useState } from "react"; 3 | 4 | export default function JsonWidget({ value, onChange, disabled, readonly, ...props }: WidgetProps) { 5 | const [val, setVal] = useState(JSON.stringify(value, null, 2)); 6 | 7 | function handleChange(e) { 8 | setVal(e.target.value); 9 | try { 10 | onChange(JSON.parse(e.target.value)); 11 | } catch (err) { 12 | console.error(err); 13 | } 14 | } 15 | 16 | return ( 17 |