@grest-ts/http 0.0.5
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/LICENSE +21 -0
- package/README.md +613 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts +14 -0
- package/dist/src/client/GGHttpSchema.createClient.d.ts.map +1 -0
- package/dist/src/client/GGHttpSchema.createClient.js +80 -0
- package/dist/src/client/GGHttpSchema.createClient.js.map +1 -0
- package/dist/src/index-browser.d.ts +8 -0
- package/dist/src/index-browser.d.ts.map +1 -0
- package/dist/src/index-browser.js +11 -0
- package/dist/src/index-browser.js.map +1 -0
- package/dist/src/index-node.d.ts +18 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +32 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts +19 -0
- package/dist/src/rpc/GGHttpRouteRPC.d.ts.map +1 -0
- package/dist/src/rpc/GGHttpRouteRPC.js +32 -0
- package/dist/src/rpc/GGHttpRouteRPC.js.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts +18 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.d.ts.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js +80 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestBuilder.js.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts +18 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.d.ts.map +1 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js +90 -0
- package/dist/src/rpc/RpcRequest/GGRpcRequestParser.js.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts +12 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.d.ts.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js +77 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseBuilder.js.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.d.ts +7 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.d.ts.map +1 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.js +21 -0
- package/dist/src/rpc/RpcResponse/GGRpcResponseParser.js.map +1 -0
- package/dist/src/schema/GGHttpSchema.d.ts +68 -0
- package/dist/src/schema/GGHttpSchema.d.ts.map +1 -0
- package/dist/src/schema/GGHttpSchema.js +18 -0
- package/dist/src/schema/GGHttpSchema.js.map +1 -0
- package/dist/src/schema/httpSchema.d.ts +43 -0
- package/dist/src/schema/httpSchema.d.ts.map +1 -0
- package/dist/src/schema/httpSchema.js +85 -0
- package/dist/src/schema/httpSchema.js.map +1 -0
- package/dist/src/server/GGHttp.d.ts +12 -0
- package/dist/src/server/GGHttp.d.ts.map +1 -0
- package/dist/src/server/GGHttp.js +16 -0
- package/dist/src/server/GGHttp.js.map +1 -0
- package/dist/src/server/GGHttpMetrics.d.ts +22 -0
- package/dist/src/server/GGHttpMetrics.d.ts.map +1 -0
- package/dist/src/server/GGHttpMetrics.js +15 -0
- package/dist/src/server/GGHttpMetrics.js.map +1 -0
- package/dist/src/server/GGHttpSchema.startServer.d.ts +30 -0
- package/dist/src/server/GGHttpSchema.startServer.d.ts.map +1 -0
- package/dist/src/server/GGHttpSchema.startServer.js +114 -0
- package/dist/src/server/GGHttpSchema.startServer.js.map +1 -0
- package/dist/src/server/GGHttpServer.d.ts +32 -0
- package/dist/src/server/GGHttpServer.d.ts.map +1 -0
- package/dist/src/server/GGHttpServer.js +116 -0
- package/dist/src/server/GGHttpServer.js.map +1 -0
- package/dist/src/server/GG_HTTP_REQUEST.d.ts +16 -0
- package/dist/src/server/GG_HTTP_REQUEST.d.ts.map +1 -0
- package/dist/src/server/GG_HTTP_REQUEST.js +10 -0
- package/dist/src/server/GG_HTTP_REQUEST.js.map +1 -0
- package/dist/src/server/GG_HTTP_SERVER.d.ts +4 -0
- package/dist/src/server/GG_HTTP_SERVER.d.ts.map +1 -0
- package/dist/src/server/GG_HTTP_SERVER.js +3 -0
- package/dist/src/server/GG_HTTP_SERVER.js.map +1 -0
- package/dist/src/tsconfig.json +17 -0
- package/dist/testkit/clientHttp/GGHttpCall.d.ts +35 -0
- package/dist/testkit/clientHttp/GGHttpCall.d.ts.map +1 -0
- package/dist/testkit/clientHttp/GGHttpCall.js +37 -0
- package/dist/testkit/clientHttp/GGHttpCall.js.map +1 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts +37 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.d.ts.map +1 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.js +29 -0
- package/dist/testkit/clientHttp/GGHttpSchema.callOn.js.map +1 -0
- package/dist/testkit/index-testkit.d.ts +8 -0
- package/dist/testkit/index-testkit.d.ts.map +1 -0
- package/dist/testkit/index-testkit.js +8 -0
- package/dist/testkit/index-testkit.js.map +1 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.d.ts +13 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.d.ts.map +1 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.js +100 -0
- package/dist/testkit/mock/GGHttpInterceptorsServer.js.map +1 -0
- package/dist/testkit/mock/GGHttpSchema.mock.d.ts +36 -0
- package/dist/testkit/mock/GGHttpSchema.mock.d.ts.map +1 -0
- package/dist/testkit/mock/GGHttpSchema.mock.js +78 -0
- package/dist/testkit/mock/GGHttpSchema.mock.js.map +1 -0
- package/dist/testkit/routing/GGApiRoutingSelector.d.ts +8 -0
- package/dist/testkit/routing/GGApiRoutingSelector.d.ts.map +1 -0
- package/dist/testkit/routing/GGApiRoutingSelector.js +4 -0
- package/dist/testkit/routing/GGApiRoutingSelector.js.map +1 -0
- package/dist/testkit/routing/GGHttpSchema.routing.d.ts +14 -0
- package/dist/testkit/routing/GGHttpSchema.routing.d.ts.map +1 -0
- package/dist/testkit/routing/GGHttpSchema.routing.js +21 -0
- package/dist/testkit/routing/GGHttpSchema.routing.js.map +1 -0
- package/dist/testkit/utils/validateContractResponse.d.ts +8 -0
- package/dist/testkit/utils/validateContractResponse.d.ts.map +1 -0
- package/dist/testkit/utils/validateContractResponse.js +68 -0
- package/dist/testkit/utils/validateContractResponse.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +74 -0
- package/src/client/GGHttpSchema.createClient.ts +107 -0
- package/src/index-browser.ts +12 -0
- package/src/index-node.ts +38 -0
- package/src/rpc/GGHttpRouteRPC.ts +42 -0
- package/src/rpc/RpcRequest/GGRpcRequestBuilder.ts +91 -0
- package/src/rpc/RpcRequest/GGRpcRequestParser.ts +100 -0
- package/src/rpc/RpcResponse/GGRpcResponseBuilder.ts +84 -0
- package/src/rpc/RpcResponse/GGRpcResponseParser.ts +23 -0
- package/src/schema/GGHttpSchema.ts +115 -0
- package/src/schema/httpSchema.ts +99 -0
- package/src/server/GGHttp.ts +27 -0
- package/src/server/GGHttpMetrics.ts +31 -0
- package/src/server/GGHttpSchema.startServer.ts +161 -0
- package/src/server/GGHttpServer.ts +133 -0
- package/src/server/GG_HTTP_REQUEST.ts +12 -0
- package/src/server/GG_HTTP_SERVER.ts +4 -0
- package/src/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Grest Games OÜ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
# HTTP Package Usage (@grest-ts/http)
|
|
2
|
+
|
|
3
|
+
How to use the HTTP package for building type-safe HTTP and WebSocket APIs.
|
|
4
|
+
|
|
5
|
+
## HTTP API Definition
|
|
6
|
+
|
|
7
|
+
### Basic API Structure
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// MyApi.ts
|
|
11
|
+
import { GGRpc, httpApi } from "@grest-ts/http"
|
|
12
|
+
import { IsArray, IsObject, IsString, IsBoolean, IsUint } from "@grest-ts/schema"
|
|
13
|
+
import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/contract"
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------
|
|
16
|
+
// Type Schemas
|
|
17
|
+
// ---------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export const IsItemId = IsString.brand("ItemId")
|
|
20
|
+
export type tItemId = typeof IsItemId.infer
|
|
21
|
+
|
|
22
|
+
export const IsItem = IsObject({
|
|
23
|
+
id: IsItemId,
|
|
24
|
+
title: IsString,
|
|
25
|
+
description: IsString.orUndefined,
|
|
26
|
+
done: IsBoolean,
|
|
27
|
+
createdAt: IsUint,
|
|
28
|
+
updatedAt: IsUint
|
|
29
|
+
})
|
|
30
|
+
export type Item = typeof IsItem.infer
|
|
31
|
+
|
|
32
|
+
export const IsCreateItemRequest = IsObject({
|
|
33
|
+
title: IsString.nonEmpty,
|
|
34
|
+
description: IsString.orUndefined
|
|
35
|
+
})
|
|
36
|
+
export type CreateItemRequest = typeof IsCreateItemRequest.infer
|
|
37
|
+
|
|
38
|
+
export const IsUpdateItemRequest = IsObject({
|
|
39
|
+
id: IsItemId,
|
|
40
|
+
title: IsString.orUndefined,
|
|
41
|
+
description: IsString.orUndefined
|
|
42
|
+
})
|
|
43
|
+
export type UpdateItemRequest = typeof IsUpdateItemRequest.infer
|
|
44
|
+
|
|
45
|
+
export const IsItemIdParam = IsObject({
|
|
46
|
+
id: IsItemId
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------
|
|
50
|
+
// Contract & API
|
|
51
|
+
// ---------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
54
|
+
list: {
|
|
55
|
+
success: IsArray(IsItem),
|
|
56
|
+
errors: [SERVER_ERROR]
|
|
57
|
+
},
|
|
58
|
+
get: {
|
|
59
|
+
input: IsItemIdParam,
|
|
60
|
+
success: IsItem,
|
|
61
|
+
errors: [NOT_FOUND, SERVER_ERROR]
|
|
62
|
+
},
|
|
63
|
+
create: {
|
|
64
|
+
input: IsCreateItemRequest,
|
|
65
|
+
success: IsItem,
|
|
66
|
+
errors: [VALIDATION_ERROR, SERVER_ERROR]
|
|
67
|
+
},
|
|
68
|
+
update: {
|
|
69
|
+
input: IsUpdateItemRequest,
|
|
70
|
+
success: IsItem,
|
|
71
|
+
errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
|
|
72
|
+
},
|
|
73
|
+
delete: {
|
|
74
|
+
input: IsItemIdParam,
|
|
75
|
+
success: undefined as undefined,
|
|
76
|
+
errors: [NOT_FOUND, SERVER_ERROR]
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
export const MyApi = httpApi(MyApiContract)
|
|
81
|
+
.pathPrefix("api/items")
|
|
82
|
+
.routes({
|
|
83
|
+
list: GGRpc.GET("list"),
|
|
84
|
+
get: GGRpc.GET("get/:id"),
|
|
85
|
+
create: GGRpc.POST("create"),
|
|
86
|
+
update: GGRpc.PUT("update"),
|
|
87
|
+
delete: GGRpc.DELETE("delete/:id")
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### HTTP Methods
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
GGRpc.GET("path") // GET request
|
|
95
|
+
GGRpc.POST("path") // POST request
|
|
96
|
+
GGRpc.PUT("path") // PUT request
|
|
97
|
+
GGRpc.PATCH("path") // PATCH request
|
|
98
|
+
GGRpc.DELETE("path") // DELETE request
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Path Parameters
|
|
102
|
+
|
|
103
|
+
Use `:paramName` in paths - parameters are matched by position:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
107
|
+
getUser: {
|
|
108
|
+
input: IsObject({ userId: IsUserId }),
|
|
109
|
+
success: IsUser,
|
|
110
|
+
errors: [NOT_FOUND, SERVER_ERROR]
|
|
111
|
+
},
|
|
112
|
+
getUserPost: {
|
|
113
|
+
input: IsObject({ userId: IsUserId, postId: IsPostId }),
|
|
114
|
+
success: IsPost,
|
|
115
|
+
errors: [NOT_FOUND, SERVER_ERROR]
|
|
116
|
+
}
|
|
117
|
+
}))
|
|
118
|
+
|
|
119
|
+
export const MyApi = httpApi(MyApiContract)
|
|
120
|
+
.pathPrefix("api")
|
|
121
|
+
.routes({
|
|
122
|
+
getUser: GGRpc.GET("users/:userId"),
|
|
123
|
+
getUserPost: GGRpc.GET("users/:userId/posts/:postId")
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Query Parameters
|
|
128
|
+
|
|
129
|
+
For GET/DELETE, object parameters become query strings:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
133
|
+
search: {
|
|
134
|
+
input: IsObject({
|
|
135
|
+
term: IsString,
|
|
136
|
+
page: IsUint.orUndefined,
|
|
137
|
+
limit: IsUint.orUndefined
|
|
138
|
+
}),
|
|
139
|
+
success: IsSearchResults,
|
|
140
|
+
errors: [SERVER_ERROR]
|
|
141
|
+
}
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
// Client usage: client.search({ term: "foo", page: 1 })
|
|
145
|
+
// Results in: GET /api/search?term=foo&page=1
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Request Body
|
|
149
|
+
|
|
150
|
+
For POST/PUT/PATCH, the input becomes the JSON body:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
154
|
+
create: {
|
|
155
|
+
input: IsCreateRequest,
|
|
156
|
+
success: IsItem,
|
|
157
|
+
errors: [VALIDATION_ERROR, SERVER_ERROR]
|
|
158
|
+
},
|
|
159
|
+
update: {
|
|
160
|
+
input: IsUpdateRequest,
|
|
161
|
+
success: IsItem,
|
|
162
|
+
errors: [VALIDATION_ERROR, SERVER_ERROR]
|
|
163
|
+
}
|
|
164
|
+
}))
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Authentication & Context
|
|
168
|
+
|
|
169
|
+
### Using Codec (Recommended for Header-Based Auth)
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// auth/UserAuth.ts
|
|
173
|
+
import { GGContextKey } from "@grest-ts/context"
|
|
174
|
+
import { IsObject, IsString } from "@grest-ts/schema"
|
|
175
|
+
|
|
176
|
+
export const IsUserAuthToken = IsString.brand("UserAuthToken")
|
|
177
|
+
export type tUserAuthToken = typeof IsUserAuthToken.infer
|
|
178
|
+
|
|
179
|
+
export const IsUserId = IsString.brand("UserId")
|
|
180
|
+
export type tUserId = typeof IsUserId.infer
|
|
181
|
+
|
|
182
|
+
export const IsUser = IsObject({
|
|
183
|
+
id: IsUserId,
|
|
184
|
+
username: IsString,
|
|
185
|
+
email: IsString
|
|
186
|
+
})
|
|
187
|
+
export type User = typeof IsUser.infer
|
|
188
|
+
|
|
189
|
+
// Define the context value schema
|
|
190
|
+
const IsUserAuthContext = IsObject({
|
|
191
|
+
token: IsUserAuthToken
|
|
192
|
+
})
|
|
193
|
+
export type UserAuthContext = typeof IsUserAuthContext.infer
|
|
194
|
+
|
|
195
|
+
// Define the header schema
|
|
196
|
+
const HEADER_AUTHORIZATION = "authorization"
|
|
197
|
+
const HeaderType = IsObject({
|
|
198
|
+
[HEADER_AUTHORIZATION]: IsString.orUndefined
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Create context key with codec
|
|
202
|
+
export const GG_USER_AUTH = new GGContextKey<UserAuthContext>("user_auth", IsUserAuthContext)
|
|
203
|
+
GG_USER_AUTH.addCodec("http", HeaderType.codecTo(IsUserAuthContext, {
|
|
204
|
+
encode: (headers) => {
|
|
205
|
+
const authHeader = headers[HEADER_AUTHORIZATION]
|
|
206
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
207
|
+
return { token: undefined as any } // Will fail validation if required
|
|
208
|
+
}
|
|
209
|
+
return { token: authHeader.substring(7) as tUserAuthToken }
|
|
210
|
+
},
|
|
211
|
+
decode: (value) => {
|
|
212
|
+
return { [HEADER_AUTHORIZATION]: value.token ? `Bearer ${value.token}` : undefined }
|
|
213
|
+
}
|
|
214
|
+
}))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Using Middleware (For Complex Logic)
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// middleware/ClientInfoMiddleware.ts
|
|
221
|
+
import { GGHttpRequest, GGHttpTransportMiddleware } from "@grest-ts/http"
|
|
222
|
+
import { GGContextKey } from "@grest-ts/context"
|
|
223
|
+
import { IsObject, IsString, IsLiteral } from "@grest-ts/schema"
|
|
224
|
+
|
|
225
|
+
export interface ClientInfo {
|
|
226
|
+
version: string
|
|
227
|
+
platform: 'web' | 'ios' | 'android'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const GG_CLIENT_INFO = new GGContextKey<ClientInfo>('clientInfo', IsObject({
|
|
231
|
+
version: IsString,
|
|
232
|
+
platform: IsLiteral("web", "ios", "android")
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
export const ClientInfoMiddleware: GGHttpTransportMiddleware = {
|
|
236
|
+
updateRequest(req: GGHttpRequest): void {
|
|
237
|
+
const info = GG_CLIENT_INFO.get()
|
|
238
|
+
if (info) {
|
|
239
|
+
req.headers['x-client-version'] = info.version
|
|
240
|
+
req.headers['x-client-platform'] = info.platform
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
parseRequest(req: GGHttpRequest): void {
|
|
244
|
+
GG_CLIENT_INFO.set({
|
|
245
|
+
version: req.headers['x-client-version'] ?? 'unknown',
|
|
246
|
+
platform: (req.headers['x-client-platform'] ?? 'web') as ClientInfo['platform']
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Adding Auth/Context to API
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { GG_USER_AUTH } from "./auth/UserAuth"
|
|
256
|
+
import { ClientInfoMiddleware } from "./middleware/ClientInfoMiddleware"
|
|
257
|
+
import { GG_INTL_LOCALE } from "@grest-ts/intl"
|
|
258
|
+
|
|
259
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
260
|
+
list: {
|
|
261
|
+
success: IsArray(IsItem),
|
|
262
|
+
errors: [NOT_AUTHORIZED, SERVER_ERROR]
|
|
263
|
+
},
|
|
264
|
+
create: {
|
|
265
|
+
input: IsCreateRequest,
|
|
266
|
+
success: IsItem,
|
|
267
|
+
errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
|
|
268
|
+
}
|
|
269
|
+
}))
|
|
270
|
+
|
|
271
|
+
// Chain multiple context providers
|
|
272
|
+
export const MyApi = httpApi(MyApiContract)
|
|
273
|
+
.pathPrefix("api/items")
|
|
274
|
+
.useHeader(GG_INTL_LOCALE) // Use codec from context key
|
|
275
|
+
.useHeader(GG_USER_AUTH) // Use codec from context key
|
|
276
|
+
.use(ClientInfoMiddleware) // Use middleware object
|
|
277
|
+
.routes({
|
|
278
|
+
list: GGRpc.GET("list"),
|
|
279
|
+
create: GGRpc.POST("create")
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Public API (No Auth)
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
export const PublicApiContract = defineApi("PublicApi", () => ({
|
|
287
|
+
status: {
|
|
288
|
+
success: IsStatusResponse,
|
|
289
|
+
errors: [SERVER_ERROR]
|
|
290
|
+
},
|
|
291
|
+
login: {
|
|
292
|
+
input: IsLoginRequest,
|
|
293
|
+
success: IsLoginResponse,
|
|
294
|
+
errors: [VALIDATION_ERROR, SERVER_ERROR]
|
|
295
|
+
}
|
|
296
|
+
}))
|
|
297
|
+
|
|
298
|
+
export const PublicApi = httpApi(PublicApiContract)
|
|
299
|
+
.pathPrefix("pub")
|
|
300
|
+
.routes({
|
|
301
|
+
status: GGRpc.GET("status"),
|
|
302
|
+
login: GGRpc.POST("login")
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Error Types
|
|
307
|
+
|
|
308
|
+
### Declaring Errors in Contract
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST } from "@grest-ts/contract"
|
|
312
|
+
|
|
313
|
+
// Custom error type
|
|
314
|
+
export class InvalidCredentialsError extends BAD_REQUEST<"INVALID_CREDENTIALS", undefined> {
|
|
315
|
+
public static TYPE = "INVALID_CREDENTIALS"
|
|
316
|
+
|
|
317
|
+
constructor() {
|
|
318
|
+
super("INVALID_CREDENTIALS", undefined)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const MyApiContract = defineApi("MyApi", () => ({
|
|
323
|
+
get: {
|
|
324
|
+
input: IsItemIdParam,
|
|
325
|
+
success: IsItem,
|
|
326
|
+
errors: [NOT_FOUND, SERVER_ERROR]
|
|
327
|
+
},
|
|
328
|
+
update: {
|
|
329
|
+
input: IsUpdateRequest,
|
|
330
|
+
success: IsItem,
|
|
331
|
+
errors: [NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR]
|
|
332
|
+
},
|
|
333
|
+
login: {
|
|
334
|
+
input: IsLoginRequest,
|
|
335
|
+
success: IsLoginResponse,
|
|
336
|
+
errors: [InvalidCredentialsError, VALIDATION_ERROR, SERVER_ERROR]
|
|
337
|
+
}
|
|
338
|
+
}))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Throwing Errors in Service
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { GGServerApi, NOT_FOUND, FORBIDDEN } from "@grest-ts/node"
|
|
345
|
+
|
|
346
|
+
export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
|
|
347
|
+
async get({ id }: { id: tItemId }): Promise<Item> {
|
|
348
|
+
const item = await this.findItem(id)
|
|
349
|
+
if (!item) throw new NOT_FOUND()
|
|
350
|
+
return item
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async update(request: UpdateRequest): Promise<Item> {
|
|
354
|
+
const item = await this.findItem(request.id)
|
|
355
|
+
if (!item) throw new NOT_FOUND()
|
|
356
|
+
|
|
357
|
+
const user = UserContext.get()
|
|
358
|
+
if (item.ownerId !== user.id) throw new FORBIDDEN()
|
|
359
|
+
|
|
360
|
+
return this.updateItem(item, request)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## HTTP Server Setup
|
|
366
|
+
|
|
367
|
+
### Using GGHttp (Fluent API)
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { GGHttp } from "@grest-ts/http"
|
|
371
|
+
|
|
372
|
+
protected compose(): void {
|
|
373
|
+
new GGHttp()
|
|
374
|
+
.http(PublicApi, publicService)
|
|
375
|
+
.http(StatusApi, {
|
|
376
|
+
status: async () => ({ status: true })
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
new GGHttp("authenticated")
|
|
380
|
+
.use(new UserContextMiddleware(userService))
|
|
381
|
+
.http(MyApi, myService)
|
|
382
|
+
.http(UserAuthApi, userService)
|
|
383
|
+
.websocket(NotificationApi, notificationService.handleConnection)
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Using GGHttpServer (Direct)
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { GGHttpServer } from "@grest-ts/http"
|
|
391
|
+
|
|
392
|
+
protected compose(): void {
|
|
393
|
+
const httpServer = new GGHttpServer()
|
|
394
|
+
MyApi.startServer(httpServer, myService)
|
|
395
|
+
OtherApi.startServer(httpServer, otherService)
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Multiple HTTP Servers
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
protected compose(): void {
|
|
403
|
+
// Main public server
|
|
404
|
+
new GGHttp()
|
|
405
|
+
.http(PublicApi, publicService)
|
|
406
|
+
|
|
407
|
+
// Internal server on different port
|
|
408
|
+
new GGHttp("internal")
|
|
409
|
+
.http(InternalApi, internalService)
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## HTTP Client
|
|
414
|
+
|
|
415
|
+
### Creating Clients
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// Unauthenticated client
|
|
419
|
+
const client = MyApi.createClient({ url: "http://localhost:3000" })
|
|
420
|
+
|
|
421
|
+
// Authenticated client
|
|
422
|
+
const authClient = MyApi.createClient(authState, { url: "http://localhost:3000" })
|
|
423
|
+
|
|
424
|
+
// Test client
|
|
425
|
+
const testClient = MyApi.createTestClient()
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Making Requests
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// Simple request
|
|
432
|
+
const items = await client.list()
|
|
433
|
+
|
|
434
|
+
// With path parameter
|
|
435
|
+
const item = await client.get({ id: "item-123" })
|
|
436
|
+
|
|
437
|
+
// With query parameters
|
|
438
|
+
const results = await client.search({ term: "foo", page: 1 })
|
|
439
|
+
|
|
440
|
+
// With body
|
|
441
|
+
const newItem = await client.create({ title: "New Item" })
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Handling Results
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Direct (throws on error)
|
|
448
|
+
const item = await client.get({ id: "item-123" })
|
|
449
|
+
|
|
450
|
+
// Using .asResult() for safe error handling
|
|
451
|
+
const result = await client.get({ id: "item-123" }).asResult()
|
|
452
|
+
if (result.success) {
|
|
453
|
+
console.log("Item:", result.data)
|
|
454
|
+
} else {
|
|
455
|
+
console.log("Error:", result.type) // "NOT_FOUND", etc.
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## WebSocket APIs
|
|
460
|
+
|
|
461
|
+
### Defining WebSocket API
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
import { webSocketApi } from "@grest-ts/http"
|
|
465
|
+
import { defineTwoWayApi, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/contract"
|
|
466
|
+
import { IsObject, IsString, IsBoolean } from "@grest-ts/schema"
|
|
467
|
+
import { GG_USER_AUTH_TOKEN } from "./auth/UserAuth"
|
|
468
|
+
|
|
469
|
+
// Message schemas
|
|
470
|
+
export const IsItemMarkedEvent = IsObject({
|
|
471
|
+
item: IsItem,
|
|
472
|
+
markedBy: IsString
|
|
473
|
+
})
|
|
474
|
+
export type ItemMarkedEvent = typeof IsItemMarkedEvent.infer
|
|
475
|
+
|
|
476
|
+
export const IsUpdateItemRequest = IsObject({
|
|
477
|
+
item: IsItem,
|
|
478
|
+
reason: IsString.orUndefined
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
export const IsUpdateItemResponse = IsObject({
|
|
482
|
+
success: IsBoolean,
|
|
483
|
+
message: IsString
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
// Contract definition
|
|
487
|
+
export const NotificationApiContract = defineTwoWayApi("NotificationApi", () => ({
|
|
488
|
+
clientToServer: {
|
|
489
|
+
updateItem: {
|
|
490
|
+
input: IsUpdateItemRequest,
|
|
491
|
+
success: IsUpdateItemResponse,
|
|
492
|
+
errors: [VALIDATION_ERROR, SERVER_ERROR]
|
|
493
|
+
},
|
|
494
|
+
ping: {}
|
|
495
|
+
},
|
|
496
|
+
serverToClient: {
|
|
497
|
+
itemMarked: {
|
|
498
|
+
input: IsItemMarkedEvent
|
|
499
|
+
},
|
|
500
|
+
areYouThere: {
|
|
501
|
+
success: IsBoolean,
|
|
502
|
+
errors: [SERVER_ERROR]
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}))
|
|
506
|
+
|
|
507
|
+
export const NotificationApi = webSocketApi(NotificationApiContract)
|
|
508
|
+
.path("ws/notifications")
|
|
509
|
+
.use(GG_USER_AUTH_TOKEN)
|
|
510
|
+
.done()
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### WebSocket Server Handler
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
import { GGSocketApi, WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/http"
|
|
517
|
+
|
|
518
|
+
type IncomingHandler = WebSocketIncoming<GGSocketApi<typeof NotificationApiContract.methods["clientToServer"]>>
|
|
519
|
+
type OutgoingConnection = WebSocketOutgoing<GGSocketApi<typeof NotificationApiContract.methods["serverToClient"]>>
|
|
520
|
+
|
|
521
|
+
export class NotificationService {
|
|
522
|
+
private connections = new Map<string, Set<OutgoingConnection>>()
|
|
523
|
+
|
|
524
|
+
handleConnection = (incoming: IncomingHandler, outgoing: OutgoingConnection): void => {
|
|
525
|
+
const user = UserContext.get()
|
|
526
|
+
|
|
527
|
+
// Track connection
|
|
528
|
+
if (!this.connections.has(user.id)) {
|
|
529
|
+
this.connections.set(user.id, new Set())
|
|
530
|
+
}
|
|
531
|
+
this.connections.get(user.id)!.add(outgoing)
|
|
532
|
+
|
|
533
|
+
// Handle incoming messages
|
|
534
|
+
incoming.on({
|
|
535
|
+
updateItem: async (request) => {
|
|
536
|
+
// Process update
|
|
537
|
+
return { success: true, message: "Updated" }
|
|
538
|
+
},
|
|
539
|
+
ping: async () => {
|
|
540
|
+
// Handle ping
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
// Handle disconnect
|
|
545
|
+
outgoing.onClose(() => {
|
|
546
|
+
this.connections.get(user.id)?.delete(outgoing)
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Broadcast to user
|
|
551
|
+
notifyUser(userId: string, event: ItemMarkedEvent): void {
|
|
552
|
+
const userConnections = this.connections.get(userId)
|
|
553
|
+
userConnections?.forEach(conn => conn.itemMarked(event))
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### WebSocket in Runtime
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
protected compose(): void {
|
|
562
|
+
new GGHttp()
|
|
563
|
+
.use(new UserContextMiddleware(userService))
|
|
564
|
+
.websocket(NotificationApi, notificationService.handleConnection)
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### WebSocket Client (Browser)
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// Connect with message handlers
|
|
572
|
+
const socket = await authenticatedSDK.connectNotification({
|
|
573
|
+
itemMarked: (event) => {
|
|
574
|
+
console.log("Item marked:", event.item.title)
|
|
575
|
+
},
|
|
576
|
+
areYouThere: async () => {
|
|
577
|
+
return true
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
// Send messages
|
|
582
|
+
const response = await socket.updateItem({ item, reason: "Updated via UI" })
|
|
583
|
+
|
|
584
|
+
// Close connection
|
|
585
|
+
socket.close()
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## SDK (Auto-Generated)
|
|
589
|
+
|
|
590
|
+
The SDK is auto-generated from your API definitions. The generated SDK provides:
|
|
591
|
+
|
|
592
|
+
- Type-safe client methods for all endpoints
|
|
593
|
+
- Automatic auth token handling
|
|
594
|
+
- WebSocket connection management
|
|
595
|
+
- Error type inference
|
|
596
|
+
|
|
597
|
+
### Using Generated SDK
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
import { UserAppSDK } from "./UserAppSDK.gen"
|
|
601
|
+
|
|
602
|
+
const sdk = new UserAppSDK({ url: "http://localhost:3000" })
|
|
603
|
+
|
|
604
|
+
// Public endpoints
|
|
605
|
+
const loginResult = await sdk.login({ username, password })
|
|
606
|
+
|
|
607
|
+
// Authenticated endpoints (returned from login)
|
|
608
|
+
const authSDK = loginResult.data.sdk
|
|
609
|
+
const items = await authSDK.checklist.list()
|
|
610
|
+
const socket = await authSDK.connectNotification({
|
|
611
|
+
itemMarked: (event) => console.log(event)
|
|
612
|
+
})
|
|
613
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GGContractApiDefinition, GGContractClient } from "@grest-ts/schema";
|
|
2
|
+
import { GGHttpSchema } from "../schema/GGHttpSchema";
|
|
3
|
+
declare module "../schema/GGHttpSchema" {
|
|
4
|
+
interface GGHttpSchema<TContract extends GGContractApiDefinition, TContext = {}> {
|
|
5
|
+
createClient(config?: GGHttpClientConfig): GGContractClient<TContract>;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface GGHttpClientConfig {
|
|
9
|
+
url?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
noValidation?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function createClient<TContract extends GGContractApiDefinition, TContext>(httpSchema: GGHttpSchema<TContract, TContext>, config?: GGHttpClientConfig): GGContractClient<TContract>;
|
|
14
|
+
//# sourceMappingURL=GGHttpSchema.createClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GGHttpSchema.createClient.d.ts","sourceRoot":"","sources":["../../../src/client/GGHttpSchema.createClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,uBAAuB,EAAE,gBAAgB,EAA4E,MAAM,kBAAkB,CAAA;AACvK,OAAO,EAAwD,YAAY,EAAC,MAAM,wBAAwB,CAAC;AAI3G,OAAO,QAAQ,wBAAwB,CAAC;IACpC,UAAU,YAAY,CAAC,SAAS,SAAS,uBAAuB,EAAE,QAAQ,GAAG,EAAE;QAC3E,YAAY,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAA;KACzE;CACJ;AAED,MAAM,WAAW,kBAAkB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB;AASD,wBAAgB,YAAY,CAAC,SAAS,SAAS,uBAAuB,EAAE,QAAQ,EAC5E,UAAU,EAAE,YAAY,CAAC,SAAS,EAAE,QAAQ,CAAC,EAC7C,MAAM,CAAC,EAAE,kBAAkB,GAC5B,gBAAgB,CAAC,SAAS,CAAC,CA+E7B"}
|