@klerick/json-api-nestjs-sdk 10.0.0-beta.8 → 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +498 -45
  3. package/cjs/src/index.js +9 -7
  4. package/cjs/src/index.js.map +1 -1
  5. package/cjs/src/lib/json-api-angular.js +22 -18
  6. package/cjs/src/lib/json-api-angular.js.map +1 -1
  7. package/cjs/src/lib/json-api-js.js +31 -11
  8. package/cjs/src/lib/json-api-js.js.map +1 -1
  9. package/cjs/src/lib/service/atomic-operations.service.js +22 -16
  10. package/cjs/src/lib/service/atomic-operations.service.js.map +1 -1
  11. package/cjs/src/lib/service/fetch-inner-client.js +3 -3
  12. package/cjs/src/lib/service/fetch-inner-client.js.map +1 -1
  13. package/cjs/src/lib/service/index.js +3 -3
  14. package/cjs/src/lib/service/index.js.map +1 -1
  15. package/cjs/src/lib/service/json-api-sdk.service.js +55 -22
  16. package/cjs/src/lib/service/json-api-sdk.service.js.map +1 -1
  17. package/cjs/src/lib/service/json-api-utils.service.js +138 -59
  18. package/cjs/src/lib/service/json-api-utils.service.js.map +1 -1
  19. package/cjs/src/lib/types/entity-chain.js +3 -0
  20. package/cjs/src/lib/types/entity-chain.js.map +1 -0
  21. package/cjs/src/lib/types/index.js +10 -9
  22. package/cjs/src/lib/types/index.js.map +1 -1
  23. package/cjs/src/lib/utils/generate-atomic-body.js +21 -20
  24. package/cjs/src/lib/utils/generate-atomic-body.js.map +1 -1
  25. package/cjs/src/lib/utils/index.js +5 -5
  26. package/cjs/src/lib/utils/index.js.map +1 -1
  27. package/cjs/src/lib/utils/utils.js +42 -3
  28. package/cjs/src/lib/utils/utils.js.map +1 -1
  29. package/cjs/src/ngModule.js +8 -8
  30. package/cjs/src/ngModule.js.map +1 -1
  31. package/mjs/package.json +1 -0
  32. package/mjs/src/index.d.ts +4 -4
  33. package/mjs/src/index.js +3 -3
  34. package/mjs/src/index.js.map +1 -1
  35. package/mjs/src/lib/json-api-angular.d.ts +10 -12
  36. package/mjs/src/lib/json-api-angular.js +11 -7
  37. package/mjs/src/lib/json-api-angular.js.map +1 -1
  38. package/mjs/src/lib/json-api-js.d.ts +4 -4
  39. package/mjs/src/lib/json-api-js.js +26 -6
  40. package/mjs/src/lib/json-api-js.js.map +1 -1
  41. package/mjs/src/lib/service/atomic-operations.service.d.ts +7 -7
  42. package/mjs/src/lib/service/atomic-operations.service.js +21 -15
  43. package/mjs/src/lib/service/atomic-operations.service.js.map +1 -1
  44. package/mjs/src/lib/service/fetch-inner-client.d.ts +2 -2
  45. package/mjs/src/lib/service/fetch-inner-client.js +1 -1
  46. package/mjs/src/lib/service/fetch-inner-client.js.map +1 -1
  47. package/mjs/src/lib/service/index.d.ts +3 -3
  48. package/mjs/src/lib/service/index.js +3 -3
  49. package/mjs/src/lib/service/index.js.map +1 -1
  50. package/mjs/src/lib/service/json-api-sdk.service.d.ts +13 -8
  51. package/mjs/src/lib/service/json-api-sdk.service.js +51 -18
  52. package/mjs/src/lib/service/json-api-sdk.service.js.map +1 -1
  53. package/mjs/src/lib/service/json-api-utils.service.d.ts +6 -5
  54. package/mjs/src/lib/service/json-api-utils.service.js +132 -53
  55. package/mjs/src/lib/service/json-api-utils.service.js.map +1 -1
  56. package/mjs/src/lib/token/index.d.ts +1 -1
  57. package/mjs/src/lib/types/atomic-operation.d.ts +11 -11
  58. package/mjs/src/lib/types/atomic-type.d.ts +2 -2
  59. package/mjs/src/lib/types/config.d.ts +2 -2
  60. package/mjs/src/lib/types/entity-chain.d.ts +32 -0
  61. package/mjs/src/lib/types/entity-chain.js +1 -0
  62. package/mjs/src/lib/types/entity-chain.js.map +1 -0
  63. package/mjs/src/lib/types/http-inner-client.d.ts +3 -3
  64. package/mjs/src/lib/types/http-request-params.d.ts +3 -0
  65. package/mjs/src/lib/types/index.d.ts +10 -9
  66. package/mjs/src/lib/types/index.js +10 -9
  67. package/mjs/src/lib/types/index.js.map +1 -1
  68. package/mjs/src/lib/types/promise-json-api-sdk.d.ts +20 -9
  69. package/mjs/src/lib/types/query-params.d.ts +1 -1
  70. package/mjs/src/lib/utils/adapter-for-axios.d.ts +1 -1
  71. package/mjs/src/lib/utils/generate-atomic-body.d.ts +7 -7
  72. package/mjs/src/lib/utils/generate-atomic-body.js +19 -18
  73. package/mjs/src/lib/utils/generate-atomic-body.js.map +1 -1
  74. package/mjs/src/lib/utils/index.d.ts +5 -5
  75. package/mjs/src/lib/utils/index.js +5 -5
  76. package/mjs/src/lib/utils/index.js.map +1 -1
  77. package/mjs/src/lib/utils/utils.d.ts +20 -1
  78. package/mjs/src/lib/utils/utils.js +36 -1
  79. package/mjs/src/lib/utils/utils.js.map +1 -1
  80. package/mjs/src/ngModule.d.ts +3 -3
  81. package/mjs/src/ngModule.js +2 -2
  82. package/mjs/src/ngModule.js.map +1 -1
  83. 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)
