@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.220
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 +1640 -0
- package/_check.sh +1 -1
- package/dist/CUPS.d.ts +7 -7
- package/dist/CUPS.d.ts.map +1 -1
- package/dist/CUPS.js +10 -12
- package/dist/Emailer/Sendgrid.d.ts +14 -14
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +16 -15
- package/dist/Emailer/fake.d.ts +1 -1
- package/dist/Emailer/service.d.ts +10 -4
- package/dist/Emailer/service.d.ts.map +1 -1
- package/dist/Emailer/service.js +3 -3
- package/dist/Emailer.d.ts +1 -1
- package/dist/MainFiberSet.d.ts +9 -9
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +3 -3
- package/dist/Model/Repository/Registry.d.ts +20 -0
- package/dist/Model/Repository/Registry.d.ts.map +1 -0
- package/dist/Model/Repository/Registry.js +17 -0
- package/dist/Model/Repository/ext.d.ts +33 -15
- package/dist/Model/Repository/ext.d.ts.map +1 -1
- package/dist/Model/Repository/ext.js +54 -2
- package/dist/Model/Repository/internal/internal.d.ts +6 -6
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +103 -51
- package/dist/Model/Repository/legacy.d.ts +1 -1
- package/dist/Model/Repository/makeRepo.d.ts +7 -6
- package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
- package/dist/Model/Repository/makeRepo.js +5 -1
- package/dist/Model/Repository/service.d.ts +28 -23
- package/dist/Model/Repository/service.d.ts.map +1 -1
- package/dist/Model/Repository/validation.d.ts +46 -17
- package/dist/Model/Repository/validation.d.ts.map +1 -1
- package/dist/Model/Repository/validation.js +5 -5
- package/dist/Model/Repository.d.ts +2 -1
- package/dist/Model/Repository.d.ts.map +1 -1
- package/dist/Model/Repository.js +2 -1
- package/dist/Model/dsl.d.ts +4 -4
- package/dist/Model/dsl.d.ts.map +1 -1
- package/dist/Model/filter/filterApi.d.ts +5 -5
- package/dist/Model/filter/filterApi.d.ts.map +1 -1
- package/dist/Model/filter/types/errors.d.ts +1 -1
- package/dist/Model/filter/types/fields.d.ts +1 -1
- package/dist/Model/filter/types/path/common.d.ts +1 -1
- package/dist/Model/filter/types/path/eager.d.ts +1 -1
- package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
- package/dist/Model/filter/types/path/eager.js +1 -1
- package/dist/Model/filter/types/path/index.d.ts +1 -1
- package/dist/Model/filter/types/utils.d.ts +1 -1
- package/dist/Model/filter/types/validator.d.ts +1 -1
- package/dist/Model/filter/types.d.ts +1 -1
- package/dist/Model/query/dsl.d.ts +139 -16
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +187 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +76 -7
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +122 -6
- package/dist/Model/query.d.ts +1 -1
- package/dist/Model.d.ts +2 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +2 -1
- package/dist/QueueMaker/SQLQueue.d.ts +5 -7
- package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
- package/dist/QueueMaker/SQLQueue.js +130 -116
- package/dist/QueueMaker/errors.d.ts +2 -2
- package/dist/QueueMaker/errors.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.d.ts +7 -4
- package/dist/QueueMaker/memQueue.d.ts.map +1 -1
- package/dist/QueueMaker/memQueue.js +75 -63
- package/dist/QueueMaker/sbqueue.d.ts +6 -3
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +52 -53
- package/dist/QueueMaker/service.d.ts +1 -1
- package/dist/RequestContext.d.ts +74 -35
- package/dist/RequestContext.d.ts.map +1 -1
- package/dist/RequestContext.js +13 -14
- package/dist/RequestFiberSet.d.ts +7 -7
- package/dist/RequestFiberSet.d.ts.map +1 -1
- package/dist/RequestFiberSet.js +3 -3
- package/dist/Store/ContextMapContainer.d.ts +19 -3
- package/dist/Store/ContextMapContainer.d.ts.map +1 -1
- package/dist/Store/ContextMapContainer.js +13 -3
- package/dist/Store/Cosmos/query.d.ts +5 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +113 -34
- package/dist/Store/Cosmos.d.ts +1 -1
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +335 -243
- package/dist/Store/Disk.d.ts +2 -2
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +72 -35
- package/dist/Store/Memory.d.ts +6 -4
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +242 -58
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +231 -0
- package/dist/Store/SQL/query.d.ts +42 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +479 -0
- package/dist/Store/SQL.d.ts +20 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +446 -0
- package/dist/Store/codeFilter.d.ts +1 -1
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +4 -2
- package/dist/Store/index.d.ts +5 -2
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +15 -3
- package/dist/Store/service.d.ts +22 -8
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +24 -6
- package/dist/Store/utils.d.ts +1 -1
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +3 -4
- package/dist/Store.d.ts +1 -1
- package/dist/adapters/SQL/Model.d.ts +31 -42
- package/dist/adapters/SQL/Model.d.ts.map +1 -1
- package/dist/adapters/SQL/Model.js +29 -38
- package/dist/adapters/SQL.d.ts +1 -1
- package/dist/adapters/ServiceBus.d.ts +11 -11
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +25 -21
- package/dist/adapters/cosmos-client.d.ts +3 -3
- package/dist/adapters/cosmos-client.d.ts.map +1 -1
- package/dist/adapters/cosmos-client.js +3 -3
- package/dist/adapters/index.d.ts +8 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -2
- package/dist/adapters/logger.d.ts +1 -1
- package/dist/adapters/logger.d.ts.map +1 -1
- package/dist/adapters/memQueue.d.ts +3 -3
- package/dist/adapters/memQueue.d.ts.map +1 -1
- package/dist/adapters/memQueue.js +3 -3
- package/dist/adapters/mongo-client.d.ts +3 -3
- package/dist/adapters/mongo-client.d.ts.map +1 -1
- package/dist/adapters/mongo-client.js +3 -3
- package/dist/adapters/redis-client.d.ts +3 -3
- package/dist/adapters/redis-client.d.ts.map +1 -1
- package/dist/adapters/redis-client.js +3 -3
- package/dist/api/ContextProvider.d.ts +8 -8
- package/dist/api/ContextProvider.d.ts.map +1 -1
- package/dist/api/ContextProvider.js +6 -6
- package/dist/api/codec.d.ts +1 -1
- package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
- package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
- package/dist/api/internal/RequestContextMiddleware.js +9 -6
- package/dist/api/internal/auth.d.ts +44 -6
- package/dist/api/internal/auth.d.ts.map +1 -1
- package/dist/api/internal/auth.js +160 -29
- package/dist/api/internal/events.d.ts +3 -3
- package/dist/api/internal/events.d.ts.map +1 -1
- package/dist/api/internal/events.js +10 -8
- package/dist/api/internal/health.d.ts +1 -1
- package/dist/api/layerUtils.d.ts +6 -6
- package/dist/api/layerUtils.d.ts.map +1 -1
- package/dist/api/layerUtils.js +5 -5
- package/dist/api/middlewares.d.ts +1 -1
- package/dist/api/reportError.d.ts +1 -1
- package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
- package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.d.ts +39 -3
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +48 -16
- package/dist/api/routing/middleware.d.ts +1 -2
- package/dist/api/routing/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware.js +1 -2
- package/dist/api/routing/schema/jwt.d.ts +1 -1
- package/dist/api/routing/schema/jwt.d.ts.map +1 -1
- package/dist/api/routing/tsort.d.ts +1 -1
- package/dist/api/routing/tsort.d.ts.map +1 -1
- package/dist/api/routing/utils.d.ts +3 -3
- package/dist/api/routing/utils.d.ts.map +1 -1
- package/dist/api/routing.d.ts +80 -37
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +109 -41
- package/dist/api/setupRequest.d.ts +8 -5
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +12 -7
- package/dist/api/util.d.ts +1 -1
- package/dist/arbs.d.ts +1 -1
- package/dist/arbs.d.ts.map +1 -1
- package/dist/arbs.js +5 -3
- package/dist/errorReporter.d.ts +4 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +20 -25
- package/dist/errors.d.ts +1 -1
- package/dist/fileUtil.d.ts +1 -1
- package/dist/fileUtil.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/logger/jsonLogger.d.ts +1 -1
- package/dist/logger/logFmtLogger.d.ts +1 -1
- package/dist/logger/shared.d.ts +1 -1
- package/dist/logger/shared.js +2 -2
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/otel.d.ts +75 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +65 -0
- package/dist/rateLimit.d.ts +9 -3
- package/dist/rateLimit.d.ts.map +1 -1
- package/dist/rateLimit.js +5 -11
- package/dist/test.d.ts +2 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +1 -1
- package/dist/vitest.d.ts +1 -1
- package/examples/query.ts +42 -38
- package/package.json +46 -37
- package/src/CUPS.ts +9 -11
- package/src/Emailer/Sendgrid.ts +17 -14
- package/src/Emailer/service.ts +9 -3
- package/src/MainFiberSet.ts +5 -6
- package/src/Model/Repository/Registry.ts +33 -0
- package/src/Model/Repository/ext.ts +96 -10
- package/src/Model/Repository/internal/internal.ts +218 -149
- package/src/Model/Repository/makeRepo.ts +12 -10
- package/src/Model/Repository/service.ts +31 -22
- package/src/Model/Repository/validation.ts +4 -4
- package/src/Model/Repository.ts +1 -0
- package/src/Model/dsl.ts +3 -3
- package/src/Model/filter/types/path/eager.ts +1 -2
- package/src/Model/query/dsl.ts +348 -18
- package/src/Model/query/new-kid-interpreter.ts +206 -6
- package/src/Model.ts +1 -0
- package/src/QueueMaker/SQLQueue.ts +144 -152
- package/src/QueueMaker/memQueue.ts +104 -103
- package/src/QueueMaker/sbqueue.ts +70 -86
- package/src/RequestContext.ts +14 -16
- package/src/RequestFiberSet.ts +2 -2
- package/src/Store/ContextMapContainer.ts +41 -2
- package/src/Store/Cosmos/query.ts +140 -43
- package/src/Store/Cosmos.ts +482 -349
- package/src/Store/Disk.ts +102 -65
- package/src/Store/Memory.ts +275 -87
- package/src/Store/SQL/Pg.ts +361 -0
- package/src/Store/SQL/query.ts +539 -0
- package/src/Store/SQL.ts +731 -0
- package/src/Store/codeFilter.ts +3 -1
- package/src/Store/index.ts +17 -2
- package/src/Store/service.ts +41 -10
- package/src/Store/utils.ts +23 -22
- package/src/adapters/SQL/Model.ts +41 -40
- package/src/adapters/ServiceBus.ts +125 -121
- package/src/adapters/cosmos-client.ts +2 -2
- package/src/adapters/index.ts +7 -0
- package/src/adapters/memQueue.ts +2 -2
- package/src/adapters/mongo-client.ts +2 -2
- package/src/adapters/redis-client.ts +2 -2
- package/src/api/ContextProvider.ts +12 -13
- package/src/api/internal/RequestContextMiddleware.ts +15 -5
- package/src/api/internal/auth.ts +246 -44
- package/src/api/internal/events.ts +13 -9
- package/src/api/layerUtils.ts +8 -8
- package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
- package/src/api/routing/middleware/middleware.ts +55 -14
- package/src/api/routing/middleware.ts +0 -2
- package/src/api/routing.ts +296 -131
- package/src/api/setupRequest.ts +28 -8
- package/src/arbs.ts +4 -2
- package/src/errorReporter.ts +62 -74
- package/src/logger/shared.ts +1 -1
- package/src/otel.ts +152 -0
- package/src/rateLimit.ts +30 -22
- package/src/test.ts +1 -1
- package/test/auth.test.ts +101 -0
- package/test/contextProvider.test.ts +11 -11
- package/test/controller.test.ts +21 -30
- package/test/dist/auth.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/date-query.test.d.ts.map +1 -0
- package/test/dist/fixtures.d.ts +26 -12
- package/test/dist/fixtures.d.ts.map +1 -1
- package/test/dist/fixtures.js +12 -10
- 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/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-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/sql-store.test.d.ts.map +1 -0
- package/test/fixtures.ts +11 -9
- package/test/query.test.ts +813 -38
- package/test/rawQuery.test.ts +301 -20
- package/test/repository-ext.test.ts +60 -0
- package/test/requires.test.ts +6 -6
- package/test/router-generator.test.ts +183 -0
- package/test/routing-interruptibility.test.ts +63 -0
- package/test/rpc-e2e-invalidation.test.ts +251 -0
- package/test/rpc-multi-middleware.test.ts +78 -9
- package/test/rpc-stream-fullstack.test.ts +300 -0
- package/test/sql-store.test.ts +1592 -0
- package/test/validateSample.test.ts +15 -12
- package/tsconfig.examples.json +1 -1
- package/tsconfig.json +0 -1
- package/tsconfig.json.bak +2 -2
- package/tsconfig.src.json +35 -35
- package/tsconfig.test.json +2 -2
- 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/eslint.config.mjs +0 -24
- package/src/Operations.ts +0 -235
- package/src/OperationsRepo.ts +0 -16
package/test/rawQuery.test.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { SqliteClient } from "@effect/sql-sqlite-node"
|
|
1
2
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
-
import { Array, Config, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S,
|
|
3
|
+
import { Array, Config, Context, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S, Struct } from "effect-app"
|
|
3
4
|
import { LogLevels } from "effect-app/utils"
|
|
4
5
|
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
5
|
-
import { and, or, project, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
6
|
+
import { and, computed, or, project, projectComputed, relation, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
6
7
|
import { makeRepo } from "../src/Model/Repository/makeRepo.js"
|
|
8
|
+
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
7
9
|
import { CosmosStoreLayer } from "../src/Store/Cosmos.js"
|
|
8
10
|
import { MemoryStoreLive } from "../src/Store/Memory.js"
|
|
11
|
+
import { SQLiteStoreLayer } from "../src/Store/SQL.js"
|
|
9
12
|
|
|
10
13
|
export const rt = ManagedRuntime.make(Layer.mergeAll(
|
|
11
14
|
Layer.effect(
|
|
@@ -24,7 +27,7 @@ class Something extends S.Class<Something>("Something")({
|
|
|
24
27
|
id: S.String,
|
|
25
28
|
name: S.String,
|
|
26
29
|
description: S.String,
|
|
27
|
-
items: S.Array(S.Struct({ id: S.String, value: S.
|
|
30
|
+
items: S.Array(S.Struct({ id: S.String, value: S.Finite, description: S.String }))
|
|
28
31
|
}) {}
|
|
29
32
|
|
|
30
33
|
const items = [
|
|
@@ -49,7 +52,7 @@ const items = [
|
|
|
49
52
|
]
|
|
50
53
|
|
|
51
54
|
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
52
|
-
class SomethingRepo extends
|
|
55
|
+
class SomethingRepo extends Context.Service<SomethingRepo>()(
|
|
53
56
|
"SomethingRepo",
|
|
54
57
|
{
|
|
55
58
|
make: Effect.gen(function*() {
|
|
@@ -76,28 +79,31 @@ class SomethingRepo extends ServiceMap.Service<SomethingRepo>()(
|
|
|
76
79
|
static readonly Test = this
|
|
77
80
|
.layer
|
|
78
81
|
.pipe(
|
|
79
|
-
Layer.provide(MemoryStoreLive)
|
|
82
|
+
Layer.provide(Layer.merge(MemoryStoreLive, RepositoryRegistryLive))
|
|
80
83
|
)
|
|
81
84
|
|
|
82
85
|
static readonly TestCosmos = this
|
|
83
86
|
.layer
|
|
84
87
|
.pipe(
|
|
85
88
|
Layer.provide(
|
|
86
|
-
Effect
|
|
87
|
-
|
|
88
|
-
Config.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
Effect
|
|
90
|
+
.gen(function*() {
|
|
91
|
+
const url = yield* Config.redacted("STORAGE_URL").pipe(
|
|
92
|
+
Config.withDefault(
|
|
93
|
+
Redacted.make(
|
|
94
|
+
// the emulator doesn't implement array projections :/ so you need an actual cloud instance!
|
|
95
|
+
"AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
|
|
96
|
+
)
|
|
92
97
|
)
|
|
93
98
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
return CosmosStoreLayer({
|
|
100
|
+
dbName: "test",
|
|
101
|
+
prefix: "",
|
|
102
|
+
url
|
|
103
|
+
})
|
|
104
|
+
.pipe(Layer.merge(RepositoryRegistryLive))
|
|
99
105
|
})
|
|
100
|
-
|
|
106
|
+
.pipe(Layer.unwrap)
|
|
101
107
|
)
|
|
102
108
|
)
|
|
103
109
|
}
|
|
@@ -107,7 +113,7 @@ describe("select first-level array fields", () => {
|
|
|
107
113
|
.gen(function*() {
|
|
108
114
|
const repo = yield* SomethingRepo
|
|
109
115
|
|
|
110
|
-
const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.
|
|
116
|
+
const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.Finite })) })
|
|
111
117
|
|
|
112
118
|
// ok crazy lol, "value" is a reserved word in CosmosDB, so we have to use t["value"] as a field name instead of t.value
|
|
113
119
|
const items = yield* repo.queryRaw(projected, {
|
|
@@ -159,7 +165,7 @@ describe("select first-level array fields", () => {
|
|
|
159
165
|
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
160
166
|
})
|
|
161
167
|
|
|
162
|
-
const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.
|
|
168
|
+
const projected = S.Struct({ name: S.String, items: S.Array(S.Struct({ id: S.String, value: S.Finite })) })
|
|
163
169
|
|
|
164
170
|
const expected = [
|
|
165
171
|
{
|
|
@@ -352,6 +358,41 @@ describe("multi-level", () => {
|
|
|
352
358
|
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
353
359
|
})
|
|
354
360
|
|
|
361
|
+
describe("computed projections", () => {
|
|
362
|
+
const test = Effect
|
|
363
|
+
.gen(function*() {
|
|
364
|
+
const repo = yield* SomethingRepo
|
|
365
|
+
const output = S.Struct({
|
|
366
|
+
id: S.String,
|
|
367
|
+
pickedCount: S.NonNegativeInt,
|
|
368
|
+
hasPicked: S.Boolean
|
|
369
|
+
})
|
|
370
|
+
const pickedFilter = where("value", "gt", 20)
|
|
371
|
+
const items = yield* repo.query(
|
|
372
|
+
projectComputed(
|
|
373
|
+
output,
|
|
374
|
+
computed({
|
|
375
|
+
pickedCount: relation<S.Codec.Encoded<typeof Something>>("items").count(pickedFilter),
|
|
376
|
+
hasPicked: relation<S.Codec.Encoded<typeof Something>>("items").any(pickedFilter)
|
|
377
|
+
})
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
expect(items).toStrictEqual([
|
|
381
|
+
{ id: "1", pickedCount: 0, hasPicked: false },
|
|
382
|
+
{ id: "2", pickedCount: 2, hasPicked: true }
|
|
383
|
+
])
|
|
384
|
+
})
|
|
385
|
+
.pipe(setupRequestContextFromCurrent())
|
|
386
|
+
|
|
387
|
+
it.skipIf(!process.env["STORAGE_URL"])("works well in CosmosDB", () =>
|
|
388
|
+
test
|
|
389
|
+
.pipe(Effect.provide(SomethingRepo.TestCosmos), rt.runPromise))
|
|
390
|
+
|
|
391
|
+
it("works well in Memory", () =>
|
|
392
|
+
test
|
|
393
|
+
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
394
|
+
})
|
|
395
|
+
|
|
355
396
|
// FUTURE: we need something like this instead:
|
|
356
397
|
/*
|
|
357
398
|
const subQuery = <T extends FieldValues>() => <TKey extends keyof T>(key: TKey, type: "some" | "every" = "some") => make<T[TKey][number]>() // todo: mark that this is sub query on field "items"
|
|
@@ -369,6 +410,246 @@ describe("multi-level", () => {
|
|
|
369
410
|
))
|
|
370
411
|
*/
|
|
371
412
|
|
|
413
|
+
// Mimic scanner MultiPick/EasyLife AllPickList shape:
|
|
414
|
+
// - parent has tagged-state with `at` timestamp
|
|
415
|
+
// - items is NonEmptyArray with state._tag + articleId/articleGTIN
|
|
416
|
+
// - controller filters `state.at gte X` and `state._tag neq closed`
|
|
417
|
+
// then projectComputed with: count, any(initial/picking/packed),
|
|
418
|
+
// every(picked/packed), collectDistinct(articleId)
|
|
419
|
+
const itemStateSchema = S.Union([
|
|
420
|
+
S.TaggedStruct("initial", { at: S.String }),
|
|
421
|
+
S.TaggedStruct("picking", { at: S.String }),
|
|
422
|
+
S.TaggedStruct("picked", { at: S.String }),
|
|
423
|
+
S.TaggedStruct("packed", { at: S.String })
|
|
424
|
+
])
|
|
425
|
+
|
|
426
|
+
class ArticleLineItem extends S.Class<ArticleLineItem>("ArticleLineItem")({
|
|
427
|
+
articleId: S.String,
|
|
428
|
+
articleGTIN: S.String,
|
|
429
|
+
state: itemStateSchema
|
|
430
|
+
}) {}
|
|
431
|
+
|
|
432
|
+
const stOrderState = S.Union([
|
|
433
|
+
S.TaggedStruct("initial", { at: S.String }),
|
|
434
|
+
S.TaggedStruct("packed", { at: S.String }),
|
|
435
|
+
S.TaggedStruct("closed", { at: S.String })
|
|
436
|
+
])
|
|
437
|
+
|
|
438
|
+
class Order extends S.Class<Order>("Order")({
|
|
439
|
+
id: S.String,
|
|
440
|
+
state: stOrderState,
|
|
441
|
+
items: S.NonEmptyArray(ArticleLineItem)
|
|
442
|
+
}) {}
|
|
443
|
+
|
|
444
|
+
const orderItems = [
|
|
445
|
+
new Order({
|
|
446
|
+
id: "o-open-1",
|
|
447
|
+
state: { _tag: "initial", at: "2026-05-08T08:00:00Z" },
|
|
448
|
+
items: [
|
|
449
|
+
new ArticleLineItem({
|
|
450
|
+
articleId: "A1",
|
|
451
|
+
articleGTIN: "G1",
|
|
452
|
+
state: { _tag: "picking", at: "2026-05-08T08:01:00Z" }
|
|
453
|
+
}),
|
|
454
|
+
new ArticleLineItem({
|
|
455
|
+
articleId: "A1",
|
|
456
|
+
articleGTIN: "G1",
|
|
457
|
+
state: { _tag: "picked", at: "2026-05-08T08:02:00Z" }
|
|
458
|
+
}),
|
|
459
|
+
new ArticleLineItem({
|
|
460
|
+
articleId: "A2",
|
|
461
|
+
articleGTIN: "G2",
|
|
462
|
+
state: { _tag: "initial", at: "2026-05-08T08:00:00Z" }
|
|
463
|
+
})
|
|
464
|
+
]
|
|
465
|
+
}),
|
|
466
|
+
new Order({
|
|
467
|
+
id: "o-allpicked-2",
|
|
468
|
+
state: { _tag: "packed", at: "2026-05-07T10:00:00Z" },
|
|
469
|
+
items: [
|
|
470
|
+
new ArticleLineItem({
|
|
471
|
+
articleId: "B1",
|
|
472
|
+
articleGTIN: "GB1",
|
|
473
|
+
state: { _tag: "picked", at: "2026-05-07T09:50:00Z" }
|
|
474
|
+
}),
|
|
475
|
+
new ArticleLineItem({
|
|
476
|
+
articleId: "B2",
|
|
477
|
+
articleGTIN: "GB2",
|
|
478
|
+
state: { _tag: "picked", at: "2026-05-07T09:55:00Z" }
|
|
479
|
+
})
|
|
480
|
+
]
|
|
481
|
+
}),
|
|
482
|
+
new Order({
|
|
483
|
+
id: "o-closed-3",
|
|
484
|
+
state: { _tag: "closed", at: "2026-05-04T10:00:00Z" },
|
|
485
|
+
items: [
|
|
486
|
+
new ArticleLineItem({
|
|
487
|
+
articleId: "C1",
|
|
488
|
+
articleGTIN: "GC1",
|
|
489
|
+
state: { _tag: "packed", at: "2026-05-04T09:00:00Z" }
|
|
490
|
+
})
|
|
491
|
+
]
|
|
492
|
+
})
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
496
|
+
class OrderRepo extends Context.Service<OrderRepo>()(
|
|
497
|
+
"OrderRepo",
|
|
498
|
+
{
|
|
499
|
+
make: Effect.gen(function*() {
|
|
500
|
+
const partitionKey = "orders-" + new Date().getTime()
|
|
501
|
+
return yield* makeRepo("Order", Order, { config: { partitionValue: () => partitionKey } })
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
) {
|
|
505
|
+
static readonly layer = Layer
|
|
506
|
+
.effect(
|
|
507
|
+
OrderRepo,
|
|
508
|
+
Effect.gen(function*() {
|
|
509
|
+
const partitionKey = "orders-" + new Date().getTime()
|
|
510
|
+
const repo = OrderRepo.of(
|
|
511
|
+
yield* makeRepo("Order", Order, {
|
|
512
|
+
config: { partitionValue: () => partitionKey }
|
|
513
|
+
})
|
|
514
|
+
)
|
|
515
|
+
yield* repo.saveAndPublish(orderItems).pipe(setupRequestContextFromCurrent("init"))
|
|
516
|
+
return repo
|
|
517
|
+
})
|
|
518
|
+
)
|
|
519
|
+
static readonly Test = this
|
|
520
|
+
.layer
|
|
521
|
+
.pipe(Layer.provide(Layer.merge(MemoryStoreLive, RepositoryRegistryLive)))
|
|
522
|
+
|
|
523
|
+
static readonly TestSqlite = this
|
|
524
|
+
.layer
|
|
525
|
+
.pipe(
|
|
526
|
+
Layer.provide(
|
|
527
|
+
Layer.merge(
|
|
528
|
+
SQLiteStoreLayer({
|
|
529
|
+
url: Redacted.make("sqlite://"),
|
|
530
|
+
prefix: "test_",
|
|
531
|
+
dbName: "test"
|
|
532
|
+
}),
|
|
533
|
+
RepositoryRegistryLive
|
|
534
|
+
)
|
|
535
|
+
),
|
|
536
|
+
Layer.provide(SqliteClient.layer({ filename: ":memory:" }))
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
describe("scanner-style AllPickList computed projections", () => {
|
|
541
|
+
const test = Effect
|
|
542
|
+
.gen(function*() {
|
|
543
|
+
const repo = yield* OrderRepo
|
|
544
|
+
type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
545
|
+
|
|
546
|
+
const projection = S.Struct({
|
|
547
|
+
id: S.String,
|
|
548
|
+
state: stOrderState,
|
|
549
|
+
articleCount: S.NonNegativeInt,
|
|
550
|
+
hasInitialItem: S.Boolean,
|
|
551
|
+
hasPickingItem: S.Boolean,
|
|
552
|
+
hasPackedItem: S.Boolean,
|
|
553
|
+
allItemsPicked: S.Boolean,
|
|
554
|
+
allItemsPacked: S.Boolean,
|
|
555
|
+
articleIds: S.Array(S.String)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
const result = yield* repo.query(
|
|
559
|
+
where("state.at", "gte", "2026-05-05T00:00:00Z"),
|
|
560
|
+
and("state._tag", "neq", "closed"),
|
|
561
|
+
projectComputed(
|
|
562
|
+
projection,
|
|
563
|
+
computed({
|
|
564
|
+
articleCount: relation<OrderEnc>("items").count(),
|
|
565
|
+
hasInitialItem: relation<OrderEnc>("items").any(where("state._tag", "initial")),
|
|
566
|
+
hasPickingItem: relation<OrderEnc>("items").any(where("state._tag", "picking")),
|
|
567
|
+
hasPackedItem: relation<OrderEnc>("items").any(where("state._tag", "packed")),
|
|
568
|
+
allItemsPicked: relation<OrderEnc>("items").every(where("state._tag", "picked")),
|
|
569
|
+
allItemsPacked: relation<OrderEnc>("items").every(where("state._tag", "packed")),
|
|
570
|
+
articleIds: relation<OrderEnc>("items").collectDistinct("articleId")
|
|
571
|
+
})
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
const byId = Object.fromEntries(result.map((r) => [r.id, r]))
|
|
576
|
+
|
|
577
|
+
expect(Object.keys(byId).sort()).toEqual(["o-allpicked-2", "o-open-1"])
|
|
578
|
+
|
|
579
|
+
const open = byId["o-open-1"]!
|
|
580
|
+
expect(open.articleCount).toBe(3)
|
|
581
|
+
expect(open.hasInitialItem).toBe(true)
|
|
582
|
+
expect(open.hasPickingItem).toBe(true)
|
|
583
|
+
expect(open.hasPackedItem).toBe(false)
|
|
584
|
+
expect(open.allItemsPicked).toBe(false)
|
|
585
|
+
expect(open.allItemsPacked).toBe(false)
|
|
586
|
+
expect([...open.articleIds].sort()).toEqual(["A1", "A2"])
|
|
587
|
+
|
|
588
|
+
const allp = byId["o-allpicked-2"]!
|
|
589
|
+
expect(allp.articleCount).toBe(2)
|
|
590
|
+
expect(allp.hasInitialItem).toBe(false)
|
|
591
|
+
expect(allp.hasPickingItem).toBe(false)
|
|
592
|
+
expect(allp.hasPackedItem).toBe(false)
|
|
593
|
+
expect(allp.allItemsPicked).toBe(true)
|
|
594
|
+
expect(allp.allItemsPacked).toBe(false)
|
|
595
|
+
expect([...allp.articleIds].sort()).toEqual(["B1", "B2"])
|
|
596
|
+
})
|
|
597
|
+
.pipe(setupRequestContextFromCurrent())
|
|
598
|
+
|
|
599
|
+
it("works well in Memory", () => test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
600
|
+
|
|
601
|
+
it("works well in SQLite", () => test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// Same but mimics the FULL controller projection: includes `items` array
|
|
605
|
+
// (NonEmptyArray) alongside the computed scalars. This tests the
|
|
606
|
+
// memory-side select pipeline that combines subKeys (items) with
|
|
607
|
+
// computedKeys in one Project node.
|
|
608
|
+
describe("scanner-style AllPickList — items + computed combined", () => {
|
|
609
|
+
const test = Effect
|
|
610
|
+
.gen(function*() {
|
|
611
|
+
const repo = yield* OrderRepo
|
|
612
|
+
type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
613
|
+
|
|
614
|
+
const projection = S.Struct({
|
|
615
|
+
id: S.String,
|
|
616
|
+
items: S.NonEmptyArray(ArticleLineItem.mapFields(Struct.pick(["articleId", "articleGTIN"]))),
|
|
617
|
+
articleCount: S.NonNegativeInt,
|
|
618
|
+
allItemsPicked: S.Boolean,
|
|
619
|
+
articleIds: S.Array(S.String)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
const result = yield* repo.query(
|
|
623
|
+
where("state.at", "gte", "2026-05-05T00:00:00Z"),
|
|
624
|
+
and("state._tag", "neq", "closed"),
|
|
625
|
+
projectComputed(
|
|
626
|
+
projection,
|
|
627
|
+
computed({
|
|
628
|
+
articleCount: relation<OrderEnc>("items").count(),
|
|
629
|
+
allItemsPicked: relation<OrderEnc>("items").every(where("state._tag", "picked")),
|
|
630
|
+
articleIds: relation<OrderEnc>("items").collectDistinct("articleId")
|
|
631
|
+
})
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
expect(result.length).toBe(2)
|
|
636
|
+
const byId = Object.fromEntries(result.map((r) => [r.id, r]))
|
|
637
|
+
const open = byId["o-open-1"]!
|
|
638
|
+
expect(open.items.length).toBe(3)
|
|
639
|
+
expect(open.items[0]).toHaveProperty("articleId")
|
|
640
|
+
expect(open.items[0]).toHaveProperty("articleGTIN")
|
|
641
|
+
expect(open.allItemsPicked).toBe(false)
|
|
642
|
+
const allp = byId["o-allpicked-2"]!
|
|
643
|
+
expect(allp.items.length).toBe(2)
|
|
644
|
+
expect(allp.allItemsPicked).toBe(true)
|
|
645
|
+
})
|
|
646
|
+
.pipe(setupRequestContextFromCurrent())
|
|
647
|
+
|
|
648
|
+
it("works well in Memory", () => test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
649
|
+
|
|
650
|
+
it("works well in SQLite", () => test.pipe(Effect.provide(OrderRepo.TestSqlite), rt.runPromise))
|
|
651
|
+
})
|
|
652
|
+
|
|
372
653
|
describe("removeByIds", () => {
|
|
373
654
|
const test = Effect
|
|
374
655
|
.gen(function*() {
|
|
@@ -405,7 +686,7 @@ describe("removeByIds", () => {
|
|
|
405
686
|
|
|
406
687
|
yield* repo.saveAndPublish(items)
|
|
407
688
|
const itemsAfterSave = yield* repo.all
|
|
408
|
-
yield* repo.removeById(
|
|
689
|
+
yield* repo.removeById([items[0]!.id, items[1]!.id])
|
|
409
690
|
|
|
410
691
|
const items2 = yield* repo.all
|
|
411
692
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
+
import { Effect, Layer, S } from "effect-app"
|
|
3
|
+
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
4
|
+
import { makeRepo } from "../src/Model/Repository.js"
|
|
5
|
+
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
6
|
+
import { MemoryStoreLive } from "../src/Store/Memory.js"
|
|
7
|
+
|
|
8
|
+
class BatchItem extends S.Class<BatchItem>("BatchItem")({
|
|
9
|
+
id: S.String,
|
|
10
|
+
label: S.String
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
const TestStoreLive = Layer.merge(MemoryStoreLive, RepositoryRegistryLive)
|
|
14
|
+
|
|
15
|
+
describe("repository ext save/remove batching", () => {
|
|
16
|
+
it.effect("supports save batching overload", () =>
|
|
17
|
+
Effect
|
|
18
|
+
.gen(function*() {
|
|
19
|
+
const repo = yield* makeRepo("BatchItem", BatchItem, {})
|
|
20
|
+
const items = [
|
|
21
|
+
new BatchItem({ id: "1", label: "one" }),
|
|
22
|
+
new BatchItem({ id: "2", label: "two" }),
|
|
23
|
+
new BatchItem({ id: "3", label: "three" }),
|
|
24
|
+
new BatchItem({ id: "4", label: "four" })
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
yield* repo.save(items, { batch: 2 })
|
|
28
|
+
|
|
29
|
+
const all = yield* repo.all
|
|
30
|
+
expect(all).toHaveLength(4)
|
|
31
|
+
expect(all.map((_) => _.id).toSorted()).toEqual(["1", "2", "3", "4"])
|
|
32
|
+
})
|
|
33
|
+
.pipe(
|
|
34
|
+
setupRequestContextFromCurrent(),
|
|
35
|
+
Effect.provide(TestStoreLive)
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
it.effect("supports remove batching overload", () =>
|
|
39
|
+
Effect
|
|
40
|
+
.gen(function*() {
|
|
41
|
+
const repo = yield* makeRepo("BatchItem", BatchItem, {})
|
|
42
|
+
const items = [
|
|
43
|
+
new BatchItem({ id: "1", label: "one" }),
|
|
44
|
+
new BatchItem({ id: "2", label: "two" }),
|
|
45
|
+
new BatchItem({ id: "3", label: "three" }),
|
|
46
|
+
new BatchItem({ id: "4", label: "four" })
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
yield* repo.save(items)
|
|
50
|
+
yield* repo.remove([items[0], items[1], items[2]], { batch: true })
|
|
51
|
+
|
|
52
|
+
const all = yield* repo.all
|
|
53
|
+
expect(all).toHaveLength(1)
|
|
54
|
+
expect(all[0]?.id).toBe("4")
|
|
55
|
+
})
|
|
56
|
+
.pipe(
|
|
57
|
+
setupRequestContextFromCurrent(),
|
|
58
|
+
Effect.provide(TestStoreLive)
|
|
59
|
+
))
|
|
60
|
+
})
|
package/test/requires.test.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "@effect/vitest"
|
|
2
|
-
import { Effect, Layer, Result,
|
|
2
|
+
import { Context, Effect, Layer, Result, RpcX, S } from "effect-app"
|
|
3
3
|
import { NotLoggedInError, UnauthorizedError } from "effect-app/client"
|
|
4
4
|
import { HttpHeaders } from "effect-app/http"
|
|
5
|
-
import * as RpcX from "effect-app/rpc"
|
|
6
5
|
import { MiddlewareMaker } from "effect-app/rpc"
|
|
7
6
|
import type { unhandled } from "effect-app/Types"
|
|
8
7
|
import { Rpc } from "effect/unstable/rpc"
|
|
@@ -63,11 +62,12 @@ const testSuite = (_mw: typeof middleware3) =>
|
|
|
63
62
|
"works",
|
|
64
63
|
Effect.fn(function*() {
|
|
65
64
|
const defaultOpts = {
|
|
65
|
+
client: null as any, // TODO?
|
|
66
66
|
headers: HttpHeaders.fromRecordUnsafe({}),
|
|
67
67
|
payload: { _tag: "Test" },
|
|
68
68
|
clientId: 0,
|
|
69
69
|
requestId: "test-id" as any,
|
|
70
|
-
rpc: { ...TestRpc, annotations:
|
|
70
|
+
rpc: { ...TestRpc, annotations: Context.make(_mw.requestContext, {}) }
|
|
71
71
|
}
|
|
72
72
|
const next = Effect.void as unknown as Effect.Effect<SuccessValue, unhandled, never>
|
|
73
73
|
const layer = _mw.layer.pipe(
|
|
@@ -89,7 +89,7 @@ const testSuite = (_mw: typeof middleware3) =>
|
|
|
89
89
|
headers: HttpHeaders.fromRecordUnsafe({ "x-user": "test-user", "x-is-manager": "true" }),
|
|
90
90
|
rpc: {
|
|
91
91
|
...defaultOpts.rpc,
|
|
92
|
-
annotations:
|
|
92
|
+
annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
|
|
93
93
|
}
|
|
94
94
|
})
|
|
95
95
|
)
|
|
@@ -127,7 +127,7 @@ const testSuite = (_mw: typeof middleware3) =>
|
|
|
127
127
|
Object.assign({ ...defaultOpts }, {
|
|
128
128
|
rpc: {
|
|
129
129
|
...defaultOpts.rpc,
|
|
130
|
-
annotations:
|
|
130
|
+
annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
|
|
131
131
|
}
|
|
132
132
|
})
|
|
133
133
|
)
|
|
@@ -153,7 +153,7 @@ const testSuite = (_mw: typeof middleware3) =>
|
|
|
153
153
|
{
|
|
154
154
|
rpc: {
|
|
155
155
|
...defaultOpts.rpc,
|
|
156
|
-
annotations:
|
|
156
|
+
annotations: Context.make(_mw.requestContext, { requireRoles: ["manager"] })
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { type MakeContext, type MakeErrors, makeRouter } from "@effect-app/infra/api/routing"
|
|
2
|
+
import { makeAllDSL, makeOneDSL } from "@effect-app/infra/Model"
|
|
3
|
+
import { expectTypeOf, it } from "@effect/vitest"
|
|
4
|
+
import { Context, Effect, Layer, RpcX, S } from "effect-app"
|
|
5
|
+
import { InvalidStateError, makeRpcClient, UnauthorizedError } from "effect-app/client"
|
|
6
|
+
import { DefaultGenericMiddlewares } from "effect-app/middleware"
|
|
7
|
+
import { type FixEnv } from "effect-app/Pure"
|
|
8
|
+
import { MiddlewareMaker } from "effect-app/rpc"
|
|
9
|
+
import { type TypeTestId } from "effect-app/TypeTest"
|
|
10
|
+
import { type ConfigError } from "effect/Config"
|
|
11
|
+
import { type RpcSerialization } from "effect/unstable/rpc/RpcSerialization"
|
|
12
|
+
import { DefaultGenericMiddlewaresLive, DevModeMiddlewareLive } from "../src/api/routing/middleware.js"
|
|
13
|
+
import { AllowAnonymous, AllowAnonymousLive, RequestContextMap, RequireRoles, RequireRolesLive, Some, SomeElse, SomeService, Test, TestLive } from "./fixtures.js"
|
|
14
|
+
|
|
15
|
+
// Inline minimal context provider (provides `Some`)
|
|
16
|
+
class CtxProvider extends RpcX.RpcMiddleware.Tag<CtxProvider, { provides: Some }>()("CtxProvider") {
|
|
17
|
+
static Default = Layer.make(this, {
|
|
18
|
+
*make() {
|
|
19
|
+
return Effect.fnUntraced(function*(effect) {
|
|
20
|
+
return yield* Effect.provideService(effect, Some, Some.of({ a: 1 }))
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Provides `SomeElse` so AllowAnonymous's requirement is met.
|
|
27
|
+
class SomeElseProvider extends RpcX.RpcMiddleware.Tag<SomeElseProvider, { provides: SomeElse }>()("SomeElseProvider") {
|
|
28
|
+
static Default = Layer.make(this, {
|
|
29
|
+
*make() {
|
|
30
|
+
return Effect.fnUntraced(function*(effect) {
|
|
31
|
+
return yield* Effect.provideService(effect, SomeElse, SomeElse.of({ b: 2 }))
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class mw extends MiddlewareMaker
|
|
38
|
+
.Tag<mw>()("mw", RequestContextMap)
|
|
39
|
+
.middleware(RequireRoles, Test)
|
|
40
|
+
.middleware(AllowAnonymous)
|
|
41
|
+
.middleware(CtxProvider)
|
|
42
|
+
.middleware(...DefaultGenericMiddlewares, SomeElseProvider)
|
|
43
|
+
{
|
|
44
|
+
static Default = this.layer.pipe(
|
|
45
|
+
Layer.provide([
|
|
46
|
+
RequireRolesLive,
|
|
47
|
+
TestLive,
|
|
48
|
+
AllowAnonymousLive,
|
|
49
|
+
CtxProvider.Default,
|
|
50
|
+
SomeElseProvider.Default,
|
|
51
|
+
DefaultGenericMiddlewaresLive,
|
|
52
|
+
DevModeMiddlewareLive,
|
|
53
|
+
SomeService.Default
|
|
54
|
+
])
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { TaggedRequestFor } = makeRpcClient(mw)
|
|
59
|
+
const Req = TaggedRequestFor("GenRouter")
|
|
60
|
+
|
|
61
|
+
class GetThing extends Req.Query<GetThing>()("GetThing", { id: S.String }, {
|
|
62
|
+
success: S.String,
|
|
63
|
+
error: UnauthorizedError
|
|
64
|
+
}) {}
|
|
65
|
+
class DoThing extends Req.Command<DoThing>()("DoThing", { id: S.String }, { success: S.Void }) {}
|
|
66
|
+
|
|
67
|
+
const Resource = { GetThing, DoThing }
|
|
68
|
+
|
|
69
|
+
const { Router, matchAll } = makeRouter(mw.Default)
|
|
70
|
+
|
|
71
|
+
class ThingRepo extends Context.Service<ThingRepo>()("ThingRepo", {
|
|
72
|
+
make: Effect.succeed({ get: (id: string) => Effect.succeed(id + "!") })
|
|
73
|
+
}) {
|
|
74
|
+
static Default = Layer.effect(this, this.make)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Case under test:
|
|
78
|
+
// `match({})` is given handlers as **shorthand generator methods** (`*GetThing(req) { ... }`).
|
|
79
|
+
// tsgo (>= 7 dev) infers `TNext = unknown` for these shorthand generators while TS6 infers `never`.
|
|
80
|
+
// `HandlerWithInputGen` in routing.ts must accept both — see the structural fix.
|
|
81
|
+
const router = Router(Resource)({
|
|
82
|
+
dependencies: [ThingRepo.Default],
|
|
83
|
+
*effect(match) {
|
|
84
|
+
const repo = yield* ThingRepo
|
|
85
|
+
|
|
86
|
+
if (Math.random() > 0.5) return yield* new InvalidStateError("nope")
|
|
87
|
+
|
|
88
|
+
return match({
|
|
89
|
+
*GetThing(req) {
|
|
90
|
+
const some = yield* Some
|
|
91
|
+
if (req.id === "boom") {
|
|
92
|
+
return yield* Effect.fail(new UnauthorizedError())
|
|
93
|
+
}
|
|
94
|
+
return yield* repo.get(req.id + String(some.a))
|
|
95
|
+
},
|
|
96
|
+
*DoThing(_req) {
|
|
97
|
+
yield* Effect.succeed(1)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Same scenario but using the `raw:` variant — exercises the `raw` path of `HandlerWithInputGen`.
|
|
104
|
+
const routerRaw = Router({ GetThing })({
|
|
105
|
+
*effect(match) {
|
|
106
|
+
return match({
|
|
107
|
+
GetThing: {
|
|
108
|
+
*raw(req) {
|
|
109
|
+
const some = yield* Some
|
|
110
|
+
return yield* Effect.succeed(req.id + String(some.a))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("router with generator-method handlers compiles", () => {
|
|
118
|
+
expectTypeOf(router).toMatchTypeOf<
|
|
119
|
+
Layer.Layer<never, ConfigError | InvalidStateError, SomeService | RpcSerialization>
|
|
120
|
+
>()
|
|
121
|
+
expectTypeOf(routerRaw).toMatchTypeOf<Layer.Layer<never, ConfigError, SomeService | RpcSerialization>>()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Type-level assertions: verify generator yields propagate to MakeErrors / MakeContext
|
|
125
|
+
type Errors = MakeErrors<typeof router[TypeTestId]>
|
|
126
|
+
type Ctx = MakeContext<typeof router[TypeTestId]>
|
|
127
|
+
expectTypeOf<Errors>().toEqualTypeOf<InvalidStateError>()
|
|
128
|
+
expectTypeOf<Ctx>().toEqualTypeOf<ThingRepo>()
|
|
129
|
+
|
|
130
|
+
const matched = matchAll({ router })
|
|
131
|
+
expectTypeOf(matched).toMatchTypeOf<
|
|
132
|
+
Layer.Layer<never, ConfigError | InvalidStateError, SomeService | RpcSerialization>
|
|
133
|
+
>()
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// DSL R-inference regression
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// `OneDSL`/`OneDSLExt.update`/`.modify` previously annotated the callback's
|
|
139
|
+
// effect R as `FixEnv<R, Evt, S1, S2>`. That deadlocked inference of `R`
|
|
140
|
+
// (TS6 → `never`, tsgo → `unknown`), causing yielded effects to leak
|
|
141
|
+
// `unknown` in the R slot when consumed by generator handlers.
|
|
142
|
+
// The fix uses bare `R` in the callback and `FixEnv<R, …>` only on the return.
|
|
143
|
+
class Item extends S.Class<Item>("Item")({ id: S.String, label: S.String }) {}
|
|
144
|
+
class Dep extends Context.Service<Dep>()("Dep", { make: Effect.succeed({ tag: "dep" as const }) }) {}
|
|
145
|
+
|
|
146
|
+
type Evt = { _tag: "Updated"; id: string }
|
|
147
|
+
|
|
148
|
+
const Items$ = makeAllDSL<Item, Evt>()
|
|
149
|
+
const Item$ = makeOneDSL<Item, Evt>()
|
|
150
|
+
|
|
151
|
+
// Callback body uses generator syntax (TNext = unknown under tsgo) and yields
|
|
152
|
+
// a service-dependent effect — R must be inferred as `Dep` (plus the
|
|
153
|
+
// canonical PureEnvEnv contributed by FixEnv on the return).
|
|
154
|
+
const oneUpdate = Item$.update((item) =>
|
|
155
|
+
Effect.gen(function*() {
|
|
156
|
+
const dep = yield* Dep
|
|
157
|
+
return new Item({ id: item.id, label: item.label + dep.tag })
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const allUpdate = Items$.update((items) =>
|
|
162
|
+
Effect.gen(function*() {
|
|
163
|
+
const dep = yield* Dep
|
|
164
|
+
return items.map((_) => new Item({ id: _.id, label: _.label + dep.tag }))
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const oneModify = Item$.modify((item, _dsl) =>
|
|
169
|
+
Effect.gen(function*() {
|
|
170
|
+
const dep = yield* Dep
|
|
171
|
+
return { ...item, tag: dep.tag }
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// `R` should be `FixEnv<Dep, Evt, …>` — never collapsed to `unknown`/`never`.
|
|
176
|
+
// The regression manifested as `unknown` here, breaking `Dep` assignability.
|
|
177
|
+
expectTypeOf(oneUpdate).toMatchTypeOf<Effect.Effect<Item, never, FixEnv<Dep, Evt, Item, Item>>>()
|
|
178
|
+
expectTypeOf(allUpdate).toMatchTypeOf<
|
|
179
|
+
Effect.Effect<readonly Item[], never, FixEnv<Dep, Evt, readonly Item[], readonly Item[]>>
|
|
180
|
+
>()
|
|
181
|
+
expectTypeOf(oneModify).toMatchTypeOf<
|
|
182
|
+
Effect.Effect<{ tag: "dep"; id: string; label: string }, never, FixEnv<Dep, Evt, Item, Item>>
|
|
183
|
+
>()
|