@effect-app/infra 4.0.0-beta.98 → 4.0.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/CHANGELOG.md +2891 -0
- package/_check.sh +1 -1
- package/dist/CUPS.d.ts +28 -9
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +35 -14
- package/dist/ClusterCosmos.d.ts +67 -0
- package/dist/ClusterCosmos.d.ts.map +1 -0
- package/dist/ClusterCosmos.js +588 -0
- package/dist/ClusterServiceBus.d.ts +67 -0
- package/dist/ClusterServiceBus.d.ts.map +1 -0
- package/dist/ClusterServiceBus.js +84 -0
- package/dist/{api/ContextProvider.d.ts → ContextProvider.d.ts} +7 -4
- package/dist/ContextProvider.d.ts.map +1 -0
- package/dist/ContextProvider.js +40 -0
- package/dist/Emailer/Sendgrid.d.ts +110 -146
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +24 -19
- package/dist/Emailer/fake.d.ts +1 -1
- package/dist/Emailer/fake.d.ts.map +1 -1
- package/dist/Emailer/fake.js +4 -4
- package/dist/MainFiberSet.d.ts +11 -7
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +22 -6
- package/dist/QueueMaker/errors.d.ts +3 -1
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/errors.js +4 -2
- package/dist/{adapters/SQL → SQL}/Model.d.ts +34 -45
- package/dist/SQL/Model.d.ts.map +1 -0
- package/dist/SQL/Model.js +469 -0
- package/dist/SQL.d.ts.map +1 -0
- package/dist/{adapters/SQL.js → SQL.js} +1 -1
- package/dist/Store/Cosmos/query.d.ts +16 -4
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +238 -42
- package/dist/Store/Cosmos.d.ts +2 -2
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +394 -319
- package/dist/Store/Disk.d.ts +4 -4
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +91 -39
- package/dist/Store/Memory.d.ts +7 -10
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +348 -78
- package/dist/Store/SQL/Pg.d.ts +2 -2
- package/dist/Store/SQL/Pg.d.ts.map +1 -1
- package/dist/Store/SQL/Pg.js +229 -154
- package/dist/Store/SQL/query.d.ts +19 -3
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +306 -12
- package/dist/Store/SQL.d.ts +22 -5
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +369 -98
- package/dist/Store/codeFilter.d.ts +4 -4
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +71 -22
- package/dist/Store/index.d.ts +6 -4
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +9 -6
- package/dist/Store/jsonDocument.d.ts +67 -0
- package/dist/Store/jsonDocument.d.ts.map +1 -0
- package/dist/Store/jsonDocument.js +107 -0
- package/dist/Store/utils.d.ts +21 -2
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +253 -10
- package/dist/WorkflowEngineCosmos.d.ts +30 -0
- package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
- package/dist/WorkflowEngineCosmos.js +542 -0
- package/dist/WorkflowEngineSqlite.d.ts +24 -0
- package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
- package/dist/WorkflowEngineSqlite.js +550 -0
- package/dist/arbs.d.ts +6 -3
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +20 -4
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +5 -0
- package/dist/cosmos-client.d.ts +40 -0
- package/dist/cosmos-client.d.ts.map +1 -0
- package/dist/cosmos-client.js +41 -0
- package/dist/errorReporter.d.ts +6 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +23 -27
- package/dist/fileUtil.d.ts +1 -1
- package/dist/fileUtil.d.ts.map +1 -1
- package/dist/fileUtil.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/internal/RequestContextMiddleware.d.ts +5 -0
- package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
- package/dist/internal/RequestContextMiddleware.js +45 -0
- package/dist/internal/auth.d.ts +53 -0
- package/dist/internal/auth.d.ts.map +1 -0
- package/dist/internal/auth.js +180 -0
- package/dist/internal/events.d.ts +11 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +49 -0
- package/dist/{api/internal → internal}/health.d.ts +1 -1
- package/dist/internal/health.d.ts.map +1 -0
- package/dist/internal/health.js +5 -0
- package/dist/{api/layerUtils.d.ts → layerUtils.d.ts} +10 -2
- package/dist/layerUtils.d.ts.map +1 -0
- package/dist/layerUtils.js +17 -0
- package/dist/logger/jsonLogger.d.ts +1 -1
- package/dist/logger/jsonLogger.d.ts.map +1 -1
- package/dist/logger/jsonLogger.js +5 -3
- package/dist/logger/logFmtLogger.d.ts +1 -1
- package/dist/logger/logFmtLogger.d.ts.map +1 -1
- package/dist/logger/logFmtLogger.js +3 -3
- package/dist/logger/shared.d.ts +2 -2
- package/dist/logger/shared.d.ts.map +1 -1
- package/dist/logger/shared.js +6 -6
- package/dist/logger.d.ts.map +1 -1
- package/dist/middlewares.d.ts.map +1 -0
- package/dist/{api/middlewares.js → middlewares.js} +1 -1
- package/dist/{adapters/mongo-client.d.ts → mongo-client.d.ts} +2 -1
- package/dist/mongo-client.d.ts.map +1 -0
- package/dist/mongo-client.js +15 -0
- package/dist/otel.d.ts +75 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +84 -0
- package/dist/rateLimit.d.ts +11 -3
- package/dist/rateLimit.d.ts.map +1 -1
- package/dist/rateLimit.js +7 -12
- package/dist/{adapters/redis-client.d.ts → redis-client.d.ts} +4 -1
- package/dist/redis-client.d.ts.map +1 -0
- package/dist/redis-client.js +98 -0
- package/dist/{api/reportError.d.ts → reportError.d.ts} +1 -1
- package/dist/reportError.d.ts.map +1 -0
- package/dist/reportError.js +28 -0
- package/dist/{api/routing → routing}/middleware/RouterMiddleware.d.ts +2 -1
- package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
- package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
- package/dist/{api/routing → routing}/middleware/middleware.d.ts +11 -6
- package/dist/routing/middleware/middleware.d.ts.map +1 -0
- package/dist/routing/middleware/middleware.js +152 -0
- package/dist/{api/routing → routing}/middleware.d.ts +0 -1
- package/dist/routing/middleware.d.ts.map +1 -0
- package/dist/{api/routing → routing}/middleware.js +1 -2
- package/dist/routing/schema/jwt.d.ts.map +1 -0
- package/dist/routing/schema/jwt.js +12 -0
- package/dist/routing/tsort.d.ts.map +1 -0
- package/dist/routing/tsort.js +51 -0
- package/dist/{api/routing → routing}/utils.d.ts +3 -3
- package/dist/routing/utils.d.ts.map +1 -0
- package/dist/routing/utils.js +45 -0
- package/dist/routing.d.ts +182 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +256 -0
- package/dist/test.d.ts +2 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +5 -5
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +14 -0
- package/docs/cluster-storage.md +26 -0
- package/docs/workflow-engine.md +262 -0
- package/examples/query.ts +23 -19
- package/package.json +36 -364
- package/src/CUPS.ts +53 -14
- package/src/ClusterCosmos.ts +1170 -0
- package/src/ClusterServiceBus.ts +244 -0
- package/src/{api/ContextProvider.ts → ContextProvider.ts} +10 -7
- package/src/Emailer/Sendgrid.ts +83 -60
- package/src/Emailer/fake.ts +4 -4
- package/src/MainFiberSet.ts +31 -12
- package/src/QueueMaker/errors.ts +3 -1
- package/src/{adapters/SQL → SQL}/Model.ts +48 -47
- package/src/SQL.ts +1 -0
- package/src/Store/Cosmos/query.ts +275 -55
- package/src/Store/Cosmos.ts +597 -515
- package/src/Store/Disk.ts +145 -71
- package/src/Store/Memory.ts +420 -115
- package/src/Store/SQL/Pg.ts +352 -268
- package/src/Store/SQL/query.ts +366 -20
- package/src/Store/SQL.ts +742 -293
- package/src/Store/codeFilter.ts +78 -28
- package/src/Store/index.ts +18 -13
- package/src/Store/jsonDocument.ts +143 -0
- package/src/Store/utils.ts +307 -30
- package/src/WorkflowEngineCosmos.ts +749 -0
- package/src/WorkflowEngineSqlite.ts +813 -0
- package/src/arbs.ts +25 -5
- package/src/cosmos-client.ts +72 -0
- package/src/errorReporter.ts +68 -78
- package/src/fileUtil.ts +1 -1
- package/src/index.ts +2 -1
- package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
- package/src/internal/auth.ts +272 -0
- package/src/{api/internal → internal}/events.ts +18 -9
- package/src/{api/layerUtils.ts → layerUtils.ts} +7 -3
- package/src/logger/jsonLogger.ts +5 -3
- package/src/logger/logFmtLogger.ts +3 -3
- package/src/logger/shared.ts +6 -5
- package/src/middlewares.ts +10 -0
- package/src/{adapters/mongo-client.ts → mongo-client.ts} +3 -1
- package/src/otel.ts +177 -0
- package/src/rateLimit.ts +35 -24
- package/src/{adapters/redis-client.ts → redis-client.ts} +6 -2
- package/src/{api/reportError.ts → reportError.ts} +3 -2
- package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +2 -1
- package/src/{api/routing → routing}/middleware/middleware.ts +75 -50
- package/src/routing/middleware.ts +4 -0
- package/src/{api/routing → routing}/schema/jwt.ts +2 -2
- package/src/{api/routing → routing}/utils.ts +2 -1
- package/src/routing.ts +823 -0
- package/src/test.ts +6 -6
- package/src/vitest.ts +1 -1
- package/test/arbs-magnitude.test.ts +33 -0
- package/test/arbs.test.ts +41 -0
- package/test/auth.test.ts +101 -0
- package/test/cluster-cosmos.test.ts +634 -0
- package/test/cluster-servicebus.test.ts +180 -0
- package/test/cluster-sqlite.test.ts +207 -0
- package/test/contextProvider.test.ts +11 -8
- package/test/controller.test.ts +23 -28
- package/test/cosmos-container-throughput.test.ts +80 -0
- package/test/cosmos-query.test.ts +368 -0
- package/test/dist/arbs-magnitude.test.d.ts.map +1 -0
- package/test/dist/arbs.test.d.ts.map +1 -0
- package/test/dist/auth.test.d.ts.map +1 -0
- package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
- package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
- package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
- package/test/dist/contextProvider.test.d.ts.map +1 -1
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/cosmos-container-throughput.test.d.ts.map +1 -0
- package/test/dist/cosmos-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +17 -9
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +8 -3
- package/test/dist/json-lower.test.d.ts.map +1 -0
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/dist/repository-ext.test.d.ts.map +1 -0
- package/test/dist/repository-legacy-document.test.d.ts.map +1 -0
- package/test/dist/requires.test.d.ts.map +1 -1
- package/test/dist/router-generator.test.d.ts.map +1 -0
- package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
- package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
- package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
- package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
- package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
- package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
- package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
- package/test/fixtures.ts +7 -2
- package/test/json-lower.test.ts +129 -0
- package/test/layerUtils.test.ts +2 -2
- package/test/query.test.ts +1232 -42
- package/test/rawQuery.test.ts +432 -7
- package/test/repository-ext.test.ts +456 -0
- package/test/repository-legacy-document.test.ts +394 -0
- package/test/requires.test.ts +7 -3
- package/test/router-generator.test.ts +187 -0
- package/test/routing-interruptibility.test.ts +66 -0
- package/test/rpc-context-map-streaming.test.ts +265 -0
- package/test/rpc-e2e-invalidation.test.ts +421 -0
- package/test/rpc-multi-middleware.test.ts +17 -11
- package/test/rpc-stream-fullstack.test.ts +307 -0
- package/test/sql-store.test.ts +1202 -47
- package/test/validateSample.test.ts +70 -42
- package/test/workflow-engine-cosmos.test.ts +345 -0
- package/test/workflow-engine-sqlite.test.ts +297 -0
- package/tsconfig.examples.json +1 -1
- package/tsconfig.json +9 -1
- package/tsconfig.src.json +35 -35
- package/tsconfig.test.json +2 -2
- package/dist/Emailer/service.d.ts +0 -55
- package/dist/Emailer/service.d.ts.map +0 -1
- package/dist/Emailer/service.js +0 -6
- package/dist/Emailer.d.ts +0 -2
- package/dist/Emailer.d.ts.map +0 -1
- package/dist/Emailer.js +0 -2
- package/dist/Model/Repository/ext.d.ts +0 -41
- package/dist/Model/Repository/ext.d.ts.map +0 -1
- package/dist/Model/Repository/ext.js +0 -65
- package/dist/Model/Repository/internal/internal.d.ts +0 -59
- package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
- package/dist/Model/Repository/internal/internal.js +0 -320
- package/dist/Model/Repository/legacy.d.ts +0 -19
- package/dist/Model/Repository/legacy.d.ts.map +0 -1
- package/dist/Model/Repository/legacy.js +0 -2
- package/dist/Model/Repository/makeRepo.d.ts +0 -49
- package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
- package/dist/Model/Repository/makeRepo.js +0 -24
- package/dist/Model/Repository/service.d.ts +0 -89
- package/dist/Model/Repository/service.d.ts.map +0 -1
- package/dist/Model/Repository/service.js +0 -2
- package/dist/Model/Repository/validation.d.ts +0 -43
- package/dist/Model/Repository/validation.d.ts.map +0 -1
- package/dist/Model/Repository/validation.js +0 -32
- package/dist/Model/Repository.d.ts +0 -6
- package/dist/Model/Repository.d.ts.map +0 -1
- package/dist/Model/Repository.js +0 -6
- package/dist/Model/dsl.d.ts +0 -32
- package/dist/Model/dsl.d.ts.map +0 -1
- package/dist/Model/dsl.js +0 -44
- package/dist/Model/filter/filterApi.d.ts +0 -30
- package/dist/Model/filter/filterApi.d.ts.map +0 -1
- package/dist/Model/filter/filterApi.js +0 -2
- package/dist/Model/filter/types/errors.d.ts +0 -29
- package/dist/Model/filter/types/errors.d.ts.map +0 -1
- package/dist/Model/filter/types/errors.js +0 -2
- package/dist/Model/filter/types/fields.d.ts +0 -15
- package/dist/Model/filter/types/fields.d.ts.map +0 -1
- package/dist/Model/filter/types/fields.js +0 -2
- package/dist/Model/filter/types/path/common.d.ts +0 -316
- package/dist/Model/filter/types/path/common.d.ts.map +0 -1
- package/dist/Model/filter/types/path/common.js +0 -2
- package/dist/Model/filter/types/path/eager.d.ts +0 -95
- package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
- package/dist/Model/filter/types/path/eager.js +0 -31
- package/dist/Model/filter/types/path/index.d.ts +0 -4
- package/dist/Model/filter/types/path/index.d.ts.map +0 -1
- package/dist/Model/filter/types/path/index.js +0 -3
- package/dist/Model/filter/types/utils.d.ts +0 -79
- package/dist/Model/filter/types/utils.d.ts.map +0 -1
- package/dist/Model/filter/types/utils.js +0 -2
- package/dist/Model/filter/types/validator.d.ts +0 -30
- package/dist/Model/filter/types/validator.d.ts.map +0 -1
- package/dist/Model/filter/types/validator.js +0 -2
- package/dist/Model/filter/types.d.ts +0 -5
- package/dist/Model/filter/types.d.ts.map +0 -1
- package/dist/Model/filter/types.js +0 -7
- package/dist/Model/query/dsl.d.ts +0 -248
- package/dist/Model/query/dsl.d.ts.map +0 -1
- package/dist/Model/query/dsl.js +0 -104
- package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
- package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
- package/dist/Model/query/new-kid-interpreter.js +0 -165
- package/dist/Model/query.d.ts +0 -15
- package/dist/Model/query.d.ts.map +0 -1
- package/dist/Model/query.js +0 -3
- package/dist/Model.d.ts +0 -4
- package/dist/Model.d.ts.map +0 -1
- package/dist/Model.js +0 -4
- package/dist/Operations.d.ts +0 -55
- package/dist/Operations.d.ts.map +0 -1
- package/dist/Operations.js +0 -102
- package/dist/OperationsRepo.d.ts +0 -41
- package/dist/OperationsRepo.d.ts.map +0 -1
- package/dist/OperationsRepo.js +0 -14
- package/dist/QueueMaker/SQLQueue.d.ts +0 -14
- package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
- package/dist/QueueMaker/SQLQueue.js +0 -132
- package/dist/QueueMaker/memQueue.d.ts +0 -11
- package/dist/QueueMaker/memQueue.d.ts.map +0 -1
- package/dist/QueueMaker/memQueue.js +0 -76
- package/dist/QueueMaker/sbqueue.d.ts +0 -12
- package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
- package/dist/QueueMaker/sbqueue.js +0 -91
- package/dist/QueueMaker/service.d.ts +0 -11
- package/dist/QueueMaker/service.d.ts.map +0 -1
- package/dist/QueueMaker/service.js +0 -4
- package/dist/RequestContext.d.ts +0 -68
- package/dist/RequestContext.d.ts.map +0 -1
- package/dist/RequestContext.js +0 -49
- package/dist/RequestFiberSet.d.ts +0 -36
- package/dist/RequestFiberSet.d.ts.map +0 -1
- package/dist/RequestFiberSet.js +0 -78
- package/dist/Store/ContextMapContainer.d.ts +0 -25
- package/dist/Store/ContextMapContainer.d.ts.map +0 -1
- package/dist/Store/ContextMapContainer.js +0 -77
- package/dist/Store/service.d.ts +0 -111
- package/dist/Store/service.d.ts.map +0 -1
- package/dist/Store/service.js +0 -79
- package/dist/Store.d.ts +0 -2
- package/dist/Store.d.ts.map +0 -1
- package/dist/Store.js +0 -2
- package/dist/adapters/SQL/Model.d.ts.map +0 -1
- package/dist/adapters/SQL/Model.js +0 -478
- package/dist/adapters/SQL.d.ts.map +0 -1
- package/dist/adapters/ServiceBus.d.ts +0 -58
- package/dist/adapters/ServiceBus.d.ts.map +0 -1
- package/dist/adapters/ServiceBus.js +0 -99
- package/dist/adapters/cosmos-client.d.ts +0 -14
- package/dist/adapters/cosmos-client.d.ts.map +0 -1
- package/dist/adapters/cosmos-client.js +0 -9
- package/dist/adapters/index.d.ts +0 -2
- package/dist/adapters/index.d.ts.map +0 -1
- package/dist/adapters/index.js +0 -2
- package/dist/adapters/logger.d.ts +0 -9
- package/dist/adapters/logger.d.ts.map +0 -1
- package/dist/adapters/logger.js +0 -3
- package/dist/adapters/memQueue.d.ts +0 -13
- package/dist/adapters/memQueue.d.ts.map +0 -1
- package/dist/adapters/memQueue.js +0 -20
- package/dist/adapters/mongo-client.d.ts.map +0 -1
- package/dist/adapters/mongo-client.js +0 -13
- package/dist/adapters/redis-client.d.ts.map +0 -1
- package/dist/adapters/redis-client.js +0 -94
- package/dist/api/ContextProvider.d.ts.map +0 -1
- package/dist/api/ContextProvider.js +0 -38
- package/dist/api/codec.d.ts.map +0 -1
- package/dist/api/codec.js +0 -5
- package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
- package/dist/api/internal/RequestContextMiddleware.js +0 -35
- package/dist/api/internal/auth.d.ts +0 -15
- package/dist/api/internal/auth.d.ts.map +0 -1
- package/dist/api/internal/auth.js +0 -47
- package/dist/api/internal/events.d.ts +0 -9
- package/dist/api/internal/events.d.ts.map +0 -1
- package/dist/api/internal/events.js +0 -42
- package/dist/api/internal/health.d.ts.map +0 -1
- package/dist/api/internal/health.js +0 -5
- package/dist/api/layerUtils.d.ts.map +0 -1
- package/dist/api/layerUtils.js +0 -16
- package/dist/api/middlewares.d.ts.map +0 -1
- package/dist/api/reportError.d.ts.map +0 -1
- package/dist/api/reportError.js +0 -27
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
- package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
- package/dist/api/routing/middleware/middleware.js +0 -130
- package/dist/api/routing/middleware.d.ts.map +0 -1
- package/dist/api/routing/schema/jwt.d.ts.map +0 -1
- package/dist/api/routing/schema/jwt.js +0 -12
- package/dist/api/routing/tsort.d.ts.map +0 -1
- package/dist/api/routing/tsort.js +0 -51
- package/dist/api/routing/utils.d.ts.map +0 -1
- package/dist/api/routing/utils.js +0 -44
- package/dist/api/routing.d.ts +0 -134
- package/dist/api/routing.d.ts.map +0 -1
- package/dist/api/routing.js +0 -167
- package/dist/api/setupRequest.d.ts +0 -15
- package/dist/api/setupRequest.d.ts.map +0 -1
- package/dist/api/setupRequest.js +0 -49
- package/dist/api/util.d.ts.map +0 -1
- package/dist/api/util.js +0 -14
- package/eslint.config.mjs +0 -24
- package/src/Emailer/service.ts +0 -52
- package/src/Emailer.ts +0 -1
- package/src/Model/Repository/ext.ts +0 -283
- package/src/Model/Repository/internal/internal.ts +0 -580
- package/src/Model/Repository/legacy.ts +0 -27
- package/src/Model/Repository/makeRepo.ts +0 -139
- package/src/Model/Repository/service.ts +0 -627
- package/src/Model/Repository/validation.ts +0 -31
- package/src/Model/Repository.ts +0 -5
- package/src/Model/dsl.ts +0 -128
- package/src/Model/filter/filterApi.ts +0 -60
- package/src/Model/filter/types/errors.ts +0 -47
- package/src/Model/filter/types/fields.ts +0 -50
- package/src/Model/filter/types/path/common.ts +0 -404
- package/src/Model/filter/types/path/eager.ts +0 -298
- package/src/Model/filter/types/path/index.ts +0 -4
- package/src/Model/filter/types/utils.ts +0 -128
- package/src/Model/filter/types/validator.ts +0 -46
- package/src/Model/filter/types.ts +0 -6
- package/src/Model/query/dsl.ts +0 -2110
- package/src/Model/query/new-kid-interpreter.ts +0 -210
- package/src/Model/query.ts +0 -13
- package/src/Model.ts +0 -3
- package/src/Operations.ts +0 -235
- package/src/OperationsRepo.ts +0 -16
- package/src/QueueMaker/SQLQueue.ts +0 -190
- package/src/QueueMaker/memQueue.ts +0 -130
- package/src/QueueMaker/sbqueue.ts +0 -137
- package/src/QueueMaker/service.ts +0 -17
- package/src/RequestContext.ts +0 -63
- package/src/RequestFiberSet.ts +0 -105
- package/src/Store/ContextMapContainer.ts +0 -116
- package/src/Store/index.test.ts.bak +0 -112
- package/src/Store/service.ts +0 -193
- package/src/Store.ts +0 -1
- package/src/adapters/SQL.ts +0 -1
- package/src/adapters/ServiceBus.ts +0 -209
- package/src/adapters/cosmos-client.ts +0 -16
- package/src/adapters/index.ts +0 -0
- package/src/adapters/logger.ts +0 -3
- package/src/adapters/memQueue.ts +0 -21
- package/src/api/internal/auth.ts +0 -68
- package/src/api/middlewares.ts +0 -10
- package/src/api/routing/middleware.ts +0 -6
- package/src/api/routing.ts +0 -598
- package/src/api/setupRequest.ts +0 -104
- package/test/dist/date-query.test.d.ts.map +0 -1
- package/tsconfig.json.bak +0 -12
- /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
- /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
- /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
- /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
- /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
- /package/dist/{api/util.d.ts → util.d.ts} +0 -0
- /package/src/{api/codec.ts → codec.ts} +0 -0
- /package/src/{api/internal → internal}/health.ts +0 -0
- /package/src/{api/routing → routing}/tsort.ts +0 -0
- /package/src/{api/util.ts → util.ts} +0 -0
package/test/sql-store.test.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import type Sqlite from "better-sqlite3"
|
|
3
3
|
import BetterSqlite from "better-sqlite3"
|
|
4
|
+
import * as S from "effect-app/Schema"
|
|
4
5
|
import { describe, expect, it } from "vitest"
|
|
5
6
|
import { parseRow } from "../src/Store/SQL.js"
|
|
6
7
|
import { buildWhereSQLQuery, pgDialect, sqliteDialect } from "../src/Store/SQL/query.js"
|
|
7
|
-
import { makeETag } from "../src/Store/utils.js"
|
|
8
|
+
import { makeETag, makeJsonLower } from "../src/Store/utils.js"
|
|
8
9
|
|
|
9
10
|
const query = (db: Sqlite.Database, sql: string, params: unknown[] = []) =>
|
|
10
11
|
db.prepare(sql).all(...params as any[]) as any[]
|
|
@@ -24,11 +25,182 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
24
25
|
expect(result.params).toContain("John")
|
|
25
26
|
})
|
|
26
27
|
|
|
28
|
+
it("where eq Date binds ISO string", () => {
|
|
29
|
+
const result = buildWhereSQLQuery(
|
|
30
|
+
sqliteDialect,
|
|
31
|
+
"id",
|
|
32
|
+
[{ t: "where", path: "n", op: "eq", value: new Date("2024-01-01T00:00:00.000Z") }],
|
|
33
|
+
"users",
|
|
34
|
+
{}
|
|
35
|
+
)
|
|
36
|
+
expect(result.params).toContain("2024-01-01T00:00:00.000Z")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("where eq app native Encoded binds via JsonLower", () => {
|
|
40
|
+
class Day {
|
|
41
|
+
readonly ymd: string
|
|
42
|
+
constructor(ymd: string) {
|
|
43
|
+
this.ymd = ymd
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const day = new Day("2024-06-01")
|
|
47
|
+
const json = {
|
|
48
|
+
toJson: (v: unknown) => v instanceof Day ? v.ymd : v,
|
|
49
|
+
jsonifyFilter: (filter: readonly { t: string; path?: string; op?: string; value?: unknown }[]) =>
|
|
50
|
+
filter.map((r) => r.t === "where" ? { ...r, value: r.value instanceof Day ? r.value.ymd : r.value } : r)
|
|
51
|
+
}
|
|
52
|
+
const result = buildWhereSQLQuery(
|
|
53
|
+
sqliteDialect,
|
|
54
|
+
"id",
|
|
55
|
+
[{ t: "where", path: "day", op: "eq", value: day }],
|
|
56
|
+
"users",
|
|
57
|
+
{},
|
|
58
|
+
undefined,
|
|
59
|
+
undefined,
|
|
60
|
+
undefined,
|
|
61
|
+
undefined,
|
|
62
|
+
undefined,
|
|
63
|
+
json as never
|
|
64
|
+
)
|
|
65
|
+
expect(result.params).toContain("2024-06-01")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("does not re-encode store-lowered Date defaultValues", () => {
|
|
69
|
+
class Item extends S.Class<Item>("SqlDefaultValuesItem")({
|
|
70
|
+
id: S.String,
|
|
71
|
+
at: S.Date
|
|
72
|
+
}) {}
|
|
73
|
+
const json = makeJsonLower({ schema: Item })
|
|
74
|
+
const at = new Date("2024-06-01T00:00:00.000Z")
|
|
75
|
+
const lowered = json.toJson({ at }) as Record<string, unknown>
|
|
76
|
+
const result = buildWhereSQLQuery(
|
|
77
|
+
sqliteDialect,
|
|
78
|
+
"id",
|
|
79
|
+
[{ t: "where", path: "at", op: "eq", value: at }],
|
|
80
|
+
"items",
|
|
81
|
+
lowered,
|
|
82
|
+
undefined,
|
|
83
|
+
undefined,
|
|
84
|
+
undefined,
|
|
85
|
+
undefined,
|
|
86
|
+
undefined,
|
|
87
|
+
json
|
|
88
|
+
)
|
|
89
|
+
expect(result.sql).toContain("COALESCE(")
|
|
90
|
+
expect(result.params).toContain("2024-06-01T00:00:00.000Z")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("where in Set binds array values", () => {
|
|
94
|
+
const result = buildWhereSQLQuery(
|
|
95
|
+
sqliteDialect,
|
|
96
|
+
"id",
|
|
97
|
+
[{ t: "where", path: "tags", op: "in", value: new Set(["a", "b"]) }],
|
|
98
|
+
"users",
|
|
99
|
+
{}
|
|
100
|
+
)
|
|
101
|
+
expect(result.params).toEqual(expect.arrayContaining(["a", "b"]))
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("where includes Date binds ISO string", () => {
|
|
105
|
+
const result = buildWhereSQLQuery(
|
|
106
|
+
sqliteDialect,
|
|
107
|
+
"id",
|
|
108
|
+
[{ t: "where", path: "dates", op: "includes", value: new Date("2024-01-01T00:00:00.000Z") }],
|
|
109
|
+
"users",
|
|
110
|
+
{}
|
|
111
|
+
)
|
|
112
|
+
expect(result.params).toContain("2024-01-01T00:00:00.000Z")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("where hasKey / hasValue / hasKeyValue on Map JSON tuples", () => {
|
|
116
|
+
const key = buildWhereSQLQuery(
|
|
117
|
+
sqliteDialect,
|
|
118
|
+
"id",
|
|
119
|
+
[{ t: "where", path: "meta", op: "hasKey", value: "n" }],
|
|
120
|
+
"users",
|
|
121
|
+
{}
|
|
122
|
+
)
|
|
123
|
+
expect(key.sql).toContain("json_extract(value, '$[0]')")
|
|
124
|
+
expect(key.params).toContain("n")
|
|
125
|
+
|
|
126
|
+
const value = buildWhereSQLQuery(
|
|
127
|
+
sqliteDialect,
|
|
128
|
+
"id",
|
|
129
|
+
[{ t: "where", path: "meta", op: "hasValue", value: 1 }],
|
|
130
|
+
"users",
|
|
131
|
+
{}
|
|
132
|
+
)
|
|
133
|
+
expect(value.sql).toContain("json_extract(value, '$[1]')")
|
|
134
|
+
expect(value.params).toContain(1)
|
|
135
|
+
|
|
136
|
+
const pair = buildWhereSQLQuery(
|
|
137
|
+
sqliteDialect,
|
|
138
|
+
"id",
|
|
139
|
+
[{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
|
|
140
|
+
"users",
|
|
141
|
+
{}
|
|
142
|
+
)
|
|
143
|
+
expect(pair.sql).toContain("json_each")
|
|
144
|
+
expect(pair.params).toContain(JSON.stringify(["n", 1]))
|
|
145
|
+
|
|
146
|
+
const anyKeys = buildWhereSQLQuery(
|
|
147
|
+
sqliteDialect,
|
|
148
|
+
"id",
|
|
149
|
+
[{ t: "where", path: "meta", op: "hasKey-any", value: ["n", "x"] }],
|
|
150
|
+
"users",
|
|
151
|
+
{}
|
|
152
|
+
)
|
|
153
|
+
expect(anyKeys.sql).toContain(" OR ")
|
|
154
|
+
expect(anyKeys.params).toEqual(expect.arrayContaining(["n", "x"]))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("pg where hasKey / hasKeyValue uses jsonb tuple elements", () => {
|
|
158
|
+
const key = buildWhereSQLQuery(
|
|
159
|
+
pgDialect,
|
|
160
|
+
"id",
|
|
161
|
+
[{ t: "where", path: "meta", op: "hasKey", value: "n" }],
|
|
162
|
+
"users",
|
|
163
|
+
{}
|
|
164
|
+
)
|
|
165
|
+
expect(key.sql).toContain("jsonb_array_elements")
|
|
166
|
+
expect(key.sql).toContain("e->0")
|
|
167
|
+
expect(key.params).toContain(JSON.stringify("n"))
|
|
168
|
+
|
|
169
|
+
const pair = buildWhereSQLQuery(
|
|
170
|
+
pgDialect,
|
|
171
|
+
"id",
|
|
172
|
+
[{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
|
|
173
|
+
"users",
|
|
174
|
+
{}
|
|
175
|
+
)
|
|
176
|
+
expect(pair.sql).toContain("@>")
|
|
177
|
+
expect(pair.sql).toContain("jsonb_build_array")
|
|
178
|
+
expect(pair.params).toContain(JSON.stringify(["n", 1]))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("where includes-any Date[] binds ISO strings", () => {
|
|
182
|
+
const result = buildWhereSQLQuery(
|
|
183
|
+
sqliteDialect,
|
|
184
|
+
"id",
|
|
185
|
+
[{
|
|
186
|
+
t: "where",
|
|
187
|
+
path: "dates",
|
|
188
|
+
op: "includes-any",
|
|
189
|
+
value: [new Date("2024-01-01T00:00:00.000Z"), new Date("2024-06-01T00:00:00.000Z")]
|
|
190
|
+
}],
|
|
191
|
+
"users",
|
|
192
|
+
{}
|
|
193
|
+
)
|
|
194
|
+
expect(result.params).toEqual(
|
|
195
|
+
expect.arrayContaining(["2024-01-01T00:00:00.000Z", "2024-06-01T00:00:00.000Z"])
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
27
199
|
it("where eq number", () => {
|
|
28
200
|
const result = buildWhereSQLQuery(
|
|
29
201
|
sqliteDialect,
|
|
30
202
|
"id",
|
|
31
|
-
[{ t: "where", path: "age", op: "eq", value: 25
|
|
203
|
+
[{ t: "where", path: "age", op: "eq", value: 25 }],
|
|
32
204
|
"users",
|
|
33
205
|
{}
|
|
34
206
|
)
|
|
@@ -40,7 +212,7 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
40
212
|
const result = buildWhereSQLQuery(
|
|
41
213
|
sqliteDialect,
|
|
42
214
|
"id",
|
|
43
|
-
[{ t: "where", path: "age", op: "gt", value: 18
|
|
215
|
+
[{ t: "where", path: "age", op: "gt", value: 18 }],
|
|
44
216
|
"users",
|
|
45
217
|
{}
|
|
46
218
|
)
|
|
@@ -70,7 +242,7 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
70
242
|
"id",
|
|
71
243
|
[
|
|
72
244
|
{ t: "where", path: "name", op: "eq", value: "Alice" },
|
|
73
|
-
{ t: "and", path: "age", op: "gt", value: 18
|
|
245
|
+
{ t: "and", path: "age", op: "gt", value: 18 }
|
|
74
246
|
],
|
|
75
247
|
"users",
|
|
76
248
|
{}
|
|
@@ -83,7 +255,7 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
83
255
|
const result = buildWhereSQLQuery(
|
|
84
256
|
sqliteDialect,
|
|
85
257
|
"id",
|
|
86
|
-
[{ t: "where", path: "id", op: "in", value: ["a", "b", "c"]
|
|
258
|
+
[{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] }],
|
|
87
259
|
"users",
|
|
88
260
|
{}
|
|
89
261
|
)
|
|
@@ -167,7 +339,7 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
167
339
|
const result = buildWhereSQLQuery(
|
|
168
340
|
sqliteDialect,
|
|
169
341
|
"id",
|
|
170
|
-
[{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"]
|
|
342
|
+
[{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] }],
|
|
171
343
|
"users",
|
|
172
344
|
{}
|
|
173
345
|
)
|
|
@@ -228,6 +400,241 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
228
400
|
expect(result.sql).toContain("LIMIT")
|
|
229
401
|
expect(result.sql).toContain("OFFSET")
|
|
230
402
|
})
|
|
403
|
+
|
|
404
|
+
it("computed relation count projection", () => {
|
|
405
|
+
const result = buildWhereSQLQuery(
|
|
406
|
+
sqliteDialect,
|
|
407
|
+
"id",
|
|
408
|
+
[],
|
|
409
|
+
"users",
|
|
410
|
+
{},
|
|
411
|
+
[{
|
|
412
|
+
key: "pickedCount",
|
|
413
|
+
computed: {
|
|
414
|
+
_tag: "relation-count",
|
|
415
|
+
path: "items",
|
|
416
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
417
|
+
}
|
|
418
|
+
}]
|
|
419
|
+
)
|
|
420
|
+
expect(result.sql).toContain(`SELECT COUNT(1) FROM json_each(data, '$.items') AS _items`)
|
|
421
|
+
expect(result.sql).toContain(`AS "pickedCount"`)
|
|
422
|
+
expect(result.params).toContain("%picked%")
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it("computed relation any projection (sqlite bool encoding)", () => {
|
|
426
|
+
const result = buildWhereSQLQuery(
|
|
427
|
+
sqliteDialect,
|
|
428
|
+
"id",
|
|
429
|
+
[],
|
|
430
|
+
"users",
|
|
431
|
+
{},
|
|
432
|
+
[{
|
|
433
|
+
key: "hasPicked",
|
|
434
|
+
computed: {
|
|
435
|
+
_tag: "relation-any",
|
|
436
|
+
path: "items",
|
|
437
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
438
|
+
}
|
|
439
|
+
}]
|
|
440
|
+
)
|
|
441
|
+
expect(result.sql).toContain("CASE WHEN EXISTS(")
|
|
442
|
+
expect(result.sql).toContain(`AS "hasPicked"`)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("computed relation every projection (sqlite emits NOT EXISTS NOT)", () => {
|
|
446
|
+
const result = buildWhereSQLQuery(
|
|
447
|
+
sqliteDialect,
|
|
448
|
+
"id",
|
|
449
|
+
[],
|
|
450
|
+
"users",
|
|
451
|
+
{},
|
|
452
|
+
[{
|
|
453
|
+
key: "allPicked",
|
|
454
|
+
computed: {
|
|
455
|
+
_tag: "relation-every",
|
|
456
|
+
path: "items",
|
|
457
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
|
|
458
|
+
}
|
|
459
|
+
}]
|
|
460
|
+
)
|
|
461
|
+
expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM json_each(data, '$.items') AS _items WHERE NOT (`)
|
|
462
|
+
expect(result.sql).toContain(`AS "allPicked"`)
|
|
463
|
+
expect(result.params).toContain("picked")
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it("computed relation-every with no filter degenerates to true", () => {
|
|
467
|
+
const result = buildWhereSQLQuery(
|
|
468
|
+
sqliteDialect,
|
|
469
|
+
"id",
|
|
470
|
+
[],
|
|
471
|
+
"users",
|
|
472
|
+
{},
|
|
473
|
+
[{
|
|
474
|
+
key: "allPicked",
|
|
475
|
+
computed: { _tag: "relation-every", path: "items", filter: [] }
|
|
476
|
+
}]
|
|
477
|
+
)
|
|
478
|
+
expect(result.sql).toContain("CASE WHEN 1=1")
|
|
479
|
+
expect(result.sql).toContain(`AS "allPicked"`)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it("computed relation-distinct-count projection (sqlite)", () => {
|
|
483
|
+
const result = buildWhereSQLQuery(
|
|
484
|
+
sqliteDialect,
|
|
485
|
+
"id",
|
|
486
|
+
[],
|
|
487
|
+
"users",
|
|
488
|
+
{},
|
|
489
|
+
[{
|
|
490
|
+
key: "positionCount",
|
|
491
|
+
computed: {
|
|
492
|
+
_tag: "relation-distinct-count",
|
|
493
|
+
path: "items",
|
|
494
|
+
field: "rowId",
|
|
495
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "neq", value: "cancelled" }]
|
|
496
|
+
}
|
|
497
|
+
}]
|
|
498
|
+
)
|
|
499
|
+
expect(result.sql).toContain(`SELECT COUNT(DISTINCT json_extract(_items.value, '$.rowId'))`)
|
|
500
|
+
expect(result.sql).toContain(`AS "positionCount"`)
|
|
501
|
+
expect(result.params).toContain("cancelled")
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it("computed relation-sum projection (sqlite casts to REAL)", () => {
|
|
505
|
+
const result = buildWhereSQLQuery(
|
|
506
|
+
sqliteDialect,
|
|
507
|
+
"id",
|
|
508
|
+
[],
|
|
509
|
+
"users",
|
|
510
|
+
{},
|
|
511
|
+
[{
|
|
512
|
+
key: "totalWeight",
|
|
513
|
+
computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
|
|
514
|
+
}]
|
|
515
|
+
)
|
|
516
|
+
expect(result.sql).toContain(`SELECT COALESCE(SUM(CAST(json_extract(_items.value, '$.weight') AS REAL)), 0)`)
|
|
517
|
+
expect(result.sql).toContain(`AS "totalWeight"`)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it("computed relation-sum-expr projection (sqlite)", () => {
|
|
521
|
+
const result = buildWhereSQLQuery(
|
|
522
|
+
sqliteDialect,
|
|
523
|
+
"id",
|
|
524
|
+
[],
|
|
525
|
+
"users",
|
|
526
|
+
{},
|
|
527
|
+
[{
|
|
528
|
+
key: "totalWeighted",
|
|
529
|
+
computed: {
|
|
530
|
+
_tag: "relation-sum-expr",
|
|
531
|
+
path: "items",
|
|
532
|
+
expression: {
|
|
533
|
+
_tag: "mul",
|
|
534
|
+
left: { _tag: "field", field: "weight" },
|
|
535
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
536
|
+
},
|
|
537
|
+
filter: []
|
|
538
|
+
}
|
|
539
|
+
}]
|
|
540
|
+
)
|
|
541
|
+
expect(result.sql).toContain(
|
|
542
|
+
`COALESCE(SUM((CAST(json_extract(_items.value, '$.weight') AS REAL) * CAST(json_extract(_items.value, '$.tradeUnit.amount') AS REAL))), 0)`
|
|
543
|
+
)
|
|
544
|
+
expect(result.sql).toContain(`AS "totalWeighted"`)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it("computed relation-sum-expr-by projection (sqlite)", () => {
|
|
548
|
+
const result = buildWhereSQLQuery(
|
|
549
|
+
sqliteDialect,
|
|
550
|
+
"id",
|
|
551
|
+
[],
|
|
552
|
+
"users",
|
|
553
|
+
{},
|
|
554
|
+
[{
|
|
555
|
+
key: "totalsByUnit",
|
|
556
|
+
computed: {
|
|
557
|
+
_tag: "relation-sum-expr-by",
|
|
558
|
+
path: "items",
|
|
559
|
+
expression: {
|
|
560
|
+
_tag: "mul",
|
|
561
|
+
left: { _tag: "field", field: "weight" },
|
|
562
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
563
|
+
},
|
|
564
|
+
unit: "tradeUnit.unit",
|
|
565
|
+
filter: []
|
|
566
|
+
}
|
|
567
|
+
}]
|
|
568
|
+
)
|
|
569
|
+
expect(result.sql).toContain(`json_group_array(json_object('unit', __unit, 'total', __total))`)
|
|
570
|
+
expect(result.sql).toContain(`GROUP BY json_extract(_items.value, '$.tradeUnit.unit')`)
|
|
571
|
+
expect(result.sql).toContain(`AS "totalsByUnit"`)
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("computed relation-sum-expr-normalized projection (sqlite)", () => {
|
|
575
|
+
const result = buildWhereSQLQuery(
|
|
576
|
+
sqliteDialect,
|
|
577
|
+
"id",
|
|
578
|
+
[],
|
|
579
|
+
"users",
|
|
580
|
+
{},
|
|
581
|
+
[{
|
|
582
|
+
key: "totalKg",
|
|
583
|
+
computed: {
|
|
584
|
+
_tag: "relation-sum-expr-normalized",
|
|
585
|
+
path: "items",
|
|
586
|
+
expression: {
|
|
587
|
+
_tag: "mul",
|
|
588
|
+
left: { _tag: "field", field: "weight" },
|
|
589
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
590
|
+
},
|
|
591
|
+
unit: "tradeUnit.unit",
|
|
592
|
+
toBase: "kg",
|
|
593
|
+
factors: { g: 0.001 },
|
|
594
|
+
filter: []
|
|
595
|
+
}
|
|
596
|
+
}]
|
|
597
|
+
)
|
|
598
|
+
expect(result.sql).toContain(
|
|
599
|
+
`CASE json_extract(_items.value, '$.tradeUnit.unit') WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`
|
|
600
|
+
)
|
|
601
|
+
expect(result.sql).toContain(`AS "totalKg"`)
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it("computed relation-collect (non-distinct) projection (sqlite)", () => {
|
|
605
|
+
const result = buildWhereSQLQuery(
|
|
606
|
+
sqliteDialect,
|
|
607
|
+
"id",
|
|
608
|
+
[],
|
|
609
|
+
"users",
|
|
610
|
+
{},
|
|
611
|
+
[{
|
|
612
|
+
key: "tags",
|
|
613
|
+
computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
|
|
614
|
+
}]
|
|
615
|
+
)
|
|
616
|
+
expect(result.sql).toContain(
|
|
617
|
+
`SELECT COALESCE(json_group_array(json_extract(_items.value, '$.articleId')), json_array())`
|
|
618
|
+
)
|
|
619
|
+
expect(result.sql).toContain(`AS "tags"`)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it("computed relation-collect (distinct) emits inner DISTINCT subquery (sqlite)", () => {
|
|
623
|
+
const result = buildWhereSQLQuery(
|
|
624
|
+
sqliteDialect,
|
|
625
|
+
"id",
|
|
626
|
+
[],
|
|
627
|
+
"users",
|
|
628
|
+
{},
|
|
629
|
+
[{
|
|
630
|
+
key: "tags",
|
|
631
|
+
computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
|
|
632
|
+
}]
|
|
633
|
+
)
|
|
634
|
+
expect(result.sql).toContain(`json_group_array(__v)`)
|
|
635
|
+
expect(result.sql).toContain(`SELECT DISTINCT json_extract(_items.value, '$.articleId') AS __v`)
|
|
636
|
+
expect(result.sql).toContain(`AS "tags"`)
|
|
637
|
+
})
|
|
231
638
|
})
|
|
232
639
|
|
|
233
640
|
describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
@@ -260,7 +667,7 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
|
260
667
|
const result = buildWhereSQLQuery(
|
|
261
668
|
pgDialect,
|
|
262
669
|
"id",
|
|
263
|
-
[{ t: "where", path: "status", op: "in", value: ["active", "pending"]
|
|
670
|
+
[{ t: "where", path: "status", op: "in", value: ["active", "pending"] }],
|
|
264
671
|
"users",
|
|
265
672
|
{}
|
|
266
673
|
)
|
|
@@ -291,33 +698,218 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
|
291
698
|
)
|
|
292
699
|
expect(result.sql).toContain("data->'address'->>'city'")
|
|
293
700
|
})
|
|
294
|
-
})
|
|
295
701
|
|
|
296
|
-
|
|
702
|
+
it("computed relation any projection", () => {
|
|
703
|
+
const result = buildWhereSQLQuery(
|
|
704
|
+
pgDialect,
|
|
705
|
+
"id",
|
|
706
|
+
[],
|
|
707
|
+
"users",
|
|
708
|
+
{},
|
|
709
|
+
[{
|
|
710
|
+
key: "hasPicked",
|
|
711
|
+
computed: {
|
|
712
|
+
_tag: "relation-any",
|
|
713
|
+
path: "items",
|
|
714
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
715
|
+
}
|
|
716
|
+
}]
|
|
717
|
+
)
|
|
718
|
+
expect(result.sql).toContain("EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items")
|
|
719
|
+
expect(result.sql).toContain(`AS "hasPicked"`)
|
|
720
|
+
})
|
|
297
721
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
722
|
+
it("computed relation-every (pg)", () => {
|
|
723
|
+
const result = buildWhereSQLQuery(
|
|
724
|
+
pgDialect,
|
|
725
|
+
"id",
|
|
726
|
+
[],
|
|
727
|
+
"users",
|
|
728
|
+
{},
|
|
729
|
+
[{
|
|
730
|
+
key: "allPicked",
|
|
731
|
+
computed: {
|
|
732
|
+
_tag: "relation-every",
|
|
733
|
+
path: "items",
|
|
734
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
|
|
735
|
+
}
|
|
736
|
+
}]
|
|
737
|
+
)
|
|
738
|
+
expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items WHERE NOT (`)
|
|
739
|
+
expect(result.sql).toContain(`AS "allPicked"`)
|
|
740
|
+
})
|
|
308
741
|
|
|
309
|
-
it("
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
742
|
+
it("computed relation-distinct-count (pg)", () => {
|
|
743
|
+
const result = buildWhereSQLQuery(
|
|
744
|
+
pgDialect,
|
|
745
|
+
"id",
|
|
746
|
+
[],
|
|
747
|
+
"users",
|
|
748
|
+
{},
|
|
749
|
+
[{
|
|
750
|
+
key: "positions",
|
|
751
|
+
computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
|
|
752
|
+
}]
|
|
753
|
+
)
|
|
754
|
+
expect(result.sql).toContain(`COUNT(DISTINCT _items->>'rowId')`)
|
|
755
|
+
expect(result.sql).toContain(`AS "positions"`)
|
|
756
|
+
})
|
|
317
757
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
758
|
+
it("computed relation-sum (pg casts via ::numeric)", () => {
|
|
759
|
+
const result = buildWhereSQLQuery(
|
|
760
|
+
pgDialect,
|
|
761
|
+
"id",
|
|
762
|
+
[],
|
|
763
|
+
"users",
|
|
764
|
+
{},
|
|
765
|
+
[{
|
|
766
|
+
key: "totalWeight",
|
|
767
|
+
computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
|
|
768
|
+
}]
|
|
769
|
+
)
|
|
770
|
+
expect(result.sql).toContain(`COALESCE(SUM((_items->>'weight')::numeric), 0)`)
|
|
771
|
+
expect(result.sql).toContain(`AS "totalWeight"`)
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
it("computed relation-sum-expr (pg)", () => {
|
|
775
|
+
const result = buildWhereSQLQuery(
|
|
776
|
+
pgDialect,
|
|
777
|
+
"id",
|
|
778
|
+
[],
|
|
779
|
+
"users",
|
|
780
|
+
{},
|
|
781
|
+
[{
|
|
782
|
+
key: "totalWeighted",
|
|
783
|
+
computed: {
|
|
784
|
+
_tag: "relation-sum-expr",
|
|
785
|
+
path: "items",
|
|
786
|
+
expression: {
|
|
787
|
+
_tag: "mul",
|
|
788
|
+
left: { _tag: "field", field: "weight" },
|
|
789
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
790
|
+
},
|
|
791
|
+
filter: []
|
|
792
|
+
}
|
|
793
|
+
}]
|
|
794
|
+
)
|
|
795
|
+
expect(result.sql).toContain(
|
|
796
|
+
`COALESCE(SUM(((_items->>'weight')::numeric * (_items->'tradeUnit'->>'amount')::numeric)), 0)`
|
|
797
|
+
)
|
|
798
|
+
expect(result.sql).toContain(`AS "totalWeighted"`)
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it("computed relation-sum-expr-by (pg)", () => {
|
|
802
|
+
const result = buildWhereSQLQuery(
|
|
803
|
+
pgDialect,
|
|
804
|
+
"id",
|
|
805
|
+
[],
|
|
806
|
+
"users",
|
|
807
|
+
{},
|
|
808
|
+
[{
|
|
809
|
+
key: "totalsByUnit",
|
|
810
|
+
computed: {
|
|
811
|
+
_tag: "relation-sum-expr-by",
|
|
812
|
+
path: "items",
|
|
813
|
+
expression: {
|
|
814
|
+
_tag: "mul",
|
|
815
|
+
left: { _tag: "field", field: "weight" },
|
|
816
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
817
|
+
},
|
|
818
|
+
unit: "tradeUnit.unit",
|
|
819
|
+
filter: []
|
|
820
|
+
}
|
|
821
|
+
}]
|
|
822
|
+
)
|
|
823
|
+
expect(result.sql).toContain(`jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total))`)
|
|
824
|
+
expect(result.sql).toContain(`GROUP BY _items->'tradeUnit'->>'unit'`)
|
|
825
|
+
expect(result.sql).toContain(`AS "totalsByUnit"`)
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
it("computed relation-sum-expr-normalized (pg)", () => {
|
|
829
|
+
const result = buildWhereSQLQuery(
|
|
830
|
+
pgDialect,
|
|
831
|
+
"id",
|
|
832
|
+
[],
|
|
833
|
+
"users",
|
|
834
|
+
{},
|
|
835
|
+
[{
|
|
836
|
+
key: "totalKg",
|
|
837
|
+
computed: {
|
|
838
|
+
_tag: "relation-sum-expr-normalized",
|
|
839
|
+
path: "items",
|
|
840
|
+
expression: {
|
|
841
|
+
_tag: "mul",
|
|
842
|
+
left: { _tag: "field", field: "weight" },
|
|
843
|
+
right: { _tag: "field", field: "tradeUnit.amount" }
|
|
844
|
+
},
|
|
845
|
+
unit: "tradeUnit.unit",
|
|
846
|
+
toBase: "kg",
|
|
847
|
+
factors: { g: 0.001 },
|
|
848
|
+
filter: []
|
|
849
|
+
}
|
|
850
|
+
}]
|
|
851
|
+
)
|
|
852
|
+
expect(result.sql).toContain(`CASE _items->'tradeUnit'->>'unit' WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`)
|
|
853
|
+
expect(result.sql).toContain(`AS "totalKg"`)
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
it("computed relation-collect (pg jsonb_agg)", () => {
|
|
857
|
+
const result = buildWhereSQLQuery(
|
|
858
|
+
pgDialect,
|
|
859
|
+
"id",
|
|
860
|
+
[],
|
|
861
|
+
"users",
|
|
862
|
+
{},
|
|
863
|
+
[{
|
|
864
|
+
key: "ids",
|
|
865
|
+
computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
|
|
866
|
+
}]
|
|
867
|
+
)
|
|
868
|
+
expect(result.sql).toContain(`COALESCE(jsonb_agg(_items->>'articleId'), '[]'::jsonb)`)
|
|
869
|
+
expect(result.sql).toContain(`AS "ids"`)
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it("computed relation-collect distinct (pg jsonb_agg DISTINCT)", () => {
|
|
873
|
+
const result = buildWhereSQLQuery(
|
|
874
|
+
pgDialect,
|
|
875
|
+
"id",
|
|
876
|
+
[],
|
|
877
|
+
"users",
|
|
878
|
+
{},
|
|
879
|
+
[{
|
|
880
|
+
key: "ids",
|
|
881
|
+
computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
|
|
882
|
+
}]
|
|
883
|
+
)
|
|
884
|
+
expect(result.sql).toContain(`COALESCE(jsonb_agg(DISTINCT _items->>'articleId'), '[]'::jsonb)`)
|
|
885
|
+
})
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
// --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
|
|
889
|
+
|
|
890
|
+
describe("SQL Store (SQLite integration)", () => {
|
|
891
|
+
const withDb = (fn: (db: Sqlite.Database) => void) => {
|
|
892
|
+
const db = new BetterSqlite(":memory:")
|
|
893
|
+
db.pragma("journal_mode = WAL")
|
|
894
|
+
try {
|
|
895
|
+
fn(db)
|
|
896
|
+
} finally {
|
|
897
|
+
db.close()
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
it("creates table and seeds data", () =>
|
|
902
|
+
withDb((db) => {
|
|
903
|
+
db.exec(
|
|
904
|
+
`CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
|
|
905
|
+
)
|
|
906
|
+
db
|
|
907
|
+
.prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
908
|
+
.run("1", "etag1", JSON.stringify({ name: "Alice", age: 30 }))
|
|
909
|
+
|
|
910
|
+
const rows = db.prepare(`SELECT * FROM "test_items"`).all()
|
|
911
|
+
expect(rows.length).toBe(1)
|
|
912
|
+
expect((rows[0] as any).id).toBe("1")
|
|
321
913
|
}))
|
|
322
914
|
|
|
323
915
|
it("data column should not contain _etag or id", () =>
|
|
@@ -367,7 +959,7 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
367
959
|
)
|
|
368
960
|
const r1 = query(db, q1.sql, q1.params)
|
|
369
961
|
expect(r1.length).toBe(1)
|
|
370
|
-
expect(
|
|
962
|
+
expect(r1[0].id).toBe("1")
|
|
371
963
|
|
|
372
964
|
const q2 = buildWhereSQLQuery(
|
|
373
965
|
sqliteDialect,
|
|
@@ -378,13 +970,13 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
378
970
|
)
|
|
379
971
|
const r2 = query(db, q2.sql, q2.params)
|
|
380
972
|
expect(r2.length).toBe(1)
|
|
381
|
-
expect(
|
|
973
|
+
expect(r2[0].id).toBe("2")
|
|
382
974
|
|
|
383
975
|
// Both queryable by id column
|
|
384
976
|
const q3 = buildWhereSQLQuery(
|
|
385
977
|
sqliteDialect,
|
|
386
978
|
"id",
|
|
387
|
-
[{ t: "where", path: "id", op: "in", value: ["1", "2"]
|
|
979
|
+
[{ t: "where", path: "id", op: "in", value: ["1", "2"] }],
|
|
388
980
|
"test_compat",
|
|
389
981
|
{}
|
|
390
982
|
)
|
|
@@ -410,7 +1002,7 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
410
1002
|
const q1 = buildWhereSQLQuery(
|
|
411
1003
|
sqliteDialect,
|
|
412
1004
|
"id",
|
|
413
|
-
[{ t: "where", path: "age", op: "gt", value: 28
|
|
1005
|
+
[{ t: "where", path: "age", op: "gt", value: 28 }],
|
|
414
1006
|
"test_noid",
|
|
415
1007
|
{}
|
|
416
1008
|
)
|
|
@@ -426,8 +1018,8 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
426
1018
|
)
|
|
427
1019
|
const r2 = query(db, q2.sql, q2.params)
|
|
428
1020
|
expect(r2.length).toBe(1)
|
|
429
|
-
expect(
|
|
430
|
-
expect((JSON.parse(
|
|
1021
|
+
expect(r2[0].id).toBe("2")
|
|
1022
|
+
expect((JSON.parse(r2[0].data) as any).name).toBe("Bob")
|
|
431
1023
|
|
|
432
1024
|
// Order + limit still works
|
|
433
1025
|
const q3 = buildWhereSQLQuery(
|
|
@@ -443,7 +1035,7 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
443
1035
|
)
|
|
444
1036
|
const r3 = query(db, q3.sql, q3.params)
|
|
445
1037
|
expect(r3.length).toBe(2)
|
|
446
|
-
expect((JSON.parse(
|
|
1038
|
+
expect((JSON.parse(r3[0].data) as any).name).toBe("Bob") // youngest first
|
|
447
1039
|
}))
|
|
448
1040
|
|
|
449
1041
|
it("query builder generates valid SQL for SQLite", () =>
|
|
@@ -476,13 +1068,13 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
476
1068
|
{}
|
|
477
1069
|
)
|
|
478
1070
|
expect(query(db, q1.sql, q1.params).length).toBe(1)
|
|
479
|
-
expect((JSON.parse(
|
|
1071
|
+
expect((JSON.parse(query(db, q1.sql, q1.params)[0].data) as any).name).toBe("Alice")
|
|
480
1072
|
|
|
481
1073
|
// Test gt
|
|
482
1074
|
const q2 = buildWhereSQLQuery(
|
|
483
1075
|
sqliteDialect,
|
|
484
1076
|
"id",
|
|
485
|
-
[{ t: "where", path: "age", op: "gt", value: 28
|
|
1077
|
+
[{ t: "where", path: "age", op: "gt", value: 28 }],
|
|
486
1078
|
"test_people",
|
|
487
1079
|
{}
|
|
488
1080
|
)
|
|
@@ -507,20 +1099,20 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
507
1099
|
"id",
|
|
508
1100
|
[
|
|
509
1101
|
{ t: "where", path: "name", op: "eq", value: "Alice" },
|
|
510
|
-
{ t: "and", path: "age", op: "gt", value: 25
|
|
1102
|
+
{ t: "and", path: "age", op: "gt", value: 25 }
|
|
511
1103
|
],
|
|
512
1104
|
"test_people",
|
|
513
1105
|
{}
|
|
514
1106
|
)
|
|
515
1107
|
const r4 = query(db, q4.sql, q4.params)
|
|
516
1108
|
expect(r4.length).toBe(1)
|
|
517
|
-
expect((JSON.parse(
|
|
1109
|
+
expect((JSON.parse(r4[0].data) as any).name).toBe("Alice")
|
|
518
1110
|
|
|
519
1111
|
// Test IN
|
|
520
1112
|
const q5 = buildWhereSQLQuery(
|
|
521
1113
|
sqliteDialect,
|
|
522
1114
|
"id",
|
|
523
|
-
[{ t: "where", path: "id", op: "in", value: ["1", "3"]
|
|
1115
|
+
[{ t: "where", path: "id", op: "in", value: ["1", "3"] }],
|
|
524
1116
|
"test_people",
|
|
525
1117
|
{}
|
|
526
1118
|
)
|
|
@@ -546,7 +1138,7 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
546
1138
|
)
|
|
547
1139
|
const r7 = query(db, q7.sql, q7.params)
|
|
548
1140
|
expect(r7.length).toBe(1)
|
|
549
|
-
expect((JSON.parse(
|
|
1141
|
+
expect((JSON.parse(r7[0].data) as any).name).toBe("Alice")
|
|
550
1142
|
|
|
551
1143
|
// Test includes (array)
|
|
552
1144
|
const q8 = buildWhereSQLQuery(
|
|
@@ -567,7 +1159,7 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
567
1159
|
{
|
|
568
1160
|
t: "or-scope",
|
|
569
1161
|
result: [
|
|
570
|
-
{ t: "where", path: "age", op: "gt", value: 30
|
|
1162
|
+
{ t: "where", path: "age", op: "gt", value: 30 },
|
|
571
1163
|
{ t: "and", path: "name", op: "contains", value: "ar" }
|
|
572
1164
|
],
|
|
573
1165
|
relation: "some"
|
|
@@ -592,7 +1184,115 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
592
1184
|
)
|
|
593
1185
|
const r10 = query(db, q10.sql, q10.params)
|
|
594
1186
|
expect(r10.length).toBe(2)
|
|
595
|
-
expect((JSON.parse(
|
|
1187
|
+
expect((JSON.parse(r10[0].data) as any).name).toBe("Charlie") // oldest first
|
|
1188
|
+
}))
|
|
1189
|
+
|
|
1190
|
+
it("computed relation-every / distinct-count / sum / collect run on SQLite", () =>
|
|
1191
|
+
withDb((db) => {
|
|
1192
|
+
db.exec(`CREATE TABLE "test_orders" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1193
|
+
const orders = [
|
|
1194
|
+
{
|
|
1195
|
+
id: "o1",
|
|
1196
|
+
items: [
|
|
1197
|
+
{ rowId: "r1", articleId: "A", weight: 1.5, state: { _tag: "picked" } },
|
|
1198
|
+
{ rowId: "r2", articleId: "A", weight: 2.5, state: { _tag: "picked" } },
|
|
1199
|
+
{ rowId: "r2", articleId: "B", weight: 0.25, state: { _tag: "picking" } }
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: "o2",
|
|
1204
|
+
items: [
|
|
1205
|
+
{ rowId: "r9", articleId: "Z", weight: 10, state: { _tag: "packed" } }
|
|
1206
|
+
]
|
|
1207
|
+
}
|
|
1208
|
+
]
|
|
1209
|
+
const insert = db.prepare(`INSERT INTO "test_orders" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1210
|
+
for (const o of orders) {
|
|
1211
|
+
const { id, ...data } = o
|
|
1212
|
+
insert.run(id, `etag_${id}`, JSON.stringify(data))
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const q = buildWhereSQLQuery(
|
|
1216
|
+
sqliteDialect,
|
|
1217
|
+
"id",
|
|
1218
|
+
[],
|
|
1219
|
+
"test_orders",
|
|
1220
|
+
{},
|
|
1221
|
+
[
|
|
1222
|
+
{
|
|
1223
|
+
key: "allPicked",
|
|
1224
|
+
computed: {
|
|
1225
|
+
_tag: "relation-every",
|
|
1226
|
+
path: "items",
|
|
1227
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
key: "positionCount",
|
|
1232
|
+
computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
key: "totalWeight",
|
|
1236
|
+
computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
key: "articleIds",
|
|
1240
|
+
computed: {
|
|
1241
|
+
_tag: "relation-collect",
|
|
1242
|
+
path: "items",
|
|
1243
|
+
field: "articleId",
|
|
1244
|
+
distinct: true,
|
|
1245
|
+
filter: []
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
] as any,
|
|
1249
|
+
[{ key: "id", direction: "ASC" }] as any
|
|
1250
|
+
)
|
|
1251
|
+
const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
|
|
1252
|
+
expect(rows.length).toBe(2)
|
|
1253
|
+
// o1: not all picked (one is "picking")
|
|
1254
|
+
expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(false)
|
|
1255
|
+
expect(rows[0]!["positionCount"]).toBe(2)
|
|
1256
|
+
expect(rows[0]!["totalWeight"]).toBeCloseTo(4.25)
|
|
1257
|
+
expect((JSON.parse(rows[0]!["articleIds"] as string) as string[]).sort()).toEqual(["A", "B"])
|
|
1258
|
+
// o2: all packed (so not "picked"), allPicked = !exists(NOT picked) = false
|
|
1259
|
+
expect(JSON.parse(rows[1]!["allPicked"] as string)).toBe(false)
|
|
1260
|
+
expect(rows[1]!["positionCount"]).toBe(1)
|
|
1261
|
+
expect(rows[1]!["totalWeight"]).toBeCloseTo(10)
|
|
1262
|
+
expect(JSON.parse(rows[1]!["articleIds"] as string)).toEqual(["Z"])
|
|
1263
|
+
}))
|
|
1264
|
+
|
|
1265
|
+
it("computed relation-every is true when all items match filter", () =>
|
|
1266
|
+
withDb((db) => {
|
|
1267
|
+
db.exec(`CREATE TABLE "test_every" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1268
|
+
db.prepare(`INSERT INTO "test_every" (id, _etag, data) VALUES (?, ?, ?)`).run(
|
|
1269
|
+
"1",
|
|
1270
|
+
"e",
|
|
1271
|
+
JSON.stringify({
|
|
1272
|
+
items: [
|
|
1273
|
+
{ state: { _tag: "picked" } },
|
|
1274
|
+
{ state: { _tag: "picked" } }
|
|
1275
|
+
]
|
|
1276
|
+
})
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
const q = buildWhereSQLQuery(
|
|
1280
|
+
sqliteDialect,
|
|
1281
|
+
"id",
|
|
1282
|
+
[],
|
|
1283
|
+
"test_every",
|
|
1284
|
+
{},
|
|
1285
|
+
[{
|
|
1286
|
+
key: "allPicked",
|
|
1287
|
+
computed: {
|
|
1288
|
+
_tag: "relation-every",
|
|
1289
|
+
path: "items",
|
|
1290
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
|
|
1291
|
+
}
|
|
1292
|
+
}]
|
|
1293
|
+
)
|
|
1294
|
+
const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
|
|
1295
|
+
expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(true)
|
|
596
1296
|
}))
|
|
597
1297
|
|
|
598
1298
|
it("namespace param is in correct position for SQLite positional placeholders", () =>
|
|
@@ -626,9 +1326,454 @@ describe("SQL Store (SQLite integration)", () => {
|
|
|
626
1326
|
const results = query(db, nsSql, params)
|
|
627
1327
|
// Should only get Alice and Bob (primary namespace), not Charlie (other namespace)
|
|
628
1328
|
expect(results.length).toBe(2)
|
|
629
|
-
const names = results.map((r) => (JSON.parse(
|
|
1329
|
+
const names = results.map((r) => (JSON.parse(r.data) as any).name).sort()
|
|
630
1330
|
expect(names).toEqual(["Alice", "Bob"])
|
|
631
1331
|
}))
|
|
1332
|
+
|
|
1333
|
+
it("aggregate: agg-count-when with GROUP BY on aliased path field (SQLite)", () =>
|
|
1334
|
+
withDb((db) => {
|
|
1335
|
+
db.exec(`CREATE TABLE "test_agg" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1336
|
+
const rows = [
|
|
1337
|
+
{ city: "NYC", state: "active" },
|
|
1338
|
+
{ city: "NYC", state: "inactive" },
|
|
1339
|
+
{ city: "NYC", state: "active" },
|
|
1340
|
+
{ city: "LA", state: "active" }
|
|
1341
|
+
]
|
|
1342
|
+
const insert = db.prepare(`INSERT INTO "test_agg" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1343
|
+
rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
|
|
1344
|
+
|
|
1345
|
+
const q = buildWhereSQLQuery(
|
|
1346
|
+
sqliteDialect,
|
|
1347
|
+
"id",
|
|
1348
|
+
[],
|
|
1349
|
+
"test_agg",
|
|
1350
|
+
{},
|
|
1351
|
+
[
|
|
1352
|
+
{ key: "city", path: "city" },
|
|
1353
|
+
{
|
|
1354
|
+
key: "activeCount",
|
|
1355
|
+
aggregate: {
|
|
1356
|
+
_tag: "agg-count-when",
|
|
1357
|
+
filter: [{ t: "where", path: "state", op: "eq", value: "active" }]
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
{ key: "total", aggregate: { _tag: "agg-count" } }
|
|
1361
|
+
] as any,
|
|
1362
|
+
[{ key: "city", direction: "ASC" }] as any
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
expect(q.sql).toContain("GROUP BY")
|
|
1366
|
+
expect(q.sql).toContain("COUNT(CASE WHEN")
|
|
1367
|
+
|
|
1368
|
+
const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
|
|
1369
|
+
expect(result.length).toBe(2)
|
|
1370
|
+
|
|
1371
|
+
const la = result.find((r) => r["city"] === "LA")!
|
|
1372
|
+
expect(la["activeCount"]).toBe(1)
|
|
1373
|
+
expect(la["total"]).toBe(1)
|
|
1374
|
+
|
|
1375
|
+
const nyc = result.find((r) => r["city"] === "NYC")!
|
|
1376
|
+
expect(nyc["activeCount"]).toBe(2)
|
|
1377
|
+
expect(nyc["total"]).toBe(3)
|
|
1378
|
+
}))
|
|
1379
|
+
|
|
1380
|
+
it("aggregate: agg-sum and agg-min / agg-max (SQLite)", () =>
|
|
1381
|
+
withDb((db) => {
|
|
1382
|
+
db.exec(`CREATE TABLE "test_agg2" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1383
|
+
const rows = [
|
|
1384
|
+
{ dept: "eng", salary: 100 },
|
|
1385
|
+
{ dept: "eng", salary: 200 },
|
|
1386
|
+
{ dept: "hr", salary: 50 }
|
|
1387
|
+
]
|
|
1388
|
+
const insert = db.prepare(`INSERT INTO "test_agg2" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1389
|
+
rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
|
|
1390
|
+
|
|
1391
|
+
const q = buildWhereSQLQuery(
|
|
1392
|
+
sqliteDialect,
|
|
1393
|
+
"id",
|
|
1394
|
+
[],
|
|
1395
|
+
"test_agg2",
|
|
1396
|
+
{},
|
|
1397
|
+
[
|
|
1398
|
+
{ key: "dept", path: "dept" },
|
|
1399
|
+
{ key: "totalSalary", aggregate: { _tag: "agg-sum", field: "salary" } },
|
|
1400
|
+
{ key: "minSalary", aggregate: { _tag: "agg-min", field: "salary" } },
|
|
1401
|
+
{ key: "maxSalary", aggregate: { _tag: "agg-max", field: "salary" } }
|
|
1402
|
+
] as any,
|
|
1403
|
+
[{ key: "dept", direction: "ASC" }] as any
|
|
1404
|
+
)
|
|
1405
|
+
|
|
1406
|
+
const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
|
|
1407
|
+
expect(result.length).toBe(2)
|
|
1408
|
+
|
|
1409
|
+
const eng = result.find((r) => r["dept"] === "eng")!
|
|
1410
|
+
expect(eng["totalSalary"]).toBeCloseTo(300)
|
|
1411
|
+
expect(eng["minSalary"]).toBeCloseTo(100)
|
|
1412
|
+
expect(eng["maxSalary"]).toBeCloseTo(200)
|
|
1413
|
+
|
|
1414
|
+
const hr = result.find((r) => r["dept"] === "hr")!
|
|
1415
|
+
expect(hr["totalSalary"]).toBeCloseTo(50)
|
|
1416
|
+
}))
|
|
1417
|
+
|
|
1418
|
+
it("aggregate: nested path field grouping (SQLite)", () =>
|
|
1419
|
+
withDb((db) => {
|
|
1420
|
+
db.exec(`CREATE TABLE "test_agg3" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1421
|
+
const rows = [
|
|
1422
|
+
{ address: { city: "NYC" }, status: "open" },
|
|
1423
|
+
{ address: { city: "NYC" }, status: "closed" },
|
|
1424
|
+
{ address: { city: "LA" }, status: "open" }
|
|
1425
|
+
]
|
|
1426
|
+
const insert = db.prepare(`INSERT INTO "test_agg3" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1427
|
+
rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
|
|
1428
|
+
|
|
1429
|
+
const q = buildWhereSQLQuery(
|
|
1430
|
+
sqliteDialect,
|
|
1431
|
+
"id",
|
|
1432
|
+
[],
|
|
1433
|
+
"test_agg3",
|
|
1434
|
+
{},
|
|
1435
|
+
[
|
|
1436
|
+
{ key: "city", path: "address.city" },
|
|
1437
|
+
{ key: "total", aggregate: { _tag: "agg-count" } }
|
|
1438
|
+
] as any,
|
|
1439
|
+
[{ key: "city", direction: "ASC" }] as any
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
expect(q.sql).toContain("GROUP BY")
|
|
1443
|
+
|
|
1444
|
+
const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
|
|
1445
|
+
expect(result.length).toBe(2)
|
|
1446
|
+
const la = result.find((r) => r["city"] === "LA")!
|
|
1447
|
+
expect(la["total"]).toBe(1)
|
|
1448
|
+
const nyc = result.find((r) => r["city"] === "NYC")!
|
|
1449
|
+
expect(nyc["total"]).toBe(2)
|
|
1450
|
+
}))
|
|
1451
|
+
})
|
|
1452
|
+
|
|
1453
|
+
// --- boolean WHERE clause tests (regression: where("x", true) must work per dialect) ---
|
|
1454
|
+
|
|
1455
|
+
describe("boolean WHERE clauses — query builder", () => {
|
|
1456
|
+
it("sqlite: eq true serializes bool → 1 integer", () => {
|
|
1457
|
+
const result = buildWhereSQLQuery(
|
|
1458
|
+
sqliteDialect,
|
|
1459
|
+
"id",
|
|
1460
|
+
[{ t: "where", path: "flag", op: "eq", value: true }],
|
|
1461
|
+
"t",
|
|
1462
|
+
{}
|
|
1463
|
+
)
|
|
1464
|
+
expect(result.sql).toContain("json_extract(data, '$.flag') = ?")
|
|
1465
|
+
expect(result.params).toEqual([1])
|
|
1466
|
+
})
|
|
1467
|
+
|
|
1468
|
+
it("sqlite: eq false serializes bool → 0 integer", () => {
|
|
1469
|
+
const result = buildWhereSQLQuery(
|
|
1470
|
+
sqliteDialect,
|
|
1471
|
+
"id",
|
|
1472
|
+
[{ t: "where", path: "flag", op: "eq", value: false }],
|
|
1473
|
+
"t",
|
|
1474
|
+
{}
|
|
1475
|
+
)
|
|
1476
|
+
expect(result.params).toEqual([0])
|
|
1477
|
+
})
|
|
1478
|
+
|
|
1479
|
+
it("sqlite: neq/in also serialize booleans", () => {
|
|
1480
|
+
const r1 = buildWhereSQLQuery(
|
|
1481
|
+
sqliteDialect,
|
|
1482
|
+
"id",
|
|
1483
|
+
[{ t: "where", path: "flag", op: "neq", value: true }],
|
|
1484
|
+
"t",
|
|
1485
|
+
{}
|
|
1486
|
+
)
|
|
1487
|
+
expect(r1.params).toEqual([1])
|
|
1488
|
+
|
|
1489
|
+
const r2 = buildWhereSQLQuery(
|
|
1490
|
+
sqliteDialect,
|
|
1491
|
+
"id",
|
|
1492
|
+
[{ t: "where", path: "flag", op: "in", value: [true, false] }],
|
|
1493
|
+
"t",
|
|
1494
|
+
{}
|
|
1495
|
+
)
|
|
1496
|
+
expect(r2.params).toEqual([1, 0])
|
|
1497
|
+
})
|
|
1498
|
+
|
|
1499
|
+
it("pg: eq true serializes bool → 'true' text (matches ->> output)", () => {
|
|
1500
|
+
const result = buildWhereSQLQuery(
|
|
1501
|
+
pgDialect,
|
|
1502
|
+
"id",
|
|
1503
|
+
[{ t: "where", path: "flag", op: "eq", value: true }],
|
|
1504
|
+
"t",
|
|
1505
|
+
{}
|
|
1506
|
+
)
|
|
1507
|
+
expect(result.sql).toContain("data->>'flag' = $1")
|
|
1508
|
+
expect(result.params).toEqual(["true"])
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
it("pg: eq false serializes bool → 'false' text", () => {
|
|
1512
|
+
const result = buildWhereSQLQuery(
|
|
1513
|
+
pgDialect,
|
|
1514
|
+
"id",
|
|
1515
|
+
[{ t: "where", path: "flag", op: "eq", value: false }],
|
|
1516
|
+
"t",
|
|
1517
|
+
{}
|
|
1518
|
+
)
|
|
1519
|
+
expect(result.params).toEqual(["false"])
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
it("pg: in with booleans serializes as text list", () => {
|
|
1523
|
+
const result = buildWhereSQLQuery(
|
|
1524
|
+
pgDialect,
|
|
1525
|
+
"id",
|
|
1526
|
+
[{ t: "where", path: "flag", op: "in", value: [true, false] }],
|
|
1527
|
+
"t",
|
|
1528
|
+
{}
|
|
1529
|
+
)
|
|
1530
|
+
expect(result.params).toEqual(["true", "false"])
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
it("non-boolean scalars pass through unchanged (sqlite)", () => {
|
|
1534
|
+
const result = buildWhereSQLQuery(
|
|
1535
|
+
sqliteDialect,
|
|
1536
|
+
"id",
|
|
1537
|
+
[{ t: "where", path: "name", op: "eq", value: "Alice" }],
|
|
1538
|
+
"t",
|
|
1539
|
+
{}
|
|
1540
|
+
)
|
|
1541
|
+
expect(result.params).toEqual(["Alice"])
|
|
1542
|
+
})
|
|
1543
|
+
|
|
1544
|
+
it("non-boolean scalars pass through unchanged (pg)", () => {
|
|
1545
|
+
const result = buildWhereSQLQuery(
|
|
1546
|
+
pgDialect,
|
|
1547
|
+
"id",
|
|
1548
|
+
[{ t: "where", path: "age", op: "gt", value: 18 }],
|
|
1549
|
+
"t",
|
|
1550
|
+
{}
|
|
1551
|
+
)
|
|
1552
|
+
expect(result.params).toEqual([18])
|
|
1553
|
+
})
|
|
1554
|
+
})
|
|
1555
|
+
|
|
1556
|
+
describe("boolean WHERE clauses — SQLite integration (end-to-end)", () => {
|
|
1557
|
+
const withDb = (fn: (db: Sqlite.Database) => void) => {
|
|
1558
|
+
const db = new BetterSqlite(":memory:")
|
|
1559
|
+
try {
|
|
1560
|
+
fn(db)
|
|
1561
|
+
} finally {
|
|
1562
|
+
db.close()
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
it("where flag = true matches only true rows", () =>
|
|
1567
|
+
withDb((db) => {
|
|
1568
|
+
db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1569
|
+
db
|
|
1570
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1571
|
+
.run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
|
|
1572
|
+
db
|
|
1573
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1574
|
+
.run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
|
|
1575
|
+
|
|
1576
|
+
const q = buildWhereSQLQuery(
|
|
1577
|
+
sqliteDialect,
|
|
1578
|
+
"id",
|
|
1579
|
+
[{ t: "where", path: "flag", op: "eq", value: true }],
|
|
1580
|
+
"t",
|
|
1581
|
+
{}
|
|
1582
|
+
)
|
|
1583
|
+
const rows = query(db, q.sql, q.params)
|
|
1584
|
+
expect(rows.length).toBe(1)
|
|
1585
|
+
expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
|
|
1586
|
+
}))
|
|
1587
|
+
|
|
1588
|
+
it("where flag = false matches only false rows", () =>
|
|
1589
|
+
withDb((db) => {
|
|
1590
|
+
db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1591
|
+
db
|
|
1592
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1593
|
+
.run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
|
|
1594
|
+
db
|
|
1595
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1596
|
+
.run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
|
|
1597
|
+
|
|
1598
|
+
const q = buildWhereSQLQuery(
|
|
1599
|
+
sqliteDialect,
|
|
1600
|
+
"id",
|
|
1601
|
+
[{ t: "where", path: "flag", op: "eq", value: false }],
|
|
1602
|
+
"t",
|
|
1603
|
+
{}
|
|
1604
|
+
)
|
|
1605
|
+
const rows = query(db, q.sql, q.params)
|
|
1606
|
+
expect(rows.length).toBe(1)
|
|
1607
|
+
expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
|
|
1608
|
+
}))
|
|
1609
|
+
|
|
1610
|
+
it("where nested boolean path works", () =>
|
|
1611
|
+
withDb((db) => {
|
|
1612
|
+
db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1613
|
+
db
|
|
1614
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1615
|
+
.run("1", "e", JSON.stringify({ meta: { active: true }, name: "Alice" }))
|
|
1616
|
+
db
|
|
1617
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1618
|
+
.run("2", "e", JSON.stringify({ meta: { active: false }, name: "Bob" }))
|
|
1619
|
+
|
|
1620
|
+
const q = buildWhereSQLQuery(
|
|
1621
|
+
sqliteDialect,
|
|
1622
|
+
"id",
|
|
1623
|
+
[{ t: "where", path: "meta.active", op: "eq", value: true }],
|
|
1624
|
+
"t",
|
|
1625
|
+
{}
|
|
1626
|
+
)
|
|
1627
|
+
const rows = query(db, q.sql, q.params)
|
|
1628
|
+
expect(rows.length).toBe(1)
|
|
1629
|
+
expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
|
|
1630
|
+
}))
|
|
1631
|
+
|
|
1632
|
+
it("where hasKey / hasValue / hasKeyValue match Map tuple JSON", () =>
|
|
1633
|
+
withDb((db) => {
|
|
1634
|
+
db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1635
|
+
db
|
|
1636
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1637
|
+
.run("1", "e", JSON.stringify({ meta: [["n", 1], ["x", 2]] }))
|
|
1638
|
+
db
|
|
1639
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1640
|
+
.run("2", "e", JSON.stringify({ meta: [["n", 9]] }))
|
|
1641
|
+
|
|
1642
|
+
const byKey = buildWhereSQLQuery(
|
|
1643
|
+
sqliteDialect,
|
|
1644
|
+
"id",
|
|
1645
|
+
[{ t: "where", path: "meta", op: "hasKey", value: "x" }],
|
|
1646
|
+
"t",
|
|
1647
|
+
{}
|
|
1648
|
+
)
|
|
1649
|
+
expect(query(db, byKey.sql, byKey.params).map((r) => r.id)).toEqual(["1"])
|
|
1650
|
+
|
|
1651
|
+
const byValue = buildWhereSQLQuery(
|
|
1652
|
+
sqliteDialect,
|
|
1653
|
+
"id",
|
|
1654
|
+
[{ t: "where", path: "meta", op: "hasValue", value: 9 }],
|
|
1655
|
+
"t",
|
|
1656
|
+
{}
|
|
1657
|
+
)
|
|
1658
|
+
expect(query(db, byValue.sql, byValue.params).map((r) => r.id)).toEqual(["2"])
|
|
1659
|
+
|
|
1660
|
+
const byPair = buildWhereSQLQuery(
|
|
1661
|
+
sqliteDialect,
|
|
1662
|
+
"id",
|
|
1663
|
+
[{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
|
|
1664
|
+
"t",
|
|
1665
|
+
{}
|
|
1666
|
+
)
|
|
1667
|
+
expect(query(db, byPair.sql, byPair.params).map((r) => r.id)).toEqual(["1"])
|
|
1668
|
+
}))
|
|
1669
|
+
|
|
1670
|
+
it("where neq boolean works", () =>
|
|
1671
|
+
withDb((db) => {
|
|
1672
|
+
db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
|
|
1673
|
+
db
|
|
1674
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1675
|
+
.run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
|
|
1676
|
+
db
|
|
1677
|
+
.prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
|
|
1678
|
+
.run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
|
|
1679
|
+
|
|
1680
|
+
const q = buildWhereSQLQuery(
|
|
1681
|
+
sqliteDialect,
|
|
1682
|
+
"id",
|
|
1683
|
+
[{ t: "where", path: "flag", op: "neq", value: true }],
|
|
1684
|
+
"t",
|
|
1685
|
+
{}
|
|
1686
|
+
)
|
|
1687
|
+
const rows = query(db, q.sql, q.params)
|
|
1688
|
+
expect(rows.length).toBe(1)
|
|
1689
|
+
expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
|
|
1690
|
+
}))
|
|
1691
|
+
})
|
|
1692
|
+
|
|
1693
|
+
// --- jsonExtractJson dialect tests (regression: booleans must survive round-trip) ---
|
|
1694
|
+
|
|
1695
|
+
describe("sqliteDialect.jsonExtractJson boolean round-trip", () => {
|
|
1696
|
+
const withDb = (fn: (db: Sqlite.Database) => void) => {
|
|
1697
|
+
const db = new BetterSqlite(":memory:")
|
|
1698
|
+
try {
|
|
1699
|
+
fn(db)
|
|
1700
|
+
} finally {
|
|
1701
|
+
db.close()
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
it("SQL uses json_type CASE to preserve booleans", () => {
|
|
1706
|
+
const sql = sqliteDialect.jsonExtractJson("flag")
|
|
1707
|
+
expect(sql).toContain("json_type(data, '$.flag')")
|
|
1708
|
+
expect(sql).toContain("'true'")
|
|
1709
|
+
expect(sql).toContain("'false'")
|
|
1710
|
+
expect(sql).toContain("json_quote(json_extract(data, '$.flag'))")
|
|
1711
|
+
})
|
|
1712
|
+
|
|
1713
|
+
it("boolean false round-trips as false (not 0)", () =>
|
|
1714
|
+
withDb((db) => {
|
|
1715
|
+
db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
|
|
1716
|
+
db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: false }))
|
|
1717
|
+
|
|
1718
|
+
const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
|
|
1719
|
+
const parsed = JSON.parse(row.v)
|
|
1720
|
+
expect(parsed).toBe(false)
|
|
1721
|
+
expect(typeof parsed).toBe("boolean")
|
|
1722
|
+
}))
|
|
1723
|
+
|
|
1724
|
+
it("boolean true round-trips as true (not 1)", () =>
|
|
1725
|
+
withDb((db) => {
|
|
1726
|
+
db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
|
|
1727
|
+
db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: true }))
|
|
1728
|
+
|
|
1729
|
+
const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
|
|
1730
|
+
const parsed = JSON.parse(row.v)
|
|
1731
|
+
expect(parsed).toBe(true)
|
|
1732
|
+
expect(typeof parsed).toBe("boolean")
|
|
1733
|
+
}))
|
|
1734
|
+
|
|
1735
|
+
it("nested boolean round-trips correctly", () =>
|
|
1736
|
+
withDb((db) => {
|
|
1737
|
+
db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
|
|
1738
|
+
db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
|
|
1739
|
+
JSON.stringify({ nested: { flag: false } })
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
const row = db
|
|
1743
|
+
.prepare(`SELECT ${sqliteDialect.jsonExtractJson("nested.flag")} AS v FROM "t"`)
|
|
1744
|
+
.get() as any
|
|
1745
|
+
const parsed = JSON.parse(row.v)
|
|
1746
|
+
expect(parsed).toBe(false)
|
|
1747
|
+
expect(typeof parsed).toBe("boolean")
|
|
1748
|
+
}))
|
|
1749
|
+
|
|
1750
|
+
it("numbers 0 and 1 still parse as numbers (not booleans)", () =>
|
|
1751
|
+
withDb((db) => {
|
|
1752
|
+
db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
|
|
1753
|
+
db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ zero: 0, one: 1 }))
|
|
1754
|
+
|
|
1755
|
+
const zRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("zero")} AS v FROM "t"`).get() as any
|
|
1756
|
+
const oRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("one")} AS v FROM "t"`).get() as any
|
|
1757
|
+
expect(JSON.parse(zRow.v)).toBe(0)
|
|
1758
|
+
expect(typeof JSON.parse(zRow.v)).toBe("number")
|
|
1759
|
+
expect(JSON.parse(oRow.v)).toBe(1)
|
|
1760
|
+
expect(typeof JSON.parse(oRow.v)).toBe("number")
|
|
1761
|
+
}))
|
|
1762
|
+
|
|
1763
|
+
it("strings, arrays, and null still round-trip correctly", () =>
|
|
1764
|
+
withDb((db) => {
|
|
1765
|
+
db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
|
|
1766
|
+
db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
|
|
1767
|
+
JSON.stringify({ s: "hello", arr: [1, 2, 3], nil: null })
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
const sRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("s")} AS v FROM "t"`).get() as any
|
|
1771
|
+
const aRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("arr")} AS v FROM "t"`).get() as any
|
|
1772
|
+
const nRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("nil")} AS v FROM "t"`).get() as any
|
|
1773
|
+
expect(JSON.parse(sRow.v)).toBe("hello")
|
|
1774
|
+
expect(JSON.parse(aRow.v)).toEqual([1, 2, 3])
|
|
1775
|
+
expect(JSON.parse(nRow.v)).toBe(null)
|
|
1776
|
+
}))
|
|
632
1777
|
})
|
|
633
1778
|
|
|
634
1779
|
// --- toRow stripping and parseRow reconstruction tests ---
|
|
@@ -638,7 +1783,7 @@ describe("toRow strips _etag and id from data", () => {
|
|
|
638
1783
|
const toRow = <IdKey extends PropertyKey>(e: any, idKey: IdKey) => {
|
|
639
1784
|
const newE = makeETag(e)
|
|
640
1785
|
const id = newE[idKey] as string
|
|
641
|
-
const { _etag, [idKey]: _id, ...rest } = newE
|
|
1786
|
+
const { _etag, [idKey]: _id, ...rest } = newE
|
|
642
1787
|
const data = JSON.stringify(rest)
|
|
643
1788
|
return { id, _etag: newE._etag!, data, item: newE }
|
|
644
1789
|
}
|
|
@@ -773,4 +1918,14 @@ describe("parseRow reconstructs full object from row", () => {
|
|
|
773
1918
|
expect(reconstructed.tags).toEqual(["admin"])
|
|
774
1919
|
expect(reconstructed._etag).toBe(newE._etag)
|
|
775
1920
|
})
|
|
1921
|
+
|
|
1922
|
+
it("lowers Date defaultValues to ISO before merge", () => {
|
|
1923
|
+
const result: any = parseRow(
|
|
1924
|
+
{ id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice" }) },
|
|
1925
|
+
"id",
|
|
1926
|
+
{ at: new Date("2024-06-01T00:00:00.000Z") }
|
|
1927
|
+
)
|
|
1928
|
+
expect(result.at).toBe("2024-06-01T00:00:00.000Z")
|
|
1929
|
+
expect(result.name).toBe("Alice")
|
|
1930
|
+
})
|
|
776
1931
|
})
|