@@ -34,6 +35,11 @@ Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.
34
35
  - [Updating Resources](#updating-resources)
35
36
  - [Deleting Resources](#deleting-resources)
36
37
  - [Relationship Operations](#relationship-operations)
38
+ - [Working with Plain Objects](#-working-with-plain-objects)
39
+ - [Using entity() Method](#using-entity-method)
40
+ - [Using String Type Names](#using-string-type-names)
41
+ - [Nullifying Relationships](#-nullifying-relationships)
42
+ - [Clearing To-Many Relationships](#-clearing-to-many-relationships)
37
43
  - [Query Options](#-query-options)
38
44
  - [Filtering](#filtering)
39
45
  - [Sorting](#sorting)
@@ -41,6 +47,10 @@ Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.
41
47
  - [Including Relationships](#including-relationships)
42
48
  - [Sparse Fieldsets](#sparse-fieldsets)
43
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)
44
54
  - [Examples](#-examples)
45
55
 
46
56
 
@@ -78,10 +88,10 @@ const jsonSdk = JsonApiJs(
78
88
 
79
89
  // 3. Use SDK
80
90
  // Fetch all users
81
- const users = await jsonSdk.jonApiSdkService.getAll(Users);
91
+ const users = await jsonSdk.jsonApiSdkService.getAll(Users);
82
92
 
83
93
  // Fetch with filtering and relationships
84
- const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
94
+ const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
85
95
  filter: {
86
96
  target: {
87
97
  isActive: { [FilterOperand.eq]: 'true' }
@@ -91,7 +101,7 @@ const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
91
101
  });
92
102
 
93
103
  // Get one user
94
- const user = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
104
+ const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
95
105
  include: ['addresses', 'comments', 'roles', 'manager']
96
106
  });
97
107
 
@@ -102,14 +112,14 @@ newUser.lastName = 'Doe';
102
112
  newUser.login = 'johndoe';
103
113
  newUser.isActive = true;
104
114
 
105
- const createdUser = await jsonSdk.jonApiSdkService.postOne(newUser);
115
+ const createdUser = await jsonSdk.jsonApiSdkService.postOne(newUser);
106
116
 
107
117
  // Update a user
108
118
  createdUser.firstName = 'Jane';
109
- const updatedUser = await jsonSdk.jonApiSdkService.patchOne(createdUser);
119
+ const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(createdUser);
110
120
 
111
121
  // Delete a user
112
- await jsonSdk.jonApiSdkService.deleteOne(createdUser);
122
+ await jsonSdk.jsonApiSdkService.deleteOne(createdUser);
113
123
  ```
114
124
 
115
125
  ### Angular Setup
@@ -128,6 +138,8 @@ import { Component, inject } from '@angular/core';
128
138
  import { bootstrapApplication } from '@angular/platform-browser';
129
139
 
130
140
  // 1. Configure in your main.ts or app.config.ts
141
+
142
+ // Option A: Direct configuration object
131
143
  const angularConfig = {
132
144
  apiHost: 'http://localhost:3000',
133
145
  idKey: 'id',
@@ -143,6 +155,23 @@ bootstrapApplication(AppComponent, {
143
155
  ],
144
156
  }).catch((err) => console.error(err));
145
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
+
146
175
  // 2. Use in your components
147
176
  @Component({
148
177
  standalone: true,
@@ -186,6 +215,10 @@ type JsonSdkConfig = {
186
215
  type JsonConfig = JsonSdkConfig & {
187
216
  adapter?: HttpInnerClient; // HTTP client adapter (default: fetch)
188
217
  }
218
+
219
+ // Angular: provideJsonApi accepts config or factory function
220
+ type JsonSdkConfigFactory = () => JsonSdkConfig;
221
+ type JsonSdkConfigOrFactory = JsonSdkConfig | JsonSdkConfigFactory;
189
222
  ```
190
223
 
191
224
  ### HTTP Adapters
@@ -225,10 +258,10 @@ Fetch all resources with optional filtering, sorting, and relationships.
225
258
  import { FilterOperand } from '@klerick/json-api-nestjs-sdk';
226
259
 
227
260
  // Fetch all users
228
- const users = await jsonSdk.jonApiSdkService.getAll(Users);
261
+ const users = await jsonSdk.jsonApiSdkService.getAll(Users);
229
262
 
230
263
  // With filtering
231
- const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
264
+ const activeUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
232
265
  filter: {
233
266
  target: {
234
267
  isActive: { [FilterOperand.eq]: 'true' },
@@ -239,7 +272,7 @@ const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
239
272
  });
240
273
 
241
274
  // Filter by relationship
242
- const usersWithRoles = await jsonSdk.jonApiSdkService.getAll(Users, {
275
+ const usersWithRoles = await jsonSdk.jsonApiSdkService.getAll(Users, {
243
276
  filter: {
244
277
  target: {
245
278
  id: { [FilterOperand.in]: ['1', '2'] }
@@ -257,7 +290,7 @@ const usersWithRoles = await jsonSdk.jonApiSdkService.getAll(Users, {
257
290
  Fetch resources with pagination (returns paginated results).
258
291
 
259
292
  ```typescript
260
- const firstPage = await jsonSdk.jonApiSdkService.getList(Users, {
293
+ const firstPage = await jsonSdk.jsonApiSdkService.getList(Users, {
261
294
  page: {
262
295
  number: 1,
263
296
  size: 10
@@ -269,7 +302,7 @@ const firstPage = await jsonSdk.jonApiSdkService.getList(Users, {
269
302
  }
270
303
  });
271
304
 
272
- const secondPage = await jsonSdk.jonApiSdkService.getList(Users, {
305
+ const secondPage = await jsonSdk.jsonApiSdkService.getList(Users, {
273
306
  page: {
274
307
  number: 2,
275
308
  size: 10
@@ -288,15 +321,15 @@ Fetch a single resource by ID.
288
321
 
289
322
  ```typescript
290
323
  // Simple fetch
291
- const user = await jsonSdk.jonApiSdkService.getOne(Users, '1');
324
+ const user = await jsonSdk.jsonApiSdkService.getOne(Users, '1');
292
325
 
293
326
  // With relationships
294
- const userWithRelations = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
327
+ const userWithRelations = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
295
328
  include: ['addresses', 'comments', 'roles', 'manager']
296
329
  });
297
330
 
298
331
  // With sparse fieldsets
299
- const userPartial = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
332
+ const userPartial = await jsonSdk.jsonApiSdkService.getOne(Users, '1', {
300
333
  fields: {
301
334
  users: ['firstName', 'lastName', 'email']
302
335
  }
@@ -317,7 +350,17 @@ newUser.lastName = 'Doe';
317
350
  newUser.login = 'johndoe';
318
351
  newUser.isActive = true;
319
352
 
320
- const createdUser = await jsonSdk.jonApiSdkService.postOne(newUser);
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);
321
364
 
322
365
  // Create with relationships
323
366
  const newAddress = new Addresses();
@@ -325,7 +368,7 @@ newAddress.city = 'New York';
325
368
  newAddress.state = 'NY';
326
369
  newAddress.country = 'USA';
327
370
 
328
- const savedAddress = await jsonSdk.jonApiSdkService.postOne(newAddress);
371
+ const savedAddress = await jsonSdk.jsonApiSdkService.postOne(newAddress);
329
372
 
330
373
  const user = new Users();
331
374
  user.firstName = 'Jane';
@@ -333,7 +376,7 @@ user.lastName = 'Doe';
333
376
  user.login = 'janedoe';
334
377
  user.addresses = savedAddress; // Set relationship
335
378
 
336
- const createdUser = await jsonSdk.jonApiSdkService.postOne(user);
379
+ const createdUser = await jsonSdk.jsonApiSdkService.postOne(user);
337
380
  ```
338
381
 
339
382
  ### Updating Resources
@@ -345,13 +388,13 @@ Update an existing resource.
345
388
  ```typescript
346
389
  // Update attributes
347
390
  user.firstName = 'Updated Name';
348
- const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
391
+ const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
349
392
 
350
393
  // Update relationships
351
- const newAddress = await jsonSdk.jonApiSdkService.postOne(addressEntity);
394
+ const newAddress = await jsonSdk.jsonApiSdkService.postOne(addressEntity);
352
395
  user.addresses = newAddress;
353
396
 
354
- const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
397
+ const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
355
398
  ```
356
399
 
357
400
  ### Deleting Resources
@@ -361,7 +404,7 @@ const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
361
404
  Delete a resource.
362
405
 
363
406
  ```typescript
364
- await jsonSdk.jonApiSdkService.deleteOne(user);
407
+ await jsonSdk.jsonApiSdkService.deleteOne(user);
365
408
  ```
366
409
 
367
410
  ### Relationship Operations
@@ -372,13 +415,196 @@ Remove relationships without deleting the related resources.
372
415
 
373
416
  ```typescript
374
417
  // Remove all roles from user
375
- await jsonSdk.jonApiSdkService.deleteRelationships(user, 'roles');
418
+ await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'roles');
376
419
 
377
420
  // Remove manager from user
378
- await jsonSdk.jonApiSdkService.deleteRelationships(user, 'manager');
421
+ await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'manager');
379
422
 
380
423
  // Remove all comments from user
381
- await jsonSdk.jonApiSdkService.deleteRelationships(user, 'comments');
424
+ await jsonSdk.jsonApiSdkService.deleteRelationships(user, 'comments');
425
+ ```
426
+
427
+ ---
428
+
429
+ ## 🏗️ Working with Plain Objects
430
+
431
+ In monorepo environments or when sharing types between frontend and backend, you may want to use plain TypeScript types/interfaces instead of classes. The SDK provides tools to work with plain objects while maintaining full type safety.
432
+
433
+ ### Using entity() Method
434
+
435
+ The `entity()` method creates a properly typed entity instance from a plain object. This is essential when:
436
+ - You share types (not classes) between frontend and backend
437
+ - The SDK needs to identify the resource type at runtime (via `constructor.name`)
438
+
439
+ ```typescript
440
+ import { JsonApiJs } from '@klerick/json-api-nestjs-sdk';
441
+
442
+ // Shared type (not a class)
443
+ interface User {
444
+ id?: number;
445
+ firstName: string;
446
+ lastName: string;
447
+ login: string;
448
+ manager?: User | null;
449
+ }
450
+
451
+ const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
452
+
453
+ // Create entity from plain object - chainable API
454
+ const createdUser = await jsonSdk.jsonApiSdkService
455
+ .entity<User>('Users', {
456
+ firstName: 'John',
457
+ lastName: 'Doe',
458
+ login: 'johndoe'
459
+ })
460
+ .postOne();
461
+
462
+ // Update entity - chainable API
463
+ const updatedUser = await jsonSdk.jsonApiSdkService
464
+ .entity<User>('Users', {
465
+ id: 1,
466
+ firstName: 'Jane'
467
+ })
468
+ .patchOne();
469
+
470
+ // Delete entity - chainable API
471
+ await jsonSdk.jsonApiSdkService
472
+ .entity<User>('Users', { id: 1 })
473
+ .deleteOne();
474
+
475
+ // Work with relationships
476
+ const userRelations = await jsonSdk.jsonApiSdkService
477
+ .entity<User>('Users', { id: 1 })
478
+ .getRelationships('manager');
479
+ ```
480
+
481
+ **Raw mode** - get the entity instance without chaining:
482
+
483
+ ```typescript
484
+ // Get raw entity instance (third argument = true)
485
+ const userEntity = jsonSdk.jsonApiSdkService.entity<User>('Users', {
486
+ firstName: 'John',
487
+ lastName: 'Doe',
488
+ login: 'johndoe'
489
+ }, true);
490
+
491
+ // Now use it with standard SDK methods
492
+ const created = await jsonSdk.jsonApiSdkService.postOne(userEntity);
493
+ ```
494
+
495
+ ### Using String Type Names
496
+
497
+ GET methods also accept string type names instead of classes:
498
+
499
+ ```typescript
500
+ // Using string type name
501
+ const users = await jsonSdk.jsonApiSdkService.getAll<User>('Users', {
502
+ include: ['manager']
503
+ });
504
+
505
+ const user = await jsonSdk.jsonApiSdkService.getOne<User>('Users', '1', {
506
+ include: ['manager']
507
+ });
508
+
509
+ const userList = await jsonSdk.jsonApiSdkService.getList<User>('Users', {
510
+ page: { number: 1, size: 10 }
511
+ });
512
+ ```
513
+
514
+ ---
515
+
516
+ ## 🔗 Nullifying Relationships
517
+
518
+ To clear a relationship (set it to `null`), use the `nullRef()` function. This is necessary because the SDK distinguishes between:
519
+ - **Missing relationship** - not included in the request (no change)
520
+ - **Null relationship** - explicitly set to `null` (clear the relationship)
521
+
522
+ ```typescript
523
+ import { JsonApiJs, nullRef } from '@klerick/json-api-nestjs-sdk';
524
+
525
+ interface User {
526
+ id?: number;
527
+ firstName: string;
528
+ manager?: User | null;
529
+ }
530
+
531
+ const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
532
+
533
+ // Clear the manager relationship
534
+ const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
535
+ id: 1,
536
+ firstName: 'John',
537
+ manager: nullRef() // This will send { data: null } for the relationship
538
+ }, true);
539
+
540
+ const updatedUser = await jsonSdk.jsonApiSdkService.patchOne(user);
541
+ // Result: user.manager is now null
542
+ ```
543
+
544
+ **How it works:**
545
+ - `nullRef()` returns a special marker object that TypeScript sees as `null`
546
+ - At runtime, the SDK detects this marker and generates `{ data: null }` in the JSON:API request body
547
+ - The server then clears the relationship
548
+
549
+ **Without nullRef:**
550
+ ```typescript
551
+ // This won't clear the relationship - it will be ignored
552
+ const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
553
+ id: 1,
554
+ firstName: 'John',
555
+ // manager is undefined - not included in request
556
+ }, true);
557
+ ```
558
+
559
+ **With nullRef:**
560
+ ```typescript
561
+ // This explicitly clears the relationship
562
+ const user = jsonSdk.jsonApiSdkService.entity<User>('Users', {
563
+ id: 1,
564
+ firstName: 'John',
565
+ manager: nullRef() // Generates: relationships: { manager: { data: null } }
566
+ }, true);
567
+ ```
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: [] } }
382
608
  ```
383
609
 
384
610
  ---
@@ -408,7 +634,7 @@ enum FilterOperand {
408
634
 
409
635
  ```typescript
410
636
  // Equal
411
- const users = await jsonSdk.jonApiSdkService.getAll(Users, {
637
+ const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
412
638
  filter: {
413
639
  target: {
414
640
  isActive: { [FilterOperand.eq]: 'true' }
@@ -417,7 +643,7 @@ const users = await jsonSdk.jonApiSdkService.getAll(Users, {
417
643
  });
418
644
 
419
645
  // Not equal
420
- const inactiveUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
646
+ const inactiveUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
421
647
  filter: {
422
648
  target: {
423
649
  isActive: { [FilterOperand.ne]: 'true' }
@@ -426,7 +652,7 @@ const inactiveUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
426
652
  });
427
653
 
428
654
  // In array
429
- const specificUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
655
+ const specificUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
430
656
  filter: {
431
657
  target: {
432
658
  id: { [FilterOperand.in]: ['1', '2', '3'] }
@@ -435,7 +661,7 @@ const specificUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
435
661
  });
436
662
 
437
663
  // LIKE search
438
- const searchUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
664
+ const searchUsers = await jsonSdk.jsonApiSdkService.getAll(Users, {
439
665
  filter: {
440
666
  target: {
441
667
  login: { [FilterOperand.like]: 'john' }
@@ -444,7 +670,7 @@ const searchUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
444
670
  });
445
671
 
446
672
  // Check null/not null
447
- const usersWithManager = await jsonSdk.jonApiSdkService.getAll(Users, {
673
+ const usersWithManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
448
674
  filter: {
449
675
  target: {
450
676
  manager: { [FilterOperand.ne]: null }
@@ -452,7 +678,7 @@ const usersWithManager = await jsonSdk.jonApiSdkService.getAll(Users, {
452
678
  }
453
679
  });
454
680
 
455
- const usersWithoutManager = await jsonSdk.jonApiSdkService.getAll(Users, {
681
+ const usersWithoutManager = await jsonSdk.jsonApiSdkService.getAll(Users, {
456
682
  filter: {
457
683
  target: {
458
684
  manager: { [FilterOperand.eq]: null }
@@ -465,7 +691,7 @@ const usersWithoutManager = await jsonSdk.jonApiSdkService.getAll(Users, {
465
691
 
466
692
  ```typescript
467
693
  // Sort by single field
468
- const users = await jsonSdk.jonApiSdkService.getList(Users, {
694
+ const users = await jsonSdk.jsonApiSdkService.getList(Users, {
469
695
  sort: {
470
696
  target: {
471
697
  id: 'ASC'
@@ -474,7 +700,7 @@ const users = await jsonSdk.jonApiSdkService.getList(Users, {
474
700
  });
475
701
 
476
702
  // Sort by multiple fields
477
- const sortedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
703
+ const sortedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
478
704
  sort: {
479
705
  target: {
480
706
  createdAt: 'DESC',
@@ -487,7 +713,7 @@ const sortedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
487
713
  ### Pagination
488
714
 
489
715
  ```typescript
490
- const paginatedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
716
+ const paginatedUsers = await jsonSdk.jsonApiSdkService.getList(Users, {
491
717
  page: {
492
718
  number: 1, // Page number (1-indexed)
493
719
  size: 20 // Items per page
@@ -499,17 +725,17 @@ const paginatedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
499
725
 
500
726
  ```typescript
501
727
  // Include single relationship
502
- const users = await jsonSdk.jonApiSdkService.getAll(Users, {
728
+ const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
503
729
  include: ['addresses']
504
730
  });
505
731
 
506
732
  // Include multiple relationships
507
- const usersWithAll = await jsonSdk.jonApiSdkService.getAll(Users, {
733
+ const usersWithAll = await jsonSdk.jsonApiSdkService.getAll(Users, {
508
734
  include: ['addresses', 'roles', 'comments', 'manager']
509
735
  });
510
736
 
511
737
  // Include nested relationships
512
- const usersWithNested = await jsonSdk.jonApiSdkService.getAll(Users, {
738
+ const usersWithNested = await jsonSdk.jsonApiSdkService.getAll(Users, {
513
739
  include: ['addresses', 'manager.addresses', 'roles']
514
740
  });
515
741
  ```
@@ -519,7 +745,7 @@ const usersWithNested = await jsonSdk.jonApiSdkService.getAll(Users, {
519
745
  Request only specific fields to reduce payload size.
520
746
 
521
747
  ```typescript
522
- const users = await jsonSdk.jonApiSdkService.getAll(Users, {
748
+ const users = await jsonSdk.jsonApiSdkService.getAll(Users, {
523
749
  fields: {
524
750
  users: ['firstName', 'lastName', 'email'],
525
751
  addresses: ['city', 'country']
@@ -614,28 +840,255 @@ const result = await jsonSdk
614
840
  // result[3] - array of all comment IDs after addition
615
841
  ```
616
842
 
617
- ### Using Temporary IDs (lid)
843
+ ### Using Local Identifiers (lid)
618
844
 
619
- Reference resources created within the same atomic request using temporary IDs.
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.
620
846
 
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
851
+
852
+ **Example - Numeric lid:**
621
853
  ```typescript
622
854
  const address = new Addresses();
623
855
  address.city = 'Boston';
624
- address.id = 10000; // Temporary ID
856
+ address.id = 10000; // Temporary ID (lid)
625
857
 
626
858
  const user = new Users();
627
859
  user.firstName = 'Alice';
628
- user.addresses = address; // Reference by temp ID
860
+ user.addresses = address; // Reference resource by temporary ID
629
861
 
630
862
  const [createdAddress, createdUser] = await jsonSdk
631
863
  .atomicFactory()
632
- .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)
633
891
  .postOne(user)
634
892
  .run();
635
893
 
636
- // Server assigns real IDs
637
- console.log(createdAddress.id); // Real ID (e.g., 1)
638
- console.log(createdUser.addresses.id); // Same real ID
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
+ });
639
1092
  ```
640
1093
 
641
1094
  ---