bazis 0.96.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/cli/build.ts +147 -0
- package/cli/codegen.ts +40 -0
- package/cli/generateModule.ts +187 -0
- package/cli/generateProject.ts +183 -0
- package/cli/main.ts +125 -0
- package/cli/moduleRegistration.ts +127 -0
- package/cli/naming.ts +124 -0
- package/cli/parseCli.ts +151 -0
- package/cli/templates/module.ts +497 -0
- package/cli/templates/pack.ts +76 -0
- package/cli/templates/passport.ts +178 -0
- package/core/agent/AgentContextBuilder.ts +567 -0
- package/core/agent/AgentDataDefinition.ts +8 -0
- package/core/agent/AgentRegistry.ts +539 -0
- package/core/agent/AgentRuntime.ts +47 -0
- package/core/agent/AgentToolExecutor.ts +2130 -0
- package/core/agent/AgentToolHooks.ts +39 -0
- package/core/agent/ModelProvider.ts +21 -0
- package/core/agent/README.md +229 -0
- package/core/agent/Tool.contract.ts +20 -0
- package/core/agent/decorators/Agent.ts +11 -0
- package/core/agent/decorators/Prompt.ts +11 -0
- package/core/agent/decorators/Task.ts +29 -0
- package/core/agent/decorators/Tool.ts +12 -0
- package/core/agent/errors.ts +43 -0
- package/core/agent/generated/catalog.ts +26 -0
- package/core/agent/index.ts +170 -0
- package/core/agent/internal/AgentExecutionDriver.ts +1497 -0
- package/core/agent/internal/AgentExecutionState.ts +775 -0
- package/core/agent/internal/AgentJsonSchema.ts +150 -0
- package/core/agent/internal/AgentModelBinding.ts +57 -0
- package/core/agent/internal/Schema.contract.ts +36 -0
- package/core/agent/internal/ToolAudit.projector.ts +163 -0
- package/core/agent/internal/ToolContract.validator.ts +102 -0
- package/core/agent/metadata.ts +398 -0
- package/core/agent/moduleContributions-v1.ts +113 -0
- package/core/agent/semantic.ts +903 -0
- package/core/agent/session/codec.ts +33 -0
- package/core/agent/session/contracts.ts +96 -0
- package/core/agent/session/entities.ts +84 -0
- package/core/agent/session/protector.ts +92 -0
- package/core/app/index.ts +5 -0
- package/core/app/runApp.ts +293 -0
- package/core/app/ui/index.ts +29 -0
- package/core/app/ui/uiProfileResolver.ts +966 -0
- package/core/app/ui/uiSurfaceHosting.ts +38 -0
- package/core/app/ui/uiSurfaceHttp.ts +476 -0
- package/core/background/BackgroundService.ts +223 -0
- package/core/background/backgroundModule.ts +42 -0
- package/core/background/decorator.ts +43 -0
- package/core/background/delay.ts +27 -0
- package/core/background/index.ts +15 -0
- package/core/cache/COMPATIBILITY.md +13 -0
- package/core/cache/ICache.ts +71 -0
- package/core/cache/MemoryCache.ts +394 -0
- package/core/cache/README.md +83 -0
- package/core/cache/SPEC.md +1355 -0
- package/core/cache/cacheModule.ts +21 -0
- package/core/cache/decorators/Cacheable.ts +38 -0
- package/core/cache/decorators/CacheableRedis.ts +39 -0
- package/core/cache/decorators/OutputCache.ts +33 -0
- package/core/cache/decorators/OutputRedisCache.ts +33 -0
- package/core/cache/decorators/cacheableMetadata.ts +78 -0
- package/core/cache/decorators/cacheableRedisMetadata.ts +81 -0
- package/core/cache/decorators/outputCacheMetadata.ts +71 -0
- package/core/cache/decorators/outputRedisCacheMetadata.ts +77 -0
- package/core/cache/di/classProviderHook.ts +26 -0
- package/core/cache/distributed/CacheCodec.ts +5 -0
- package/core/cache/distributed/DistributedCache.ts +403 -0
- package/core/cache/distributed/DistributedCacheDriver.ts +83 -0
- package/core/cache/distributed/DistributedCacheStores.ts +28 -0
- package/core/cache/distributed/IDistributedCache.ts +40 -0
- package/core/cache/distributed/NamedCacheRegistry.ts +23 -0
- package/core/cache/distributed/codecs.ts +32 -0
- package/core/cache/distributed/index.ts +19 -0
- package/core/cache/errors/CacheError.ts +16 -0
- package/core/cache/http/CachedHttpPayload.ts +213 -0
- package/core/cache/http/applyClientCacheHeaders.ts +61 -0
- package/core/cache/http/buildOutputCacheKey.ts +154 -0
- package/core/cache/http/buildRedisOutputCacheKey.ts +16 -0
- package/core/cache/http/composeOutputCache.ts +130 -0
- package/core/cache/http/outputCacheMiddleware.ts +111 -0
- package/core/cache/http/outputCacheSecurityWarning.ts +58 -0
- package/core/cache/http/outputRedisCacheMiddleware.ts +118 -0
- package/core/cache/index.ts +97 -0
- package/core/cache/internal/AutomaticCacheKey.ts +106 -0
- package/core/cache/internal/CacheKeyGuard.ts +56 -0
- package/core/cache/internal/buildCacheProviders.ts +146 -0
- package/core/cache/internal/normalizeCacheKey.ts +9 -0
- package/core/cache/internal/resolveCachePolicy.ts +32 -0
- package/core/cache/internal/resolveCacheableRedisPolicy.ts +17 -0
- package/core/cache/internal/resolveOutputRedisCachePolicy.ts +17 -0
- package/core/cache/providers/cachedProviders.ts +68 -0
- package/core/cache/services/cacheProxy.ts +243 -0
- package/core/cache/tokens/CACHE_POLICIES.ts +5 -0
- package/core/cache/tokens/DISTRIBUTED_CACHE.ts +30 -0
- package/core/cache/types/CacheEntry.ts +7 -0
- package/core/cache/types/CacheOptions.ts +60 -0
- package/core/cache/types/CachePolicy.ts +74 -0
- package/core/cache/types/CacheableRedisOptions.ts +23 -0
- package/core/cache/types/OutputRedisCacheOptions.ts +25 -0
- package/core/di/README.md +104 -0
- package/core/di/ServiceCollection.ts +185 -0
- package/core/di/ServiceProvider.ts +689 -0
- package/core/di/ServiceScope.ts +61 -0
- package/core/di/container.ts +38 -0
- package/core/di/errors/AmbiguousNamedDependencyError.ts +10 -0
- package/core/di/errors/AsyncResolutionRequiredError.ts +12 -0
- package/core/di/errors/CircularDependencyError.ts +10 -0
- package/core/di/errors/ClassDependenciesMismatchError.ts +15 -0
- package/core/di/errors/DiError.ts +6 -0
- package/core/di/errors/InvalidProviderError.ts +9 -0
- package/core/di/errors/ModuleEncapsulationError.ts +7 -0
- package/core/di/errors/ModuleOwnedProviderConflictError.ts +8 -0
- package/core/di/errors/NamedDependencyNotFoundError.ts +7 -0
- package/core/di/errors/OptionsValidationError.ts +7 -0
- package/core/di/errors/ProviderNotFoundError.ts +10 -0
- package/core/di/errors/ScopeDisposedError.ts +7 -0
- package/core/di/errors/ScopedServiceFromRootError.ts +9 -0
- package/core/di/errors/ServiceValidationError.ts +7 -0
- package/core/di/errors/index.ts +14 -0
- package/core/di/extensions/activator.ts +39 -0
- package/core/di/extensions/application.ts +117 -0
- package/core/di/extensions/hosted-service.ts +124 -0
- package/core/di/extensions/index.ts +23 -0
- package/core/di/extensions/options-reloadable.ts +137 -0
- package/core/di/extensions/options.ts +91 -0
- package/core/di/extensions/service-provider-token.ts +21 -0
- package/core/di/index.ts +97 -0
- package/core/di/internal/GraphValidator.ts +119 -0
- package/core/di/internal/HostedServicePlan.ts +158 -0
- package/core/di/internal/NamedTokenIndex.ts +55 -0
- package/core/di/internal/OpenGenericRegistration.ts +14 -0
- package/core/di/internal/ResolutionPlan.ts +18 -0
- package/core/di/internal/ResolutionScopeState.ts +19 -0
- package/core/di/internal/ResolutionTracker.ts +153 -0
- package/core/di/internal/ScopeLifecycle.ts +159 -0
- package/core/di/internal/ServiceRegistration.ts +14 -0
- package/core/di/internal/ServiceRegistry.ts +252 -0
- package/core/di/internal/classDeps.ts +38 -0
- package/core/di/internal/disposal.ts +58 -0
- package/core/di/internal/providerDeps.ts +26 -0
- package/core/di/module/DI.ts +146 -0
- package/core/di/module/Global.ts +25 -0
- package/core/di/module/Module.ts +44 -0
- package/core/di/module/ModuleRegistrar.ts +69 -0
- package/core/di/module/autoDeps.ts +39 -0
- package/core/di/module/classProviderHooks.ts +66 -0
- package/core/di/module/collectModuleConfigs.ts +57 -0
- package/core/di/module/collectModuleControllers.ts +53 -0
- package/core/di/module/collectModuleUiProfiles.ts +53 -0
- package/core/di/module/createContainer.ts +261 -0
- package/core/di/module/encapsulation.ts +416 -0
- package/core/di/module/encapsulationHooks.ts +36 -0
- package/core/di/module/generatedProviderAttachments.ts +102 -0
- package/core/di/module/index.ts +41 -0
- package/core/di/module/moduleExtensions.ts +35 -0
- package/core/di/module/moduleNameBinding.ts +136 -0
- package/core/di/module/moduleOwnedProviderContributors.ts +409 -0
- package/core/di/module/providerSelection.ts +90 -0
- package/core/di/module/shortcuts.ts +111 -0
- package/core/di/module/types/BazisModule.ts +71 -0
- package/core/di/module/types/DiRegistrar.ts +24 -0
- package/core/di/module/types/index.ts +2 -0
- package/core/di/provider/index.ts +26 -0
- package/core/di/provider/providerGuards.ts +17 -0
- package/core/di/provider/types/AsyncFactoryProvider.ts +15 -0
- package/core/di/provider/types/ClassProvider.ts +18 -0
- package/core/di/provider/types/FactoryProvider.ts +20 -0
- package/core/di/provider/types/KeyedDependency.ts +20 -0
- package/core/di/provider/types/LazyDependency.ts +31 -0
- package/core/di/provider/types/NamedDependency.ts +16 -0
- package/core/di/provider/types/Provider.ts +10 -0
- package/core/di/provider/types/ProviderDefinition.ts +11 -0
- package/core/di/provider/types/ProviderDependencyList.ts +11 -0
- package/core/di/provider/types/ProviderLifetime.ts +1 -0
- package/core/di/provider/types/ProviderOwnership.ts +7 -0
- package/core/di/provider/types/ResolvedDeps.ts +18 -0
- package/core/di/provider/types/ValueProvider.ts +6 -0
- package/core/di/provider/types/index.ts +13 -0
- package/core/di/token.ts +84 -0
- package/core/di/types/BuildServiceProviderOptions.ts +4 -0
- package/core/di/types/ServiceKey.ts +1 -0
- package/core/di/types/ServiceResolver.ts +16 -0
- package/core/di/types/index.ts +3 -0
- package/core/generatedFingerprint.ts +60 -0
- package/core/generatedRuntime.ts +187 -0
- package/core/grpc/Grpc.module.ts +23 -0
- package/core/grpc/GrpcBinding.contract.ts +35 -0
- package/core/grpc/GrpcCall.ts +73 -0
- package/core/grpc/GrpcClient.ts +172 -0
- package/core/grpc/GrpcClientCall.ts +267 -0
- package/core/grpc/GrpcError.ts +19 -0
- package/core/grpc/GrpcRequest.binding.ts +45 -0
- package/core/grpc/GrpcServer.ts +204 -0
- package/core/grpc/GrpcStatus.ts +8 -0
- package/core/grpc/GrpcTransport.ts +174 -0
- package/core/grpc/Metadata.ts +73 -0
- package/core/grpc/ProtoCodec.ts +225 -0
- package/core/grpc/ProtoParser.ts +192 -0
- package/core/grpc/ProtoReader.ts +34 -0
- package/core/grpc/ProtoSchema.ts +52 -0
- package/core/grpc/ServerCredentials.ts +30 -0
- package/core/grpc/clientContracts.ts +39 -0
- package/core/grpc/contracts.ts +45 -0
- package/core/grpc/decorators.ts +82 -0
- package/core/grpc/grpcClientProvider.ts +9 -0
- package/core/grpc/index.ts +17 -0
- package/core/grpc/moduleControllers.ts +38 -0
- package/core/grpc/protobuf.ts +80 -0
- package/core/grpc/serviceDefinition.ts +20 -0
- package/core/http/Authorization/Authorize.ts +52 -0
- package/core/http/Authorization/authorizeComposer.ts +31 -0
- package/core/http/Authorization/metadata.ts +158 -0
- package/core/http/Binding/ParameterBinder.ts +109 -0
- package/core/http/Binding/RequestModel.ts +27 -0
- package/core/http/Binding/autoBindings.ts +125 -0
- package/core/http/Binding/bindings.ts +19 -0
- package/core/http/Binding/convert.ts +48 -0
- package/core/http/Binding/generatedSpec.ts +18 -0
- package/core/http/Binding/listModelRegistry.ts +47 -0
- package/core/http/Binding/modelBinder.ts +345 -0
- package/core/http/Binding/modelValidator.ts +30 -0
- package/core/http/Binding/requestModelRegistry.ts +117 -0
- package/core/http/Decorators/attributes.ts +92 -0
- package/core/http/Decorators/controller.ts +24 -0
- package/core/http/Decorators/metadata.ts +107 -0
- package/core/http/Decorators/routes.ts +131 -0
- package/core/http/Errors/HttpError.ts +92 -0
- package/core/http/HttpContext/HttpContext.ts +148 -0
- package/core/http/HttpContext/ResponseBuilder.ts +37 -0
- package/core/http/HttpContext/inspectableRedirects.ts +47 -0
- package/core/http/HttpContext/principal.ts +24 -0
- package/core/http/HttpContext/responseLifetime.ts +45 -0
- package/core/http/HttpServer.ts +627 -0
- package/core/http/Middleware/accessLog.ts +90 -0
- package/core/http/Middleware/cors.ts +160 -0
- package/core/http/Middleware/errorHandler.ts +100 -0
- package/core/http/Middleware/pipeline.ts +21 -0
- package/core/http/Middleware/rateLimit.ts +104 -0
- package/core/http/Middleware/securityHeaders.ts +118 -0
- package/core/http/Middleware/types.ts +19 -0
- package/core/http/OpenApi/generatedOpenApiRegistry.ts +102 -0
- package/core/http/OpenApi/openApiDocs.ts +75 -0
- package/core/http/OpenApi/openApiDocument.ts +356 -0
- package/core/http/README.md +139 -0
- package/core/http/Results/HttpResult.ts +19 -0
- package/core/http/Results/normalize.ts +93 -0
- package/core/http/Results/results.ts +70 -0
- package/core/http/Routing/Router.ts +201 -0
- package/core/http/Routing/RouterBuilder.ts +184 -0
- package/core/http/Routing/template.ts +112 -0
- package/core/http/SPEC.md +981 -0
- package/core/http/Versioning/VersionReader.ts +20 -0
- package/core/http/WebSocketUpgrade.ts +29 -0
- package/core/http/composeRouteMiddlewareComposers.ts +21 -0
- package/core/http/correlation/createCorrelationIdMiddleware.ts +79 -0
- package/core/http/httpModule.ts +48 -0
- package/core/http/index.ts +140 -0
- package/core/http/middlewareTokens.ts +48 -0
- package/core/http/options.ts +134 -0
- package/core/http-client/index.ts +105 -0
- package/core/infra/Infra.ts +158 -0
- package/core/infra/InfraConnector.ts +86 -0
- package/core/infra/InfraLifecycle.ts +86 -0
- package/core/infra/cache/RedisDistributedCacheBackend.ts +88 -0
- package/core/infra/cache/RedisDistributedCacheDriver.ts +186 -0
- package/core/infra/cache/index.ts +7 -0
- package/core/infra/connectorConfig.ts +29 -0
- package/core/infra/connectorIdentity.ts +39 -0
- package/core/infra/connectors/codex/CodexAppServer.ts +146 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +258 -0
- package/core/infra/connectors/codex/CodexPolicy.ts +114 -0
- package/core/infra/connectors/codex/CodexTurn.ts +133 -0
- package/core/infra/connectors/codex/contracts.ts +82 -0
- package/core/infra/connectors/codex.ts +26 -0
- package/core/infra/connectors/llm.ts +78 -0
- package/core/infra/connectors/llmRouter.ts +443 -0
- package/core/infra/connectors/openaiCompatible.ts +590 -0
- package/core/infra/connectors/openaiTextStream.ts +134 -0
- package/core/infra/connectors/opensearch.ts +307 -0
- package/core/infra/connectors/postgres.ts +105 -0
- package/core/infra/connectors/redis.ts +132 -0
- package/core/infra/index.ts +58 -0
- package/core/kernel/ApplicationLifetime.ts +91 -0
- package/core/kernel/Bazis.ts +73 -0
- package/core/kernel/Environment.ts +53 -0
- package/core/kernel/Kernel.ts +391 -0
- package/core/kernel/KernelBuilder.ts +172 -0
- package/core/kernel/LifecycleCoordinator.ts +204 -0
- package/core/kernel/README.md +66 -0
- package/core/kernel/SupervisedHostedService.ts +147 -0
- package/core/kernel/config/ConfigRegistry.ts +48 -0
- package/core/kernel/config/Configuration.ts +110 -0
- package/core/kernel/config/README.md +62 -0
- package/core/kernel/config/Secret.ts +29 -0
- package/core/kernel/config/addConfigOptions.ts +63 -0
- package/core/kernel/config/defineConfig.ts +211 -0
- package/core/kernel/config/sources.ts +123 -0
- package/core/kernel/correlation/requestContext.ts +57 -0
- package/core/kernel/errors/ConfigKeyMissingError.ts +7 -0
- package/core/kernel/errors/KernelError.ts +7 -0
- package/core/kernel/errors/ShutdownTimeoutError.ts +7 -0
- package/core/kernel/errors/StartupAbortedError.ts +7 -0
- package/core/kernel/errors/StartupTimeoutError.ts +7 -0
- package/core/kernel/errors/index.ts +5 -0
- package/core/kernel/events/EventBus.ts +140 -0
- package/core/kernel/events/EventToken.ts +17 -0
- package/core/kernel/events/eventSubscribers.ts +126 -0
- package/core/kernel/events/kernelEvents.ts +16 -0
- package/core/kernel/events/onEvent.ts +32 -0
- package/core/kernel/health/HealthCheckContracts.ts +42 -0
- package/core/kernel/health/HealthService.ts +93 -0
- package/core/kernel/index.ts +96 -0
- package/core/kernel/internal/awaitAbortable.ts +18 -0
- package/core/kernel/internal/awaitShutdown.ts +21 -0
- package/core/kernel/internal/reportDiagnosticFailure.ts +8 -0
- package/core/kernel/internal/validateTimeout.ts +8 -0
- package/core/kernel/lifecycleHooks.ts +18 -0
- package/core/kernel/logging/ConsoleLogger.ts +68 -0
- package/core/kernel/logging/Logger.ts +22 -0
- package/core/kernel/types/ConfigSource.ts +9 -0
- package/core/kernel/types/EnvironmentName.ts +1 -0
- package/core/kernel/types/KernelOptions.ts +23 -0
- package/core/kernel/types/LifecycleHook.ts +18 -0
- package/core/kernel/types/index.ts +4 -0
- package/core/orm/DATABASE_PROVIDER.ts +5 -0
- package/core/orm/OrmHostedPlan.validator.ts +55 -0
- package/core/orm/OrmLifecycle.ts +146 -0
- package/core/orm/OrmOwnedStoreLifecycle.ts +96 -0
- package/core/orm/buildOrmProviders.ts +152 -0
- package/core/orm/databaseConnector.ts +108 -0
- package/core/orm/encapsulationHook.ts +19 -0
- package/core/orm/index.ts +95 -0
- package/core/orm/listQuery.ts +198 -0
- package/core/orm/ormModule.ts +173 -0
- package/core/orm/ownedStoreContributions.ts +195 -0
- package/core/orm/registerRepositories.ts +27 -0
- package/core/orm/repository.ts +45 -0
- package/core/orm/strictSchemaHostedIdentity.ts +17 -0
- package/core/scripts/agent-codegen.ts +1166 -0
- package/core/scripts/di-generate-target.ts +90 -0
- package/core/scripts/di-generate.ts +1837 -0
- package/core/scripts/orm-predicate-codegen.ts +33 -0
- package/core/scripts/request-model-codegen.ts +326 -0
- package/core/websocket/DESIGN.md +297 -0
- package/core/websocket/WsRateLimiter.ts +101 -0
- package/core/websocket/adapter/adapter.interface.ts +45 -0
- package/core/websocket/adapter/in-memory.adapter.ts +190 -0
- package/core/websocket/adapter/redis-delivery-scripts.ts +135 -0
- package/core/websocket/adapter/redis-delivery-store.ts +86 -0
- package/core/websocket/adapter/redis-operations.ts +33 -0
- package/core/websocket/adapter/redis.adapter.ts +218 -0
- package/core/websocket/codec/binary.codec.ts +87 -0
- package/core/websocket/codec/json.codec.ts +34 -0
- package/core/websocket/codec/packet-codec.interface.ts +17 -0
- package/core/websocket/decorators.ts +165 -0
- package/core/websocket/explorer.ts +126 -0
- package/core/websocket/index.ts +78 -0
- package/core/websocket/middleware.ts +146 -0
- package/core/websocket/packet-codec.ts +75 -0
- package/core/websocket/reliable-delivery.ts +56 -0
- package/core/websocket/session-creation.ts +16 -0
- package/core/websocket/session-manager.ts +856 -0
- package/core/websocket/socket-wrapper.ts +314 -0
- package/core/websocket/topic-cache.ts +59 -0
- package/core/websocket/types.ts +122 -0
- package/core/websocket/websocketModule.ts +96 -0
- package/core/websocket/ws-dispatch.ts +123 -0
- package/core/websocket/ws-server.ts +1707 -0
- package/index.ts +281 -0
- package/library/boundary/diagnostics-v1.ts +136 -0
- package/library/boundary/index.ts +73 -0
- package/library/boundary/json/decodeBoundedJsonV1.ts +518 -0
- package/library/boundary/json/limits-v1.ts +46 -0
- package/library/boundary/json/normalizeBoundedJsonV1.ts +256 -0
- package/library/boundary/json/types-v1.ts +11 -0
- package/library/boundary/schema/boundarySchemaHashV1.ts +6 -0
- package/library/boundary/schema/collectBoundaryValueDiagnosticsV1.ts +325 -0
- package/library/boundary/schema/decodeBoundarySchemaV1.ts +646 -0
- package/library/boundary/schema/index.ts +8 -0
- package/library/boundary/schema/types-v1.ts +99 -0
- package/library/boundary/schema/validateBoundaryValueV1.ts +58 -0
- package/library/boundary/serialization/canonicalJsonV1.ts +66 -0
- package/library/http-client/HttpClient.ts +969 -0
- package/library/http-client/HttpClientFactory.ts +45 -0
- package/library/http-client/errors.ts +68 -0
- package/library/http-client/index.ts +43 -0
- package/library/http-client/interceptors.ts +40 -0
- package/library/http-client/progress.ts +62 -0
- package/library/http-client/redirectProtocol.ts +6 -0
- package/library/http-client/serialize.ts +133 -0
- package/library/http-client/types.ts +174 -0
- package/library/jsonapi/ListQueryError.ts +25 -0
- package/library/jsonapi/ListRequest.ts +37 -0
- package/library/jsonapi/decorators.ts +161 -0
- package/library/jsonapi/document.ts +124 -0
- package/library/jsonapi/index.ts +40 -0
- package/library/jsonapi/parseListQuery.ts +291 -0
- package/library/jsonapi/types.ts +125 -0
- package/library/jwt/JwtEncoder.ts +40 -0
- package/library/jwt/JwtKeyRing.ts +164 -0
- package/library/jwt/JwtValidator.ts +210 -0
- package/library/jwt/TokenIssuer.ts +149 -0
- package/library/jwt/TokenService.ts +51 -0
- package/library/jwt/base64url.ts +61 -0
- package/library/jwt/claims.ts +36 -0
- package/library/jwt/errors.ts +44 -0
- package/library/jwt/index.ts +59 -0
- package/library/jwt/limits.ts +8 -0
- package/library/jwt/signing/Hs256Algorithm.ts +62 -0
- package/library/jwt/signing/Rs256Algorithm.ts +133 -0
- package/library/jwt/signing/SigningAlgorithm.ts +18 -0
- package/library/jwt/signing/pem.ts +30 -0
- package/library/openapi/codegen.ts +875 -0
- package/library/openapi/document.ts +326 -0
- package/library/openapi/index.ts +13 -0
- package/library/openapi/types.ts +69 -0
- package/library/openapi/ui.ts +182 -0
- package/library/orm/DatabaseFacade.ts +209 -0
- package/library/orm/DbContext.ts +107 -0
- package/library/orm/DbContextFactory.ts +25 -0
- package/library/orm/DbContextOptions.ts +33 -0
- package/library/orm/Metadata/DynamicModelBuilder.ts +497 -0
- package/library/orm/Metadata/KeyTuple.ts +36 -0
- package/library/orm/Metadata/ModelBuilder.ts +299 -0
- package/library/orm/Metadata/OrmModel.ts +80 -0
- package/library/orm/Metadata/ValueConverter.ts +57 -0
- package/library/orm/Metadata/conventions.ts +54 -0
- package/library/orm/Metadata/decorators.ts +396 -0
- package/library/orm/Metadata/types.ts +140 -0
- package/library/orm/Providers/OwnedStoreCatalog.reader.ts +1802 -0
- package/library/orm/Providers/PostgresDialect.ts +234 -0
- package/library/orm/Providers/PostgresProvider.ts +1494 -0
- package/library/orm/Providers/PostgresSchema.decoder.ts +107 -0
- package/library/orm/Providers/limits.ts +29 -0
- package/library/orm/Providers/operationDeadline.ts +43 -0
- package/library/orm/Providers/ormOwnedStoreRuntime.ts +19 -0
- package/library/orm/Providers/ormTransactionRuntime.ts +117 -0
- package/library/orm/Providers/propertyConversion.ts +14 -0
- package/library/orm/Providers/resolvedForeignKey.ts +16 -0
- package/library/orm/Providers/transactionCallbacks.ts +45 -0
- package/library/orm/Providers/transactionOutcome.ts +82 -0
- package/library/orm/Providers/transactionScopes.ts +78 -0
- package/library/orm/Providers/types.ts +207 -0
- package/library/orm/Query/DbSet.ts +78 -0
- package/library/orm/Query/EntityQuery.ts +247 -0
- package/library/orm/Query/ImmediateMutations.ts +226 -0
- package/library/orm/Query/IncludeLoader.ts +200 -0
- package/library/orm/Query/QueryPlan.ts +55 -0
- package/library/orm/Query/SqlTranslator.ts +195 -0
- package/library/orm/Query/conditions.ts +127 -0
- package/library/orm/Query/materialize.ts +39 -0
- package/library/orm/README.md +256 -0
- package/library/orm/Repository/README.md +684 -0
- package/library/orm/Repository/Repository.ts +68 -0
- package/library/orm/Saving/CommandBuilder.ts +139 -0
- package/library/orm/Saving/ExecutionStrategy.ts +128 -0
- package/library/orm/Saving/SaveExecutor.ts +413 -0
- package/library/orm/Saving/applyConventions.ts +64 -0
- package/library/orm/Schema/CheckExpression.ts +327 -0
- package/library/orm/Schema/ExactSchemaVerifier.ts +168 -0
- package/library/orm/Schema/ExpectedSchema.ts +43 -0
- package/library/orm/Schema/MigrationRunner.ts +146 -0
- package/library/orm/Schema/Migrator.ts +73 -0
- package/library/orm/Schema/OrmOwnedStore.ts +123 -0
- package/library/orm/Schema/OwnedStoreAdmission.ts +752 -0
- package/library/orm/Schema/OwnedStoreCanonical.ts +273 -0
- package/library/orm/Schema/OwnedStoreCatalog.ts +1325 -0
- package/library/orm/Schema/SafeAdditiveSchema.ts +100 -0
- package/library/orm/Schema/SchemaAdmission.ts +62 -0
- package/library/orm/Schema/SchemaDiffer.ts +78 -0
- package/library/orm/Schema/formatSchemaDifferences.ts +62 -0
- package/library/orm/Schema/introspection.ts +52 -0
- package/library/orm/Schema/physicalColumnTypes.ts +37 -0
- package/library/orm/Schema/tableKey.ts +26 -0
- package/library/orm/Tracking/ChangeTracker.ts +448 -0
- package/library/orm/Tracking/EntityState.ts +13 -0
- package/library/orm/Transactions/OrmTransaction.ts +30 -0
- package/library/orm/Transactions/TransactionScopeCoordinator.ts +377 -0
- package/library/orm/errors.ts +190 -0
- package/library/orm/index.ts +162 -0
- package/library/orm/runtime.ts +14 -0
- package/library/redaction/index.ts +166 -0
- package/library/ui/UiProfile.ts +77 -0
- package/library/ui/compiled-v1.ts +131 -0
- package/library/ui/compiler/compileUiSurfaceV1.ts +717 -0
- package/library/ui/compiler/contracts-v1.ts +25 -0
- package/library/ui/compiler/index.ts +12 -0
- package/library/ui/compiler/openApiIndex.ts +246 -0
- package/library/ui/defineUiProfile.ts +340 -0
- package/library/ui/diagnostics-v1.ts +33 -0
- package/library/ui/index.ts +129 -0
- package/library/ui/profile-v1.ts +191 -0
- package/library/ui/serialization.ts +130 -0
- package/library/ui/uiProfileAuthoring.ts +289 -0
- package/library/ui/uiProfileAuthoringMetadata.ts +78 -0
- package/library/validation/MessageRegistry.ts +144 -0
- package/library/validation/README.md +62 -0
- package/library/validation/RuleCompiler.ts +183 -0
- package/library/validation/RuleEngine.ts +401 -0
- package/library/validation/SPEC.md +579 -0
- package/library/validation/ValidationError.ts +37 -0
- package/library/validation/ValidationResult.ts +63 -0
- package/library/validation/Validator.ts +101 -0
- package/library/validation/index.ts +16 -0
- package/library/validation/metadata.ts +69 -0
- package/library/validation/modelValidatorAdapter.ts +119 -0
- package/library/validation/types/ValidationCode.ts +69 -0
- package/library/validation/types/ValidatorOptions.ts +153 -0
- package/package.json +80 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { FilterOperator, ListQueryOptions, PageOptions } from "./types";
|
|
2
|
+
|
|
3
|
+
// Polyfill: Bun runs TC39 decorators natively, but Symbol.metadata may be
|
|
4
|
+
// missing at runtime. Symbol.for gives one shared symbol for all module copies.
|
|
5
|
+
(Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata");
|
|
6
|
+
|
|
7
|
+
/** Key under which the list-request schema is stored in the class metadata. */
|
|
8
|
+
const LIST_SCHEMA = Symbol.for("bazis:listing:schema");
|
|
9
|
+
|
|
10
|
+
/** Permissions of one field: whether it is sortable and which filter operators are allowed. */
|
|
11
|
+
export interface FieldSchema {
|
|
12
|
+
sortable: boolean;
|
|
13
|
+
readonly filterOps: FilterOperator[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The whole request-class schema (fields + paging limits + allowed includes). */
|
|
17
|
+
export interface ListSchema {
|
|
18
|
+
readonly fields: Record<string, FieldSchema>;
|
|
19
|
+
page?: PageOptions;
|
|
20
|
+
include?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SchemaMetadata {
|
|
24
|
+
[LIST_SCHEMA]?: ListSchema;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parameters of the {@link ListOptions} decorator (class level). */
|
|
28
|
+
export interface ListOptionsConfig {
|
|
29
|
+
/** Default page size. */
|
|
30
|
+
readonly defaultSize?: number;
|
|
31
|
+
/** Maximum allowed page size. */
|
|
32
|
+
readonly maxSize?: number;
|
|
33
|
+
/** Allowed include paths (`?include=`). */
|
|
34
|
+
readonly include?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* TC39 decorator metadata is inherited prototypically: a subclass's metadata
|
|
39
|
+
* has the parent's metadata as its prototype. On the first write to a concrete
|
|
40
|
+
* class we copy on write so the parent schema is not mutated.
|
|
41
|
+
*/
|
|
42
|
+
function ensureOwnSchema(metadata: SchemaMetadata): ListSchema {
|
|
43
|
+
if (!Object.prototype.hasOwnProperty.call(metadata, LIST_SCHEMA)) {
|
|
44
|
+
const inherited = metadata[LIST_SCHEMA];
|
|
45
|
+
metadata[LIST_SCHEMA] = inherited
|
|
46
|
+
? { fields: cloneFields(inherited.fields), page: inherited.page, include: inherited.include }
|
|
47
|
+
: { fields: {} };
|
|
48
|
+
}
|
|
49
|
+
return metadata[LIST_SCHEMA]!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cloneFields(fields: Record<string, FieldSchema>): Record<string, FieldSchema> {
|
|
53
|
+
const out: Record<string, FieldSchema> = {};
|
|
54
|
+
for (const [name, schema] of Object.entries(fields)) {
|
|
55
|
+
out[name] = { sortable: schema.sortable, filterOps: [...schema.filterOps] };
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fieldOf(schema: ListSchema, name: string): FieldSchema {
|
|
61
|
+
return (schema.fields[name] ??= { sortable: false, filterOps: [] });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertInstanceField(context: ClassFieldDecoratorContext, decorator: string): void {
|
|
65
|
+
if (context.static) {
|
|
66
|
+
throw new Error(`${decorator}: static field "${String(context.name)}" is not supported.`);
|
|
67
|
+
}
|
|
68
|
+
if (context.private) {
|
|
69
|
+
throw new Error(`${decorator}: private field "${String(context.name)}" is not supported.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Allows sorting by the field (`?sort=field` / `?sort=-field`).
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* class UserListQuery extends ListRequest<User> {
|
|
78
|
+
* @Sortable() name!: string;
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function Sortable() {
|
|
83
|
+
return (_value: undefined, context: ClassFieldDecoratorContext): void => {
|
|
84
|
+
assertInstanceField(context, "@Sortable");
|
|
85
|
+
fieldOf(ensureOwnSchema(context.metadata as SchemaMetadata), String(context.name)).sortable = true;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Allows filtering by the field with the listed operators
|
|
91
|
+
* (`?filter[field][op]=value`).
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* class UserListQuery extends ListRequest<User> {
|
|
95
|
+
* @Filterable("eq", "contains") name!: string;
|
|
96
|
+
* @Filterable("gte", "lte", "in") age!: number;
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function Filterable(...operators: FilterOperator[]) {
|
|
101
|
+
return (_value: undefined, context: ClassFieldDecoratorContext): void => {
|
|
102
|
+
assertInstanceField(context, "@Filterable");
|
|
103
|
+
const field = fieldOf(ensureOwnSchema(context.metadata as SchemaMetadata), String(context.name));
|
|
104
|
+
for (const operator of operators) {
|
|
105
|
+
if (!field.filterOps.includes(operator)) {
|
|
106
|
+
field.filterOps.push(operator);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Paging limits and allowed includes for a request class.
|
|
114
|
+
*
|
|
115
|
+
* ```ts
|
|
116
|
+
* @ListOptions({ defaultSize: 20, maxSize: 100, include: ["posts"] })
|
|
117
|
+
* class UserListQuery extends ListRequest<User> { ... }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function ListOptions(config: ListOptionsConfig) {
|
|
121
|
+
return (_value: unknown, context: ClassDecoratorContext): void => {
|
|
122
|
+
const schema = ensureOwnSchema(context.metadata as SchemaMetadata);
|
|
123
|
+
schema.page = { defaultSize: config.defaultSize, maxSize: config.maxSize };
|
|
124
|
+
if (config.include !== undefined) {
|
|
125
|
+
schema.include = config.include;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Schema of a request class (including the inherited one), or `undefined`. */
|
|
131
|
+
export function listSchemaOf(ctor: unknown): ListSchema | undefined {
|
|
132
|
+
if (typeof ctor !== "function") {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const metadata = (ctor as unknown as { [key: symbol]: unknown })[Symbol.metadata as symbol] as
|
|
136
|
+
| SchemaMetadata
|
|
137
|
+
| undefined;
|
|
138
|
+
return metadata?.[LIST_SCHEMA];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Builds {@link ListQueryOptions} (the allow list for {@link parseListQuery})
|
|
143
|
+
* from the declarative schema of a request class.
|
|
144
|
+
*/
|
|
145
|
+
export function optionsFromSchema(ctor: unknown): ListQueryOptions {
|
|
146
|
+
const schema = listSchemaOf(ctor);
|
|
147
|
+
if (schema === undefined) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
const sort: string[] = [];
|
|
151
|
+
const filter: Record<string, readonly FilterOperator[]> = {};
|
|
152
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
153
|
+
if (field.sortable) {
|
|
154
|
+
sort.push(name);
|
|
155
|
+
}
|
|
156
|
+
if (field.filterOps.length > 0) {
|
|
157
|
+
filter[name] = field.filterOps;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { sort, filter, include: schema.include, page: schema.page };
|
|
161
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ListQuery } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Collection metadata (page counters), put into the response `meta`. */
|
|
4
|
+
export interface ListMeta {
|
|
5
|
+
/** Total number of records with filters applied (without paging). */
|
|
6
|
+
readonly total: number;
|
|
7
|
+
/** Current page number (1-based). */
|
|
8
|
+
readonly page: number;
|
|
9
|
+
/** Page size. */
|
|
10
|
+
readonly size: number;
|
|
11
|
+
/** Total pages = `ceil(total / size)`. */
|
|
12
|
+
readonly pageCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** JSON:API-style paging links (`links`). */
|
|
16
|
+
export interface ListLinks {
|
|
17
|
+
readonly self: string;
|
|
18
|
+
readonly first: string;
|
|
19
|
+
readonly last: string;
|
|
20
|
+
readonly prev?: string;
|
|
21
|
+
readonly next?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collection response document. `data` holds the items (entities or DTOs),
|
|
26
|
+
* `meta` the counters, `links` the page navigation (if `basePath` is set).
|
|
27
|
+
*/
|
|
28
|
+
export interface ListDocument<T> {
|
|
29
|
+
readonly data: readonly T[];
|
|
30
|
+
readonly meta: ListMeta;
|
|
31
|
+
readonly links?: ListLinks;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options for building a {@link ListDocument}. */
|
|
35
|
+
export interface BuildListDocumentOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Base path for paging links, for example `"/api/users"`. If not set,
|
|
38
|
+
* `links` are not built.
|
|
39
|
+
*/
|
|
40
|
+
readonly basePath?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds a JSON:API-like collection document from the page items, the original
|
|
45
|
+
* request and the total number of records.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const { items, total } = await paginate(repo.query(), query);
|
|
49
|
+
* return buildListDocument(items, query, total, { basePath: "/api/users" });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function buildListDocument<T>(
|
|
53
|
+
items: readonly T[],
|
|
54
|
+
query: ListQuery,
|
|
55
|
+
total: number,
|
|
56
|
+
options: BuildListDocumentOptions = {},
|
|
57
|
+
): ListDocument<T> {
|
|
58
|
+
const size = query.page.size;
|
|
59
|
+
const pageCount = size > 0 ? Math.max(1, Math.ceil(total / size)) : 1;
|
|
60
|
+
const meta: ListMeta = { total, page: query.page.number, size, pageCount };
|
|
61
|
+
|
|
62
|
+
if (options.basePath === undefined) {
|
|
63
|
+
return { data: items, meta };
|
|
64
|
+
}
|
|
65
|
+
return { data: items, meta, links: buildLinks(query, options.basePath, meta) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Serializes the request into a JSON:API query string (without the leading `?`).
|
|
70
|
+
* Useful for building links and for round trips in tests.
|
|
71
|
+
*/
|
|
72
|
+
export function serializeListQuery(query: ListQuery, pageNumber?: number): string {
|
|
73
|
+
return toSearchParams(query, pageNumber).toString();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildLinks(query: ListQuery, basePath: string, meta: ListMeta): ListLinks {
|
|
77
|
+
const link = (page: number): string => `${basePath}?${serializeListQuery(query, page)}`;
|
|
78
|
+
const links: ListLinks = {
|
|
79
|
+
self: link(meta.page),
|
|
80
|
+
first: link(1),
|
|
81
|
+
last: link(meta.pageCount),
|
|
82
|
+
...(meta.page > 1 ? { prev: link(meta.page - 1) } : {}),
|
|
83
|
+
...(meta.page < meta.pageCount ? { next: link(meta.page + 1) } : {}),
|
|
84
|
+
};
|
|
85
|
+
return links;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toSearchParams(query: ListQuery, pageNumber?: number): URLSearchParams {
|
|
89
|
+
const params = new URLSearchParams();
|
|
90
|
+
|
|
91
|
+
if (query.sort.length > 0) {
|
|
92
|
+
params.set("sort", query.sort.map((s) => `${s.dir === "desc" ? "-" : ""}${s.field}`).join(","));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const rule of query.filters) {
|
|
96
|
+
const key = rule.op === "eq" ? `filter[${rule.field}]` : `filter[${rule.field}][${rule.op}]`;
|
|
97
|
+
const value = Array.isArray(rule.value) ? rule.value.join(",") : (rule.value as string);
|
|
98
|
+
params.append(key, value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
query.or.forEach((group, index) => {
|
|
102
|
+
for (const rule of group) {
|
|
103
|
+
const key =
|
|
104
|
+
rule.op === "eq"
|
|
105
|
+
? `filter[or][${index}][${rule.field}]`
|
|
106
|
+
: `filter[or][${index}][${rule.field}][${rule.op}]`;
|
|
107
|
+
const value = Array.isArray(rule.value) ? rule.value.join(",") : (rule.value as string);
|
|
108
|
+
params.append(key, value);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
params.set("page[number]", String(pageNumber ?? query.page.number));
|
|
113
|
+
params.set("page[size]", String(query.page.size));
|
|
114
|
+
|
|
115
|
+
if (query.include.length > 0) {
|
|
116
|
+
params.set("include", query.include.join(","));
|
|
117
|
+
}
|
|
118
|
+
for (const [type, list] of Object.entries(query.fields)) {
|
|
119
|
+
if (list.length > 0) {
|
|
120
|
+
params.set(`fields[${type}]`, list.join(","));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return params;
|
|
124
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal list request in the style of the JSON:API specification.
|
|
3
|
+
*
|
|
4
|
+
* A pure library (Web APIs only, zero dependencies on the kernel/environment),
|
|
5
|
+
* compatible with binary compilation. It consists of:
|
|
6
|
+
* - {@link parseListQuery}: parses query parameters (`sort`/`filter`/`page`/
|
|
7
|
+
* `include`/`fields`) into a typed, validated {@link ListQuery};
|
|
8
|
+
* - {@link buildListDocument}: builds the collection response (`data`/`meta`/`links`);
|
|
9
|
+
* - {@link ListQueryError}: the parsing error (the HTTP layer maps it to 400).
|
|
10
|
+
*
|
|
11
|
+
* Applying the request to a data source is a separate integration layer:
|
|
12
|
+
* the ORM bridge is `paginate(...)` in `@/core/orm`, the binding is `ListRequest`
|
|
13
|
+
* in `@/core/http`.
|
|
14
|
+
*/
|
|
15
|
+
export { parseListQuery } from "./parseListQuery";
|
|
16
|
+
export { buildListDocument, serializeListQuery } from "./document";
|
|
17
|
+
export type { BuildListDocumentOptions, ListDocument, ListLinks, ListMeta } from "./document";
|
|
18
|
+
export { ListQueryError, type ListQueryProblem } from "./ListQueryError";
|
|
19
|
+
export { ListRequest } from "./ListRequest";
|
|
20
|
+
export {
|
|
21
|
+
Filterable,
|
|
22
|
+
ListOptions,
|
|
23
|
+
Sortable,
|
|
24
|
+
listSchemaOf,
|
|
25
|
+
optionsFromSchema,
|
|
26
|
+
type FieldSchema,
|
|
27
|
+
type ListOptionsConfig,
|
|
28
|
+
type ListSchema,
|
|
29
|
+
} from "./decorators";
|
|
30
|
+
export type {
|
|
31
|
+
FilterGroup,
|
|
32
|
+
FilterOperator,
|
|
33
|
+
FilterRule,
|
|
34
|
+
ListQuery,
|
|
35
|
+
ListQueryOptions,
|
|
36
|
+
PageOptions,
|
|
37
|
+
PageParams,
|
|
38
|
+
SortDirection,
|
|
39
|
+
SortField,
|
|
40
|
+
} from "./types";
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { ListQueryError, type ListQueryProblem } from "./ListQueryError";
|
|
2
|
+
import type {
|
|
3
|
+
FilterGroup,
|
|
4
|
+
FilterOperator,
|
|
5
|
+
FilterRule,
|
|
6
|
+
ListQuery,
|
|
7
|
+
ListQueryOptions,
|
|
8
|
+
PageParams,
|
|
9
|
+
SortDirection,
|
|
10
|
+
SortField,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
14
|
+
const MAX_PAGE_SIZE = 100;
|
|
15
|
+
|
|
16
|
+
const FILTER_OPERATORS: ReadonlySet<string> = new Set<FilterOperator>([
|
|
17
|
+
"eq",
|
|
18
|
+
"ne",
|
|
19
|
+
"gt",
|
|
20
|
+
"gte",
|
|
21
|
+
"lt",
|
|
22
|
+
"lte",
|
|
23
|
+
"like",
|
|
24
|
+
"contains",
|
|
25
|
+
"startsWith",
|
|
26
|
+
"endsWith",
|
|
27
|
+
"in",
|
|
28
|
+
"nin",
|
|
29
|
+
"isNull",
|
|
30
|
+
"isNotNull",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const VALUELESS_OPERATORS: ReadonlySet<FilterOperator> = new Set(["isNull", "isNotNull"]);
|
|
34
|
+
const MULTI_VALUE_OPERATORS: ReadonlySet<FilterOperator> = new Set(["in", "nin"]);
|
|
35
|
+
|
|
36
|
+
/** `filter[field]` or `filter[field][op]`. */
|
|
37
|
+
const FILTER_KEY = /^filter\[([^\]]+)\](?:\[([^\]]+)\])?$/;
|
|
38
|
+
/** `filter[or][N][field]` or `filter[or][N][field][op]` (N is the OR group index). */
|
|
39
|
+
const FILTER_OR_KEY = /^filter\[or\]\[(\d+)\]\[([^\]]+)\](?:\[([^\]]+)\])?$/;
|
|
40
|
+
/** `fields[type]`. */
|
|
41
|
+
const FIELDS_KEY = /^fields\[([^\]]+)\]$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses JSON:API-style query parameters into a typed {@link ListQuery}.
|
|
45
|
+
*
|
|
46
|
+
* Secure by default: sorting/filtering/include are allowed only for fields
|
|
47
|
+
* from {@link ListQueryOptions}; anything else produces a {@link ListQueryError}
|
|
48
|
+
* (with the list of all problems at once).
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* const query = parseListQuery(url.searchParams, {
|
|
52
|
+
* sort: ["name", "createdAt"],
|
|
53
|
+
* filter: { age: ["gte", "lte"], name: ["contains"] },
|
|
54
|
+
* page: { defaultSize: 20, maxSize: 100 },
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function parseListQuery(params: URLSearchParams, options: ListQueryOptions = {}): ListQuery {
|
|
59
|
+
const problems: ListQueryProblem[] = [];
|
|
60
|
+
|
|
61
|
+
const sort = parseSort(params, options, problems);
|
|
62
|
+
const filters = parseFilters(params, options, problems);
|
|
63
|
+
const or = parseOrGroups(params, options, problems);
|
|
64
|
+
const page = parsePage(params, options, problems);
|
|
65
|
+
const include = parseInclude(params, options, problems);
|
|
66
|
+
const fields = parseFields(params);
|
|
67
|
+
|
|
68
|
+
if (problems.length > 0) {
|
|
69
|
+
throw new ListQueryError(problems);
|
|
70
|
+
}
|
|
71
|
+
return { sort, filters, or, page, include, fields };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseSort(
|
|
75
|
+
params: URLSearchParams,
|
|
76
|
+
options: ListQueryOptions,
|
|
77
|
+
problems: ListQueryProblem[],
|
|
78
|
+
): SortField[] {
|
|
79
|
+
const result: SortField[] = [];
|
|
80
|
+
for (const token of splitCsv(params.getAll("sort"))) {
|
|
81
|
+
const dir: SortDirection = token.startsWith("-") ? "desc" : "asc";
|
|
82
|
+
const field = token.replace(/^[-+]/, "");
|
|
83
|
+
if (field === "") {
|
|
84
|
+
problems.push({ parameter: "sort", message: `empty sort field in "${token}"` });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!options.sort?.includes(field)) {
|
|
88
|
+
problems.push({ parameter: "sort", message: `sorting by "${field}" is not allowed` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
result.push({ field, dir });
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseFilters(
|
|
97
|
+
params: URLSearchParams,
|
|
98
|
+
options: ListQueryOptions,
|
|
99
|
+
problems: ListQueryProblem[],
|
|
100
|
+
): FilterRule[] {
|
|
101
|
+
const result: FilterRule[] = [];
|
|
102
|
+
for (const [key, rawValue] of params) {
|
|
103
|
+
const match = FILTER_KEY.exec(key);
|
|
104
|
+
if (!match) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const field = match[1]!;
|
|
108
|
+
const opToken = match[2] ?? "eq";
|
|
109
|
+
const parameter = match[2] ? `filter[${field}][${opToken}]` : `filter[${field}]`;
|
|
110
|
+
const rule = parseFilterLeaf(field, opToken, rawValue, parameter, options, problems);
|
|
111
|
+
if (rule !== undefined) {
|
|
112
|
+
result.push(rule);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parses OR groups `filter[or][N][field][op]=v`. Conditions with the same `N`
|
|
120
|
+
* form a group (AND); groups are returned in ascending `N` order (different
|
|
121
|
+
* groups are OR). Each leaf is validated against the same allow list as the
|
|
122
|
+
* top-level filters.
|
|
123
|
+
*/
|
|
124
|
+
function parseOrGroups(
|
|
125
|
+
params: URLSearchParams,
|
|
126
|
+
options: ListQueryOptions,
|
|
127
|
+
problems: ListQueryProblem[],
|
|
128
|
+
): FilterGroup[] {
|
|
129
|
+
const groups = new Map<number, FilterRule[]>();
|
|
130
|
+
for (const [key, rawValue] of params) {
|
|
131
|
+
const match = FILTER_OR_KEY.exec(key);
|
|
132
|
+
if (!match) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const index = Number(match[1]);
|
|
136
|
+
const field = match[2]!;
|
|
137
|
+
const opToken = match[3] ?? "eq";
|
|
138
|
+
const parameter = match[3]
|
|
139
|
+
? `filter[or][${index}][${field}][${opToken}]`
|
|
140
|
+
: `filter[or][${index}][${field}]`;
|
|
141
|
+
const rule = parseFilterLeaf(field, opToken, rawValue, parameter, options, problems);
|
|
142
|
+
if (rule !== undefined) {
|
|
143
|
+
const group = groups.get(index) ?? [];
|
|
144
|
+
group.push(rule);
|
|
145
|
+
groups.set(index, group);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...groups.keys()].sort((a, b) => a - b).map((key) => groups.get(key)!);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Parses and validates one filter condition (`field`/`op`/`value`). */
|
|
152
|
+
function parseFilterLeaf(
|
|
153
|
+
field: string,
|
|
154
|
+
opToken: string,
|
|
155
|
+
rawValue: string,
|
|
156
|
+
parameter: string,
|
|
157
|
+
options: ListQueryOptions,
|
|
158
|
+
problems: ListQueryProblem[],
|
|
159
|
+
): FilterRule | undefined {
|
|
160
|
+
if (!FILTER_OPERATORS.has(opToken)) {
|
|
161
|
+
problems.push({ parameter, message: `unknown filter operator "${opToken}"` });
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const op = opToken as FilterOperator;
|
|
165
|
+
|
|
166
|
+
const allowedOps = options.filter?.[field];
|
|
167
|
+
if (!allowedOps) {
|
|
168
|
+
problems.push({ parameter, message: `filtering by "${field}" is not allowed` });
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!allowedOps.includes(op)) {
|
|
172
|
+
problems.push({ parameter, message: `operator "${op}" is not allowed for "${field}"` });
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (VALUELESS_OPERATORS.has(op)) {
|
|
177
|
+
return { field, op, value: "" };
|
|
178
|
+
}
|
|
179
|
+
if (MULTI_VALUE_OPERATORS.has(op)) {
|
|
180
|
+
const values = rawValue
|
|
181
|
+
.split(",")
|
|
182
|
+
.map((value) => value.trim())
|
|
183
|
+
.filter((value) => value !== "");
|
|
184
|
+
if (values.length === 0) {
|
|
185
|
+
problems.push({ parameter, message: `operator "${op}" needs a non-empty comma-separated list` });
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return { field, op, value: values };
|
|
189
|
+
}
|
|
190
|
+
return { field, op, value: rawValue };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parsePage(
|
|
194
|
+
params: URLSearchParams,
|
|
195
|
+
options: ListQueryOptions,
|
|
196
|
+
problems: ListQueryProblem[],
|
|
197
|
+
): PageParams {
|
|
198
|
+
const maxSize = options.page?.maxSize ?? MAX_PAGE_SIZE;
|
|
199
|
+
const defaultSize = clamp(options.page?.defaultSize ?? DEFAULT_PAGE_SIZE, 1, maxSize);
|
|
200
|
+
|
|
201
|
+
const requestedSize = parsePositiveInt(params.get("page[size]") ?? params.get("page[limit]"), "page[size]", problems);
|
|
202
|
+
const size = requestedSize === undefined ? defaultSize : clamp(requestedSize, 1, maxSize);
|
|
203
|
+
|
|
204
|
+
const offsetRaw = params.get("page[offset]");
|
|
205
|
+
if (offsetRaw !== null && offsetRaw !== "") {
|
|
206
|
+
const offset = parseNonNegativeInt(offsetRaw, "page[offset]", problems) ?? 0;
|
|
207
|
+
return { number: Math.floor(offset / size) + 1, size, offset, limit: size };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const number = parsePositiveInt(params.get("page[number]"), "page[number]", problems) ?? 1;
|
|
211
|
+
return { number, size, offset: (number - 1) * size, limit: size };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseInclude(
|
|
215
|
+
params: URLSearchParams,
|
|
216
|
+
options: ListQueryOptions,
|
|
217
|
+
problems: ListQueryProblem[],
|
|
218
|
+
): string[] {
|
|
219
|
+
const result: string[] = [];
|
|
220
|
+
for (const path of splitCsv(params.getAll("include"))) {
|
|
221
|
+
if (!options.include?.includes(path)) {
|
|
222
|
+
problems.push({ parameter: "include", message: `include "${path}" is not allowed` });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
result.push(path);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseFields(params: URLSearchParams): Record<string, readonly string[]> {
|
|
231
|
+
const fields: Record<string, string[]> = {};
|
|
232
|
+
for (const [key, value] of params) {
|
|
233
|
+
const match = FIELDS_KEY.exec(key);
|
|
234
|
+
if (!match) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const type = match[1]!;
|
|
238
|
+
const list = value
|
|
239
|
+
.split(",")
|
|
240
|
+
.map((item) => item.trim())
|
|
241
|
+
.filter((item) => item !== "");
|
|
242
|
+
fields[type] = (fields[type] ?? []).concat(list);
|
|
243
|
+
}
|
|
244
|
+
return fields;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function splitCsv(values: readonly string[]): string[] {
|
|
248
|
+
const tokens: string[] = [];
|
|
249
|
+
for (const value of values) {
|
|
250
|
+
for (const token of value.split(",")) {
|
|
251
|
+
const trimmed = token.trim();
|
|
252
|
+
if (trimmed !== "") {
|
|
253
|
+
tokens.push(trimmed);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return tokens;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parsePositiveInt(
|
|
261
|
+
raw: string | null,
|
|
262
|
+
parameter: string,
|
|
263
|
+
problems: ListQueryProblem[],
|
|
264
|
+
): number | undefined {
|
|
265
|
+
if (raw === null || raw === "") {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
const value = Number(raw);
|
|
269
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
270
|
+
problems.push({ parameter, message: "must be a positive integer" });
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseNonNegativeInt(
|
|
277
|
+
raw: string,
|
|
278
|
+
parameter: string,
|
|
279
|
+
problems: ListQueryProblem[],
|
|
280
|
+
): number | undefined {
|
|
281
|
+
const value = Number(raw);
|
|
282
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
283
|
+
problems.push({ parameter, message: "must be a non-negative integer" });
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function clamp(value: number, min: number, max: number): number {
|
|
290
|
+
return Math.min(Math.max(value, min), max);
|
|
291
|
+
}
|