@grest-ts/http 0.0.12 → 0.0.14

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.
Files changed (2) hide show
  1. package/README.md +101 -107
  2. package/package.json +11 -11
package/README.md CHANGED
@@ -13,9 +13,8 @@ How to use the HTTP package for building type-safe HTTP and WebSocket APIs.
13
13
 
14
14
  ```typescript
15
15
  // MyApi.ts
16
- import { GGRpc, httpApi } from "@grest-ts/http"
17
- import { IsArray, IsObject, IsString, IsBoolean, IsUint } from "@grest-ts/schema"
18
- import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/contract"
16
+ import { GGRpc, httpSchema } from "@grest-ts/http"
17
+ import { GGContractClass, IsArray, IsObject, IsString, IsBoolean, IsUint, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR } from "@grest-ts/schema"
19
18
 
20
19
  // ---------------------------------------------------------
21
20
  // Type Schemas
@@ -55,7 +54,7 @@ export const IsItemIdParam = IsObject({
55
54
  // Contract & API
56
55
  // ---------------------------------------------------------
57
56
 
58
- export const MyApiContract = defineApi("MyApi", () => ({
57
+ export const MyApiContract = new GGContractClass("MyApi", {
59
58
  list: {
60
59
  success: IsArray(IsItem),
61
60
  errors: [SERVER_ERROR]
@@ -77,12 +76,11 @@ export const MyApiContract = defineApi("MyApi", () => ({
77
76
  },
78
77
  delete: {
79
78
  input: IsItemIdParam,
80
- success: undefined as undefined,
81
79
  errors: [NOT_FOUND, SERVER_ERROR]
82
80
  }
83
- }))
81
+ })
84
82
 
85
- export const MyApi = httpApi(MyApiContract)
83
+ export const MyApi = httpSchema(MyApiContract)
86
84
  .pathPrefix("api/items")
87
85
  .routes({
88
86
  list: GGRpc.GET("list"),
@@ -99,7 +97,6 @@ export const MyApi = httpApi(MyApiContract)
99
97
  GGRpc.GET("path") // GET request
100
98
  GGRpc.POST("path") // POST request
101
99
  GGRpc.PUT("path") // PUT request
102
- GGRpc.PATCH("path") // PATCH request
103
100
  GGRpc.DELETE("path") // DELETE request
104
101
  ```
105
102
 
@@ -108,7 +105,7 @@ GGRpc.DELETE("path") // DELETE request
108
105
  Use `:paramName` in paths - parameters are matched by position:
109
106
 
110
107
  ```typescript
111
- export const MyApiContract = defineApi("MyApi", () => ({
108
+ export const MyApiContract = new GGContractClass("MyApi", {
112
109
  getUser: {
113
110
  input: IsObject({ userId: IsUserId }),
114
111
  success: IsUser,
@@ -119,9 +116,9 @@ export const MyApiContract = defineApi("MyApi", () => ({
119
116
  success: IsPost,
120
117
  errors: [NOT_FOUND, SERVER_ERROR]
121
118
  }
122
- }))
119
+ })
123
120
 
124
- export const MyApi = httpApi(MyApiContract)
121
+ export const MyApi = httpSchema(MyApiContract)
125
122
  .pathPrefix("api")
126
123
  .routes({
127
124
  getUser: GGRpc.GET("users/:userId"),
@@ -134,7 +131,7 @@ export const MyApi = httpApi(MyApiContract)
134
131
  For GET/DELETE, object parameters become query strings:
135
132
 
136
133
  ```typescript
