akanjs 0.0.1 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +54 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.ko.md +244 -0
- package/README.md +237 -6
- package/base/base.ts +194 -0
- package/base/baseEnv.ts +162 -0
- package/base/index.ts +6 -0
- package/base/primitiveRegistry.ts +342 -0
- package/base/symbols.ts +11 -0
- package/base/types.ts +137 -0
- package/base/utils.ts +30 -0
- package/cli/application/application.command.ts +213 -0
- package/cli/application/application.interface.tsx +225 -0
- package/cli/application/application.runner.ts +359 -0
- package/cli/application/application.script.ts +244 -0
- package/cli/build.ts +25 -0
- package/cli/cloud/cloud.command.ts +48 -0
- package/cli/cloud/cloud.runner.ts +158 -0
- package/cli/cloud/cloud.script.ts +40 -0
- package/cli/guideline/guideline.command.ts +31 -0
- package/cli/guideline/guideline.prompt.ts +212 -0
- package/cli/guideline/guideline.runner.ts +53 -0
- package/cli/guideline/guideline.script.ts +33 -0
- package/cli/guidelines/___library/sharedUiStructureDescription.en.md +767 -0
- package/cli/guidelines/___library/utilUiStructureDescription.en.md +395 -0
- package/cli/guidelines/___lint/lintRuleDescription.en.md +64 -0
- package/cli/guidelines/___module/moduleStructureDescription.en.md +80 -0
- package/cli/guidelines/componentRule/componentRule.generate.json +131 -0
- package/cli/guidelines/componentRule/componentRule.instruction.md +637 -0
- package/cli/guidelines/cssRule/cssRule.generate.json +93 -0
- package/cli/guidelines/cssRule/cssRule.instruction.md +435 -0
- package/cli/guidelines/databaseModule/databaseModule.generate.json +51 -0
- package/cli/guidelines/databaseModule/databaseModule.instruction.md +612 -0
- package/cli/guidelines/docPageRule/docPageRule.generate.json +72 -0
- package/cli/guidelines/docPageRule/docPageRule.instruction.md +389 -0
- package/cli/guidelines/enumConstant/enumConstant.generate.json +24 -0
- package/cli/guidelines/enumConstant/enumConstant.instruction.md +232 -0
- package/cli/guidelines/framework/framework.generate.json +119 -0
- package/cli/guidelines/framework/framework.instruction.md +1110 -0
- package/cli/guidelines/howToUseStore/howToUseStore.generate.json +0 -0
- package/cli/guidelines/modelConstant/modelConstant.generate.json +128 -0
- package/cli/guidelines/modelConstant/modelConstant.instruction.md +506 -0
- package/cli/guidelines/modelDictionary/modelDictionary.generate.json +108 -0
- package/cli/guidelines/modelDictionary/modelDictionary.instruction.md +547 -0
- package/cli/guidelines/modelDocument/modelDocument.generate.json +129 -0
- package/cli/guidelines/modelDocument/modelDocument.instruction.md +536 -0
- package/cli/guidelines/modelService/modelService.generate.json +135 -0
- package/cli/guidelines/modelService/modelService.instruction.md +708 -0
- package/cli/guidelines/modelSignal/modelSignal.generate.json +201 -0
- package/cli/guidelines/modelSignal/modelSignal.instruction.md +552 -0
- package/cli/guidelines/modelStore/modelStore.generate.json +130 -0
- package/cli/guidelines/modelStore/modelStore.instruction.md +585 -0
- package/cli/guidelines/modelTemplate/modelTemplate.generate.json +104 -0
- package/cli/guidelines/modelTemplate/modelTemplate.instruction.md +604 -0
- package/cli/guidelines/modelUnit/modelUnit.generate.json +92 -0
- package/cli/guidelines/modelUnit/modelUnit.instruction.md +541 -0
- package/cli/guidelines/modelUtil/modelUtil.generate.json +93 -0
- package/cli/guidelines/modelUtil/modelUtil.instruction.md +752 -0
- package/cli/guidelines/modelView/modelView.generate.json +80 -0
- package/cli/guidelines/modelView/modelView.instruction.md +1005 -0
- package/cli/guidelines/modelZone/modelZone.generate.json +126 -0
- package/cli/guidelines/modelZone/modelZone.instruction.md +528 -0
- package/cli/guidelines/scalarConstant/scalarConstant.generate.json +106 -0
- package/cli/guidelines/scalarConstant/scalarConstant.instruction.md +442 -0
- package/cli/guidelines/scalarDictionary/scalarDictionary.generate.json +83 -0
- package/cli/guidelines/scalarDictionary/scalarDictionary.instruction.md +273 -0
- package/cli/guidelines/scalarModule/scalarModule.generate.json +35 -0
- package/cli/guidelines/scalarModule/scalarModule.instruction.md +81 -0
- package/cli/guidelines/sharedUiUsage/sharedUiUsage.generate.json +108 -0
- package/cli/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +310 -0
- package/cli/guidelines/utilUiUsage/utilUiUsage.generate.json +140 -0
- package/cli/guidelines/utilUiUsage/utilUiUsage.instruction.md +339 -0
- package/cli/index.js +11401 -0
- package/cli/index.ts +24 -0
- package/cli/library/library.command.ts +28 -0
- package/cli/library/library.runner.ts +69 -0
- package/cli/library/library.script.ts +39 -0
- package/cli/module/module.command.ts +35 -0
- package/cli/module/module.prompt.ts +18 -0
- package/cli/module/module.request.ts +319 -0
- package/cli/module/module.runner.ts +76 -0
- package/cli/module/module.script.ts +161 -0
- package/cli/package/package.command.ts +32 -0
- package/cli/package/package.runner.ts +64 -0
- package/cli/package/package.script.ts +35 -0
- package/cli/page/page.command.ts +14 -0
- package/cli/page/page.runner.ts +14 -0
- package/cli/page/page.script.ts +12 -0
- package/cli/scalar/scalar.command.ts +21 -0
- package/cli/scalar/scalar.prompt.ts +131 -0
- package/cli/scalar/scalar.runner.ts +38 -0
- package/cli/scalar/scalar.script.ts +16 -0
- package/cli/templates/__scalar/__model__/__model__.constant.ts +15 -0
- package/cli/templates/__scalar/__model__/__model__.dictionary.ts +19 -0
- package/cli/templates/__scalar/__model__/__model__.document.ts +15 -0
- package/cli/templates/app/akan.config.ts +14 -0
- package/cli/templates/app/capacitor.config.ts.template +8 -0
- package/cli/templates/app/common/commonLogic.ts +12 -0
- package/cli/templates/app/common/index.ts +10 -0
- package/cli/templates/app/env/env.client.debug.ts.template +7 -0
- package/cli/templates/app/env/env.client.develop.ts.template +7 -0
- package/cli/templates/app/env/env.client.local.ts.template +7 -0
- package/cli/templates/app/env/env.client.main.ts.template +7 -0
- package/cli/templates/app/env/env.client.testing.ts.template +7 -0
- package/cli/templates/app/env/env.client.ts +21 -0
- package/cli/templates/app/env/env.client.type.ts +16 -0
- package/cli/templates/app/env/env.server.debug.ts.template +7 -0
- package/cli/templates/app/env/env.server.develop.ts.template +9 -0
- package/cli/templates/app/env/env.server.local.ts.template +14 -0
- package/cli/templates/app/env/env.server.main.ts.template +9 -0
- package/cli/templates/app/env/env.server.testing.ts.template +6 -0
- package/cli/templates/app/env/env.server.ts +20 -0
- package/cli/templates/app/lib/___appName__/__appName__.dictionary.ts +17 -0
- package/cli/templates/app/lib/___appName__/__appName__.service.ts +13 -0
- package/cli/templates/app/lib/___appName__/__appName__.signal.ts +17 -0
- package/cli/templates/app/lib/___appName__/__appName__.store.ts +15 -0
- package/cli/templates/app/lib/option.ts +17 -0
- package/cli/templates/app/main.ts +16 -0
- package/cli/templates/app/package.json.template +5 -0
- package/cli/templates/app/page/_index.tsx +172 -0
- package/cli/templates/app/page/_layout.tsx +32 -0
- package/cli/templates/app/page/styles.css.template +53 -0
- package/cli/templates/app/public/favicon.ico +0 -0
- package/cli/templates/app/public/logo.png +0 -0
- package/cli/templates/app/srvkit/backendLogic.ts +12 -0
- package/cli/templates/app/srvkit/index.ts +10 -0
- package/cli/templates/app/tsconfig.json.template +21 -0
- package/cli/templates/app/ui/UiComponent.ts +16 -0
- package/cli/templates/app/ui/index.ts +10 -0
- package/cli/templates/app/webkit/frontendLogic.ts +12 -0
- package/cli/templates/app/webkit/index.ts +10 -0
- package/cli/templates/client.ts +46 -0
- package/cli/templates/crudPages/[__model__Id]/edit/page.tsx +47 -0
- package/cli/templates/crudPages/[__model__Id]/page.tsx +52 -0
- package/cli/templates/crudPages/new/page.tsx +42 -0
- package/cli/templates/crudPages/page.tsx +43 -0
- package/cli/templates/crudSinglePage/page.tsx +37 -0
- package/cli/templates/env/_env.server.type.ts +16 -0
- package/cli/templates/facetIndex/index.ts +32 -0
- package/cli/templates/index.ts +10 -0
- package/cli/templates/lib/__lib/lib.constant.ts +38 -0
- package/cli/templates/lib/__lib/lib.dictionary.ts +33 -0
- package/cli/templates/lib/__lib/lib.document.ts +35 -0
- package/cli/templates/lib/__lib/lib.service.ts +33 -0
- package/cli/templates/lib/__lib/lib.signal.ts +34 -0
- package/cli/templates/lib/__lib/lib.store.ts +33 -0
- package/cli/templates/lib/cnst.ts +29 -0
- package/cli/templates/lib/db.ts +32 -0
- package/cli/templates/lib/dict.ts +49 -0
- package/cli/templates/lib/sig.ts +60 -0
- package/cli/templates/lib/srv.ts +34 -0
- package/cli/templates/lib/st.ts +31 -0
- package/cli/templates/lib/useClient.ts +19 -0
- package/cli/templates/lib/useServer.ts +7 -0
- package/cli/templates/libRoot/.gitignore.template +16 -0
- package/cli/templates/libRoot/akan.config.ts +12 -0
- package/cli/templates/libRoot/base/baseLogic.ts +10 -0
- package/cli/templates/libRoot/base/index.ts +8 -0
- package/cli/templates/libRoot/common/commonLogic.ts +10 -0
- package/cli/templates/libRoot/common/index.ts +8 -0
- package/cli/templates/libRoot/env/env.server.example.ts.template +6 -0
- package/cli/templates/libRoot/env/env.server.testing.ts.template +6 -0
- package/cli/templates/libRoot/lib/___libName__/__libName__.dictionary.ts +15 -0
- package/cli/templates/libRoot/lib/___libName__/__libName__.service.ts +11 -0
- package/cli/templates/libRoot/lib/___libName__/__libName__.store.ts +13 -0
- package/cli/templates/libRoot/lib/option.ts +17 -0
- package/cli/templates/libRoot/package.json.template +5 -0
- package/cli/templates/libRoot/srvkit/backendLogic.ts +10 -0
- package/cli/templates/libRoot/srvkit/index.ts +8 -0
- package/cli/templates/libRoot/tsconfig.json.template +11 -0
- package/cli/templates/libRoot/ui/index.ts +7 -0
- package/cli/templates/libRoot/webkit/frontendLogic.ts +10 -0
- package/cli/templates/libRoot/webkit/index.ts +8 -0
- package/cli/templates/localDev/docker-compose.yaml.template +37 -0
- package/cli/templates/module/__Model__.Template.tsx +36 -0
- package/cli/templates/module/__Model__.Unit.tsx +26 -0
- package/cli/templates/module/__Model__.Util.tsx +30 -0
- package/cli/templates/module/__Model__.View.tsx +29 -0
- package/cli/templates/module/__Model__.Zone.tsx +43 -0
- package/cli/templates/module/__model__.constant.ts +24 -0
- package/cli/templates/module/__model__.dictionary.ts +30 -0
- package/cli/templates/module/__model__.document.ts +23 -0
- package/cli/templates/module/__model__.service.ts +16 -0
- package/cli/templates/module/__model__.signal.ts +27 -0
- package/cli/templates/module/__model__.store.ts +19 -0
- package/cli/templates/module/index.tsx +44 -0
- package/cli/templates/moduleRoot/index.tsx +31 -0
- package/cli/templates/pkgRoot/tsconfig.json.template +10 -0
- package/cli/templates/server.ts +45 -0
- package/cli/templates/workspaceRoot/.env.template +14 -0
- package/cli/templates/workspaceRoot/.gitignore.template +130 -0
- package/cli/templates/workspaceRoot/.vscode/settings.json.template +23 -0
- package/cli/templates/workspaceRoot/biome.json.template +175 -0
- package/cli/templates/workspaceRoot/bunfig.toml +4 -0
- package/cli/templates/workspaceRoot/infra/app/Chart.yaml.template +6 -0
- package/cli/templates/workspaceRoot/infra/app/templates/frontend.yaml.template +182 -0
- package/cli/templates/workspaceRoot/infra/app/values/_common-values.yaml.template +183 -0
- package/cli/templates/workspaceRoot/package.json.template +7 -0
- package/cli/templates/workspaceRoot/tsconfig.json.template +26 -0
- package/cli/testHelpers.ts +171 -0
- package/cli/workspace/workspace.command.ts +60 -0
- package/cli/workspace/workspace.runner.ts +57 -0
- package/cli/workspace/workspace.script.ts +68 -0
- package/client/capacitor.ts +68 -0
- package/client/clientRuntime.ts +141 -0
- package/client/cookie.ts +121 -0
- package/client/createFont.ts +8 -0
- package/client/csrTypes.ts +259 -0
- package/client/decorators.ts +25 -0
- package/client/device.ts +147 -0
- package/client/fetch.ts +1 -0
- package/client/index.ts +15 -0
- package/client/locale.ts +4 -0
- package/client/makePageProto.tsx +178 -0
- package/client/router.ts +286 -0
- package/client/rscNavigation.ts +19 -0
- package/client/storage.ts +33 -0
- package/client/translator.ts +43 -0
- package/client/types.ts +228 -0
- package/client/useClient.ts +1 -0
- package/common/Logger.ts +177 -0
- package/common/applyMixins.ts +14 -0
- package/common/capitalize.ts +4 -0
- package/common/deepObjectify.ts +27 -0
- package/common/formatNumber.ts +14 -0
- package/common/formatPhone.ts +7 -0
- package/common/getAllPropertyDescriptors.ts +13 -0
- package/common/hmrPhase.ts +17 -0
- package/common/httpClient.ts +101 -0
- package/common/index.ts +50 -0
- package/common/isDayjs.ts +3 -0
- package/common/isEmail.ts +2 -0
- package/common/isPhoneNumber.ts +8 -0
- package/common/isQueryEqual.ts +23 -0
- package/common/isValidDate.ts +14 -0
- package/common/localeConfig.ts +41 -0
- package/common/lowerlize.ts +4 -0
- package/common/mergeVersion.ts +10 -0
- package/common/objectify.ts +7 -0
- package/common/pathGet.ts +13 -0
- package/common/pathSet.ts +16 -0
- package/common/randomPick.ts +1 -0
- package/common/randomPicks.ts +12 -0
- package/common/routeConvention.ts +203 -0
- package/common/sleep.ts +8 -0
- package/common/splitVersion.ts +11 -0
- package/common/subRoute.ts +34 -0
- package/common/types.ts +15 -0
- package/constant/constantRegistry.ts +334 -0
- package/constant/crystalize.ts +45 -0
- package/constant/deserialize.ts +70 -0
- package/constant/fieldInfo.ts +415 -0
- package/constant/getDefault.ts +18 -0
- package/constant/immerify.ts +16 -0
- package/constant/index.ts +10 -0
- package/constant/purify.ts +118 -0
- package/constant/serialize.ts +87 -0
- package/constant/types.ts +105 -0
- package/constant/via.ts +360 -0
- package/devkit/aiEditor.ts +299 -0
- package/devkit/akanApp/akanApp.host.ts +356 -0
- package/devkit/akanApp/index.ts +1 -0
- package/devkit/akanConfig/akanConfig.ts +332 -0
- package/devkit/akanConfig/index.ts +2 -0
- package/devkit/akanConfig/types.ts +187 -0
- package/devkit/applicationBuildReporter.ts +69 -0
- package/devkit/applicationBuildRunner.ts +302 -0
- package/devkit/applicationReleasePackager.ts +205 -0
- package/devkit/artifact/implicitRootLayout.ts +155 -0
- package/devkit/artifact/index.ts +1 -0
- package/devkit/artifact/routeSeedIndex.ts +128 -0
- package/devkit/auth.ts +41 -0
- package/devkit/builder.ts +164 -0
- package/devkit/capacitor.base.config.ts +74 -0
- package/devkit/capacitorApp.ts +385 -0
- package/devkit/commandDecorators/argMeta.ts +88 -0
- package/devkit/commandDecorators/command.ts +314 -0
- package/devkit/commandDecorators/commandBuilder.ts +188 -0
- package/devkit/commandDecorators/commandMeta.ts +6 -0
- package/devkit/commandDecorators/dependencyBuilder.ts +100 -0
- package/devkit/commandDecorators/helpFormatter.ts +217 -0
- package/devkit/commandDecorators/index.ts +8 -0
- package/devkit/commandDecorators/targetMeta.ts +31 -0
- package/devkit/commandDecorators/types.ts +10 -0
- package/devkit/constants.ts +25 -0
- package/devkit/createTunnel.ts +36 -0
- package/devkit/dependencyScanner.ts +339 -0
- package/devkit/executors.ts +1326 -0
- package/devkit/extractDeps.ts +85 -0
- package/devkit/fileEditor.ts +106 -0
- package/devkit/fileSys.ts +39 -0
- package/devkit/frontendBuild/allRoutesBuilder.ts +103 -0
- package/devkit/frontendBuild/clientBuildTypes.ts +114 -0
- package/devkit/frontendBuild/clientEntriesBundler.ts +300 -0
- package/devkit/frontendBuild/clientEntryDiscovery.ts +196 -0
- package/devkit/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/devkit/frontendBuild/cssCompiler.ts +279 -0
- package/devkit/frontendBuild/cssImportResolver.ts +116 -0
- package/devkit/frontendBuild/fontOptimizer.ts +427 -0
- package/devkit/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/devkit/frontendBuild/hmrWatcher.ts +101 -0
- package/devkit/frontendBuild/index.ts +18 -0
- package/devkit/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/devkit/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/devkit/frontendBuild/precompressArtifacts.ts +59 -0
- package/devkit/frontendBuild/routeClientBuilder.ts +290 -0
- package/devkit/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/devkit/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/devkit/frontendBuild/vendorSpecifiers.ts +16 -0
- package/devkit/frontendBuild/watchRootResolver.ts +28 -0
- package/devkit/getCredentials.ts +19 -0
- package/devkit/getDirname.ts +3 -0
- package/devkit/getModelFileData.ts +59 -0
- package/devkit/getRelatedCnsts.ts +300 -0
- package/devkit/guideline.ts +19 -0
- package/devkit/incrementalBuilder/incrementalBuilder.host.ts +78 -0
- package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +330 -0
- package/devkit/incrementalBuilder/index.ts +1 -0
- package/devkit/index.ts +36 -0
- package/devkit/lint/no-import-client-functions.grit +32 -0
- package/devkit/lint/no-import-external-library.grit +21 -0
- package/devkit/lint/no-js-private-class-method.grit +42 -0
- package/devkit/lint/no-use-client-in-server.grit +7 -0
- package/devkit/lint/non-scalar-props-restricted.grit +13 -0
- package/devkit/linter.ts +248 -0
- package/devkit/mobile/index.ts +1 -0
- package/devkit/mobile/mobileTarget.ts +47 -0
- package/devkit/prompter.ts +71 -0
- package/devkit/scanInfo.ts +605 -0
- package/devkit/selectModel.ts +11 -0
- package/devkit/spinner.ts +54 -0
- package/devkit/sshTunnel.ts +151 -0
- package/devkit/streamAi.ts +45 -0
- package/devkit/transforms/barrelAnalyzer.ts +249 -0
- package/devkit/transforms/barrelImportsPlugin.ts +451 -0
- package/devkit/transforms/externalizeFrameworkPlugin.ts +159 -0
- package/devkit/transforms/index.ts +5 -0
- package/devkit/transforms/rscUseClientTransform.ts +52 -0
- package/devkit/transforms/useClientBundlePlugin.ts +47 -0
- package/devkit/typeChecker.ts +260 -0
- package/devkit/types.ts +42 -0
- package/devkit/ui/MultiScrollList.tsx +228 -0
- package/devkit/ui/ScrollList.tsx +106 -0
- package/devkit/ui/index.ts +2 -0
- package/devkit/uploadRelease.ts +95 -0
- package/devkit/useStdoutDimensions.ts +20 -0
- package/dictionary/base.dictionary.ts +91 -0
- package/dictionary/dictInfo.ts +1079 -0
- package/dictionary/dictionary.ts +10 -0
- package/dictionary/index.ts +12 -0
- package/dictionary/locale.ts +230 -0
- package/dictionary/trans.ts +196 -0
- package/document/by.ts +39 -0
- package/document/dataLoader.ts +91 -0
- package/document/database.ts +168 -0
- package/document/databaseRegistry.ts +113 -0
- package/document/documentQuery.ts +143 -0
- package/document/documentSchema.ts +91 -0
- package/document/filterMeta.ts +210 -0
- package/document/index.ts +11 -0
- package/document/into.ts +169 -0
- package/document/loaderInfo.ts +50 -0
- package/document/schema.ts +24 -0
- package/document/types.ts +41 -0
- package/fetch/client/fetchClient.ts +600 -0
- package/fetch/client/httpClient.ts +157 -0
- package/fetch/client/index.ts +3 -0
- package/fetch/client/wsClient.ts +246 -0
- package/fetch/fetchType/appliedReturn.type.ts +108 -0
- package/fetch/fetchType/buildFetch.type.ts +24 -0
- package/fetch/fetchType/clientSignal.type.ts +22 -0
- package/fetch/fetchType/endpointFetch.type.ts +56 -0
- package/fetch/fetchType/index.ts +5 -0
- package/fetch/fetchType/sliceFetch.type.ts +173 -0
- package/fetch/index.ts +4 -0
- package/fetch/requestStorage.ts +127 -0
- package/fetch/serializer/fetch.serializer.ts +131 -0
- package/fetch/serializer/index.ts +1 -0
- package/fetch/types.ts +28 -0
- package/package.json +202 -5
- package/server/SSR_MEMORY_DIAGNOSIS.md +107 -0
- package/server/akanApp.ts +855 -0
- package/server/akanLib.ts +43 -0
- package/server/akanOption.ts +42 -0
- package/server/akanServer.ts +369 -0
- package/server/artifact/builderRpc.ts +124 -0
- package/server/artifact/index.ts +6 -0
- package/server/artifact/ipcTypes.ts +46 -0
- package/server/artifact/manifestTypes.ts +19 -0
- package/server/artifact/routeClientCache.ts +224 -0
- package/server/artifact/routeSeedIndexStore.ts +89 -0
- package/server/artifact/routesManifestStore.ts +120 -0
- package/server/decorators.ts +95 -0
- package/server/di/diLifecycle.ts +520 -0
- package/server/di/index.ts +4 -0
- package/server/di/predefinedAdaptor.ts +77 -0
- package/server/di/resolveAdaptorHierarchy.ts +82 -0
- package/server/di/resolveHierarchy.ts +89 -0
- package/server/di/resolveServiceHierarchy.ts +47 -0
- package/server/di/utils.ts +40 -0
- package/server/hmr/changeBatch.ts +14 -0
- package/server/hmr/clientScript.ts +310 -0
- package/server/hmr/devHmrController.ts +395 -0
- package/server/hmr/index.ts +4 -0
- package/server/hmr/wsHub.ts +56 -0
- package/server/imageOptimizer.ts +427 -0
- package/server/imageOptimizerError.ts +8 -0
- package/server/index.ts +14 -0
- package/server/lifecycle/shutdownManager.ts +52 -0
- package/server/logging/rotatingLogWriter.ts +190 -0
- package/server/processMetricsCollector.ts +78 -0
- package/server/proxy/akanResponse.ts +16 -0
- package/server/proxy/bunRequestFields.ts +12 -0
- package/server/proxy/hostBasePathWebProxy.ts +112 -0
- package/server/proxy/index.ts +15 -0
- package/server/proxy/localeWebProxy.ts +71 -0
- package/server/proxy/types.ts +41 -0
- package/server/proxy/webProxyRunner.ts +90 -0
- package/server/resolver/database.resolver.ts +340 -0
- package/server/resolver/index.ts +3 -0
- package/server/resolver/resolver.contract.fixture.ts +222 -0
- package/server/resolver/service.resolver.ts +176 -0
- package/server/resolver/signal.resolver.ts +386 -0
- package/server/robots.ts +32 -0
- package/server/routeElementComposer.tsx +91 -0
- package/server/routeTreeBuilder.ts +279 -0
- package/server/routing/apiRouter.ts +226 -0
- package/server/rscClient.tsx +140 -0
- package/server/rscWorker.tsx +625 -0
- package/server/rscWorkerHost.ts +616 -0
- package/server/sitemap.ts +90 -0
- package/server/ssrFromRscRenderer.tsx +285 -0
- package/server/ssrTypes.ts +48 -0
- package/server/types/react-server-dom-webpack.d.ts +91 -0
- package/server/types.tsx +109 -0
- package/server/vendor/akanjs-base.ts +1 -0
- package/server/vendor/akanjs-common.ts +1 -0
- package/server/vendor/akanjs-constant.ts +1 -0
- package/server/vendor/akanjs-store.ts +1 -0
- package/server/vendor/react-dom-client.ts +5 -0
- package/server/vendor/react-dom.ts +21 -0
- package/server/vendor/react-jsx-dev-runtime.ts +5 -0
- package/server/vendor/react-jsx-runtime.ts +5 -0
- package/server/vendor/react-refresh-runtime.ts +6 -0
- package/server/vendor/react-server-dom-webpack-client-browser.ts +12 -0
- package/server/vendor/react.ts +51 -0
- package/server/vendor/scheduler.ts +23 -0
- package/server/webRouter.ts +712 -0
- package/service/adapt.ts +47 -0
- package/service/base.service.ts +18 -0
- package/service/index.ts +10 -0
- package/service/injectInfo.ts +393 -0
- package/service/ipcTypes.ts +127 -0
- package/service/predefinedAdaptor/cache.adaptor.ts +105 -0
- package/service/predefinedAdaptor/compress.adaptor.ts +357 -0
- package/service/predefinedAdaptor/database.adaptor.ts +1310 -0
- package/service/predefinedAdaptor/index.ts +13 -0
- package/service/predefinedAdaptor/logging.adaptor.ts +36 -0
- package/service/predefinedAdaptor/queue.adaptor.ts +42 -0
- package/service/predefinedAdaptor/role.adaptor.ts +18 -0
- package/service/predefinedAdaptor/schedule.adaptor.ts +188 -0
- package/service/predefinedAdaptor/solidCache.adaptor.ts +143 -0
- package/service/predefinedAdaptor/solidPubsub.adaptor.ts +98 -0
- package/service/predefinedAdaptor/solidQueue.adaptor.ts +203 -0
- package/service/predefinedAdaptor/solidSqlite.ts +85 -0
- package/service/predefinedAdaptor/sqlitePath.ts +20 -0
- package/service/predefinedAdaptor/storage.adaptor.ts +121 -0
- package/service/predefinedAdaptor/websocket.adaptor.ts +313 -0
- package/service/serve.ts +223 -0
- package/service/serviceModule.ts +221 -0
- package/service/serviceRegistry.ts +26 -0
- package/service/types.ts +113 -0
- package/signal/base.signal.ts +46 -0
- package/signal/endpoint.ts +122 -0
- package/signal/endpointInfo.ts +374 -0
- package/signal/exception.ts +50 -0
- package/signal/guard.ts +18 -0
- package/signal/guards.ts +16 -0
- package/signal/index.ts +18 -0
- package/signal/intercept.ts +39 -0
- package/signal/internal.ts +56 -0
- package/signal/internalArg.ts +38 -0
- package/signal/internalInfo.ts +208 -0
- package/signal/middleware.ts +108 -0
- package/signal/serializer/fetch.serializer.ts +128 -0
- package/signal/serializer/index.ts +1 -0
- package/signal/serverSignal.ts +114 -0
- package/signal/signalContext.ts +446 -0
- package/signal/signalRegistry.ts +82 -0
- package/signal/slice.ts +158 -0
- package/signal/sliceInfo.ts +254 -0
- package/signal/types.ts +131 -0
- package/store/action.ts +1067 -0
- package/store/baseSt.ts +81 -0
- package/store/hooks.ts +2 -0
- package/store/index.ts +8 -0
- package/store/rootStore.ts +26 -0
- package/store/state.ts +226 -0
- package/store/stateBuilder.ts +429 -0
- package/store/stateInfo.ts +66 -0
- package/store/store.ts +194 -0
- package/store/storeInstance.ts +382 -0
- package/store/storeRegistry.ts +84 -0
- package/store/types.ts +69 -0
- package/store/withSelector.ts +62 -0
- package/test/index.ts +16 -0
- package/test/playwright.config.base.ts +48 -0
- package/test/playwright.pageAgent.ts +41 -0
- package/test/sample.ts +19 -0
- package/test/sampleOf.ts +70 -0
- package/test/signalTest.preload.ts +10 -0
- package/test/signalTestRuntime.ts +126 -0
- package/test/testServer.ts +187 -0
- package/ui/BottomSheet.tsx +103 -0
- package/ui/Button.tsx +70 -0
- package/ui/ClientSide.tsx +11 -0
- package/ui/Clipboard.tsx +49 -0
- package/ui/Constant/Doc.tsx +696 -0
- package/ui/Constant/Mermaid.tsx +149 -0
- package/ui/Constant/index.ts +6 -0
- package/ui/Constant/schemaDoc.ts +324 -0
- package/ui/Copy.tsx +27 -0
- package/ui/CsrImage.tsx +38 -0
- package/ui/Data/CardList.tsx +141 -0
- package/ui/Data/Dashboard.tsx +72 -0
- package/ui/Data/Insight.tsx +35 -0
- package/ui/Data/Item.tsx +285 -0
- package/ui/Data/ListContainer.tsx +320 -0
- package/ui/Data/Pagination.tsx +54 -0
- package/ui/Data/QueryMaker.tsx +21 -0
- package/ui/Data/TableList.tsx +161 -0
- package/ui/Data/index.ts +11 -0
- package/ui/Data/index_.tsx +11 -0
- package/ui/DatePicker.tsx +184 -0
- package/ui/Dialog/Action.tsx +15 -0
- package/ui/Dialog/Close.tsx +22 -0
- package/ui/Dialog/Content.tsx +11 -0
- package/ui/Dialog/Modal.tsx +137 -0
- package/ui/Dialog/Provider.tsx +33 -0
- package/ui/Dialog/Title.tsx +15 -0
- package/ui/Dialog/Trigger.tsx +22 -0
- package/ui/Dialog/context.ts +20 -0
- package/ui/Dialog/index.tsx +15 -0
- package/ui/DragAction.tsx +129 -0
- package/ui/DraggableList.tsx +230 -0
- package/ui/Dropdown.tsx +51 -0
- package/ui/Empty.tsx +32 -0
- package/ui/Field.tsx +1556 -0
- package/ui/FontFace.tsx +26 -0
- package/ui/Image.tsx +185 -0
- package/ui/InfiniteScroll.tsx +67 -0
- package/ui/Input.tsx +636 -0
- package/ui/KeyboardAvoiding.tsx +62 -0
- package/ui/Layout/BottomAction.tsx +15 -0
- package/ui/Layout/BottomInset.tsx +42 -0
- package/ui/Layout/BottomTab.tsx +71 -0
- package/ui/Layout/Header.tsx +38 -0
- package/ui/Layout/LeftSider.tsx +34 -0
- package/ui/Layout/Navbar.tsx +46 -0
- package/ui/Layout/RightSider.tsx +41 -0
- package/ui/Layout/Sider.tsx +69 -0
- package/ui/Layout/Template.tsx +11 -0
- package/ui/Layout/TopLeftAction.tsx +28 -0
- package/ui/Layout/Unit.tsx +19 -0
- package/ui/Layout/View.tsx +12 -0
- package/ui/Layout/Zone.tsx +10 -0
- package/ui/Layout/index.ts +27 -0
- package/ui/Link/Back.tsx +15 -0
- package/ui/Link/Close.tsx +20 -0
- package/ui/Link/CsrLink.tsx +41 -0
- package/ui/Link/Lang.tsx +16 -0
- package/ui/Link/SsrLink.tsx +68 -0
- package/ui/Link/index.tsx +32 -0
- package/ui/Link/types.ts +47 -0
- package/ui/Load/Edit.tsx +19 -0
- package/ui/Load/Edit_Client.tsx +120 -0
- package/ui/Load/Page.tsx +40 -0
- package/ui/Load/PageCSR.tsx +39 -0
- package/ui/Load/Pagination.tsx +89 -0
- package/ui/Load/Units.tsx +330 -0
- package/ui/Load/View.tsx +115 -0
- package/ui/Load/index.ts +11 -0
- package/ui/Load/index_.tsx +6 -0
- package/ui/Loading/Area.tsx +10 -0
- package/ui/Loading/Button.tsx +17 -0
- package/ui/Loading/Input.tsx +17 -0
- package/ui/Loading/ProgressBar.tsx +14 -0
- package/ui/Loading/Skeleton.tsx +22 -0
- package/ui/Loading/Spin.tsx +22 -0
- package/ui/Loading/index.tsx +8 -0
- package/ui/Menu.tsx +273 -0
- package/ui/Modal.tsx +87 -0
- package/ui/Model/AdminPanel.tsx +51 -0
- package/ui/Model/Edit.tsx +46 -0
- package/ui/Model/EditModal.tsx +301 -0
- package/ui/Model/EditWrapper.tsx +48 -0
- package/ui/Model/LoadInit.tsx +11 -0
- package/ui/Model/New.tsx +46 -0
- package/ui/Model/NewWrapper.tsx +20 -0
- package/ui/Model/NewWrapper_Client.tsx +57 -0
- package/ui/Model/Remove.tsx +75 -0
- package/ui/Model/RemoveWrapper.tsx +38 -0
- package/ui/Model/SureToRemove.tsx +108 -0
- package/ui/Model/View.tsx +65 -0
- package/ui/Model/ViewEditModal.tsx +125 -0
- package/ui/Model/ViewModal.tsx +71 -0
- package/ui/Model/ViewWrapper.tsx +38 -0
- package/ui/Model/index.ts +33 -0
- package/ui/Model/index_.tsx +16 -0
- package/ui/More.tsx +56 -0
- package/ui/ObjectId.tsx +29 -0
- package/ui/Pagination.tsx +120 -0
- package/ui/Popconfirm.tsx +135 -0
- package/ui/Portal.tsx +10 -0
- package/ui/Radio.tsx +53 -0
- package/ui/RecentTime.tsx +123 -0
- package/ui/Refresh.tsx +24 -0
- package/ui/ScreenNavigator.tsx +129 -0
- package/ui/Select.tsx +322 -0
- package/ui/Signal/Arg.tsx +352 -0
- package/ui/Signal/Doc.tsx +231 -0
- package/ui/Signal/Listener.tsx +74 -0
- package/ui/Signal/Message.tsx +267 -0
- package/ui/Signal/Object.tsx +131 -0
- package/ui/Signal/PubSub.tsx +247 -0
- package/ui/Signal/Request.tsx +30 -0
- package/ui/Signal/Response.tsx +76 -0
- package/ui/Signal/RestApi.tsx +390 -0
- package/ui/Signal/WebSocket.tsx +47 -0
- package/ui/Signal/index.ts +10 -0
- package/ui/Signal/makeExample.ts +75 -0
- package/ui/System/CSR.tsx +420 -0
- package/ui/System/Client.tsx +296 -0
- package/ui/System/Common.tsx +83 -0
- package/ui/System/DevModeToggle.tsx +21 -0
- package/ui/System/Gtag.tsx +69 -0
- package/ui/System/Messages.tsx +175 -0
- package/ui/System/Reconnect.tsx +156 -0
- package/ui/System/Root.tsx +10 -0
- package/ui/System/SSR.tsx +163 -0
- package/ui/System/SelectLanguage.tsx +47 -0
- package/ui/System/ThemeToggle.tsx +98 -0
- package/ui/System/index.tsx +23 -0
- package/ui/Tab/Menu.tsx +56 -0
- package/ui/Tab/Menus.tsx +10 -0
- package/ui/Tab/Panel.tsx +25 -0
- package/ui/Tab/Provider.tsx +25 -0
- package/ui/Tab/context.ts +16 -0
- package/ui/Tab/index.tsx +11 -0
- package/ui/Table.tsx +131 -0
- package/ui/ToggleSelect.tsx +144 -0
- package/ui/Unauthorized.tsx +29 -0
- package/ui/animated.tsx +8 -0
- package/ui/fontCss.ts +86 -0
- package/ui/index.ts +44 -0
- package/ui/styles.css +617 -0
- package/webkit/bootCsr.tsx +275 -0
- package/webkit/createRobotPage.ts +15 -0
- package/webkit/createSitemapPage.ts +4 -0
- package/webkit/index.ts +19 -0
- package/webkit/lazy.tsx +37 -0
- package/webkit/types.ts +7 -0
- package/webkit/useCamera.tsx +99 -0
- package/webkit/useCodepush.tsx +99 -0
- package/webkit/useContact.tsx +48 -0
- package/webkit/useCsrValues.ts +661 -0
- package/webkit/useDebounce.ts +24 -0
- package/webkit/useFetch.ts +48 -0
- package/webkit/useGeoLocation.tsx +24 -0
- package/webkit/useHistory.ts +76 -0
- package/webkit/useInterval.ts +21 -0
- package/webkit/useLocation.ts +69 -0
- package/webkit/usePurchase.tsx +156 -0
- package/webkit/usePushNoti.tsx +48 -0
- package/webkit/useThrottle.ts +22 -0
- package/src/index.js +0 -2
- package/src/index.js.map +0 -1
- package/src/lib/akan2.js +0 -4
- package/src/lib/akan2.js.map +0 -1
- package/src/run.js +0 -4
- package/src/run.js.map +0 -1
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
import { Database, type SQLQueryBindings, type Statement } from "bun:sqlite";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { InArgs, InValue, Client as LibsqlClient } from "@libsql/client";
|
|
6
|
+
import { type BaseEnv, dayjs, FIELD_META, type PromiseOrObject } from "akanjs/base";
|
|
7
|
+
import type { ConstantModel } from "akanjs/constant";
|
|
8
|
+
import {
|
|
9
|
+
createDocumentId,
|
|
10
|
+
type DatabaseModel,
|
|
11
|
+
type DocumentQuery,
|
|
12
|
+
type DocumentQueryNode,
|
|
13
|
+
type DocumentSchema,
|
|
14
|
+
type DocumentUpdate,
|
|
15
|
+
type DocumentUpdateOptions,
|
|
16
|
+
documentQueryHelper,
|
|
17
|
+
encodeDocumentValue,
|
|
18
|
+
type SchemaOf,
|
|
19
|
+
sanitizeJson,
|
|
20
|
+
} from "akanjs/document";
|
|
21
|
+
import type { Sql } from "postgres";
|
|
22
|
+
import { adapt } from "../adapt";
|
|
23
|
+
import { resolveDefaultSqliteFile } from "./sqlitePath";
|
|
24
|
+
|
|
25
|
+
export interface SqliteDatabaseConfig {
|
|
26
|
+
filePath?: string;
|
|
27
|
+
journalMode?: "WAL" | "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "OFF";
|
|
28
|
+
busyTimeoutMs?: number;
|
|
29
|
+
synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
30
|
+
foreignKeys?: boolean;
|
|
31
|
+
cacheSize?: number;
|
|
32
|
+
tempStore?: "DEFAULT" | "FILE" | "MEMORY";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LibsqlDatabaseConfig {
|
|
36
|
+
url?: string;
|
|
37
|
+
authToken?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PostgresDatabaseConfig {
|
|
41
|
+
url?: string;
|
|
42
|
+
host?: string;
|
|
43
|
+
port?: number;
|
|
44
|
+
database?: string;
|
|
45
|
+
user?: string;
|
|
46
|
+
password?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DatabaseConfig {
|
|
50
|
+
driver?: "sqlite" | "libsql" | "postgres";
|
|
51
|
+
sqlite?: SqliteDatabaseConfig;
|
|
52
|
+
libsql?: LibsqlDatabaseConfig;
|
|
53
|
+
postgres?: PostgresDatabaseConfig;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DocumentStore {
|
|
57
|
+
ensure(): Promise<void>;
|
|
58
|
+
create(data: DocumentRecord): Promise<any>;
|
|
59
|
+
clone(data: DocumentRecord & { id: string }): Promise<any>;
|
|
60
|
+
update(id: string, patch: DocumentRecord): Promise<any>;
|
|
61
|
+
remove(id: string): Promise<any>;
|
|
62
|
+
updateOneByQuery(
|
|
63
|
+
query: DocumentQuery,
|
|
64
|
+
update: DocumentUpdate,
|
|
65
|
+
options?: DocumentUpdateOptions,
|
|
66
|
+
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
|
|
67
|
+
updateManyByQuery(
|
|
68
|
+
query: DocumentQuery,
|
|
69
|
+
update: DocumentUpdate,
|
|
70
|
+
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
|
|
71
|
+
deleteManyByQuery(
|
|
72
|
+
query: DocumentQuery,
|
|
73
|
+
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
|
|
74
|
+
bulkWrite(
|
|
75
|
+
operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdate; upsert?: boolean } }[],
|
|
76
|
+
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
|
|
77
|
+
find(
|
|
78
|
+
query?: DocumentQuery,
|
|
79
|
+
options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
|
|
80
|
+
): Promise<any[]>;
|
|
81
|
+
findIds(
|
|
82
|
+
query?: DocumentQuery,
|
|
83
|
+
options?: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number },
|
|
84
|
+
): Promise<string[]>;
|
|
85
|
+
findOne(
|
|
86
|
+
query?: DocumentQuery,
|
|
87
|
+
options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
|
|
88
|
+
): Promise<any | null>;
|
|
89
|
+
findId(
|
|
90
|
+
query?: DocumentQuery,
|
|
91
|
+
options?: { sort?: SortOption; skip?: number | null; sample?: boolean },
|
|
92
|
+
): Promise<string | null>;
|
|
93
|
+
pickOne(query?: DocumentQuery, options?: { sort?: SortOption; skip?: number | null; sample?: boolean }): Promise<any>;
|
|
94
|
+
pickById(id: string): Promise<any>;
|
|
95
|
+
exists(query?: DocumentQuery): Promise<string | null>;
|
|
96
|
+
count(query?: DocumentQuery): Promise<number>;
|
|
97
|
+
insight(query?: DocumentQuery): Promise<any>;
|
|
98
|
+
hydrate(data: DocumentRecord, originalData?: DocumentRecord): any;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SqlResultRows<Row = Record<string, unknown>> {
|
|
102
|
+
rows: Row[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface AkanSqlStatement {
|
|
106
|
+
run(...params: unknown[]): Promise<unknown>;
|
|
107
|
+
get<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row | null>;
|
|
108
|
+
all<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row[]>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface AkanSqlClient {
|
|
112
|
+
execute(sql: string, params?: unknown[] | Record<string, unknown>): Promise<unknown>;
|
|
113
|
+
prepare(sql: string): AkanSqlStatement;
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface DatabaseAdaptor {
|
|
118
|
+
getConnection(): AkanSqlClient;
|
|
119
|
+
getStore(constant: ConstantModel, database: DatabaseModel, schema: DocumentSchema): DocumentStore;
|
|
120
|
+
transaction<T>(fn: () => PromiseOrObject<T>): Promise<T>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface SqliteEnv extends BaseEnv {
|
|
124
|
+
workspaceRoot?: string;
|
|
125
|
+
database?: DatabaseConfig;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface TransactionContext {
|
|
129
|
+
afterCommit: (() => PromiseOrObject<void>)[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const BASE_COLUMNS = new Set(["id", "createdAt", "updatedAt", "removedAt"]);
|
|
133
|
+
const RESERVED_RE = /^sqlite_|^_akan_meta$/i;
|
|
134
|
+
const REF_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
135
|
+
const toSafeRefName = (value: string) => value.replace(/[^A-Za-z0-9_]+/g, "_").replace(/_+/g, "_");
|
|
136
|
+
type DocumentRecord = Record<string, unknown>;
|
|
137
|
+
type MutableDocumentRecord = Record<string, unknown>;
|
|
138
|
+
type FieldMap = Record<string, { getProps: () => Record<string, unknown>; [key: string]: unknown }>;
|
|
139
|
+
type SortOption = Record<string, 1 | -1> | null | undefined;
|
|
140
|
+
type QueryOperatorName = Exclude<
|
|
141
|
+
DocumentQueryNode,
|
|
142
|
+
{ kind: "all" } | { kind: "any" } | { kind: "not" } | { kind: "raw" }
|
|
143
|
+
>["op"];
|
|
144
|
+
interface SqliteDocumentRow {
|
|
145
|
+
id: string;
|
|
146
|
+
createdAt: number | string;
|
|
147
|
+
updatedAt: number | string;
|
|
148
|
+
removedAt?: number | string | null;
|
|
149
|
+
_doc: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface DocumentDatabaseOwner {
|
|
153
|
+
getConnection(): AkanSqlClient;
|
|
154
|
+
getMeta(key: string): Promise<string | undefined> | string | undefined;
|
|
155
|
+
setMeta(key: string, value: string): Promise<void>;
|
|
156
|
+
afterCommit(fn: () => PromiseOrObject<void>): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class BunSqliteStatement implements AkanSqlStatement {
|
|
160
|
+
constructor(private readonly statement: Statement) {}
|
|
161
|
+
async run(...params: unknown[]) {
|
|
162
|
+
return this.statement.run(...(params as SQLQueryBindings[]));
|
|
163
|
+
}
|
|
164
|
+
async get<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row | null> {
|
|
165
|
+
return (this.statement.get(...(params as SQLQueryBindings[])) as Row | null) ?? null;
|
|
166
|
+
}
|
|
167
|
+
async all<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row[]> {
|
|
168
|
+
return this.statement.all(...(params as SQLQueryBindings[])) as Row[];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class BunSqliteClient implements AkanSqlClient {
|
|
173
|
+
constructor(readonly db: Database) {}
|
|
174
|
+
async execute(sql: string, params: unknown[] | Record<string, unknown> = []) {
|
|
175
|
+
const values = Array.isArray(params) ? params : Object.values(params);
|
|
176
|
+
return this.db.query(sql).run(...(values as SQLQueryBindings[]));
|
|
177
|
+
}
|
|
178
|
+
prepare(sql: string): AkanSqlStatement {
|
|
179
|
+
return new BunSqliteStatement(this.db.query(sql));
|
|
180
|
+
}
|
|
181
|
+
async close() {
|
|
182
|
+
this.db.close();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class LibsqlStatement implements AkanSqlStatement {
|
|
187
|
+
constructor(
|
|
188
|
+
private readonly client: LibsqlClient,
|
|
189
|
+
private readonly sql: string,
|
|
190
|
+
) {}
|
|
191
|
+
async run(...params: unknown[]) {
|
|
192
|
+
const args = toLibsqlArgs(params);
|
|
193
|
+
return await this.client.execute({ sql: this.sql, args });
|
|
194
|
+
}
|
|
195
|
+
async get<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row | null> {
|
|
196
|
+
const args = toLibsqlArgs(params);
|
|
197
|
+
const result = await this.client.execute({ sql: this.sql, args });
|
|
198
|
+
return (result.rows[0] as Row | undefined) ?? null;
|
|
199
|
+
}
|
|
200
|
+
async all<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row[]> {
|
|
201
|
+
const args = toLibsqlArgs(params);
|
|
202
|
+
const result = await this.client.execute({ sql: this.sql, args });
|
|
203
|
+
return result.rows as Row[];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
class LibsqlAkanClient implements AkanSqlClient {
|
|
208
|
+
constructor(readonly client: LibsqlClient) {}
|
|
209
|
+
async execute(sql: string, params: unknown[] | Record<string, unknown> = []) {
|
|
210
|
+
return await this.client.execute({ sql, args: toLibsqlArgs(Array.isArray(params) ? params : [params]) });
|
|
211
|
+
}
|
|
212
|
+
prepare(sql: string): AkanSqlStatement {
|
|
213
|
+
return new LibsqlStatement(this.client, sql);
|
|
214
|
+
}
|
|
215
|
+
async close() {
|
|
216
|
+
this.client.close();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class PostgresStatement implements AkanSqlStatement {
|
|
221
|
+
constructor(
|
|
222
|
+
private readonly client: Sql,
|
|
223
|
+
private readonly sql: string,
|
|
224
|
+
) {}
|
|
225
|
+
async run(...params: unknown[]) {
|
|
226
|
+
const { sql, params: positionalParams } = toPostgresSql(this.sql, params);
|
|
227
|
+
return await this.client.unsafe(sql, positionalParams as any[]);
|
|
228
|
+
}
|
|
229
|
+
async get<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row | null> {
|
|
230
|
+
const rows = await this.all<Row>(...params);
|
|
231
|
+
return rows[0] ?? null;
|
|
232
|
+
}
|
|
233
|
+
async all<Row = Record<string, unknown>>(...params: unknown[]): Promise<Row[]> {
|
|
234
|
+
const { sql, params: positionalParams } = toPostgresSql(this.sql, params);
|
|
235
|
+
return (await this.client.unsafe(sql, positionalParams as any[])) as Row[];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class PostgresAkanClient implements AkanSqlClient {
|
|
240
|
+
constructor(readonly client: Sql) {}
|
|
241
|
+
async execute(sql: string, params: unknown[] | Record<string, unknown> = []) {
|
|
242
|
+
return await this.client.unsafe(sql, Array.isArray(params) ? (params as any[]) : Object.values(params));
|
|
243
|
+
}
|
|
244
|
+
prepare(sql: string): AkanSqlStatement {
|
|
245
|
+
return new PostgresStatement(this.client, sql);
|
|
246
|
+
}
|
|
247
|
+
async close() {
|
|
248
|
+
await this.client.end();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const quoteIdent = (identifier: string) => `"${identifier.replaceAll('"', '""')}"`;
|
|
253
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
254
|
+
!!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Buffer);
|
|
255
|
+
const toLibsqlValue = (value: unknown): InValue => {
|
|
256
|
+
if (
|
|
257
|
+
value === null ||
|
|
258
|
+
typeof value === "string" ||
|
|
259
|
+
typeof value === "number" ||
|
|
260
|
+
typeof value === "bigint" ||
|
|
261
|
+
typeof value === "boolean" ||
|
|
262
|
+
value instanceof Date ||
|
|
263
|
+
value instanceof Uint8Array ||
|
|
264
|
+
value instanceof ArrayBuffer
|
|
265
|
+
) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
if (value instanceof Buffer) return new Uint8Array(value);
|
|
269
|
+
return JSON.stringify(value);
|
|
270
|
+
};
|
|
271
|
+
const toLibsqlArgs = (params: unknown[]): InArgs => {
|
|
272
|
+
if (params.length === 1 && isPlainObject(params[0])) {
|
|
273
|
+
return Object.fromEntries(Object.entries(params[0]).map(([key, value]) => [key, toLibsqlValue(value)]));
|
|
274
|
+
}
|
|
275
|
+
return params.map(toLibsqlValue);
|
|
276
|
+
};
|
|
277
|
+
const toPostgresSql = (sql: string, params: unknown[]) => {
|
|
278
|
+
if (params.length === 1 && isPlainObject(params[0])) {
|
|
279
|
+
const named = params[0];
|
|
280
|
+
const values: unknown[] = [];
|
|
281
|
+
const text = sql.replace(/\$[A-Za-z_][A-Za-z0-9_]*/g, (token) => {
|
|
282
|
+
values.push(named[token.slice(1)]);
|
|
283
|
+
return `$${values.length}`;
|
|
284
|
+
});
|
|
285
|
+
return { sql: text, params: values };
|
|
286
|
+
}
|
|
287
|
+
let index = 0;
|
|
288
|
+
return {
|
|
289
|
+
sql: sql.replace(/\?/g, () => `$${++index}`),
|
|
290
|
+
params,
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const jsonPath = (path: string) =>
|
|
294
|
+
`$.${path
|
|
295
|
+
.split(".")
|
|
296
|
+
.map((part) => part.replaceAll('"', '\\"'))
|
|
297
|
+
.join(".")}`;
|
|
298
|
+
const encodeSqlValue = (value: unknown) => encodeDocumentValue(value);
|
|
299
|
+
const QUERY_OPERATOR_KEYS = new Set([
|
|
300
|
+
"eq",
|
|
301
|
+
"ne",
|
|
302
|
+
"oneOf",
|
|
303
|
+
"notOneOf",
|
|
304
|
+
"gt",
|
|
305
|
+
"gte",
|
|
306
|
+
"lt",
|
|
307
|
+
"lte",
|
|
308
|
+
"between",
|
|
309
|
+
"exists",
|
|
310
|
+
"missing",
|
|
311
|
+
"empty",
|
|
312
|
+
"has",
|
|
313
|
+
"contains",
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const stableJson = (value: unknown): string => {
|
|
317
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
318
|
+
if (value && typeof value === "object") {
|
|
319
|
+
return `{${Object.entries(value as Record<string, unknown>)
|
|
320
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
321
|
+
.map(([key, val]) => `${JSON.stringify(key)}:${stableJson(val)}`)
|
|
322
|
+
.join(",")}}`;
|
|
323
|
+
}
|
|
324
|
+
return JSON.stringify(value);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const descriptorHash = async (value: unknown) => {
|
|
328
|
+
const bytes = new TextEncoder().encode(stableJson(value));
|
|
329
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
330
|
+
return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
class QueryCompiler {
|
|
334
|
+
constructor(private readonly fields: FieldMap) {}
|
|
335
|
+
|
|
336
|
+
compile(query?: DocumentQuery): { where: string; params: unknown[] } {
|
|
337
|
+
if (!query || (typeof query === "object" && !Array.isArray(query) && Object.keys(query).length === 0)) {
|
|
338
|
+
return { where: "1 = 1", params: [] };
|
|
339
|
+
}
|
|
340
|
+
const compiled = this.compileNode(query);
|
|
341
|
+
return { where: compiled.sql || "1 = 1", params: compiled.params };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
orderBy(sort: Record<string, 1 | -1> = { createdAt: -1 }) {
|
|
345
|
+
return Object.entries(sort)
|
|
346
|
+
.map(([path, direction]) => `${this.fieldExpr(path)} ${direction === 1 ? "ASC" : "DESC"}`)
|
|
347
|
+
.join(", ");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fieldExpr(path: string) {
|
|
351
|
+
this.assertPath(path);
|
|
352
|
+
return BASE_COLUMNS.has(path) ? quoteIdent(path) : `json_extract("_doc", ${JSON.stringify(jsonPath(path))})`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private compileNode(query: DocumentQuery): { sql: string; params: unknown[] } {
|
|
356
|
+
if (this.isQueryNode(query)) {
|
|
357
|
+
if (query.kind === "all" || query.kind === "any") {
|
|
358
|
+
const parts = query.queries.map((sub) => this.compileNode(sub)).filter((part) => part.sql);
|
|
359
|
+
if (!parts.length) return { sql: "1 = 1", params: [] };
|
|
360
|
+
const joiner = query.kind === "all" ? " AND " : " OR ";
|
|
361
|
+
return {
|
|
362
|
+
sql: `(${parts.map((part) => part.sql).join(joiner)})`,
|
|
363
|
+
params: parts.flatMap((part) => part.params),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (query.kind === "not") {
|
|
367
|
+
const part = this.compileNode(query.query);
|
|
368
|
+
return { sql: `NOT (${part.sql})`, params: part.params };
|
|
369
|
+
}
|
|
370
|
+
if (query.kind === "raw") {
|
|
371
|
+
if (/[;]/.test(query.sql)) throw new Error("Raw SQL query fragments must be a single statement fragment");
|
|
372
|
+
return { sql: `(${query.sql})`, params: query.params };
|
|
373
|
+
}
|
|
374
|
+
throw new Error("Operator nodes must be attached to a document path");
|
|
375
|
+
}
|
|
376
|
+
const parts = Object.entries(query).flatMap(([path, value]) => {
|
|
377
|
+
if (path.startsWith("$")) throw new Error(`Mongo-style query operator is not supported: ${path}`);
|
|
378
|
+
if (value === undefined) throw new Error(`Undefined query value is not allowed: ${path}`);
|
|
379
|
+
return [this.compileField(path, value)];
|
|
380
|
+
});
|
|
381
|
+
if (!parts.length) return { sql: "1 = 1", params: [] };
|
|
382
|
+
return {
|
|
383
|
+
sql: `(${parts.map((part) => part.sql).join(" AND ")})`,
|
|
384
|
+
params: parts.flatMap((part) => part.params),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private compileField(path: string, value: unknown): { sql: string; params: unknown[] } {
|
|
389
|
+
this.assertPath(path);
|
|
390
|
+
const field = this.fields[path]?.getProps?.() ?? this.fields[path];
|
|
391
|
+
const expr = this.fieldExpr(path);
|
|
392
|
+
if (this.isQueryNode(value)) {
|
|
393
|
+
if (value.kind !== "op") return this.compileNode({ [path]: value } as DocumentQuery);
|
|
394
|
+
switch (value.op) {
|
|
395
|
+
case "eq":
|
|
396
|
+
return value.value === null
|
|
397
|
+
? { sql: `${expr} IS NULL`, params: [] }
|
|
398
|
+
: { sql: `${expr} = ?`, params: [encodeSqlValue(value.value)] };
|
|
399
|
+
case "ne":
|
|
400
|
+
return value.value === null
|
|
401
|
+
? { sql: `${expr} IS NOT NULL`, params: [] }
|
|
402
|
+
: { sql: `${expr} != ?`, params: [encodeSqlValue(value.value)] };
|
|
403
|
+
case "oneOf": {
|
|
404
|
+
const values = (value.value as unknown[]) ?? [];
|
|
405
|
+
if (!values.length) return { sql: "0 = 1", params: [] };
|
|
406
|
+
if (field?.isArray) {
|
|
407
|
+
const parts = values.map((item) => this.compileArrayHas(path, item));
|
|
408
|
+
return {
|
|
409
|
+
sql: `(${parts.map((part) => part.sql).join(" OR ")})`,
|
|
410
|
+
params: parts.flatMap((part) => part.params),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return { sql: `${expr} IN (${values.map(() => "?").join(", ")})`, params: values.map(encodeSqlValue) };
|
|
414
|
+
}
|
|
415
|
+
case "notOneOf": {
|
|
416
|
+
const values = (value.value as unknown[]) ?? [];
|
|
417
|
+
if (!values.length) return { sql: "1 = 1", params: [] };
|
|
418
|
+
if (field?.isArray) {
|
|
419
|
+
const parts = values.map((item) => this.compileArrayHas(path, item));
|
|
420
|
+
return {
|
|
421
|
+
sql: `NOT (${parts.map((part) => part.sql).join(" OR ")})`,
|
|
422
|
+
params: parts.flatMap((part) => part.params),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return { sql: `${expr} NOT IN (${values.map(() => "?").join(", ")})`, params: values.map(encodeSqlValue) };
|
|
426
|
+
}
|
|
427
|
+
case "gt":
|
|
428
|
+
case "gte":
|
|
429
|
+
case "lt":
|
|
430
|
+
case "lte": {
|
|
431
|
+
const operators = { gt: ">", gte: ">=", lt: "<", lte: "<=" } as const;
|
|
432
|
+
return { sql: `${expr} ${operators[value.op]} ?`, params: [encodeSqlValue(value.value)] };
|
|
433
|
+
}
|
|
434
|
+
case "between": {
|
|
435
|
+
const [from, to] = value.value as [unknown, unknown];
|
|
436
|
+
return { sql: `(${expr} >= ? AND ${expr} <= ?)`, params: [encodeSqlValue(from), encodeSqlValue(to)] };
|
|
437
|
+
}
|
|
438
|
+
case "exists":
|
|
439
|
+
return BASE_COLUMNS.has(path)
|
|
440
|
+
? { sql: `${expr} IS NOT NULL`, params: [] }
|
|
441
|
+
: { sql: `json_type("_doc", ?) IS NOT NULL`, params: [jsonPath(path)] };
|
|
442
|
+
case "missing":
|
|
443
|
+
return BASE_COLUMNS.has(path)
|
|
444
|
+
? { sql: `${expr} IS NULL`, params: [] }
|
|
445
|
+
: { sql: `json_type("_doc", ?) IS NULL`, params: [jsonPath(path)] };
|
|
446
|
+
case "empty":
|
|
447
|
+
return BASE_COLUMNS.has(path)
|
|
448
|
+
? { sql: `${expr} IS NULL`, params: [] }
|
|
449
|
+
: {
|
|
450
|
+
sql: `(json_type("_doc", ?) IS NULL OR json_type("_doc", ?) = 'null')`,
|
|
451
|
+
params: [jsonPath(path), jsonPath(path)],
|
|
452
|
+
};
|
|
453
|
+
case "has":
|
|
454
|
+
return this.compileArrayHas(path, value.value);
|
|
455
|
+
case "contains":
|
|
456
|
+
return { sql: `${expr} LIKE ?`, params: [`%${String(value.value)}%`] };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
460
|
+
const operators = value as Record<string, unknown>;
|
|
461
|
+
const keys = Object.keys(operators);
|
|
462
|
+
const legacyKey = keys.find((key) => key.startsWith("$"));
|
|
463
|
+
if (legacyKey) throw new Error(`Mongo-style query operator is not supported on ${path}: ${legacyKey}`);
|
|
464
|
+
if (keys.some((key) => QUERY_OPERATOR_KEYS.has(key))) {
|
|
465
|
+
const parts = keys.flatMap((key) => {
|
|
466
|
+
if (!QUERY_OPERATOR_KEYS.has(key)) return [];
|
|
467
|
+
if (key === "exists")
|
|
468
|
+
return [this.compileField(path, { kind: "op", op: operators.exists ? "exists" : "missing" })];
|
|
469
|
+
if (key === "missing")
|
|
470
|
+
return [this.compileField(path, { kind: "op", op: operators.missing ? "missing" : "exists" })];
|
|
471
|
+
if (key === "empty")
|
|
472
|
+
return [this.compileField(path, { kind: "op", op: operators.empty ? "empty" : "exists" })];
|
|
473
|
+
return [this.compileField(path, { kind: "op", op: key as QueryOperatorName, value: operators[key] })];
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
sql: `(${parts.map((part) => part.sql).join(" AND ")})`,
|
|
477
|
+
params: parts.flatMap((part) => part.params),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (field?.isArray && !Array.isArray(value)) return this.compileArrayHas(path, value);
|
|
482
|
+
return value === null
|
|
483
|
+
? { sql: `${expr} IS NULL`, params: [] }
|
|
484
|
+
: { sql: `${expr} = ?`, params: [encodeSqlValue(value)] };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private compileArrayHas(path: string, value: unknown): { sql: string; params: unknown[] } {
|
|
488
|
+
const arrayPath = BASE_COLUMNS.has(path)
|
|
489
|
+
? quoteIdent(path)
|
|
490
|
+
: `json_extract("_doc", ${JSON.stringify(jsonPath(path))})`;
|
|
491
|
+
return {
|
|
492
|
+
sql: `EXISTS (SELECT 1 FROM json_each(${arrayPath}) WHERE json_each.value = ?)`,
|
|
493
|
+
params: [encodeSqlValue(value)],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private assertPath(path: string) {
|
|
498
|
+
const root = path.split(".")[0];
|
|
499
|
+
if (BASE_COLUMNS.has(root)) return;
|
|
500
|
+
if (!this.fields[root]) throw new Error(`Unknown document field path: ${path}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private isQueryNode(value: unknown): value is DocumentQueryNode {
|
|
504
|
+
return !!value && typeof value === "object" && "kind" in value;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export class SqliteDocumentStore {
|
|
509
|
+
readonly schema: DocumentSchema;
|
|
510
|
+
readonly table: string;
|
|
511
|
+
readonly compiler: QueryCompiler;
|
|
512
|
+
#insertStmt: AkanSqlStatement | null = null;
|
|
513
|
+
|
|
514
|
+
constructor(
|
|
515
|
+
private readonly owner: DocumentDatabaseOwner,
|
|
516
|
+
readonly constant: ConstantModel,
|
|
517
|
+
readonly database: DatabaseModel,
|
|
518
|
+
schema: DocumentSchema,
|
|
519
|
+
) {
|
|
520
|
+
this.schema = schema;
|
|
521
|
+
this.table = database.refName;
|
|
522
|
+
this.compiler = new QueryCompiler(database.doc[FIELD_META] as unknown as FieldMap);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async ensure() {
|
|
526
|
+
this.assertValidRefName(this.table);
|
|
527
|
+
const db = this.owner.getConnection();
|
|
528
|
+
await db.execute(
|
|
529
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(this.table)} (
|
|
530
|
+
"id" TEXT PRIMARY KEY NOT NULL,
|
|
531
|
+
"createdAt" INTEGER NOT NULL,
|
|
532
|
+
"updatedAt" INTEGER NOT NULL,
|
|
533
|
+
"removedAt" INTEGER,
|
|
534
|
+
"_doc" TEXT NOT NULL
|
|
535
|
+
)`,
|
|
536
|
+
);
|
|
537
|
+
await this.owner.setMeta(
|
|
538
|
+
`table:${this.table}`,
|
|
539
|
+
await descriptorHash({ table: this.table, columns: ["id", "createdAt", "updatedAt", "removedAt", "_doc"] }),
|
|
540
|
+
);
|
|
541
|
+
for (const [idx, index] of this.schema.indexes.entries()) {
|
|
542
|
+
const name = index.name ?? `${this.table}_${Object.keys(index.fields).map(toSafeRefName).join("_")}_${idx}`;
|
|
543
|
+
this.assertValidRefName(name);
|
|
544
|
+
const hash = await descriptorHash(index);
|
|
545
|
+
const metaKey = `index:${this.table}:${name}`;
|
|
546
|
+
const existing = await this.owner.getMeta(metaKey);
|
|
547
|
+
if (existing && existing !== hash) throw new Error(`Index descriptor mismatch: ${name}`);
|
|
548
|
+
const expressions = Object.entries(index.fields).map(([field, mode]) =>
|
|
549
|
+
mode === "text" ? this.compiler.fieldExpr(field) : this.compiler.fieldExpr(field),
|
|
550
|
+
);
|
|
551
|
+
const unique = index.unique ? "UNIQUE " : "";
|
|
552
|
+
await db.execute(
|
|
553
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdent(name)} ON ${quoteIdent(this.table)} (${expressions.join(", ")})`,
|
|
554
|
+
);
|
|
555
|
+
await this.owner.setMeta(metaKey, hash);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async create(data: DocumentRecord) {
|
|
560
|
+
const now = Date.now();
|
|
561
|
+
const id = data.id ?? createDocumentId(now);
|
|
562
|
+
const doc = this.hydrate(
|
|
563
|
+
this.prepareDocument({
|
|
564
|
+
...data,
|
|
565
|
+
id,
|
|
566
|
+
createdAt: data.createdAt ?? dayjs(now),
|
|
567
|
+
updatedAt: data.updatedAt ?? dayjs(now),
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
570
|
+
await this.runHooks("save", "create", doc, "pre");
|
|
571
|
+
await this.runHooks("create", "create", doc, "pre");
|
|
572
|
+
const row = this.toRow(doc);
|
|
573
|
+
await this.insertStmt().run(row.id, row.createdAt, row.updatedAt, row.removedAt, row._doc);
|
|
574
|
+
await this.runHooks("create", "create", doc, "post");
|
|
575
|
+
await this.runHooks("save", "create", doc, "post");
|
|
576
|
+
return doc;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async clone(data: DocumentRecord & { id: string }) {
|
|
580
|
+
return this.create(data);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async update(id: string, patch: DocumentRecord) {
|
|
584
|
+
const current = await this.pickById(id);
|
|
585
|
+
const doc = this.hydrate(this.prepareDocument({ ...current, ...patch, id, updatedAt: dayjs() }), current);
|
|
586
|
+
await this.runHooks("save", "update", doc, "pre");
|
|
587
|
+
await this.runHooks("update", "update", doc, "pre");
|
|
588
|
+
const row = this.toRow(doc);
|
|
589
|
+
await this.owner
|
|
590
|
+
.getConnection()
|
|
591
|
+
.prepare(
|
|
592
|
+
`UPDATE ${quoteIdent(this.table)} SET "createdAt" = ?, "updatedAt" = ?, "removedAt" = ?, "_doc" = ? WHERE "id" = ?`,
|
|
593
|
+
)
|
|
594
|
+
.run(row.createdAt, row.updatedAt, row.removedAt, row._doc, id);
|
|
595
|
+
await this.runHooks("update", "update", doc, "post");
|
|
596
|
+
await this.runHooks("save", "update", doc, "post");
|
|
597
|
+
return doc;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async remove(id: string) {
|
|
601
|
+
return this.update(id, { removedAt: dayjs() });
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async updateOneByQuery(query: DocumentQuery, update: DocumentUpdate, options: DocumentUpdateOptions = {}) {
|
|
605
|
+
const doc = await this.findOne(query);
|
|
606
|
+
if (!doc) {
|
|
607
|
+
if (!options.upsert) return { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedId: null };
|
|
608
|
+
const inserted = await this.create(this.applyDocumentUpdate(this.extractInsertBase(query), update, true));
|
|
609
|
+
return { acknowledged: true, matchedCount: 0, modifiedCount: 1, upsertedId: inserted.id };
|
|
610
|
+
}
|
|
611
|
+
await this.update(doc.id, this.applyDocumentUpdate(doc, update));
|
|
612
|
+
return { acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedId: null };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async updateManyByQuery(query: DocumentQuery, update: DocumentUpdate) {
|
|
616
|
+
const docs = await this.find(query);
|
|
617
|
+
for (const doc of docs) await this.update(doc.id, this.applyDocumentUpdate(doc, update));
|
|
618
|
+
return { acknowledged: true, matchedCount: docs.length, modifiedCount: docs.length };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async deleteManyByQuery(query: DocumentQuery) {
|
|
622
|
+
return this.updateManyByQuery(query, { set: { removedAt: dayjs() } });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async bulkWrite(operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdate; upsert?: boolean } }[]) {
|
|
626
|
+
let matchedCount = 0;
|
|
627
|
+
let modifiedCount = 0;
|
|
628
|
+
let upsertedId: string | null = null;
|
|
629
|
+
for (const operation of operations) {
|
|
630
|
+
const result = await this.updateOneByQuery(operation.updateOne.filter, operation.updateOne.update, {
|
|
631
|
+
upsert: operation.updateOne.upsert,
|
|
632
|
+
});
|
|
633
|
+
matchedCount += result.matchedCount;
|
|
634
|
+
modifiedCount += result.modifiedCount;
|
|
635
|
+
upsertedId ??= result.upsertedId ?? null;
|
|
636
|
+
}
|
|
637
|
+
return { acknowledged: true, matchedCount, modifiedCount, upsertedId };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async find(
|
|
641
|
+
query?: DocumentQuery,
|
|
642
|
+
options: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number } = {},
|
|
643
|
+
) {
|
|
644
|
+
const { where, params } = this.safeQuery(query);
|
|
645
|
+
const limitValue = Number(options.limit ?? 0);
|
|
646
|
+
const skipValue = Number(options.skip ?? 0);
|
|
647
|
+
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
648
|
+
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
649
|
+
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
650
|
+
const rows = await this.owner
|
|
651
|
+
.getConnection()
|
|
652
|
+
.prepare(`SELECT * FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`)
|
|
653
|
+
.all<SqliteDocumentRow>(...params);
|
|
654
|
+
return rows.map((row) => this.hydrate(this.fromRow(row)));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async findIds(
|
|
658
|
+
query?: DocumentQuery,
|
|
659
|
+
options: { sort?: SortOption; skip?: number | null; limit?: number | null; sample?: number } = {},
|
|
660
|
+
) {
|
|
661
|
+
const { where, params } = this.safeQuery(query);
|
|
662
|
+
const limitValue = Number(options.limit ?? 0);
|
|
663
|
+
const skipValue = Number(options.skip ?? 0);
|
|
664
|
+
const limit = limitValue ? ` LIMIT ${limitValue}` : "";
|
|
665
|
+
const offset = skipValue ? ` OFFSET ${skipValue}` : "";
|
|
666
|
+
const order = options.sample ? "ORDER BY random()" : `ORDER BY ${this.compiler.orderBy(options.sort ?? undefined)}`;
|
|
667
|
+
const rows = await this.owner
|
|
668
|
+
.getConnection()
|
|
669
|
+
.prepare(`SELECT "id" FROM ${quoteIdent(this.table)} WHERE ${where} ${order}${limit}${offset}`)
|
|
670
|
+
.all<{ id: string }>(...params);
|
|
671
|
+
return rows.map((row) => row.id);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async findOne(query?: DocumentQuery, options: { sort?: SortOption; skip?: number | null; sample?: boolean } = {}) {
|
|
675
|
+
return (await this.find(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async findId(query?: DocumentQuery, options: { sort?: SortOption; skip?: number | null; sample?: boolean } = {}) {
|
|
679
|
+
return (await this.findIds(query, { ...options, limit: 1, sample: options.sample ? 1 : undefined })).at(0) ?? null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async pickOne(query?: DocumentQuery, options: { sort?: SortOption; skip?: number | null; sample?: boolean } = {}) {
|
|
683
|
+
const doc = await this.findOne(query, options);
|
|
684
|
+
if (!doc) throw new Error(`No Document (${this.table}): ${JSON.stringify(query)}`);
|
|
685
|
+
return doc;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async pickById(id: string) {
|
|
689
|
+
const doc = await this.findOne({ id } as DocumentQuery);
|
|
690
|
+
if (!doc) throw new Error(`No Document (${this.table}): ${id}`);
|
|
691
|
+
return doc;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async exists(query?: DocumentQuery) {
|
|
695
|
+
return this.findId(query);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async count(query?: DocumentQuery) {
|
|
699
|
+
const { where, params } = this.safeQuery(query);
|
|
700
|
+
const row = await this.owner
|
|
701
|
+
.getConnection()
|
|
702
|
+
.prepare(`SELECT count(*) as count FROM ${quoteIdent(this.table)} WHERE ${where}`)
|
|
703
|
+
.get<{ count: number }>(...params);
|
|
704
|
+
return row?.count ?? 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async insight(query?: DocumentQuery) {
|
|
708
|
+
const insightFields = this.constant.insight[FIELD_META] as unknown as FieldMap;
|
|
709
|
+
const result: DocumentRecord = {};
|
|
710
|
+
for (const [key, field] of Object.entries(insightFields)) {
|
|
711
|
+
const props = field.getProps();
|
|
712
|
+
if (!props.accumulate) {
|
|
713
|
+
result[key] = props.default;
|
|
714
|
+
} else if (
|
|
715
|
+
typeof props.accumulate === "object" &&
|
|
716
|
+
!Object.keys(props.accumulate as Record<string, unknown>).some((key) => key.startsWith("$"))
|
|
717
|
+
) {
|
|
718
|
+
result[key] = await this.count(documentQueryHelper.all(query ?? {}, props.accumulate as DocumentQuery));
|
|
719
|
+
} else {
|
|
720
|
+
result[key] = await this.count(query);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async search(
|
|
727
|
+
searchText: string | undefined | null,
|
|
728
|
+
options: { skip?: number | null; limit?: number | null; sort?: SortOption } = {},
|
|
729
|
+
) {
|
|
730
|
+
const textFields = this.schema.indexes.flatMap((index) =>
|
|
731
|
+
Object.entries(index.fields)
|
|
732
|
+
.filter(([, mode]) => mode === "text")
|
|
733
|
+
.map(([field]) => field),
|
|
734
|
+
);
|
|
735
|
+
const query =
|
|
736
|
+
searchText && textFields.length
|
|
737
|
+
? documentQueryHelper.any(
|
|
738
|
+
...textFields.map((field) =>
|
|
739
|
+
documentQueryHelper.raw(`${this.compiler.fieldExpr(field)} LIKE ?`, [`%${searchText}%`]),
|
|
740
|
+
),
|
|
741
|
+
)
|
|
742
|
+
: {};
|
|
743
|
+
const docs = await this.find(query, options);
|
|
744
|
+
const count = await this.count(query);
|
|
745
|
+
return { docs, count };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private safeQuery(query?: DocumentQuery) {
|
|
749
|
+
return this.compiler.compile(documentQueryHelper.all(documentQueryHelper.empty("removedAt"), query ?? {}));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private prepareDocument(data: DocumentRecord) {
|
|
753
|
+
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
754
|
+
const doc: MutableDocumentRecord = {};
|
|
755
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
756
|
+
const props = field.getProps();
|
|
757
|
+
const value = data[key];
|
|
758
|
+
if (value === undefined) {
|
|
759
|
+
if (props.default !== undefined && props.default !== null) {
|
|
760
|
+
doc[key] = typeof props.default === "function" ? props.default(data) : props.default;
|
|
761
|
+
} else if (!props.nullable && !["removedAt"].includes(key)) {
|
|
762
|
+
if (["id", "createdAt", "updatedAt"].includes(key)) continue;
|
|
763
|
+
throw new Error(`Missing required field: ${key}`);
|
|
764
|
+
}
|
|
765
|
+
} else if (value === null && !props.nullable) {
|
|
766
|
+
throw new Error(`Field is not nullable: ${key}`);
|
|
767
|
+
} else {
|
|
768
|
+
doc[key] = value;
|
|
769
|
+
}
|
|
770
|
+
if (props.enum && doc[key] !== undefined && doc[key] !== null) {
|
|
771
|
+
const values = Array.isArray(doc[key]) ? doc[key] : [doc[key]];
|
|
772
|
+
const fieldEnum = props.enum as { has: (value: unknown) => boolean } | undefined;
|
|
773
|
+
const invalidValue = fieldEnum ? values.find((value: unknown) => !fieldEnum.has(value)) : undefined;
|
|
774
|
+
if (invalidValue !== undefined) throw new Error(`Invalid enum value for ${key}: ${invalidValue}`);
|
|
775
|
+
}
|
|
776
|
+
const validate = props.validate as ((value: unknown, doc: MutableDocumentRecord) => boolean) | undefined;
|
|
777
|
+
if (validate && doc[key] !== undefined && doc[key] !== null && !validate(doc[key], doc)) {
|
|
778
|
+
throw new Error(`Invalid field value: ${key}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return { ...data, ...doc };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private extractInsertBase(query: DocumentQuery): Record<string, unknown> {
|
|
785
|
+
if (!query || typeof query !== "object" || Array.isArray(query) || "kind" in query) return {};
|
|
786
|
+
return Object.fromEntries(
|
|
787
|
+
Object.entries(query).flatMap(([key, value]) => {
|
|
788
|
+
if (["all", "any"].includes(key) || key.startsWith("$")) return [];
|
|
789
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) return [[key, value]];
|
|
790
|
+
return [];
|
|
791
|
+
}),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
private applyDocumentUpdate(source: DocumentRecord, update: DocumentUpdate, isInsert = false) {
|
|
796
|
+
const legacyKey = Object.keys(update).find((key) => key.startsWith("$"));
|
|
797
|
+
if (legacyKey) throw new Error(`Mongo-style update operator is not supported: ${legacyKey}`);
|
|
798
|
+
const doc = { ...source };
|
|
799
|
+
const setPath = (path: string, value: unknown) => {
|
|
800
|
+
const parts = path.split(".");
|
|
801
|
+
let target: MutableDocumentRecord = doc;
|
|
802
|
+
for (const part of parts.slice(0, -1)) {
|
|
803
|
+
target[part] ??= {};
|
|
804
|
+
target = target[part] as MutableDocumentRecord;
|
|
805
|
+
}
|
|
806
|
+
target[parts.at(-1) as string] = value;
|
|
807
|
+
};
|
|
808
|
+
const unsetPath = (path: string) => {
|
|
809
|
+
const parts = path.split(".");
|
|
810
|
+
let target: MutableDocumentRecord = doc;
|
|
811
|
+
for (const part of parts.slice(0, -1)) {
|
|
812
|
+
if (!target[part] || typeof target[part] !== "object") return;
|
|
813
|
+
target = target[part] as MutableDocumentRecord;
|
|
814
|
+
}
|
|
815
|
+
delete target[parts.at(-1) as string];
|
|
816
|
+
};
|
|
817
|
+
const addToSet = (path: string, value: unknown) => {
|
|
818
|
+
const current = path.split(".").reduce<unknown>((obj, key) => (obj as DocumentRecord | undefined)?.[key], doc) as
|
|
819
|
+
| unknown[]
|
|
820
|
+
| undefined;
|
|
821
|
+
const next = Array.isArray(current) ? current : [];
|
|
822
|
+
if (!next.some((item) => stableJson(item) === stableJson(value))) setPath(path, [...next, value]);
|
|
823
|
+
};
|
|
824
|
+
const pull = (path: string, value: unknown) => {
|
|
825
|
+
const current = path.split(".").reduce<unknown>((obj, key) => (obj as DocumentRecord | undefined)?.[key], doc) as
|
|
826
|
+
| unknown[]
|
|
827
|
+
| undefined;
|
|
828
|
+
if (Array.isArray(current))
|
|
829
|
+
setPath(
|
|
830
|
+
path,
|
|
831
|
+
current.filter((item) => stableJson(item) !== stableJson(value)),
|
|
832
|
+
);
|
|
833
|
+
};
|
|
834
|
+
const push = (path: string, value: unknown) => {
|
|
835
|
+
const current = path.split(".").reduce<unknown>((obj, key) => (obj as DocumentRecord | undefined)?.[key], doc) as
|
|
836
|
+
| unknown[]
|
|
837
|
+
| undefined;
|
|
838
|
+
setPath(path, [...(Array.isArray(current) ? current : []), value]);
|
|
839
|
+
};
|
|
840
|
+
if (update.set) {
|
|
841
|
+
Object.entries(update.set).forEach(([path, value]) => {
|
|
842
|
+
setPath(path, value);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (update.unset) (Array.isArray(update.unset) ? update.unset : Object.keys(update.unset)).forEach(unsetPath);
|
|
846
|
+
if (update.addToSet) {
|
|
847
|
+
Object.entries(update.addToSet).forEach(([path, value]) => {
|
|
848
|
+
addToSet(path, value);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (update.pull) {
|
|
852
|
+
Object.entries(update.pull).forEach(([path, value]) => {
|
|
853
|
+
pull(path, value);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
if (update.push) {
|
|
857
|
+
Object.entries(update.push).forEach(([path, value]) => {
|
|
858
|
+
push(path, value);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
if (update.inc) {
|
|
862
|
+
Object.entries(update.inc).forEach(([path, value]) => {
|
|
863
|
+
const current = path.split(".").reduce<unknown>((obj, key) => (obj as DocumentRecord | undefined)?.[key], doc);
|
|
864
|
+
setPath(path, Number(current ?? 0) + value);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (isInsert && update.setOnInsert) {
|
|
868
|
+
Object.entries(update.setOnInsert).forEach(([path, value]) => {
|
|
869
|
+
setPath(path, value);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const operatorKeys = new Set(["set", "unset", "addToSet", "pull", "push", "inc", "setOnInsert"]);
|
|
873
|
+
const direct = Object.fromEntries(Object.entries(update).filter(([key]) => !operatorKeys.has(key)));
|
|
874
|
+
Object.assign(doc, direct);
|
|
875
|
+
return doc;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private toRow(doc: DocumentRecord) {
|
|
879
|
+
const payload = { ...doc };
|
|
880
|
+
delete payload.id;
|
|
881
|
+
delete payload.createdAt;
|
|
882
|
+
delete payload.updatedAt;
|
|
883
|
+
delete payload.removedAt;
|
|
884
|
+
return {
|
|
885
|
+
id: doc.id,
|
|
886
|
+
createdAt: Number(encodeSqlValue(doc.createdAt ?? dayjs())),
|
|
887
|
+
updatedAt: Number(encodeSqlValue(doc.updatedAt ?? dayjs())),
|
|
888
|
+
removedAt: doc.removedAt ? Number(encodeSqlValue(doc.removedAt)) : null,
|
|
889
|
+
_doc: JSON.stringify(sanitizeJson(payload)),
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private fromRow(row: SqliteDocumentRow) {
|
|
894
|
+
const payload = this.decodeDocumentPayload(JSON.parse(row._doc));
|
|
895
|
+
return {
|
|
896
|
+
id: row.id,
|
|
897
|
+
createdAt: dayjs(Number(row.createdAt)),
|
|
898
|
+
updatedAt: dayjs(Number(row.updatedAt)),
|
|
899
|
+
removedAt: row.removedAt ? dayjs(Number(row.removedAt)) : undefined,
|
|
900
|
+
...payload,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private decodeDocumentPayload(payload: Record<string, unknown>) {
|
|
905
|
+
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
906
|
+
return Object.fromEntries(
|
|
907
|
+
Object.entries(payload).map(([key, value]) => {
|
|
908
|
+
const props = fields[key]?.getProps?.();
|
|
909
|
+
return [key, props ? this.decodeFieldValue(value, props) : value];
|
|
910
|
+
}),
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private decodeFieldValue(value: unknown, props: Record<string, unknown>): unknown {
|
|
915
|
+
if (value === undefined || value === null) return value;
|
|
916
|
+
if (props.isMap) {
|
|
917
|
+
const entries = value instanceof Map ? [...value.entries()] : Object.entries(value as Record<string, unknown>);
|
|
918
|
+
return new Map(entries.map(([key, item]) => [key, this.decodeMapValue(item, props)]));
|
|
919
|
+
}
|
|
920
|
+
if (props.modelRef === Date) {
|
|
921
|
+
if (Array.isArray(value)) return value.map((item) => (item === null ? item : dayjs(Number(item))));
|
|
922
|
+
return dayjs(Number(value));
|
|
923
|
+
}
|
|
924
|
+
if (Array.isArray(value)) return value.map((item) => this.decodeNestedValue(item, props));
|
|
925
|
+
return this.decodeNestedValue(value, props);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private decodeMapValue(value: unknown, props: Record<string, unknown>) {
|
|
929
|
+
if (value === undefined || value === null) return value;
|
|
930
|
+
if (props.of === Date) return dayjs(Number(value));
|
|
931
|
+
return value;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private decodeNestedValue(value: unknown, props: Record<string, unknown>): unknown {
|
|
935
|
+
if (!value || typeof value !== "object") return value;
|
|
936
|
+
if (!props.isClass || !props.isScalar) return value;
|
|
937
|
+
const scalarFields = (props.modelRef as { [FIELD_META]?: FieldMap } | undefined)?.[FIELD_META];
|
|
938
|
+
if (!scalarFields) return value;
|
|
939
|
+
return Object.fromEntries(
|
|
940
|
+
Object.entries(value as Record<string, unknown>).map(([key, nested]) => {
|
|
941
|
+
const nestedProps = scalarFields[key]?.getProps?.();
|
|
942
|
+
return [key, nestedProps ? this.decodeFieldValue(nested, nestedProps) : nested];
|
|
943
|
+
}),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
hydrate(data: DocumentRecord, originalData: DocumentRecord = data) {
|
|
948
|
+
const store = this;
|
|
949
|
+
const original = JSON.parse(JSON.stringify(sanitizeJson(originalData) ?? {})) as Record<string, unknown>;
|
|
950
|
+
const isNew = !originalData.id;
|
|
951
|
+
const doc = Object.assign(Object.create(this.database.doc.prototype), data);
|
|
952
|
+
Object.defineProperties(doc, {
|
|
953
|
+
set: {
|
|
954
|
+
value(patch: DocumentRecord) {
|
|
955
|
+
Object.assign(this, patch);
|
|
956
|
+
return this;
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
save: {
|
|
960
|
+
async value() {
|
|
961
|
+
return this.id ? store.update(this.id, this) : store.create(this);
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
refresh: {
|
|
965
|
+
async value() {
|
|
966
|
+
Object.assign(this, await store.pickById(this.id));
|
|
967
|
+
return this;
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
isModified: {
|
|
971
|
+
value(field?: string) {
|
|
972
|
+
if (isNew) return true;
|
|
973
|
+
if (!field) return JSON.stringify(sanitizeJson(this)) !== JSON.stringify(original);
|
|
974
|
+
return JSON.stringify(sanitizeJson(this[field])) !== JSON.stringify(original[field]);
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
toJSON: {
|
|
978
|
+
value() {
|
|
979
|
+
return sanitizeJson(this);
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
toObject: {
|
|
983
|
+
value() {
|
|
984
|
+
return sanitizeJson(this);
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
return doc;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private async runHooks(
|
|
992
|
+
saveType: "save" | "create" | "update" | "remove",
|
|
993
|
+
crudType: "create" | "update" | "remove",
|
|
994
|
+
doc: DocumentRecord,
|
|
995
|
+
phase: "pre" | "post",
|
|
996
|
+
) {
|
|
997
|
+
const hooks = phase === "pre" ? this.schema.preHooks.get(saveType) : this.schema.postHooks.get(saveType);
|
|
998
|
+
for (const hook of hooks ?? []) {
|
|
999
|
+
const run = () => hook.call(doc, () => undefined, crudType);
|
|
1000
|
+
if (phase === "post") await this.owner.afterCommit(run);
|
|
1001
|
+
else await run();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private insertStmt() {
|
|
1006
|
+
this.#insertStmt ??= this.owner
|
|
1007
|
+
.getConnection()
|
|
1008
|
+
.prepare(
|
|
1009
|
+
`INSERT INTO ${quoteIdent(this.table)} ("id", "createdAt", "updatedAt", "removedAt", "_doc") VALUES (?, ?, ?, ?, ?)`,
|
|
1010
|
+
);
|
|
1011
|
+
return this.#insertStmt;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private assertValidRefName(refName: string) {
|
|
1015
|
+
if (!REF_NAME_RE.test(refName) || RESERVED_RE.test(refName))
|
|
1016
|
+
throw new Error(`Invalid database identifier: ${refName}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
export class SqliteDatabase
|
|
1020
|
+
extends adapt("sqliteDatabase", ({ env }) => ({
|
|
1021
|
+
config: env((env: SqliteEnv) => {
|
|
1022
|
+
const appName = env.appName ?? "akan";
|
|
1023
|
+
const environment = env.environment ?? "local";
|
|
1024
|
+
const defaultFile = resolveDefaultSqliteFile({
|
|
1025
|
+
appName,
|
|
1026
|
+
fileName: `${appName}-${environment}.db`,
|
|
1027
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
1028
|
+
workspaceRoot: env.workspaceRoot,
|
|
1029
|
+
});
|
|
1030
|
+
return {
|
|
1031
|
+
journalMode: "WAL",
|
|
1032
|
+
busyTimeoutMs: 5000,
|
|
1033
|
+
synchronous: "NORMAL",
|
|
1034
|
+
foreignKeys: true,
|
|
1035
|
+
...env.database?.sqlite,
|
|
1036
|
+
filePath: env.database?.sqlite?.filePath ?? process.env.SQLITE_DATABASE_PATH ?? defaultFile,
|
|
1037
|
+
} satisfies Required<
|
|
1038
|
+
Pick<SqliteDatabaseConfig, "filePath" | "journalMode" | "busyTimeoutMs" | "synchronous" | "foreignKeys">
|
|
1039
|
+
> &
|
|
1040
|
+
SqliteDatabaseConfig;
|
|
1041
|
+
}),
|
|
1042
|
+
}))
|
|
1043
|
+
implements DatabaseAdaptor
|
|
1044
|
+
{
|
|
1045
|
+
#db!: Database;
|
|
1046
|
+
#client!: BunSqliteClient;
|
|
1047
|
+
#stores = new Map<string, SqliteDocumentStore>();
|
|
1048
|
+
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1049
|
+
|
|
1050
|
+
override async onInit() {
|
|
1051
|
+
await mkdir(path.dirname(this.config.filePath), { recursive: true });
|
|
1052
|
+
this.#db = new Database(this.config.filePath, { strict: true, create: true });
|
|
1053
|
+
this.#client = new BunSqliteClient(this.#db);
|
|
1054
|
+
this.#db.run(`PRAGMA journal_mode = ${this.config.journalMode ?? "WAL"}`);
|
|
1055
|
+
this.#db.run(`PRAGMA busy_timeout = ${this.config.busyTimeoutMs ?? 5000}`);
|
|
1056
|
+
this.#db.run(`PRAGMA synchronous = ${this.config.synchronous ?? "NORMAL"}`);
|
|
1057
|
+
this.#db.run(`PRAGMA foreign_keys = ${this.config.foreignKeys === false ? "OFF" : "ON"}`);
|
|
1058
|
+
if (this.config.cacheSize) this.#db.run(`PRAGMA cache_size = ${this.config.cacheSize}`);
|
|
1059
|
+
if (this.config.tempStore) this.#db.run(`PRAGMA temp_store = ${this.config.tempStore}`);
|
|
1060
|
+
this.#db.run(
|
|
1061
|
+
`CREATE TABLE IF NOT EXISTS "_akan_meta" ("key" TEXT PRIMARY KEY NOT NULL, "value" TEXT NOT NULL, "updatedAt" INTEGER NOT NULL)`,
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
override async onDestroy() {
|
|
1066
|
+
this.#db?.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1067
|
+
await this.#client?.close();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
getConnection() {
|
|
1071
|
+
return this.#client;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
getStore(constant: ConstantModel, database: DatabaseModel, schema: SchemaOf) {
|
|
1075
|
+
const existing = this.#stores.get(database.refName);
|
|
1076
|
+
if (existing) return existing;
|
|
1077
|
+
const store = new SqliteDocumentStore(this, constant, database, schema as DocumentSchema);
|
|
1078
|
+
this.#stores.set(database.refName, store);
|
|
1079
|
+
void store.ensure();
|
|
1080
|
+
return store;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
getMeta(key: string) {
|
|
1084
|
+
return (this.#db.query(`SELECT "value" FROM "_akan_meta" WHERE "key" = ?`).get(key) as { value: string } | null)
|
|
1085
|
+
?.value;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async setMeta(key: string, value: string) {
|
|
1089
|
+
this.#db
|
|
1090
|
+
.query(
|
|
1091
|
+
`INSERT INTO "_akan_meta" ("key", "value", "updatedAt") VALUES (?, ?, ?) ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", "updatedAt" = excluded."updatedAt"`,
|
|
1092
|
+
)
|
|
1093
|
+
.run(key, value, Date.now());
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async transaction<T>(fn: () => PromiseOrObject<T>): Promise<T> {
|
|
1097
|
+
const active = this.#transaction.getStore();
|
|
1098
|
+
if (active) return await fn();
|
|
1099
|
+
const context: TransactionContext = { afterCommit: [] };
|
|
1100
|
+
return await this.#transaction.run(context, async () => {
|
|
1101
|
+
this.#db.run("BEGIN IMMEDIATE");
|
|
1102
|
+
try {
|
|
1103
|
+
const result = await fn();
|
|
1104
|
+
this.#db.run("COMMIT");
|
|
1105
|
+
for (const hook of context.afterCommit) await hook();
|
|
1106
|
+
return result;
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
this.#db.run("ROLLBACK");
|
|
1109
|
+
throw err;
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async afterCommit(fn: () => PromiseOrObject<void>) {
|
|
1115
|
+
const active = this.#transaction.getStore();
|
|
1116
|
+
if (!active) return await fn();
|
|
1117
|
+
active.afterCommit.push(fn);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
checkpoint(mode: "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE" = "TRUNCATE") {
|
|
1121
|
+
this.#db.run(`PRAGMA wal_checkpoint(${mode})`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
vacuum() {
|
|
1125
|
+
this.#db.run("VACUUM");
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export class LibsqlDatabase
|
|
1130
|
+
extends adapt("libsqlDatabase", ({ env }) => ({
|
|
1131
|
+
config: env((env: SqliteEnv) => {
|
|
1132
|
+
const appName = env.appName ?? "akan";
|
|
1133
|
+
const environment = env.environment ?? "local";
|
|
1134
|
+
const defaultFile = resolveDefaultSqliteFile({
|
|
1135
|
+
appName,
|
|
1136
|
+
fileName: `${appName}-${environment}.db`,
|
|
1137
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
1138
|
+
workspaceRoot: env.workspaceRoot,
|
|
1139
|
+
});
|
|
1140
|
+
return {
|
|
1141
|
+
url:
|
|
1142
|
+
env.database?.libsql?.url ??
|
|
1143
|
+
process.env.LIBSQL_URL ??
|
|
1144
|
+
process.env.LIBSQL_URI ??
|
|
1145
|
+
`file:${env.database?.sqlite?.filePath ?? process.env.SQLITE_DATABASE_PATH ?? defaultFile}`,
|
|
1146
|
+
authToken: env.database?.libsql?.authToken ?? process.env.LIBSQL_AUTH_TOKEN,
|
|
1147
|
+
} satisfies LibsqlDatabaseConfig;
|
|
1148
|
+
}),
|
|
1149
|
+
}))
|
|
1150
|
+
implements DatabaseAdaptor
|
|
1151
|
+
{
|
|
1152
|
+
#client!: LibsqlAkanClient;
|
|
1153
|
+
#stores = new Map<string, SqliteDocumentStore>();
|
|
1154
|
+
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1155
|
+
|
|
1156
|
+
override async onInit() {
|
|
1157
|
+
const url = this.config.url ?? "file:local.db";
|
|
1158
|
+
if (url.startsWith("file:")) await mkdir(path.dirname(url.slice(5)), { recursive: true });
|
|
1159
|
+
const { createClient } = await import("@libsql/client");
|
|
1160
|
+
this.#client = new LibsqlAkanClient(createClient({ url, authToken: this.config.authToken }));
|
|
1161
|
+
await this.#client.execute(
|
|
1162
|
+
`CREATE TABLE IF NOT EXISTS "_akan_meta" ("key" TEXT PRIMARY KEY NOT NULL, "value" TEXT NOT NULL, "updatedAt" INTEGER NOT NULL)`,
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
override async onDestroy() {
|
|
1167
|
+
await this.#client?.close();
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
getConnection() {
|
|
1171
|
+
return this.#client;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
getStore(constant: ConstantModel, database: DatabaseModel, schema: SchemaOf) {
|
|
1175
|
+
const existing = this.#stores.get(database.refName);
|
|
1176
|
+
if (existing) return existing;
|
|
1177
|
+
const store = new SqliteDocumentStore(this, constant, database, schema as DocumentSchema);
|
|
1178
|
+
this.#stores.set(database.refName, store);
|
|
1179
|
+
void store.ensure();
|
|
1180
|
+
return store;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async getMeta(key: string) {
|
|
1184
|
+
return (await this.#client.prepare(`SELECT "value" FROM "_akan_meta" WHERE "key" = ?`).get<{ value: string }>(key))
|
|
1185
|
+
?.value;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async setMeta(key: string, value: string) {
|
|
1189
|
+
await this.#client
|
|
1190
|
+
.prepare(
|
|
1191
|
+
`INSERT INTO "_akan_meta" ("key", "value", "updatedAt") VALUES (?, ?, ?) ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", "updatedAt" = excluded."updatedAt"`,
|
|
1192
|
+
)
|
|
1193
|
+
.run(key, value, Date.now());
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async transaction<T>(fn: () => PromiseOrObject<T>): Promise<T> {
|
|
1197
|
+
const active = this.#transaction.getStore();
|
|
1198
|
+
if (active) return await fn();
|
|
1199
|
+
const context: TransactionContext = { afterCommit: [] };
|
|
1200
|
+
return await this.#transaction.run(context, async () => {
|
|
1201
|
+
await this.#client.execute("BEGIN IMMEDIATE");
|
|
1202
|
+
try {
|
|
1203
|
+
const result = await fn();
|
|
1204
|
+
await this.#client.execute("COMMIT");
|
|
1205
|
+
for (const hook of context.afterCommit) await hook();
|
|
1206
|
+
return result;
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
await this.#client.execute("ROLLBACK");
|
|
1209
|
+
throw err;
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async afterCommit(fn: () => PromiseOrObject<void>) {
|
|
1215
|
+
const active = this.#transaction.getStore();
|
|
1216
|
+
if (!active) return await fn();
|
|
1217
|
+
active.afterCommit.push(fn);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export class PostgresDatabase
|
|
1222
|
+
extends adapt("postgresDatabase", ({ env }) => ({
|
|
1223
|
+
config: env((env: SqliteEnv) => {
|
|
1224
|
+
return {
|
|
1225
|
+
url: env.database?.postgres?.url ?? process.env.POSTGRES_URL ?? process.env.POSTGRES_URI,
|
|
1226
|
+
host: env.database?.postgres?.host ?? process.env.POSTGRES_HOST ?? "localhost",
|
|
1227
|
+
port: env.database?.postgres?.port ?? Number(process.env.POSTGRES_PORT ?? 5432),
|
|
1228
|
+
database: env.database?.postgres?.database ?? process.env.POSTGRES_DATABASE ?? "akan",
|
|
1229
|
+
user: env.database?.postgres?.user ?? process.env.POSTGRES_USER ?? "akan",
|
|
1230
|
+
password: env.database?.postgres?.password ?? process.env.POSTGRES_PASSWORD ?? "akan",
|
|
1231
|
+
} satisfies PostgresDatabaseConfig;
|
|
1232
|
+
}),
|
|
1233
|
+
}))
|
|
1234
|
+
implements DatabaseAdaptor
|
|
1235
|
+
{
|
|
1236
|
+
#client!: PostgresAkanClient;
|
|
1237
|
+
#stores = new Map<string, SqliteDocumentStore>();
|
|
1238
|
+
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1239
|
+
|
|
1240
|
+
override async onInit() {
|
|
1241
|
+
const { default: postgres } = await import("postgres");
|
|
1242
|
+
const sql = this.config.url
|
|
1243
|
+
? postgres(this.config.url)
|
|
1244
|
+
: postgres({
|
|
1245
|
+
host: this.config.host,
|
|
1246
|
+
port: this.config.port,
|
|
1247
|
+
database: this.config.database,
|
|
1248
|
+
username: this.config.user,
|
|
1249
|
+
password: this.config.password,
|
|
1250
|
+
});
|
|
1251
|
+
this.#client = new PostgresAkanClient(sql);
|
|
1252
|
+
await this.#client.execute(
|
|
1253
|
+
`CREATE TABLE IF NOT EXISTS "_akan_meta" ("key" TEXT PRIMARY KEY NOT NULL, "value" TEXT NOT NULL, "updatedAt" BIGINT NOT NULL)`,
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
override async onDestroy() {
|
|
1258
|
+
await this.#client?.close();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
getConnection() {
|
|
1262
|
+
return this.#client;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
getStore(constant: ConstantModel, database: DatabaseModel, schema: SchemaOf) {
|
|
1266
|
+
const existing = this.#stores.get(database.refName);
|
|
1267
|
+
if (existing) return existing;
|
|
1268
|
+
const store = new SqliteDocumentStore(this, constant, database, schema as DocumentSchema);
|
|
1269
|
+
this.#stores.set(database.refName, store);
|
|
1270
|
+
void store.ensure();
|
|
1271
|
+
return store;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async getMeta(key: string) {
|
|
1275
|
+
return (await this.#client.prepare(`SELECT "value" FROM "_akan_meta" WHERE "key" = $1`).get<{ value: string }>(key))
|
|
1276
|
+
?.value;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
async setMeta(key: string, value: string) {
|
|
1280
|
+
await this.#client
|
|
1281
|
+
.prepare(
|
|
1282
|
+
`INSERT INTO "_akan_meta" ("key", "value", "updatedAt") VALUES ($1, $2, $3) ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", "updatedAt" = excluded."updatedAt"`,
|
|
1283
|
+
)
|
|
1284
|
+
.run(key, value, Date.now());
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async transaction<T>(fn: () => PromiseOrObject<T>): Promise<T> {
|
|
1288
|
+
const active = this.#transaction.getStore();
|
|
1289
|
+
if (active) return await fn();
|
|
1290
|
+
const context: TransactionContext = { afterCommit: [] };
|
|
1291
|
+
return await this.#transaction.run(context, async () => {
|
|
1292
|
+
await this.#client.execute("BEGIN");
|
|
1293
|
+
try {
|
|
1294
|
+
const result = await fn();
|
|
1295
|
+
await this.#client.execute("COMMIT");
|
|
1296
|
+
for (const hook of context.afterCommit) await hook();
|
|
1297
|
+
return result;
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
await this.#client.execute("ROLLBACK");
|
|
1300
|
+
throw err;
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async afterCommit(fn: () => PromiseOrObject<void>) {
|
|
1306
|
+
const active = this.#transaction.getStore();
|
|
1307
|
+
if (!active) return await fn();
|
|
1308
|
+
active.afterCommit.push(fn);
|
|
1309
|
+
}
|
|
1310
|
+
}
|