Skip to content

Commit 57db551

Browse files
Copilotpatroza
andauthored
Split TaggedRequestFor into Query/Command factories and enforce helper surfaces by request kind (#676)
* chore: plan command-query tagged requests Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * feat: split tagged requests into Query and Command with typed helpers Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * fix: refine vue helper typing and tests for query vs command requests Agent-Logs-Url: https://github.com/effect-app/libs/sessions/207b39e6-c3a8-4822-b7ad-31589e0a984e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * fix: remove query command helpers and gate request helpers to commands Agent-Logs-Url: https://github.com/effect-app/libs/sessions/a83dabc4-7697-47f9-b57a-cff535a0200d Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * fix: rename vue request helper from fetch to request Agent-Logs-Url: https://github.com/effect-app/libs/sessions/94200174-a762-4f4f-9694-fcde39079683 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * test: refine makeClient command wrap type checks Agent-Logs-Url: https://github.com/effect-app/libs/sessions/0f13e328-67ff-4d47-a725-8bd28ad338d4 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * docs: add request-kind and client helper JSDoc Agent-Logs-Url: https://github.com/effect-app/libs/sessions/4258e834-6915-4825-a2b0-a32258f1510e Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * refactor: reshape MutationExtWithInput type alias Agent-Logs-Url: https://github.com/effect-app/libs/sessions/df606d86-8d4f-4d9b-9c8e-3af0b7dfbfc8 Co-authored-by: patroza <42661+patroza@users.noreply.github.com> * Revert "refactor: reshape MutationExtWithInput type alias" This reverts commit c303476. * ffs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: patroza <42661+patroza@users.noreply.github.com> Co-authored-by: Patrick Roza <contact@patrickroza.com>
1 parent aa27f0a commit 57db551

10 files changed

Lines changed: 233 additions & 109 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"effect-app": patch
3+
"@effect-app/vue": patch
4+
---
5+
6+
Split `TaggedRequestFor` into `Query` and `Command` factories, and mark generated request classes with `type: "query" | "command"`.
7+
8+
Vue client helpers now expose query-only helpers (`query`, `suspense`, `fetch`) for query requests and mutation-only helpers (`mutate`, `fetch`) for command requests.

packages/effect-app/src/client/apiClientFactory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type Req = S.Top & {
3939
config?: Record<string, any>
4040
readonly id: string
4141
readonly moduleName: string
42+
readonly type: "command" | "query"
4243
readonly "~decodingServices"?: unknown
4344
}
4445

packages/effect-app/src/client/makeClient.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ type TaggedRequestForResult<
2424
Success extends S.Top,
2525
Error extends S.Top,
2626
Config,
27-
ModuleName extends string
27+
ModuleName extends string,
28+
Type extends "command" | "query"
2829
> =
2930
& S.EnhancedClass<Self, S.TaggedStruct<Tag, Payload>, {}>
3031
& {
@@ -36,6 +37,7 @@ type TaggedRequestForResult<
3637
readonly "~encodingServices": S.Codec.EncodingServices<Success> | S.Codec.EncodingServices<Error>
3738
readonly id: `${ModuleName}.${Tag}`
3839
readonly moduleName: ModuleName
40+
readonly type: Type
3941
}
4042

4143
export const makeRpcClient = <
@@ -84,7 +86,10 @@ export const makeRpcClient = <
8486
return RequestClass
8587
}
8688

