@effect-app/vue 4.0.0-beta.22 → 4.0.0-beta.221
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 +1613 -0
- package/dist/commander.d.ts +634 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +1070 -0
- package/dist/confirm.d.ts +21 -0
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +26 -0
- package/dist/errorReporter.d.ts +7 -5
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +15 -6
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +46 -13
- package/dist/index.d.ts +1 -1
- package/dist/intl.d.ts +15 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +9 -0
- package/dist/lib.d.ts +8 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +35 -10
- package/dist/makeClient.d.ts +156 -339
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +225 -376
- package/dist/makeContext.d.ts +1 -1
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +9 -0
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +54 -34
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +139 -46
- package/dist/query.d.ts +20 -39
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +133 -72
- package/dist/routeParams.d.ts +3 -2
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +10 -6
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +32 -18
- package/dist/toast.d.ts +51 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +34 -0
- package/dist/withToast.d.ts +30 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +64 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +48 -50
- package/src/commander.ts +3406 -0
- package/src/{experimental/confirm.ts → confirm.ts} +12 -14
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +61 -18
- package/src/intl.ts +12 -0
- package/src/lib.ts +48 -20
- package/src/makeClient.ts +574 -1134
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +8 -5
- package/src/mutate.ts +268 -127
- package/src/query.ts +203 -183
- package/src/routeParams.ts +3 -2
- package/src/runtime.ts +46 -21
- package/src/{experimental/toast.ts → toast.ts} +15 -27
- package/src/{experimental/withToast.ts → withToast.ts} +46 -12
- package/test/Mutation.test.ts +181 -24
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/lib.test.d.ts.map +1 -0
- package/test/dist/streamFinal.test.d.ts.map +1 -0
- package/test/dist/streamFn.test.d.ts.map +1 -0
- package/test/dist/stubs.d.ts +3531 -122
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +187 -32
- package/test/form-validation-errors.test.ts +25 -20
- package/test/form.test.ts +22 -3
- package/test/lib.test.ts +240 -0
- package/test/makeClient.test.ts +292 -38
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +223 -43
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +0 -1
- package/tsconfig.json.bak +5 -2
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/dist/experimental/commander.d.ts +0 -359
- package/dist/experimental/commander.d.ts.map +0 -1
- package/dist/experimental/commander.js +0 -557
- package/dist/experimental/confirm.d.ts +0 -19
- package/dist/experimental/confirm.d.ts.map +0 -1
- package/dist/experimental/confirm.js +0 -28
- package/dist/experimental/intl.d.ts +0 -16
- package/dist/experimental/intl.d.ts.map +0 -1
- package/dist/experimental/intl.js +0 -5
- package/dist/experimental/makeUseCommand.d.ts +0 -8
- package/dist/experimental/makeUseCommand.d.ts.map +0 -1
- package/dist/experimental/makeUseCommand.js +0 -13
- package/dist/experimental/toast.d.ts +0 -47
- package/dist/experimental/toast.d.ts.map +0 -1
- package/dist/experimental/toast.js +0 -41
- package/dist/experimental/withToast.d.ts +0 -25
- package/dist/experimental/withToast.d.ts.map +0 -1
- package/dist/experimental/withToast.js +0 -45
- package/eslint.config.mjs +0 -24
- package/src/experimental/commander.ts +0 -1835
- package/src/experimental/intl.ts +0 -9
package/test/lib.test.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { computed, isProxy, isReactive, isRef, reactive, ref } from "vue"
|
|
3
|
+
import { deepToRaw } from "../src/lib.js"
|
|
4
|
+
|
|
5
|
+
type DeepMapKey = { id: string } | "list"
|
|
6
|
+
type DeepMapValue = { nestedSet: Set<{ ok: boolean } | Date> } | Array<{ count: number }>
|
|
7
|
+
type DeepSetValue = Map<string, { value: number }> | Array<{ value: number }>
|
|
8
|
+
|
|
9
|
+
const expectPlainDeep = (value: unknown): void => {
|
|
10
|
+
expect(isRef(value)).toBe(false)
|
|
11
|
+
expect(isReactive(value)).toBe(false)
|
|
12
|
+
expect(isProxy(value)).toBe(false)
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
value.forEach(expectPlainDeep)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (value instanceof Map) {
|
|
20
|
+
value.forEach((entryValue, entryKey) => {
|
|
21
|
+
expectPlainDeep(entryKey)
|
|
22
|
+
expectPlainDeep(entryValue)
|
|
23
|
+
})
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (value instanceof Set) {
|
|
28
|
+
value.forEach((entry) => {
|
|
29
|
+
expectPlainDeep(entry)
|
|
30
|
+
})
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value instanceof Date) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value && typeof value === "object") {
|
|
39
|
+
Object.values(value).forEach(expectPlainDeep)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("deepToRaw", () => {
|
|
44
|
+
it("supports non-object root inputs", () => {
|
|
45
|
+
expect(deepToRaw(1)).toBe(1)
|
|
46
|
+
expect(deepToRaw("x")).toBe("x")
|
|
47
|
+
expect(deepToRaw(null)).toBe(null)
|
|
48
|
+
expect(deepToRaw(undefined)).toBe(undefined)
|
|
49
|
+
expect(deepToRaw(ref(123))).toBe(123)
|
|
50
|
+
|
|
51
|
+
const rootArray = deepToRaw(reactive([reactive({ n: 1 }), ref(2)]))
|
|
52
|
+
expect(rootArray).toEqual([{ n: 1 }, 2])
|
|
53
|
+
expect(Array.isArray(rootArray)).toBe(true)
|
|
54
|
+
|
|
55
|
+
const rootMap = deepToRaw(
|
|
56
|
+
reactive(new Map<string, unknown>([["k", reactive({ n: 1 })], ["r", ref(2)]]))
|
|
57
|
+
)
|
|
58
|
+
expect(rootMap).toBeInstanceOf(Map)
|
|
59
|
+
expect(rootMap.get("k")).toEqual({ n: 1 })
|
|
60
|
+
expect(rootMap.get("r")).toBe(2)
|
|
61
|
+
|
|
62
|
+
const rootSet = deepToRaw(reactive(new Set([reactive({ n: 1 }), ref(2)])))
|
|
63
|
+
expect(rootSet).toBeInstanceOf(Set)
|
|
64
|
+
expect(Array.from(rootSet)).toEqual([{ n: 1 }, 2])
|
|
65
|
+
|
|
66
|
+
const date = new Date("2024-02-03T00:00:00.000Z")
|
|
67
|
+
const rootDate = deepToRaw(date)
|
|
68
|
+
expect(rootDate).toBeInstanceOf(Date)
|
|
69
|
+
expect(rootDate).not.toBe(date)
|
|
70
|
+
expect(rootDate.toISOString()).toBe(date.toISOString())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("unwraps nested objects and arrays without leaving vue proxies behind", () => {
|
|
74
|
+
const source = reactive({
|
|
75
|
+
list: [
|
|
76
|
+
reactive({
|
|
77
|
+
nested: reactive({
|
|
78
|
+
count: 1,
|
|
79
|
+
items: [reactive({ label: "a" }), reactive({ label: "b" })]
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
],
|
|
83
|
+
plain: reactive({ ok: true })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const result = deepToRaw(source)
|
|
87
|
+
|
|
88
|
+
expect(Array.isArray(result.list)).toBe(true)
|
|
89
|
+
expect(Array.isArray(result.list[0]?.nested.items)).toBe(true)
|
|
90
|
+
expect(result).toEqual({
|
|
91
|
+
list: [{ nested: { count: 1, items: [{ label: "a" }, { label: "b" }] } }],
|
|
92
|
+
plain: { ok: true }
|
|
93
|
+
})
|
|
94
|
+
expectPlainDeep(result)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("preserves maps and sets while deeply unwrapping nested entries", () => {
|
|
98
|
+
const key = reactive({ id: "key" })
|
|
99
|
+
const nestedDate = new Date("2024-01-02T03:04:05.000Z")
|
|
100
|
+
const map = reactive(
|
|
101
|
+
new Map<DeepMapKey, DeepMapValue>([
|
|
102
|
+
[key, reactive({ nestedSet: reactive(new Set([{ ok: true }, nestedDate])) })],
|
|
103
|
+
["list", reactive([{ count: 2 }])]
|
|
104
|
+
])
|
|
105
|
+
)
|
|
106
|
+
const set = reactive(
|
|
107
|
+
new Set<DeepSetValue>([
|
|
108
|
+
reactive(new Map([["deep", reactive({ value: 3 })]])),
|
|
109
|
+
reactive([{ value: 4 }])
|
|
110
|
+
])
|
|
111
|
+
)
|
|
112
|
+
const source = reactive({
|
|
113
|
+
map,
|
|
114
|
+
set
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const result = deepToRaw(source)
|
|
118
|
+
|
|
119
|
+
expect(result.map).toBeInstanceOf(Map)
|
|
120
|
+
expect(result.set).toBeInstanceOf(Set)
|
|
121
|
+
|
|
122
|
+
const entries = Array.from(result.map.entries())
|
|
123
|
+
expect(entries[0]?.[0]).toEqual({ id: "key" })
|
|
124
|
+
expect(entries[0]?.[0]).not.toBe(key)
|
|
125
|
+
expect(entries[0]?.[1]).toEqual({ nestedSet: new Set([{ ok: true }, nestedDate]) })
|
|
126
|
+
expect(entries[1]?.[1]).toEqual([{ count: 2 }])
|
|
127
|
+
|
|
128
|
+
const setValues = Array.from(result.set.values())
|
|
129
|
+
expect(setValues[0]).toBeInstanceOf(Map)
|
|
130
|
+
expect(setValues[1]).toEqual([{ value: 4 }])
|
|
131
|
+
expect((setValues[0] as Map<string, { value: number }>).get("deep")).toEqual({ value: 3 })
|
|
132
|
+
|
|
133
|
+
expectPlainDeep(result)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("keeps nested dates as dates, including dates reached through refs", () => {
|
|
137
|
+
const date = new Date("2025-06-07T08:09:10.000Z")
|
|
138
|
+
const source = reactive({
|
|
139
|
+
createdAt: date,
|
|
140
|
+
nested: reactive({
|
|
141
|
+
updatedAt: ref(date),
|
|
142
|
+
list: [ref(date)],
|
|
143
|
+
map: reactive(new Map([["at", ref(date)]])),
|
|
144
|
+
set: reactive(new Set([ref(date)]))
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const result = deepToRaw(source)
|
|
149
|
+
|
|
150
|
+
expect(result.createdAt).toBeInstanceOf(Date)
|
|
151
|
+
expect(result.nested.updatedAt).toBeInstanceOf(Date)
|
|
152
|
+
expect(result.nested.list[0]).toBeInstanceOf(Date)
|
|
153
|
+
expect(result.nested.map).toBeInstanceOf(Map)
|
|
154
|
+
expect(result.nested.set).toBeInstanceOf(Set)
|
|
155
|
+
|
|
156
|
+
const updatedAt = result.nested.updatedAt
|
|
157
|
+
const firstListDate = result.nested.list[0]
|
|
158
|
+
const mappedDate = result.nested.map.get("at")
|
|
159
|
+
const firstSetDate = Array.from(result.nested.set)[0]
|
|
160
|
+
|
|
161
|
+
if (!(updatedAt instanceof Date)) {
|
|
162
|
+
throw new Error("expected updatedAt to be a Date")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!(firstListDate instanceof Date)) {
|
|
166
|
+
throw new Error("expected first list item to be a Date")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!(mappedDate instanceof Date)) {
|
|
170
|
+
throw new Error("expected mapped date to be a Date")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!(firstSetDate instanceof Date)) {
|
|
174
|
+
throw new Error("expected first set item to be a Date")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
expect(result.createdAt.toISOString()).toBe(date.toISOString())
|
|
178
|
+
expect(updatedAt.toISOString()).toBe(date.toISOString())
|
|
179
|
+
expect(firstListDate.toISOString()).toBe(date.toISOString())
|
|
180
|
+
expect(mappedDate.toISOString()).toBe(date.toISOString())
|
|
181
|
+
expect(firstSetDate.toISOString()).toBe(date.toISOString())
|
|
182
|
+
|
|
183
|
+
expectPlainDeep(result)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("unwraps computed values nested in refs/plain objects and deepToRawes the computed result", () => {
|
|
187
|
+
const source = {
|
|
188
|
+
innerRef: ref({
|
|
189
|
+
computedValue: computed(() =>
|
|
190
|
+
reactive({
|
|
191
|
+
list: [reactive({ n: 1 }), reactive({ n: 2 })],
|
|
192
|
+
map: reactive(new Map([["k", reactive({ nested: true })]])),
|
|
193
|
+
set: reactive(new Set([reactive({ fromSet: true })]))
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
}),
|
|
197
|
+
plainComputed: computed(() => reactive({ date: ref(new Date("2025-01-01T00:00:00.000Z")) }))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const result = deepToRaw(source)
|
|
201
|
+
|
|
202
|
+
expect(result).toEqual({
|
|
203
|
+
innerRef: {
|
|
204
|
+
computedValue: {
|
|
205
|
+
list: [{ n: 1 }, { n: 2 }],
|
|
206
|
+
map: new Map([["k", { nested: true }]]),
|
|
207
|
+
set: new Set([{ fromSet: true }])
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
plainComputed: {
|
|
211
|
+
date: new Date("2025-01-01T00:00:00.000Z")
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const innerRefValue = Reflect.get(result, "innerRef")
|
|
216
|
+
if (!innerRefValue || typeof innerRefValue !== "object") {
|
|
217
|
+
throw new Error("expected innerRef to be an object")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const computedValue = Reflect.get(innerRefValue, "computedValue")
|
|
221
|
+
if (!computedValue || typeof computedValue !== "object") {
|
|
222
|
+
throw new Error("expected computedValue to be an object")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const computedMap = Reflect.get(computedValue, "map")
|
|
226
|
+
const computedSet = Reflect.get(computedValue, "set")
|
|
227
|
+
|
|
228
|
+
const plainComputedValue = Reflect.get(result, "plainComputed")
|
|
229
|
+
if (!plainComputedValue || typeof plainComputedValue !== "object") {
|
|
230
|
+
throw new Error("expected plainComputed to be an object")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const computedDate = Reflect.get(plainComputedValue, "date")
|
|
234
|
+
|
|
235
|
+
expect(computedMap).toBeInstanceOf(Map)
|
|
236
|
+
expect(computedSet).toBeInstanceOf(Set)
|
|
237
|
+
expect(computedDate).toBeInstanceOf(Date)
|
|
238
|
+
expectPlainDeep(result)
|
|
239
|
+
})
|
|
240
|
+
})
|
package/test/makeClient.test.ts
CHANGED
|
@@ -1,95 +1,349 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import { expect, expectTypeOf, it } from "@effect/vitest"
|
|
3
|
+
import { configureInvalidation, makeQueryKey } from "effect-app/client"
|
|
4
|
+
import * as S from "effect-app/Schema"
|
|
5
|
+
import * as Exit from "effect/Exit"
|
|
6
|
+
import type { CommandFromRequest } from "../src/makeClient.js"
|
|
7
|
+
import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
|
|
8
|
+
|
|
9
|
+
const somethingInvalidationResources = {
|
|
10
|
+
Something: {
|
|
11
|
+
GetSomething2: Something.GetSomething2,
|
|
12
|
+
GetSomething2WithDependencies: Something.GetSomething2WithDependencies,
|
|
13
|
+
GetSomething3: Something.GetSomething3,
|
|
14
|
+
GetSomething4: Something.GetSomething4
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
|
|
19
|
+
expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
|
|
20
|
+
expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
|
|
21
|
+
|
|
22
|
+
expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
|
|
23
|
+
expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
|
|
24
|
+
expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
|
|
25
|
+
expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
|
|
26
|
+
|
|
27
|
+
expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
|
|
28
|
+
expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">()
|
|
29
|
+
|
|
30
|
+
const invalidates = configureInvalidation<{
|
|
31
|
+
Something: typeof Something
|
|
32
|
+
SomethingElse: typeof SomethingElse
|
|
33
|
+
}>()((queryKey, { Something, SomethingElse }) => [
|
|
34
|
+
{ filters: { queryKey } },
|
|
35
|
+
{ filters: { queryKey: makeQueryKey(Something.GetSomething2) } },
|
|
36
|
+
{ filters: { queryKey: makeQueryKey(SomethingElse.GetSomething2) } }
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
expectTypeOf(invalidates.invalidatesQueries).toBeFunction()
|
|
40
|
+
configureInvalidation<{ Something: typeof Something }>()((_queryKey, { Something }) => {
|
|
41
|
+
// @ts-expect-error commands are intentionally excluded from configured resources
|
|
42
|
+
void Something.DoSomething
|
|
43
|
+
return []
|
|
11
44
|
})
|
|
12
45
|
|
|
13
|
-
const
|
|
46
|
+
const { clientFor } = useClient()
|
|
47
|
+
const client = clientFor(
|
|
48
|
+
Something,
|
|
49
|
+
undefined,
|
|
50
|
+
somethingInvalidationResources
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// only queries, no commands, and no commands who require resources; shouldn't require invalidation resources args!
|
|
54
|
+
clientFor({ GetSomething: Something.GetSomething2 })
|
|
55
|
+
|
|
56
|
+
// @ts-expect-error invalidation resources should be required when any command configures them
|
|
57
|
+
clientFor(Something)
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error invalidation resources for this module reject extra top-level resources
|
|
60
|
+
clientFor(Something, undefined, { ...somethingInvalidationResources, SomethingElse })
|
|
61
|
+
|
|
62
|
+
const doSomethingInvalidation = client.DoSomething.Request.config["invalidatesQueries"]
|
|
63
|
+
if (doSomethingInvalidation) {
|
|
64
|
+
const entries = doSomethingInvalidation(
|
|
65
|
+
["$Something"],
|
|
66
|
+
somethingInvalidationResources,
|
|
67
|
+
{ id: "abc" },
|
|
68
|
+
Exit.succeed(123)
|
|
69
|
+
)
|
|
70
|
+
expect(Array.isArray(entries)).toBe(true)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SomethingCommand = SomethingReq.Command
|
|
74
|
+
|
|
75
|
+
class TypeInferenceWithSuccess extends SomethingCommand<TypeInferenceWithSuccess>()("TypeInferenceWithSuccess", {
|
|
76
|
+
id: S.String
|
|
77
|
+
}, {
|
|
78
|
+
success: S.FiniteFromString
|
|
79
|
+
}, (_queryKey, _resources, input, result) => {
|
|
80
|
+
expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
|
|
81
|
+
expectTypeOf(result).toEqualTypeOf<Exit.Exit<number, never>>()
|
|
82
|
+
return []
|
|
83
|
+
}) {}
|
|
84
|
+
void TypeInferenceWithSuccess
|
|
85
|
+
|
|
86
|
+
class TypeInferenceWithoutSuccess extends SomethingCommand<TypeInferenceWithoutSuccess>()(
|
|
87
|
+
"TypeInferenceWithoutSuccess",
|
|
88
|
+
{
|
|
89
|
+
id: S.String
|
|
90
|
+
},
|
|
91
|
+
{},
|
|
92
|
+
(_queryKey, _resources, input, result) => {
|
|
93
|
+
expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
|
|
94
|
+
expectTypeOf(result).toEqualTypeOf<Exit.Exit<void, never>>()
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
) {}
|
|
98
|
+
void TypeInferenceWithoutSuccess
|
|
99
|
+
|
|
100
|
+
type MixedResources = {
|
|
101
|
+
Something: typeof Something
|
|
102
|
+
Misc: {
|
|
103
|
+
value: number
|
|
104
|
+
GetSomething2: typeof Something.GetSomething2
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class TypeInferenceResourceFiltering extends SomethingCommand<
|
|
109
|
+
TypeInferenceResourceFiltering,
|
|
110
|
+
MixedResources
|
|
111
|
+
>()("TypeInferenceResourceFiltering", {
|
|
112
|
+
id: S.String
|
|
113
|
+
}, {
|
|
114
|
+
success: S.FiniteFromString
|
|
115
|
+
}, (_queryKey, resources, _input, _result) => {
|
|
116
|
+
expectTypeOf(resources.Something.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
|
|
117
|
+
expectTypeOf(resources.Misc.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
|
|
118
|
+
|
|
119
|
+
// @ts-expect-error commands must be filtered from invalidation resources
|
|
120
|
+
void resources.Something.DoSomething
|
|
121
|
+
// @ts-expect-error non-query values must be filtered from invalidation resources
|
|
122
|
+
void resources.Misc.value
|
|
123
|
+
|
|
124
|
+
return []
|
|
125
|
+
}) {}
|
|
126
|
+
void TypeInferenceResourceFiltering
|
|
127
|
+
|
|
128
|
+
type WithSuccessInvalidation = NonNullable<typeof TypeInferenceWithSuccess.config.invalidatesQueries> // @ts-expect-error input should be required when command payload is non-empty
|
|
129
|
+
;((_queryKey, _resources) => []) satisfies WithSuccessInvalidation
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("clientFor handler shape — props variants", () => {
|
|
133
|
+
const { clientFor } = useClient()
|
|
134
|
+
const client = clientFor(
|
|
135
|
+
Something,
|
|
136
|
+
undefined,
|
|
137
|
+
somethingInvalidationResources
|
|
138
|
+
)
|
|
139
|
+
expect(client).toBeDefined()
|
|
140
|
+
|
|
141
|
+
// no-props (no fields): handler is (i: void) => Effect — callable without arg
|
|
142
|
+
expectTypeOf(client.DoNoProps.handler).toBeFunction()
|
|
143
|
+
client.DoNoProps.handler()
|
|
144
|
+
|
|
145
|
+
// no-props: request mirrors handler — (i: void) => Effect, callable without arg
|
|
146
|
+
expectTypeOf(client.DoNoProps.request).toBeFunction()
|
|
147
|
+
client.DoNoProps.request()
|
|
148
|
+
// optional-only: any fields → function handler. Input matches `make`, which for
|
|
149
|
+
// fully-optional payload is omittable.
|
|
150
|
+
expectTypeOf(client.DoOptionalOnly.handler).toBeFunction()
|
|
151
|
+
// arg may be omitted entirely
|
|
152
|
+
client.DoOptionalOnly.handler()
|
|
153
|
+
// or supplied with all-optional payload
|
|
154
|
+
client.DoOptionalOnly.handler({})
|
|
155
|
+
client.DoOptionalOnly.handler({ name: "x" })
|
|
156
|
+
|
|
157
|
+
// required-only: function, `id` required
|
|
158
|
+
expectTypeOf(client.DoRequiredOnly.handler).toBeFunction()
|
|
159
|
+
client.DoRequiredOnly.handler({ id: "x" })
|
|
160
|
+
// @ts-expect-error id is required
|
|
161
|
+
client.DoRequiredOnly.handler({})
|
|
162
|
+
// @ts-expect-error arg cannot be omitted
|
|
163
|
+
client.DoRequiredOnly.handler()
|
|
164
|
+
|
|
165
|
+
// mixed: id required, name optional
|
|
166
|
+
expectTypeOf(client.DoMixed.handler).toBeFunction()
|
|
167
|
+
client.DoMixed.handler({ id: "x" })
|
|
168
|
+
client.DoMixed.handler({ id: "x", name: "y" })
|
|
169
|
+
// @ts-expect-error id required
|
|
170
|
+
client.DoMixed.handler({ name: "y" })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("CommandFromRequest input shape — props variants", () => {
|
|
174
|
+
type NoPropsArg = Parameters<CommandFromRequest<typeof Something.DoNoProps>["handle"]>[0]
|
|
175
|
+
|
|
176
|
+
// no-props (no fields) → void input; void parameter is implicitly optional, so handle() works
|
|
177
|
+
expectTypeOf<NoPropsArg>().toBeVoid()
|
|
178
|
+
|
|
179
|
+
// type-only assignability checks for the remaining variants
|
|
180
|
+
if (false as boolean) {
|
|
181
|
+
const noProps = null as unknown as CommandFromRequest<typeof Something.DoNoProps>
|
|
182
|
+
const optOnly = null as unknown as CommandFromRequest<typeof Something.DoOptionalOnly>
|
|
183
|
+
const reqOnly = null as unknown as CommandFromRequest<typeof Something.DoRequiredOnly>
|
|
184
|
+
const mixed = null as unknown as CommandFromRequest<typeof Something.DoMixed>
|
|
185
|
+
|
|
186
|
+
// no-props → void param, calling without args is fine
|
|
187
|
+
noProps.handle()
|
|
188
|
+
|
|
189
|
+
// optional-only → matches `make` (fully optional, arg omittable)
|
|
190
|
+
optOnly.handle()
|
|
191
|
+
optOnly.handle({})
|
|
192
|
+
optOnly.handle({ name: "x" })
|
|
193
|
+
|
|
194
|
+
// required-only → id required
|
|
195
|
+
reqOnly.handle({ id: "x" })
|
|
196
|
+
// @ts-expect-error id required
|
|
197
|
+
reqOnly.handle({})
|
|
198
|
+
|
|
199
|
+
// mixed → id required, name optional
|
|
200
|
+
mixed.handle({ id: "x" })
|
|
201
|
+
mixed.handle({ id: "x", name: "y" })
|
|
202
|
+
// @ts-expect-error id required
|
|
203
|
+
mixed.handle({ name: "y" })
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it.skip("query type tests", () => {
|
|
208
|
+
const { clientFor } = useClient()
|
|
209
|
+
const client = clientFor(
|
|
210
|
+
Something,
|
|
211
|
+
() => ({
|
|
212
|
+
GetSomething2WithDependencies: (queryKey) => [
|
|
213
|
+
{ filters: { queryKey } },
|
|
214
|
+
{
|
|
215
|
+
filters: {
|
|
216
|
+
queryKey: makeQueryKey(
|
|
217
|
+
SomethingElse
|
|
218
|
+
.GetSomething2
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}),
|
|
224
|
+
somethingInvalidationResources
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
const q = client.GetSomething2.query
|
|
228
|
+
|
|
229
|
+
const [, z] = q({ id: "a" })
|
|
14
230
|
const valz = z.value
|
|
15
231
|
expectTypeOf(valz).toEqualTypeOf<number | undefined>()
|
|
16
232
|
|
|
17
|
-
const [, a] =
|
|
233
|
+
const [, a] = q({ id: "a" }, { placeholderData: () => 123 })
|
|
18
234
|
const val1 = a.value
|
|
19
235
|
expectTypeOf(val1).toEqualTypeOf<number>()
|
|
20
236
|
|
|
21
|
-
const [, bbbb] =
|
|
237
|
+
const [, bbbb] = q({ id: "a" }, { select: (data) => data.toString() })
|
|
22
238
|
const val = bbbb.value
|
|
23
239
|
expectTypeOf(val).toEqualTypeOf<string | undefined>()
|
|
24
240
|
|
|
25
|
-
const [, ccc] =
|
|
241
|
+
const [, ccc] = q({ id: "a" }, { placeholderData: () => 123, select: (data) => data.toString() })
|
|
26
242
|
const val2 = ccc.value
|
|
27
243
|
expectTypeOf(val2).toEqualTypeOf<string>()
|
|
28
244
|
|
|
29
|
-
const [, ddd] =
|
|
245
|
+
const [, ddd] = q({ id: "a" }, { initialData: 123, select: (data) => data.toString() })
|
|
30
246
|
const val3 = ddd.value
|
|
31
247
|
expectTypeOf(val3).toEqualTypeOf<string>()
|
|
32
248
|
|
|
33
|
-
const [, eee] =
|
|
249
|
+
const [, eee] = q({ id: "a" }, { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
|
|
34
250
|
const val4 = eee.value
|
|
35
251
|
expectTypeOf(val4).toEqualTypeOf<string>()
|
|
36
252
|
})
|
|
37
253
|
|
|
38
254
|
it.skip("works", () => {
|
|
39
|
-
const { clientFor
|
|
40
|
-
const client = clientFor(Something)
|
|
255
|
+
const { clientFor } = useClient()
|
|
256
|
+
const client = clientFor(Something, undefined, somethingInvalidationResources)
|
|
41
257
|
const Command = useExperimental()
|
|
42
258
|
|
|
43
259
|
// just for jsdoc / type testing.
|
|
44
|
-
const a0 = client.GetSomething2(null as any)
|
|
45
|
-
const a00 = client.
|
|
260
|
+
const a0 = client.GetSomething2.request(null as any)
|
|
261
|
+
const a00 = client.DoSomething.mutate(null as any)
|
|
46
262
|
const a = client.GetSomething2.suspense(null as any)
|
|
47
263
|
const b = client.GetSomething2.query(null as any)
|
|
48
264
|
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
265
|
+
const de = client.GetSomething3.handler(null as any)
|
|
266
|
+
const de2 = client.GetSomething3.handler({ id: null })
|
|
267
|
+
|
|
268
|
+
const de3 = client.GetSomething4.handler()
|
|
269
|
+
void client.GetSomething4.handler
|
|
52
270
|
|
|
271
|
+
// @ts-expect-error query requests no longer expose command helpers
|
|
53
272
|
const e = client.GetSomething2.wrap(null as any)
|
|
273
|
+
// @ts-expect-error query requests no longer expose command helpers
|
|
54
274
|
const f = client.GetSomething2.fn(null as any)
|
|
55
275
|
|
|
56
|
-
// @ts-expect-error
|
|
57
|
-
const e0 = client.GetSomething2WithDependencies.wrap
|
|
58
|
-
// @ts-expect-error
|
|
59
|
-
const e000 = Command.wrap(client.GetSomething2WithDependencies)
|
|
60
|
-
const e00 = client.GetSomething2WithDependencies.
|
|
61
|
-
null as any
|
|
62
|
-
)
|
|
63
|
-
const e0000 =
|
|
64
|
-
Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect<number, never, never>).handle
|
|
276
|
+
// @ts-expect-error query requests no longer expose command helpers
|
|
277
|
+
const e0 = client.GetSomething2WithDependencies.wrap
|
|
278
|
+
// @ts-expect-error query request does not match Command.wrap mutation signature
|
|
279
|
+
const e000 = Command.wrap(client.GetSomething2WithDependencies)
|
|
280
|
+
const e00 = client.GetSomething2WithDependencies.request(null as any)
|
|
65
281
|
// @ts-expect-error dependencies required that are not provided
|
|
66
282
|
const e1 = client.GetSomething2WithDependencies.suspense(null as any)
|
|
67
283
|
// @ts-expect-error dependencies required that are not provided
|
|
68
284
|
const e2 = client.GetSomething2WithDependencies.query(null as any)
|
|
285
|
+
// @ts-expect-error query requests no longer expose command helpers
|
|
69
286
|
const f0 = client.GetSomething2WithDependencies.fn(null as any)
|
|
70
287
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
288
|
+
const g0 = client.DoSomething.wrap(null as any)
|
|
289
|
+
const g = client.DoSomething.mutate.wrap(null as any)
|
|
290
|
+
const g1 = client.DoSomething.mutate.project(S.String)
|
|
291
|
+
const g2 = g1(null as any)
|
|
292
|
+
const g3 = g1.wrap(null as any)
|
|
293
|
+
const g4 = client.helpers.doSomethingMutation.project(S.String)
|
|
294
|
+
const g5 = g4(null as any)
|
|
295
|
+
const g6 = g4.wrap(null as any)
|
|
296
|
+
const h = client.DoSomething.mutate.fn(null as any)
|
|
297
|
+
|
|
298
|
+
// projection
|
|
299
|
+
// GetSomething2 uses FiniteFromString, that means Codec is String -> Number
|
|
300
|
+
// when we project that to S.String, it should work as the encoded shapes are identical
|
|
301
|
+
// aka, when we project, we skip decoding with the original codec, and instead use the provided one
|
|
302
|
+
// we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
|
|
303
|
+
const projected = client.GetSomething2.project(S.String)
|
|
304
|
+
// @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
|
|
305
|
+
client.GetSomething2.project(S.Number)
|
|
306
|
+
const p0 = projected.request(null as any)
|
|
307
|
+
|
|
308
|
+
// struct example: success schema encodes to { a: string | null }
|
|
309
|
+
// good: projection schema also expects { a: string | null } on the encoded side
|
|
310
|
+
const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
|
|
311
|
+
// bad: { a: S.String } has encoded type { a: string } — does not accept null
|
|
312
|
+
// @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
|
|
313
|
+
client.GetStructNullable.project(S.Struct({ a: S.String }))
|
|
314
|
+
|
|
315
|
+
const p00 = projected.query(null as any)
|
|
316
|
+
const p = projected.suspense(null as any)
|
|
73
317
|
|
|
74
318
|
expect(true).toBe(true)
|
|
75
319
|
console.log({
|
|
76
320
|
a,
|
|
77
321
|
a0,
|
|
78
322
|
a00,
|
|
79
|
-
c0,
|
|
80
323
|
b,
|
|
81
|
-
c,
|
|
82
|
-
d,
|
|
83
324
|
e,
|
|
325
|
+
de,
|
|
326
|
+
de2,
|
|
327
|
+
de3,
|
|
84
328
|
e0,
|
|
85
329
|
e00,
|
|
86
330
|
e000,
|
|
87
|
-
e0000,
|
|
88
331
|
e1,
|
|
89
332
|
e2,
|
|
90
333
|
f,
|
|
91
334
|
f0,
|
|
335
|
+
g0,
|
|
92
336
|
g,
|
|
93
|
-
|
|
337
|
+
g1,
|
|
338
|
+
g2,
|
|
339
|
+
g3,
|
|
340
|
+
g4,
|
|
341
|
+
g5,
|
|
342
|
+
g6,
|
|
343
|
+
h,
|
|
344
|
+
p0,
|
|
345
|
+
p00,
|
|
346
|
+
p,
|
|
347
|
+
projectedStruct
|
|
94
348
|
})
|
|
95
349
|
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime and type tests for the `final` schema on stream requests.
|
|
3
|
+
*
|
|
4
|
+
* The `final` option on a stream request schema lets callers model which type
|
|
5
|
+
* the last emitted stream element is.
|
|
6
|
+
*/
|
|
7
|
+
import { expect, it } from "@effect/vitest"
|
|
8
|
+
import * as Effect from "effect-app/Effect"
|
|
9
|
+
import * as S from "effect-app/Schema"
|
|
10
|
+
import * as Stream from "effect/Stream"
|
|
11
|
+
import { asStreamResult } from "../src/mutate.js"
|
|
12
|
+
import { ExportComplete, OperationProgress, Something } from "./stubs.js"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// asStreamResult — low-level primitive, always returns void
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
it.live("asStreamResult returns void and updates ref with each element", () =>
|
|
19
|
+
Effect.gen(function*() {
|
|
20
|
+
const events: number[] = [1, 2, 3]
|
|
21
|
+
const [ref, execute] = asStreamResult(() => Stream.fromIterable(events))
|
|
22
|
+
|
|
23
|
+
yield* execute()
|
|
24
|
+
|
|
25
|
+
// ref should hold the last emitted value
|
|
26
|
+
expect(ref.value._tag).toBe("Success")
|
|
27
|
+
if (ref.value._tag === "Success") {
|
|
28
|
+
expect(ref.value.value).toBe(3)
|
|
29
|
+
expect(ref.value.waiting).toBe(false)
|
|
30
|
+
}
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
it("stream request without final: .final is undefined", () => {
|
|
34
|
+
const req = Something.StreamWithoutFinal
|
|
35
|
+
expect((req as any).final).toBeUndefined()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("stream request with final: .final holds the ExportComplete schema", () => {
|
|
39
|
+
const req = Something.StreamWithFinal
|
|
40
|
+
expect((req as any).final).toBeDefined()
|
|
41
|
+
// Verify the schema decodes correctly
|
|
42
|
+
const decoded = S.decodeUnknownSync((req as any).final)({ _tag: "ExportComplete", fileUrl: "https://x.com" })
|
|
43
|
+
expect(decoded).toBeInstanceOf(ExportComplete)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Runtime: last stream value is accessible via the reactive ref after stream ends
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
it.live("last stream value is accessible via reactive ref after stream ends", () =>
|
|
51
|
+
Effect.gen(function*() {
|
|
52
|
+
const progress = new OperationProgress({ completed: 1 as S.NonNegativeInt, total: 2 as S.NonNegativeInt })
|
|
53
|
+
const complete = new ExportComplete({ fileUrl: "https://example.com/file.csv" as S.NonEmptyString })
|
|
54
|
+
|
|
55
|
+
const [ref, execute] = asStreamResult(() => Stream.make(progress, complete))
|
|
56
|
+
|
|
57
|
+
yield* execute()
|
|
58
|
+
|
|
59
|
+
expect(ref.value._tag).toBe("Success")
|
|
60
|
+
if (ref.value._tag === "Success") {
|
|
61
|
+
expect(ref.value.value).toBe(complete)
|
|
62
|
+
expect(ref.value.waiting).toBe(false)
|
|
63
|
+
}
|
|
64
|
+
}))
|