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.
Files changed (169) hide show
  1. package/.eslintrc.cjs +11 -0
  2. package/.prettierignore +6 -0
  3. package/CHANGELOG.md +4106 -0
  4. package/_cjs/Config/SecretURL.cjs +58 -0
  5. package/_cjs/Config/SecretURL.cjs.map +1 -0
  6. package/_cjs/Config/internal/configSecretURL.cjs +88 -0
  7. package/_cjs/Config/internal/configSecretURL.cjs.map +1 -0
  8. package/_cjs/Inputify.type.cjs +6 -0
  9. package/_cjs/Inputify.type.cjs.map +1 -0
  10. package/_cjs/Operations.cjs +76 -0
  11. package/_cjs/Operations.cjs.map +1 -0
  12. package/_cjs/Pure.cjs +201 -0
  13. package/_cjs/Pure.cjs.map +1 -0
  14. package/_cjs/Request.cjs +76 -0
  15. package/_cjs/Request.cjs.map +1 -0
  16. package/_cjs/Widen.type.cjs +6 -0
  17. package/_cjs/Widen.type.cjs.map +1 -0
  18. package/_cjs/_ext/date.cjs +64 -0
  19. package/_cjs/_ext/date.cjs.map +1 -0
  20. package/_cjs/_ext/misc.cjs +121 -0
  21. package/_cjs/_ext/misc.cjs.map +1 -0
  22. package/_cjs/_global.cjs +24 -0
  23. package/_cjs/_global.cjs.map +1 -0
  24. package/_cjs/_global.ext.cjs +5 -0
  25. package/_cjs/_global.ext.cjs.map +1 -0
  26. package/_cjs/_global.schema.cjs +4 -0
  27. package/_cjs/_global.schema.cjs.map +1 -0
  28. package/_cjs/client/QueryResult.cjs +116 -0
  29. package/_cjs/client/QueryResult.cjs.map +1 -0
  30. package/_cjs/client/clientFor.cjs +159 -0
  31. package/_cjs/client/clientFor.cjs.map +1 -0
  32. package/_cjs/client/config.cjs +21 -0
  33. package/_cjs/client/config.cjs.map +1 -0
  34. package/_cjs/client/errors.cjs +116 -0
  35. package/_cjs/client/errors.cjs.map +1 -0
  36. package/_cjs/client/fetch.cjs +178 -0
  37. package/_cjs/client/fetch.cjs.map +1 -0
  38. package/_cjs/client.cjs +61 -0
  39. package/_cjs/client.cjs.map +1 -0
  40. package/_cjs/faker.cjs +31 -0
  41. package/_cjs/faker.cjs.map +1 -0
  42. package/_cjs/ids.cjs +24 -0
  43. package/_cjs/ids.cjs.map +1 -0
  44. package/_cjs/index.cjs +27 -0
  45. package/_cjs/index.cjs.map +1 -0
  46. package/_cjs/refinements.cjs +97 -0
  47. package/_cjs/refinements.cjs.map +1 -0
  48. package/_cjs/schema.cjs +50 -0
  49. package/_cjs/schema.cjs.map +1 -0
  50. package/_cjs/schema.test.cjs +9 -0
  51. package/_cjs/schema.test.cjs.map +1 -0
  52. package/_cjs/service.cjs +97 -0
  53. package/_cjs/service.cjs.map +1 -0
  54. package/_cjs/utils.cjs +17 -0
  55. package/_cjs/utils.cjs.map +1 -0
  56. package/_src/Config/SecretURL.ts +103 -0
  57. package/_src/Config/internal/configSecretURL.ts +85 -0
  58. package/_src/Inputify.type.ts +13 -0
  59. package/_src/Operations.ts +70 -0
  60. package/_src/Pure.ts +525 -0
  61. package/_src/Request.ts +106 -0
  62. package/_src/Widen.type.ts +28 -0
  63. package/_src/_ext/date.ts +84 -0
  64. package/_src/_ext/misc.ts +161 -0
  65. package/_src/_global/stm.ts.bak +35 -0
  66. package/_src/_global.ext.ts +3 -0
  67. package/_src/_global.schema.ts +106 -0
  68. package/_src/_global.ts +119 -0
  69. package/_src/client/QueryResult.ts +120 -0
  70. package/_src/client/clientFor.ts +260 -0
  71. package/_src/client/config.ts +13 -0
  72. package/_src/client/errors.ts +129 -0
  73. package/_src/client/fetch.ts +253 -0
  74. package/_src/client.ts +7 -0
  75. package/_src/faker.ts +32 -0
  76. package/_src/ids.ts +35 -0
  77. package/_src/index.ts +4 -0
  78. package/_src/refinements.ts +92 -0
  79. package/_src/schema/_schema.ts.bak +208 -0
  80. package/_src/schema/api/date.ts.bak +78 -0
  81. package/_src/schema/api.ts.bak +20 -0
  82. package/_src/schema/overrides.ts.bak +76 -0
  83. package/_src/schema/shared.ts.bak +334 -0
  84. package/_src/schema.test.ts +3 -0
  85. package/_src/schema.ts +37 -0
  86. package/_src/service.ts +119 -0
  87. package/_src/utils.ts +1 -0
  88. package/dist/Config/SecretURL.d.ts +82 -0
  89. package/dist/Config/SecretURL.d.ts.map +1 -0
  90. package/dist/Config/SecretURL.js +49 -0
  91. package/dist/Config/internal/configSecretURL.d.ts +24 -0
  92. package/dist/Config/internal/configSecretURL.d.ts.map +1 -0
  93. package/dist/Config/internal/configSecretURL.js +75 -0
  94. package/dist/Inputify.type.d.ts +10 -0
  95. package/dist/Inputify.type.d.ts.map +1 -0
  96. package/dist/Inputify.type.js +2 -0
  97. package/dist/Operations.d.ts +170 -0
  98. package/dist/Operations.d.ts.map +1 -0
  99. package/dist/Operations.js +87 -0
  100. package/dist/Pure.d.ts +169 -0
  101. package/dist/Pure.d.ts.map +1 -0
  102. package/dist/Pure.js +167 -0
  103. package/dist/Request.d.ts +49 -0
  104. package/dist/Request.d.ts.map +1 -0
  105. package/dist/Request.js +58 -0
  106. package/dist/Widen.type.d.ts +19 -0
  107. package/dist/Widen.type.d.ts.map +1 -0
  108. package/dist/Widen.type.js +2 -0
  109. package/dist/_ext/date.d.ts +71 -0
  110. package/dist/_ext/date.d.ts.map +1 -0
  111. package/dist/_ext/date.js +58 -0
  112. package/dist/_ext/misc.d.ts +77 -0
  113. package/dist/_ext/misc.d.ts.map +1 -0
  114. package/dist/_ext/misc.js +98 -0
  115. package/dist/_global.d.ts +70 -0
  116. package/dist/_global.d.ts.map +1 -0
  117. package/dist/_global.ext.d.ts +3 -0
  118. package/dist/_global.ext.d.ts.map +1 -0
  119. package/dist/_global.ext.js +4 -0
  120. package/dist/_global.js +76 -0
  121. package/dist/_global.schema.d.ts +6 -0
  122. package/dist/_global.schema.d.ts.map +1 -0
  123. package/dist/_global.schema.js +6 -0
  124. package/dist/client/QueryResult.d.ts +85 -0
  125. package/dist/client/QueryResult.d.ts.map +1 -0
  126. package/dist/client/QueryResult.js +85 -0
  127. package/dist/client/clientFor.d.ts +44 -0
  128. package/dist/client/clientFor.d.ts.map +1 -0
  129. package/dist/client/clientFor.js +144 -0
  130. package/dist/client/config.d.ts +14 -0
  131. package/dist/client/config.d.ts.map +1 -0
  132. package/dist/client/config.js +11 -0
  133. package/dist/client/errors.d.ts +206 -0
  134. package/dist/client/errors.d.ts.map +1 -0
  135. package/dist/client/errors.js +130 -0
  136. package/dist/client/fetch.d.ts +61 -0
  137. package/dist/client/fetch.d.ts.map +1 -0
  138. package/dist/client/fetch.js +127 -0
  139. package/dist/client.d.ts +6 -0
  140. package/dist/client.d.ts.map +1 -0
  141. package/dist/client.js +7 -0
  142. package/dist/faker.d.ts +7 -0
  143. package/dist/faker.d.ts.map +1 -0
  144. package/dist/faker.js +24 -0
  145. package/dist/ids.d.ts +32 -0
  146. package/dist/ids.d.ts.map +1 -0
  147. package/dist/ids.js +17 -0
  148. package/dist/index.d.ts +4 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +4 -0
  151. package/dist/refinements.d.ts +57 -0
  152. package/dist/refinements.d.ts.map +1 -0
  153. package/dist/refinements.js +85 -0
  154. package/dist/schema.d.ts +7 -0
  155. package/dist/schema.d.ts.map +1 -0
  156. package/dist/schema.js +22 -0
  157. package/dist/schema.test.d.ts.map +1 -0
  158. package/dist/service.d.ts +47 -0
  159. package/dist/service.d.ts.map +1 -0
  160. package/dist/service.js +83 -0
  161. package/dist/utils.d.ts +2 -0
  162. package/dist/utils.d.ts.map +1 -0
  163. package/dist/utils.js +2 -0
  164. package/package.json +315 -0
  165. package/tsconfig.json +114 -0
  166. package/tsconfig.json.bak +47 -0
  167. package/tsplus.config.json +7 -0
  168. package/vitest.config.ts +5 -0
  169. 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
@@ -0,0 +1,7 @@
1
+ // codegen:start {preset: barrel, include: ./client/*.ts}
2
+ export * from "./client/clientFor.js"
3
+ export * from "./client/config.js"
4
+ export * from "./client/errors.js"
5
+ export * from "./client/fetch.js"
6
+ export * from "./client/QueryResult.js"
7
+ // codegen:end
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())())