laravel-query-gate-sdk 1.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/README.md +689 -0
- package/dist/index.d.mts +493 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +692 -0
- package/dist/index.mjs +635 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# Laravel Query Gate SDK
|
|
2
|
+
|
|
3
|
+
A contract-driven TypeScript SDK for [Laravel Query Gate](https://github.com/behindSolution/laravel-query-gate) that provides strongly-typed API interactions with compile-time safety.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Contract-Driven**: One contract per resource defines all operations
|
|
8
|
+
- **Type-Safe**: Full TypeScript support with compile-time validation
|
|
9
|
+
- **Fluent API**: Chainable, immutable builder pattern
|
|
10
|
+
- **Laravel-Native Error Handling**: Built-in support for all Laravel error responses
|
|
11
|
+
- **Zero Runtime Overhead**: Contracts exist only for TypeScript, no reflection
|
|
12
|
+
- **Framework Agnostic**: Works with any frontend framework or Node.js
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install laravel-query-gate-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
yarn add laravel-query-gate-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add laravel-query-gate-sdk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { queryGate, configureQueryGate } from 'laravel-query-gate-sdk'
|
|
32
|
+
|
|
33
|
+
// 1. Configure the SDK
|
|
34
|
+
configureQueryGate({
|
|
35
|
+
baseUrl: 'https://api.example.com',
|
|
36
|
+
defaultHeaders: {
|
|
37
|
+
Authorization: 'Bearer your-token',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// 2. Define your contract
|
|
42
|
+
interface PostContract {
|
|
43
|
+
get: Post[]
|
|
44
|
+
create: CreatePostPayload
|
|
45
|
+
update: UpdatePostPayload
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Use the SDK
|
|
49
|
+
const posts = await queryGate<PostContract>('posts')
|
|
50
|
+
.filter('status', 'eq', 'published')
|
|
51
|
+
.get()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Table of Contents
|
|
55
|
+
|
|
56
|
+
- [Configuration](#configuration)
|
|
57
|
+
- [Defining Contracts](#defining-contracts)
|
|
58
|
+
- [Read Operations](#read-operations)
|
|
59
|
+
- [Write Operations](#write-operations)
|
|
60
|
+
- [Custom Actions](#custom-actions)
|
|
61
|
+
- [Query Builder](#query-builder)
|
|
62
|
+
- [Versioning](#versioning)
|
|
63
|
+
- [Headers & Options](#headers--options)
|
|
64
|
+
- [Error Handling](#error-handling)
|
|
65
|
+
- [API Reference](#api-reference)
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
### Global Configuration
|
|
70
|
+
|
|
71
|
+
Configure the SDK once at application startup:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { configureQueryGate } from 'laravel-query-gate-sdk'
|
|
75
|
+
|
|
76
|
+
configureQueryGate({
|
|
77
|
+
baseUrl: 'https://api.example.com',
|
|
78
|
+
defaultHeaders: {
|
|
79
|
+
'Authorization': 'Bearer token',
|
|
80
|
+
'X-Tenant-ID': 'tenant-1',
|
|
81
|
+
},
|
|
82
|
+
defaultFetchOptions: {
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
mode: 'cors',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Isolated Instances
|
|
90
|
+
|
|
91
|
+
For multi-tenant applications or testing, create isolated instances:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createQueryGate } from 'laravel-query-gate-sdk'
|
|
95
|
+
|
|
96
|
+
const tenantApi = createQueryGate({
|
|
97
|
+
baseUrl: 'https://tenant1.api.example.com',
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const posts = await tenantApi<PostContract>('posts').get()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Configuration Options
|
|
104
|
+
|
|
105
|
+
| Option | Type | Description |
|
|
106
|
+
|--------|------|-------------|
|
|
107
|
+
| `baseUrl` | `string` | **Required.** Base URL for all API requests |
|
|
108
|
+
| `defaultHeaders` | `Record<string, string>` | Headers included in every request |
|
|
109
|
+
| `defaultFetchOptions` | `Partial<RequestInit>` | Default fetch options (credentials, mode, etc.) |
|
|
110
|
+
|
|
111
|
+
## Defining Contracts
|
|
112
|
+
|
|
113
|
+
Contracts define the shape of your API resources. Each resource has one contract that describes:
|
|
114
|
+
|
|
115
|
+
- **Read operations** (`get`)
|
|
116
|
+
- **Write operations** (`create`, `update`)
|
|
117
|
+
- **Custom actions** (`actions`)
|
|
118
|
+
|
|
119
|
+
### Basic Contract
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import type { ResourceContract } from 'laravel-query-gate-sdk'
|
|
123
|
+
|
|
124
|
+
interface Post {
|
|
125
|
+
id: number
|
|
126
|
+
title: string
|
|
127
|
+
content: string
|
|
128
|
+
status: 'draft' | 'published'
|
|
129
|
+
created_at: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface CreatePostPayload {
|
|
133
|
+
title: string
|
|
134
|
+
content: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface UpdatePostPayload {
|
|
138
|
+
title?: string
|
|
139
|
+
content?: string
|
|
140
|
+
status?: 'draft' | 'published'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface PostContract extends ResourceContract {
|
|
144
|
+
get: Post[]
|
|
145
|
+
create: CreatePostPayload
|
|
146
|
+
update: UpdatePostPayload
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Contract with Custom Actions
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
interface PostContract extends ResourceContract {
|
|
154
|
+
get: Post[]
|
|
155
|
+
create: CreatePostPayload
|
|
156
|
+
update: UpdatePostPayload
|
|
157
|
+
|
|
158
|
+
actions: {
|
|
159
|
+
// Action without payload
|
|
160
|
+
publish: {
|
|
161
|
+
method: 'post'
|
|
162
|
+
payload?: never
|
|
163
|
+
response: Post
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Action with payload
|
|
167
|
+
bulkPublish: {
|
|
168
|
+
method: 'post'
|
|
169
|
+
payload: { ids: number[] }
|
|
170
|
+
response: { updated: number }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// GET action
|
|
174
|
+
stats: {
|
|
175
|
+
method: 'get'
|
|
176
|
+
payload?: never
|
|
177
|
+
response: { total: number; published: number }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Search action
|
|
181
|
+
search: {
|
|
182
|
+
method: 'post'
|
|
183
|
+
payload: { query: string; filters?: Record<string, unknown> }
|
|
184
|
+
response: Post[]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Read-Only Contract
|
|
191
|
+
|
|
192
|
+
If a resource only supports reading:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface ReadOnlyPostContract extends ResourceContract {
|
|
196
|
+
get: Post[]
|
|
197
|
+
// No create or update - those methods will be hidden by TypeScript
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Read Operations
|
|
202
|
+
|
|
203
|
+
### Fetch Collection
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const posts = await queryGate<PostContract>('posts').get()
|
|
207
|
+
// posts: Post[]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Fetch Single Resource
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const post = await queryGate<PostContract>('posts').id(1).get()
|
|
214
|
+
// post: Post
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### With Query Parameters
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const posts = await queryGate<PostContract>('posts')
|
|
221
|
+
.filter('status', 'eq', 'published')
|
|
222
|
+
.filter('author_id', 'eq', 1)
|
|
223
|
+
.sort('created_at', 'desc')
|
|
224
|
+
.page(1)
|
|
225
|
+
.perPage(10)
|
|
226
|
+
.get()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Write Operations
|
|
230
|
+
|
|
231
|
+
### Create (POST)
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const newPost = await queryGate<PostContract>('posts').post({
|
|
235
|
+
title: 'My New Post',
|
|
236
|
+
content: 'Hello, world!',
|
|
237
|
+
})
|
|
238
|
+
// newPost: Post
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Update (PATCH)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const updatedPost = await queryGate<PostContract>('posts')
|
|
245
|
+
.id(1)
|
|
246
|
+
.patch({
|
|
247
|
+
title: 'Updated Title',
|
|
248
|
+
status: 'published',
|
|
249
|
+
})
|
|
250
|
+
// updatedPost: Post
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Delete
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
await queryGate<PostContract>('posts').id(1).delete()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Custom Actions
|
|
260
|
+
|
|
261
|
+
Custom actions allow you to call non-CRUD endpoints defined in your Laravel Query Gate resource.
|
|
262
|
+
|
|
263
|
+
### Action Without Payload
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// POST /posts/1/publish
|
|
267
|
+
const publishedPost = await queryGate<PostContract>('posts')
|
|
268
|
+
.id(1)
|
|
269
|
+
.action('publish')
|
|
270
|
+
.post()
|
|
271
|
+
// publishedPost: Post
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Action With Payload
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// POST /posts/bulk-publish
|
|
278
|
+
const result = await queryGate<PostContract>('posts')
|
|
279
|
+
.action('bulkPublish')
|
|
280
|
+
.post({ ids: [1, 2, 3, 4, 5] })
|
|
281
|
+
// result: { updated: number }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### GET Action
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// GET /posts/stats
|
|
288
|
+
const stats = await queryGate<PostContract>('posts')
|
|
289
|
+
.action('stats')
|
|
290
|
+
.get()
|
|
291
|
+
// stats: { total: number; published: number }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Type Safety
|
|
295
|
+
|
|
296
|
+
TypeScript enforces correct usage:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Error: payload not allowed for 'publish' action
|
|
300
|
+
await queryGate<PostContract>('posts')
|
|
301
|
+
.id(1)
|
|
302
|
+
.action('publish')
|
|
303
|
+
.post({ foo: 'bar' }) // TypeScript error!
|
|
304
|
+
|
|
305
|
+
// Error: payload required for 'bulkPublish' action
|
|
306
|
+
await queryGate<PostContract>('posts')
|
|
307
|
+
.action('bulkPublish')
|
|
308
|
+
.post() // TypeScript error!
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Query Builder
|
|
312
|
+
|
|
313
|
+
### Filters
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
queryGate<PostContract>('posts')
|
|
317
|
+
.filter('status', 'eq', 'published')
|
|
318
|
+
.filter('views', 'gte', 100)
|
|
319
|
+
.filter('category_id', 'in', [1, 2, 3])
|
|
320
|
+
.get()
|
|
321
|
+
// Generates: ?filter[status][eq]=published&filter[views][gte]=100&filter[category_id][in]=1,2,3
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Filtering on Relations:**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
queryGate<PostContract>('posts')
|
|
328
|
+
.filter('author.name', 'like', 'John')
|
|
329
|
+
.filter('category.is_active', 'eq', 1)
|
|
330
|
+
.get()
|
|
331
|
+
// Generates: ?filter[author.name][like]=John&filter[category.is_active][eq]=1
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Available Operators:**
|
|
335
|
+
|
|
336
|
+
| Operator | Description |
|
|
337
|
+
|----------|-------------|
|
|
338
|
+
| `eq` | Equal to |
|
|
339
|
+
| `neq` | Not equal to |
|
|
340
|
+
| `gt` | Greater than |
|
|
341
|
+
| `gte` | Greater or equal |
|
|
342
|
+
| `lt` | Less than |
|
|
343
|
+
| `lte` | Less or equal |
|
|
344
|
+
| `in` | In array |
|
|
345
|
+
| `not_in` | Not in array |
|
|
346
|
+
| `like` | Pattern match |
|
|
347
|
+
| `between` | Range (two values) |
|
|
348
|
+
|
|
349
|
+
### Sorting
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
queryGate<PostContract>('posts')
|
|
353
|
+
.sort('created_at', 'desc')
|
|
354
|
+
.sort('title', 'asc')
|
|
355
|
+
.get()
|
|
356
|
+
// Generates: ?sort=created_at:desc,title:asc
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Pagination
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
queryGate<PostContract>('posts')
|
|
363
|
+
.page(2)
|
|
364
|
+
.perPage(25)
|
|
365
|
+
.get()
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Versioning
|
|
369
|
+
|
|
370
|
+
Laravel Query Gate supports API versioning via the `X-Query-Version` header:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const posts = await queryGate<PostContract>('posts')
|
|
374
|
+
.version('2.0')
|
|
375
|
+
.get()
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Headers & Options
|
|
379
|
+
|
|
380
|
+
### Single Header
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
queryGate<PostContract>('posts')
|
|
384
|
+
.header('X-Custom-Header', 'value')
|
|
385
|
+
.get()
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Multiple Headers
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
queryGate<PostContract>('posts')
|
|
392
|
+
.headers({
|
|
393
|
+
'X-Custom-Header': 'value',
|
|
394
|
+
'X-Another-Header': 'another-value',
|
|
395
|
+
})
|
|
396
|
+
.get()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Fetch Options
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
const controller = new AbortController()
|
|
403
|
+
|
|
404
|
+
queryGate<PostContract>('posts')
|
|
405
|
+
.options({
|
|
406
|
+
signal: controller.signal,
|
|
407
|
+
credentials: 'include',
|
|
408
|
+
mode: 'cors',
|
|
409
|
+
cache: 'no-cache',
|
|
410
|
+
})
|
|
411
|
+
.get()
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Error Handling
|
|
415
|
+
|
|
416
|
+
The SDK provides specific error classes for all common Laravel error responses.
|
|
417
|
+
|
|
418
|
+
### Error Types
|
|
419
|
+
|
|
420
|
+
| Status | Error Class | Laravel Context |
|
|
421
|
+
|--------|-------------|-----------------|
|
|
422
|
+
| 401 | `QueryGateUnauthorizedError` | Auth middleware |
|
|
423
|
+
| 403 | `QueryGateForbiddenError` | Policy/Gate denial |
|
|
424
|
+
| 404 | `QueryGateNotFoundError` | Model not found |
|
|
425
|
+
| 419 | `QueryGateCsrfMismatchError` | CSRF token invalid/expired |
|
|
426
|
+
| 422 | `QueryGateValidationError` | Form Request validation |
|
|
427
|
+
| 429 | `QueryGateRateLimitError` | Throttle middleware |
|
|
428
|
+
| 500 | `QueryGateServerError` | Internal server error |
|
|
429
|
+
| 503 | `QueryGateServiceUnavailableError` | Maintenance mode |
|
|
430
|
+
| - | `QueryGateNetworkError` | Connection failed |
|
|
431
|
+
| - | `QueryGateHttpError` | Other HTTP errors |
|
|
432
|
+
|
|
433
|
+
### Type Guards
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import {
|
|
437
|
+
isUnauthorizedError,
|
|
438
|
+
isForbiddenError,
|
|
439
|
+
isNotFoundError,
|
|
440
|
+
isCsrfMismatchError,
|
|
441
|
+
isValidationError,
|
|
442
|
+
isRateLimitError,
|
|
443
|
+
isServerError,
|
|
444
|
+
isServiceUnavailableError,
|
|
445
|
+
isNetworkError,
|
|
446
|
+
isHttpError,
|
|
447
|
+
} from 'laravel-query-gate-sdk'
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Complete Error Handling Example
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import {
|
|
454
|
+
queryGate,
|
|
455
|
+
isUnauthorizedError,
|
|
456
|
+
isForbiddenError,
|
|
457
|
+
isNotFoundError,
|
|
458
|
+
isCsrfMismatchError,
|
|
459
|
+
isValidationError,
|
|
460
|
+
isRateLimitError,
|
|
461
|
+
isServerError,
|
|
462
|
+
isServiceUnavailableError,
|
|
463
|
+
isNetworkError,
|
|
464
|
+
} from 'laravel-query-gate-sdk'
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await queryGate<PostContract>('posts').post({
|
|
468
|
+
title: 'New Post',
|
|
469
|
+
content: 'Content here',
|
|
470
|
+
})
|
|
471
|
+
} catch (error) {
|
|
472
|
+
// 401 - Unauthorized
|
|
473
|
+
if (isUnauthorizedError(error)) {
|
|
474
|
+
console.error('Please log in')
|
|
475
|
+
redirectToLogin()
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 403 - Forbidden
|
|
480
|
+
if (isForbiddenError(error)) {
|
|
481
|
+
console.error('You do not have permission')
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 404 - Not Found
|
|
486
|
+
if (isNotFoundError(error)) {
|
|
487
|
+
console.error('Resource not found')
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 419 - CSRF Token Mismatch
|
|
492
|
+
if (isCsrfMismatchError(error)) {
|
|
493
|
+
console.error('Session expired. Please refresh.')
|
|
494
|
+
refreshCsrfToken()
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// 422 - Validation Error
|
|
499
|
+
if (isValidationError(error)) {
|
|
500
|
+
console.error('Validation failed:', error.originalMessage)
|
|
501
|
+
|
|
502
|
+
// Access all errors
|
|
503
|
+
console.error('Errors:', error.errors)
|
|
504
|
+
// { title: ['The title field is required.'], ... }
|
|
505
|
+
|
|
506
|
+
// Check specific field
|
|
507
|
+
if (error.hasFieldError('title')) {
|
|
508
|
+
console.error('Title error:', error.getFirstFieldError('title'))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Get all fields with errors
|
|
512
|
+
console.error('Fields with errors:', error.getErrorFields())
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 429 - Rate Limit
|
|
517
|
+
if (isRateLimitError(error)) {
|
|
518
|
+
console.error('Too many requests')
|
|
519
|
+
|
|
520
|
+
if (error.hasRetryAfter()) {
|
|
521
|
+
console.error(`Retry after ${error.retryAfter} seconds`)
|
|
522
|
+
const retryDate = error.getRetryDate()
|
|
523
|
+
console.error(`Retry at: ${retryDate?.toLocaleTimeString()}`)
|
|
524
|
+
}
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 500 - Server Error
|
|
529
|
+
if (isServerError(error)) {
|
|
530
|
+
console.error('Server error. Please try again later.')
|
|
531
|
+
reportToErrorTracking(error)
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 503 - Service Unavailable (Maintenance)
|
|
536
|
+
if (isServiceUnavailableError(error)) {
|
|
537
|
+
console.error('Service temporarily unavailable')
|
|
538
|
+
|
|
539
|
+
if (error.hasRetryAfter()) {
|
|
540
|
+
console.error(`Back in approximately ${error.retryAfter} seconds`)
|
|
541
|
+
}
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Network Error
|
|
546
|
+
if (isNetworkError(error)) {
|
|
547
|
+
console.error('Network error:', error.originalError.message)
|
|
548
|
+
console.error('Please check your internet connection')
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Unknown error
|
|
553
|
+
throw error
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Validation Error Details
|
|
558
|
+
|
|
559
|
+
The `QueryGateValidationError` provides helper methods for working with Laravel's validation error format:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
if (isValidationError(error)) {
|
|
563
|
+
// Get all errors for a field
|
|
564
|
+
const titleErrors: string[] = error.getFieldErrors('title')
|
|
565
|
+
// ['The title field is required.', 'The title must be at least 3 characters.']
|
|
566
|
+
|
|
567
|
+
// Get first error for a field
|
|
568
|
+
const firstError: string | undefined = error.getFirstFieldError('title')
|
|
569
|
+
// 'The title field is required.'
|
|
570
|
+
|
|
571
|
+
// Check if field has errors
|
|
572
|
+
const hasError: boolean = error.hasFieldError('title')
|
|
573
|
+
// true
|
|
574
|
+
|
|
575
|
+
// Get all fields with errors
|
|
576
|
+
const fields: string[] = error.getErrorFields()
|
|
577
|
+
// ['title', 'content', 'author_id']
|
|
578
|
+
|
|
579
|
+
// Access raw errors object
|
|
580
|
+
const rawErrors: Record<string, string[]> = error.errors
|
|
581
|
+
// { title: [...], content: [...] }
|
|
582
|
+
|
|
583
|
+
// Access original Laravel message
|
|
584
|
+
const message: string = error.originalMessage
|
|
585
|
+
// 'The given data was invalid.'
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Rate Limit & Service Unavailable Retry-After
|
|
590
|
+
|
|
591
|
+
Both `QueryGateRateLimitError` (429) and `QueryGateServiceUnavailableError` (503) support the `Retry-After` header:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
if (isRateLimitError(error) || isServiceUnavailableError(error)) {
|
|
595
|
+
// Check if retry information is available
|
|
596
|
+
if (error.hasRetryAfter()) {
|
|
597
|
+
// Get seconds until retry
|
|
598
|
+
const seconds: number | null = error.retryAfter
|
|
599
|
+
// 60
|
|
600
|
+
|
|
601
|
+
// Get Date object for when retry is allowed
|
|
602
|
+
const retryDate: Date | null = error.getRetryDate()
|
|
603
|
+
// Date object
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## API Reference
|
|
609
|
+
|
|
610
|
+
### Entry Points
|
|
611
|
+
|
|
612
|
+
#### `configureQueryGate(config: QueryGateConfig): void`
|
|
613
|
+
|
|
614
|
+
Configure the SDK globally.
|
|
615
|
+
|
|
616
|
+
#### `queryGate<T>(resource: string): QueryGateBuilder<T>`
|
|
617
|
+
|
|
618
|
+
Create a builder for a resource using global configuration.
|
|
619
|
+
|
|
620
|
+
#### `createQueryGate(config: QueryGateConfig): <T>(resource: string) => QueryGateBuilder<T>`
|
|
621
|
+
|
|
622
|
+
Create an isolated SDK instance with its own configuration.
|
|
623
|
+
|
|
624
|
+
### QueryGateBuilder Methods
|
|
625
|
+
|
|
626
|
+
| Method | Description |
|
|
627
|
+
|--------|-------------|
|
|
628
|
+
| `.id(id)` | Set resource ID for single resource operations |
|
|
629
|
+
| `.filter(field, operator, value)` | Add a filter |
|
|
630
|
+
| `.sort(field, direction?)` | Add sorting (default: 'asc') |
|
|
631
|
+
| `.page(page)` | Set page number |
|
|
632
|
+
| `.perPage(count)` | Set items per page |
|
|
633
|
+
| `.version(version)` | Set API version header |
|
|
634
|
+
| `.header(key, value)` | Add a single header |
|
|
635
|
+
| `.headers(record)` | Add multiple headers |
|
|
636
|
+
| `.options(fetchOptions)` | Set fetch options |
|
|
637
|
+
| `.get()` | Execute GET request |
|
|
638
|
+
| `.post(payload)` | Execute POST request |
|
|
639
|
+
| `.action(name)` | Access custom action |
|
|
640
|
+
|
|
641
|
+
### QueryGateBuilderWithId Methods
|
|
642
|
+
|
|
643
|
+
Available after calling `.id()`:
|
|
644
|
+
|
|
645
|
+
| Method | Description |
|
|
646
|
+
|--------|-------------|
|
|
647
|
+
| `.get()` | Fetch single resource |
|
|
648
|
+
| `.patch(payload)` | Update resource |
|
|
649
|
+
| `.delete()` | Delete resource |
|
|
650
|
+
| `.action(name)` | Execute action on resource |
|
|
651
|
+
|
|
652
|
+
### Types
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import type {
|
|
656
|
+
ResourceContract,
|
|
657
|
+
ActionContract,
|
|
658
|
+
FilterOperator,
|
|
659
|
+
SortDirection,
|
|
660
|
+
QueryGateConfig,
|
|
661
|
+
ValidationErrors,
|
|
662
|
+
} from 'laravel-query-gate-sdk'
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
## Design Philosophy
|
|
666
|
+
|
|
667
|
+
The SDK follows these principles:
|
|
668
|
+
|
|
669
|
+
1. **Explicit contracts over magic** - All behavior is declared in contracts
|
|
670
|
+
2. **Compile-time safety over runtime guessing** - TypeScript catches errors before runtime
|
|
671
|
+
3. **Orchestration over domain logic** - SDK is a transport layer only
|
|
672
|
+
4. **Consistency over brevity** - Predictable API surface
|
|
673
|
+
|
|
674
|
+
## Requirements
|
|
675
|
+
|
|
676
|
+
- Node.js 18+
|
|
677
|
+
- TypeScript 5.0+ (recommended)
|
|
678
|
+
|
|
679
|
+
## License
|
|
680
|
+
|
|
681
|
+
MIT
|
|
682
|
+
|
|
683
|
+
## Contributing
|
|
684
|
+
|
|
685
|
+
Contributions are welcome! Please read the contributing guidelines before submitting a PR.
|
|
686
|
+
|
|
687
|
+
## Related
|
|
688
|
+
|
|
689
|
+
- [Laravel Query Gate](https://github.com/behindSolution/laravel-query-gate) - The Laravel package this SDK is built for
|