Skip to content
Merged
8 changes: 8 additions & 0 deletions .changeset/tagged-request-command-query-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"effect-app": patch
"@effect-app/vue": patch
---

Split `TaggedRequestFor` into `Query` and `Command` factories, and mark generated request classes with `type: "query" | "command"`.

Vue client helpers now expose query-only helpers (`query`, `suspense`, `fetch`) for query requests and mutation-only helpers (`mutate`, `fetch`) for command requests.
1 change: 1 addition & 0 deletions packages/effect-app/src/client/apiClientFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type Req = S.Top & {
config?: Record<string, any>
readonly id: string
readonly moduleName: string
readonly type: "command" | "query"
readonly "~decodingServices"?: unknown
}

Expand Down
47 changes: 38 additions & 9 deletions packages/effect-app/src/client/makeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ type TaggedRequestForResult<
Success extends S.Top,
Error extends S.Top,
Config,
ModuleName extends string
ModuleName extends string,
Type extends "command" | "query"
> =
& S.EnhancedClass<Self, S.TaggedStruct<Tag, Payload>, {}>
& {
Expand All @@ -36,6 +37,7 @@ type TaggedRequestForResult<
readonly "~encodingServices": S.Codec.EncodingServices<Success> | S.Codec.EncodingServices<Error>
readonly id: `${ModuleName}.${Tag}`
readonly moduleName: ModuleName
readonly type: Type
}

export const makeRpcClient = <
Expand Down Expand Up @@ -84,7 +86,10 @@ export const makeRpcClient = <
return RequestClass
}

function TaggedRequestFor<ModuleName extends string>(moduleName: ModuleName) {
function makeTaggedRequestWithMeta<ModuleName extends string, Type extends "command" | "query">(
moduleName: ModuleName,
type: Type
) {
function TaggedRequestWithMeta<Self>(): {
<Tag extends string, Payload extends S.Struct.Fields, C extends ServiceMap>(
tag: Tag,
Expand All @@ -97,7 +102,8 @@ export const makeRpcClient = <
SchemaOrFields<C["success"]>,
ErrorResult<C>,
Omit<C, "success" | "error">,
ModuleName
ModuleName,
Type
>
<Tag extends string, Payload extends S.Struct.Fields, C extends Pick<ServiceMap, "success">>(
tag: Tag,
Expand All @@ -110,7 +116,8 @@ export const makeRpcClient = <
SchemaOrFields<C["success"]>,
ErrorResult<C>,
Omit<C, "success" | "error">,
ModuleName
ModuleName,
Type
>
<Tag extends string, Payload extends S.Struct.Fields, C extends Pick<ServiceMap, "error">>(
tag: Tag,
Expand All @@ -123,7 +130,8 @@ export const makeRpcClient = <
typeof ForceVoid,
ErrorResult<C>,
Omit<C, "success" | "error">,
ModuleName
ModuleName,
Type
>
<Tag extends string, Payload extends S.Struct.Fields, C extends Record<string, any>>(
tag: Tag,
Expand All @@ -136,7 +144,8 @@ export const makeRpcClient = <
typeof ForceVoid,
ErrorResult<C>,
Omit<C, "success" | "error">,
ModuleName
ModuleName,
Type
>
<Tag extends string, Payload extends S.Struct.Fields>(
tag: Tag,
Expand All @@ -148,7 +157,8 @@ export const makeRpcClient = <
typeof ForceVoid,
ErrorResult<{}>,
Record<string, never>,
ModuleName
ModuleName,
Type
>
} {
return (<Tag extends string, Fields extends S.Struct.Fields, C extends ServiceMap>(
Expand All @@ -157,11 +167,30 @@ export const makeRpcClient = <
config?: C
) => {
const cls = makeRequestClass(tag, fields, config)
Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName })
Object.assign(cls, { id: `${moduleName}.${tag}`, moduleName, type })
return cls
}) as any
}
return Object.assign(TaggedRequestWithMeta, { moduleName } as const)
return Object.assign(TaggedRequestWithMeta, { moduleName, type } as const)
}

function TaggedRequestFor<ModuleName extends string>(moduleName: ModuleName) {
const Query = makeTaggedRequestWithMeta(moduleName, "query")
const Command = makeTaggedRequestWithMeta(moduleName, "command")

return {
moduleName,
/**
* Create query request classes for this module.
* Queries read state and should not mutate server state.
*/
Query,
/**
* Create command request classes for this module.
* Commands mutate state and should avoid returning complex read models.
*/
Command
} as const
}

return {
Expand Down
4 changes: 3 additions & 1 deletion packages/effect-app/test/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class RequestContextMap extends RpcContextMap.makeMap({
}) {}

const { TaggedRequestFor } = makeRpcClient(RequestContextMap)
const TaggedRequest = TaggedRequestFor("Test")
const TaggedRequest = TaggedRequestFor("Test").Query

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

test("ForceVoid decodes and encodes as void", () => {
expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined)
Expand All @@ -42,4 +43,5 @@ test("ForceVoid decodes and encodes as void", () => {
readonly newUsersLastWeek: number
}>()
expectTypeOf<typeof _statsError>().toEqualTypeOf<NotLoggedInError | UnauthorizedError>()
expectTypeOf<typeof _statsRequestType>().toEqualTypeOf<"query">()
})
12 changes: 7 additions & 5 deletions packages/infra/test/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,15 @@ export const middleware3 = MiddlewareMaker

export const { TaggedRequestFor } = makeRpcClient(RequestContextMap)
const Req = TaggedRequestFor("Something")
const Command = Req.Command
const Query = Req.Query

export class Eff extends Req<Eff>()("Eff", {}, { success: S.Void }) {}
export class Gen extends Req<Gen>()("Gen", {}) {}
export class Eff extends Command<Eff>()("Eff", {}, { success: S.Void }) {}
export class Gen extends Command<Gen>()("Gen", {}) {}

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

export class DoSomething extends Req<DoSomething>()("DoSomething", {
export class DoSomething extends Command<DoSomething>()("DoSomething", {
id: S.String
}, { success: S.Void }) {}

Expand All @@ -228,11 +230,11 @@ export class DoSomething extends Req<DoSomething>()("DoSomething", {
// )
// )

export class GetSomething extends Req<GetSomething>()("GetSomething", {
export class GetSomething extends Query<GetSomething>()("GetSomething", {
id: S.String
}, { success: S.String }) {}

export class GetSomething2 extends Req<GetSomething2>()("GetSomething2", {
export class GetSomething2 extends Query<GetSomething2>()("GetSomething2", {
id: S.String
}, { success: S.FiniteFromString }) {}

Expand Down
4 changes: 2 additions & 2 deletions packages/vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

### Minor Changes

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

`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.
`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.

### Patch Changes

Expand Down
Loading
Loading