@klerick/json-api-nestjs-sdk 10.0.0-beta.1 → 10.0.0-beta.10
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 +148 -0
- package/README.md +811 -0
- package/cjs/src/index.js +2 -1
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/lib/json-api-js.js +21 -2
- package/cjs/src/lib/json-api-js.js.map +1 -1
- package/cjs/src/lib/service/atomic-operations.service.js.map +1 -1
- package/cjs/src/lib/service/fetch-inner-client.js +8 -2
- package/cjs/src/lib/service/fetch-inner-client.js.map +1 -1
- package/cjs/src/lib/service/json-api-sdk.service.js +42 -14
- package/cjs/src/lib/service/json-api-sdk.service.js.map +1 -1
- package/cjs/src/lib/service/json-api-utils.service.js +35 -21
- package/cjs/src/lib/service/json-api-utils.service.js.map +1 -1
- package/cjs/src/lib/types/entity-chain.js +3 -0
- package/cjs/src/lib/types/entity-chain.js.map +1 -0
- package/cjs/src/lib/types/index.js +1 -0
- package/cjs/src/lib/types/index.js.map +1 -1
- package/cjs/src/lib/utils/adapter-for-axios.js.map +1 -1
- package/cjs/src/lib/utils/generate-atomic-body.js +14 -2
- package/cjs/src/lib/utils/generate-atomic-body.js.map +1 -1
- package/cjs/src/lib/utils/http-params.js +1 -1
- package/cjs/src/lib/utils/http-params.js.map +1 -1
- package/cjs/src/lib/utils/utils.js +26 -2
- package/cjs/src/lib/utils/utils.js.map +1 -1
- package/mjs/src/index.d.ts +2 -2
- package/mjs/src/index.js +1 -1
- package/mjs/src/index.js.map +1 -1
- package/mjs/src/lib/json-api-js.js +20 -0
- package/mjs/src/lib/json-api-js.js.map +1 -1
- package/mjs/src/lib/service/atomic-operations.service.d.ts +2 -2
- package/mjs/src/lib/service/atomic-operations.service.js.map +1 -1
- package/mjs/src/lib/service/json-api-sdk.service.d.ts +8 -3
- package/mjs/src/lib/service/json-api-sdk.service.js +28 -7
- package/mjs/src/lib/service/json-api-sdk.service.js.map +1 -1
- package/mjs/src/lib/service/json-api-utils.service.js +5 -2
- package/mjs/src/lib/service/json-api-utils.service.js.map +1 -1
- package/mjs/src/lib/types/atomic-operation.d.ts +4 -4
- package/mjs/src/lib/types/atomic-operation.js +0 -1
- package/mjs/src/lib/types/config.js +0 -1
- package/mjs/src/lib/types/entity-chain.d.ts +32 -0
- package/mjs/src/lib/types/entity-chain.js +1 -0
- package/mjs/src/lib/types/entity-chain.js.map +1 -0
- package/mjs/src/lib/types/http-inner-client.js +0 -1
- package/mjs/src/lib/types/http-request-params.js +0 -1
- package/mjs/src/lib/types/index.d.ts +1 -0
- package/mjs/src/lib/types/index.js +1 -0
- package/mjs/src/lib/types/index.js.map +1 -1
- package/mjs/src/lib/types/promise-json-api-sdk.d.ts +14 -3
- package/mjs/src/lib/types/promise-json-api-sdk.js +0 -1
- package/mjs/src/lib/types/query-params.d.ts +3 -3
- package/mjs/src/lib/types/utils.js +0 -1
- package/mjs/src/lib/utils/adapter-for-axios.d.ts +2 -2
- package/mjs/src/lib/utils/adapter-for-axios.js.map +1 -1
- package/mjs/src/lib/utils/generate-atomic-body.js +3 -0
- package/mjs/src/lib/utils/generate-atomic-body.js.map +1 -1
- package/mjs/src/lib/utils/utils.d.ts +9 -0
- package/mjs/src/lib/utils/utils.js +17 -0
- package/mjs/src/lib/utils/utils.js.map +1 -1
- package/package.json +26 -27
- package/cjs/package.json +0 -73
- package/cjs/src/index.d.ts +0 -5
- package/cjs/src/lib/constants/index.d.ts +0 -1
- package/cjs/src/lib/json-api-angular.d.ts +0 -21
- package/cjs/src/lib/json-api-js.d.ts +0 -15
- package/cjs/src/lib/service/atomic-operations.service.d.ts +0 -22
- package/cjs/src/lib/service/fetch-inner-client.d.ts +0 -20
- package/cjs/src/lib/service/index.d.ts +0 -3
- package/cjs/src/lib/service/json-api-sdk.service.d.ts +0 -21
- package/cjs/src/lib/service/json-api-utils.service.d.ts +0 -28
- package/cjs/src/lib/token/index.d.ts +0 -4
- package/cjs/src/lib/types/atomic-operation.d.ts +0 -33
- package/cjs/src/lib/types/atomic-type.d.ts +0 -23
- package/cjs/src/lib/types/config.d.ts +0 -14
- package/cjs/src/lib/types/filter-operand.d.ts +0 -10
- package/cjs/src/lib/types/http-inner-client.d.ts +0 -19
- package/cjs/src/lib/types/http-request-params.d.ts +0 -20
- package/cjs/src/lib/types/index.d.ts +0 -9
- package/cjs/src/lib/types/promise-json-api-sdk.d.ts +0 -16
- package/cjs/src/lib/types/query-params.d.ts +0 -47
- package/cjs/src/lib/types/utils.d.ts +0 -6
- package/cjs/src/lib/utils/adapter-for-axios.d.ts +0 -3
- package/cjs/src/lib/utils/entity-array.d.ts +0 -9
- package/cjs/src/lib/utils/generate-atomic-body.d.ts +0 -22
- package/cjs/src/lib/utils/http-params.d.ts +0 -154
- package/cjs/src/lib/utils/index.d.ts +0 -5
- package/cjs/src/lib/utils/utils.d.ts +0 -4
- package/cjs/src/ngModule.d.ts +0 -4
- package/mjs/package.json +0 -75
package/README.md
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
<p align='center'>
|
|
2
|
+
<a href="https://www.npmjs.com/package/@klerick/json-api-nestjs-sdk" target="_blank"><img src="https://img.shields.io/npm/v/@klerick/json-api-nestjs-sdk.svg" alt="NPM Version" /></a>
|
|
3
|
+
<a href="https://www.npmjs.com/package/@klerick/json-api-nestjs-sdk" target="_blank"><img src="https://img.shields.io/npm/l/@klerick/json-api-nestjs-sdk.svg" alt="Package License" /></a>
|
|
4
|
+
<a href="https://www.npmjs.com/package/@klerick/json-api-nestjs-sdk" target="_blank"><img src="https://img.shields.io/npm/dm/json-api-nestjs-sdk.svg" alt="NPM Downloads" /></a>
|
|
5
|
+
<a href="http://commitizen.github.io/cz-cli/" target="_blank"><img src="https://img.shields.io/badge/commitizen-friendly-brightgreen.svg" alt="Commitizen friendly" /></a>
|
|
6
|
+
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/klerick/02a4c98cf7008fea2af70dc2d50f4cb7/raw/json-api-nestjs-sdk.json" alt="Coverage Badge" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
# JSON:API Client SDK
|
|
10
|
+
|
|
11
|
+
Type-safe TypeScript/JavaScript client for consuming [JSON:API](https://jsonapi.org/) endpoints built with [@klerick/json-api-nestjs](https://www.npmjs.com/package/@klerick/json-api-nestjs).
|
|
12
|
+
|
|
13
|
+
## ✨ Features
|
|
14
|
+
|
|
15
|
+
- 🎯 **Full Type Safety** - Complete TypeScript support with type inference from your entities
|
|
16
|
+
- 🔍 **Advanced Filtering** - Rich query builder with operators (eq, ne, in, like, gt, lt, etc.)
|
|
17
|
+
- 📦 **Relationship Handling** - Easy include, sparse fieldsets, and relationship management
|
|
18
|
+
- ⚡ **Atomic Operations** - Batch multiple operations in a single request with rollback support
|
|
19
|
+
- 🌐 **Multiple HTTP Clients** - Works with Axios, Fetch API, and Angular HttpClient
|
|
20
|
+
- 📄 **Pagination & Sorting** - Built-in support for pagination and multi-field sorting
|
|
21
|
+
- 🔄 **Observable or Promise** - Choose your async style (RxJS Observable or native Promise)
|
|
22
|
+
- 🔗 **Relationship Operations** - Post, patch, and delete relationships independently
|
|
23
|
+
|
|
24
|
+
## 📚 Table of Contents
|
|
25
|
+
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Quick Start](#-quick-start)
|
|
28
|
+
- [Basic Setup (Axios)](#basic-setup-axios)
|
|
29
|
+
- [Angular Setup](#angular-setup)
|
|
30
|
+
- [Configuration](#-configuration)
|
|
31
|
+
- [API Methods](#-api-methods)
|
|
32
|
+
- [Fetching Resources](#fetching-resources)
|
|
33
|
+
- [Creating Resources](#creating-resources)
|
|
34
|
+
- [Updating Resources](#updating-resources)
|
|
35
|
+
- [Deleting Resources](#deleting-resources)
|
|
36
|
+
- [Relationship Operations](#relationship-operations)
|
|
37
|
+
- [Working with Plain Objects](#-working-with-plain-objects)
|
|
38
|
+
- [Using entity() Method](#using-entity-method)
|
|
39
|
+
- [Using String Type Names](#using-string-type-names)
|
|
40
|
+
- [Nullifying Relationships](#-nullifying-relationships)
|
|
41
|
+
- [Query Options](#-query-options)
|
|
42
|
+
- [Filtering](#filtering)
|
|
43
|
+
- [Sorting](#sorting)
|
|
44
|
+
- [Pagination](#pagination)
|
|
45
|
+
- [Including Relationships](#including-relationships)
|
|
46
|
+
- [Sparse Fieldsets](#sparse-fieldsets)
|
|
47
|
+
- [Atomic Operations](#-atomic-operations)
|
|
48
|
+
- [Examples](#-examples)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @klerick/json-api-nestjs-sdk
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 🚀 Quick Start
|
|
60
|
+
|
|
61
|
+
### Basic Setup (Axios)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { JsonApiJs, adapterForAxios, FilterOperand } from '@klerick/json-api-nestjs-sdk';
|
|
65
|
+
import axios from 'axios';
|
|
66
|
+
import { Users } from './entities'; // Your entity classes
|
|
67
|
+
|
|
68
|
+
// 1. Create adapter
|
|
69
|
+
const axiosAdapter = adapterForAxios(axios);
|
|
70
|
+
|
|
71
|
+
// 2. Configure SDK
|
|
72
|
+
const jsonSdk = JsonApiJs(
|
|
73
|
+
{
|
|
74
|
+
adapter: axiosAdapter,
|
|
75
|
+
apiHost: 'http://localhost:3000',
|
|
76
|
+
apiPrefix: 'api',
|
|
77
|
+
dateFields: ['createdAt', 'updatedAt'],
|
|
78
|
+
operationUrl: 'operation',
|
|
79
|
+
},
|
|
80
|
+
true // true = return Promises, false = return Observables
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// 3. Use SDK
|
|
84
|
+
// Fetch all users
|
|
85
|
+
const users = await jsonSdk.jonApiSdkService.getAll(Users);
|
|
86
|
+
|
|
87
|
+
// Fetch with filtering and relationships
|
|
88
|
+
const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
89
|
+
filter: {
|
|
90
|
+
target: {
|
|
91
|
+
isActive: { [FilterOperand.eq]: 'true' }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
include: ['addresses', 'roles']
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Get one user
|
|
98
|
+
const user = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
|
|
99
|
+
include: ['addresses', 'comments', 'roles', 'manager']
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Create a user
|
|
103
|
+
const newUser = new Users();
|
|
104
|
+
newUser.firstName = 'John';
|
|
105
|
+
newUser.lastName = 'Doe';
|
|
106
|
+
newUser.login = 'johndoe';
|
|
107
|
+
newUser.isActive = true;
|
|
108
|
+
|
|
109
|
+
const createdUser = await jsonSdk.jonApiSdkService.postOne(newUser);
|
|
110
|
+
|
|
111
|
+
// Update a user
|
|
112
|
+
createdUser.firstName = 'Jane';
|
|
113
|
+
const updatedUser = await jsonSdk.jonApiSdkService.patchOne(createdUser);
|
|
114
|
+
|
|
115
|
+
// Delete a user
|
|
116
|
+
await jsonSdk.jonApiSdkService.deleteOne(createdUser);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Angular Setup
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import {
|
|
123
|
+
provideJsonApi,
|
|
124
|
+
AtomicFactory,
|
|
125
|
+
JsonApiSdkService
|
|
126
|
+
} from '@klerick/json-api-nestjs-sdk/ngModule';
|
|
127
|
+
import {
|
|
128
|
+
provideHttpClient,
|
|
129
|
+
withFetch,
|
|
130
|
+
} from '@angular/common/http';
|
|
131
|
+
import { Component, inject } from '@angular/core';
|
|
132
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
133
|
+
|
|
134
|
+
// 1. Configure in your main.ts or app.config.ts
|
|
135
|
+
const angularConfig = {
|
|
136
|
+
apiHost: 'http://localhost:3000',
|
|
137
|
+
idKey: 'id',
|
|
138
|
+
apiPrefix: 'api',
|
|
139
|
+
operationUrl: 'operation',
|
|
140
|
+
dateFields: ['createdAt', 'updatedAt']
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
bootstrapApplication(AppComponent, {
|
|
144
|
+
providers: [
|
|
145
|
+
provideHttpClient(withFetch()),
|
|
146
|
+
provideJsonApi(angularConfig)
|
|
147
|
+
],
|
|
148
|
+
}).catch((err) => console.error(err));
|
|
149
|
+
|
|
150
|
+
// 2. Use in your components
|
|
151
|
+
@Component({
|
|
152
|
+
standalone: true,
|
|
153
|
+
selector: 'app-users',
|
|
154
|
+
templateUrl: './users.component.html',
|
|
155
|
+
})
|
|
156
|
+
export class UsersComponent {
|
|
157
|
+
private jsonApiService = inject(JsonApiSdkService);
|
|
158
|
+
private atomicFactory = inject(AtomicFactory);
|
|
159
|
+
|
|
160
|
+
async loadUsers() {
|
|
161
|
+
const users = await this.jsonApiService.getAll(Users, {
|
|
162
|
+
include: ['addresses']
|
|
163
|
+
});
|
|
164
|
+
return users;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async createMultipleResources() {
|
|
168
|
+
const result = await this.atomicFactory()
|
|
169
|
+
.postOne(newUser)
|
|
170
|
+
.postOne(newAddress)
|
|
171
|
+
.run();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## ⚙️ Configuration
|
|
177
|
+
|
|
178
|
+
### JsonConfig Type
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
type JsonSdkConfig = {
|
|
182
|
+
apiHost: string; // Base URL of your API (e.g., 'http://localhost:3000')
|
|
183
|
+
apiPrefix?: string; // API prefix (e.g., 'api' -> '/api/users')
|
|
184
|
+
idKey?: string; // Name of ID field (default: 'id')
|
|
185
|
+
idIsNumber?: boolean; // Parse IDs as numbers (default: false)
|
|
186
|
+
operationUrl?: string; // URL path for atomic operations (default: 'operation')
|
|
187
|
+
dateFields?: string[]; // Fields to convert to Date objects (e.g., ['createdAt', 'updatedAt'])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type JsonConfig = JsonSdkConfig & {
|
|
191
|
+
adapter?: HttpInnerClient; // HTTP client adapter (default: fetch)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### HTTP Adapters
|
|
196
|
+
|
|
197
|
+
**Axios Adapter:**
|
|
198
|
+
```typescript
|
|
199
|
+
import { adapterForAxios } from '@klerick/json-api-nestjs-sdk';
|
|
200
|
+
import axios from 'axios';
|
|
201
|
+
|
|
202
|
+
const adapter = adapterForAxios(axios);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Fetch API (default):**
|
|
206
|
+
```typescript
|
|
207
|
+
// No adapter needed, fetch is used by default
|
|
208
|
+
const jsonSdk = JsonApiJs({
|
|
209
|
+
apiHost: 'http://localhost:3000',
|
|
210
|
+
apiPrefix: 'api',
|
|
211
|
+
}, true);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Custom Adapter:**
|
|
215
|
+
|
|
216
|
+
See [HttpInnerClient interface](https://github.com/klerick/nestjs-json-api/blob/master/libs/json-api/json-api-nestjs-sdk/src/lib/types/http-inner-client.ts) for implementation details.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 📖 API Methods
|
|
221
|
+
|
|
222
|
+
### Fetching Resources
|
|
223
|
+
|
|
224
|
+
#### `getAll(Entity, options?)`
|
|
225
|
+
|
|
226
|
+
Fetch all resources with optional filtering, sorting, and relationships.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { FilterOperand } from '@klerick/json-api-nestjs-sdk';
|
|
230
|
+
|
|
231
|
+
// Fetch all users
|
|
232
|
+
const users = await jsonSdk.jonApiSdkService.getAll(Users);
|
|
233
|
+
|
|
234
|
+
// With filtering
|
|
235
|
+
const activeUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
236
|
+
filter: {
|
|
237
|
+
target: {
|
|
238
|
+
isActive: { [FilterOperand.eq]: 'true' },
|
|
239
|
+
id: { [FilterOperand.in]: ['1', '2', '3'] }
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
include: ['addresses', 'roles']
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Filter by relationship
|
|
246
|
+
const usersWithRoles = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
247
|
+
filter: {
|
|
248
|
+
target: {
|
|
249
|
+
id: { [FilterOperand.in]: ['1', '2'] }
|
|
250
|
+
},
|
|
251
|
+
roles: {
|
|
252
|
+
name: { [FilterOperand.eq]: 'admin' }
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
include: ['roles']
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `getList(Entity, options)`
|
|
260
|
+
|
|
261
|
+
Fetch resources with pagination (returns paginated results).
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const firstPage = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
265
|
+
page: {
|
|
266
|
+
number: 1,
|
|
267
|
+
size: 10
|
|
268
|
+
},
|
|
269
|
+
sort: {
|
|
270
|
+
target: {
|
|
271
|
+
id: 'ASC'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const secondPage = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
277
|
+
page: {
|
|
278
|
+
number: 2,
|
|
279
|
+
size: 10
|
|
280
|
+
},
|
|
281
|
+
sort: {
|
|
282
|
+
target: {
|
|
283
|
+
createdAt: 'DESC'
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### `getOne(Entity, id, options?)`
|
|
290
|
+
|
|
291
|
+
Fetch a single resource by ID.
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Simple fetch
|
|
295
|
+
const user = await jsonSdk.jonApiSdkService.getOne(Users, '1');
|
|
296
|
+
|
|
297
|
+
// With relationships
|
|
298
|
+
const userWithRelations = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
|
|
299
|
+
include: ['addresses', 'comments', 'roles', 'manager']
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// With sparse fieldsets
|
|
303
|
+
const userPartial = await jsonSdk.jonApiSdkService.getOne(Users, '1', {
|
|
304
|
+
fields: {
|
|
305
|
+
users: ['firstName', 'lastName', 'email']
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Creating Resources
|
|
311
|
+
|
|
312
|
+
#### `postOne(entity, options?)`
|
|
313
|
+
|
|
314
|
+
Create a new resource.
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// Simple create
|
|
318
|
+
const newUser = new Users();
|
|
319
|
+
newUser.firstName = 'John';
|
|
320
|
+
newUser.lastName = 'Doe';
|
|
321
|
+
newUser.login = 'johndoe';
|
|
322
|
+
newUser.isActive = true;
|
|
323
|
+
|
|
324
|
+
const createdUser = await jsonSdk.jonApiSdkService.postOne(newUser);
|
|
325
|
+
|
|
326
|
+
// Create with relationships
|
|
327
|
+
const newAddress = new Addresses();
|
|
328
|
+
newAddress.city = 'New York';
|
|
329
|
+
newAddress.state = 'NY';
|
|
330
|
+
newAddress.country = 'USA';
|
|
331
|
+
|
|
332
|
+
const savedAddress = await jsonSdk.jonApiSdkService.postOne(newAddress);
|
|
333
|
+
|
|
334
|
+
const user = new Users();
|
|
335
|
+
user.firstName = 'Jane';
|
|
336
|
+
user.lastName = 'Doe';
|
|
337
|
+
user.login = 'janedoe';
|
|
338
|
+
user.addresses = savedAddress; // Set relationship
|
|
339
|
+
|
|
340
|
+
const createdUser = await jsonSdk.jonApiSdkService.postOne(user);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Updating Resources
|
|
344
|
+
|
|
345
|
+
#### `patchOne(entity, options?)`
|
|
346
|
+
|
|
347
|
+
Update an existing resource.
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// Update attributes
|
|
351
|
+
user.firstName = 'Updated Name';
|
|
352
|
+
const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
|
|
353
|
+
|
|
354
|
+
// Update relationships
|
|
355
|
+
const newAddress = await jsonSdk.jonApiSdkService.postOne(addressEntity);
|
|
356
|
+
user.addresses = newAddress;
|
|
357
|
+
|
|
358
|
+
const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Deleting Resources
|
|
362
|
+
|
|
363
|
+
#### `deleteOne(entity)`
|
|
364
|
+
|
|
365
|
+
Delete a resource.
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
await jsonSdk.jonApiSdkService.deleteOne(user);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Relationship Operations
|
|
372
|
+
|
|
373
|
+
#### `deleteRelationships(entity, relationshipName)`
|
|
374
|
+
|
|
375
|
+
Remove relationships without deleting the related resources.
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// Remove all roles from user
|
|
379
|
+
await jsonSdk.jonApiSdkService.deleteRelationships(user, 'roles');
|
|
380
|
+
|
|
381
|
+
// Remove manager from user
|
|
382
|
+
await jsonSdk.jonApiSdkService.deleteRelationships(user, 'manager');
|
|
383
|
+
|
|
384
|
+
// Remove all comments from user
|
|
385
|
+
await jsonSdk.jonApiSdkService.deleteRelationships(user, 'comments');
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## 🏗️ Working with Plain Objects
|
|
391
|
+
|
|
392
|
+
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.
|
|
393
|
+
|
|
394
|
+
### Using entity() Method
|
|
395
|
+
|
|
396
|
+
The `entity()` method creates a properly typed entity instance from a plain object. This is essential when:
|
|
397
|
+
- You share types (not classes) between frontend and backend
|
|
398
|
+
- The SDK needs to identify the resource type at runtime (via `constructor.name`)
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { JsonApiJs } from '@klerick/json-api-nestjs-sdk';
|
|
402
|
+
|
|
403
|
+
// Shared type (not a class)
|
|
404
|
+
interface User {
|
|
405
|
+
id?: number;
|
|
406
|
+
firstName: string;
|
|
407
|
+
lastName: string;
|
|
408
|
+
login: string;
|
|
409
|
+
manager?: User | null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
|
|
413
|
+
|
|
414
|
+
// Create entity from plain object - chainable API
|
|
415
|
+
const createdUser = await jsonSdk.jonApiSdkService
|
|
416
|
+
.entity<User>('Users', {
|
|
417
|
+
firstName: 'John',
|
|
418
|
+
lastName: 'Doe',
|
|
419
|
+
login: 'johndoe'
|
|
420
|
+
})
|
|
421
|
+
.postOne();
|
|
422
|
+
|
|
423
|
+
// Update entity - chainable API
|
|
424
|
+
const updatedUser = await jsonSdk.jonApiSdkService
|
|
425
|
+
.entity<User>('Users', {
|
|
426
|
+
id: 1,
|
|
427
|
+
firstName: 'Jane'
|
|
428
|
+
})
|
|
429
|
+
.patchOne();
|
|
430
|
+
|
|
431
|
+
// Delete entity - chainable API
|
|
432
|
+
await jsonSdk.jonApiSdkService
|
|
433
|
+
.entity<User>('Users', { id: 1 })
|
|
434
|
+
.deleteOne();
|
|
435
|
+
|
|
436
|
+
// Work with relationships
|
|
437
|
+
const userRelations = await jsonSdk.jonApiSdkService
|
|
438
|
+
.entity<User>('Users', { id: 1 })
|
|
439
|
+
.getRelationships('manager');
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Raw mode** - get the entity instance without chaining:
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// Get raw entity instance (third argument = true)
|
|
446
|
+
const userEntity = jsonSdk.jonApiSdkService.entity<User>('Users', {
|
|
447
|
+
firstName: 'John',
|
|
448
|
+
lastName: 'Doe',
|
|
449
|
+
login: 'johndoe'
|
|
450
|
+
}, true);
|
|
451
|
+
|
|
452
|
+
// Now use it with standard SDK methods
|
|
453
|
+
const created = await jsonSdk.jonApiSdkService.postOne(userEntity);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Using String Type Names
|
|
457
|
+
|
|
458
|
+
GET methods also accept string type names instead of classes:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// Using string type name
|
|
462
|
+
const users = await jsonSdk.jonApiSdkService.getAll<User>('Users', {
|
|
463
|
+
include: ['manager']
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const user = await jsonSdk.jonApiSdkService.getOne<User>('Users', '1', {
|
|
467
|
+
include: ['manager']
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const userList = await jsonSdk.jonApiSdkService.getList<User>('Users', {
|
|
471
|
+
page: { number: 1, size: 10 }
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 🔗 Nullifying Relationships
|
|
478
|
+
|
|
479
|
+
To clear a relationship (set it to `null`), use the `nullRef()` function. This is necessary because the SDK distinguishes between:
|
|
480
|
+
- **Missing relationship** - not included in the request (no change)
|
|
481
|
+
- **Null relationship** - explicitly set to `null` (clear the relationship)
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { JsonApiJs, nullRef } from '@klerick/json-api-nestjs-sdk';
|
|
485
|
+
|
|
486
|
+
interface User {
|
|
487
|
+
id?: number;
|
|
488
|
+
firstName: string;
|
|
489
|
+
manager?: User | null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const jsonSdk = JsonApiJs({ apiHost: 'http://localhost:3000', apiPrefix: 'api' }, true);
|
|
493
|
+
|
|
494
|
+
// Clear the manager relationship
|
|
495
|
+
const user = jsonSdk.jonApiSdkService.entity<User>('Users', {
|
|
496
|
+
id: 1,
|
|
497
|
+
firstName: 'John',
|
|
498
|
+
manager: nullRef() // This will send { data: null } for the relationship
|
|
499
|
+
}, true);
|
|
500
|
+
|
|
501
|
+
const updatedUser = await jsonSdk.jonApiSdkService.patchOne(user);
|
|
502
|
+
// Result: user.manager is now null
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**How it works:**
|
|
506
|
+
- `nullRef()` returns a special marker object that TypeScript sees as `null`
|
|
507
|
+
- At runtime, the SDK detects this marker and generates `{ data: null }` in the JSON:API request body
|
|
508
|
+
- The server then clears the relationship
|
|
509
|
+
|
|
510
|
+
**Without nullRef:**
|
|
511
|
+
```typescript
|
|
512
|
+
// This won't clear the relationship - it will be ignored
|
|
513
|
+
const user = jsonSdk.jonApiSdkService.entity<User>('Users', {
|
|
514
|
+
id: 1,
|
|
515
|
+
firstName: 'John',
|
|
516
|
+
// manager is undefined - not included in request
|
|
517
|
+
}, true);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**With nullRef:**
|
|
521
|
+
```typescript
|
|
522
|
+
// This explicitly clears the relationship
|
|
523
|
+
const user = jsonSdk.jonApiSdkService.entity<User>('Users', {
|
|
524
|
+
id: 1,
|
|
525
|
+
firstName: 'John',
|
|
526
|
+
manager: nullRef() // Generates: relationships: { manager: { data: null } }
|
|
527
|
+
}, true);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 🔍 Query Options
|
|
533
|
+
|
|
534
|
+
### Filtering
|
|
535
|
+
|
|
536
|
+
Available operators:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
enum FilterOperand {
|
|
540
|
+
eq = 'eq', // Equal
|
|
541
|
+
ne = 'ne', // Not equal
|
|
542
|
+
in = 'in', // In array
|
|
543
|
+
nin = 'nin', // Not in array
|
|
544
|
+
lt = 'lt', // Less than
|
|
545
|
+
lte = 'lte', // Less than or equal
|
|
546
|
+
gt = 'gt', // Greater than
|
|
547
|
+
gte = 'gte', // Greater than or equal
|
|
548
|
+
like = 'like', // SQL LIKE
|
|
549
|
+
re = 'regexp', // Regular expression
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Examples:**
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// Equal
|
|
557
|
+
const users = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
558
|
+
filter: {
|
|
559
|
+
target: {
|
|
560
|
+
isActive: { [FilterOperand.eq]: 'true' }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Not equal
|
|
566
|
+
const inactiveUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
567
|
+
filter: {
|
|
568
|
+
target: {
|
|
569
|
+
isActive: { [FilterOperand.ne]: 'true' }
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// In array
|
|
575
|
+
const specificUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
576
|
+
filter: {
|
|
577
|
+
target: {
|
|
578
|
+
id: { [FilterOperand.in]: ['1', '2', '3'] }
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// LIKE search
|
|
584
|
+
const searchUsers = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
585
|
+
filter: {
|
|
586
|
+
target: {
|
|
587
|
+
login: { [FilterOperand.like]: 'john' }
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Check null/not null
|
|
593
|
+
const usersWithManager = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
594
|
+
filter: {
|
|
595
|
+
target: {
|
|
596
|
+
manager: { [FilterOperand.ne]: null }
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const usersWithoutManager = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
602
|
+
filter: {
|
|
603
|
+
target: {
|
|
604
|
+
manager: { [FilterOperand.eq]: null }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Sorting
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// Sort by single field
|
|
614
|
+
const users = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
615
|
+
sort: {
|
|
616
|
+
target: {
|
|
617
|
+
id: 'ASC'
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Sort by multiple fields
|
|
623
|
+
const sortedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
624
|
+
sort: {
|
|
625
|
+
target: {
|
|
626
|
+
createdAt: 'DESC',
|
|
627
|
+
lastName: 'ASC'
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Pagination
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
const paginatedUsers = await jsonSdk.jonApiSdkService.getList(Users, {
|
|
637
|
+
page: {
|
|
638
|
+
number: 1, // Page number (1-indexed)
|
|
639
|
+
size: 20 // Items per page
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Including Relationships
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// Include single relationship
|
|
648
|
+
const users = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
649
|
+
include: ['addresses']
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Include multiple relationships
|
|
653
|
+
const usersWithAll = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
654
|
+
include: ['addresses', 'roles', 'comments', 'manager']
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Include nested relationships
|
|
658
|
+
const usersWithNested = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
659
|
+
include: ['addresses', 'manager.addresses', 'roles']
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Sparse Fieldsets
|
|
664
|
+
|
|
665
|
+
Request only specific fields to reduce payload size.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
const users = await jsonSdk.jonApiSdkService.getAll(Users, {
|
|
669
|
+
fields: {
|
|
670
|
+
users: ['firstName', 'lastName', 'email'],
|
|
671
|
+
addresses: ['city', 'country']
|
|
672
|
+
},
|
|
673
|
+
include: ['addresses']
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## ⚡ Atomic Operations
|
|
680
|
+
|
|
681
|
+
Execute multiple operations in a single HTTP request. All operations succeed or fail together.
|
|
682
|
+
|
|
683
|
+
### Basic Atomic Operation
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
const newUser = new Users();
|
|
687
|
+
newUser.firstName = 'John';
|
|
688
|
+
newUser.lastName = 'Doe';
|
|
689
|
+
newUser.login = 'johndoe';
|
|
690
|
+
|
|
691
|
+
const result = await jsonSdk.atomicFactory()
|
|
692
|
+
.postOne(newUser)
|
|
693
|
+
.run();
|
|
694
|
+
|
|
695
|
+
console.log(result[0]); // Created user
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Multiple Operations
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// Create multiple related resources
|
|
702
|
+
const address = new Addresses();
|
|
703
|
+
address.city = 'New York';
|
|
704
|
+
address.state = 'NY';
|
|
705
|
+
address.country = 'USA';
|
|
706
|
+
|
|
707
|
+
const role = new Roles();
|
|
708
|
+
role.name = 'Admin';
|
|
709
|
+
role.key = 'admin';
|
|
710
|
+
|
|
711
|
+
const user = new Users();
|
|
712
|
+
user.firstName = 'Jane';
|
|
713
|
+
user.lastName = 'Doe';
|
|
714
|
+
user.login = 'janedoe';
|
|
715
|
+
user.addresses = address;
|
|
716
|
+
user.roles = [role];
|
|
717
|
+
|
|
718
|
+
const [createdAddress, createdRole, createdUser] = await jsonSdk
|
|
719
|
+
.atomicFactory()
|
|
720
|
+
.postOne(address)
|
|
721
|
+
.postOne(role)
|
|
722
|
+
.postOne(user)
|
|
723
|
+
.run();
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Mixed Operations (POST, PATCH, Relationships)
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
// Create user first
|
|
730
|
+
const newUser = new Users();
|
|
731
|
+
newUser.firstName = 'John';
|
|
732
|
+
newUser.login = 'john';
|
|
733
|
+
|
|
734
|
+
const [createdUser] = await jsonSdk.atomicFactory()
|
|
735
|
+
.postOne(newUser)
|
|
736
|
+
.run();
|
|
737
|
+
|
|
738
|
+
// Then update and manage relationships atomically
|
|
739
|
+
const patchUser = Object.assign(new Users(), createdUser);
|
|
740
|
+
patchUser.firstName = 'John Updated';
|
|
741
|
+
patchUser.roles = [role1];
|
|
742
|
+
|
|
743
|
+
const patchUser2 = Object.assign(new Users(), createdUser);
|
|
744
|
+
patchUser2.comments = [comment1];
|
|
745
|
+
|
|
746
|
+
const patchUser3 = Object.assign(new Users(), createdUser);
|
|
747
|
+
patchUser3.comments = [comment2];
|
|
748
|
+
|
|
749
|
+
const result = await jsonSdk
|
|
750
|
+
.atomicFactory()
|
|
751
|
+
.patchOne(patchUser) // Update user attributes and set roles
|
|
752
|
+
.patchOne(patchUser2) // Set comments
|
|
753
|
+
.patchRelationships(patchUser2, 'comments') // Replace comments (keep only comment1)
|
|
754
|
+
.postRelationships(patchUser3, 'comments') // Add comment2 to existing comments
|
|
755
|
+
.run();
|
|
756
|
+
|
|
757
|
+
// result[0] - updated user
|
|
758
|
+
// result[1] - updated user with comments
|
|
759
|
+
// result[2] - array of comment IDs after replacement
|
|
760
|
+
// result[3] - array of all comment IDs after addition
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Using Temporary IDs (lid)
|
|
764
|
+
|
|
765
|
+
Reference resources created within the same atomic request using temporary IDs.
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
const address = new Addresses();
|
|
769
|
+
address.city = 'Boston';
|
|
770
|
+
address.id = 10000; // Temporary ID
|
|
771
|
+
|
|
772
|
+
const user = new Users();
|
|
773
|
+
user.firstName = 'Alice';
|
|
774
|
+
user.addresses = address; // Reference by temp ID
|
|
775
|
+
|
|
776
|
+
const [createdAddress, createdUser] = await jsonSdk
|
|
777
|
+
.atomicFactory()
|
|
778
|
+
.postOne(address)
|
|
779
|
+
.postOne(user)
|
|
780
|
+
.run();
|
|
781
|
+
|
|
782
|
+
// Server assigns real IDs
|
|
783
|
+
console.log(createdAddress.id); // Real ID (e.g., 1)
|
|
784
|
+
console.log(createdUser.addresses.id); // Same real ID
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## 💡 Examples
|
|
790
|
+
|
|
791
|
+
For comprehensive real-world examples, see the [E2E test suite](https://github.com/klerick/nestjs-json-api/tree/master/apps/json-api-server-e2e/src/json-api/json-api-sdk):
|
|
792
|
+
|
|
793
|
+
- **[GET Operations](https://github.com/klerick/nestjs-json-api/blob/master/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts)** - Fetching, filtering, pagination, sparse fieldsets
|
|
794
|
+
- **[POST Operations](https://github.com/klerick/nestjs-json-api/blob/master/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts)** - Creating resources with relationships
|
|
795
|
+
- **[PATCH Operations](https://github.com/klerick/nestjs-json-api/blob/master/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts)** - Updating resources and relationships
|
|
796
|
+
- **[Atomic Operations](https://github.com/klerick/nestjs-json-api/blob/master/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts)** - Batch requests with rollback
|
|
797
|
+
- **[Common Decorators](https://github.com/klerick/nestjs-json-api/blob/master/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts)** - Guards, interceptors, custom behavior
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## 📝 License
|
|
802
|
+
|
|
803
|
+
MIT
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## 🔗 Related Packages
|
|
808
|
+
|
|
809
|
+
- [@klerick/json-api-nestjs](https://www.npmjs.com/package/@klerick/json-api-nestjs) - JSON:API server implementation for NestJS
|
|
810
|
+
- [@klerick/json-api-nestjs-typeorm](https://www.npmjs.com/package/@klerick/json-api-nestjs-typeorm) - TypeORM adapter
|
|
811
|
+
- [@klerick/json-api-nestjs-microorm](https://www.npmjs.com/package/@klerick/json-api-nestjs-microorm) - MikroORM adapter
|