effect-app 0.152.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +11 -0
- package/.prettierignore +6 -0
- package/CHANGELOG.md +4106 -0
- package/_cjs/Config/SecretURL.cjs +58 -0
- package/_cjs/Config/SecretURL.cjs.map +1 -0
- package/_cjs/Config/internal/configSecretURL.cjs +88 -0
- package/_cjs/Config/internal/configSecretURL.cjs.map +1 -0
- package/_cjs/Inputify.type.cjs +6 -0
- package/_cjs/Inputify.type.cjs.map +1 -0
- package/_cjs/Operations.cjs +76 -0
- package/_cjs/Operations.cjs.map +1 -0
- package/_cjs/Pure.cjs +201 -0
- package/_cjs/Pure.cjs.map +1 -0
- package/_cjs/Request.cjs +76 -0
- package/_cjs/Request.cjs.map +1 -0
- package/_cjs/Widen.type.cjs +6 -0
- package/_cjs/Widen.type.cjs.map +1 -0
- package/_cjs/_ext/date.cjs +64 -0
- package/_cjs/_ext/date.cjs.map +1 -0
- package/_cjs/_ext/misc.cjs +121 -0
- package/_cjs/_ext/misc.cjs.map +1 -0
- package/_cjs/_global.cjs +24 -0
- package/_cjs/_global.cjs.map +1 -0
- package/_cjs/_global.ext.cjs +5 -0
- package/_cjs/_global.ext.cjs.map +1 -0
- package/_cjs/_global.schema.cjs +4 -0
- package/_cjs/_global.schema.cjs.map +1 -0
- package/_cjs/client/QueryResult.cjs +116 -0
- package/_cjs/client/QueryResult.cjs.map +1 -0
- package/_cjs/client/clientFor.cjs +159 -0
- package/_cjs/client/clientFor.cjs.map +1 -0
- package/_cjs/client/config.cjs +21 -0
- package/_cjs/client/config.cjs.map +1 -0
- package/_cjs/client/errors.cjs +116 -0
- package/_cjs/client/errors.cjs.map +1 -0
- package/_cjs/client/fetch.cjs +178 -0
- package/_cjs/client/fetch.cjs.map +1 -0
- package/_cjs/client.cjs +61 -0
- package/_cjs/client.cjs.map +1 -0
- package/_cjs/faker.cjs +31 -0
- package/_cjs/faker.cjs.map +1 -0
- package/_cjs/ids.cjs +24 -0
- package/_cjs/ids.cjs.map +1 -0
- package/_cjs/index.cjs +27 -0
- package/_cjs/index.cjs.map +1 -0
- package/_cjs/refinements.cjs +97 -0
- package/_cjs/refinements.cjs.map +1 -0
- package/_cjs/schema.cjs +50 -0
- package/_cjs/schema.cjs.map +1 -0
- package/_cjs/schema.test.cjs +9 -0
- package/_cjs/schema.test.cjs.map +1 -0
- package/_cjs/service.cjs +97 -0
- package/_cjs/service.cjs.map +1 -0
- package/_cjs/utils.cjs +17 -0
- package/_cjs/utils.cjs.map +1 -0
- package/_src/Config/SecretURL.ts +103 -0
- package/_src/Config/internal/configSecretURL.ts +85 -0
- package/_src/Inputify.type.ts +13 -0
- package/_src/Operations.ts +70 -0
- package/_src/Pure.ts +525 -0
- package/_src/Request.ts +106 -0
- package/_src/Widen.type.ts +28 -0
- package/_src/_ext/date.ts +84 -0
- package/_src/_ext/misc.ts +161 -0
- package/_src/_global/stm.ts.bak +35 -0
- package/_src/_global.ext.ts +3 -0
- package/_src/_global.schema.ts +106 -0
- package/_src/_global.ts +119 -0
- package/_src/client/QueryResult.ts +120 -0
- package/_src/client/clientFor.ts +260 -0
- package/_src/client/config.ts +13 -0
- package/_src/client/errors.ts +129 -0
- package/_src/client/fetch.ts +253 -0
- package/_src/client.ts +7 -0
- package/_src/faker.ts +32 -0
- package/_src/ids.ts +35 -0
- package/_src/index.ts +4 -0
- package/_src/refinements.ts +92 -0
- package/_src/schema/_schema.ts.bak +208 -0
- package/_src/schema/api/date.ts.bak +78 -0
- package/_src/schema/api.ts.bak +20 -0
- package/_src/schema/overrides.ts.bak +76 -0
- package/_src/schema/shared.ts.bak +334 -0
- package/_src/schema.test.ts +3 -0
- package/_src/schema.ts +37 -0
- package/_src/service.ts +119 -0
- package/_src/utils.ts +1 -0
- package/dist/Config/SecretURL.d.ts +82 -0
- package/dist/Config/SecretURL.d.ts.map +1 -0
- package/dist/Config/SecretURL.js +49 -0
- package/dist/Config/internal/configSecretURL.d.ts +24 -0
- package/dist/Config/internal/configSecretURL.d.ts.map +1 -0
- package/dist/Config/internal/configSecretURL.js +75 -0
- package/dist/Inputify.type.d.ts +10 -0
- package/dist/Inputify.type.d.ts.map +1 -0
- package/dist/Inputify.type.js +2 -0
- package/dist/Operations.d.ts +170 -0
- package/dist/Operations.d.ts.map +1 -0
- package/dist/Operations.js +87 -0
- package/dist/Pure.d.ts +169 -0
- package/dist/Pure.d.ts.map +1 -0
- package/dist/Pure.js +167 -0
- package/dist/Request.d.ts +49 -0
- package/dist/Request.d.ts.map +1 -0
- package/dist/Request.js +58 -0
- package/dist/Widen.type.d.ts +19 -0
- package/dist/Widen.type.d.ts.map +1 -0
- package/dist/Widen.type.js +2 -0
- package/dist/_ext/date.d.ts +71 -0
- package/dist/_ext/date.d.ts.map +1 -0
- package/dist/_ext/date.js +58 -0
- package/dist/_ext/misc.d.ts +77 -0
- package/dist/_ext/misc.d.ts.map +1 -0
- package/dist/_ext/misc.js +98 -0
- package/dist/_global.d.ts +70 -0
- package/dist/_global.d.ts.map +1 -0
- package/dist/_global.ext.d.ts +3 -0
- package/dist/_global.ext.d.ts.map +1 -0
- package/dist/_global.ext.js +4 -0
- package/dist/_global.js +76 -0
- package/dist/_global.schema.d.ts +6 -0
- package/dist/_global.schema.d.ts.map +1 -0
- package/dist/_global.schema.js +6 -0
- package/dist/client/QueryResult.d.ts +85 -0
- package/dist/client/QueryResult.d.ts.map +1 -0
- package/dist/client/QueryResult.js +85 -0
- package/dist/client/clientFor.d.ts +44 -0
- package/dist/client/clientFor.d.ts.map +1 -0
- package/dist/client/clientFor.js +144 -0
- package/dist/client/config.d.ts +14 -0
- package/dist/client/config.d.ts.map +1 -0
- package/dist/client/config.js +11 -0
- package/dist/client/errors.d.ts +206 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +130 -0
- package/dist/client/fetch.d.ts +61 -0
- package/dist/client/fetch.d.ts.map +1 -0
- package/dist/client/fetch.js +127 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +7 -0
- package/dist/faker.d.ts +7 -0
- package/dist/faker.d.ts.map +1 -0
- package/dist/faker.js +24 -0
- package/dist/ids.d.ts +32 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +17 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/refinements.d.ts +57 -0
- package/dist/refinements.d.ts.map +1 -0
- package/dist/refinements.js +85 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +22 -0
- package/dist/schema.test.d.ts.map +1 -0
- package/dist/service.d.ts +47 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +83 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +2 -0
- package/package.json +315 -0
- package/tsconfig.json +114 -0
- package/tsconfig.json.bak +47 -0
- package/tsplus.config.json +7 -0
- package/vitest.config.ts +5 -0
- package/wallaby.cjs +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
|
|
4
|
+
import { REST } from "effect-app/schema"
|
|
5
|
+
import * as utils from "effect-app/utils"
|
|
6
|
+
import { Path } from "path-parser"
|
|
7
|
+
import type { ApiConfig } from "./config.js"
|
|
8
|
+
import type { SupportedErrors } from "./errors.js"
|
|
9
|
+
import type { FetchError, FetchResponse } from "./fetch.js"
|
|
10
|
+
import {
|
|
11
|
+
fetchApi,
|
|
12
|
+
fetchApi3S,
|
|
13
|
+
fetchApi3SE,
|
|
14
|
+
makePathWithBody,
|
|
15
|
+
makePathWithQuery,
|
|
16
|
+
mapResponseM,
|
|
17
|
+
ResponseError
|
|
18
|
+
} from "./fetch.js"
|
|
19
|
+
|
|
20
|
+
export * from "./config.js"
|
|
21
|
+
|
|
22
|
+
type Requests = Record<string, any>
|
|
23
|
+
type AnyRequest =
|
|
24
|
+
& Omit<
|
|
25
|
+
REST.QueryRequest<any, any, any, any, any, any>,
|
|
26
|
+
"method"
|
|
27
|
+
>
|
|
28
|
+
& REST.RequestSchemed<any, any>
|
|
29
|
+
|
|
30
|
+
const cache = new Map<any, Client<any>>()
|
|
31
|
+
|
|
32
|
+
export type Client<M extends Requests> =
|
|
33
|
+
& RequestHandlers<
|
|
34
|
+
ApiConfig | HttpClient.Default,
|
|
35
|
+
SupportedErrors | FetchError | ResponseError,
|
|
36
|
+
M
|
|
37
|
+
>
|
|
38
|
+
& RequestHandlersE<
|
|
39
|
+
ApiConfig | HttpClient.Default,
|
|
40
|
+
SupportedErrors | FetchError | ResponseError,
|
|
41
|
+
M
|
|
42
|
+
>
|
|
43
|
+
|
|
44
|
+
export function clientFor<M extends Requests>(
|
|
45
|
+
models: M
|
|
46
|
+
): Client<Omit<M, "meta">> {
|
|
47
|
+
const found = cache.get(models)
|
|
48
|
+
if (found) {
|
|
49
|
+
return found
|
|
50
|
+
}
|
|
51
|
+
const m = clientFor_(models)
|
|
52
|
+
cache.set(models, m)
|
|
53
|
+
return m
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clientFor_<M extends Requests>(models: M) {
|
|
57
|
+
return (models
|
|
58
|
+
.$$
|
|
59
|
+
.keys
|
|
60
|
+
// ignore module interop with automatic default exports..
|
|
61
|
+
.filter((x) => x !== "default" && x !== "meta")
|
|
62
|
+
.reduce((prev, cur) => {
|
|
63
|
+
const h = models[cur]
|
|
64
|
+
|
|
65
|
+
const Request_ = REST.extractRequest(h) as AnyRequest
|
|
66
|
+
const Response = REST.extractResponse(h)
|
|
67
|
+
|
|
68
|
+
// @ts-expect-error doc
|
|
69
|
+
const actionName = utils.uncapitalize(cur)
|
|
70
|
+
const m = (models as any).meta as { moduleName: string }
|
|
71
|
+
if (!m) throw new Error("No meta defined in Resource!")
|
|
72
|
+
const requestName = `${m.moduleName}.${cur as string}`
|
|
73
|
+
.replaceAll(".js", "")
|
|
74
|
+
|
|
75
|
+
const Request = class extends (Request_ as any) {
|
|
76
|
+
static path = "/" + requestName + (Request_.path === "/" ? "" : Request_.path)
|
|
77
|
+
static method = Request_.method as REST.SupportedMethods === "AUTO"
|
|
78
|
+
? REST.determineMethod(cur as string, Request_)
|
|
79
|
+
: Request_.method
|
|
80
|
+
} as unknown as AnyRequest
|
|
81
|
+
|
|
82
|
+
if ((Request_ as any).method === "AUTO") {
|
|
83
|
+
Object.assign(Request, {
|
|
84
|
+
[Request.method === "GET" || Request.method === "DELETE" ? "Query" : "Body"]: (Request_ as any).Auto
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const b = Object.assign({}, h, { Request, Response })
|
|
89
|
+
|
|
90
|
+
const meta = {
|
|
91
|
+
Request,
|
|
92
|
+
Response,
|
|
93
|
+
mapPath: Request.path
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const res = Response as Schema<any>
|
|
97
|
+
const parseResponse = flow(res.decodeUnknown, (_) => _.mapError((err) => new ResponseError(err)))
|
|
98
|
+
|
|
99
|
+
const parseResponseE = flow(parseResponse, (x) => x.andThen(res.encode))
|
|
100
|
+
|
|
101
|
+
const path = new Path(Request.path)
|
|
102
|
+
|
|
103
|
+
// TODO: look into ast, look for propertySignatures, etc.
|
|
104
|
+
// TODO: and fix type wise
|
|
105
|
+
// if we don't need fields, then also dont require an argument.
|
|
106
|
+
const fields = [Request.Body, Request.Query, Request.Path]
|
|
107
|
+
.filter((x) => x)
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
109
|
+
.flatMap((x) => x.ast.propertySignatures)
|
|
110
|
+
// @ts-expect-error doc
|
|
111
|
+
prev[actionName] = Request.method === "GET"
|
|
112
|
+
? fields.length === 0
|
|
113
|
+
? {
|
|
114
|
+
handler: fetchApi(Request.method, Request.path)
|
|
115
|
+
.flatMap(mapResponseM(parseResponse))
|
|
116
|
+
.withSpan("client.request", {
|
|
117
|
+
attributes: { "request.name": requestName }
|
|
118
|
+
}),
|
|
119
|
+
...meta
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
handler: (req: any) =>
|
|
123
|
+
fetchApi(Request.method, makePathWithQuery(path, Request.encodeSync(req)))
|
|
124
|
+
.flatMap(mapResponseM(parseResponse))
|
|
125
|
+
.withSpan("client.request", {
|
|
126
|
+
attributes: { "request.name": requestName }
|
|
127
|
+
}),
|
|
128
|
+
...meta,
|
|
129
|
+
mapPath: (req: any) => req ? makePathWithQuery(path, Request.encodeSync(req)) : Request.path
|
|
130
|
+
}
|
|
131
|
+
: fields.length === 0
|
|
132
|
+
? {
|
|
133
|
+
handler: fetchApi3S(b)({}).withSpan("client.request", {
|
|
134
|
+
attributes: { "request.name": requestName }
|
|
135
|
+
}),
|
|
136
|
+
...meta
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
handler: (req: any) =>
|
|
140
|
+
fetchApi3S(b)(req).withSpan("client.request", {
|
|
141
|
+
attributes: { "request.name": requestName }
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
...meta,
|
|
145
|
+
mapPath: (req: any) =>
|
|
146
|
+
req
|
|
147
|
+
? Request.method === "DELETE"
|
|
148
|
+
? makePathWithQuery(path, Request.encodeSync(req))
|
|
149
|
+
: makePathWithBody(path, Request.encodeSync(req))
|
|
150
|
+
: Request.path
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// generate handler
|
|
154
|
+
|
|
155
|
+
// @ts-expect-error doc
|
|
156
|
+
prev[`${actionName}E`] = Request.method === "GET"
|
|
157
|
+
? fields.length === 0
|
|
158
|
+
? {
|
|
159
|
+
handler: fetchApi(Request.method, Request.path)
|
|
160
|
+
.flatMap(mapResponseM(parseResponseE))
|
|
161
|
+
.withSpan("client.request", {
|
|
162
|
+
attributes: { "request.name": requestName }
|
|
163
|
+
}),
|
|
164
|
+
...meta
|
|
165
|
+
}
|
|
166
|
+
: {
|
|
167
|
+
handler: (req: any) =>
|
|
168
|
+
fetchApi(Request.method, makePathWithQuery(path, Request.encodeSync(req)))
|
|
169
|
+
.flatMap(mapResponseM(parseResponseE))
|
|
170
|
+
.withSpan("client.request", {
|
|
171
|
+
attributes: { "request.name": requestName }
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
...meta,
|
|
175
|
+
mapPath: (req: any) => req ? makePathWithQuery(path, Request.encodeSync(req)) : Request.path
|
|
176
|
+
}
|
|
177
|
+
: fields.length === 0
|
|
178
|
+
? {
|
|
179
|
+
handler: fetchApi3SE(b)({}).withSpan("client.request", {
|
|
180
|
+
attributes: { "request.name": requestName }
|
|
181
|
+
}),
|
|
182
|
+
...meta
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
handler: (req: any) =>
|
|
186
|
+
fetchApi3SE(b)(req).withSpan("client.request", {
|
|
187
|
+
attributes: { "request.name": requestName }
|
|
188
|
+
}),
|
|
189
|
+
|
|
190
|
+
...meta,
|
|
191
|
+
mapPath: (req: any) =>
|
|
192
|
+
req
|
|
193
|
+
? Request.method === "DELETE"
|
|
194
|
+
? makePathWithQuery(path, Request.encodeSync(req))
|
|
195
|
+
: makePathWithBody(path, Request.encodeSync(req))
|
|
196
|
+
: Request.path
|
|
197
|
+
}
|
|
198
|
+
// generate handler
|
|
199
|
+
|
|
200
|
+
return prev
|
|
201
|
+
}, {} as Client<M>))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type ExtractResponse<T> = T extends Schema<any, any, any> ? Schema.To<T>
|
|
205
|
+
: T extends unknown ? void
|
|
206
|
+
: never
|
|
207
|
+
|
|
208
|
+
export type ExtractEResponse<T> = T extends Schema<any, any, any> ? Schema.From<T>
|
|
209
|
+
: T extends unknown ? void
|
|
210
|
+
: never
|
|
211
|
+
|
|
212
|
+
type HasEmptyTo<T extends Schema<any, any, any>> = T extends { struct: Schema<any, any, any> }
|
|
213
|
+
? Schema.To<T["struct"]> extends Record<any, never> ? true : Schema.To<T> extends Record<any, never> ? true : false
|
|
214
|
+
: false
|
|
215
|
+
|
|
216
|
+
type RequestHandlers<R, E, M extends Requests> = {
|
|
217
|
+
[K in keyof M & string as Uncapitalize<K>]: HasEmptyTo<REST.GetRequest<M[K]>> extends true ? {
|
|
218
|
+
handler: Effect<FetchResponse<ExtractResponse<REST.GetResponse<M[K]>>>, E, R>
|
|
219
|
+
Request: REST.GetRequest<M[K]>
|
|
220
|
+
Reponse: ExtractResponse<REST.GetResponse<M[K]>>
|
|
221
|
+
mapPath: string
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
handler: (
|
|
225
|
+
req: InstanceType<REST.GetRequest<M[K]>>
|
|
226
|
+
) => Effect<
|
|
227
|
+
FetchResponse<ExtractResponse<REST.GetResponse<M[K]>>>,
|
|
228
|
+
E,
|
|
229
|
+
R
|
|
230
|
+
>
|
|
231
|
+
Request: REST.GetRequest<M[K]>
|
|
232
|
+
Reponse: ExtractResponse<REST.GetResponse<M[K]>>
|
|
233
|
+
mapPath: (req: InstanceType<REST.GetRequest<M[K]>>) => string
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type RequestHandlersE<R, E, M extends Requests> = {
|
|
238
|
+
[K in keyof M & string as `${Uncapitalize<K>}E`]: HasEmptyTo<REST.GetRequest<M[K]>> extends true ? {
|
|
239
|
+
handler: Effect<
|
|
240
|
+
FetchResponse<ExtractEResponse<REST.GetResponse<M[K]>>>,
|
|
241
|
+
E,
|
|
242
|
+
R
|
|
243
|
+
>
|
|
244
|
+
Request: REST.GetRequest<M[K]>
|
|
245
|
+
Reponse: ExtractResponse<REST.GetResponse<M[K]>>
|
|
246
|
+
mapPath: string
|
|
247
|
+
}
|
|
248
|
+
: {
|
|
249
|
+
handler: (
|
|
250
|
+
req: InstanceType<REST.GetRequest<M[K]>>
|
|
251
|
+
) => Effect<
|
|
252
|
+
FetchResponse<ExtractEResponse<REST.GetResponse<M[K]>>>,
|
|
253
|
+
E,
|
|
254
|
+
R
|
|
255
|
+
>
|
|
256
|
+
Request: REST.GetRequest<M[K]>
|
|
257
|
+
Reponse: ExtractResponse<REST.GetResponse<M[K]>>
|
|
258
|
+
mapPath: (req: InstanceType<REST.GetRequest<M[K]>>) => string
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ApiConfig {
|
|
2
|
+
apiUrl: string
|
|
3
|
+
headers: Option<HashMap<string, string>>
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const tag = GenericTag<ApiConfig>("@services/tag")
|
|
7
|
+
export const layer = (config: ApiConfig) => tag.makeLayer(config)
|
|
8
|
+
export const ApiConfig = {
|
|
9
|
+
Tag: tag,
|
|
10
|
+
layer
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getConfig = <R, E, A>(self: (cfg: ApiConfig) => Effect<A, E, R>) => tag.flatMap(self)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { TaggedError } from "effect-app/schema"
|
|
2
|
+
|
|
3
|
+
/** @tsplus type NotFoundError */
|
|
4
|
+
@useClassFeaturesForSchema
|
|
5
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
6
|
+
// @ts-expect-error type not used
|
|
7
|
+
export class NotFoundError<ItemType = string> extends TaggedError<NotFoundError<ItemType>>()("NotFoundError", {
|
|
8
|
+
type: string,
|
|
9
|
+
id: unknown
|
|
10
|
+
}) {
|
|
11
|
+
override get message() {
|
|
12
|
+
return `Didn't find ${this.type}#${JSON.stringify(this.id)}`
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @tsplus type InvalidStateError */
|
|
17
|
+
@useClassFeaturesForSchema
|
|
18
|
+
export class InvalidStateError extends TaggedError<InvalidStateError>()("InvalidStateError", {
|
|
19
|
+
message: string
|
|
20
|
+
}) {
|
|
21
|
+
constructor(messageOrObject: string | { message: string }, disableValidation = false) {
|
|
22
|
+
super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, disableValidation)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @tsplus type ValidationError */
|
|
27
|
+
@useClassFeaturesForSchema
|
|
28
|
+
export class ValidationError extends TaggedError<ValidationError>()("ValidationError", {
|
|
29
|
+
errors: array(unknown) // meh
|
|
30
|
+
}) {
|
|
31
|
+
override get message() {
|
|
32
|
+
return `Validation failed: ${this.errors.map((e) => JSON.stringify(e)).join(", ")}`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @tsplus type NotLoggedInError */
|
|
37
|
+
@useClassFeaturesForSchema
|
|
38
|
+
export class NotLoggedInError extends TaggedError<NotLoggedInError>()("NotLoggedInError", {
|
|
39
|
+
message: string.optional()
|
|
40
|
+
}) {
|
|
41
|
+
constructor(messageOrObject?: string | { message?: string }, disableValidation = false) {
|
|
42
|
+
super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, disableValidation)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The user carries a valid Userprofile, but there is a problem with the login none the less.
|
|
48
|
+
*/
|
|
49
|
+
/** @tsplus type LoginError */
|
|
50
|
+
@useClassFeaturesForSchema
|
|
51
|
+
export class LoginError extends TaggedError<LoginError>()("NotLoggedInError", {
|
|
52
|
+
message: string.optional()
|
|
53
|
+
}) {
|
|
54
|
+
constructor(messageOrObject?: string | { message?: string }, disableValidation = false) {
|
|
55
|
+
super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, disableValidation)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @tsplus type UnauthorizedError */
|
|
60
|
+
@useClassFeaturesForSchema
|
|
61
|
+
export class UnauthorizedError extends TaggedError<UnauthorizedError>()("UnauthorizedError", {
|
|
62
|
+
message: string.optional()
|
|
63
|
+
}) {
|
|
64
|
+
constructor(messageOrObject?: string | { message?: string }, disableValidation = false) {
|
|
65
|
+
super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject }, disableValidation)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type OptimisticConcurrencyDetails = {
|
|
70
|
+
readonly type: string
|
|
71
|
+
readonly id: string
|
|
72
|
+
readonly current?: string | undefined
|
|
73
|
+
readonly found?: string | undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @tsplus type OptimisticConcurrencyException */
|
|
77
|
+
@useClassFeaturesForSchema
|
|
78
|
+
export class OptimisticConcurrencyException extends TaggedError<OptimisticConcurrencyException>()(
|
|
79
|
+
"OptimisticConcurrencyException",
|
|
80
|
+
{ message: string }
|
|
81
|
+
) {
|
|
82
|
+
readonly details?: OptimisticConcurrencyDetails
|
|
83
|
+
constructor(
|
|
84
|
+
args: OptimisticConcurrencyDetails | { message: string },
|
|
85
|
+
disableValidation = false
|
|
86
|
+
) {
|
|
87
|
+
super("message" in args ? args : { message: `Existing ${args.type} ${args.id} record changed` }, disableValidation)
|
|
88
|
+
if (!("message" in args)) {
|
|
89
|
+
this.details = args
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const MutationOnlyErrors = [
|
|
95
|
+
InvalidStateError,
|
|
96
|
+
OptimisticConcurrencyException
|
|
97
|
+
] as const
|
|
98
|
+
|
|
99
|
+
const GeneralErrors = [
|
|
100
|
+
NotFoundError,
|
|
101
|
+
NotLoggedInError,
|
|
102
|
+
LoginError,
|
|
103
|
+
UnauthorizedError,
|
|
104
|
+
ValidationError
|
|
105
|
+
] as const
|
|
106
|
+
|
|
107
|
+
export const SupportedErrors = union(
|
|
108
|
+
...MutationOnlyErrors,
|
|
109
|
+
...GeneralErrors
|
|
110
|
+
)
|
|
111
|
+
// .pipe(named("SupportedErrors"))
|
|
112
|
+
// .pipe(withDefaults)
|
|
113
|
+
export type SupportedErrors = Schema.To<typeof SupportedErrors>
|
|
114
|
+
|
|
115
|
+
// ideal?
|
|
116
|
+
// export const QueryErrors = union({ ...GeneralErrors })
|
|
117
|
+
// .pipe(named("QueryErrors"))
|
|
118
|
+
// .pipe(withDefaults)
|
|
119
|
+
// export type QueryErrors = Schema.To<typeof QueryErrors>
|
|
120
|
+
// export const MutationErrors = union({ ...GeneralErrors, ...GeneralErrors })
|
|
121
|
+
// .pipe(named("MutationErrors"))
|
|
122
|
+
// .pipe(withDefaults)
|
|
123
|
+
|
|
124
|
+
// export type MutationErrors = Schema.To<typeof MutationErrors>
|
|
125
|
+
|
|
126
|
+
export const MutationErrors = SupportedErrors
|
|
127
|
+
export const QueryErrors = SupportedErrors
|
|
128
|
+
export type MutationErrors = Schema.To<typeof MutationErrors>
|
|
129
|
+
export type QueryErrors = Schema.To<typeof QueryErrors>
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { constant } from "@effect-app/core/Function"
|
|
3
|
+
import type { Headers, HttpError, HttpRequestError, HttpResponseError, Method } from "@effect-app/core/http/http-client"
|
|
4
|
+
import type { REST } from "effect-app/schema"
|
|
5
|
+
import { Path } from "path-parser"
|
|
6
|
+
import qs from "query-string"
|
|
7
|
+
import { ApiConfig } from "./config.js"
|
|
8
|
+
import {
|
|
9
|
+
InvalidStateError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
NotLoggedInError,
|
|
12
|
+
OptimisticConcurrencyException,
|
|
13
|
+
UnauthorizedError,
|
|
14
|
+
ValidationError
|
|
15
|
+
} from "./errors.js"
|
|
16
|
+
|
|
17
|
+
export type FetchError = HttpError<string>
|
|
18
|
+
|
|
19
|
+
export class ResponseError {
|
|
20
|
+
public readonly _tag = "ResponseError"
|
|
21
|
+
constructor(public readonly error: unknown) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getClient = HttpClient.flatMap((defaultClient) =>
|
|
25
|
+
ApiConfig
|
|
26
|
+
.Tag
|
|
27
|
+
.map(({ apiUrl, headers }) =>
|
|
28
|
+
defaultClient
|
|
29
|
+
.filterStatusOk
|
|
30
|
+
.mapRequest((_) =>
|
|
31
|
+
_
|
|
32
|
+
.acceptJson
|
|
33
|
+
.prependUrl(apiUrl)
|
|
34
|
+
.setHeaders({
|
|
35
|
+
"request-id": headers.flatMap((_) => _.get("request-id")).value ?? StringId.make(),
|
|
36
|
+
...headers.map((_) => Object.fromEntries(_)).value
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
.tapRequest((r) =>
|
|
40
|
+
Effect
|
|
41
|
+
.logDebug(`[HTTP] ${r.method}`)
|
|
42
|
+
.annotateLogs("url", r.url)
|
|
43
|
+
.annotateLogs("body", r.body._tag === "Uint8Array" ? new TextDecoder().decode(r.body.body) : r.body._tag)
|
|
44
|
+
.annotateLogs("headers", r.headers)
|
|
45
|
+
)
|
|
46
|
+
.mapEffect((_) =>
|
|
47
|
+
_.status === 204
|
|
48
|
+
? Effect.sync(() => ({ status: _.status, body: void 0, headers: _.headers }))
|
|
49
|
+
: _.json.map((body) => ({ status: _.status, body, headers: _.headers }))
|
|
50
|
+
)
|
|
51
|
+
.catchTag(
|
|
52
|
+
"ResponseError",
|
|
53
|
+
(err) => {
|
|
54
|
+
const toError = <R, From, To>(s: Schema<To, From, R>) =>
|
|
55
|
+
err.response.json.flatMap((_) => s.decodeUnknown(_).catchAll(() => Effect.fail(err))).flatMap(Effect.fail)
|
|
56
|
+
|
|
57
|
+
// opposite of api's `defaultErrorHandler`
|
|
58
|
+
if (err.response.status === 404) {
|
|
59
|
+
return toError(NotFoundError)
|
|
60
|
+
}
|
|
61
|
+
if (err.response.status === 400) {
|
|
62
|
+
return toError(ValidationError)
|
|
63
|
+
}
|
|
64
|
+
if (err.response.status === 401) {
|
|
65
|
+
return toError(NotLoggedInError)
|
|
66
|
+
}
|
|
67
|
+
if (err.response.status === 422) {
|
|
68
|
+
return toError(InvalidStateError)
|
|
69
|
+
}
|
|
70
|
+
if (err.response.status === 403) {
|
|
71
|
+
return toError(UnauthorizedError)
|
|
72
|
+
}
|
|
73
|
+
if (err.response.status === 412) {
|
|
74
|
+
return toError(OptimisticConcurrencyException)
|
|
75
|
+
}
|
|
76
|
+
return Effect.fail(err)
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
.catchTags({
|
|
80
|
+
"ResponseError": (err) =>
|
|
81
|
+
err
|
|
82
|
+
.response
|
|
83
|
+
.text
|
|
84
|
+
// TODO
|
|
85
|
+
.orDie
|
|
86
|
+
.flatMap((_) =>
|
|
87
|
+
Effect.fail({
|
|
88
|
+
_tag: "HttpErrorResponse" as const,
|
|
89
|
+
response: { body: Option.fromNullable(_), status: err.response.status, headers: err.response.headers }
|
|
90
|
+
} as HttpResponseError<unknown>)
|
|
91
|
+
),
|
|
92
|
+
"RequestError": (err) => Effect.fail({ _tag: "HttpErrorRequest", error: err.error } as HttpRequestError)
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export function fetchApi(
|
|
98
|
+
method: Method,
|
|
99
|
+
path: string,
|
|
100
|
+
body?: unknown
|
|
101
|
+
) {
|
|
102
|
+
return getClient
|
|
103
|
+
.flatMap((client) =>
|
|
104
|
+
(method === "GET"
|
|
105
|
+
? client.request(ClientRequest.make(method)(path))
|
|
106
|
+
: body === undefined
|
|
107
|
+
? client.request(
|
|
108
|
+
ClientRequest
|
|
109
|
+
.make(method)(path)
|
|
110
|
+
)
|
|
111
|
+
: client.request(
|
|
112
|
+
ClientRequest
|
|
113
|
+
.make(method)(path)
|
|
114
|
+
.jsonBody(body)
|
|
115
|
+
))
|
|
116
|
+
.withSpan("http.request", { attributes: { "http.method": method, "http.url": path } })
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function fetchApi2S<RequestR, RequestFrom, RequestTo, ResponseR, ResponseFrom, ResponseTo>(
|
|
121
|
+
request: Schema<RequestTo, RequestFrom, RequestR>,
|
|
122
|
+
response: Schema<ResponseTo, ResponseFrom, ResponseR>
|
|
123
|
+
) {
|
|
124
|
+
const encodeRequest = request.encode
|
|
125
|
+
const decRes = response.decodeUnknown
|
|
126
|
+
const decodeRes = (u: unknown) => decRes(u).mapError((err) => new ResponseError(err))
|
|
127
|
+
return (method: Method, path: Path) => (req: RequestTo) => {
|
|
128
|
+
return encodeRequest(req).andThen((encoded) =>
|
|
129
|
+
fetchApi(
|
|
130
|
+
method,
|
|
131
|
+
method === "DELETE"
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
133
|
+
? makePathWithQuery(path, encoded as any)
|
|
134
|
+
: makePathWithBody(path, encoded as any),
|
|
135
|
+
encoded
|
|
136
|
+
)
|
|
137
|
+
.flatMap(mapResponseM(decodeRes))
|
|
138
|
+
.map((i) => ({
|
|
139
|
+
...i,
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
141
|
+
body: i.body as ResponseTo
|
|
142
|
+
}))
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function fetchApi3S<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
|
|
148
|
+
Request,
|
|
149
|
+
Response
|
|
150
|
+
}: {
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
152
|
+
Request: REST.RequestSchemed<RequestA, RequestE>
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
154
|
+
Response: REST.ReqRes<ResponseA, ResponseE, any>
|
|
155
|
+
}) {
|
|
156
|
+
return fetchApi2S(Request, Response)(
|
|
157
|
+
Request.method,
|
|
158
|
+
new Path(Request.path)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function fetchApi3SE<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
|
|
163
|
+
Request,
|
|
164
|
+
Response
|
|
165
|
+
}: {
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
167
|
+
Request: REST.RequestSchemed<RequestA, RequestE>
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
169
|
+
Response: REST.ReqRes<ResponseA, ResponseE, any>
|
|
170
|
+
}) {
|
|
171
|
+
const a = fetchApi2S(Request, Response)(
|
|
172
|
+
Request.method,
|
|
173
|
+
new Path(Request.path)
|
|
174
|
+
)
|
|
175
|
+
const encode = S.encode(Response)
|
|
176
|
+
return (req: RequestA) => a(req).flatMap(mapResponseM((_) => encode(_)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function makePathWithQuery(
|
|
180
|
+
path: Path,
|
|
181
|
+
pars: Record<
|
|
182
|
+
string,
|
|
183
|
+
| string
|
|
184
|
+
| number
|
|
185
|
+
| boolean
|
|
186
|
+
| readonly string[]
|
|
187
|
+
| readonly number[]
|
|
188
|
+
| readonly boolean[]
|
|
189
|
+
| null
|
|
190
|
+
>
|
|
191
|
+
) {
|
|
192
|
+
const forQs = ReadonlyRecord.filter(pars, (_, k) => !path.params.includes(k))
|
|
193
|
+
const q = forQs // { ...forQs, _: JSON.stringify(forQs) } // TODO: drop completely individual keys from query?, sticking to json only
|
|
194
|
+
return (
|
|
195
|
+
path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
|
|
196
|
+
+ (Object.keys(q).length
|
|
197
|
+
? "?" + qs.stringify(q)
|
|
198
|
+
: "")
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function makePathWithBody(
|
|
203
|
+
path: Path,
|
|
204
|
+
pars: Record<
|
|
205
|
+
string,
|
|
206
|
+
| string
|
|
207
|
+
| number
|
|
208
|
+
| boolean
|
|
209
|
+
| readonly string[]
|
|
210
|
+
| readonly number[]
|
|
211
|
+
| readonly boolean[]
|
|
212
|
+
| null
|
|
213
|
+
>
|
|
214
|
+
) {
|
|
215
|
+
return path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function mapResponse<T, A>(map: (t: T) => A) {
|
|
219
|
+
return (r: FetchResponse<T>): FetchResponse<A> => {
|
|
220
|
+
return { ...r, body: map(r.body) }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function mapResponseM<T, R, E, A>(map: (t: T) => Effect<A, E, R>) {
|
|
225
|
+
return (r: FetchResponse<T>): Effect<FetchResponse<A>, E, R> => {
|
|
226
|
+
return Effect.all({
|
|
227
|
+
body: map(r.body),
|
|
228
|
+
headers: Effect.sync(() => r.headers),
|
|
229
|
+
status: Effect.sync(() => r.status)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export type FetchResponse<T> = { body: T; headers: Headers; status: number }
|
|
234
|
+
|
|
235
|
+
export const EmptyResponse = Object.freeze({ body: null, headers: {}, status: 404 })
|
|
236
|
+
export const EmptyResponseM = Effect.sync(() => EmptyResponse)
|
|
237
|
+
const EmptyResponseMThunk_ = constant(EmptyResponseM)
|
|
238
|
+
export function EmptyResponseMThunk<T>(): Effect<
|
|
239
|
+
Readonly<{
|
|
240
|
+
body: null | T
|
|
241
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
242
|
+
headers: {}
|
|
243
|
+
status: 404
|
|
244
|
+
}>,
|
|
245
|
+
never,
|
|
246
|
+
unknown
|
|
247
|
+
> {
|
|
248
|
+
return EmptyResponseMThunk_()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function getBody<R, E, A>(eff: Effect<FetchResponse<A | null>, E, R>) {
|
|
252
|
+
return eff.flatMap((r) => r.body === null ? Effect.die("Not found") : Effect.sync(() => r.body))
|
|
253
|
+
}
|
package/_src/client.ts
ADDED
package/_src/faker.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// FILE HAS SIDE EFFECTS!
|
|
2
|
+
import type { Faker } from "@faker-js/faker"
|
|
3
|
+
import type * as FC from "fast-check"
|
|
4
|
+
|
|
5
|
+
// TODO: inject faker differently, so we dont care about multiple instances of library.
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line prefer-const
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
let faker: Faker = undefined as any as Faker
|
|
10
|
+
export function setFaker(f: Faker) {
|
|
11
|
+
faker = f
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getFaker() {
|
|
15
|
+
if (!faker) throw new Error("You forgot to load faker library")
|
|
16
|
+
return faker
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const fakerToArb = <T>(fakerGen: () => T) => (fc: typeof FC) => {
|
|
20
|
+
return fc
|
|
21
|
+
.integer()
|
|
22
|
+
.noBias() // same probability to generate each of the allowed integers
|
|
23
|
+
.noShrink() // shrink on a seed makes no sense
|
|
24
|
+
.map((seed) => {
|
|
25
|
+
faker.seed(seed) // seed the generator
|
|
26
|
+
return fakerGen() // call it
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const fakerArb = <T>(
|
|
31
|
+
gen: (fake: Faker) => () => T
|
|
32
|
+
): (a: any) => FC.Arbitrary<T> => fakerToArb(() => gen(getFaker())())
|