@klerick/json-api-nestjs-sdk 10.0.0-beta.9 → 10.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/CHANGELOG.md +111 -0
- package/README.md +365 -58
- package/cjs/src/index.js +9 -8
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/lib/json-api-angular.js +22 -18
- package/cjs/src/lib/json-api-angular.js.map +1 -1
- package/cjs/src/lib/json-api-js.js +11 -11
- package/cjs/src/lib/json-api-js.js.map +1 -1
- package/cjs/src/lib/service/atomic-operations.service.js +22 -16
- package/cjs/src/lib/service/atomic-operations.service.js.map +1 -1
- package/cjs/src/lib/service/fetch-inner-client.js +3 -3
- package/cjs/src/lib/service/fetch-inner-client.js.map +1 -1
- package/cjs/src/lib/service/index.js +3 -3
- package/cjs/src/lib/service/index.js.map +1 -1
- package/cjs/src/lib/service/json-api-sdk.service.js +33 -21
- package/cjs/src/lib/service/json-api-sdk.service.js.map +1 -1
- package/cjs/src/lib/service/json-api-utils.service.js +135 -59
- package/cjs/src/lib/service/json-api-utils.service.js.map +1 -1
- package/cjs/src/lib/types/index.js +10 -10
- package/cjs/src/lib/types/index.js.map +1 -1
- package/cjs/src/lib/utils/generate-atomic-body.js +21 -20
- package/cjs/src/lib/utils/generate-atomic-body.js.map +1 -1
- package/cjs/src/lib/utils/index.js +5 -5
- package/cjs/src/lib/utils/index.js.map +1 -1
- package/cjs/src/lib/utils/utils.js +23 -3
- package/cjs/src/lib/utils/utils.js.map +1 -1
- package/cjs/src/ngModule.js +8 -8
- package/cjs/src/ngModule.js.map +1 -1
- package/mjs/package.json +1 -0
- package/mjs/src/index.d.ts +4 -4
- package/mjs/src/index.js +3 -3
- package/mjs/src/index.js.map +1 -1
- package/mjs/src/lib/json-api-angular.d.ts +10 -12
- package/mjs/src/lib/json-api-angular.js +11 -7
- package/mjs/src/lib/json-api-angular.js.map +1 -1
- package/mjs/src/lib/json-api-js.d.ts +4 -4
- package/mjs/src/lib/json-api-js.js +6 -6
- package/mjs/src/lib/json-api-js.js.map +1 -1
- package/mjs/src/lib/service/atomic-operations.service.d.ts +7 -7
- package/mjs/src/lib/service/atomic-operations.service.js +21 -15
- package/mjs/src/lib/service/atomic-operations.service.js.map +1 -1
- package/mjs/src/lib/service/fetch-inner-client.d.ts +2 -2
- package/mjs/src/lib/service/fetch-inner-client.js +1 -1
- package/mjs/src/lib/service/fetch-inner-client.js.map +1 -1
- package/mjs/src/lib/service/index.d.ts +3 -3
- package/mjs/src/lib/service/index.js +3 -3
- package/mjs/src/lib/service/index.js.map +1 -1
- package/mjs/src/lib/service/json-api-sdk.service.d.ts +14 -14
- package/mjs/src/lib/service/json-api-sdk.service.js +29 -17
- package/mjs/src/lib/service/json-api-sdk.service.js.map +1 -1
- package/mjs/src/lib/service/json-api-utils.service.d.ts +6 -5
- package/mjs/src/lib/service/json-api-utils.service.js +128 -52
- package/mjs/src/lib/service/json-api-utils.service.js.map +1 -1
- package/mjs/src/lib/token/index.d.ts +1 -1
- package/mjs/src/lib/types/atomic-operation.d.ts +11 -11
- package/mjs/src/lib/types/atomic-type.d.ts +2 -2
- package/mjs/src/lib/types/config.d.ts +2 -2
- package/mjs/src/lib/types/entity-chain.d.ts +6 -6
- package/mjs/src/lib/types/http-inner-client.d.ts +3 -3
- package/mjs/src/lib/types/http-request-params.d.ts +3 -0
- package/mjs/src/lib/types/index.d.ts +10 -10
- package/mjs/src/lib/types/index.js +10 -10
- package/mjs/src/lib/types/index.js.map +1 -1
- package/mjs/src/lib/types/promise-json-api-sdk.d.ts +16 -16
- package/mjs/src/lib/types/query-params.d.ts +1 -1
- package/mjs/src/lib/utils/adapter-for-axios.d.ts +1 -1
- package/mjs/src/lib/utils/generate-atomic-body.d.ts +7 -7
- package/mjs/src/lib/utils/generate-atomic-body.js +19 -18
- package/mjs/src/lib/utils/generate-atomic-body.js.map +1 -1
- package/mjs/src/lib/utils/index.d.ts +5 -5
- package/mjs/src/lib/utils/index.js +5 -5
- package/mjs/src/lib/utils/index.js.map +1 -1
- package/mjs/src/lib/utils/utils.d.ts +11 -1
- package/mjs/src/lib/utils/utils.js +19 -1
- package/mjs/src/lib/utils/utils.js.map +1 -1
- package/mjs/src/ngModule.d.ts +3 -3
- package/mjs/src/ngModule.js +2 -2
- package/mjs/src/ngModule.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.
|
|
|
16
16
|
- 🔍 **Advanced Filtering** - Rich query builder with operators (eq, ne, in, like, gt, lt, etc.)
|
|
17
17
|
- 📦 **Relationship Handling** - Easy include, sparse fieldsets, and relationship management
|
|
18
18
|
- ⚡ **Atomic Operations** - Batch multiple operations in a single request with rollback support
|
|
19
|
+
- 📤 **Meta Support** - Pass additional business logic data with requests (JSON:API spec compliant)
|
|
19
20
|
- 🌐 **Multiple HTTP Clients** - Works with Axios, Fetch API, and Angular HttpClient
|
|
20
21
|
- 📄 **Pagination & Sorting** - Built-in support for pagination and multi-field sorting
|
|
21
22
|
- 🔄 **Observable or Promise** - Choose your async style (RxJS Observable or native Promise)
|
|
@@ -38,6 +39,7 @@ Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.
|
|
|
38
39
|
- [Using entity() Method](#using-entity-method)
|
|
39
40
|
- [Using String Type Names](#using-string-type-names)
|
|
40
41
|
- [Nullifying Relationships](#-nullifying-relationships)
|
|
42
|
+
- [Clearing To-Many Relationships](#-clearing-to-many-relationships)
|
|
41
43
|
- [Query Options](#-query-options)
|
|
42
44
|
- [Filtering](#filtering)
|
|
43
45
|
- [Sorting](#sorting)
|
|
@@ -45,6 +47,10 @@ Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.
|
|
|
45
47
|
- [Including Relationships](#including-relationships)
|
|
46
48
|
- [Sparse Fieldsets](#sparse-fieldsets)
|
|
47
49
|
- [Atomic Operations](#-atomic-operations)
|
|
50
|
+
- [Meta Support](#-meta-support)
|
|
51
|
+
- [Regular Operations with Meta](#regular-operations-with-meta)
|
|
52
|
+
- [Entity Chain with Meta](#entity-chain-with-meta)
|
|
53
|
+
- [Atomic Operations with Meta](#atomic-operations-with-meta)
|
|
48
54
|
- [Examples](#-examples)
|
|
49
55
|
|
|
50
56
|
|
|
@@ -82,10 +88,10 @@ const jsonSdk = JsonApiJs(
|
|
|
82
88
|
|
|
83
89
|
// 3. Use SDK
|
|
84
90
|
// Fetch all users
|
|
85
|
-
const users = await jsonSdk.
|
|
91
|
+
const users = await jsonSdk.jsonApiSdkService.getAll(Users);
|
|
86
92
|
|
|
87
93
|
// Fetch with filtering and relationships
|
|
88
|
-
const activeUsers = await jsonSdk.
|
|
94
|
+
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
89
95
|
filter: {
|
|
90
96
|
target: {
|
|
91
97
|
isActive: { [FilterOperand.eq]: 'true' }
|
|
@@ -95,7 +101,7 @@ const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
95
101
|
});
|
|
96
102
|
|
|
97
103
|
// Get one user
|
|
98
|
-
const user = await jsonSdk.
|
|
104
|
+
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
|
|
99
105
|
include: ['addresses', 'comments', 'roles', 'manager']
|
|
100
106
|
});
|
|
101
107
|
|
|
@@ -106,14 +112,14 @@ newUser.lastName = 'Doe';
|
|
|
106
112
|
newUser.login = 'johndoe';
|
|
107
113
|
newUser.isActive = true;
|
|
108
114
|
|
|
109
|
-
const createdUser = await jsonSdk.
|
|
115
|
+
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);
|
|
110
116
|
|
|
111
117
|
// Update a user
|
|
112
118
|
createdUser.firstName = 'Jane';
|
|
113
|
-
const updatedUser = await jsonSdk.
|
|
119
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(createdUser);
|
|
114
120
|
|
|
115
121
|
// Delete a user
|
|
116
|
-
await jsonSdk.
|
|
122
|
+
await jsonSdk.jsonApiSdkService.deleteOne(createdUser);
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
### Angular Setup
|
|
@@ -132,6 +138,8 @@ import { Component, inject } from '@angular/core';
|
|
|
132
138
|
import { bootstrapApplication } from '@angular/platform-browser';
|
|
133
139
|
|
|
134
140
|
// 1. Configure in your main.ts or app.config.ts
|
|
141
|
+
|
|
142
|
+
// Option A: Direct configuration object
|
|
135
143
|
const angularConfig = {
|
|
136
144
|
apiHost: 'http://localhost:3000',
|
|
137
145
|
idKey: 'id',
|
|
@@ -147,6 +155,23 @@ bootstrapApplication(AppComponent, {
|
|
|
147
155
|
],
|
|
148
156
|
}).catch((err) => console.error(err));
|
|
149
157
|
|
|
158
|
+
// Option B: Factory function (useful for dynamic configuration)
|
|
159
|
+
bootstrapApplication(AppComponent, {
|
|
160
|
+
providers: [
|
|
161
|
+
provideHttpClient(withFetch()),
|
|
162
|
+
provideJsonApi(() => {
|
|
163
|
+
const env = inject(EnvironmentService);
|
|
164
|
+
return {
|
|
165
|
+
apiHost: env.apiUrl,
|
|
166
|
+
idKey: 'id',
|
|
167
|
+
apiPrefix: 'api',
|
|
168
|
+
operationUrl: 'operation',
|
|
169
|
+
dateFields: ['createdAt', 'updatedAt']
|
|
170
|
+
};
|
|
171
|
+
})
|
|
172
|
+
],
|
|
173
|
+
}).catch((err) => console.error(err));
|
|
174
|
+
|
|
150
175
|
// 2. Use in your components
|
|
151
176
|
@Component({
|
|
152
177
|
standalone: true,
|
|
@@ -190,6 +215,10 @@ type JsonSdkConfig = {
|
|
|
190
215
|
type JsonConfig = JsonSdkConfig & {
|
|
191
216
|
adapter?: HttpInnerClient; // HTTP client adapter (default: fetch)
|
|
192
217
|
}
|
|
218
|
+
|
|
219
|
+
// Angular: provideJsonApi accepts config or factory function
|
|
220
|
+
type JsonSdkConfigFactory = () => JsonSdkConfig;
|
|
221
|
+
type JsonSdkConfigOrFactory = JsonSdkConfig | JsonSdkConfigFactory;
|
|
193
222
|
```
|
|
194
223
|
|
|
195
224
|
### HTTP Adapters
|
|
@@ -229,10 +258,10 @@ Fetch all resources with optional filtering, sorting, and relationships.
|
|
|
229
258
|
import { FilterOperand } from '@klerick/json-api-nestjs-sdk';
|
|
230
259
|
|
|
231
260
|
// Fetch all users
|
|
232
|
-
const users = await jsonSdk.
|
|
261
|
+
const users = await jsonSdk.jsonApiSdkService.getAll(Users);
|
|
233
262
|
|
|
234
263
|
// With filtering
|
|
235
|
-
const activeUsers = await jsonSdk.
|
|
264
|
+
const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
236
265
|
filter: {
|
|
237
266
|
target: {
|
|
238
267
|
isActive: { [FilterOperand.eq]: 'true' },
|
|
@@ -243,7 +272,7 @@ const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
243
272
|
});
|
|
244
273
|
|
|
245
274
|
// Filter by relationship
|
|
246
|
-
const usersWithRoles = await jsonSdk.
|
|
275
|
+
const usersWithRoles = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
247
276
|
filter: {
|
|
248
277
|
target: {
|
|
249
278
|
id: { [FilterOperand.in]: ['1', '2'] }
|
|
@@ -261,7 +290,7 @@ const usersWithRoles = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
261
290
|
Fetch resources with pagination (returns paginated results).
|
|
262
291
|
|
|
263
292
|
```typescript
|
|
264
|
-
const firstPage = await jsonSdk.
|
|
293
|
+
const firstPage = await jsonSdk.jsonApiSdkService.getList(Users, {
|
|
265
294
|
page: {
|
|
266
295
|
number: 1,
|
|
267
296
|
size: 10
|
|
@@ -273,7 +302,7 @@ const firstPage = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
|
273
302
|
}
|
|
274
303
|
});
|
|
275
304
|
|
|
276
|
-
const secondPage = await jsonSdk.
|
|
305
|
+
const secondPage = await jsonSdk.jsonApiSdkService.getList(Users, {
|
|
277
306
|
page: {
|
|
278
307
|
number: 2,
|
|
279
308
|
size: 10
|
|
@@ -292,15 +321,15 @@ Fetch a single resource by ID.
|
|
|
292
321
|
|
|
293
322
|
```typescript
|
|
294
323
|
// Simple fetch
|
|
295
|
-
const user = await jsonSdk.
|
|
324
|
+
const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1');
|
|
296
325
|
|
|
297
326
|
// With relationships
|
|
298
|
-
const userWithRelations = await jsonSdk.
|
|
327
|
+
const userWithRelations = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
|
|
299
328
|
include: ['addresses', 'comments', 'roles', 'manager']
|
|
300
329
|
});
|
|
301
330
|
|
|
302
331
|
// With sparse fieldsets
|
|
303
|
-
const userPartial = await jsonSdk.
|
|
332
|
+
const userPartial = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
|
|
304
333
|
fields: {
|
|
305
334
|
users: ['firstName', 'lastName', 'email']
|
|
306
335
|
}
|
|
@@ -321,7 +350,17 @@ newUser.lastName = 'Doe';
|
|
|
321
350
|
newUser.login = 'johndoe';
|
|
322
351
|
newUser.isActive = true;
|
|
323
352
|
|
|
324
|
-
const createdUser = await jsonSdk.
|
|
353
|
+
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);
|
|
354
|
+
|
|
355
|
+
// Create with client-generated ID
|
|
356
|
+
// Note: Server must have `allowSetId: true` option enabled
|
|
357
|
+
const userWithId = new Users();
|
|
358
|
+
userWithId.id = 'my-custom-uuid';
|
|
359
|
+
userWithId.firstName = 'Jane';
|
|
360
|
+
userWithId.lastName = 'Doe';
|
|
361
|
+
userWithId.login = 'janedoe';
|
|
362
|
+
|
|
363
|
+
const createdUserWithId = await jsonSdk.jsonApiSdkService.postOne(userWithId);
|
|
325
364
|
|
|
326
365
|
// Create with relationships
|
|
327
366
|
const newAddress = new Addresses();
|
|
@@ -329,7 +368,7 @@ newAddress.city = 'New York';
|
|
|
329
368
|
newAddress.state = 'NY';
|
|
330
369
|
newAddress.country = 'USA';
|
|
331
370
|
|
|
332
|
-
const savedAddress = await jsonSdk.
|
|
371
|
+
const savedAddress = await jsonSdk.jsonApiSdkService.postOne(newAddress);
|
|
333
372
|
|
|
334
373
|
const user = new Users();
|
|
335
374
|
user.firstName = 'Jane';
|
|
@@ -337,7 +376,7 @@ user.lastName = 'Doe';
|
|
|
337
376
|
user.login = 'janedoe';
|
|
338
377
|
user.addresses = savedAddress; // Set relationship
|
|
339
378
|
|
|
340
|
-
const createdUser = await jsonSdk.
|
|
379
|
+
const createdUser = await jsonSdk.jsonApiSdkService.postOne(user);
|
|
341
380
|
```
|
|
342
381
|
|
|
343
382
|
### Updating Resources
|
|
@@ -349,13 +388,13 @@ Update an existing resource.
|
|
|
349
388
|
```typescript
|
|
350
389
|
// Update attributes
|
|
351
390
|
user.firstName = 'Updated Name';
|
|
352
|
-
const updatedUser = await jsonSdk.
|
|
391
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
|
|
353
392
|
|
|
354
393
|
// Update relationships
|
|
355
|
-
const newAddress = await jsonSdk.
|
|
394
|
+
const newAddress = await jsonSdk.jsonApiSdkService.postOne(addressEntity);
|
|
356
395
|
user.addresses = newAddress;
|
|
357
396
|
|
|
358
|
-
const updatedUser = await jsonSdk.
|
|
397
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
|
|
359
398
|
```
|
|
360
399
|
|
|
361
400
|
### Deleting Resources
|
|
@@ -365,7 +404,7 @@ const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
|
|
|
365
404
|
Delete a resource.
|
|
366
405
|
|
|
367
406
|
```typescript
|
|
368
|
-
await jsonSdk.
|
|
407
|
+
await jsonSdk.jsonApiSdkService.deleteOne(user);
|
|
369
408
|
```
|
|
370
409
|
|
|
371
410
|
### Relationship Operations
|
|
@@ -376,13 +415,13 @@ Remove relationships without deleting the related resources.
|
|
|
376
415
|
|
|
377
416
|
```typescript
|
|
378
417
|
// Remove all roles from user
|
|
379
|
-
await jsonSdk.
|
|
418
|
+
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles');
|
|
380
419
|
|
|
381
420
|
// Remove manager from user
|
|
382
|
-
await jsonSdk.
|
|
421
|
+
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'manager');
|
|
383
422
|
|
|
384
423
|
// Remove all comments from user
|
|
385
|
-
await jsonSdk.
|
|
424
|
+
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'comments');
|
|
386
425
|
```
|
|
387
426
|
|
|
388
427
|
---
|
|
@@ -412,7 +451,7 @@ interface User {
|
|
|
412
451
|
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
|
|
413
452
|
|
|
414
453
|
// Create entity from plain object - chainable API
|
|
415
|
-
const createdUser = await jsonSdk.
|
|
454
|
+
const createdUser = await jsonSdk.jsonApiSdkService
|
|
416
455
|
.entity<User>('Users', {
|
|
417
456
|
firstName: 'John',
|
|
418
457
|
lastName: 'Doe',
|
|
@@ -421,7 +460,7 @@ const createdUser = await jsonSdk.jonApiSdkService
|
|
|
421
460
|
.postOne();
|
|
422
461
|
|
|
423
462
|
// Update entity - chainable API
|
|
424
|
-
const updatedUser = await jsonSdk.
|
|
463
|
+
const updatedUser = await jsonSdk.jsonApiSdkService
|
|
425
464
|
.entity<User>('Users', {
|
|
426
465
|
id: 1,
|
|
427
466
|
firstName: 'Jane'
|
|
@@ -429,12 +468,12 @@ const updatedUser = await jsonSdk.jonApiSdkService
|
|
|
429
468
|
.patchOne();
|
|
430
469
|
|
|
431
470
|
// Delete entity - chainable API
|
|
432
|
-
await jsonSdk.
|
|
471
|
+
await jsonSdk.jsonApiSdkService
|
|
433
472
|
.entity<User>('Users', { id: 1 })
|
|
434
473
|
.deleteOne();
|
|
435
474
|
|
|
436
475
|
// Work with relationships
|
|
437
|
-
const userRelations = await jsonSdk.
|
|
476
|
+
const userRelations = await jsonSdk.jsonApiSdkService
|
|
438
477
|
.entity<User>('Users', { id: 1 })
|
|
439
478
|
.getRelationships('manager');
|
|
440
479
|
```
|
|
@@ -443,14 +482,14 @@ const userRelations = await jsonSdk.jonApiSdkService
|
|
|
443
482
|
|
|
444
483
|
```typescript
|
|
445
484
|
// Get raw entity instance (third argument = true)
|
|
446
|
-
const userEntity = jsonSdk.
|
|
485
|
+
const userEntity = jsonSdk.jsonApiSdkService.entity<User>('Users', {
|
|
447
486
|
firstName: 'John',
|
|
448
487
|
lastName: 'Doe',
|
|
449
488
|
login: 'johndoe'
|
|
450
489
|
}, true);
|
|
451
490
|
|
|
452
491
|
// Now use it with standard SDK methods
|
|
453
|
-
const created = await jsonSdk.
|
|
492
|
+
const created = await jsonSdk.jsonApiSdkService.postOne(userEntity);
|
|
454
493
|
```
|
|
455
494
|
|
|
456
495
|
### Using String Type Names
|
|
@@ -459,15 +498,15 @@ GET methods also accept string type names instead of classes:
|
|
|
459
498
|
|
|
460
499
|
```typescript
|
|
461
500
|
// Using string type name
|
|
462
|
-
const users = await jsonSdk.
|
|
501
|
+
const users = await jsonSdk.jsonApiSdkService.getAll<User>('Users', {
|
|
463
502
|
include: ['manager']
|
|
464
503
|
});
|
|
465
504
|
|
|
466
|
-
const user = await jsonSdk.
|
|
505
|
+
const user = await jsonSdk.jsonApiSdkService.getOne<User>('Users', '1', {
|
|
467
506
|
include: ['manager']
|
|
468
507
|
});
|
|
469
508
|
|
|
470
|
-
const userList = await jsonSdk.
|
|
509
|
+
const userList = await jsonSdk.jsonApiSdkService.getList<User>('Users', {
|
|
471
510
|
page: { number: 1, size: 10 }
|
|
472
511
|
});
|
|
473
512
|
```
|
|
@@ -492,13 +531,13 @@ interface User {
|
|
|
492
531
|
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
|
|
493
532
|
|
|
494
533
|
// Clear the manager relationship
|
|
495
|
-
const user = jsonSdk.
|
|
534
|
+
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
|
|
496
535
|
id: 1,
|
|
497
536
|
firstName: 'John',
|
|
498
537
|
manager: nullRef() // This will send { data: null } for the relationship
|
|
499
538
|
}, true);
|
|
500
539
|
|
|
501
|
-
const updatedUser = await jsonSdk.
|
|
540
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
|
|
502
541
|
// Result: user.manager is now null
|
|
503
542
|
```
|
|
504
543
|
|
|
@@ -510,7 +549,7 @@ const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
|
|
|
510
549
|
**Without nullRef:**
|
|
511
550
|
```typescript
|
|
512
551
|
// This won't clear the relationship - it will be ignored
|
|
513
|
-
const user = jsonSdk.
|
|
552
|
+
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
|
|
514
553
|
id: 1,
|
|
515
554
|
firstName: 'John',
|
|
516
555
|
// manager is undefined - not included in request
|
|
@@ -520,13 +559,54 @@ const user = jsonSdk.jonApiSdkService.entity<User>('Users', {
|
|
|
520
559
|
**With nullRef:**
|
|
521
560
|
```typescript
|
|
522
561
|
// This explicitly clears the relationship
|
|
523
|
-
const user = jsonSdk.
|
|
562
|
+
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
|
|
524
563
|
id: 1,
|
|
525
564
|
firstName: 'John',
|
|
526
565
|
manager: nullRef() // Generates: relationships: { manager: { data: null } }
|
|
527
566
|
}, true);
|
|
528
567
|
```
|
|
529
568
|
|
|
569
|
+
## 🔗 Clearing To-Many Relationships
|
|
570
|
+
|
|
571
|
+
To clear all items from a to-many relationship (ManyToMany, OneToMany), use the `emptyArrayRef()` function:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
import { JsonApiJs, emptyArrayRef } from '@klerick/json-api-nestjs-sdk';
|
|
575
|
+
|
|
576
|
+
interface User {
|
|
577
|
+
id?: number;
|
|
578
|
+
firstName: string;
|
|
579
|
+
roles?: Role[];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
|
|
583
|
+
|
|
584
|
+
// Clear all roles from user
|
|
585
|
+
const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
|
|
586
|
+
id: 1,
|
|
587
|
+
firstName: 'John',
|
|
588
|
+
roles: emptyArrayRef() // This will send { data: [] } for the relationship
|
|
589
|
+
}, true);
|
|
590
|
+
|
|
591
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
|
|
592
|
+
// Result: user.roles is now an empty array
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Why emptyArrayRef is needed:**
|
|
596
|
+
- An empty array `[]` would be treated as an attribute (not a relationship)
|
|
597
|
+
- `emptyArrayRef()` marks it as a relationship that should be cleared
|
|
598
|
+
- At runtime, the SDK generates `{ data: [] }` in the JSON:API request body
|
|
599
|
+
|
|
600
|
+
**Comparison:**
|
|
601
|
+
```typescript
|
|
602
|
+
// ❌ This won't work - empty array treated as attribute
|
|
603
|
+
const user = { id: 1, roles: [] };
|
|
604
|
+
|
|
605
|
+
// ✅ This works - explicitly clears the relationship
|
|
606
|
+
const user = { id: 1, roles: emptyArrayRef() };
|
|
607
|
+
// Generates: relationships: { roles: { data: [] } }
|
|
608
|
+
```
|
|
609
|
+
|
|
530
610
|
---
|
|
531
611
|
|
|
532
612
|
## 🔍 Query Options
|
|
@@ -554,7 +634,7 @@ enum FilterOperand {
|
|
|
554
634
|
|
|
555
635
|
```typescript
|
|
556
636
|
// Equal
|
|
557
|
-
const users = await jsonSdk.
|
|
637
|
+
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
558
638
|
filter: {
|
|
559
639
|
target: {
|
|
560
640
|
isActive: { [FilterOperand.eq]: 'true' }
|
|
@@ -563,7 +643,7 @@ const users = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
563
643
|
});
|
|
564
644
|
|
|
565
645
|
// Not equal
|
|
566
|
-
const inactiveUsers = await jsonSdk.
|
|
646
|
+
const inactiveUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
567
647
|
filter: {
|
|
568
648
|
target: {
|
|
569
649
|
isActive: { [FilterOperand.ne]: 'true' }
|
|
@@ -572,7 +652,7 @@ const inactiveUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
572
652
|
});
|
|
573
653
|
|
|
574
654
|
// In array
|
|
575
|
-
const specificUsers = await jsonSdk.
|
|
655
|
+
const specificUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
576
656
|
filter: {
|
|
577
657
|
target: {
|
|
578
658
|
id: { [FilterOperand.in]: ['1', '2', '3'] }
|
|
@@ -581,7 +661,7 @@ const specificUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
581
661
|
});
|
|
582
662
|
|
|
583
663
|
// LIKE search
|
|
584
|
-
const searchUsers = await jsonSdk.
|
|
664
|
+
const searchUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
585
665
|
filter: {
|
|
586
666
|
target: {
|
|
587
667
|
login: { [FilterOperand.like]: 'john' }
|
|
@@ -590,7 +670,7 @@ const searchUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
590
670
|
});
|
|
591
671
|
|
|
592
672
|
// Check null/not null
|
|
593
|
-
const usersWithManager = await jsonSdk.
|
|
673
|
+
const usersWithManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
594
674
|
filter: {
|
|
595
675
|
target: {
|
|
596
676
|
manager: { [FilterOperand.ne]: null }
|
|
@@ -598,7 +678,7 @@ const usersWithManager = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
598
678
|
}
|
|
599
679
|
});
|
|
600
680
|
|
|
601
|
-
const usersWithoutManager = await jsonSdk.
|
|
681
|
+
const usersWithoutManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
602
682
|
filter: {
|
|
603
683
|
target: {
|
|
604
684
|
manager: { [FilterOperand.eq]: null }
|
|
@@ -611,7 +691,7 @@ const usersWithoutManager = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
611
691
|
|
|
612
692
|
```typescript
|
|
613
693
|
// Sort by single field
|
|
614
|
-
const users = await jsonSdk.
|
|
694
|
+
const users = await jsonSdk.jsonApiSdkService.getList(Users, {
|
|
615
695
|
sort: {
|
|
616
696
|
target: {
|
|
617
697
|
id: 'ASC'
|
|
@@ -620,7 +700,7 @@ const users = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
|
620
700
|
});
|
|
621
701
|
|
|
622
702
|
// Sort by multiple fields
|
|
623
|
-
const sortedUsers = await jsonSdk.
|
|
703
|
+
const sortedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
|
|
624
704
|
sort: {
|
|
625
705
|
target: {
|
|
626
706
|
createdAt: 'DESC',
|
|
@@ -633,7 +713,7 @@ const sortedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
|
633
713
|
### Pagination
|
|
634
714
|
|
|
635
715
|
```typescript
|
|
636
|
-
const paginatedUsers = await jsonSdk.
|
|
716
|
+
const paginatedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
|
|
637
717
|
page: {
|
|
638
718
|
number: 1, // Page number (1-indexed)
|
|
639
719
|
size: 20 // Items per page
|
|
@@ -645,17 +725,17 @@ const paginatedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
|
645
725
|
|
|
646
726
|
```typescript
|
|
647
727
|
// Include single relationship
|
|
648
|
-
const users = await jsonSdk.
|
|
728
|
+
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
649
729
|
include: ['addresses']
|
|
650
730
|
});
|
|
651
731
|
|
|
652
732
|
// Include multiple relationships
|
|
653
|
-
const usersWithAll = await jsonSdk.
|
|
733
|
+
const usersWithAll = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
654
734
|
include: ['addresses', 'roles', 'comments', 'manager']
|
|
655
735
|
});
|
|
656
736
|
|
|
657
737
|
// Include nested relationships
|
|
658
|
-
const usersWithNested = await jsonSdk.
|
|
738
|
+
const usersWithNested = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
659
739
|
include: ['addresses', 'manager.addresses', 'roles']
|
|
660
740
|
});
|
|
661
741
|
```
|
|
@@ -665,7 +745,7 @@ const usersWithNested = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
|
665
745
|
Request only specific fields to reduce payload size.
|
|
666
746
|
|
|
667
747
|
```typescript
|
|
668
|
-
const users = await jsonSdk.
|
|
748
|
+
const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
|
|
669
749
|
fields: {
|
|
670
750
|
users: ['firstName', 'lastName', 'email'],
|
|
671
751
|
addresses: ['city', 'country']
|
|
@@ -760,28 +840,255 @@ const result = await jsonSdk
|
|
|
760
840
|
// result[3] - array of all comment IDs after addition
|
|
761
841
|
```
|
|
762
842
|
|
|
763
|
-
### Using
|
|
843
|
+
### Using Local Identifiers (lid)
|
|
844
|
+
|
|
845
|
+
Create and reference resources within the same atomic request using local identifiers (`lid`). This allows you to establish relationships between resources being created in a single batch operation.
|
|
764
846
|
|
|
765
|
-
|
|
847
|
+
**How it works:**
|
|
848
|
+
1. Assign a temporary ID to the resource (any unique value - number or string)
|
|
849
|
+
2. Reference this ID in relationships of other resources in the same request
|
|
850
|
+
3. Server automatically replaces temporary IDs with real database IDs
|
|
766
851
|
|
|
852
|
+
**Example - Numeric lid:**
|
|
767
853
|
```typescript
|
|
768
854
|
const address = new Addresses();
|
|
769
855
|
address.city = 'Boston';
|
|
770
|
-
address.id = 10000; // Temporary ID
|
|
856
|
+
address.id = 10000; // Temporary ID (lid)
|
|
771
857
|
|
|
772
858
|
const user = new Users();
|
|
773
859
|
user.firstName = 'Alice';
|
|
774
|
-
user.addresses = address; // Reference by
|
|
860
|
+
user.addresses = address; // Reference resource by temporary ID
|
|
775
861
|
|
|
776
862
|
const [createdAddress, createdUser] = await jsonSdk
|
|
777
863
|
.atomicFactory()
|
|
778
|
-
.postOne(address)
|
|
864
|
+
.postOne(address) // First operation: create address with lid=10000
|
|
865
|
+
.postOne(user) // Second operation: reference address via lid
|
|
866
|
+
.run();
|
|
867
|
+
|
|
868
|
+
// Server assigns real IDs and maintains relationships
|
|
869
|
+
console.log(createdAddress.id); // Real ID (e.g., 42)
|
|
870
|
+
console.log(createdUser.addresses.id); // Same real ID (42)
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
**Example - UUID lid (for entities with UUID primary keys):**
|
|
874
|
+
```typescript
|
|
875
|
+
const book1 = new BookList();
|
|
876
|
+
book1.id = '550e8400-e29b-41d4-a716-446655440000'; // Temporary UUID (lid)
|
|
877
|
+
book1.title = 'TypeScript Handbook';
|
|
878
|
+
|
|
879
|
+
const book2 = new BookList();
|
|
880
|
+
book2.id = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; // Another temporary UUID (lid)
|
|
881
|
+
book2.title = 'Advanced Node.js';
|
|
882
|
+
|
|
883
|
+
const user = new Users();
|
|
884
|
+
user.firstName = 'John';
|
|
885
|
+
user.books = [book1, book2]; // Reference both books by their temporary UUIDs
|
|
886
|
+
|
|
887
|
+
const [createdBook1, createdBook2, createdUser] = await jsonSdk
|
|
888
|
+
.atomicFactory()
|
|
889
|
+
.postOne(book1)
|
|
890
|
+
.postOne(book2)
|
|
779
891
|
.postOne(user)
|
|
780
892
|
.run();
|
|
781
893
|
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
console.log(
|
|
894
|
+
// All temporary IDs are used as actual IDs (for UUID fields)
|
|
895
|
+
// or replaced with real IDs (for autoincrement fields)
|
|
896
|
+
console.log(createdBook1.id); // UUID from lid
|
|
897
|
+
console.log(createdBook2.id); // UUID from lid
|
|
898
|
+
console.log(createdUser.books.map(b => b.id)); // Both book UUIDs
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
**Important notes:**
|
|
902
|
+
- Local identifiers (lid) are only valid within a single atomic request
|
|
903
|
+
- The SDK automatically handles lid assignment in the request body
|
|
904
|
+
- For numeric IDs: lid is replaced with the actual database-generated ID
|
|
905
|
+
- For UUID IDs: lid can be used as the actual ID (if server has `allowSetId: true`)
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## 📤 Meta Support
|
|
910
|
+
|
|
911
|
+
The SDK supports passing `meta` objects with requests to send additional business logic data that doesn't belong to the entity itself, according to [JSON:API specification](https://jsonapi.org/format/#document-meta).
|
|
912
|
+
|
|
913
|
+
### Regular Operations with Meta
|
|
914
|
+
|
|
915
|
+
```typescript
|
|
916
|
+
// POST with meta
|
|
917
|
+
const newUser = new Users();
|
|
918
|
+
newUser.firstName = 'John';
|
|
919
|
+
newUser.lastName = 'Doe';
|
|
920
|
+
newUser.login = 'johndoe';
|
|
921
|
+
|
|
922
|
+
const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser, {
|
|
923
|
+
source: 'import',
|
|
924
|
+
batchId: '12345'
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Request body:
|
|
928
|
+
{
|
|
929
|
+
"data": {
|
|
930
|
+
"type": "users",
|
|
931
|
+
"attributes": { "firstName": "John", "lastName": "Doe", "login": "johndoe" }
|
|
932
|
+
},
|
|
933
|
+
"meta": {
|
|
934
|
+
"source": "import",
|
|
935
|
+
"batchId": "12345"
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// PATCH with meta
|
|
940
|
+
user.firstName = 'Jane';
|
|
941
|
+
const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user, {
|
|
942
|
+
updatedBy: 'admin',
|
|
943
|
+
reason: 'name correction'
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// Relationship operations with meta
|
|
947
|
+
await jsonSdk.jsonApiSdkService.postRelationships(user, 'roles', {
|
|
948
|
+
addedBy: 'admin',
|
|
949
|
+
timestamp: Date.now()
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
await jsonSdk.jsonApiSdkService.patchRelationships(user, 'roles', {
|
|
953
|
+
source: 'sync',
|
|
954
|
+
syncId: 'abc123'
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
|
|
958
|
+
removedBy: 'admin',
|
|
959
|
+
reason: 'access revoked'
|
|
960
|
+
});
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**Important:** `deleteOne()` does NOT support meta because HTTP DELETE for resources has no body according to JSON:API spec. However, `deleteRelationships()` DOES support meta because relationship deletion endpoints use a request body.
|
|
964
|
+
|
|
965
|
+
### Entity Chain with Meta
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
// Chain mode - Observable
|
|
969
|
+
const user = await jsonSdk.jsonApiSdkService
|
|
970
|
+
.entity('Users', {
|
|
971
|
+
firstName: 'John',
|
|
972
|
+
lastName: 'Doe',
|
|
973
|
+
login: 'johndoe'
|
|
974
|
+
})
|
|
975
|
+
.postOne({ source: 'mobile-app', version: '2.0' });
|
|
976
|
+
|
|
977
|
+
// Update with meta
|
|
978
|
+
await jsonSdk.jsonApiSdkService
|
|
979
|
+
.entity('Users', { id: 1, firstName: 'Jane' })
|
|
980
|
+
.patchOne({ updatedBy: 'user-123' });
|
|
981
|
+
|
|
982
|
+
// Relationship operations with meta
|
|
983
|
+
await jsonSdk.jsonApiSdkService
|
|
984
|
+
.entity('Users', user)
|
|
985
|
+
.patchRelationships('roles', { operation: 'bulk-update' });
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### Atomic Operations with Meta
|
|
989
|
+
|
|
990
|
+
Each operation in an atomic request can have its own meta:
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
const address1 = new Addresses();
|
|
994
|
+
address1.id = 999; // Local identifier (lid)
|
|
995
|
+
address1.city = 'New York';
|
|
996
|
+
|
|
997
|
+
const address2 = new Addresses();
|
|
998
|
+
address2.id = 1000; // Local identifier (lid)
|
|
999
|
+
address2.city = 'Boston';
|
|
1000
|
+
|
|
1001
|
+
const user1 = new Users();
|
|
1002
|
+
user1.firstName = 'Alice';
|
|
1003
|
+
user1.addresses = address1;
|
|
1004
|
+
|
|
1005
|
+
const user2 = new Users();
|
|
1006
|
+
user2.firstName = 'Bob';
|
|
1007
|
+
user2.addresses = address2;
|
|
1008
|
+
|
|
1009
|
+
const [addr1, addr2, createdUser1, createdUser2] = await jsonSdk
|
|
1010
|
+
.atomicFactory()
|
|
1011
|
+
.postOne(address1)
|
|
1012
|
+
.postOne(address2)
|
|
1013
|
+
.postOne(user1, { source: 'import', priority: 'high' }) // User 1 with meta
|
|
1014
|
+
.postOne(user2, { source: 'import', priority: 'normal' }) // User 2 with different meta
|
|
1015
|
+
.run();
|
|
1016
|
+
|
|
1017
|
+
// Request body:
|
|
1018
|
+
{
|
|
1019
|
+
"atomic:operations": [
|
|
1020
|
+
{
|
|
1021
|
+
"op": "add",
|
|
1022
|
+
"ref": { "type": "addresses", "lid": "999" },
|
|
1023
|
+
"data": { "type": "addresses", "attributes": { "city": "New York" } }
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"op": "add",
|
|
1027
|
+
"ref": { "type": "addresses", "lid": "1000" },
|
|
1028
|
+
"data": { "type": "addresses", "attributes": { "city": "Boston" } }
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"op": "add",
|
|
1032
|
+
"ref": { "type": "users" },
|
|
1033
|
+
"data": {
|
|
1034
|
+
"type": "users",
|
|
1035
|
+
"attributes": { "firstName": "Alice" },
|
|
1036
|
+
"relationships": { "addresses": { "data": { "type": "addresses", "id": "999" } } }
|
|
1037
|
+
},
|
|
1038
|
+
"meta": { "source": "import", "priority": "high" } // Meta for operation 3
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"op": "add",
|
|
1042
|
+
"ref": { "type": "users" },
|
|
1043
|
+
"data": {
|
|
1044
|
+
"type": "users",
|
|
1045
|
+
"attributes": { "firstName": "Bob" },
|
|
1046
|
+
"relationships": { "addresses": { "data": { "type": "addresses", "id": "1000" } } }
|
|
1047
|
+
},
|
|
1048
|
+
"meta": { "source": "import", "priority": "normal" } // Meta for operation 4
|
|
1049
|
+
}
|
|
1050
|
+
]
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Meta with Entity Chain in Atomic Operations
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
const addressEntity = jsonSdk.jsonApiSdkService.entity('Addresses', {
|
|
1058
|
+
id: 999,
|
|
1059
|
+
city: 'New York'
|
|
1060
|
+
}, true);
|
|
1061
|
+
|
|
1062
|
+
const userEntity = jsonSdk.jsonApiSdkService.entity('Users', {
|
|
1063
|
+
firstName: 'Alice',
|
|
1064
|
+
addresses: jsonSdk.jsonApiSdkService.entity('Addresses', { id: 999 }, true)
|
|
1065
|
+
}, true);
|
|
1066
|
+
|
|
1067
|
+
const [createdAddress, createdUser] = await jsonSdk
|
|
1068
|
+
.atomicFactory()
|
|
1069
|
+
.postOne(addressEntity)
|
|
1070
|
+
.postOne(userEntity, { source: 'import', batchId: '12345' })
|
|
1071
|
+
.run();
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
### Operations WITHOUT Meta Support
|
|
1075
|
+
|
|
1076
|
+
The following operations do NOT accept meta parameter:
|
|
1077
|
+
|
|
1078
|
+
```typescript
|
|
1079
|
+
// ❌ deleteOne - HTTP DELETE has no body
|
|
1080
|
+
await jsonSdk.jsonApiSdkService.deleteOne(user);
|
|
1081
|
+
// Cannot pass meta here
|
|
1082
|
+
|
|
1083
|
+
// ❌ deleteOne in atomic operations
|
|
1084
|
+
await jsonSdk.atomicFactory()
|
|
1085
|
+
.deleteOne(user) // No meta parameter
|
|
1086
|
+
.run();
|
|
1087
|
+
|
|
1088
|
+
// ✅ But deleteRelationships DOES support meta (has body)
|
|
1089
|
+
await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles', {
|
|
1090
|
+
removedBy: 'admin'
|
|
1091
|
+
});
|
|
785
1092
|
```
|
|
786
1093
|
|
|
787
1094
|
---
|