effect-app 4.0.0-beta.2 → 4.0.0-beta.20
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 +101 -0
- package/dist/Operations.d.ts +14 -14
- package/dist/Pure.js +1 -1
- 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 +27 -24
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +23 -22
- 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 +6 -6
- package/dist/client/makeClient.d.ts.map +1 -1
- package/dist/client/makeClient.js +3 -14
- 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/rpc/MiddlewareMaker.d.ts +1 -1
- package/dist/rpc/MiddlewareMaker.d.ts.map +1 -1
- package/dist/rpc/MiddlewareMaker.js +1 -1
- package/dist/rpc/RpcMiddleware.d.ts +1 -1
- package/dist/rpc/RpcMiddleware.d.ts.map +1 -1
- 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/Pure.ts +2 -2
- 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 +56 -40
- 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 +6 -19
- package/src/ids.ts +1 -1
- package/src/index.ts +0 -1
- package/src/rpc/MiddlewareMaker.ts +2 -1
- package/src/rpc/RpcMiddleware.ts +2 -2
- 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/schema.test.ts +32 -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/ext.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
3
|
-
import { Effect, Option, pipe, Schema, type SchemaAST, SchemaGetter, SchemaIssue,
|
|
3
|
+
import { Effect, Option, pipe, Schema, type SchemaAST, SchemaGetter, SchemaIssue, SchemaTransformation, ServiceMap } from "effect"
|
|
4
4
|
import * as S from "effect/Schema"
|
|
5
5
|
import { type NonEmptyReadonlyArray } from "../Array.js"
|
|
6
6
|
import { extendM, typedKeysOf } from "../utils.js"
|
|
7
7
|
import { type AST } from "./schema.js"
|
|
8
8
|
|
|
9
|
+
type ProvidedCodec<Self extends S.Top, R> = S.Codec<
|
|
10
|
+
Self["Type"],
|
|
11
|
+
Self["Encoded"],
|
|
12
|
+
Exclude<Self["DecodingServices"], R>,
|
|
13
|
+
Exclude<Self["EncodingServices"], R>
|
|
14
|
+
>
|
|
15
|
+
|
|
9
16
|
// TODO: v4 migration — withConstructorDefault signature changed, propertySignature removed
|
|
10
17
|
// Constraint relaxed from `Self extends S.Top & S.WithoutConstructorDefault` to `Self extends S.Top`
|
|
11
18
|
// because `.pipe()` widens the schema type to `Top` which doesn't satisfy `WithoutConstructorDefault`.
|
|
@@ -69,7 +76,7 @@ export const Literal = <Literals extends NonEmptyReadonlyArray<AST.LiteralValue>
|
|
|
69
76
|
/**
|
|
70
77
|
* Like the default Schema `Array` but with `withDefault` => []
|
|
71
78
|
*/
|
|
72
|
-
export function Array<
|
|
79
|
+
export function Array<ValueSchema extends S.Top>(value: ValueSchema) {
|
|
73
80
|
return pipe(
|
|
74
81
|
S.Array(value),
|
|
75
82
|
(s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => [])) })
|
|
@@ -79,7 +86,7 @@ export function Array<Value extends S.Top>(value: Value) {
|
|
|
79
86
|
/**
|
|
80
87
|
* Like the default Schema `Map` but with `withDefault` => []
|
|
81
88
|
*/
|
|
82
|
-
function Map_<
|
|
89
|
+
function Map_<KeySchema extends S.Top, ValueSchema extends S.Top>(input: { key: KeySchema; value: ValueSchema }) {
|
|
83
90
|
return pipe(
|
|
84
91
|
S.ReadonlyMap(input.key, input.value),
|
|
85
92
|
(s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new global.Map())) })
|
|
@@ -91,18 +98,19 @@ export { Map_ as Map }
|
|
|
91
98
|
/**
|
|
92
99
|
* Like the default Schema `ReadonlySet` but with `withDefault` => new Set()
|
|
93
100
|
*/
|
|
94
|
-
export const ReadonlySet = <
|
|
101
|
+
export const ReadonlySet = <ValueSchema extends S.Top>(value: ValueSchema) =>
|
|
95
102
|
pipe(
|
|
96
103
|
S.ReadonlySet(value),
|
|
97
|
-
(s) =>
|
|
104
|
+
(s) =>
|
|
105
|
+
Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => new Set<S.Schema.Type<ValueSchema>>())) })
|
|
98
106
|
)
|
|
99
107
|
|
|
100
108
|
/**
|
|
101
109
|
* Like the default Schema `ReadonlyMap` but with `withDefault` => new Map()
|
|
102
110
|
*/
|
|
103
|
-
export const ReadonlyMap = <
|
|
104
|
-
readonly key:
|
|
105
|
-
readonly value:
|
|
111
|
+
export const ReadonlyMap = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
|
|
112
|
+
readonly key: KeySchema
|
|
113
|
+
readonly value: ValueSchema
|
|
106
114
|
}) =>
|
|
107
115
|
pipe(
|
|
108
116
|
S.ReadonlyMap(pair.key, pair.value),
|
|
@@ -112,25 +120,24 @@ export const ReadonlyMap = <K extends S.Top, V extends S.Top>(pair: {
|
|
|
112
120
|
/**
|
|
113
121
|
* Like the default Schema `NullOr` but with `withDefault` => null
|
|
114
122
|
*/
|
|
115
|
-
export const NullOr = <
|
|
123
|
+
export const NullOr = <Schema extends S.Top>(self: Schema) =>
|
|
116
124
|
pipe(
|
|
117
125
|
S.NullOr(self),
|
|
118
126
|
(s) => Object.assign(s, { withDefault: s.pipe(withDefaultConstructor(() => null)) })
|
|
119
127
|
)
|
|
120
128
|
|
|
121
|
-
export const defaultDate =
|
|
129
|
+
export const defaultDate = <Schema extends S.Top>(schema: Schema) =>
|
|
130
|
+
schema.pipe(withDefaultConstructor(() => new global.Date()))
|
|
122
131
|
|
|
123
|
-
export const defaultBool =
|
|
132
|
+
export const defaultBool = <Schema extends S.Top>(schema: Schema) => schema.pipe(withDefaultConstructor(() => false))
|
|
124
133
|
|
|
125
|
-
export const defaultNullable = (
|
|
126
|
-
s: S.Top
|
|
127
|
-
) => s.pipe(withDefaultConstructor(() => null))
|
|
134
|
+
export const defaultNullable = <Schema extends S.Top>(schema: Schema) => schema.pipe(withDefaultConstructor(() => null))
|
|
128
135
|
|
|
129
|
-
export const defaultArray =
|
|
136
|
+
export const defaultArray = <Schema extends S.Top>(schema: Schema) => schema.pipe(withDefaultConstructor(() => []))
|
|
130
137
|
|
|
131
|
-
export const defaultMap =
|
|
138
|
+
export const defaultMap = <Schema extends S.Top>(schema: Schema) => schema.pipe(withDefaultConstructor(() => new Map()))
|
|
132
139
|
|
|
133
|
-
export const defaultSet =
|
|
140
|
+
export const defaultSet = <Schema extends S.Top>(schema: Schema) => schema.pipe(withDefaultConstructor(() => new Set()))
|
|
134
141
|
|
|
135
142
|
export const withDefaultMake = <Self extends S.Top>(s: Self) => {
|
|
136
143
|
const a = Object.assign(S.decodeSync(s as any) as WithDefaults<Self>, s)
|
|
@@ -240,29 +247,30 @@ export const transformToOrFail = <To extends S.Top, From extends S.Top, RD>(
|
|
|
240
247
|
)
|
|
241
248
|
)
|
|
242
249
|
|
|
243
|
-
// TODO: v4 migration — S.declare API changed (no [self] + decode/encode pattern)
|
|
244
|
-
// Need to find v4 equivalent for contextual schema wrapping
|
|
245
250
|
export const provide = <Self extends S.Top, R>(
|
|
246
251
|
self: Self,
|
|
247
252
|
context: ServiceMap.ServiceMap<R>
|
|
248
|
-
):
|
|
253
|
+
): ProvidedCodec<Self, R> => {
|
|
249
254
|
const prov = Effect.provide(context)
|
|
250
|
-
return
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
|
|
254
|
-
self,
|
|
255
|
-
SchemaTransformation.transformOrFail({
|
|
256
|
-
decode: (n: any) => prov(SchemaParser.decodeUnknownEffect(self)(n)),
|
|
257
|
-
encode: (n: any) => prov(SchemaParser.encodeUnknownEffect(self)(n))
|
|
258
|
-
}) as any
|
|
259
|
-
) as any
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
// TODO: v4 migration — ServiceMap.pick and S.declare pattern removed
|
|
263
|
-
export const contextFromServices = <Self extends S.Top, Tags extends readonly any[]>(
|
|
264
|
-
_self: Self,
|
|
265
|
-
..._services: Tags
|
|
266
|
-
): any => {
|
|
267
|
-
throw new Error("contextFromServices: not yet migrated to v4")
|
|
255
|
+
return self.pipe(
|
|
256
|
+
S.middlewareDecoding((effect) => prov(effect)),
|
|
257
|
+
S.middlewareEncoding((effect) => prov(effect))
|
|
258
|
+
) as ProvidedCodec<Self, R>
|
|
268
259
|
}
|
|
260
|
+
export const contextFromServices = <
|
|
261
|
+
Self extends S.Top,
|
|
262
|
+
Tags extends ReadonlyArray<ServiceMap.Key<any, any>>
|
|
263
|
+
>(
|
|
264
|
+
self: Self,
|
|
265
|
+
...services: Tags
|
|
266
|
+
): Effect.Effect<
|
|
267
|
+
ProvidedCodec<Self, ServiceMap.Service.Identifier<Tags[number]>>,
|
|
268
|
+
never,
|
|
269
|
+
ServiceMap.Service.Identifier<Tags[number]>
|
|
270
|
+
> =>
|
|
271
|
+
Effect.gen(function*() {
|
|
272
|
+
const context: ServiceMap.ServiceMap<ServiceMap.Service.Identifier<Tags[number]>> = ServiceMap.pick(...services)(
|
|
273
|
+
yield* Effect.services<ServiceMap.Service.Identifier<Tags[number]>>()
|
|
274
|
+
)
|
|
275
|
+
return provide(self, context)
|
|
276
|
+
})
|
|
@@ -140,11 +140,10 @@ const minLength = 6
|
|
|
140
140
|
const maxLength = 50
|
|
141
141
|
const size = 21
|
|
142
142
|
const length = 10 * size
|
|
143
|
-
const StringIdArb = ():
|
|
143
|
+
const StringIdArb = (): S.LazyArbitrary<StringId> => (fc) =>
|
|
144
144
|
fc
|
|
145
145
|
.uint8Array({ minLength: length, maxLength: length })
|
|
146
|
-
.map((_
|
|
147
|
-
|
|
146
|
+
.map((_) => customRandom(urlAlphabet, size, (size) => _.subarray(0, size))() as StringId)
|
|
148
147
|
/**
|
|
149
148
|
* A string that is at least 6 characters long and a maximum of 50.
|
|
150
149
|
*/
|
|
@@ -155,7 +154,7 @@ export const StringId = extendM(
|
|
|
155
154
|
fromBrand(nominal<StringId>(), {
|
|
156
155
|
identifier: "StringId",
|
|
157
156
|
title: "StringId",
|
|
158
|
-
|
|
157
|
+
toArbitrary: () => (fc) => StringIdArb()(fc),
|
|
159
158
|
jsonSchema: {}
|
|
160
159
|
})
|
|
161
160
|
),
|
|
@@ -178,19 +177,21 @@ export function prefixedStringId<Brand extends StringId>() {
|
|
|
178
177
|
) => {
|
|
179
178
|
type FullPrefix = `${Prefix}${Separator}`
|
|
180
179
|
const pref = `${prefix}${separator ?? "-"}` as FullPrefix
|
|
181
|
-
const arb = ():
|
|
180
|
+
const arb = (): S.LazyArbitrary<string & Brand> => (fc) =>
|
|
182
181
|
StringIdArb()(fc).map(
|
|
183
|
-
(x
|
|
182
|
+
(x) => (pref + x.substring(0, 50 - pref.length)) as Brand
|
|
184
183
|
)
|
|
185
184
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
186
185
|
const s: S.Codec<string & Brand, string> = StringId
|
|
187
186
|
.pipe(
|
|
188
187
|
S.refine((x: string): x is string & Brand => x.startsWith(pref), {
|
|
189
|
-
arbitrary: arb,
|
|
190
188
|
identifier: name,
|
|
191
189
|
title: name
|
|
190
|
+
}),
|
|
191
|
+
S.annotate({
|
|
192
|
+
toArbitrary: () => (fc) => arb()(fc)
|
|
192
193
|
})
|
|
193
|
-
) as
|
|
194
|
+
) as S.Codec<string & Brand, string>
|
|
194
195
|
const schema = s.pipe(withDefaultMake)
|
|
195
196
|
const make = () => (pref + StringId.make().substring(0, 50 - pref.length)) as Brand
|
|
196
197
|
|
|
@@ -217,10 +218,10 @@ export const brandedStringId = <
|
|
|
217
218
|
Brand extends StringIdBrand
|
|
218
219
|
>() =>
|
|
219
220
|
withDefaultMake(
|
|
220
|
-
Object.assign(Object.create(StringId), StringId) as S.Codec<string & Brand> & {
|
|
221
|
+
Object.assign(Object.create(StringId), StringId) as S.Codec<string & Brand, string> & {
|
|
221
222
|
make: () => string & Brand
|
|
222
|
-
withDefault:
|
|
223
|
-
} & WithDefaults<S.Codec<string & Brand>>
|
|
223
|
+
withDefault: S.withConstructorDefault<S.Codec<string & Brand, string> & S.WithoutConstructorDefault>
|
|
224
|
+
} & WithDefaults<S.Codec<string & Brand, string>>
|
|
224
225
|
)
|
|
225
226
|
|
|
226
227
|
export interface PrefixedStringUtils<
|
|
@@ -232,7 +233,7 @@ export interface PrefixedStringUtils<
|
|
|
232
233
|
readonly unsafeFrom: (str: string) => Brand
|
|
233
234
|
prefixSafe: <REST extends string>(str: `${Prefix}${Separator}${REST}`) => Brand
|
|
234
235
|
readonly prefix: Prefix
|
|
235
|
-
readonly withDefault:
|
|
236
|
+
readonly withDefault: S.withConstructorDefault<S.Codec<Brand, string> & S.WithoutConstructorDefault>
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
export interface UrlBrand extends Simplify<B.Brand<"Url"> & NonEmptyStringBrand> {}
|
|
@@ -247,10 +248,12 @@ export const Url = S
|
|
|
247
248
|
.String
|
|
248
249
|
.pipe(
|
|
249
250
|
S.refine(isUrl, {
|
|
250
|
-
arbitrary: (): any => (fc: any) => fc.webUrl().map((_: any) => _ as Url),
|
|
251
251
|
identifier: "Url",
|
|
252
252
|
title: "Url",
|
|
253
253
|
jsonSchema: { format: "uri" }
|
|
254
254
|
}),
|
|
255
|
+
S.annotate({
|
|
256
|
+
toArbitrary: () => (fc) => fc.webUrl().map((_) => _ as Url)
|
|
257
|
+
}),
|
|
255
258
|
withDefaultMake
|
|
256
259
|
)
|
|
@@ -17,8 +17,10 @@ export const PhoneNumber = S
|
|
|
17
17
|
identifier: "PhoneNumber",
|
|
18
18
|
title: "PhoneNumber",
|
|
19
19
|
description: "a phone number with at least 7 digits",
|
|
20
|
-
arbitrary: () => (fc: any) => Numbers(7, 10)(fc).map((_: any) => _ as PhoneNumber),
|
|
21
20
|
jsonSchema: { format: "phone" }
|
|
22
21
|
}),
|
|
22
|
+
S.annotate({
|
|
23
|
+
toArbitrary: () => (fc) => Numbers(7, 10)(fc).map((_) => _ as PhoneNumber)
|
|
24
|
+
}),
|
|
23
25
|
withDefaultMake
|
|
24
26
|
)
|
package/src/Schema.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Array, Option, pipe, 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"
|
|
7
|
+
import { PhoneNumber as PhoneNumberT, type PhoneNumber as PhoneNumberType } from "./Schema/phoneNumber.js"
|
|
8
8
|
import type { AST } from "./Schema/schema.js"
|
|
9
9
|
import { extendM } from "./utils.js"
|
|
10
10
|
|
|
@@ -40,34 +40,39 @@ export interface WithOptionalSpan {
|
|
|
40
40
|
[SpanId]?: Tracer.Span
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const makeEmail = S.decodeSync(EmailT as any) as (value: string) => EmailType
|
|
44
|
+
const makePhoneNumber = S.decodeSync(PhoneNumberT as any) as (value: string) => PhoneNumberType
|
|
45
|
+
|
|
43
46
|
export const Email = EmailT
|
|
44
47
|
.pipe(
|
|
45
48
|
S.annotate({
|
|
46
49
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
47
|
-
|
|
50
|
+
toArbitrary: () => (fc) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(makeEmail)
|
|
48
51
|
}),
|
|
49
52
|
withDefaultMake
|
|
50
53
|
)
|
|
51
54
|
|
|
52
|
-
export type Email =
|
|
55
|
+
export type Email = EmailType
|
|
53
56
|
|
|
54
57
|
export const PhoneNumber = PhoneNumberT
|
|
55
58
|
.pipe(
|
|
56
59
|
S.annotate({
|
|
57
|
-
|
|
60
|
+
toArbitrary: () => (fc) =>
|
|
58
61
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
59
|
-
fakerArb((faker) => faker.phone.number)(fc).map(
|
|
62
|
+
fakerArb((faker) => faker.phone.number)(fc).map(makePhoneNumber)
|
|
60
63
|
}),
|
|
61
64
|
withDefaultMake
|
|
62
65
|
)
|
|
63
66
|
|
|
64
|
-
export
|
|
65
|
-
|
|
67
|
+
export type PhoneNumber = PhoneNumberType
|
|
68
|
+
|
|
69
|
+
export const makeIs = <A extends { _tag: string }, I, RD, RE>(
|
|
70
|
+
schema: S.Codec<A, I, RD, RE>
|
|
66
71
|
) => {
|
|
67
72
|
// In v4, transformations are stored as encoding on nodes, not as wrapper nodes.
|
|
68
73
|
// Union member ASTs are directly Objects (TypeLiteral equivalent).
|
|
69
74
|
if (SchemaAST.isUnion(schema.ast)) {
|
|
70
|
-
return schema.ast.types.reduce((acc
|
|
75
|
+
return schema.ast.types.reduce((acc, t: AST.AST) => {
|
|
71
76
|
if (!SchemaAST.isObjects(t)) return acc
|
|
72
77
|
const tag = Array.findFirst(t.propertySignatures, (_: any) => {
|
|
73
78
|
if (_.name === "_tag" && SchemaAST.isLiteral(_.type)) {
|
|
@@ -89,8 +94,8 @@ export const makeIs = <A extends { _tag: string }, I, R>(
|
|
|
89
94
|
throw new Error("Unsupported")
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
export const makeIsAnyOf = <A extends { _tag: string }, I,
|
|
93
|
-
schema: S.Codec<A, I,
|
|
97
|
+
export const makeIsAnyOf = <A extends { _tag: string }, I, RD, RE>(
|
|
98
|
+
schema: S.Codec<A, I, RD, RE>
|
|
94
99
|
): IsAny<A> => {
|
|
95
100
|
if (SchemaAST.isUnion(schema.ast)) {
|
|
96
101
|
return <Keys extends A["_tag"][]>(...keys: Keys) => (a: A): a is ExtractUnion<A, ElemType<Keys>> =>
|
|
@@ -106,20 +111,30 @@ export interface IsAny<A extends { _tag: string }> {
|
|
|
106
111
|
<Keys extends A["_tag"][]>(...keys: Keys): (a: A) => a is ExtractUnion<A, ElemType<Keys>>
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
const getTagLiteral = <Tag extends string>(schema: S.tag<Tag>): Tag => {
|
|
115
|
+
if (!SchemaAST.isLiteral(schema.ast)) {
|
|
116
|
+
throw new Error("Unsupported _tag schema: expected a literal AST")
|
|
117
|
+
}
|
|
118
|
+
return schema.ast.literal as Tag
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type TaggedUnionMap<Members extends readonly (S.Top & { fields: { _tag: S.tag<string> } })[]> = {
|
|
122
|
+
[Key in Members[number] as Key["fields"]["_tag"]["Type"]]: Key
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
export const taggedUnionMap = <
|
|
110
126
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
127
|
Members extends readonly (S.Top & { fields: { _tag: S.tag<string> } })[]
|
|
112
128
|
>(
|
|
113
129
|
self: Members
|
|
114
|
-
) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}, {} as any)
|
|
130
|
+
) => {
|
|
131
|
+
const out = {} as TaggedUnionMap<Members>
|
|
132
|
+
for (const key of self) {
|
|
133
|
+
const tag = getTagLiteral(key.fields._tag) as keyof TaggedUnionMap<Members>
|
|
134
|
+
out[tag] = key as TaggedUnionMap<Members>[typeof tag]
|
|
135
|
+
}
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
123
138
|
|
|
124
139
|
export const tags = <
|
|
125
140
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -127,40 +142,41 @@ export const tags = <
|
|
|
127
142
|
>(
|
|
128
143
|
self: Members
|
|
129
144
|
) =>
|
|
130
|
-
S.Literals(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
S.Literals(
|
|
146
|
+
self.map((key) => getTagLiteral(key.fields._tag)) as {
|
|
147
|
+
[Index in keyof Members]: S.Schema.Type<Members[Index]["fields"]["_tag"]>
|
|
148
|
+
}
|
|
149
|
+
) as S.Literals<
|
|
150
|
+
{
|
|
151
|
+
[Index in keyof Members]: S.Schema.Type<Members[Index]["fields"]["_tag"]>
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
|
|
155
|
+
export const ExtendTaggedUnion = <A extends { readonly _tag: string }, I, R>(
|
|
139
156
|
schema: S.Codec<A, I, R>
|
|
140
157
|
) =>
|
|
141
158
|
extendM(
|
|
142
159
|
schema,
|
|
143
160
|
(_) => ({
|
|
144
|
-
is: S.is(schema
|
|
145
|
-
isA: makeIs(_
|
|
146
|
-
isAnyOf: makeIsAnyOf(_
|
|
161
|
+
// is: S.is(schema), // only works with never DecodingServices
|
|
162
|
+
isA: makeIs(_),
|
|
163
|
+
isAnyOf: makeIsAnyOf(_) /*, map: taggedUnionMap(a) */
|
|
147
164
|
})
|
|
148
165
|
)
|
|
149
166
|
|
|
150
167
|
export const TaggedUnion = <
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
Members extends NonEmptyReadonlyArray<
|
|
169
|
+
S.Codec<{ readonly _tag: string }, any, any, any> & { fields: { _tag: S.tag<string> } }
|
|
170
|
+
>
|
|
153
171
|
>(...a: Members) =>
|
|
154
172
|
pipe(
|
|
155
173
|
S.Union(a),
|
|
156
174
|
(_) =>
|
|
157
175
|
extendM(_, (_) => ({
|
|
158
|
-
is: S.is(_
|
|
159
|
-
isA: makeIs(_
|
|
160
|
-
isAnyOf: makeIsAnyOf(_
|
|
176
|
+
// is: S.is(_), // only works with never DecodingServices
|
|
177
|
+
isA: makeIs(_),
|
|
178
|
+
isAnyOf: makeIsAnyOf(_),
|
|
161
179
|
tagMap: taggedUnionMap(a),
|
|
162
|
-
tags: tags(a
|
|
180
|
+
tags: tags(a)
|
|
163
181
|
}))
|
|
164
182
|
)
|
|
165
|
-
|
|
166
|
-
export type PhoneNumber = PhoneNumberT
|
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,4 +1,4 @@
|
|
|
1
|
-
import { type GetContextConfig, type RequestContextMapTagAny } from "../rpc/RpcContextMap.js"
|
|
1
|
+
import { type GetContextConfig, type GetEffectError, type RequestContextMapTagAny } from "../rpc/RpcContextMap.js"
|
|
2
2
|
import * as S from "../Schema.js"
|
|
3
3
|
import { AST } from "../Schema.js"
|
|
4
4
|
|
|
@@ -44,8 +44,8 @@ export const makeRpcClient = <
|
|
|
44
44
|
|
|
45
45
|
type MergeError<E> = [GeneralErrors] extends [never] ? SchemaOrFields<E> : S.Union<[SchemaOrFields<E>, GeneralErrors]>
|
|
46
46
|
type ErrorResult<C> = C extends { error: infer E } ? MergeError<E>
|
|
47
|
-
: [GeneralErrors] extends [never] ?
|
|
48
|
-
:
|
|
47
|
+
: [GeneralErrors] extends [never] ? GetEffectError<RequestContextMap["config"], C>
|
|
48
|
+
: MergeError<GetEffectError<RequestContextMap["config"], C>>
|
|
49
49
|
|
|
50
50
|
function TaggedRequest<_Self>(): {
|
|
51
51
|
<Tag extends string, Payload extends S.Struct.Fields, C extends ServiceMap>(
|
|
@@ -71,7 +71,7 @@ export const makeRpcClient = <
|
|
|
71
71
|
<Tag extends string, Payload extends S.Struct.Fields>(
|
|
72
72
|
tag: Tag,
|
|
73
73
|
fields: Payload
|
|
74
|
-
): TaggedRequestResult<Tag, Payload, S.Codec<void>, ErrorResult<
|
|
74
|
+
): TaggedRequestResult<Tag, Payload, S.Codec<void>, ErrorResult<{}>, Record<string, never>>
|
|
75
75
|
} {
|
|
76
76
|
// TODO: filter errors based on config + take care of inversion
|
|
77
77
|
const errorSchemas = Object.values(rcs.config).map((_) => _.error)
|
|
@@ -92,22 +92,9 @@ export const makeRpcClient = <
|
|
|
92
92
|
: S.Struct(config.success)
|
|
93
93
|
: ForceVoid
|
|
94
94
|
|
|
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, {
|
|
95
|
+
const RequestClass = S.TaggedClass<any>()(tag, fields)
|
|
96
|
+
Object.assign(RequestClass, {
|
|
109
97
|
_tag: tag,
|
|
110
|
-
fields: taggedFields,
|
|
111
98
|
success: successSchema,
|
|
112
99
|
error: failureSchema,
|
|
113
100
|
config
|
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
|
@@ -14,7 +14,8 @@ import { type AddMiddleware, type AnyDynamic, type RpcDynamic, type RpcMiddlewar
|
|
|
14
14
|
import * as RpcMiddlewareX from "./RpcMiddleware.js"
|
|
15
15
|
|
|
16
16
|
// adapter for effect/rpc v3 middleware provides. (in effect-smol (v4), it's just a Service Identifier, no tags.)
|
|
17
|
-
|
|
17
|
+
// hm?
|
|
18
|
+
type MakeTags<A> = A
|
|
18
19
|
|
|
19
20
|
export interface MiddlewareMaker<
|
|
20
21
|
Self,
|
package/src/rpc/RpcMiddleware.ts
CHANGED
|
@@ -226,8 +226,8 @@ export type ExtractProvides<R extends Rpc.Any, Tag extends string> = R extends
|
|
|
226
226
|
Rpc.Rpc<Tag, infer _Payload, infer _Success, infer _Error, infer _Middleware, infer _Requires> ? _Middleware extends {
|
|
227
227
|
readonly provides: infer _P
|
|
228
228
|
} ? [_P] extends [never] ? never
|
|
229
|
-
: _P extends ServiceMap.Service<infer _I, infer _S> ? _I
|
|
230
|
-
: never
|
|
229
|
+
: _P /*_P extends ServiceMap.Service<infer _I, infer _S> ? _I
|
|
230
|
+
: never */
|
|
231
231
|
: never
|
|
232
232
|
: never
|
|
233
233
|
|