ts-procedures 7.0.0-beta.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +38 -0
- package/build/client/bind-callable.test.d.ts +1 -0
- package/build/client/bind-callable.test.js +132 -0
- package/build/client/bind-callable.test.js.map +1 -0
- package/build/client/index.js +14 -0
- package/build/client/index.js.map +1 -1
- package/build/client/types.d.ts +21 -0
- package/build/codegen/bundle-size.test.js +3 -1
- package/build/codegen/bundle-size.test.js.map +1 -1
- package/build/codegen/e2e.test.js +5 -6
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-client-runtime.js +1 -0
- package/build/codegen/emit-client-runtime.js.map +1 -1
- package/build/codegen/emit-scope.js +25 -81
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/emit-scope.test.js +70 -69
- package/build/codegen/emit-scope.test.js.map +1 -1
- package/docs/client-and-codegen.md +1 -1
- package/package.json +1 -1
- package/src/client/bind-callable.test.ts +137 -0
- package/src/client/index.ts +21 -0
- package/src/client/types.ts +25 -0
- package/src/codegen/bundle-size.test.ts +3 -1
- package/src/codegen/e2e.test.ts +5 -6
- package/src/codegen/emit-client-runtime.ts +1 -0
- package/src/codegen/emit-scope.test.ts +70 -69
- package/src/codegen/emit-scope.ts +26 -84
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { createClient } from './index.js'
|
|
3
|
+
import { ClientHttpError, ClientNetworkError } from './errors.js'
|
|
4
|
+
import type { ClientAdapter } from './types.js'
|
|
5
|
+
|
|
6
|
+
const okAdapter: ClientAdapter = {
|
|
7
|
+
request: vi.fn(async () => ({ status: 200, headers: {}, body: { id: '42' } })),
|
|
8
|
+
stream: vi.fn(async () => { throw new Error('n/a') }),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('ClientInstance.bindCallable', () => {
|
|
12
|
+
it('returns a callable that throws on failure', async () => {
|
|
13
|
+
const client = createClient({
|
|
14
|
+
adapter: { request: vi.fn(async () => { throw new TypeError('x') }), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
15
|
+
basePath: 'https://api.x',
|
|
16
|
+
scopes: (c) => ({
|
|
17
|
+
getUser: c.bindCallable<{ id: string }, { id: string }>({
|
|
18
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
19
|
+
}),
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
await expect(client.getUser({ id: '1' })).rejects.toBeInstanceOf(ClientNetworkError)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('exposes .safe returning ResultNoTyped', async () => {
|
|
26
|
+
const client = createClient({
|
|
27
|
+
adapter: { request: vi.fn(async () => ({ status: 500, headers: {}, body: {} })), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
28
|
+
basePath: 'https://api.x',
|
|
29
|
+
scopes: (c) => ({
|
|
30
|
+
getUser: c.bindCallable<{ id: string }, { id: string }>({
|
|
31
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
const r = await client.getUser.safe({ id: '1' })
|
|
36
|
+
expect(r.ok).toBe(false)
|
|
37
|
+
if (!r.ok) {
|
|
38
|
+
expect(r.kind).toBe('http')
|
|
39
|
+
expect(r.error).toBeInstanceOf(ClientHttpError)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('preserves descriptor.name as fn.name for stack traces', () => {
|
|
44
|
+
const client = createClient({
|
|
45
|
+
adapter: okAdapter,
|
|
46
|
+
basePath: 'https://api.x',
|
|
47
|
+
scopes: (c) => ({
|
|
48
|
+
getUser: c.bindCallable<{ id: string }, { id: string }>({
|
|
49
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
expect(client.getUser.name).toBe('GetUser')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('resolves the response value on success', async () => {
|
|
57
|
+
const client = createClient({
|
|
58
|
+
adapter: { request: vi.fn(async () => ({ status: 200, headers: {}, body: { id: '42' } })), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
59
|
+
basePath: 'https://api.x',
|
|
60
|
+
scopes: (c) => ({
|
|
61
|
+
getUser: c.bindCallable<{ id: string }, { id: string }>({
|
|
62
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
63
|
+
}),
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
const result = await client.getUser({ id: '1' })
|
|
67
|
+
expect(result).toEqual({ id: '42' })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('.safe resolves with ok: true on success', async () => {
|
|
71
|
+
const client = createClient({
|
|
72
|
+
adapter: { request: vi.fn(async () => ({ status: 200, headers: {}, body: { id: '42' } })), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
73
|
+
basePath: 'https://api.x',
|
|
74
|
+
scopes: (c) => ({
|
|
75
|
+
getUser: c.bindCallable<{ id: string }, { id: string }>({
|
|
76
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
const r = await client.getUser.safe({ id: '1' })
|
|
81
|
+
expect(r.ok).toBe(true)
|
|
82
|
+
if (r.ok) {
|
|
83
|
+
expect(r.value).toEqual({ id: '42' })
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('ClientInstance.bindCallableTyped', () => {
|
|
89
|
+
it('exposes .safe returning Result with typed errors', async () => {
|
|
90
|
+
class ApiUseCaseError extends Error {
|
|
91
|
+
constructor(public status: number) { super('use-case') }
|
|
92
|
+
}
|
|
93
|
+
const client = createClient({
|
|
94
|
+
adapter: { request: vi.fn(async () => ({ status: 500, headers: {}, body: {} })), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
95
|
+
basePath: 'https://api.x',
|
|
96
|
+
scopes: (c) => ({
|
|
97
|
+
getUser: c.bindCallableTyped<{ id: string }, { id: string }, ApiUseCaseError>({
|
|
98
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
99
|
+
}),
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
const r = await client.getUser.safe({ id: '1' })
|
|
103
|
+
expect(r.ok).toBe(false)
|
|
104
|
+
// Falls through to 'http' since no registry was wired — the test verifies
|
|
105
|
+
// the .safe surface exists, not the typed dispatch path.
|
|
106
|
+
if (!r.ok) {
|
|
107
|
+
expect(['http', 'typed', 'network', 'timeout', 'aborted', 'parse', 'usage', 'unknown']).toContain(r.kind)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('preserves descriptor.name as fn.name for stack traces', () => {
|
|
112
|
+
const client = createClient({
|
|
113
|
+
adapter: okAdapter,
|
|
114
|
+
basePath: 'https://api.x',
|
|
115
|
+
scopes: (c) => ({
|
|
116
|
+
getUser: c.bindCallableTyped<{ id: string }, { id: string }, Error>({
|
|
117
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
expect(client.getUser.name).toBe('GetUser')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('resolves the response value on success', async () => {
|
|
125
|
+
const client = createClient({
|
|
126
|
+
adapter: { request: vi.fn(async () => ({ status: 200, headers: {}, body: { id: '99' } })), stream: vi.fn(async () => { throw new Error('n/a') }) },
|
|
127
|
+
basePath: 'https://api.x',
|
|
128
|
+
scopes: (c) => ({
|
|
129
|
+
getUser: c.bindCallableTyped<{ id: string }, { id: string }, Error>({
|
|
130
|
+
name: 'GetUser', scope: 'users', path: '/users/:id', method: 'get', kind: 'rpc',
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
const result = await client.getUser({ id: '1' })
|
|
135
|
+
expect(result).toEqual({ id: '99' })
|
|
136
|
+
})
|
|
137
|
+
})
|
package/src/client/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ProcedureCallOptions,
|
|
9
9
|
TypedStream,
|
|
10
10
|
Result,
|
|
11
|
+
ResultNoTyped,
|
|
11
12
|
} from './types.js'
|
|
12
13
|
|
|
13
14
|
// ── createClient ──────────────────────────────────────────
|
|
@@ -72,6 +73,26 @@ export function createClient<TScopes>(config: CreateClientConfig<TScopes>): TSco
|
|
|
72
73
|
})
|
|
73
74
|
},
|
|
74
75
|
|
|
76
|
+
bindCallable<TParams, TResponse>(descriptor: Omit<CallDescriptor, 'params'>) {
|
|
77
|
+
const call = (params: TParams, options?: ProcedureCallOptions) =>
|
|
78
|
+
instance.call<TResponse>({ ...descriptor, params }, options)
|
|
79
|
+
Object.defineProperty(call, 'name', { value: descriptor.name, configurable: true })
|
|
80
|
+
return Object.assign(call, {
|
|
81
|
+
safe: (params: TParams, options?: ProcedureCallOptions) =>
|
|
82
|
+
instance.safeCall<TResponse>({ ...descriptor, params }, options) as Promise<ResultNoTyped<TResponse>>,
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
bindCallableTyped<TParams, TResponse, ETyped>(descriptor: Omit<CallDescriptor, 'params'>) {
|
|
87
|
+
const call = (params: TParams, options?: ProcedureCallOptions) =>
|
|
88
|
+
instance.call<TResponse>({ ...descriptor, params }, options)
|
|
89
|
+
Object.defineProperty(call, 'name', { value: descriptor.name, configurable: true })
|
|
90
|
+
return Object.assign(call, {
|
|
91
|
+
safe: (params: TParams, options?: ProcedureCallOptions) =>
|
|
92
|
+
instance.safeCall<TResponse, ETyped>({ ...descriptor, params }, options),
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
|
|
75
96
|
stream<TYield, TReturn>(
|
|
76
97
|
descriptor: StreamDescriptor,
|
|
77
98
|
options?: ProcedureCallOptions,
|
package/src/client/types.ts
CHANGED
|
@@ -237,6 +237,31 @@ export interface ClientInstance {
|
|
|
237
237
|
options?: ProcedureCallOptions,
|
|
238
238
|
): Promise<Result<TResponse, ETyped>>
|
|
239
239
|
stream<TYield, TReturn>(descriptor: StreamDescriptor, options?: ProcedureCallOptions): TypedStream<TYield, TReturn>
|
|
240
|
+
/**
|
|
241
|
+
* Wires a callable with a `.safe` sibling for routes without declared typed errors.
|
|
242
|
+
* Used by codegen to produce a compact one-line callable per route.
|
|
243
|
+
*
|
|
244
|
+
* The returned function's `.name` is set to `descriptor.name` for nicer stack traces.
|
|
245
|
+
*/
|
|
246
|
+
bindCallable<TParams, TResponse>(
|
|
247
|
+
descriptor: Omit<CallDescriptor, 'params'>,
|
|
248
|
+
): {
|
|
249
|
+
(params: TParams, options?: ProcedureCallOptions): Promise<TResponse>
|
|
250
|
+
safe(params: TParams, options?: ProcedureCallOptions): Promise<ResultNoTyped<TResponse>>
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Wires a callable with a `.safe` sibling for routes with declared typed errors.
|
|
254
|
+
* The `.safe` form returns `Result<TResponse, ETyped>` where `ETyped` is the
|
|
255
|
+
* union of route-declared error classes.
|
|
256
|
+
*
|
|
257
|
+
* The returned function's `.name` is set to `descriptor.name` for nicer stack traces.
|
|
258
|
+
*/
|
|
259
|
+
bindCallableTyped<TParams, TResponse, ETyped>(
|
|
260
|
+
descriptor: Omit<CallDescriptor, 'params'>,
|
|
261
|
+
): {
|
|
262
|
+
(params: TParams, options?: ProcedureCallOptions): Promise<TResponse>
|
|
263
|
+
safe(params: TParams, options?: ProcedureCallOptions): Promise<Result<TResponse, ETyped>>
|
|
264
|
+
}
|
|
240
265
|
}
|
|
241
266
|
|
|
242
267
|
// ── createClient Config ──────────────────────────────────
|
|
@@ -59,7 +59,9 @@ describe('bundle size budget', () => {
|
|
|
59
59
|
// and link back to spec section "Tests / item 10". For now this guard
|
|
60
60
|
// catches catastrophic regressions only, not subtle bloat.
|
|
61
61
|
//
|
|
62
|
-
// PERROUTEDELTA_BASELINE: 613.2
|
|
62
|
+
// PERROUTEDELTA_BASELINE: 613.2 (7.0.0-beta.0, Object.assign inline emission)
|
|
63
|
+
// After bindCallable refactor (beta.1): baseline dropped to 218.8 (~64% reduction)
|
|
64
|
+
// — Object.assign + duplicated descriptor removed; one helper call per route.
|
|
63
65
|
expect(perRouteDelta).toBeLessThan(1500)
|
|
64
66
|
})
|
|
65
67
|
|
package/src/codegen/e2e.test.ts
CHANGED
|
@@ -223,12 +223,12 @@ describe('E2E: generateClient full pipeline', () => {
|
|
|
223
223
|
expect(content).toContain('bindUsersScope')
|
|
224
224
|
})
|
|
225
225
|
|
|
226
|
-
it('users.ts uses client.
|
|
226
|
+
it('users.ts uses client.bindCallable for RPC route', async () => {
|
|
227
227
|
tmpDir = makeTmpDir()
|
|
228
228
|
await generateClient({ envelope, outDir: tmpDir })
|
|
229
229
|
|
|
230
230
|
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
231
|
-
expect(content).toContain('client.
|
|
231
|
+
expect(content).toContain('client.bindCallable<')
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
// ── events.ts — Stream route ───────────────────────────────────────────────
|
|
@@ -764,10 +764,9 @@ void run
|
|
|
764
764
|
await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
|
|
765
765
|
|
|
766
766
|
const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
|
|
767
|
-
|
|
768
|
-
expect(content).toContain('
|
|
769
|
-
expect(content).toContain('
|
|
770
|
-
expect(content).toContain('Promise<Users.UpdateUser.Response>')
|
|
767
|
+
// New emission: client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>({...})
|
|
768
|
+
expect(content).toContain('client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>')
|
|
769
|
+
expect(content).toContain('client.bindCallable<Users.UpdateUser.Params, Users.UpdateUser.Response>')
|
|
771
770
|
})
|
|
772
771
|
|
|
773
772
|
it('events.ts wraps stream types in namespace', async () => {
|
|
@@ -272,7 +272,7 @@ describe('emitScopeFile', () => {
|
|
|
272
272
|
|
|
273
273
|
it('imports ClientInstance and ProcedureCallOptions but not TypedStream', async () => {
|
|
274
274
|
const output = await emitScopeFile(rpcGroup)
|
|
275
|
-
expect(output).toContain("import type { ClientInstance, ProcedureCallOptions
|
|
275
|
+
expect(output).toContain("import type { ClientInstance, ProcedureCallOptions } from 'ts-procedures/client'")
|
|
276
276
|
expect(output).not.toContain('TypedStream')
|
|
277
277
|
})
|
|
278
278
|
|
|
@@ -291,10 +291,10 @@ describe('emitScopeFile', () => {
|
|
|
291
291
|
expect(output).toContain('export function bindUsersScope(client: ClientInstance)')
|
|
292
292
|
})
|
|
293
293
|
|
|
294
|
-
it('callable uses client.
|
|
294
|
+
it('callable uses client.bindCallable with kind: rpc', async () => {
|
|
295
295
|
const output = await emitScopeFile(rpcGroup)
|
|
296
296
|
expect(output).toContain("kind: 'rpc'")
|
|
297
|
-
expect(output).toContain('client.
|
|
297
|
+
expect(output).toContain('client.bindCallable<')
|
|
298
298
|
})
|
|
299
299
|
|
|
300
300
|
it('callable includes JSDoc with method and path', async () => {
|
|
@@ -323,10 +323,10 @@ describe('emitScopeFile', () => {
|
|
|
323
323
|
expect(output).toContain('export type UpdatePostResponse')
|
|
324
324
|
})
|
|
325
325
|
|
|
326
|
-
it('callable uses client.
|
|
326
|
+
it('callable uses client.bindCallable with kind: api', async () => {
|
|
327
327
|
const output = await emitScopeFile(apiGroup)
|
|
328
328
|
expect(output).toContain("kind: 'api'")
|
|
329
|
-
expect(output).toContain('client.
|
|
329
|
+
expect(output).toContain('client.bindCallable<')
|
|
330
330
|
})
|
|
331
331
|
|
|
332
332
|
it('callable includes JSDoc with method and fullPath', async () => {
|
|
@@ -346,7 +346,7 @@ describe('emitScopeFile', () => {
|
|
|
346
346
|
it('imports TypedStream when stream routes are present', async () => {
|
|
347
347
|
const output = await emitScopeFile(streamGroup)
|
|
348
348
|
expect(output).toContain('TypedStream')
|
|
349
|
-
expect(output).toContain("import type { ClientInstance, ProcedureCallOptions, TypedStream
|
|
349
|
+
expect(output).toContain("import type { ClientInstance, ProcedureCallOptions, TypedStream } from 'ts-procedures/client'")
|
|
350
350
|
})
|
|
351
351
|
|
|
352
352
|
it('unwraps SSE envelope for yieldType', async () => {
|
|
@@ -427,8 +427,8 @@ describe('emitScopeFile', () => {
|
|
|
427
427
|
|
|
428
428
|
it('callable references fully qualified namespace types', async () => {
|
|
429
429
|
const output = await emitScopeFile(rpcGroup, { namespaceTypes: true })
|
|
430
|
-
|
|
431
|
-
expect(output).toContain('
|
|
430
|
+
// New emission: GetUser: client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>({...})
|
|
431
|
+
expect(output).toContain('client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>')
|
|
432
432
|
})
|
|
433
433
|
|
|
434
434
|
it('still emits the bind function', async () => {
|
|
@@ -454,8 +454,8 @@ describe('emitScopeFile', () => {
|
|
|
454
454
|
|
|
455
455
|
it('callable references fully qualified namespace types', async () => {
|
|
456
456
|
const output = await emitScopeFile(apiGroup, { namespaceTypes: true })
|
|
457
|
-
|
|
458
|
-
expect(output).toContain('
|
|
457
|
+
// New emission: UpdatePost: client.bindCallable<Posts.UpdatePost.Params, Posts.UpdatePost.Response>({...})
|
|
458
|
+
expect(output).toContain('client.bindCallable<Posts.UpdatePost.Params, Posts.UpdatePost.Response>')
|
|
459
459
|
})
|
|
460
460
|
})
|
|
461
461
|
|
|
@@ -528,12 +528,14 @@ describe('emitScopeFile', () => {
|
|
|
528
528
|
|
|
529
529
|
it('v1 callable has no version suffix', async () => {
|
|
530
530
|
const output = await emitScopeFile(rpcVersionedGroup)
|
|
531
|
-
|
|
531
|
+
// New emission: GetUser: client.bindCallable<GetUserParams, GetUserResponse>({...})
|
|
532
|
+
expect(output).toContain('GetUser: client.bindCallable<GetUserParams, GetUserResponse>')
|
|
532
533
|
})
|
|
533
534
|
|
|
534
535
|
it('v2 callable uses V2 suffix', async () => {
|
|
535
536
|
const output = await emitScopeFile(rpcVersionedGroup)
|
|
536
|
-
|
|
537
|
+
// New emission: GetUserV2: client.bindCallable<GetUserV2Params, GetUserV2Response>({...})
|
|
538
|
+
expect(output).toContain('GetUserV2: client.bindCallable<GetUserV2Params, GetUserV2Response>')
|
|
537
539
|
})
|
|
538
540
|
|
|
539
541
|
it('v1 callable uses its own path', async () => {
|
|
@@ -591,14 +593,14 @@ describe('emitScopeFile', () => {
|
|
|
591
593
|
|
|
592
594
|
it('v1 callable references unversioned namespace types', async () => {
|
|
593
595
|
const output = await emitScopeFile(rpcVersionedGroup, { namespaceTypes: true })
|
|
594
|
-
|
|
595
|
-
expect(output).toContain('
|
|
596
|
+
// New emission: GetUser: client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>({...})
|
|
597
|
+
expect(output).toContain('client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>')
|
|
596
598
|
})
|
|
597
599
|
|
|
598
600
|
it('v2 callable references versioned namespace types', async () => {
|
|
599
601
|
const output = await emitScopeFile(rpcVersionedGroup, { namespaceTypes: true })
|
|
600
|
-
|
|
601
|
-
expect(output).toContain('
|
|
602
|
+
// New emission: GetUserV2: client.bindCallable<Users.GetUserV2.Params, Users.GetUserV2.Response>({...})
|
|
603
|
+
expect(output).toContain('client.bindCallable<Users.GetUserV2.Params, Users.GetUserV2.Response>')
|
|
602
604
|
})
|
|
603
605
|
})
|
|
604
606
|
})
|
|
@@ -683,75 +685,74 @@ const rpcGroupNoErrors: ScopeGroup = {
|
|
|
683
685
|
}
|
|
684
686
|
|
|
685
687
|
describe('emitScopeFile .safe sibling on RPC', () => {
|
|
686
|
-
it('emits
|
|
688
|
+
it('emits bindCallableTyped for RPC callable when route has errors', async () => {
|
|
687
689
|
const out = await emitScopeFile(rpcGroupWithErrors, {
|
|
688
690
|
namespaceTypes: true,
|
|
689
691
|
errorKeys: new Set(['NotFound']),
|
|
690
692
|
serviceName: 'Api',
|
|
691
693
|
})
|
|
692
|
-
//
|
|
693
|
-
expect(out).
|
|
694
|
-
//
|
|
695
|
-
expect(out).
|
|
696
|
-
// The safe variant's return type should be Result<Response, RouteErrors> (namespace-qualified)
|
|
697
|
-
expect(out).toMatch(/Promise<Result<.*\.Response,.*\.Errors>>/)
|
|
694
|
+
// With errors: uses bindCallableTyped<Params, Response, Errors>
|
|
695
|
+
expect(out).toContain('client.bindCallableTyped<')
|
|
696
|
+
// No Object.assign in generated output
|
|
697
|
+
expect(out).not.toContain('Object.assign')
|
|
698
698
|
})
|
|
699
699
|
|
|
700
|
-
it('emits
|
|
700
|
+
it('emits bindCallable for RPC callable when route has no errors', async () => {
|
|
701
701
|
const out = await emitScopeFile(rpcGroupNoErrors, {
|
|
702
702
|
namespaceTypes: true,
|
|
703
703
|
serviceName: 'Api',
|
|
704
704
|
})
|
|
705
|
-
//
|
|
706
|
-
expect(out).
|
|
707
|
-
//
|
|
708
|
-
expect(out).
|
|
709
|
-
// Without declared errors, use ResultNoTyped (namespace-qualified)
|
|
710
|
-
expect(out).toMatch(/Promise<ResultNoTyped<.*\.Response>>/)
|
|
705
|
+
// Without errors: uses bindCallable<Params, Response>
|
|
706
|
+
expect(out).toContain('client.bindCallable<')
|
|
707
|
+
// No Object.assign in generated output
|
|
708
|
+
expect(out).not.toContain('Object.assign')
|
|
711
709
|
})
|
|
712
710
|
|
|
713
|
-
it('
|
|
711
|
+
it('scope files no longer directly import Result/ResultNoTyped (inferred from helper)', async () => {
|
|
714
712
|
const out = await emitScopeFile(rpcGroupNoErrors, {
|
|
715
713
|
serviceName: 'Api',
|
|
716
714
|
})
|
|
717
|
-
|
|
718
|
-
expect(out).toContain('ResultNoTyped')
|
|
715
|
+
// Result/ResultNoTyped are inferred from ClientInstance.bindCallable return type
|
|
716
|
+
expect(out).not.toContain('ResultNoTyped')
|
|
717
|
+
expect(out).not.toMatch(/import.*Result.*from/)
|
|
719
718
|
expect(out).toContain("from 'ts-procedures/client'")
|
|
720
719
|
})
|
|
721
720
|
|
|
722
|
-
it('namespace mode:
|
|
721
|
+
it('namespace mode: uses route Errors namespace alias as third type arg', async () => {
|
|
723
722
|
const out = await emitScopeFile(rpcGroupWithErrors, {
|
|
724
723
|
namespaceTypes: true,
|
|
725
724
|
errorKeys: new Set(['NotFound']),
|
|
726
725
|
serviceName: 'Api',
|
|
727
726
|
})
|
|
728
727
|
// errorsRef in namespace mode is the route's Errors type alias: Scope.Route.Errors
|
|
729
|
-
expect(out).
|
|
728
|
+
expect(out).toContain('client.bindCallableTyped<Users.GetUser.Params, Users.GetUser.Response, Users.GetUser.Errors>')
|
|
730
729
|
})
|
|
731
730
|
|
|
732
|
-
it('flat mode:
|
|
731
|
+
it('flat mode: uses route Errors type alias as third type arg', async () => {
|
|
733
732
|
const out = await emitScopeFile(rpcGroupWithErrors, {
|
|
734
733
|
namespaceTypes: false,
|
|
735
734
|
errorKeys: new Set(['NotFound']),
|
|
736
735
|
serviceName: 'Api',
|
|
737
736
|
})
|
|
738
737
|
// errorsRef in flat mode is the injected GetUserErrors type alias
|
|
739
|
-
expect(out).
|
|
738
|
+
expect(out).toContain('client.bindCallableTyped<GetUserParams, GetUserResponse, GetUserErrors>')
|
|
740
739
|
})
|
|
741
740
|
|
|
742
|
-
it('
|
|
741
|
+
it('route with errors uses bindCallableTyped', async () => {
|
|
743
742
|
const out = await emitScopeFile(rpcGroupWithErrors, {
|
|
744
743
|
errorKeys: new Set(['NotFound']),
|
|
745
744
|
serviceName: 'Api',
|
|
746
745
|
})
|
|
747
|
-
expect(out).toContain('client.
|
|
746
|
+
expect(out).toContain('client.bindCallableTyped<')
|
|
747
|
+
expect(out).not.toContain('client.bindCallable<')
|
|
748
748
|
})
|
|
749
749
|
|
|
750
|
-
it('
|
|
750
|
+
it('route without errors uses bindCallable (not bindCallableTyped)', async () => {
|
|
751
751
|
const out = await emitScopeFile(rpcGroupNoErrors, {
|
|
752
752
|
serviceName: 'Api',
|
|
753
753
|
})
|
|
754
|
-
expect(out).toContain('client.
|
|
754
|
+
expect(out).toContain('client.bindCallable<')
|
|
755
|
+
expect(out).not.toContain('client.bindCallableTyped<')
|
|
755
756
|
})
|
|
756
757
|
})
|
|
757
758
|
|
|
@@ -825,78 +826,74 @@ const apiGroupNoErrors: ScopeGroup = {
|
|
|
825
826
|
}
|
|
826
827
|
|
|
827
828
|
describe('emitScopeFile .safe sibling on API', () => {
|
|
828
|
-
it('emits
|
|
829
|
+
it('emits bindCallableTyped for API callable when route has errors', async () => {
|
|
829
830
|
const out = await emitScopeFile(apiGroupWithErrors, {
|
|
830
831
|
namespaceTypes: true,
|
|
831
832
|
errorKeys: new Set(['NotFound']),
|
|
832
833
|
serviceName: 'Api',
|
|
833
834
|
})
|
|
834
|
-
//
|
|
835
|
-
expect(out).
|
|
836
|
-
//
|
|
837
|
-
expect(out).
|
|
838
|
-
// The safe variant's return type should be Result<Response, RouteErrors> (namespace-qualified)
|
|
839
|
-
expect(out).toMatch(/Promise<Result<.*\.Response,.*\.Errors>>/)
|
|
835
|
+
// With errors: uses bindCallableTyped<Params, Response, Errors>
|
|
836
|
+
expect(out).toContain('client.bindCallableTyped<')
|
|
837
|
+
// No Object.assign in generated output
|
|
838
|
+
expect(out).not.toContain('Object.assign')
|
|
840
839
|
})
|
|
841
840
|
|
|
842
|
-
it('emits
|
|
841
|
+
it('emits bindCallable for API callable when route has no errors', async () => {
|
|
843
842
|
const out = await emitScopeFile(apiGroupNoErrors, {
|
|
844
843
|
namespaceTypes: true,
|
|
845
844
|
serviceName: 'Api',
|
|
846
845
|
})
|
|
847
|
-
//
|
|
848
|
-
expect(out).
|
|
849
|
-
//
|
|
850
|
-
expect(out).
|
|
851
|
-
// Without declared errors, use ResultNoTyped (namespace-qualified)
|
|
852
|
-
expect(out).toMatch(/Promise<ResultNoTyped<.*\.Response>>/)
|
|
846
|
+
// Without errors: uses bindCallable<Params, Response>
|
|
847
|
+
expect(out).toContain('client.bindCallable<')
|
|
848
|
+
// No Object.assign in generated output
|
|
849
|
+
expect(out).not.toContain('Object.assign')
|
|
853
850
|
})
|
|
854
851
|
|
|
855
|
-
it('namespace mode:
|
|
852
|
+
it('namespace mode: uses route Errors namespace alias as third type arg', async () => {
|
|
856
853
|
const out = await emitScopeFile(apiGroupWithErrors, {
|
|
857
854
|
namespaceTypes: true,
|
|
858
855
|
errorKeys: new Set(['NotFound']),
|
|
859
856
|
serviceName: 'Api',
|
|
860
857
|
})
|
|
861
858
|
// errorsRef in namespace mode is the route's Errors type alias: Scope.Route.Errors
|
|
862
|
-
expect(out).
|
|
859
|
+
expect(out).toContain('client.bindCallableTyped<Posts.UpdatePost.Params, Posts.UpdatePost.Response, Posts.UpdatePost.Errors>')
|
|
863
860
|
})
|
|
864
861
|
|
|
865
|
-
it('flat mode:
|
|
862
|
+
it('flat mode: uses route Errors type alias as third type arg', async () => {
|
|
866
863
|
const out = await emitScopeFile(apiGroupWithErrors, {
|
|
867
864
|
namespaceTypes: false,
|
|
868
865
|
errorKeys: new Set(['NotFound']),
|
|
869
866
|
serviceName: 'Api',
|
|
870
867
|
})
|
|
871
868
|
// errorsRef in flat mode is the injected UpdatePostErrors type alias
|
|
872
|
-
expect(out).
|
|
869
|
+
expect(out).toContain('client.bindCallableTyped<UpdatePostParams, UpdatePostResponse, UpdatePostErrors>')
|
|
873
870
|
})
|
|
874
871
|
|
|
875
|
-
it('
|
|
872
|
+
it('route with errors uses bindCallableTyped', async () => {
|
|
876
873
|
const out = await emitScopeFile(apiGroupWithErrors, {
|
|
877
874
|
errorKeys: new Set(['NotFound']),
|
|
878
875
|
serviceName: 'Api',
|
|
879
876
|
})
|
|
880
|
-
expect(out).toContain('client.
|
|
877
|
+
expect(out).toContain('client.bindCallableTyped<')
|
|
878
|
+
expect(out).not.toContain('client.bindCallable<')
|
|
881
879
|
})
|
|
882
880
|
|
|
883
|
-
it('
|
|
881
|
+
it('route without errors uses bindCallable (not bindCallableTyped)', async () => {
|
|
884
882
|
const out = await emitScopeFile(apiGroupNoErrors, {
|
|
885
883
|
serviceName: 'Api',
|
|
886
884
|
})
|
|
887
|
-
expect(out).toContain('client.
|
|
885
|
+
expect(out).toContain('client.bindCallable<')
|
|
886
|
+
expect(out).not.toContain('client.bindCallableTyped<')
|
|
888
887
|
})
|
|
889
888
|
|
|
890
|
-
it('callable uses fullPath not path in
|
|
889
|
+
it('callable uses fullPath not path in the descriptor', async () => {
|
|
891
890
|
const out = await emitScopeFile(apiGroupWithErrors, {
|
|
892
891
|
errorKeys: new Set(['NotFound']),
|
|
893
892
|
serviceName: 'Api',
|
|
894
893
|
})
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
expect(
|
|
898
|
-
const afterSafeCall = out.slice(safeCallIdx)
|
|
899
|
-
expect(afterSafeCall).toContain("path: '/api/posts/:id'")
|
|
894
|
+
// The descriptor in the helper call should reference the fullPath
|
|
895
|
+
expect(out).toContain("path: '/api/posts/:id'")
|
|
896
|
+
expect(out).not.toContain("path: '/posts/:id'")
|
|
900
897
|
})
|
|
901
898
|
})
|
|
902
899
|
|
|
@@ -916,6 +913,8 @@ describe('emitScopeFile streams omit .safe sibling', () => {
|
|
|
916
913
|
expect(out).not.toContain('WatchEvents.safe')
|
|
917
914
|
// Object.assign wrapper is not used for stream routes
|
|
918
915
|
expect(out).not.toMatch(/WatchEvents\s*:\s*Object\.assign/)
|
|
916
|
+
// bindCallable helpers are not used for stream routes
|
|
917
|
+
expect(out).not.toMatch(/WatchEvents\s*:\s*client\.bindCallable/)
|
|
919
918
|
})
|
|
920
919
|
|
|
921
920
|
it('stream callable returns TypedStream, not Result', async () => {
|
|
@@ -942,5 +941,7 @@ describe('emitScopeFile streams omit .safe sibling', () => {
|
|
|
942
941
|
// But still no .safe property
|
|
943
942
|
expect(out).not.toContain('WatchEvents.safe')
|
|
944
943
|
expect(out).not.toMatch(/WatchEvents\s*:\s*Object\.assign/)
|
|
944
|
+
// bindCallable helpers are not used for stream routes
|
|
945
|
+
expect(out).not.toMatch(/WatchEvents\s*:\s*client\.bindCallable/)
|
|
945
946
|
})
|
|
946
947
|
})
|