137
- export const MyApiContract = defineApi("MyApi", () => ({
134
+ export const MyApiContract = new GGContractClass("MyApi", {
138
135
  search: {
139
136
  input: IsObject({
140
137
  term: IsString,
@@ -144,7 +141,7 @@ export const MyApiContract = defineApi("MyApi", () => ({
144
141
  success: IsSearchResults,
145
142
  errors: [SERVER_ERROR]
146
143
  }
147
- }))
144
+ })
148
145
 
149
146
  // Client usage: client.search({ term: "foo", page: 1 })
150
147
  // Results in: GET /api/search?term=foo&page=1
@@ -152,10 +149,10 @@ export const MyApiContract = defineApi("MyApi", () => ({
152
149
 
153
150
  ### Request Body
154
151
 
155
- For POST/PUT/PATCH, the input becomes the JSON body:
152
+ For POST/PUT, the input becomes the JSON body:
156
153
 
157
154
  ```typescript
158
- export const MyApiContract = defineApi("MyApi", () => ({
155
+ export const MyApiContract = new GGContractClass("MyApi", {
159
156
  create: {
160
157
  input: IsCreateRequest,
161
158
  success: IsItem,
@@ -166,7 +163,7 @@ export const MyApiContract = defineApi("MyApi", () => ({
166
163
  success: IsItem,
167
164
  errors: [VALIDATION_ERROR, SERVER_ERROR]
168
165
  }
169
- }))
166
+ })
170
167
  ```
171
168
 
172
169
  ## Authentication & Context
@@ -261,7 +258,7 @@ import { GG_USER_AUTH } from "./auth/UserAuth"
261
258
  import { ClientInfoMiddleware } from "./middleware/ClientInfoMiddleware"
262
259
  import { GG_INTL_LOCALE } from "@grest-ts/intl"
263
260
 
264
- export const MyApiContract = defineApi("MyApi", () => ({
261
+ export const MyApiContract = new GGContractClass("MyApi", {
265
262
  list: {
266
263
  success: IsArray(IsItem),
267
264
  errors: [NOT_AUTHORIZED, SERVER_ERROR]
@@ -271,10 +268,10 @@ export const MyApiContract = defineApi("MyApi", () => ({
271
268
  success: IsItem,
272
269
  errors: [NOT_AUTHORIZED, VALIDATION_ERROR, SERVER_ERROR]
273
270
  }
274
- }))
271
+ })
275
272
 
276
273
  // Chain multiple context providers
277
- export const MyApi = httpApi(MyApiContract)
274
+ export const MyApi = httpSchema(MyApiContract)
278
275
  .pathPrefix("api/items")
279
276
  .useHeader(GG_INTL_LOCALE) // Use codec from context key
280
277
  .useHeader(GG_USER_AUTH) // Use codec from context key
@@ -288,7 +285,7 @@ export const MyApi = httpApi(MyApiContract)
288
285
  ### Public API (No Auth)
289
286
 
290
287
  ```typescript
291
- export const PublicApiContract = defineApi("PublicApi", () => ({
288
+ export const PublicApiContract = new GGContractClass("PublicApi", {
292
289
  status: {
293
290
  success: IsStatusResponse,
294
291
  errors: [SERVER_ERROR]
@@ -298,9 +295,9 @@ export const PublicApiContract = defineApi("PublicApi", () => ({
298
295
  success: IsLoginResponse,
299
296
  errors: [VALIDATION_ERROR, SERVER_ERROR]
300
297
  }
301
- }))
298
+ })
302
299
 
303
- export const PublicApi = httpApi(PublicApiContract)
300
+ export const PublicApi = httpSchema(PublicApiContract)
304
301
  .pathPrefix("pub")
305
302
  .routes({
306
303
  status: GGRpc.GET("status"),
@@ -313,18 +310,12 @@ export const PublicApi = httpApi(PublicApiContract)
313
310
  ### Declaring Errors in Contract
314
311
 
315
312
  ```typescript
316
- import { defineApi, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST } from "@grest-ts/contract"
313
+ import { GGContractClass, NOT_FOUND, FORBIDDEN, VALIDATION_ERROR, SERVER_ERROR, BAD_REQUEST, ERROR } from "@grest-ts/schema"
317
314
 
318
315
  // Custom error type
319
- export class InvalidCredentialsError extends BAD_REQUEST<"INVALID_CREDENTIALS", undefined> {
320
- public static TYPE = "INVALID_CREDENTIALS"
316
+ const INVALID_CREDENTIALS = ERROR.define("INVALID_CREDENTIALS", 400)
321
317
 
322
- constructor() {
323
- super("INVALID_CREDENTIALS", undefined)
324
- }
325
- }
326
-
327
- export const MyApiContract = defineApi("MyApi", () => ({
318
+ export const MyApiContract = new GGContractClass("MyApi", {
328
319
  get: {
329
320
  input: IsItemIdParam,
330
321
  success: IsItem,
@@ -338,17 +329,17 @@ export const MyApiContract = defineApi("MyApi", () => ({
338
329
  login: {
339
330
  input: IsLoginRequest,
340
331
  success: IsLoginResponse,
341
- errors: [InvalidCredentialsError, VALIDATION_ERROR, SERVER_ERROR]
332
+ errors: [INVALID_CREDENTIALS, VALIDATION_ERROR, SERVER_ERROR]
342
333
  }
343
- }))
334
+ })
344
335
  ```
345
336
 
346
337
  ### Throwing Errors in Service
347
338
 
348
339
  ```typescript
349
- import { GGServerApi, NOT_FOUND, FORBIDDEN } from "@grest-ts/node"
340
+ import { NOT_FOUND, FORBIDDEN } from "@grest-ts/schema"
350
341
 
351
- export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
342
+ export class MyService {
352
343
  async get({ id }: { id: tItemId }): Promise<Item> {
353
344
  const item = await this.findItem(id)
354
345
  if (!item) throw new NOT_FOUND()
@@ -359,7 +350,7 @@ export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
359
350
  const item = await this.findItem(request.id)
360
351
  if (!item) throw new NOT_FOUND()
361
352
 
362
- const user = UserContext.get()
353
+ const user = GG_USER_AUTH.get()
363
354
  if (item.ownerId !== user.id) throw new FORBIDDEN()
364
355
 
365
356
  return this.updateItem(item, request)
@@ -372,32 +363,33 @@ export class MyService implements GGServerApi<typeof MyApiContract["methods"]> {
372
363
  ### Using GGHttp (Fluent API)
373
364
 
374
365
  ```typescript
375
- import { GGHttp } from "@grest-ts/http"
366
+ import { GGHttp, GGHttpServer } from "@grest-ts/http"
376
367
 
377
368
  protected compose(): void {
378
- new GGHttp()
369
+ const httpServer = new GGHttpServer()
370
+
371
+ new GGHttp(httpServer)
379
372
  .http(PublicApi, publicService)
380
373
  .http(StatusApi, {
381
374
  status: async () => ({ status: true })
382
375
  })
383
376
 
384
- new GGHttp("authenticated")
377
+ new GGHttp(httpServer)
385
378
  .use(new UserContextMiddleware(userService))
386
379
  .http(MyApi, myService)
387
380
  .http(UserAuthApi, userService)
388
- .websocket(NotificationApi, notificationService.handleConnection)
389
381
  }
390
382
  ```
391
383
 
392
- ### Using GGHttpServer (Direct)
384
+ ### Using GGHttpSchema.register (Direct)
393
385
 
394
386
  ```typescript
395
387
  import { GGHttpServer } from "@grest-ts/http"
396
388
 
397
389
  protected compose(): void {
398
390
  const httpServer = new GGHttpServer()
399
- MyApi.startServer(httpServer, myService)
400
- OtherApi.startServer(httpServer, otherService)
391
+ MyApi.register(myService, { http: httpServer })
392
+ OtherApi.register(otherService, { http: httpServer })
401
393
  }
402
394
  ```
403
395
 
@@ -406,11 +398,13 @@ protected compose(): void {
406
398
  ```typescript
407
399
  protected compose(): void {
408
400
  // Main public server
409
- new GGHttp()
401
+ const publicServer = new GGHttpServer()
402
+ new GGHttp(publicServer)
410
403
  .http(PublicApi, publicService)
411
404
 
412
405
  // Internal server on different port
413
- new GGHttp("internal")
406
+ const internalServer = new GGHttpServer({ port: 9090 })
407
+ new GGHttp(internalServer)
414
408
  .http(InternalApi, internalService)
415
409
  }
416
410
  ```
@@ -420,29 +414,34 @@ protected compose(): void {
420
414
  ### Creating Clients
421
415
 
422
416
  ```typescript
423
- // Unauthenticated client
417
+ import { GGHttpClientConfig } from "@grest-ts/http"
418
+
419
+ // With explicit URL
424
420
  const client = MyApi.createClient({ url: "http://localhost:3000" })
425
421
 
426
- // Authenticated client
427
- const authClient = MyApi.createClient(authState, { url: "http://localhost:3000" })
422
+ // Browser same-origin (use empty string)
423
+ const client = MyApi.createClient({ url: "" })
424
+
425
+ // With options
426
+ const client = MyApi.createClient({ url: "http://localhost:3000", timeout: 30000 })
428
427
 
429
- // Test client
430
- const testClient = MyApi.createTestClient()
428
+ // Without URL (uses service discovery)
429
+ const client = MyApi.createClient()
431
430
  ```
432
431
 
433
432
  ### Making Requests
434
433
 
435
434
  ```typescript
436
- // Simple request
435
+ // Simple request (no input)
437
436
  const items = await client.list()
438
437
 
439
438
  // With path parameter
440
439
  const item = await client.get({ id: "item-123" })
441
440
 
442
- // With query parameters
441
+ // With query parameters (GET request)
443
442
  const results = await client.search({ term: "foo", page: 1 })
444
443
 
445
- // With body
444
+ // With body (POST request)
446
445
  const newItem = await client.create({ title: "New Item" })
447
446
  ```
448
447
 
@@ -459,6 +458,18 @@ if (result.success) {
459
458
  } else {
460
459
  console.log("Error:", result.type) // "NOT_FOUND", etc.
461
460
  }
461
+
462
+ // Using .orDefault() for fallback values
463
+ const item = await client.get({ id: "item-123" }).orDefault(() => defaultItem)
464
+
465
+ // Using .or() for error recovery
466
+ const item = await client.get({ id: "item-123" }).or((error) => {
467
+ if (error.type === "NOT_FOUND") return defaultItem
468
+ throw error
469
+ })
470
+
471
+ // Using .map() to transform the result
472
+ const title = await client.get({ id: "item-123" }).map(item => item.title)
462
473
  ```
463
474
 
464
475
  ## WebSocket APIs
@@ -466,10 +477,8 @@ if (result.success) {
466
477
  ### Defining WebSocket API
467
478
 
468
479
  ```typescript
469
- import { webSocketApi } from "@grest-ts/http"
470
- import { defineTwoWayApi, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/contract"
471
- import { IsObject, IsString, IsBoolean } from "@grest-ts/schema"
472
- import { GG_USER_AUTH_TOKEN } from "./auth/UserAuth"
480
+ import { defineSocketContract, webSocketSchema } from "@grest-ts/websocket"
481
+ import { IsObject, IsString, IsBoolean, SERVER_ERROR, VALIDATION_ERROR } from "@grest-ts/schema"
473
482
 
474
483
  // Message schemas
475
484
  export const IsItemMarkedEvent = IsObject({
@@ -489,7 +498,7 @@ export const IsUpdateItemResponse = IsObject({
489
498
  })
490
499
 
491
500
  // Contract definition
492
- export const NotificationApiContract = defineTwoWayApi("NotificationApi", () => ({
501
+ export const NotificationApiContract = defineSocketContract("NotificationApi", {
493
502
  clientToServer: {
494
503
  updateItem: {
495
504
  input: IsUpdateItemRequest,
@@ -507,27 +516,24 @@ export const NotificationApiContract = defineTwoWayApi("NotificationApi", () =>
507
516
  errors: [SERVER_ERROR]
508
517
  }
509
518
  }
510
- }))
519
+ })
511
520
 
512
- export const NotificationApi = webSocketApi(NotificationApiContract)
521
+ export const NotificationApi = webSocketSchema(NotificationApiContract)
513
522
  .path("ws/notifications")
514
- .use(GG_USER_AUTH_TOKEN)
523
+ .use(AuthMiddleware)
515
524
  .done()
516
525
  ```
517
526
 
518
527
  ### WebSocket Server Handler
519
528
 
520
529
  ```typescript
521
- import { GGSocketApi, WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/http"
522
-
523
- type IncomingHandler = WebSocketIncoming<GGSocketApi<typeof NotificationApiContract.methods["clientToServer"]>>
524
- type OutgoingConnection = WebSocketOutgoing<GGSocketApi<typeof NotificationApiContract.methods["serverToClient"]>>
530
+ import { WebSocketIncoming, WebSocketOutgoing } from "@grest-ts/websocket"
525
531
 
526
532
  export class NotificationService {
527
- private connections = new Map<string, Set<OutgoingConnection>>()
533
+ private connections = new Map<string, Set<WebSocketOutgoing<any>>>()
528
534
 
529
- handleConnection = (incoming: IncomingHandler, outgoing: OutgoingConnection): void => {
530
- const user = UserContext.get()
535
+ handleConnection = (incoming: WebSocketIncoming<any>, outgoing: WebSocketOutgoing<any>): void => {
536
+ const user = GG_USER_AUTH.get()
531
537
 
532
538
  // Track connection
533
539
  if (!this.connections.has(user.id)) {
@@ -563,56 +569,44 @@ export class NotificationService {
563
569
  ### WebSocket in Runtime
564
570
 
565
571
  ```typescript
572
+ import { GGHttpServer } from "@grest-ts/http"
573
+
566
574
  protected compose(): void {
567
- new GGHttp()
575
+ const httpServer = new GGHttpServer()
576
+
577
+ new GGHttp(httpServer)
568
578
  .use(new UserContextMiddleware(userService))
569
- .websocket(NotificationApi, notificationService.handleConnection)
579
+ .http(MyApi, myService)
580
+
581
+ // Register WebSocket on the same HTTP server
582
+ NotificationApi.register(notificationService.handleConnection, { http: httpServer })
570
583
  }
571
584
  ```
572
585
 
573
- ### WebSocket Client (Browser)
586
+ ### WebSocket Client
574
587
 
575
588
  ```typescript
576
- // Connect with message handlers
577
- const socket = await authenticatedSDK.connectNotification({
578
- itemMarked: (event) => {
579
- console.log("Item marked:", event.item.title)
580
- },
581
- areYouThere: async () => {
582
- return true
583
- }
589
+ import { GGSocketPool } from "@grest-ts/websocket"
590
+
591
+ // Connect via socket pool (reuses connections for same URL + headers)
592
+ const socket = await GGSocketPool.getOrConnect({
593
+ domain: "ws://localhost:3000",
594
+ path: "/ws/notifications",
595
+ middlewares: NotificationApi.middlewares
584
596
  })
585
597
 
586
- // Send messages
587
- const response = await socket.updateItem({ item, reason: "Updated via UI" })
598
+ // Send messages via the socket
599
+ const response = await socket.send("NotificationApi.updateItem", { item, reason: "Updated via UI" }, true)
600
+
601
+ // Handle disconnect
602
+ socket.onClose(() => {
603
+ console.log("Disconnected")
604
+ })
588
605
 
589
606
  // Close connection
590
607
  socket.close()
591
- ```
592
-
593
- ## SDK (Auto-Generated)
594
-
595
- The SDK is auto-generated from your API definitions. The generated SDK provides:
596
-
597
- - Type-safe client methods for all endpoints
598
- - Automatic auth token handling
599
- - WebSocket connection management
600
- - Error type inference
601
-
602
- ### Using Generated SDK
603
608
 
604
- ```typescript
605
- import { UserAppSDK } from "./UserAppSDK.gen"
606
-
607
- const sdk = new UserAppSDK({ url: "http://localhost:3000" })
608
-
609
- // Public endpoints
610
- const loginResult = await sdk.login({ username, password })
611
-
612
- // Authenticated endpoints (returned from login)
613
- const authSDK = loginResult.data.sdk
614
- const items = await authSDK.checklist.list()
615
- const socket = await authSDK.connectNotification({
616
- itemMarked: (event) => console.log(event)
617
- })
609
+ // Close all pooled connections
610
+ await GGSocketPool.closeAll()
618
611
  ```
612
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grest-ts/http",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "HTTP server and client library for Node.js and browser",
@@ -55,20 +55,20 @@
55
55
  "node": ">=25"
56
56
  },
57
57
  "dependencies": {
58
- "@grest-ts/common": "0.0.12",
59
- "@grest-ts/context": "0.0.12",
60
- "@grest-ts/logger": "0.0.12",
61
- "@grest-ts/schema": "0.0.12",
62
- "@grest-ts/trace": "0.0.12",
58
+ "@grest-ts/common": "0.0.14",
59
+ "@grest-ts/context": "0.0.14",
60
+ "@grest-ts/logger": "0.0.14",
61
+ "@grest-ts/schema": "0.0.14",
62
+ "@grest-ts/trace": "0.0.14",
63
63
  "find-my-way": "^9.4.0"
64
64
  },
65
65
  "peerDependencies": {
66
- "@grest-ts/discovery": "0.0.12",
67
- "@grest-ts/locator": "0.0.12",
68
- "@grest-ts/metrics": "0.0.12"
66
+ "@grest-ts/discovery": "0.0.14",
67
+ "@grest-ts/locator": "0.0.14",
68
+ "@grest-ts/metrics": "0.0.14"
69
69
  },
70
70
  "devDependencies": {
71
- "@grest-ts/testkit": "0.0.12",
72
- "@grest-ts/x-packager": "0.0.12"
71
+ "@grest-ts/testkit": "0.0.14",
72
+ "@grest-ts/x-packager": "0.0.14"
73
73
  }
74
74
  }