effect 4.0.0-beta.80 → 4.0.0-beta.81
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/dist/Config.d.ts.map +1 -1
- package/dist/Config.js +5 -2
- package/dist/Config.js.map +1 -1
- package/dist/Schema.d.ts +1 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/unstable/encoding/Sse.d.ts +18 -14
- package/dist/unstable/encoding/Sse.d.ts.map +1 -1
- package/dist/unstable/encoding/Sse.js +7 -4
- package/dist/unstable/encoding/Sse.js.map +1 -1
- package/dist/unstable/httpapi/HttpApi.d.ts +1 -1
- package/dist/unstable/httpapi/HttpApi.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApi.js +1 -0
- package/dist/unstable/httpapi/HttpApi.js.map +1 -1
- package/dist/unstable/httpapi/HttpApiBuilder.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApiBuilder.js +92 -11
- package/dist/unstable/httpapi/HttpApiBuilder.js.map +1 -1
- package/dist/unstable/httpapi/HttpApiClient.d.ts +6 -1
- package/dist/unstable/httpapi/HttpApiClient.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApiClient.js +114 -3
- package/dist/unstable/httpapi/HttpApiClient.js.map +1 -1
- package/dist/unstable/httpapi/HttpApiEndpoint.d.ts +60 -50
- package/dist/unstable/httpapi/HttpApiEndpoint.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApiEndpoint.js +116 -5
- package/dist/unstable/httpapi/HttpApiEndpoint.js.map +1 -1
- package/dist/unstable/httpapi/HttpApiGroup.d.ts +1 -1
- package/dist/unstable/httpapi/HttpApiGroup.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApiSchema.d.ts +116 -2
- package/dist/unstable/httpapi/HttpApiSchema.d.ts.map +1 -1
- package/dist/unstable/httpapi/HttpApiSchema.js +75 -5
- package/dist/unstable/httpapi/HttpApiSchema.js.map +1 -1
- package/dist/unstable/httpapi/OpenApi.d.ts +15 -0
- package/dist/unstable/httpapi/OpenApi.d.ts.map +1 -1
- package/dist/unstable/httpapi/OpenApi.js +82 -13
- package/dist/unstable/httpapi/OpenApi.js.map +1 -1
- package/dist/unstable/reactivity/AtomHttpApi.d.ts +2 -2
- package/dist/unstable/reactivity/AtomHttpApi.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Config.ts +5 -2
- package/src/Schema.ts +1 -1
- package/src/unstable/encoding/Sse.ts +34 -20
- package/src/unstable/httpapi/HttpApi.ts +2 -1
- package/src/unstable/httpapi/HttpApiBuilder.ts +148 -3
- package/src/unstable/httpapi/HttpApiClient.ts +196 -5
- package/src/unstable/httpapi/HttpApiEndpoint.ts +209 -18
- package/src/unstable/httpapi/HttpApiGroup.ts +1 -1
- package/src/unstable/httpapi/HttpApiSchema.ts +249 -5
- package/src/unstable/httpapi/OpenApi.ts +130 -17
- package/src/unstable/reactivity/AtomHttpApi.ts +2 -2
|
@@ -21,8 +21,10 @@ import * as Schema from "../../Schema.ts"
|
|
|
21
21
|
import * as SchemaAST from "../../SchemaAST.ts"
|
|
22
22
|
import * as SchemaIssue from "../../SchemaIssue.ts"
|
|
23
23
|
import * as SchemaTransformation from "../../SchemaTransformation.ts"
|
|
24
|
+
import * as Stream from "../../Stream.ts"
|
|
24
25
|
import type { Simplify } from "../../Types.ts"
|
|
25
26
|
import * as UndefinedOr from "../../UndefinedOr.ts"
|
|
27
|
+
import * as Sse from "../encoding/Sse.ts"
|
|
26
28
|
import * as HttpBody from "../http/HttpBody.ts"
|
|
27
29
|
import * as HttpClient from "../http/HttpClient.ts"
|
|
28
30
|
import * as HttpClientError from "../http/HttpClientError.ts"
|
|
@@ -68,6 +70,30 @@ export type ForApi<Api extends HttpApi.Any, E = never, R = never> = Api extends
|
|
|
68
70
|
HttpApi.HttpApi<infer _Id, infer Groups> ? Client<Groups, E, R> :
|
|
69
71
|
never
|
|
70
72
|
|
|
73
|
+
type SuccessType<S> = S extends HttpApiSchema.StreamSse<
|
|
74
|
+
infer _Events,
|
|
75
|
+
infer _Error,
|
|
76
|
+
infer _Value
|
|
77
|
+
> ? Stream.Stream<
|
|
78
|
+
_Value,
|
|
79
|
+
_Error["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry,
|
|
80
|
+
never
|
|
81
|
+
>
|
|
82
|
+
: S extends HttpApiSchema.StreamUint8Array ? Stream.Stream<Uint8Array, HttpClientError.HttpClientError, never>
|
|
83
|
+
: S extends Schema.Top ? S["Type"]
|
|
84
|
+
: never
|
|
85
|
+
|
|
86
|
+
type SuccessDecodingServices<S> = S extends HttpApiSchema.StreamSse<
|
|
87
|
+
infer _Events,
|
|
88
|
+
infer _Error,
|
|
89
|
+
infer _Value
|
|
90
|
+
> ?
|
|
91
|
+
| _Events["DecodingServices"]
|
|
92
|
+
| _Error["DecodingServices"]
|
|
93
|
+
: S extends HttpApiSchema.StreamUint8Array ? never
|
|
94
|
+
: S extends Schema.Top ? S["DecodingServices"]
|
|
95
|
+
: never
|
|
96
|
+
|
|
71
97
|
/**
|
|
72
98
|
* Helper types used to describe generated HTTP API clients, including endpoint
|
|
73
99
|
* methods, response modes, and grouped client shapes.
|
|
@@ -135,7 +161,7 @@ export declare namespace Client {
|
|
|
135
161
|
] ? <Mode extends ResponseMode = ResponseMode>(
|
|
136
162
|
request: Simplify<HttpApiEndpoint.ClientRequest<_Params, _Query, _Payload, _Headers, Mode>>
|
|
137
163
|
) => Effect.Effect<
|
|
138
|
-
Response<_Success
|
|
164
|
+
Response<SuccessType<_Success>, Mode>,
|
|
139
165
|
| HttpApiMiddleware.Error<_Middleware>
|
|
140
166
|
| HttpApiMiddleware.ClientError<_Middleware>
|
|
141
167
|
| E
|
|
@@ -146,7 +172,10 @@ export declare namespace Client {
|
|
|
146
172
|
| _Query["EncodingServices"]
|
|
147
173
|
| _Payload["EncodingServices"]
|
|
148
174
|
| _Headers["EncodingServices"]
|
|
149
|
-
| ([Mode] extends ["response-only"] ? never
|
|
175
|
+
| ([Mode] extends ["response-only"] ? never
|
|
176
|
+
:
|
|
177
|
+
| SuccessDecodingServices<_Success>
|
|
178
|
+
| _Error["DecodingServices"])
|
|
150
179
|
> :
|
|
151
180
|
never
|
|
152
181
|
|
|
@@ -312,9 +341,25 @@ export const makeClient = <ApiId extends string, Groups extends HttpApiGroup.Any
|
|
|
312
341
|
Effect.fail
|
|
313
342
|
)
|
|
314
343
|
})
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
|
|
345
|
+
const successAlternatives = new Map<number, Array<ResponseAlternative>>()
|
|
346
|
+
for (const [status, schemas] of successes.entries()) {
|
|
347
|
+
const grouped = groupSchemasByContentType(schemas)
|
|
348
|
+
for (const [contentType, schemas] of grouped.entries()) {
|
|
349
|
+
addResponseAlternative(successAlternatives, status, contentType, schemasToResponse(schemas))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const streamSuccess of getStreamSuccessSchemas(endpoint)) {
|
|
353
|
+
addResponseAlternative(
|
|
354
|
+
successAlternatives,
|
|
355
|
+
HttpApiSchema.getStatusStream(streamSuccess),
|
|
356
|
+
streamSuccess.contentType,
|
|
357
|
+
streamToResponse(streamSuccess)
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
for (const [status, alternatives] of successAlternatives.entries()) {
|
|
361
|
+
decodeMap[status] = makeResponseDecoder(alternatives)
|
|
362
|
+
}
|
|
318
363
|
|
|
319
364
|
// encoders
|
|
320
365
|
const encodeParams = UndefinedOr.map(endpoint.params, Schema.encodeUnknownEffect)
|
|
@@ -648,6 +693,152 @@ function schemasToResponse(schemas: readonly [Schema.Top, ...Array<Schema.Top>])
|
|
|
648
693
|
return (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap(response.arrayBuffer, decode)
|
|
649
694
|
}
|
|
650
695
|
|
|
696
|
+
type ResponseDecoder = (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<unknown, unknown, unknown>
|
|
697
|
+
|
|
698
|
+
interface ResponseAlternative {
|
|
699
|
+
readonly contentType: string
|
|
700
|
+
readonly decode: ResponseDecoder
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function addResponseAlternative(
|
|
704
|
+
map: Map<number, Array<ResponseAlternative>>,
|
|
705
|
+
status: number,
|
|
706
|
+
contentType: string,
|
|
707
|
+
decode: ResponseDecoder
|
|
708
|
+
) {
|
|
709
|
+
const normalizedContentType = normalizeContentType(contentType)
|
|
710
|
+
const alternatives = map.get(status)
|
|
711
|
+
if (alternatives === undefined) {
|
|
712
|
+
map.set(status, [{ contentType: normalizedContentType, decode }])
|
|
713
|
+
} else {
|
|
714
|
+
alternatives.push({ contentType: normalizedContentType, decode })
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function makeResponseDecoder(alternatives: ReadonlyArray<ResponseAlternative>): ResponseDecoder {
|
|
719
|
+
const first = alternatives[0]
|
|
720
|
+
if (alternatives.length === 1 && first !== undefined) {
|
|
721
|
+
return first.decode
|
|
722
|
+
}
|
|
723
|
+
return (response) => {
|
|
724
|
+
const contentType = normalizeContentType(response.headers["content-type"] ?? "")
|
|
725
|
+
const alternative = alternatives.find((alternative) => alternative.contentType === contentType)
|
|
726
|
+
return alternative === undefined
|
|
727
|
+
? failUnsupportedContentType(response, contentType, alternatives)
|
|
728
|
+
: alternative.decode(response)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function groupSchemasByContentType(
|
|
733
|
+
schemas: Arr.NonEmptyReadonlyArray<Schema.Top>
|
|
734
|
+
): Map<string, Arr.NonEmptyReadonlyArray<Schema.Top>> {
|
|
735
|
+
const grouped = new Map<string, [Schema.Top, ...Array<Schema.Top>]>()
|
|
736
|
+
for (const schema of schemas) {
|
|
737
|
+
const contentType = HttpApiSchema.getResponseEncoding(schema.ast).contentType
|
|
738
|
+
const existing = grouped.get(contentType)
|
|
739
|
+
if (existing === undefined) {
|
|
740
|
+
grouped.set(contentType, [schema])
|
|
741
|
+
} else {
|
|
742
|
+
existing.push(schema)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return grouped
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function normalizeContentType(contentType: string): string {
|
|
749
|
+
const normalized = contentType.toLowerCase().trim()
|
|
750
|
+
const index = normalized.indexOf(";")
|
|
751
|
+
return index === -1 ? normalized : normalized.slice(0, index).trim()
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function failUnsupportedContentType(
|
|
755
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
756
|
+
contentType: string,
|
|
757
|
+
alternatives: ReadonlyArray<ResponseAlternative>
|
|
758
|
+
) {
|
|
759
|
+
const expected = Array.from(new Set(alternatives.map((alternative) => alternative.contentType))).join(", ")
|
|
760
|
+
return Effect.fail(
|
|
761
|
+
new HttpClientError.HttpClientError({
|
|
762
|
+
reason: new HttpClientError.DecodeError({
|
|
763
|
+
request: response.request,
|
|
764
|
+
response,
|
|
765
|
+
description: `Unsupported response content-type for status ${response.status}: ${
|
|
766
|
+
contentType || "<missing>"
|
|
767
|
+
}. Expected one of: ${expected}`
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const reservedStreamFailureEvent = "effect/httpapi/stream/failure"
|
|
774
|
+
|
|
775
|
+
function getStreamSuccessSchemas(endpoint: HttpApiEndpoint.AnyWithProps): Array<HttpApiSchema.StreamSchema> {
|
|
776
|
+
const schemas: Array<HttpApiSchema.StreamSchema> = []
|
|
777
|
+
for (const schema of endpoint.success) {
|
|
778
|
+
if (HttpApiSchema.isStreamSchema(schema)) {
|
|
779
|
+
schemas.push(schema)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return schemas
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function streamToResponse(streamSchema: HttpApiSchema.StreamSchema) {
|
|
786
|
+
return (response: HttpClientResponse.HttpClientResponse) =>
|
|
787
|
+
Effect.map(Effect.context<never>(), (context) =>
|
|
788
|
+
Stream.provideContext(
|
|
789
|
+
HttpApiSchema.isStreamUint8Array(streamSchema) ?
|
|
790
|
+
response.stream :
|
|
791
|
+
decodeSseStream(response.stream, streamSchema),
|
|
792
|
+
context as Context.Context<unknown>
|
|
793
|
+
))
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function decodeSseStream(
|
|
797
|
+
stream: Stream.Stream<Uint8Array, HttpClientError.HttpClientError>,
|
|
798
|
+
declaration: HttpApiSchema.StreamSse<Sse.EventCodec, Schema.Top, unknown>
|
|
799
|
+
): Stream.Stream<unknown, unknown, unknown> {
|
|
800
|
+
const Event = Schema.Union([
|
|
801
|
+
declaration.events,
|
|
802
|
+
Schema.Struct({
|
|
803
|
+
event: Schema.Literal(reservedStreamFailureEvent),
|
|
804
|
+
data: Schema.fromJsonString(Schema.toCodecJson(Schema.Cause(declaration.error, Schema.Defect())))
|
|
805
|
+
})
|
|
806
|
+
])
|
|
807
|
+
const events = Stream.transformPull(
|
|
808
|
+
stream.pipe(
|
|
809
|
+
Stream.decodeText,
|
|
810
|
+
Stream.pipeThroughChannel(Sse.decodeSchema(Event))
|
|
811
|
+
),
|
|
812
|
+
(pull) =>
|
|
813
|
+
Effect.sync(() => {
|
|
814
|
+
let failureCause: Cause.Cause<unknown> | undefined = undefined
|
|
815
|
+
return Effect.suspend(() => {
|
|
816
|
+
if (failureCause) {
|
|
817
|
+
return Effect.failCause(failureCause)
|
|
818
|
+
}
|
|
819
|
+
return Effect.flatMap(pull, (events) => {
|
|
820
|
+
for (let i = 0; i < events.length; i++) {
|
|
821
|
+
const event = events[i]
|
|
822
|
+
if (event.event === reservedStreamFailureEvent) {
|
|
823
|
+
if (i === 0) {
|
|
824
|
+
return Effect.failCause(event.data)
|
|
825
|
+
}
|
|
826
|
+
failureCause = event.data
|
|
827
|
+
events = events.slice(0, i) as any
|
|
828
|
+
break
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return Effect.succeed(events)
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
})
|
|
835
|
+
)
|
|
836
|
+
if (declaration.sseMode === "data") {
|
|
837
|
+
return Stream.map(events, (event) => event.data)
|
|
838
|
+
}
|
|
839
|
+
return events
|
|
840
|
+
}
|
|
841
|
+
|
|
651
842
|
const ArrayBuffer = Schema.instanceOf(globalThis.ArrayBuffer, {
|
|
652
843
|
expected: "ArrayBuffer"
|
|
653
844
|
})
|
|
@@ -19,6 +19,7 @@ import { identity } from "../../Function.ts"
|
|
|
19
19
|
import { type Pipeable, pipeArguments } from "../../Pipeable.ts"
|
|
20
20
|
import * as Predicate from "../../Predicate.ts"
|
|
21
21
|
import * as Schema from "../../Schema.ts"
|
|
22
|
+
import * as AST from "../../SchemaAST.ts"
|
|
22
23
|
import type * as Stream from "../../Stream.ts"
|
|
23
24
|
import type * as Types from "../../Types.ts"
|
|
24
25
|
import type { HttpMethod } from "../http/HttpMethod.ts"
|
|
@@ -53,6 +54,48 @@ export type PayloadMap = ReadonlyMap<string, {
|
|
|
53
54
|
readonly schemas: [Schema.Top, ...Array<Schema.Top>]
|
|
54
55
|
}>
|
|
55
56
|
|
|
57
|
+
type SuccessType<S> = S extends HttpApiSchema.StreamSse<
|
|
58
|
+
infer _Events,
|
|
59
|
+
infer _Error,
|
|
60
|
+
infer _Value
|
|
61
|
+
> ? Stream.Stream<_Value, _Error["Type"], never>
|
|
62
|
+
: S extends HttpApiSchema.StreamUint8Array ? Stream.Stream<Uint8Array, unknown, never>
|
|
63
|
+
: S extends Schema.Top ? S["Type"]
|
|
64
|
+
: never
|
|
65
|
+
|
|
66
|
+
type SuccessEncodingServices<S> = S extends HttpApiSchema.StreamSse<
|
|
67
|
+
infer _Events,
|
|
68
|
+
infer _Error,
|
|
69
|
+
infer _Value
|
|
70
|
+
> ? _Events["EncodingServices"] | _Error["EncodingServices"]
|
|
71
|
+
: S extends HttpApiSchema.StreamUint8Array ? never
|
|
72
|
+
: S extends Schema.Top ? S["EncodingServices"]
|
|
73
|
+
: never
|
|
74
|
+
|
|
75
|
+
type SuccessDecodingServices<S> = S extends HttpApiSchema.StreamSse<
|
|
76
|
+
infer _Events,
|
|
77
|
+
infer _Error,
|
|
78
|
+
infer _Value
|
|
79
|
+
> ? _Events["DecodingServices"] | _Error["DecodingServices"]
|
|
80
|
+
: S extends HttpApiSchema.StreamUint8Array ? never
|
|
81
|
+
: S extends Schema.Top ? S["DecodingServices"]
|
|
82
|
+
: never
|
|
83
|
+
|
|
84
|
+
type ExtractSuccessOrArray<S extends SuccessConstraint> = S extends ReadonlyArray<Schema.Top> ? S[number] : S
|
|
85
|
+
|
|
86
|
+
type ExtractBufferedSuccess<S extends SuccessConstraint> = Exclude<
|
|
87
|
+
Extract<ExtractSuccessOrArray<S>, Schema.Top>,
|
|
88
|
+
HttpApiSchema.StreamSchema
|
|
89
|
+
>
|
|
90
|
+
|
|
91
|
+
type ExtractStreamSuccess<S extends SuccessConstraint> = ExtractSuccessOrArray<S> extends infer Success ?
|
|
92
|
+
Success extends HttpApiSchema.StreamSchema ? Success : never
|
|
93
|
+
: never
|
|
94
|
+
|
|
95
|
+
type JsonSuccessOrArray<S extends SuccessConstraint> = [ExtractBufferedSuccess<S>] extends [never] ?
|
|
96
|
+
ExtractStreamSuccess<S>
|
|
97
|
+
: Json<ExtractBufferedSuccess<S>> | ExtractStreamSuccess<S>
|
|
98
|
+
|
|
56
99
|
/**
|
|
57
100
|
* Represents an API endpoint. An API endpoint is mapped to a single route on
|
|
58
101
|
* the underlying `HttpRouter`.
|
|
@@ -587,7 +630,7 @@ export type ServerServices<Endpoint> = Endpoint extends HttpApiEndpoint<
|
|
|
587
630
|
| _Query["DecodingServices"]
|
|
588
631
|
| _Payload["DecodingServices"]
|
|
589
632
|
| _Headers["DecodingServices"]
|
|
590
|
-
| _Success
|
|
633
|
+
| SuccessEncodingServices<_Success>
|
|
591
634
|
| _Error["EncodingServices"]
|
|
592
635
|
| HttpApiMiddleware.ErrorServicesEncode<_M>
|
|
593
636
|
: never
|
|
@@ -616,7 +659,7 @@ export type ClientServices<Endpoint> = Endpoint extends HttpApiEndpoint<
|
|
|
616
659
|
| _Query["EncodingServices"]
|
|
617
660
|
| _Payload["EncodingServices"]
|
|
618
661
|
| _Headers["EncodingServices"]
|
|
619
|
-
| _Success
|
|
662
|
+
| SuccessDecodingServices<_Success>
|
|
620
663
|
| _Error["DecodingServices"]
|
|
621
664
|
: never
|
|
622
665
|
|
|
@@ -672,7 +715,7 @@ export type ErrorServicesDecode<Endpoint> = Endpoint extends HttpApiEndpoint<
|
|
|
672
715
|
*/
|
|
673
716
|
export type Handler<Endpoint extends Any, E, R> = (
|
|
674
717
|
request: Types.Simplify<Request<Endpoint>>
|
|
675
|
-
) => Effect<Endpoint["~Success"]
|
|
718
|
+
) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
|
|
676
719
|
|
|
677
720
|
/**
|
|
678
721
|
* The raw server handler for an endpoint, receiving a request shape without a
|
|
@@ -683,7 +726,7 @@ export type Handler<Endpoint extends Any, E, R> = (
|
|
|
683
726
|
*/
|
|
684
727
|
export type HandlerRaw<Endpoint extends Any, E, R> = (
|
|
685
728
|
request: Types.Simplify<RequestRaw<Endpoint>>
|
|
686
|
-
) => Effect<Endpoint["~Success"]
|
|
729
|
+
) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
|
|
687
730
|
|
|
688
731
|
/**
|
|
689
732
|
* Selects the endpoint with the specified name from a union of endpoints.
|
|
@@ -736,7 +779,7 @@ export type HandlerRawWithName<Endpoints extends Any, Name extends string, E, R>
|
|
|
736
779
|
*/
|
|
737
780
|
export type SuccessWithName<Endpoints extends Any, Name extends string> = Success<
|
|
738
781
|
WithName<Endpoints, Name>
|
|
739
|
-
>
|
|
782
|
+
> extends infer S ? SuccessType<S> : never
|
|
740
783
|
|
|
741
784
|
/**
|
|
742
785
|
* Computes the full error value union for the endpoint with the specified name in
|
|
@@ -1024,7 +1067,7 @@ export type PayloadConstraint<Method extends HttpMethod> = Method extends HttpMe
|
|
|
1024
1067
|
string,
|
|
1025
1068
|
Schema.Encoder<string | ReadonlyArray<string> | undefined, unknown>
|
|
1026
1069
|
> :
|
|
1027
|
-
|
|
1070
|
+
Schema.Top | ReadonlyArray<Schema.Top>
|
|
1028
1071
|
|
|
1029
1072
|
/**
|
|
1030
1073
|
* Payload constraint used when automatic codecs are enabled: no-body methods
|
|
@@ -1056,6 +1099,13 @@ export type SuccessConstraint = Schema.Top | ReadonlyArray<Schema.Top>
|
|
|
1056
1099
|
*/
|
|
1057
1100
|
export type ErrorConstraint = Schema.Top | ReadonlyArray<Schema.Top>
|
|
1058
1101
|
|
|
1102
|
+
type ErrorWithoutStream<S extends ErrorConstraint> = [
|
|
1103
|
+
Extract<
|
|
1104
|
+
S extends ReadonlyArray<Schema.Top> ? S[number] : S,
|
|
1105
|
+
HttpApiSchema.StreamSchema
|
|
1106
|
+
>
|
|
1107
|
+
] extends [never] ? S : never
|
|
1108
|
+
|
|
1059
1109
|
/**
|
|
1060
1110
|
* Creates endpoint constructors for a specific HTTP method. The resulting
|
|
1061
1111
|
* constructor builds an `HttpApiEndpoint` from a name, path, and optional request
|
|
@@ -1073,7 +1123,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1073
1123
|
Query extends Schema.Top | Schema.Struct.Fields = never,
|
|
1074
1124
|
Payload extends PayloadConstraintCodecs<Method> = never,
|
|
1075
1125
|
Headers extends Schema.Top | Schema.Struct.Fields = never,
|
|
1076
|
-
const Success extends
|
|
1126
|
+
const Success extends SuccessConstraint = HttpApiSchema.NoContent,
|
|
1077
1127
|
const Error extends Schema.Top | ReadonlyArray<Schema.Top> = never
|
|
1078
1128
|
>(
|
|
1079
1129
|
name: Name,
|
|
@@ -1085,7 +1135,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1085
1135
|
readonly headers?: Headers | undefined
|
|
1086
1136
|
readonly payload?: Payload | undefined
|
|
1087
1137
|
readonly success?: Success | undefined
|
|
1088
|
-
readonly error?: Error | undefined
|
|
1138
|
+
readonly error?: ErrorWithoutStream<Error> | undefined
|
|
1089
1139
|
}
|
|
1090
1140
|
): HttpApiEndpoint<
|
|
1091
1141
|
Name,
|
|
@@ -1096,7 +1146,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1096
1146
|
Method extends HttpMethod.WithBody ? Json<ExtractSchemaOrArray<Payload>>
|
|
1097
1147
|
: StringTree<ExtractSchemaOrArray<Payload>>,
|
|
1098
1148
|
StringTree<Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers>,
|
|
1099
|
-
|
|
1149
|
+
JsonSuccessOrArray<Success>,
|
|
1100
1150
|
Json<Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error>
|
|
1101
1151
|
>
|
|
1102
1152
|
<
|
|
@@ -1118,7 +1168,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1118
1168
|
readonly headers?: Headers | undefined
|
|
1119
1169
|
readonly payload?: Payload | undefined
|
|
1120
1170
|
readonly success?: Success | undefined
|
|
1121
|
-
readonly error?: Error | undefined
|
|
1171
|
+
readonly error?: ErrorWithoutStream<Error> | undefined
|
|
1122
1172
|
}
|
|
1123
1173
|
): HttpApiEndpoint<
|
|
1124
1174
|
Name,
|
|
@@ -1128,7 +1178,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1128
1178
|
Query extends Schema.Struct.Fields ? Schema.Struct<Query> : Query,
|
|
1129
1179
|
ExtractSchemaOrArray<Payload>,
|
|
1130
1180
|
ExtractSchemaOrArray<Headers>,
|
|
1131
|
-
|
|
1181
|
+
ExtractSuccessOrArray<Success>,
|
|
1132
1182
|
Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error
|
|
1133
1183
|
>
|
|
1134
1184
|
} =>
|
|
@@ -1151,7 +1201,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1151
1201
|
readonly headers?: Headers | undefined
|
|
1152
1202
|
readonly payload?: Payload | undefined
|
|
1153
1203
|
readonly success?: Success | undefined
|
|
1154
|
-
readonly error?: Error | undefined
|
|
1204
|
+
readonly error?: ErrorWithoutStream<Error> | undefined
|
|
1155
1205
|
}
|
|
1156
1206
|
): HttpApiEndpoint<
|
|
1157
1207
|
Name,
|
|
@@ -1163,7 +1213,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1163
1213
|
: Payload extends ReadonlyArray<Schema.Top> ? Payload[number]
|
|
1164
1214
|
: Payload,
|
|
1165
1215
|
Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers,
|
|
1166
|
-
|
|
1216
|
+
ExtractSuccessOrArray<Success>,
|
|
1167
1217
|
Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error
|
|
1168
1218
|
> => {
|
|
1169
1219
|
const disableCodecs = options?.disableCodecs ?? false
|
|
@@ -1176,8 +1226,8 @@ export const make = <Method extends HttpMethod>(method: Method): {
|
|
|
1176
1226
|
query: ensureStruct(options?.query, transformStringTree),
|
|
1177
1227
|
headers: ensureStruct(options?.headers, transformStringTree),
|
|
1178
1228
|
payload: getPayload(options?.payload, method, disableCodecs),
|
|
1179
|
-
success:
|
|
1180
|
-
error:
|
|
1229
|
+
success: getSuccessResponse(options?.success, method, disableCodecs),
|
|
1230
|
+
error: getErrorResponse(options?.error, disableCodecs),
|
|
1181
1231
|
annotations: Context.empty(),
|
|
1182
1232
|
middlewares: new Set()
|
|
1183
1233
|
})
|
|
@@ -1257,13 +1307,154 @@ function getPayload(
|
|
|
1257
1307
|
return result
|
|
1258
1308
|
}
|
|
1259
1309
|
|
|
1260
|
-
|
|
1310
|
+
const reservedStreamFailureEvent = "effect/httpapi/stream/failure"
|
|
1311
|
+
|
|
1312
|
+
function getSuccessResponse(
|
|
1261
1313
|
success: Schema.Top | ReadonlyArray<Schema.Top> | undefined,
|
|
1314
|
+
method: HttpMethod,
|
|
1262
1315
|
disableCodecs: boolean
|
|
1263
1316
|
): Set<Schema.Top> {
|
|
1264
1317
|
if (success === undefined) return new Set()
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1318
|
+
const schemas = Arr.ensure(success)
|
|
1319
|
+
validateSuccessResponse(schemas, method)
|
|
1320
|
+
return new Set(
|
|
1321
|
+
disableCodecs ?
|
|
1322
|
+
schemas :
|
|
1323
|
+
schemas.map((schema) => HttpApiSchema.isStreamSchema(schema) ? schema : transformResponse(schema))
|
|
1324
|
+
)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function getErrorResponse(
|
|
1328
|
+
error: Schema.Top | ReadonlyArray<Schema.Top> | undefined,
|
|
1329
|
+
disableCodecs: boolean
|
|
1330
|
+
): Set<Schema.Top> {
|
|
1331
|
+
if (error === undefined) return new Set()
|
|
1332
|
+
const schemas = Arr.ensure(error)
|
|
1333
|
+
for (const schema of schemas) {
|
|
1334
|
+
if (HttpApiSchema.isStreamSchema(schema)) {
|
|
1335
|
+
throw new Error("Streaming schemas are not supported in error responses")
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return new Set(disableCodecs ? schemas : schemas.map(transformResponse))
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function validateSuccessResponse(schemas: ReadonlyArray<Schema.Top>, method: HttpMethod) {
|
|
1342
|
+
const statuses = new Map<number, {
|
|
1343
|
+
readonly stream?: HttpApiSchema.StreamSchema | undefined
|
|
1344
|
+
bufferedContentTypes: Set<string>
|
|
1345
|
+
noContent: boolean
|
|
1346
|
+
}>()
|
|
1347
|
+
|
|
1348
|
+
for (const schema of schemas) {
|
|
1349
|
+
if (HttpApiSchema.isStreamSchema(schema)) {
|
|
1350
|
+
validateStreamSuccess(schema, method)
|
|
1351
|
+
const status = HttpApiSchema.getStatusStream(schema)
|
|
1352
|
+
const entry = getStatusEntry(statuses, status)
|
|
1353
|
+
if (entry.stream !== undefined) {
|
|
1354
|
+
throw new Error(`Multiple streaming success responses for status: ${status}`)
|
|
1355
|
+
}
|
|
1356
|
+
if (entry.noContent) {
|
|
1357
|
+
throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`)
|
|
1358
|
+
}
|
|
1359
|
+
if (entry.bufferedContentTypes.has(normalizeResponseContentType(schema.contentType))) {
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
`Cannot combine buffered and streaming success responses for status ${status} and content-type: ${schema.contentType}`
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
statuses.set(status, { ...entry, stream: schema })
|
|
1365
|
+
} else {
|
|
1366
|
+
const status = HttpApiSchema.getStatusSuccess(schema.ast)
|
|
1367
|
+
const entry = getStatusEntry(statuses, status)
|
|
1368
|
+
const noContent = HttpApiSchema.isNoContent(schema.ast)
|
|
1369
|
+
if (entry.stream !== undefined) {
|
|
1370
|
+
if (noContent) {
|
|
1371
|
+
throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`)
|
|
1372
|
+
}
|
|
1373
|
+
const encoding = HttpApiSchema.getResponseEncoding(schema.ast)
|
|
1374
|
+
if (
|
|
1375
|
+
normalizeResponseContentType(encoding.contentType) === normalizeResponseContentType(entry.stream.contentType)
|
|
1376
|
+
) {
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`Cannot combine buffered and streaming success responses for status ${status} and content-type: ${encoding.contentType}`
|
|
1379
|
+
)
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (!noContent) {
|
|
1383
|
+
entry.bufferedContentTypes.add(
|
|
1384
|
+
normalizeResponseContentType(HttpApiSchema.getResponseEncoding(schema.ast).contentType)
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1387
|
+
entry.noContent = entry.noContent || noContent
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function normalizeResponseContentType(contentType: string): string {
|
|
1393
|
+
const normalized = contentType.toLowerCase().trim()
|
|
1394
|
+
const index = normalized.indexOf(";")
|
|
1395
|
+
return index === -1 ? normalized : normalized.slice(0, index).trim()
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function getStatusEntry(
|
|
1399
|
+
statuses: Map<number, {
|
|
1400
|
+
readonly stream?: HttpApiSchema.StreamSchema | undefined
|
|
1401
|
+
bufferedContentTypes: Set<string>
|
|
1402
|
+
noContent: boolean
|
|
1403
|
+
}>,
|
|
1404
|
+
status: number
|
|
1405
|
+
) {
|
|
1406
|
+
let entry = statuses.get(status)
|
|
1407
|
+
if (entry === undefined) {
|
|
1408
|
+
entry = { bufferedContentTypes: new Set(), noContent: false }
|
|
1409
|
+
statuses.set(status, entry)
|
|
1410
|
+
}
|
|
1411
|
+
return entry
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function validateStreamSuccess(schema: HttpApiSchema.StreamSchema, method: HttpMethod) {
|
|
1415
|
+
if (method === "HEAD") {
|
|
1416
|
+
throw new Error("HEAD endpoints cannot declare streaming success responses")
|
|
1417
|
+
}
|
|
1418
|
+
if (HttpApiSchema.isStreamSse(schema) && hasReservedSseEventName(schema.events.ast)) {
|
|
1419
|
+
throw new Error(`SSE event name is reserved: ${reservedStreamFailureEvent}`)
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function hasReservedSseEventName(ast: AST.AST): boolean {
|
|
1424
|
+
return hasReservedEventName(AST.toEncoded(ast), new Set())
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function hasReservedEventName(ast: AST.AST, seen: Set<AST.AST>): boolean {
|
|
1428
|
+
if (seen.has(ast)) return false
|
|
1429
|
+
seen.add(ast)
|
|
1430
|
+
if (AST.isUnion(ast)) {
|
|
1431
|
+
return ast.types.some((type) => hasReservedEventName(type, seen))
|
|
1432
|
+
}
|
|
1433
|
+
if (AST.isSuspend(ast)) {
|
|
1434
|
+
return hasReservedEventName(ast.thunk(), seen)
|
|
1435
|
+
}
|
|
1436
|
+
if (!AST.isObjects(ast)) return false
|
|
1437
|
+
const event = ast.propertySignatures.find((ps) => ps.name === "event")
|
|
1438
|
+
return event !== undefined && hasReservedEventLiteral(event.type, seen)
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function hasReservedEventLiteral(ast: AST.AST, seen: Set<AST.AST>): boolean {
|
|
1442
|
+
if (seen.has(ast)) return false
|
|
1443
|
+
seen.add(ast)
|
|
1444
|
+
const encoded = AST.toEncoded(ast)
|
|
1445
|
+
if (encoded !== ast) {
|
|
1446
|
+
return hasReservedEventLiteral(encoded, seen)
|
|
1447
|
+
}
|
|
1448
|
+
if (AST.isLiteral(ast)) {
|
|
1449
|
+
return ast.literal === reservedStreamFailureEvent
|
|
1450
|
+
}
|
|
1451
|
+
if (AST.isUnion(ast)) {
|
|
1452
|
+
return ast.types.some((type) => hasReservedEventLiteral(type, seen))
|
|
1453
|
+
}
|
|
1454
|
+
if (AST.isSuspend(ast)) {
|
|
1455
|
+
return hasReservedEventLiteral(ast.thunk(), seen)
|
|
1456
|
+
}
|
|
1457
|
+
return false
|
|
1267
1458
|
}
|
|
1268
1459
|
|
|
1269
1460
|
function transformResponse(schema: Schema.Top): Schema.Top {
|
|
@@ -57,7 +57,7 @@ export interface HttpApiGroup<
|
|
|
57
57
|
/**
|
|
58
58
|
* Add an `HttpApiEndpoint` to an `HttpApiGroup`.
|
|
59
59
|
*/
|
|
60
|
-
add<A extends NonEmptyReadonlyArray<HttpApiEndpoint.Any>>(
|
|
60
|
+
add<const A extends NonEmptyReadonlyArray<HttpApiEndpoint.Any>>(
|
|
61
61
|
...endpoints: A
|
|
62
62
|
): HttpApiGroup<Id, Endpoints | A[number], TopLevel>
|
|
63
63
|
|