effect-app 4.0.0-beta.3 → 4.0.0-beta.31
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/CHANGELOG.md +165 -0
- package/dist/Effect.d.ts.map +1 -1
- package/dist/Effect.js +3 -2
- package/dist/Operations.d.ts +48 -12
- package/dist/Operations.d.ts.map +1 -1
- package/dist/Pure.d.ts.map +1 -1
- package/dist/Pure.js +11 -11
- package/dist/Schema/Class.d.ts.map +1 -1
- package/dist/Schema/Class.js +1 -7
- package/dist/Schema/brand.d.ts +8 -5
- package/dist/Schema/brand.d.ts.map +1 -1
- package/dist/Schema/brand.js +1 -1
- package/dist/Schema/email.d.ts.map +1 -1
- package/dist/Schema/email.js +4 -3
- package/dist/Schema/ext.d.ts +26 -25
- package/dist/Schema/ext.d.ts.map +1 -1
- package/dist/Schema/ext.js +13 -20
- package/dist/Schema/moreStrings.d.ts +6 -6
- package/dist/Schema/moreStrings.d.ts.map +1 -1
- package/dist/Schema/moreStrings.js +6 -4
- package/dist/Schema/phoneNumber.d.ts.map +1 -1
- package/dist/Schema/phoneNumber.js +3 -2
- package/dist/Schema.d.ts +18 -52
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +40 -61
- package/dist/ServiceMap.d.ts +3 -3
- package/dist/ServiceMap.d.ts.map +1 -1
- package/dist/ServiceMap.js +1 -1
- package/dist/client/apiClientFactory.d.ts.map +1 -1
- package/dist/client/apiClientFactory.js +8 -9
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +1 -1
- package/dist/client/makeClient.d.ts +8 -4
- package/dist/client/makeClient.d.ts.map +1 -1
- package/dist/client/makeClient.js +13 -15
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +5 -5
- package/dist/ids.d.ts +6 -6
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +24 -5
- package/package.json +3 -7
- package/src/Effect.ts +3 -2
- package/src/Pure.ts +12 -13
- package/src/Schema/Class.ts +0 -6
- package/src/Schema/brand.ts +13 -7
- package/src/Schema/email.ts +4 -2
- package/src/Schema/ext.ts +46 -38
- package/src/Schema/moreStrings.ts +16 -13
- package/src/Schema/phoneNumber.ts +3 -1
- package/src/Schema.ts +76 -100
- package/src/ServiceMap.ts +7 -6
- package/src/client/apiClientFactory.ts +12 -15
- package/src/client/errors.ts +12 -3
- package/src/client/makeClient.ts +25 -20
- package/src/http/Request.ts +7 -4
- package/src/ids.ts +1 -1
- package/src/index.ts +0 -1
- package/src/utils.ts +26 -4
- package/test/dist/moreStrings.test.d.ts.map +1 -0
- package/test/dist/rpc.test.d.ts.map +1 -1
- package/test/moreStrings.test.ts +17 -0
- package/test/rpc.test.ts +10 -0
- package/test/schema.test.ts +178 -1
- package/dist/Struct.d.ts +0 -44
- package/dist/Struct.d.ts.map +0 -1
- package/dist/Struct.js +0 -29
- package/src/Struct.ts +0 -54
package/src/Schema.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SchemaAST, type Tracer } from "effect"
|
|
2
2
|
import * as S from "effect/Schema"
|
|
3
3
|
import type { NonEmptyReadonlyArray } from "./Array.js"
|
|
4
4
|
import { fakerArb } from "./faker.js"
|
|
5
|
-
import { Email as EmailT } from "./Schema/email.js"
|
|
5
|
+
import { Email as EmailT, type Email as EmailType } from "./Schema/email.js"
|
|
6
6
|
import { withDefaultMake } from "./Schema/ext.js"
|
|
7
|
-
import { PhoneNumber as PhoneNumberT } from "./Schema/phoneNumber.js"
|
|
8
|
-
import type { AST } from "./Schema/schema.js"
|
|
7
|
+
import { PhoneNumber as PhoneNumberT, type PhoneNumber as PhoneNumberType } from "./Schema/phoneNumber.js"
|
|
9
8
|
import { extendM } from "./utils.js"
|
|
10
9
|
|
|
11
10
|
export * from "effect/Schema"
|
|
@@ -40,127 +39,104 @@ export interface WithOptionalSpan {
|
|
|
40
39
|
[SpanId]?: Tracer.Span
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
const makeEmail = S.decodeSync(EmailT as any) as (value: string) => EmailType
|
|
43
|
+
const makePhoneNumber = S.decodeSync(PhoneNumberT as any) as (value: string) => PhoneNumberType
|
|
44
|
+
|
|
43
45
|
export const Email = EmailT
|
|
44
46
|
.pipe(
|
|
45
47
|
S.annotate({
|
|
46
48
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
47
|
-
|
|
49
|
+
toArbitrary: () => (fc) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(makeEmail)
|
|
48
50
|
}),
|
|
49
51
|
withDefaultMake
|
|
50
52
|
)
|
|
51
53
|
|
|
52
|
-
export type Email =
|
|
54
|
+
export type Email = EmailType
|
|
53
55
|
|
|
54
56
|
export const PhoneNumber = PhoneNumberT
|
|
55
57
|
.pipe(
|
|
56
58
|
S.annotate({
|
|
57
|
-
|
|
59
|
+
toArbitrary: () => (fc) =>
|
|
58
60
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
59
|
-
fakerArb((faker) => faker.phone.number)(fc).map(
|
|
61
|
+
fakerArb((faker) => faker.phone.number)(fc).map(makePhoneNumber)
|
|
60
62
|
}),
|
|
61
63
|
withDefaultMake
|
|
62
64
|
)
|
|
63
65
|
|
|
64
|
-
export
|
|
65
|
-
schema: S.Codec<A, I, R>
|
|
66
|
-
) => {
|
|
67
|
-
// In v4, transformations are stored as encoding on nodes, not as wrapper nodes.
|
|
68
|
-
// Union member ASTs are directly Objects (TypeLiteral equivalent).
|
|
69
|
-
if (SchemaAST.isUnion(schema.ast)) {
|
|
70
|
-
return schema.ast.types.reduce((acc: any, t: AST.AST) => {
|
|
71
|
-
if (!SchemaAST.isObjects(t)) return acc
|
|
72
|
-
const tag = Array.findFirst(t.propertySignatures, (_: any) => {
|
|
73
|
-
if (_.name === "_tag" && SchemaAST.isLiteral(_.type)) {
|
|
74
|
-
return Option.some(_.type)
|
|
75
|
-
}
|
|
76
|
-
return Option.none()
|
|
77
|
-
})
|
|
78
|
-
const ast = Option.getOrUndefined(tag)
|
|
79
|
-
if (!ast) {
|
|
80
|
-
return acc
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
...acc,
|
|
84
|
-
[String((ast as SchemaAST.Literal).literal)]: (x: { _tag: string }) =>
|
|
85
|
-
x._tag === (ast as SchemaAST.Literal).literal
|
|
86
|
-
}
|
|
87
|
-
}, {} as Is<A>)
|
|
88
|
-
}
|
|
89
|
-
throw new Error("Unsupported")
|
|
90
|
-
}
|
|
66
|
+
export type PhoneNumber = PhoneNumberType
|
|
91
67
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new Error("Unsupported")
|
|
68
|
+
// Copied from SchemaAST.collectSentinels (marked @internal in effect).
|
|
69
|
+
// Returns all { key, literal } pairs that can discriminate a union member.
|
|
70
|
+
const getTagFromAST = (schema: S.Top): string => {
|
|
71
|
+
const sentinels = collectSentinelsFromAST(schema.ast)
|
|
72
|
+
const sentinel = sentinels.find((s) => s.key === "_tag")
|
|
73
|
+
if (sentinel !== undefined && typeof sentinel.literal === "string") return sentinel.literal
|
|
74
|
+
throw new Error("No _tag literal found on schema member")
|
|
100
75
|
}
|
|
101
76
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
77
|
+
function collectSentinelsFromAST(
|
|
78
|
+
ast: SchemaAST.AST
|
|
79
|
+
): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> {
|
|
80
|
+
switch (ast._tag) {
|
|
81
|
+
case "Declaration": {
|
|
82
|
+
const s = ast.annotations?.["~sentinels"]
|
|
83
|
+
return Array.isArray(s) ? s : []
|
|
84
|
+
}
|
|
85
|
+
case "Objects":
|
|
86
|
+
return ast.propertySignatures.flatMap(
|
|
87
|
+
(ps): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> => {
|
|
88
|
+
const type = ps.type
|
|
89
|
+
if (!SchemaAST.isOptional(type)) {
|
|
90
|
+
if (SchemaAST.isLiteral(type)) return [{ key: ps.name, literal: type.literal }]
|
|
91
|
+
if (SchemaAST.isUniqueSymbol(type)) return [{ key: ps.name, literal: type.symbol }]
|
|
92
|
+
}
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
case "Suspend":
|
|
97
|
+
return collectSentinelsFromAST(ast.thunk())
|
|
98
|
+
default:
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
107
101
|
}
|
|
108
102
|
|
|
109
|
-
export const taggedUnionMap = <
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
-
Members extends readonly (S.Top & { fields: { _tag: S.tag<string> } })[]
|
|
112
|
-
>(
|
|
113
|
-
self: Members
|
|
114
|
-
) =>
|
|
115
|
-
self.reduce((acc, key) => {
|
|
116
|
-
// TODO: v4 migration — PropertySignatureDeclaration removed, need v4 AST traversal
|
|
117
|
-
const ast = key.fields._tag.ast as any
|
|
118
|
-
const tag = ((ast.type ?? ast) as SchemaAST.Literal).literal as string // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
|
119
|
-
;(acc as any)[tag] = key as any
|
|
120
|
-
return acc
|
|
121
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
-
}, {} as any)
|
|
123
|
-
|
|
124
103
|
export const tags = <
|
|
125
|
-
|
|
126
|
-
Members extends NonEmptyReadonlyArray<(S.Top & { fields: { _tag: S.tag<string> } })>
|
|
104
|
+
Members extends NonEmptyReadonlyArray<(S.Top & { readonly Type: { readonly _tag: string } })>
|
|
127
105
|
>(
|
|
128
106
|
self: Members
|
|
129
107
|
) =>
|
|
130
|
-
S.Literals(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
108
|
+
S.Literals(
|
|
109
|
+
self.map(getTagFromAST) as {
|
|
110
|
+
[Index in keyof Members]: Members[Index]["Type"]["_tag"]
|
|
111
|
+
}
|
|
112
|
+
) as S.Literals<
|
|
113
|
+
{
|
|
114
|
+
[Index in keyof Members]: Members[Index]["Type"]["_tag"]
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
|
|
118
|
+
type TaggedUnionMembers = NonEmptyReadonlyArray<
|
|
119
|
+
S.Top & { readonly Type: { readonly _tag: string } }
|
|
120
|
+
>
|
|
121
|
+
|
|
122
|
+
type TaggedUnionTags<Members extends TaggedUnionMembers> = S.Literals<
|
|
123
|
+
{
|
|
124
|
+
[Index in keyof Members]: Members[Index]["Type"]["_tag"]
|
|
125
|
+
}
|
|
126
|
+
>
|
|
149
127
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
extendM(_, (_) => ({
|
|
158
|
-
is: S.is(_ as any),
|
|
159
|
-
isA: makeIs(_ as any),
|
|
160
|
-
isAnyOf: makeIsAnyOf(_ as any),
|
|
161
|
-
tagMap: taggedUnionMap(a),
|
|
162
|
-
tags: tags(a as any)
|
|
163
|
-
}))
|
|
164
|
-
)
|
|
128
|
+
type TaggedUnionWithTags<Members extends TaggedUnionMembers> = S.toTaggedUnion<"_tag", Members> & {
|
|
129
|
+
readonly tags: TaggedUnionTags<Members>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const extendTaggedUnionWithTags = <Members extends TaggedUnionMembers>(
|
|
133
|
+
schema: S.Union<Members>
|
|
134
|
+
): TaggedUnionWithTags<Members> => extendM(schema.pipe(S.toTaggedUnion("_tag")), () => ({ tags: tags(schema.members) }))
|
|
165
135
|
|
|
166
|
-
export
|
|
136
|
+
export const ExtendTaggedUnion = <Members extends TaggedUnionMembers>(
|
|
137
|
+
schema: S.Union<Members>
|
|
138
|
+
): TaggedUnionWithTags<Members> => extendTaggedUnionWithTags(schema)
|
|
139
|
+
|
|
140
|
+
export const TaggedUnion = <
|
|
141
|
+
Members extends TaggedUnionMembers
|
|
142
|
+
>(...a: Members): TaggedUnionWithTags<Members> => extendTaggedUnionWithTags(S.Union(a))
|
package/src/ServiceMap.ts
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { type Effect, Layer, type Scope, type Types } from "effect"
|
|
9
9
|
import * as ServiceMap from "effect/ServiceMap"
|
|
10
|
-
import { Yieldable } from "./Effect.js"
|
|
10
|
+
import { type Yieldable } from "./Effect.js"
|
|
11
11
|
|
|
12
12
|
export * from "effect/ServiceMap"
|
|
13
13
|
|
|
14
|
-
export interface Opaque<Self extends object, in out Shape extends object>
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
export interface Opaque<Self extends object, in out Shape extends object>
|
|
15
|
+
extends ServiceMap.Key<Self, Self>, Yieldable<Opaque<Self, Shape>, Self, never, Self>
|
|
16
|
+
{
|
|
17
|
+
of(this: void, self: Shape): Self
|
|
17
18
|
serviceMap(self: Shape): ServiceMap.ServiceMap<Self>
|
|
18
19
|
// a version that leverages the Shape -> Self conversion
|
|
19
20
|
toLayer: <E, R>(
|
|
@@ -151,7 +152,7 @@ export const Opaque: {
|
|
|
151
152
|
id: Identifier,
|
|
152
153
|
options?: {
|
|
153
154
|
readonly make: ((...args: Args) => Effect.Effect<Shape, E, R>) | Effect.Effect<Shape, E, R> | undefined
|
|
154
|
-
}
|
|
155
|
+
}
|
|
155
156
|
) =>
|
|
156
157
|
& OpaqueClass<Self, Identifier, Shape>
|
|
157
158
|
& ([Types.unassigned] extends [R] ? unknown
|
|
@@ -181,7 +182,7 @@ export const Opaque: {
|
|
|
181
182
|
const svc = ServiceMap.Service()(id, options) as any
|
|
182
183
|
return Object.assign(svc, {
|
|
183
184
|
toLayer: (eff: Effect.Effect<any, any, any>) => {
|
|
184
|
-
return Layer.effect(svc
|
|
185
|
+
return Layer.effect(svc, eff)
|
|
185
186
|
}
|
|
186
187
|
})
|
|
187
188
|
}
|
|
@@ -50,17 +50,17 @@ export const HttpClientLayer = (config: ApiConfig) =>
|
|
|
50
50
|
Effect
|
|
51
51
|
.gen(function*() {
|
|
52
52
|
const baseClient = yield* HttpClient.HttpClient
|
|
53
|
-
const ctx = yield* RequestName
|
|
54
53
|
const client = baseClient.pipe(
|
|
55
54
|
HttpClient.mapRequest(HttpClientRequest.prependUrl(config.url + "/rpc")),
|
|
56
55
|
HttpClient.mapRequest(
|
|
57
56
|
HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => ({}))))
|
|
58
57
|
),
|
|
59
|
-
HttpClient.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
HttpClient.mapRequestEffect((req) =>
|
|
59
|
+
Effect.map(RequestName.asEffect(), (ctx) =>
|
|
60
|
+
flow(
|
|
61
|
+
HttpClientRequest.appendUrlParam("action", ctx.requestName),
|
|
62
|
+
HttpClientRequest.appendUrl("/" + ctx.moduleName)
|
|
63
|
+
)(req))
|
|
64
64
|
)
|
|
65
65
|
)
|
|
66
66
|
return client
|
|
@@ -76,7 +76,7 @@ export const HttpClientFromConfigLayer = Layer.unwrap(
|
|
|
76
76
|
|
|
77
77
|
export const RpcSerializationLayer = (config: ApiConfig) =>
|
|
78
78
|
Layer.mergeAll(
|
|
79
|
-
RpcSerialization.
|
|
79
|
+
RpcSerialization.layerNdjson,
|
|
80
80
|
HttpClientLayer(config)
|
|
81
81
|
)
|
|
82
82
|
|
|
@@ -91,7 +91,7 @@ const getFiltered = <M extends Requests>(resource: M) => {
|
|
|
91
91
|
// TODO: Record.filter
|
|
92
92
|
const filtered = typedKeysOf(resource).reduce((acc, cur) => {
|
|
93
93
|
if (
|
|
94
|
-
Predicate.
|
|
94
|
+
Predicate.isObjectKeyword(resource[cur])
|
|
95
95
|
&& (resource[cur].success)
|
|
96
96
|
) {
|
|
97
97
|
acc[cur as keyof Filtered] = resource[cur] as any
|
|
@@ -146,10 +146,7 @@ const makeRpcTag = <M extends Requests>(resource: M) => {
|
|
|
146
146
|
// Use Layer.effect directly (not TheClient.toLayer) so TypeScript properly excludes Scope
|
|
147
147
|
const layer = Layer.effect(
|
|
148
148
|
TheClient,
|
|
149
|
-
|
|
150
|
-
RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName }),
|
|
151
|
-
(cl) => (cl as any)[meta.moduleName]
|
|
152
|
-
)
|
|
149
|
+
RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName })
|
|
153
150
|
)
|
|
154
151
|
return Object.assign(TheClient, { layer })
|
|
155
152
|
}
|
|
@@ -188,7 +185,7 @@ const makeApiClientFactory = Effect
|
|
|
188
185
|
const filtered = getFiltered(resource)
|
|
189
186
|
return {
|
|
190
187
|
mr,
|
|
191
|
-
client:
|
|
188
|
+
client: typedKeysOf(filtered)
|
|
192
189
|
.reduce((prev, cur) => {
|
|
193
190
|
const h = filtered[cur]!
|
|
194
191
|
|
|
@@ -211,7 +208,7 @@ const makeApiClientFactory = Effect
|
|
|
211
208
|
const layers = requestLevelLayers.pipe(Layer.provideMerge(requestNameLayer))
|
|
212
209
|
|
|
213
210
|
const fields = Struct.omit(Request.fields, ["_tag"] as const)
|
|
214
|
-
const requestAttr = h._tag
|
|
211
|
+
const requestAttr = `${meta.moduleName}.${h._tag}`
|
|
215
212
|
// @ts-expect-error doc
|
|
216
213
|
prev[cur] = Object.keys(fields).length === 0
|
|
217
214
|
? {
|
|
@@ -246,7 +243,7 @@ const makeApiClientFactory = Effect
|
|
|
246
243
|
}
|
|
247
244
|
|
|
248
245
|
return prev
|
|
249
|
-
}, {} as Client<M, M["meta"]["moduleName"]>)
|
|
246
|
+
}, {} as Client<M, M["meta"]["moduleName"]>)
|
|
250
247
|
}
|
|
251
248
|
})
|
|
252
249
|
|
package/src/client/errors.ts
CHANGED
|
@@ -43,7 +43,10 @@ export class InvalidStateError extends TaggedError<InvalidStateError>()("Invalid
|
|
|
43
43
|
message: S.String
|
|
44
44
|
}) {
|
|
45
45
|
constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) {
|
|
46
|
-
super(
|
|
46
|
+
super(
|
|
47
|
+
typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any,
|
|
48
|
+
disableValidation as any
|
|
49
|
+
)
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -51,7 +54,10 @@ export class ServiceUnavailableError extends TaggedError<ServiceUnavailableError
|
|
|
51
54
|
message: S.String
|
|
52
55
|
}) {
|
|
53
56
|
constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) {
|
|
54
|
-
super(
|
|
57
|
+
super(
|
|
58
|
+
typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any,
|
|
59
|
+
disableValidation as any
|
|
60
|
+
)
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -116,7 +122,10 @@ export class OptimisticConcurrencyException extends TaggedError<OptimisticConcur
|
|
|
116
122
|
| ({ message: string; cause?: unknown; raw?: unknown }),
|
|
117
123
|
disableValidation?: boolean
|
|
118
124
|
) {
|
|
119
|
-
super(
|
|
125
|
+
super(
|
|
126
|
+
"message" in args ? args : { message: `Existing ${args.type} ${args.id} record changed` } as any,
|
|
127
|
+
disableValidation as any
|
|
128
|
+
)
|
|
120
129
|
if (!("message" in args)) {
|
|
121
130
|
this.details = args
|
|
122
131
|
}
|
package/src/client/makeClient.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SchemaGetter, SchemaTransformation } from "effect"
|
|
2
|
+
import { Link } from "effect/SchemaAST"
|
|
3
|
+
import { Transformation } from "effect/SchemaTransformation"
|
|
4
|
+
import { type GetContextConfig, type GetEffectError, type RequestContextMapTagAny } from "../rpc/RpcContextMap.js"
|
|
2
5
|
import * as S from "../Schema.js"
|
|
3
6
|
import { AST } from "../Schema.js"
|
|
4
7
|
|
|
5
8
|
const merge = (a: any, b: Array<any>) =>
|
|
6
9
|
a !== undefined && b.length ? S.Union([a, ...b]) : a !== undefined ? a : b.length ? S.Union(b) : S.Never
|
|
7
10
|
|
|
11
|
+
const undefinedToNull = new Link(
|
|
12
|
+
S.AST.null,
|
|
13
|
+
new Transformation(
|
|
14
|
+
SchemaGetter.transform(() => undefined),
|
|
15
|
+
SchemaGetter.transform(() => null)
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* Whatever the input, we will only decode or encode to void
|
|
10
21
|
*/
|
|
11
|
-
const ForceVoid
|
|
22
|
+
export const ForceVoid = S
|
|
23
|
+
.Any
|
|
24
|
+
.pipe(
|
|
25
|
+
S.decodeTo(S.Any, SchemaTransformation.transform({ decode: () => void 0 as void, encode: () => void 0 }))
|
|
26
|
+
)
|
|
27
|
+
.annotate({
|
|
28
|
+
toCodecJson: () => undefinedToNull
|
|
29
|
+
})
|
|
12
30
|
|
|
13
31
|
type SchemaOrFields<T> = T extends S.Top ? T : T extends S.Struct.Fields ? S.Struct<T> : S.Void
|
|
14
32
|
|
|
@@ -62,16 +80,16 @@ export const makeRpcClient = <
|
|
|
62
80
|
tag: Tag,
|
|
63
81
|
fields: Payload,
|
|
64
82
|
config: RequestConfig & C
|
|
65
|
-
): TaggedRequestResult<Tag, Payload,
|
|
83
|
+
): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<C>, Omit<C, "success" | "error">>
|
|
66
84
|
<Tag extends string, Payload extends S.Struct.Fields, C extends Record<string, any>>(
|
|
67
85
|
tag: Tag,
|
|
68
86
|
fields: Payload,
|
|
69
87
|
config: C & RequestConfig
|
|
70
|
-
): TaggedRequestResult<Tag, Payload,
|
|
88
|
+
): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<C>, Omit<C, "success" | "error">>
|
|
71
89
|
<Tag extends string, Payload extends S.Struct.Fields>(
|
|
72
90
|
tag: Tag,
|
|
73
91
|
fields: Payload
|
|
74
|
-
): TaggedRequestResult<Tag, Payload,
|
|
92
|
+
): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<{}>, Record<string, never>>
|
|
75
93
|
} {
|
|
76
94
|
// TODO: filter errors based on config + take care of inversion
|
|
77
95
|
const errorSchemas = Object.values(rcs.config).map((_) => _.error)
|
|
@@ -92,22 +110,9 @@ export const makeRpcClient = <
|
|
|
92
110
|
: S.Struct(config.success)
|
|
93
111
|
: ForceVoid
|
|
94
112
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const taggedFields = { _tag: S.tag(tag), ...fields }
|
|
98
|
-
|
|
99
|
-
const RequestClass = class {
|
|
100
|
-
constructor(payload?: any) {
|
|
101
|
-
if (payload) {
|
|
102
|
-
Object.assign(this, payload)
|
|
103
|
-
}
|
|
104
|
-
;(this as any)._tag = tag
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
Object.assign(RequestClass, payloadSchema, {
|
|
113
|
+
const RequestClass = S.TaggedClass<any>()(tag, fields)
|
|
114
|
+
Object.assign(RequestClass, {
|
|
109
115
|
_tag: tag,
|
|
110
|
-
fields: taggedFields,
|
|
111
116
|
success: successSchema,
|
|
112
117
|
error: failureSchema,
|
|
113
118
|
config
|
package/src/http/Request.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Option } from "effect"
|
|
1
2
|
import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
|
|
2
3
|
import * as Effect from "../Effect.js"
|
|
3
4
|
import { HttpClient, HttpClientError, HttpClientRequest, HttpHeaders } from "./internal/lib.js"
|
|
@@ -24,16 +25,18 @@ export const demandJson = (client: HttpClient.HttpClient) =>
|
|
|
24
25
|
.mapRequest(client, (_) => HttpClientRequest.acceptJson(_))
|
|
25
26
|
.pipe(HttpClient.transform((r, request) =>
|
|
26
27
|
Effect.tap(r, (response) =>
|
|
27
|
-
|
|
28
|
-
.
|
|
29
|
-
|
|
28
|
+
Option
|
|
29
|
+
.exists(
|
|
30
|
+
HttpHeaders.get(response.headers, "Content-Type"),
|
|
31
|
+
(_) => _.startsWith("application/json")
|
|
32
|
+
)
|
|
30
33
|
? Effect.void
|
|
31
34
|
: Effect.fail(
|
|
32
35
|
new HttpClientError.DecodeError({
|
|
33
36
|
request,
|
|
34
37
|
response,
|
|
35
38
|
description: "not json response: "
|
|
36
|
-
+ HttpHeaders.get(response.headers, "Content-Type")
|
|
39
|
+
+ Option.getOrElse(HttpHeaders.get(response.headers, "Content-Type"), () => "<missing>")
|
|
37
40
|
})
|
|
38
41
|
))
|
|
39
42
|
))
|
package/src/ids.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { brandedStringId, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake
|
|
1
|
+
import { brandedStringId, type Codec, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake } from "effect-app/Schema"
|
|
2
2
|
import type { B } from "effect-app/Schema/schema"
|
|
3
3
|
import type { Simplify } from "effect/Types"
|
|
4
4
|
import { S } from "./index.js"
|
package/src/index.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
|
4
|
-
import { Effect, Exit, Fiber, Option, Record } from "effect"
|
|
4
|
+
import { Cause, Effect, Exit, Fiber, Option, Record } from "effect"
|
|
5
5
|
import { dual } from "effect/Function"
|
|
6
|
-
import { isFunction } from "effect/Predicate"
|
|
6
|
+
import { isFunction, isObject } from "effect/Predicate"
|
|
7
7
|
import * as Result from "effect/Result"
|
|
8
8
|
import type { GetFieldType, NumericDictionary, PropertyPath } from "lodash"
|
|
9
9
|
import { identity, pipe } from "./Function.js"
|
|
@@ -924,8 +924,8 @@ export const runtimeFiberAsPromise = <A, E>(fiber: Fiber.Fiber<A, E>, signal?: A
|
|
|
924
924
|
if (Exit.isSuccess(exit)) {
|
|
925
925
|
resolve(exit.value)
|
|
926
926
|
} else {
|
|
927
|
-
//
|
|
928
|
-
reject(exit.cause)
|
|
927
|
+
// eslint-disable-next-line
|
|
928
|
+
reject(Cause.squash(exit.cause))
|
|
929
929
|
}
|
|
930
930
|
})
|
|
931
931
|
)
|
|
@@ -950,3 +950,25 @@ export type UnionToTuples<T, U = T> = [T] extends [never] ? []
|
|
|
950
950
|
| [T, ...UnionToTuples<Exclude<U, T>>]
|
|
951
951
|
| UnionToTuples<Exclude<U, T>>
|
|
952
952
|
: []
|
|
953
|
+
|
|
954
|
+
const genConstructor = (function*() {}).constructor
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* @example
|
|
958
|
+
* ```ts
|
|
959
|
+
* import { Utils } from "effect"
|
|
960
|
+
*
|
|
961
|
+
* function* generatorFn() {
|
|
962
|
+
* yield 1
|
|
963
|
+
* yield 2
|
|
964
|
+
* }
|
|
965
|
+
*
|
|
966
|
+
* console.log(Utils.isGeneratorFunction(generatorFn)) // true
|
|
967
|
+
* console.log(Utils.isGeneratorFunction(() => {})) // false
|
|
968
|
+
* ```
|
|
969
|
+
*
|
|
970
|
+
* @category predicates
|
|
971
|
+
* @since 3.11.0
|
|
972
|
+
*/
|
|
973
|
+
export const isGeneratorFunction = (u: unknown): u is (...args: Array<any>) => Generator<any, any, any> =>
|
|
974
|
+
isObject(u) && u.constructor === genConstructor
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moreStrings.test.d.ts","sourceRoot":"","sources":["../moreStrings.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAErF,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAE7C,qBAAa,iBAAkB,SAAQ,sBAIrC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIL,qBAAa,KAAM,SAAQ,UAQzB;CAAG"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { S } from "effect-app"
|
|
2
|
+
import * as fc from "fast-check"
|
|
3
|
+
import { urlAlphabet } from "nanoid"
|
|
4
|
+
import { test } from "vitest"
|
|
5
|
+
|
|
6
|
+
const nanoidAlphabet = new Set(urlAlphabet)
|
|
7
|
+
|
|
8
|
+
const isNanoId = (value: string) => value.length === 21 && Array.from(value).every((char) => nanoidAlphabet.has(char))
|
|
9
|
+
|
|
10
|
+
test("StringId arbitrary generates nanoid-shaped values", () => {
|
|
11
|
+
fc.assert(
|
|
12
|
+
fc.property(S.toArbitrary(S.StringId), (value) => {
|
|
13
|
+
expect(isNanoId(value)).toBe(true)
|
|
14
|
+
expect(S.is(S.StringId)(value)).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
)
|
|
17
|
+
})
|
package/test/rpc.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
1
2
|
import { makeRpcClient, NotLoggedInError, UnauthorizedError } from "../src/client.js"
|
|
3
|
+
import { ForceVoid } from "../src/client/makeClient.js"
|
|
2
4
|
import { S } from "../src/index.js"
|
|
3
5
|
import { RpcContextMap } from "../src/rpc.js"
|
|
4
6
|
|
|
@@ -21,3 +23,11 @@ export class Stats extends TaggedRequest<Stats>()("Stats", {}, {
|
|
|
21
23
|
}) {}
|
|
22
24
|
|
|
23
25
|
declare const _stats: typeof Stats.success.Type
|
|
26
|
+
|
|
27
|
+
test("ForceVoid decodes and encodes as void", () => {
|
|
28
|
+
expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined)
|
|
29
|
+
expect(S.is(ForceVoid)(undefined)).toBe(true)
|
|
30
|
+
expect(S.decodeUnknownSync(ForceVoid)("test")).toBe(undefined)
|
|
31
|
+
expect(S.is(ForceVoid)("test")).toBe(true)
|
|
32
|
+
expect(S.encodeUnknownSync(ForceVoid)("test")).toBe(undefined)
|
|
33
|
+
})
|