@fougere/core 0.2.0-alpha.2 → 0.4.0-alpha.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/README.md +3 -2
- package/dist/boot/AppLifecycle.d.ts +18 -0
- package/dist/boot/AppLifecycle.d.ts.map +1 -0
- package/dist/boot/AppLifecycle.js +42 -0
- package/dist/boot/AppLifecycle.js.map +1 -0
- package/dist/boot/Emissions.d.ts +126 -0
- package/dist/boot/Emissions.d.ts.map +1 -0
- package/dist/boot/Emissions.js +233 -0
- package/dist/boot/Emissions.js.map +1 -0
- package/dist/boot/HandlerFacade.d.ts +72 -0
- package/dist/boot/HandlerFacade.d.ts.map +1 -0
- package/dist/boot/HandlerFacade.js +174 -0
- package/dist/boot/HandlerFacade.js.map +1 -0
- package/dist/boot/Lifecycle.d.ts +4 -0
- package/dist/boot/Lifecycle.d.ts.map +1 -0
- package/dist/boot/Lifecycle.js +3 -0
- package/dist/boot/Lifecycle.js.map +1 -0
- package/dist/boot/ambient-port.d.ts +38 -0
- package/dist/boot/ambient-port.d.ts.map +1 -0
- package/dist/boot/ambient-port.js +2 -0
- package/dist/boot/ambient-port.js.map +1 -0
- package/dist/boot/ambient.als.d.ts +3 -0
- package/dist/boot/ambient.als.d.ts.map +1 -0
- package/dist/boot/ambient.als.js +34 -0
- package/dist/boot/ambient.als.js.map +1 -0
- package/dist/boot/ambient.queue.d.ts +10 -0
- package/dist/boot/ambient.queue.d.ts.map +1 -0
- package/dist/boot/ambient.queue.js +60 -0
- package/dist/boot/ambient.queue.js.map +1 -0
- package/dist/boot/apply.d.ts +22 -0
- package/dist/boot/apply.d.ts.map +1 -0
- package/dist/boot/apply.js +38 -0
- package/dist/boot/apply.js.map +1 -0
- package/dist/boot/auth.d.ts +53 -0
- package/dist/boot/auth.d.ts.map +1 -0
- package/dist/boot/auth.js +2 -0
- package/dist/boot/auth.js.map +1 -0
- package/dist/{binding.d.ts → boot/binding.d.ts} +14 -17
- package/dist/boot/binding.d.ts.map +1 -0
- package/dist/{binding.js → boot/binding.js} +19 -10
- package/dist/boot/binding.js.map +1 -0
- package/dist/{boot.d.ts → boot/boot.d.ts} +25 -5
- package/dist/boot/boot.d.ts.map +1 -0
- package/dist/boot/boot.js +70 -0
- package/dist/boot/boot.js.map +1 -0
- package/dist/boot/bootstrap.d.ts.map +1 -0
- package/dist/boot/bootstrap.js +722 -0
- package/dist/boot/bootstrap.js.map +1 -0
- package/dist/boot/frame.d.ts +44 -0
- package/dist/boot/frame.d.ts.map +1 -0
- package/dist/boot/frame.js +172 -0
- package/dist/boot/frame.js.map +1 -0
- package/dist/boot/ownership.d.ts +42 -0
- package/dist/boot/ownership.d.ts.map +1 -0
- package/dist/boot/ownership.js +127 -0
- package/dist/boot/ownership.js.map +1 -0
- package/dist/boot/ports.d.ts +17 -0
- package/dist/boot/ports.d.ts.map +1 -0
- package/dist/boot/ports.js +57 -0
- package/dist/boot/ports.js.map +1 -0
- package/dist/{remote.d.ts → boot/remote.d.ts} +11 -3
- package/dist/boot/remote.d.ts.map +1 -0
- package/dist/{remote.js → boot/remote.js} +26 -28
- package/dist/boot/remote.js.map +1 -0
- package/dist/{seed.d.ts → boot/seed.d.ts} +13 -1
- package/dist/boot/seed.d.ts.map +1 -0
- package/dist/{seed.js → boot/seed.js} +24 -3
- package/dist/boot/seed.js.map +1 -0
- package/dist/boot/together.d.ts +26 -0
- package/dist/boot/together.d.ts.map +1 -0
- package/dist/boot/together.js +163 -0
- package/dist/boot/together.js.map +1 -0
- package/dist/boot/types.d.ts +260 -0
- package/dist/boot/types.d.ts.map +1 -0
- package/dist/boot/types.js.map +1 -0
- package/dist/builtins/config.d.ts +9 -0
- package/dist/builtins/config.d.ts.map +1 -1
- package/dist/builtins/config.js +16 -1
- package/dist/builtins/config.js.map +1 -1
- package/dist/builtins/logger.d.ts +37 -4
- package/dist/builtins/logger.d.ts.map +1 -1
- package/dist/builtins/logger.js +66 -7
- package/dist/builtins/logger.js.map +1 -1
- package/dist/config-loader.d.ts +39 -35
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +15 -54
- package/dist/config-loader.js.map +1 -1
- package/dist/contract/Call.d.ts +10 -0
- package/dist/contract/Call.d.ts.map +1 -0
- package/dist/contract/Call.js +15 -0
- package/dist/contract/Call.js.map +1 -0
- package/dist/contract/Invocation.d.ts +27 -0
- package/dist/contract/Invocation.d.ts.map +1 -0
- package/dist/contract/Invocation.js +59 -0
- package/dist/contract/Invocation.js.map +1 -0
- package/dist/contract/RouteAddress.d.ts +17 -0
- package/dist/contract/RouteAddress.d.ts.map +1 -0
- package/dist/contract/RouteAddress.js +38 -0
- package/dist/contract/RouteAddress.js.map +1 -0
- package/dist/contract.d.ts +25 -19
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +25 -10
- package/dist/contract.js.map +1 -1
- package/dist/crypto/encoding.d.ts +25 -0
- package/dist/crypto/encoding.d.ts.map +1 -0
- package/dist/crypto/encoding.js +58 -0
- package/dist/crypto/encoding.js.map +1 -0
- package/dist/crypto/node.d.ts +4 -0
- package/dist/crypto/node.d.ts.map +1 -0
- package/dist/crypto/node.js +25 -0
- package/dist/crypto/node.js.map +1 -0
- package/dist/crypto/port.d.ts +42 -0
- package/dist/crypto/port.d.ts.map +1 -0
- package/dist/crypto/port.js +19 -0
- package/dist/crypto/port.js.map +1 -0
- package/dist/crypto/webcrypto.d.ts +4 -0
- package/dist/crypto/webcrypto.d.ts.map +1 -0
- package/dist/crypto/webcrypto.js +34 -0
- package/dist/crypto/webcrypto.js.map +1 -0
- package/dist/dispatch/ArgumentResolver.d.ts +10 -0
- package/dist/dispatch/ArgumentResolver.d.ts.map +1 -0
- package/dist/dispatch/ArgumentResolver.js +12 -0
- package/dist/dispatch/ArgumentResolver.js.map +1 -0
- package/dist/dispatch/ArrayResult.d.ts +3 -0
- package/dist/dispatch/ArrayResult.d.ts.map +1 -0
- package/dist/dispatch/ArrayResult.js +11 -0
- package/dist/dispatch/ArrayResult.js.map +1 -0
- package/dist/dispatch/DispatchEvent.d.ts +18 -0
- package/dist/dispatch/DispatchEvent.d.ts.map +1 -0
- package/dist/dispatch/DispatchEvent.js +30 -0
- package/dist/dispatch/DispatchEvent.js.map +1 -0
- package/dist/dispatch/DispatchLifecycle.d.ts +9 -0
- package/dist/dispatch/DispatchLifecycle.d.ts.map +1 -0
- package/dist/dispatch/DispatchLifecycle.js +25 -0
- package/dist/dispatch/DispatchLifecycle.js.map +1 -0
- package/dist/dispatch/DispatchPort.d.ts +6 -0
- package/dist/dispatch/DispatchPort.d.ts.map +1 -0
- package/dist/dispatch/DispatchPort.js +2 -0
- package/dist/dispatch/DispatchPort.js.map +1 -0
- package/dist/dispatch/Dispatcher.d.ts +16 -0
- package/dist/dispatch/Dispatcher.d.ts.map +1 -0
- package/dist/dispatch/Dispatcher.js +44 -0
- package/dist/dispatch/Dispatcher.js.map +1 -0
- package/dist/dispatch/InFlight.d.ts +12 -0
- package/dist/dispatch/InFlight.d.ts.map +1 -0
- package/dist/dispatch/InFlight.js +46 -0
- package/dist/dispatch/InFlight.js.map +1 -0
- package/dist/dispatch/InputValidator.d.ts +7 -0
- package/dist/dispatch/InputValidator.d.ts.map +1 -0
- package/dist/dispatch/InputValidator.js +22 -0
- package/dist/dispatch/InputValidator.js.map +1 -0
- package/dist/dispatch/LocalRoutePolicy.d.ts +11 -0
- package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -0
- package/dist/dispatch/LocalRoutePolicy.js +32 -0
- package/dist/dispatch/LocalRoutePolicy.js.map +1 -0
- package/dist/dispatch/OperationExecutor.d.ts +25 -0
- package/dist/dispatch/OperationExecutor.d.ts.map +1 -0
- package/dist/dispatch/OperationExecutor.js +32 -0
- package/dist/dispatch/OperationExecutor.js.map +1 -0
- package/dist/dispatch/OperationRoute.d.ts +13 -0
- package/dist/dispatch/OperationRoute.d.ts.map +1 -0
- package/dist/dispatch/OperationRoute.js +15 -0
- package/dist/dispatch/OperationRoute.js.map +1 -0
- package/dist/dispatch/OutputProjector.d.ts +12 -0
- package/dist/dispatch/OutputProjector.d.ts.map +1 -0
- package/dist/dispatch/OutputProjector.js +30 -0
- package/dist/dispatch/OutputProjector.js.map +1 -0
- package/dist/dispatch/OutputView.d.ts +8 -0
- package/dist/dispatch/OutputView.d.ts.map +1 -0
- package/dist/dispatch/OutputView.js +10 -0
- package/dist/dispatch/OutputView.js.map +1 -0
- package/dist/dispatch/PresenterArgumentResolver.d.ts +12 -0
- package/dist/dispatch/PresenterArgumentResolver.d.ts.map +1 -0
- package/dist/dispatch/PresenterArgumentResolver.js +20 -0
- package/dist/dispatch/PresenterArgumentResolver.js.map +1 -0
- package/dist/dispatch/PresenterExecutor.d.ts +11 -0
- package/dist/dispatch/PresenterExecutor.d.ts.map +1 -0
- package/dist/dispatch/PresenterExecutor.js +56 -0
- package/dist/dispatch/PresenterExecutor.js.map +1 -0
- package/dist/dispatch/RemoteRouteResolver.d.ts +13 -0
- package/dist/dispatch/RemoteRouteResolver.d.ts.map +1 -0
- package/dist/dispatch/RemoteRouteResolver.js +18 -0
- package/dist/dispatch/RemoteRouteResolver.js.map +1 -0
- package/dist/dispatch/Route.d.ts +10 -0
- package/dist/dispatch/Route.d.ts.map +1 -0
- package/dist/dispatch/Route.js +2 -0
- package/dist/dispatch/Route.js.map +1 -0
- package/dist/dispatch/RouteNotFoundError.d.ts +10 -0
- package/dist/dispatch/RouteNotFoundError.d.ts.map +1 -0
- package/dist/dispatch/RouteNotFoundError.js +24 -0
- package/dist/dispatch/RouteNotFoundError.js.map +1 -0
- package/dist/dispatch/RoutePolicy.d.ts +8 -0
- package/dist/dispatch/RoutePolicy.d.ts.map +1 -0
- package/dist/dispatch/RoutePolicy.js +2 -0
- package/dist/dispatch/RoutePolicy.js.map +1 -0
- package/dist/dispatch/RouteRegistry.d.ts +18 -0
- package/dist/dispatch/RouteRegistry.d.ts.map +1 -0
- package/dist/dispatch/RouteRegistry.js +62 -0
- package/dist/dispatch/RouteRegistry.js.map +1 -0
- package/dist/dispatch/RouteResolver.d.ts +7 -0
- package/dist/dispatch/RouteResolver.d.ts.map +1 -0
- package/dist/dispatch/RouteResolver.js +2 -0
- package/dist/dispatch/RouteResolver.js.map +1 -0
- package/dist/dispatch/StorageGuard.d.ts +15 -0
- package/dist/dispatch/StorageGuard.d.ts.map +1 -0
- package/dist/dispatch/StorageGuard.js +63 -0
- package/dist/dispatch/StorageGuard.js.map +1 -0
- package/dist/effective-operation.d.ts +90 -0
- package/dist/effective-operation.d.ts.map +1 -0
- package/dist/effective-operation.js +449 -0
- package/dist/effective-operation.js.map +1 -0
- package/dist/emit.d.ts +2 -2
- package/dist/emit.d.ts.map +1 -1
- package/dist/emit.js +2 -2
- package/dist/emit.js.map +1 -1
- package/dist/entry/DynamicFacade.d.ts +10 -0
- package/dist/entry/DynamicFacade.d.ts.map +1 -0
- package/dist/entry/DynamicFacade.js +27 -0
- package/dist/entry/DynamicFacade.js.map +1 -0
- package/dist/entry/FacadeEntry.d.ts +11 -0
- package/dist/entry/FacadeEntry.d.ts.map +1 -0
- package/dist/entry/FacadeEntry.js +26 -0
- package/dist/entry/FacadeEntry.js.map +1 -0
- package/dist/entry/TransportEntry.d.ts +13 -0
- package/dist/entry/TransportEntry.d.ts.map +1 -0
- package/dist/entry/TransportEntry.js +24 -0
- package/dist/entry/TransportEntry.js.map +1 -0
- package/dist/frond-config.d.ts +24 -7
- package/dist/frond-config.d.ts.map +1 -1
- package/dist/frond-config.js.map +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +3 -2
- package/dist/graph.js.map +1 -1
- package/dist/identity-keys.d.ts +14 -0
- package/dist/identity-keys.d.ts.map +1 -0
- package/dist/identity-keys.js +37 -0
- package/dist/identity-keys.js.map +1 -0
- package/dist/identity.d.ts +58 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +182 -0
- package/dist/identity.js.map +1 -0
- package/dist/imports.d.ts +2 -2
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.js +7 -3
- package/dist/imports.js.map +1 -1
- package/dist/index.d.ts +57 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -32
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +16 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +25 -0
- package/dist/loader.js.map +1 -0
- package/dist/node.d.ts +21 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +24 -0
- package/dist/node.js.map +1 -0
- package/dist/orm.d.ts +69 -8
- package/dist/orm.d.ts.map +1 -1
- package/dist/orm.js +35 -2
- package/dist/orm.js.map +1 -1
- package/dist/prefab/collector.d.ts +29 -0
- package/dist/prefab/collector.d.ts.map +1 -0
- package/dist/prefab/collector.js +34 -0
- package/dist/prefab/collector.js.map +1 -0
- package/dist/{crud.d.ts → prefab/crud.d.ts} +7 -8
- package/dist/prefab/crud.d.ts.map +1 -0
- package/dist/{crud.js → prefab/crud.js} +39 -5
- package/dist/prefab/crud.js.map +1 -0
- package/dist/prefab/mirror.d.ts +52 -0
- package/dist/prefab/mirror.d.ts.map +1 -0
- package/dist/prefab/mirror.js +66 -0
- package/dist/prefab/mirror.js.map +1 -0
- package/dist/prefab/prefab.d.ts +15 -0
- package/dist/prefab/prefab.d.ts.map +1 -0
- package/dist/prefab/prefab.js +19 -0
- package/dist/prefab/prefab.js.map +1 -0
- package/dist/{presenter.d.ts → prefab/presenter.d.ts} +6 -17
- package/dist/prefab/presenter.d.ts.map +1 -0
- package/dist/{presenter.js → prefab/presenter.js} +5 -23
- package/dist/prefab/presenter.js.map +1 -0
- package/dist/prefab/repository.d.ts +104 -0
- package/dist/prefab/repository.d.ts.map +1 -0
- package/dist/prefab/repository.js +62 -0
- package/dist/prefab/repository.js.map +1 -0
- package/dist/scan/Fronds.d.ts +45 -0
- package/dist/scan/Fronds.d.ts.map +1 -0
- package/dist/scan/Fronds.js +66 -0
- package/dist/scan/Fronds.js.map +1 -0
- package/dist/scan/conventions.d.ts +74 -0
- package/dist/scan/conventions.d.ts.map +1 -0
- package/dist/scan/conventions.js +82 -0
- package/dist/scan/conventions.js.map +1 -0
- package/dist/scan/emit.d.ts +14 -0
- package/dist/scan/emit.d.ts.map +1 -0
- package/dist/scan/emit.js +212 -0
- package/dist/scan/emit.js.map +1 -0
- package/dist/scan/frond.d.ts +224 -0
- package/dist/scan/frond.d.ts.map +1 -0
- package/dist/scan/frond.js +2 -0
- package/dist/scan/frond.js.map +1 -0
- package/dist/{handler-parser.d.ts → scan/handler-parser.d.ts} +20 -9
- package/dist/scan/handler-parser.d.ts.map +1 -0
- package/dist/scan/handler-parser.js +665 -0
- package/dist/scan/handler-parser.js.map +1 -0
- package/dist/scan/scan-cache.d.ts +16 -0
- package/dist/scan/scan-cache.d.ts.map +1 -0
- package/dist/{scan-cache.js → scan/scan-cache.js} +27 -20
- package/dist/scan/scan-cache.js.map +1 -0
- package/dist/scan/scanner.d.ts +20 -0
- package/dist/scan/scanner.d.ts.map +1 -0
- package/dist/{scanner.js → scan/scanner.js} +212 -167
- package/dist/scan/scanner.js.map +1 -0
- package/dist/verify.d.ts +16 -3
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +21 -18
- package/dist/verify.js.map +1 -1
- package/dist/{call.d.ts → wire/call.d.ts} +76 -7
- package/dist/wire/call.d.ts.map +1 -0
- package/dist/{call.js → wire/call.js} +53 -80
- package/dist/wire/call.js.map +1 -0
- package/dist/{middleware.d.ts → wire/errors.d.ts} +18 -19
- package/dist/wire/errors.d.ts.map +1 -0
- package/dist/{middleware.js → wire/errors.js} +26 -14
- package/dist/wire/errors.js.map +1 -0
- package/dist/{errors.d.ts → wire/http-error.d.ts} +5 -6
- package/dist/wire/http-error.d.ts.map +1 -0
- package/dist/{errors.js → wire/http-error.js} +7 -6
- package/dist/wire/http-error.js.map +1 -0
- package/dist/wire/invocation.d.ts +4 -0
- package/dist/wire/invocation.d.ts.map +1 -0
- package/dist/wire/invocation.js +3 -0
- package/dist/wire/invocation.js.map +1 -0
- package/dist/{middleware/logger.d.ts → wire/loggerMiddleware.d.ts} +3 -3
- package/dist/wire/loggerMiddleware.d.ts.map +1 -0
- package/dist/{middleware/logger.js → wire/loggerMiddleware.js} +2 -2
- package/dist/wire/loggerMiddleware.js.map +1 -0
- package/dist/wire/middleware.d.ts +29 -0
- package/dist/wire/middleware.d.ts.map +1 -0
- package/dist/wire/middleware.js +20 -0
- package/dist/wire/middleware.js.map +1 -0
- package/dist/{operation.d.ts → wire/operation.d.ts} +39 -10
- package/dist/wire/operation.d.ts.map +1 -0
- package/dist/wire/operation.js +167 -0
- package/dist/wire/operation.js.map +1 -0
- package/package.json +27 -8
- package/src/boot/AppLifecycle.ts +50 -0
- package/src/boot/Emissions.ts +263 -0
- package/src/boot/HandlerFacade.ts +242 -0
- package/src/boot/Lifecycle.ts +3 -0
- package/src/boot/ambient-port.ts +41 -0
- package/src/boot/ambient.als.ts +43 -0
- package/src/boot/ambient.queue.ts +75 -0
- package/src/boot/apply.ts +47 -0
- package/src/boot/auth.ts +55 -0
- package/src/boot/binding.ts +182 -0
- package/src/boot/boot.ts +125 -0
- package/src/boot/bootstrap.ts +839 -0
- package/src/boot/frame.ts +210 -0
- package/src/boot/ownership.ts +140 -0
- package/src/boot/ports.ts +66 -0
- package/src/boot/remote.ts +179 -0
- package/src/boot/seed.ts +158 -0
- package/src/boot/together.ts +215 -0
- package/src/boot/types.ts +261 -0
- package/src/builtins/config.ts +19 -0
- package/src/builtins/logger.ts +175 -0
- package/src/config-loader.ts +146 -0
- package/src/contract/Call.ts +18 -0
- package/src/contract/Invocation.ts +72 -0
- package/src/contract/RouteAddress.ts +49 -0
- package/src/contract.ts +74 -0
- package/src/crypto/encoding.ts +59 -0
- package/src/crypto/node.ts +30 -0
- package/src/crypto/port.ts +45 -0
- package/src/crypto/webcrypto.ts +55 -0
- package/src/define.ts +10 -0
- package/src/dispatch/ArgumentResolver.ts +17 -0
- package/src/dispatch/ArrayResult.ts +10 -0
- package/src/dispatch/DispatchEvent.ts +38 -0
- package/src/dispatch/DispatchLifecycle.ts +23 -0
- package/src/dispatch/DispatchPort.ts +6 -0
- package/src/dispatch/Dispatcher.ts +48 -0
- package/src/dispatch/InFlight.ts +49 -0
- package/src/dispatch/InputValidator.ts +28 -0
- package/src/dispatch/LocalRoutePolicy.ts +34 -0
- package/src/dispatch/OperationExecutor.ts +60 -0
- package/src/dispatch/OperationRoute.ts +18 -0
- package/src/dispatch/OutputProjector.ts +32 -0
- package/src/dispatch/OutputView.ts +9 -0
- package/src/dispatch/PresenterArgumentResolver.ts +25 -0
- package/src/dispatch/PresenterExecutor.ts +55 -0
- package/src/dispatch/RemoteRouteResolver.ts +21 -0
- package/src/dispatch/Route.ts +11 -0
- package/src/dispatch/RouteNotFoundError.ts +29 -0
- package/src/dispatch/RoutePolicy.ts +8 -0
- package/src/dispatch/RouteRegistry.ts +73 -0
- package/src/dispatch/RouteResolver.ts +7 -0
- package/src/dispatch/StorageGuard.ts +71 -0
- package/src/effective-operation.ts +597 -0
- package/src/emit.ts +90 -0
- package/src/entry/DynamicFacade.ts +28 -0
- package/src/entry/FacadeEntry.ts +32 -0
- package/src/entry/TransportEntry.ts +30 -0
- package/src/frond-config.ts +138 -0
- package/src/graph.ts +180 -0
- package/src/identity-keys.ts +43 -0
- package/src/identity.ts +235 -0
- package/src/imports.ts +123 -0
- package/src/index.ts +106 -0
- package/src/loader.ts +41 -0
- package/src/node.ts +28 -0
- package/src/orm.ts +252 -0
- package/src/prefab/collector.ts +36 -0
- package/src/prefab/crud.ts +188 -0
- package/src/prefab/mirror.ts +122 -0
- package/src/prefab/prefab.ts +46 -0
- package/src/prefab/presenter.ts +63 -0
- package/src/prefab/repository.ts +170 -0
- package/src/scan/Fronds.ts +73 -0
- package/src/scan/conventions.ts +122 -0
- package/src/scan/emit.ts +231 -0
- package/src/scan/frond.ts +235 -0
- package/src/scan/handler-parser.ts +818 -0
- package/src/scan/scan-cache.ts +115 -0
- package/src/scan/scanner.ts +758 -0
- package/src/verify.ts +193 -0
- package/src/wire/call.ts +369 -0
- package/src/wire/errors.ts +124 -0
- package/src/wire/http-error.ts +77 -0
- package/src/wire/invocation.ts +10 -0
- package/src/wire/loggerMiddleware.ts +29 -0
- package/src/wire/middleware.ts +47 -0
- package/src/wire/operation.ts +258 -0
- package/dist/binding.d.ts.map +0 -1
- package/dist/binding.js.map +0 -1
- package/dist/boot.d.ts.map +0 -1
- package/dist/boot.js +0 -53
- package/dist/boot.js.map +0 -1
- package/dist/bootstrap.d.ts.map +0 -1
- package/dist/bootstrap.js +0 -879
- package/dist/bootstrap.js.map +0 -1
- package/dist/call.d.ts.map +0 -1
- package/dist/call.js.map +0 -1
- package/dist/collector.d.ts +0 -28
- package/dist/collector.d.ts.map +0 -1
- package/dist/collector.js +0 -32
- package/dist/collector.js.map +0 -1
- package/dist/crud.d.ts.map +0 -1
- package/dist/crud.js.map +0 -1
- package/dist/egress.d.ts +0 -52
- package/dist/egress.d.ts.map +0 -1
- package/dist/egress.js +0 -186
- package/dist/egress.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/handler-parser.d.ts.map +0 -1
- package/dist/handler-parser.js +0 -505
- package/dist/handler-parser.js.map +0 -1
- package/dist/invocation.d.ts +0 -20
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js +0 -15
- package/dist/invocation.js.map +0 -1
- package/dist/middleware/error-handler.d.ts +0 -15
- package/dist/middleware/error-handler.d.ts.map +0 -1
- package/dist/middleware/error-handler.js +0 -29
- package/dist/middleware/error-handler.js.map +0 -1
- package/dist/middleware/logger.d.ts.map +0 -1
- package/dist/middleware/logger.js.map +0 -1
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js.map +0 -1
- package/dist/mirror.d.ts +0 -93
- package/dist/mirror.d.ts.map +0 -1
- package/dist/mirror.js +0 -96
- package/dist/mirror.js.map +0 -1
- package/dist/operation.d.ts.map +0 -1
- package/dist/operation.js +0 -95
- package/dist/operation.js.map +0 -1
- package/dist/presenter.d.ts.map +0 -1
- package/dist/presenter.js.map +0 -1
- package/dist/remote.d.ts.map +0 -1
- package/dist/remote.js.map +0 -1
- package/dist/repository.d.ts +0 -55
- package/dist/repository.d.ts.map +0 -1
- package/dist/repository.js +0 -26
- package/dist/repository.js.map +0 -1
- package/dist/scan-cache.d.ts +0 -10
- package/dist/scan-cache.d.ts.map +0 -1
- package/dist/scan-cache.js.map +0 -1
- package/dist/scanner.d.ts +0 -34
- package/dist/scanner.d.ts.map +0 -1
- package/dist/scanner.js.map +0 -1
- package/dist/seed.d.ts.map +0 -1
- package/dist/seed.js.map +0 -1
- package/dist/types.d.ts +0 -421
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- /package/dist/{bootstrap.d.ts → boot/bootstrap.d.ts} +0 -0
- /package/dist/{types.js → boot/types.js} +0 -0
package/src/orm.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { upperFirst, type SchemaView } from '@fougere/schema';
|
|
2
|
+
|
|
3
|
+
/** Options for list queries — pagination, sorting, counting. */
|
|
4
|
+
export interface ListOptions {
|
|
5
|
+
/** Number of records to return. */
|
|
6
|
+
limit?: number;
|
|
7
|
+
|
|
8
|
+
/** Offset-based: skip N records. */
|
|
9
|
+
offset?: number;
|
|
10
|
+
|
|
11
|
+
/** Page-based: 1-indexed page number (requires limit). */
|
|
12
|
+
page?: number;
|
|
13
|
+
|
|
14
|
+
/** Cursor-based: fetch records after this ID. */
|
|
15
|
+
after?: string;
|
|
16
|
+
|
|
17
|
+
/** Field name to order by. */
|
|
18
|
+
orderBy?: string;
|
|
19
|
+
|
|
20
|
+
/** Sort direction (default: 'asc'). */
|
|
21
|
+
order?: 'asc' | 'desc';
|
|
22
|
+
|
|
23
|
+
/** If true, also returns total count (for pagination UIs). */
|
|
24
|
+
count?: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Equality criteria, field by field — `{ orderId: '…' }`. Named rather than spread
|
|
28
|
+
* across the options so an unknown key stays ignored instead of silently becoming a
|
|
29
|
+
* filter. `listBy(criteria)` is the same thing said as an intention.
|
|
30
|
+
*/
|
|
31
|
+
where?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Result of list() — extends Array so it's backward compatible. */
|
|
35
|
+
export interface ListResult<T> extends Array<T> {
|
|
36
|
+
/** Total number of matching records (only set when count: true). */
|
|
37
|
+
total?: number;
|
|
38
|
+
/** Cursor of the last item (for cursor-based pagination). */
|
|
39
|
+
endCursor?: string;
|
|
40
|
+
/** Whether more records exist after endCursor. */
|
|
41
|
+
hasMore?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The keys `list()` answers to. Anything else is a mistake, and saying so is the point:
|
|
46
|
+
* the façade refuses an unknown key in a CLIENT's input (`Unknown field`) — this applies the
|
|
47
|
+
* same rule to the framework's own arguments. `list({ orderId })` used to be accepted and
|
|
48
|
+
* the filter dropped, so a one-to-many relation quietly returned the whole table.
|
|
49
|
+
*/
|
|
50
|
+
const LIST_OPTION_KEYS = [
|
|
51
|
+
'limit', 'offset', 'page', 'after', 'orderBy', 'order', 'count', 'where', 'select',
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
/** Refuse an option the port does not answer to, naming it and what was expected. */
|
|
55
|
+
export function assertListOptions(options: object | undefined, entity: string): void {
|
|
56
|
+
if (!options) return;
|
|
57
|
+
const legal = new Set<string>(LIST_OPTION_KEYS);
|
|
58
|
+
const strangers = Object.keys(options).filter((key) => !legal.has(key));
|
|
59
|
+
if (strangers.length === 0) return;
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${entity}.list(): unknown option ${strangers.map((s) => `\`${s}\``).join(', ')}. ` +
|
|
62
|
+
`Known options are ${LIST_OPTION_KEYS.join(', ')} — to filter, pass \`where: { ${strangers[0]}: … }\`.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Select option — restrict returned fields to those of a SchemaView. */
|
|
67
|
+
export interface SelectOption {
|
|
68
|
+
select?: SchemaView;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Per-entity ORM — scoped CRUD operations on a single entity type. */
|
|
72
|
+
export interface EntityOrm<T = Record<string, unknown>> {
|
|
73
|
+
list(options?: ListOptions & SelectOption): Promise<ListResult<T>>;
|
|
74
|
+
findById(id: string, options?: SelectOption): Promise<T | undefined>;
|
|
75
|
+
/**
|
|
76
|
+
* Read by criteria — `findBy({ email })` for the one, `findAllBy({ orderId })` for the
|
|
77
|
+
* many, which is what a one-to-many relation *is*.
|
|
78
|
+
*
|
|
79
|
+
* Both existed on the SQL implementation from the start and neither was declared here.
|
|
80
|
+
* A port that hides what it offers is a port nobody can use: `auth-better` cast its way
|
|
81
|
+
* in (`orm as OrmWithFindBy`), a presenter that needed the lines of an order read the
|
|
82
|
+
* whole table instead, and the GraphQL relation resolver passed criteria to `list()`
|
|
83
|
+
* — which drops what it does not know.
|
|
84
|
+
*/
|
|
85
|
+
findBy(criteria: Partial<T> | Record<string, unknown>, options?: SelectOption): Promise<T | undefined>;
|
|
86
|
+
findAllBy(criteria: Partial<T> | Record<string, unknown>, options?: SelectOption): Promise<T[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Read a SET of rows by their key, in one go — the gesture every other one was
|
|
89
|
+
* being bent into.
|
|
90
|
+
*
|
|
91
|
+
* `findAllBy` compares with `=`, so a list of ids could not be passed to it; the
|
|
92
|
+
* only way to read N rows was `list()` then filter in memory, which is what 14 of
|
|
93
|
+
* 14 handlers of a real app measured on 2026-08-14 were doing. It is invisible on
|
|
94
|
+
* SQLite and it is the whole table the day the rows are not local.
|
|
95
|
+
*
|
|
96
|
+
* It answers a MAP, because the caller holds keys and not positions: a page zips
|
|
97
|
+
* against it by `get(row.authorId)`, a miss is the absence of a key, and a repeated
|
|
98
|
+
* key is one entry. The list form this replaced promised that zip and could not
|
|
99
|
+
* keep it — dropping a miss shifts every later position — so each caller rebuilt
|
|
100
|
+
* the very index the implementation had just thrown away.
|
|
101
|
+
*/
|
|
102
|
+
findByKeys(ids: readonly string[], options?: SelectOption): Promise<Map<string, T>>;
|
|
103
|
+
/**
|
|
104
|
+
* Its dual: the rows that point AT each of these keys, grouped by the one they point at.
|
|
105
|
+
*
|
|
106
|
+
* `findByKeys` answers the side a row designates — one each, at most. This answers the
|
|
107
|
+
* side that designates the row — several each, and a key with none is simply absent.
|
|
108
|
+
* Together they are both directions of a relation, each in one query.
|
|
109
|
+
*
|
|
110
|
+
* Written because the page-shaped gesture only covered one of them: a presenter holding
|
|
111
|
+
* its page and wanting "the items of these lists" had no vocabulary for it and read the
|
|
112
|
+
* whole table instead, measured on a real app. `field` names the foreign key on THIS
|
|
113
|
+
* entity, the one carrying the value.
|
|
114
|
+
*/
|
|
115
|
+
findAllByKeys(field: string, keys: readonly string[], options?: SelectOption): Promise<Map<string, T[]>>;
|
|
116
|
+
create(input: Partial<T>, options?: SelectOption): Promise<T>;
|
|
117
|
+
/**
|
|
118
|
+
* Write the row, or make the existing one look like this — one statement.
|
|
119
|
+
*
|
|
120
|
+
* What an import needs and the port did not have: `create` throws on the second run,
|
|
121
|
+
* so re-reading a source meant deleting first. Measured pulling an API twice.
|
|
122
|
+
*
|
|
123
|
+
* The key and the creation stamps survive an overwrite — a row keeps the moment it
|
|
124
|
+
* appeared. An engine with no upsert clause refuses by name rather than emulating
|
|
125
|
+
* one with a read in front, which would promise an atomicity it has not got.
|
|
126
|
+
*/
|
|
127
|
+
upsert(input: Partial<T>, options?: SelectOption): Promise<T>;
|
|
128
|
+
/**
|
|
129
|
+
* A whole page in one statement — what an import writes through.
|
|
130
|
+
*
|
|
131
|
+
* Row by row, 500 rows were 500 statements (measured); the shape of an import is a
|
|
132
|
+
* page, so the write is one too. Answers how many rows were written rather than the
|
|
133
|
+
* rows: `create` hands back the complete row because a caller acts on it, and an
|
|
134
|
+
* import acts on none of them — re-reading a page for a symmetry nobody uses would
|
|
135
|
+
* double the work.
|
|
136
|
+
*/
|
|
137
|
+
upsertAll(inputs: readonly Partial<T>[], options?: SelectOption): Promise<number>;
|
|
138
|
+
update(id: string, input: Partial<T>, options?: SelectOption): Promise<T>;
|
|
139
|
+
delete(id: string): Promise<boolean>;
|
|
140
|
+
/** Returns a scoped ORM that restricts all read results to the fields of the given schema. */
|
|
141
|
+
output(schema: SchemaView): EntityOrm<T>;
|
|
142
|
+
/**
|
|
143
|
+
* What this ORM wraps — the Kysely instance for the SQL one, something else elsewhere.
|
|
144
|
+
*
|
|
145
|
+
* Every judge sits on the ORM's own methods, so a statement issued here meets none of
|
|
146
|
+
* them: a value the entity refuses lands in the table without a word. It is the port's
|
|
147
|
+
* own escape hatch rather than a handle on the side, so it keeps the scope the container
|
|
148
|
+
* gave you — `productOrm.client` reaches the products, not the whole database.
|
|
149
|
+
*
|
|
150
|
+
* `unknown` on purpose: the client belongs to the implementation, and narrowing it is
|
|
151
|
+
* the caller saying out loud which one they are standing on.
|
|
152
|
+
*/
|
|
153
|
+
readonly client: unknown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Factory that creates an EntityOrm for a given entity.
|
|
158
|
+
* Called by bootstrap for every scanned entity.
|
|
159
|
+
*/
|
|
160
|
+
export type OrmFactory = (entity: SchemaView, name: string) => EntityOrm;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Container key of an entity's storage — 'reading' → 'ReadingOrm'.
|
|
164
|
+
*
|
|
165
|
+
* The twin of {@link repositoryKeyOf} and of `facadeKeyOf`: the key of a thing
|
|
166
|
+
* lives with the thing. This one was spelled by hand in four places in
|
|
167
|
+
* `bootstrap.ts` and a fifth in `scanner.ts` — where the SCAN derives what a
|
|
168
|
+
* constructor asks for. Two readers of one convention, neither of them naming it,
|
|
169
|
+
* so a rename would have moved one and left the other resolving to nothing.
|
|
170
|
+
*/
|
|
171
|
+
export function ormKeyOf(entity: string): string {
|
|
172
|
+
return `${upperFirst(entity)}Orm`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* `Together<[Account, Ledger]>` — writes that stand or fall as one.
|
|
177
|
+
*
|
|
178
|
+
* `EntityOrm` is the port whose every gesture is ONE statement, and one statement is
|
|
179
|
+
* atomic in every engine. This is the port whose unit is a BLOCK: what the callback did
|
|
180
|
+
* happens entirely, or not at all. Nothing else separates them — the arity of the unit
|
|
181
|
+
* of work is the whole distinction, which is why this lives here and not in a file of
|
|
182
|
+
* its own.
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* constructor(private together: Together<[Account, Ledger]>) {}
|
|
186
|
+
*
|
|
187
|
+
* await this.together.run(async ([accounts, ledger]) => {
|
|
188
|
+
* await accounts.update(from, { balance: b - amount });
|
|
189
|
+
* await ledger.create({ from, to, amount });
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* **The second list is providers**, rebuilt inside the frame so that what THEY write is
|
|
194
|
+
* covered too — a `Mirror` writes its pages through `EntityOrm<T>`, so naming it puts
|
|
195
|
+
* them under the same unwind, with no locator and no second injection path:
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* constructor(private together: Together<[RateCard, Ledger], [RateMirror]>) {}
|
|
199
|
+
*
|
|
200
|
+
* await this.together.run(async ([rates, ledger], [mirror]) => { await mirror.refresh(); });
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Two lists rather than one, because in a signature an entity and a provider are both
|
|
204
|
+
* written as a class name and their instance types do not separate them — the type would
|
|
205
|
+
* have to guess, by looking for methods or for a brand, and both answers are worse than
|
|
206
|
+
* saying it. They are two different facts anyway: what the unwind covers, and what is
|
|
207
|
+
* rebuilt to make that true.
|
|
208
|
+
*
|
|
209
|
+
* **All or nothing is the promise; isolation is not.** On one engine the members are
|
|
210
|
+
* rebuilt over a transaction and the engine gives both. Across engines a transaction
|
|
211
|
+
* cannot exist, so the frame keeps the before-image of each write and replays the
|
|
212
|
+
* inverse in reverse order — the unwind holds, the isolation does not, and a reader
|
|
213
|
+
* between two writes sees the half. The boot says which of the two it built rather
|
|
214
|
+
* than letting the author assume the stronger one.
|
|
215
|
+
*/
|
|
216
|
+
export interface Together<E extends readonly unknown[], P extends readonly unknown[] = []> {
|
|
217
|
+
run<R>(fn: (entities: { [K in keyof E]: EntityOrm<E[K]> }, providers: P) => Promise<R>): Promise<R>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The container key of a frame — `[['Account', 'Ledger'], ['RateMirror']]` →
|
|
222
|
+
* 'Account+Ledger|RateMirrorTogether'.
|
|
223
|
+
*
|
|
224
|
+
* The DECLARED order, not a sorted one. The tuples are what the callback destructures, so
|
|
225
|
+
* reordering here would hand `[ledger, accounts]` to a signature that says the opposite —
|
|
226
|
+
* the type and the runtime disagreeing about the same line. Two orders of one frame are
|
|
227
|
+
* therefore two keys and two registrations; they cost nothing, a frame holding no state
|
|
228
|
+
* between runs.
|
|
229
|
+
*/
|
|
230
|
+
export function togetherKeyOf(entities: readonly string[], providers: readonly string[] = []): string {
|
|
231
|
+
const named = entities.map(upperFirst).join(SEPARATOR);
|
|
232
|
+
return `${named}${providers.length ? KINDS + providers.map(upperFirst).join(SEPARATOR) : ''}${FRAME}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const FRAME = 'Together';
|
|
236
|
+
const SEPARATOR = '+';
|
|
237
|
+
/** Separates the two lists — what the unwind covers, and what is rebuilt to make it true. */
|
|
238
|
+
const KINDS = '|';
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The members behind a frame key, or `undefined` when the key is not one.
|
|
242
|
+
*
|
|
243
|
+
* The dual of the line above, and here for the reason `factOfEmitKey` is: the boot reads
|
|
244
|
+
* the key back out of a handler's `deps` to know which frames to build. A key and the way
|
|
245
|
+
* to undo it belong together — the pair that is split is the pair that drifts.
|
|
246
|
+
*/
|
|
247
|
+
export function membersOfTogetherKey(key: string): { entities: string[]; providers: string[] } | undefined {
|
|
248
|
+
if (key.length <= FRAME.length || !key.endsWith(FRAME)) return undefined;
|
|
249
|
+
const [entities, providers = ''] = key.slice(0, -FRAME.length).split(KINDS);
|
|
250
|
+
const split = (list: string) => list.split(SEPARATOR).filter(Boolean);
|
|
251
|
+
return { entities: split(entities!), providers: split(providers) };
|
|
252
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collector(Type) — resolves one handler input parameter from the invocation context.
|
|
3
|
+
*
|
|
4
|
+
* The target is a NAME matched against the parameter's type, never a schema: nothing
|
|
5
|
+
* here nor in the scan reads its fields. So `Ability`, built per call from state, sits
|
|
6
|
+
* beside `User`, loaded from a row.
|
|
7
|
+
*
|
|
8
|
+
* The dual of Presenter only in direction. A presenter's subject is a ROW — it adds
|
|
9
|
+
* computed fields to one, so its target must be an entity. A collector's subject is a
|
|
10
|
+
* PARAMETER.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* export default class UserCollector extends Collector(User) {
|
|
15
|
+
* constructor(private userOrm: UserOrm) { super(); }
|
|
16
|
+
*
|
|
17
|
+
* async collect(ctx: InvocationContext) {
|
|
18
|
+
* return this.userOrm.findById(ctx.state.userId as string);
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { upperFirst } from '@fougere/schema';
|
|
25
|
+
|
|
26
|
+
export function Collector<T extends abstract new (...args: any[]) => any>(target: T) {
|
|
27
|
+
class CollectorBase {
|
|
28
|
+
static readonly __entity = target;
|
|
29
|
+
}
|
|
30
|
+
return CollectorBase;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Container key of a collector — 'user' → 'UserCollector', 'ability' → 'AbilityCollector'. */
|
|
34
|
+
export function collectorKeyOf(type: string): string {
|
|
35
|
+
return `${upperFirst(type)}Collector`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { EntityOrm, ListOptions, ListResult } from '../orm.js';
|
|
2
|
+
import type { OperationContract } from '../wire/operation.js';
|
|
3
|
+
import type { EntityConstructor, SchemaView } from '@fougere/schema';
|
|
4
|
+
|
|
5
|
+
/** The id of the row an op acts on — a route segment, or a query fallback. */
|
|
6
|
+
const byId = { name: 'id', source: { kind: 'param' as const, name: 'id' }, optional: false };
|
|
7
|
+
const fromBody = { name: 'input', source: { kind: 'body' as const }, optional: false };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The same five, written as the scan would have written them.
|
|
11
|
+
*
|
|
12
|
+
* `binding` says WHERE an argument is read from; it never says of what type, and a
|
|
13
|
+
* GraphQL argument needs the type. So a producer that fills only `binding` leaves
|
|
14
|
+
* `adapter/graphql` with nothing to declare — it drops the op (`registerOperations`,
|
|
15
|
+
* `if (!sig) continue`) and four of the five CRUD ops vanish from the schema while
|
|
16
|
+
* the façade and REST still serve them. Measured on an installed app: 16 handlers,
|
|
17
|
+
* 14 with unresolvable heritage, zero `createX` in the schema.
|
|
18
|
+
*/
|
|
19
|
+
const idParam = { name: 'id', type: { raw: 'string', name: 'string' } };
|
|
20
|
+
const inputParam = (entity: string) => ({ name: 'input', type: { raw: `Partial<${entity}>`, name: entity } });
|
|
21
|
+
const returns = (raw: string, name: string, extra?: { array?: boolean; nullable?: boolean }) =>
|
|
22
|
+
({ raw, name, ...extra });
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The five ops a Crud handler brings, declared rather than discovered.
|
|
26
|
+
*
|
|
27
|
+
* The mixin built them, so it alone knows their contract in full: what judges
|
|
28
|
+
* their input, where each argument comes from. It says so on the class, at
|
|
29
|
+
* runtime — which is what makes the guarantee independent of the AST scan (an
|
|
30
|
+
* installed app cannot resolve this file, and never needs to).
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* `output` says the entity, and saying it costs nothing at runtime.
|
|
34
|
+
*
|
|
35
|
+
* The façade already projected onto the entity when nothing else was named, so this
|
|
36
|
+
* changes no result: `outputFieldsFor` reads `contractOutput` and falls back to the
|
|
37
|
+
* entity, and both are the same shape here. What changes is that the sentence now
|
|
38
|
+
* EXISTS — the identity card publishes `output` per op, and a card was measured
|
|
39
|
+
* carrying none at all (2026-08-06, ten ops, zero outputs), which typed every remote
|
|
40
|
+
* return as `unknown` for anyone building on it.
|
|
41
|
+
*
|
|
42
|
+
* It does not close the view: only an explicit `__opOutputs` does (`closed: perOp !==
|
|
43
|
+
* undefined`), so a named view still wins and a presenter's computed fields still ride
|
|
44
|
+
* out. `delete` names none — a boolean is not a shape.
|
|
45
|
+
*/
|
|
46
|
+
function crudOps(entity: SchemaView & { partial?: () => SchemaView }): Record<string, OperationContract> {
|
|
47
|
+
const name = (entity as { name?: string }).name ?? 'Entity';
|
|
48
|
+
const input = inputParam(name);
|
|
49
|
+
return {
|
|
50
|
+
list: {
|
|
51
|
+
output: entity, cardinality: 'page',
|
|
52
|
+
binding: [{ name: 'options', source: { kind: 'query' }, optional: true }],
|
|
53
|
+
signature: {
|
|
54
|
+
name: 'list', returnType: returns(`ListResult<${name}>`, 'ListResult'),
|
|
55
|
+
params: [{ name: 'options', type: { raw: 'ListOptions', name: 'ListOptions' }, optional: true }],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
findById: {
|
|
59
|
+
output: entity, cardinality: 'maybe', binding: [byId],
|
|
60
|
+
signature: { name: 'findById', returnType: returns(`${name} | undefined`, name, { nullable: true }), params: [idParam] },
|
|
61
|
+
},
|
|
62
|
+
create: {
|
|
63
|
+
input: entity, output: entity, cardinality: 'one', binding: [fromBody],
|
|
64
|
+
signature: { name: 'create', returnType: returns(name, name), params: [input] },
|
|
65
|
+
},
|
|
66
|
+
// The patch view carries its own mode: an absent field is untouched, an
|
|
67
|
+
// immutable one re-supplied is refused.
|
|
68
|
+
update: {
|
|
69
|
+
input: entity.partial?.(), output: entity, cardinality: 'one', binding: [byId, fromBody],
|
|
70
|
+
signature: { name: 'update', returnType: returns(name, name), params: [idParam, input] },
|
|
71
|
+
},
|
|
72
|
+
delete: {
|
|
73
|
+
cardinality: 'none', binding: [byId],
|
|
74
|
+
signature: { name: 'delete', returnType: returns('boolean', 'boolean'), params: [idParam] },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The mixin's single "trust me" point — the twin of `asSchemaConstructor` in @fougere/schema.
|
|
81
|
+
*
|
|
82
|
+
* The implementation returns whatever the ORM hands back; the declaration names the view
|
|
83
|
+
* each op emits at the port. TypeScript cannot connect the two (the view is a runtime
|
|
84
|
+
* argument, the type is a generic), so one assertion states that the mixin honours what it
|
|
85
|
+
* declared — and the façade makes it true, projecting each op's result onto its view
|
|
86
|
+
* (`outputFieldsFor` in `bootstrap.ts`).
|
|
87
|
+
*/
|
|
88
|
+
function asCrudConstructor<T, V>(impl: object): CrudConstructor<T, V> {
|
|
89
|
+
return impl as CrudConstructor<T, V>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The five ops the mixin fabricates. */
|
|
93
|
+
export type CrudOpName = 'list' | 'findById' | 'create' | 'update' | 'delete';
|
|
94
|
+
|
|
95
|
+
/** Which view each op speaks — omitted ops speak the entity, the trivial view. */
|
|
96
|
+
export type CrudViews = Partial<Record<CrudOpName, EntityConstructor>>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The view an op emits, fabricated: the one declared for it, the single view when the
|
|
100
|
+
* whole handler declares one, the entity otherwise. `PostCard` is not a hand-written
|
|
101
|
+
* type — `Post.pick(...)` derives it field by field, so an op's return is a projection
|
|
102
|
+
* of the entity exactly like the entity is the projection that keeps everything.
|
|
103
|
+
*/
|
|
104
|
+
type OutOf<V, K extends CrudOpName, T> =
|
|
105
|
+
// Bracketed on purpose: a naked `V extends …` DISTRIBUTES, and the no-view default
|
|
106
|
+
// is the empty map, whose `keyof` is `never` — distribution would then collapse
|
|
107
|
+
// every op's output to `never` instead of falling through to the entity.
|
|
108
|
+
[V] extends [EntityConstructor] ? InstanceType<V & EntityConstructor>
|
|
109
|
+
: K extends keyof V ? (V[K] extends EntityConstructor ? InstanceType<V[K]> : T)
|
|
110
|
+
: T;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The five ops, typed from the entity and its views.
|
|
114
|
+
*
|
|
115
|
+
* Two things the mixin declares but does not own. The **output** is the view the
|
|
116
|
+
* handler names (`Crud(Post, { list: PostCard })`) — fabricated, so a redefinition
|
|
117
|
+
* that returns cards stays assignable. The **trailing parameters** are resolved by
|
|
118
|
+
* type from the container (`delete(id, user?: User)` gets its user from a
|
|
119
|
+
* collector): the mixin cannot know them, they belong to the app, so it declares that
|
|
120
|
+
* a tail exists and that it supplies none — which is what `never` says, and what keeps
|
|
121
|
+
* a judged redefinition assignable.
|
|
122
|
+
*/
|
|
123
|
+
export interface CrudOps<T, V = {}> {
|
|
124
|
+
orm: EntityOrm<T>;
|
|
125
|
+
list(options?: ListOptions, ...collected: never[]): Promise<ListResult<OutOf<V, 'list', T>>>;
|
|
126
|
+
findById(id: string, ...collected: never[]): Promise<OutOf<V, 'findById', T> | undefined>;
|
|
127
|
+
create(input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'create', T>>;
|
|
128
|
+
update(id: string, input: Partial<T>, ...collected: never[]): Promise<OutOf<V, 'update', T>>;
|
|
129
|
+
delete(id: string, ...collected: never[]): Promise<boolean>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The prefab handler class — its ops, plus the statics the bootstrap and adapters read. */
|
|
133
|
+
export interface CrudConstructor<T, V = {}> {
|
|
134
|
+
// `EntityOrm<T>`, not the bare `EntityOrm`: a handler that injects a second ORM has to
|
|
135
|
+
// spell its own constructor, and `super(orm)` with the ORM the container hands it —
|
|
136
|
+
// typed on the entity, as the `orm` property below already says — was refused.
|
|
137
|
+
new (orm: EntityOrm<T>): CrudOps<T, V>;
|
|
138
|
+
readonly __entity: unknown;
|
|
139
|
+
readonly __output: unknown;
|
|
140
|
+
readonly __opOutputs?: CrudViews;
|
|
141
|
+
readonly __ops: Record<string, OperationContract>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mixin — extends Crud(Entity) to get all 5 typed CRUD methods.
|
|
146
|
+
*
|
|
147
|
+
* The second argument (optional) names the view the ops emit, and comes in two
|
|
148
|
+
* spellings of one idea — a view per op, or one view for all five:
|
|
149
|
+
*
|
|
150
|
+
* Crud(Post) → every op emits Post
|
|
151
|
+
* Crud(Post, { list: PostCard }) → list emits cards, the rest emit Post. Declaration
|
|
152
|
+
* only: the handler keeps its full-row ORM, so a
|
|
153
|
+
* judge can still read `body`.
|
|
154
|
+
* Crud(Post, PostPublic) → every op emits PostPublic, and the bootstrap
|
|
155
|
+
* scopes the injected ORM via .output(PostPublic) —
|
|
156
|
+
* the whole handler speaks the restricted view.
|
|
157
|
+
*/
|
|
158
|
+
export function Crud<E extends EntityConstructor, V extends CrudViews | EntityConstructor = {}>(
|
|
159
|
+
entity: E,
|
|
160
|
+
output?: V,
|
|
161
|
+
): CrudConstructor<InstanceType<E>, V> {
|
|
162
|
+
// The entity() factory class IS the data type — no Infer needed.
|
|
163
|
+
type T = InstanceType<E>;
|
|
164
|
+
|
|
165
|
+
// A view is a class (it carries fields) ; a map of views is a plain object.
|
|
166
|
+
const perOp = typeof output === 'object' && output !== null ? (output as CrudViews) : undefined;
|
|
167
|
+
const wholeHandler = typeof output === 'function' ? (output as EntityConstructor) : undefined;
|
|
168
|
+
|
|
169
|
+
return asCrudConstructor<T, V>(class CrudHandler {
|
|
170
|
+
static __entity = entity;
|
|
171
|
+
/** Handler-wide view only — a per-op map must NOT scope the ORM the judges read. */
|
|
172
|
+
static __output = wholeHandler ?? entity;
|
|
173
|
+
static __opOutputs = perOp;
|
|
174
|
+
/** What this prefab handler declares — read by the façade, merged under the author's own methods. */
|
|
175
|
+
static __ops: Record<string, OperationContract> = crudOps(entity as unknown as SchemaView & { partial?: () => SchemaView });
|
|
176
|
+
|
|
177
|
+
orm: EntityOrm<T>;
|
|
178
|
+
constructor(orm: EntityOrm) {
|
|
179
|
+
this.orm = orm as EntityOrm<T>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async list(options?: ListOptions): Promise<ListResult<T>> { return this.orm.list(options) as Promise<ListResult<T>>; }
|
|
183
|
+
async findById(id: string): Promise<T | undefined> { return this.orm.findById(id); }
|
|
184
|
+
async create(input: Partial<T>): Promise<T> { return this.orm.create(input); }
|
|
185
|
+
async update(id: string, input: Partial<T>): Promise<T> { return this.orm.update(id, input); }
|
|
186
|
+
async delete(id: string): Promise<boolean> { return this.orm.delete(id); }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { FieldSet, Lifecycle, type EntityConstructor, type Fields, type ValidationResult } from '@fougere/schema';
|
|
2
|
+
import type { EntityOrm } from '../orm.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A paginated local copy of a source that cannot be queried directly.
|
|
6
|
+
*
|
|
7
|
+
* The shape must contain an `updated()` field used as a high-water mark. Subclasses
|
|
8
|
+
* implement `pull`; `refresh` validates and upserts each returned page.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* export default class PartnerCatalog extends Mirror(BookCard) {
|
|
12
|
+
* constructor(orm: EntityOrm<BookCard>, private catalog: Facade<CatalogHandler>) {
|
|
13
|
+
* super(orm);
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* async *pull(since?: Date) {
|
|
17
|
+
* for (let page = 0; page !== null;) {
|
|
18
|
+
* const body = await fetch(`${api}?page=${page}&since=${since?.toISOString() ?? ''}`);
|
|
19
|
+
* const { items, next } = await body.json();
|
|
20
|
+
* yield items.map(toCard);
|
|
21
|
+
* page = next;
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
/** What one refresh did — enough to log it, and to decide whether to run again. */
|
|
28
|
+
export interface Refreshed {
|
|
29
|
+
/** Rows written, counting a replaced row once. */
|
|
30
|
+
written: number;
|
|
31
|
+
/** The age the pull was asked to start from — absent on the first ever run. */
|
|
32
|
+
since?: Date;
|
|
33
|
+
/** How long the whole pass took, pull included. */
|
|
34
|
+
ms: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MirrorOf<T> {
|
|
38
|
+
/** The copy's own storage — where a page lands, and where its age is read. */
|
|
39
|
+
orm: EntityOrm<T>;
|
|
40
|
+
/** Pages of rows to write. The one thing a mirror's author supplies. */
|
|
41
|
+
pull(since?: Date): AsyncIterable<Partial<T>[]>;
|
|
42
|
+
/** Run one pass: read the high-water mark, pull from there, write each page. */
|
|
43
|
+
refresh(): Promise<Refreshed>;
|
|
44
|
+
/** When the freshest row here was written — `undefined` while the copy is empty. */
|
|
45
|
+
freshness(): Promise<Date | undefined>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MirrorConstructor<T> {
|
|
49
|
+
new (orm: EntityOrm<T>): MirrorOf<T>;
|
|
50
|
+
readonly __entity: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function Mirror<E extends EntityConstructor>(shape: E): MirrorConstructor<InstanceType<E>> {
|
|
54
|
+
type T = InstanceType<E>;
|
|
55
|
+
|
|
56
|
+
// Refuse an undated copy before it can be used as if it held live rows.
|
|
57
|
+
const declared = ageFieldOf(shape);
|
|
58
|
+
if (declared === undefined) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Mirror(${(shape as { name?: string }).name ?? '?'}): the shape carries no \`updated()\` field — ` +
|
|
61
|
+
`a copy has to be able to say when it was pulled, and \`refresh\` reads its high-water mark from it.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const age: string = declared;
|
|
65
|
+
|
|
66
|
+
abstract class MirrorBase implements MirrorOf<T> {
|
|
67
|
+
static readonly __entity = shape;
|
|
68
|
+
|
|
69
|
+
constructor(public orm: EntityOrm<T>) {}
|
|
70
|
+
|
|
71
|
+
abstract pull(since?: Date): AsyncIterable<Partial<T>[]>;
|
|
72
|
+
|
|
73
|
+
/** The freshest persisted stamp; no process-local freshness state is kept. */
|
|
74
|
+
async freshness(): Promise<Date | undefined> {
|
|
75
|
+
const [newest] = await this.orm.list({ orderBy: age, order: 'desc', limit: 1 });
|
|
76
|
+
const value = (newest as Record<string, unknown> | undefined)?.[age];
|
|
77
|
+
return value instanceof Date ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async refresh(): Promise<Refreshed> {
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
const since = await this.freshness();
|
|
83
|
+
let written = 0;
|
|
84
|
+
// Preserve the source's page boundaries: one page becomes one upsert.
|
|
85
|
+
for await (const page of this.pull(since)) {
|
|
86
|
+
if (page.length === 0) continue;
|
|
87
|
+
written += await this.orm.upsertAll(judgePage(shape, page) as Partial<T>[]);
|
|
88
|
+
}
|
|
89
|
+
return { written, since, ms: Date.now() - started };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return MirrorBase as unknown as MirrorConstructor<T>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The field whose lifecycle stamps every update. */
|
|
97
|
+
export function ageFieldOf(shape: unknown): string | undefined {
|
|
98
|
+
const fields = (shape as { getFields?: () => Fields }).getFields?.();
|
|
99
|
+
if (!fields) return undefined;
|
|
100
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
101
|
+
if (Lifecycle.of(field).stampedOnUpdate) return name;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Validate and decode a page, naming a refused row by its declared primary field. */
|
|
107
|
+
function judgePage<T>(shape: unknown, page: Partial<T>[]): Record<string, unknown>[] {
|
|
108
|
+
const judge = (shape as { validate?: (input: unknown) => ValidationResult<unknown> }).validate;
|
|
109
|
+
if (typeof judge !== 'function') return page as Record<string, unknown>[];
|
|
110
|
+
|
|
111
|
+
const name = (shape as { name?: string }).name ?? 'mirror';
|
|
112
|
+
const fields = (shape as { getFields?: () => Fields }).getFields?.();
|
|
113
|
+
const primary = fields ? FieldSet.of(fields).primary : undefined;
|
|
114
|
+
return page.map((row, index) => {
|
|
115
|
+
const verdict = judge.call(shape, row);
|
|
116
|
+
if (verdict.success) return verdict.data as Record<string, unknown>;
|
|
117
|
+
const key = primary === undefined ? undefined : (row as Record<string, unknown>)[primary];
|
|
118
|
+
const where = key !== undefined ? `row ${primary} ${JSON.stringify(key)}` : `row ${index} of this page`;
|
|
119
|
+
const why = verdict.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
|
|
120
|
+
throw new Error(`${name} mirror refused ${where} — ${why}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { EntityConstructor } from '@fougere/schema';
|
|
2
|
+
import type { PresenterViews } from './presenter.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a prefab class declares about ITSELF, at runtime.
|
|
6
|
+
*
|
|
7
|
+
* `Crud(Post)`, `Presenter(Post)`, `Repository(Post)`, `Collector(User)` and
|
|
8
|
+
* `Mirror(BookCard)` all fabricate a class, and only the fabricator knows what it was
|
|
9
|
+
* built on: the AST scan is workspace-only, so an INSTALLED prefab resolves to nothing
|
|
10
|
+
* and a statement written in source would be lost. Hence a runtime marker — the same
|
|
11
|
+
* reason `__ops` exists.
|
|
12
|
+
*
|
|
13
|
+
* It was three spellings for one statement: `static __entity` on three of the five, a
|
|
14
|
+
* private `Symbol.for('fougere:x_target')` on the other two, and BOTH on `Repository`
|
|
15
|
+
* and `Mirror` — the same class recording its subject twice, under two names, with two
|
|
16
|
+
* readers that could disagree. A mark that answers one question answers it once.
|
|
17
|
+
*/
|
|
18
|
+
interface Prefab {
|
|
19
|
+
/** The shape this class was built on. `Mirror` calls it a shape; it is the same slot. */
|
|
20
|
+
readonly __entity?: EntityConstructor;
|
|
21
|
+
/** The view each computed field emits — `Presenter(Order, { items: [OrderItemView] })`. */
|
|
22
|
+
readonly __views?: PresenterViews;
|
|
23
|
+
/** The view the whole handler speaks — `Crud(Post, PostPublic)`. Same slot, one level up. */
|
|
24
|
+
readonly __output?: EntityConstructor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shape a prefab was built on, or `undefined` when the class is not one.
|
|
29
|
+
*
|
|
30
|
+
* A plain property read: JavaScript walks a constructor's own prototype chain, so a
|
|
31
|
+
* subclass of a prefab answers what its base recorded. Three of the five accessors this
|
|
32
|
+
* replaces walked that chain by hand, and two did not — one question, two mechanisms.
|
|
33
|
+
*/
|
|
34
|
+
export function targetOf(ctor: unknown): EntityConstructor | undefined {
|
|
35
|
+
return (ctor as Prefab | undefined)?.__entity;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The views a prefab declares, same rule. */
|
|
39
|
+
export function viewsOf(ctor: unknown): PresenterViews | undefined {
|
|
40
|
+
return (ctor as Prefab | undefined)?.__views;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The handler-wide view a prefab declares, same rule. */
|
|
44
|
+
export function outputOf(ctor: unknown): EntityConstructor | undefined {
|
|
45
|
+
return (ctor as Prefab | undefined)?.__output;
|
|
46
|
+
}
|