87-
function TaggedRequestFor<ModuleName extends string>(moduleName: ModuleName) {
89+
function makeTaggedRequestWithMeta<ModuleName extends string, Type extends "command" | "query">(
90+
moduleName: ModuleName,
91+
type: Type
92+
) {
8893
function TaggedRequestWithMeta<Self>(): {
8994
<Tag extends string, Payload extends S.Struct.Fields, C extends ServiceMap>(
9095
tag: Tag,
@@ -97,7 +102,8 @@ export const makeRpcClient = <
97102
SchemaOrFields<C["success"]>,
98103
ErrorResult<C>,
99104
Omit<C, "success" | "error">,
100-
ModuleName
105+
ModuleName,
106+
Type
101107
>
102108
<Tag extends string, Payload extends S.Struct.Fields, C extends Pick<ServiceMap, "success">>(
103109
tag: Tag,
@@ -110,7 +116,8 @@ export const makeRpcClient = <
110116
SchemaOrFields<C["success"]>,
111117
ErrorResult<C>,
112118
Omit<C, "success" | "error">,
113-
ModuleName
119+
ModuleName,
120+
Type
114121
>
115122
<Tag extends string, Payload extends S.Struct.Fields, C extends Pick<ServiceMap, "error">>(
116123
tag: Tag,
@@ -123,7 +130,8 @@ export const makeRpcClient = <
123130
typeof ForceVoid,
124131
ErrorResult<C>,
125132
Omit<C, "success" | "error">,
126-
ModuleName
133+
ModuleName,
134+
Type
127135
>
128136
<Tag extends string, Payload extends S.Struct.Fields, C extends Record<string, any>>(
129137
tag: Tag,
@@ -136,7 +144,8 @@ export const makeRpcClient = <
136144
typeof ForceVoid,
137145
ErrorResult<C>,
138146
Omit<C, "success" | "error">,
139-
ModuleName
147+
ModuleName,
148+
Type
140149
>
141150
<Tag extends string, Payload extends S.Struct.Fields>(
142151
tag: Tag,
@@ -148,7 +157,8 @@ export const makeRpcClient = <
148157
typeof ForceVoid,
149158
ErrorResult<{}>,
150159
Record<string, never>,
151-
ModuleName
160+
ModuleName,
161+
Type
152162
>
153163
} {
154164
return (<Tag extends string, Fields extends S.Struct.Fields, C extends ServiceMap>(
@@ -157,11 +167,30 @@ export const makeRpcClient = <
157167
config?: C
158168
) => {
159169
const cls = makeRequestClass(tag, fields, config)
160-
Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName })
170+
Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName, type })
161171
return cls
162172
}) as any
163173
}
164-
return Object.assign(TaggedRequestWithMeta, { moduleName } as const)
174+
return Object.assign(TaggedRequestWithMeta, { moduleName, type } as const)
175+
}
176+
177+
function TaggedRequestFor<ModuleName extends string>(moduleName: ModuleName) {
178+
const Query = makeTaggedRequestWithMeta(moduleName, "query")
179+
const Command = makeTaggedRequestWithMeta(moduleName, "command")
180+
181+
return {
182+
moduleName,
183+
/**
184+
* Create query request classes for this module.
185+
* Queries read state and should not mutate server state.
186+
*/
187+
Query,
188+
/**
189+
* Create command request classes for this module.
190+
* Commands mutate state and should avoid returning complex read models.
191+
*/
192+
Command
193+
} as const
165194
}
166195

167196
return {

packages/effect-app/test/rpc.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class RequestContextMap extends RpcContextMap.makeMap({
1111
}) {}
1212

1313
const { TaggedRequestFor } = makeRpcClient(RequestContextMap)
14-
const TaggedRequest = TaggedRequestFor("Test")
14+
const TaggedRequest = TaggedRequestFor("Test").Query
1515

1616
export class Stats extends TaggedRequest<Stats>()("Stats", {}, {
1717
allowedRoles: ["manager"],
@@ -26,6 +26,7 @@ export class Stats extends TaggedRequest<Stats>()("Stats", {}, {
2626
declare const _stats: typeof Stats.Type
2727
declare const _statsSuccess: typeof Stats.success.Type
2828
declare const _statsError: typeof Stats.error.Type
29+
declare const _statsRequestType: typeof Stats.type
2930

3031
test("ForceVoid decodes and encodes as void", () => {
3132
expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined)
@@ -42,4 +43,5 @@ test("ForceVoid decodes and encodes as void", () => {
4243
readonly newUsersLastWeek: number
4344
}>()
4445
expectTypeOf<typeof _statsError>().toEqualTypeOf<NotLoggedInError | UnauthorizedError>()
46+
expectTypeOf<typeof _statsRequestType>().toEqualTypeOf<"query">()
4547
})

packages/infra/test/controller.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,15 @@ export const middleware3 = MiddlewareMaker
204204

205205
export const { TaggedRequestFor } = makeRpcClient(RequestContextMap)
206206
const Req = TaggedRequestFor("Something")
207+
const Command = Req.Command
208+
const Query = Req.Query
207209

208-
export class Eff extends Req<Eff>()("Eff", {}, { success: S.Void }) {}
209-
export class Gen extends Req<Gen>()("Gen", {}) {}
210+
export class Eff extends Command<Eff>()("Eff", {}, { success: S.Void }) {}
211+
export class Gen extends Command<Gen>()("Gen", {}) {}
210212

211213
expectTypeOf(Eff.error).toEqualTypeOf<typeof Gen.error>()
212214

213-
export class DoSomething extends Req<DoSomething>()("DoSomething", {
215+
export class DoSomething extends Command<DoSomething>()("DoSomething", {
214216
id: S.String
215217
}, { success: S.Void }) {}
216218

@@ -228,11 +230,11 @@ export class DoSomething extends Req<DoSomething>()("DoSomething", {
228230
// )
229231
// )
230232

231-
export class GetSomething extends Req<GetSomething>()("GetSomething", {
233+
export class GetSomething extends Query<GetSomething>()("GetSomething", {
232234
id: S.String
233235
}, { success: S.String }) {}
234236

235-
export class GetSomething2 extends Req<GetSomething2>()("GetSomething2", {
237+
export class GetSomething2 extends Query<GetSomething2>()("GetSomething2", {
236238
id: S.String
237239
}, { success: S.FiniteFromString }) {}
238240

packages/vue/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
### Minor Changes
66

7-
- 6a3d364: Client entries are now plain objects; use `.fetch` to invoke the request.
7+
- 6a3d364: Client entries are now plain objects; use `.request` to invoke the request.
88

9-
`client.Xxx` no longer is callable or an `Effect` itself. Call `client.Xxx.fetch(input)` (or `client.Xxx.fetch` for input-less requests) instead. `.mutate`, `.query`, `.suspense`, `.wrap`, and `.fn` are unchanged.
9+
`client.Xxx` no longer is callable or an `Effect` itself. Call `client.Xxx.request(input)` (or `client.Xxx.request` for input-less requests) instead. `.mutate`, `.query`, `.suspense`, `.wrap`, and `.fn` are unchanged.
1010

1111
### Patch Changes
1212

0 commit comments

Comments
 (0)