json-rest-api 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +392 -130
- package/TODO.todo +113 -0
- package/api.js +628 -0
- package/docs/API.md +1306 -0
- package/docs/FIXES_APPLIED.md +59 -0
- package/docs/GUIDES.md +1792 -0
- package/docs/RESOURCES_IN_EXPRESS.md +456 -0
- package/docs/example-versioning.js +168 -0
- package/docs/example.js +195 -0
- package/index.js +58 -207
- package/package.json +15 -25
- package/plugins/API guide.md +364 -0
- package/plugins/api-registry.js +142 -0
- package/plugins/api-versioning.md +135 -0
- package/plugins/http.js +273 -0
- package/plugins/logging.js +255 -0
- package/plugins/memory.js +182 -0
- package/plugins/mysql.js +753 -0
- package/plugins/openapi.js +401 -0
- package/plugins/positioning.js +220 -0
- package/plugins/security.js +261 -0
- package/plugins/validation.js +173 -0
- package/plugins/versioning.js +288 -0
- package/resource-helper.js +79 -0
- package/schema.js +481 -0
- package/test-basic.js +107 -0
- package/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/LICENSE +0 -20
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
● Programmatic API Usage Guide
|
|
2
|
+
|
|
3
|
+
Here's how to use the JSON REST API methods programmatically:
|
|
4
|
+
|
|
5
|
+
Basic Setup
|
|
6
|
+
|
|
7
|
+
import { createApi, Schema } from './jsonrestapi/index.js';
|
|
8
|
+
|
|
9
|
+
// Create API instance
|
|
10
|
+
const api = createApi({
|
|
11
|
+
storage: 'memory', // or 'mysql'
|
|
12
|
+
http: false // Disable HTTP if using programmatically only
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Define a schema
|
|
16
|
+
const userSchema = new Schema({
|
|
17
|
+
id: { type: 'id' },
|
|
18
|
+
name: { type: 'string', required: true },
|
|
19
|
+
email: { type: 'string', required: true, lowercase: true },
|
|
20
|
+
age: { type: 'number', min: 0 },
|
|
21
|
+
active: { type: 'boolean', default: true }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Register schema
|
|
25
|
+
api.addResource('users', userSchema);
|
|
26
|
+
|
|
27
|
+
CRUD Operations
|
|
28
|
+
|
|
29
|
+
1. Create (Insert)
|
|
30
|
+
|
|
31
|
+
// Simple insert
|
|
32
|
+
const newUser = await api.insert({
|
|
33
|
+
name: 'John Doe',
|
|
34
|
+
email: 'john@example.com',
|
|
35
|
+
age: 30
|
|
36
|
+
}, {
|
|
37
|
+
type: 'users'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log(newUser);
|
|
41
|
+
// Output: { data: { id: '1', type: 'users', attributes: { name: 'John
|
|
42
|
+
Doe', ... } } }
|
|
43
|
+
|
|
44
|
+
// Insert with positioning (if PositioningPlugin is used)
|
|
45
|
+
const secondUser = await api.insert({
|
|
46
|
+
name: 'Jane Smith',
|
|
47
|
+
email: 'jane@example.com',
|
|
48
|
+
beforeId: null // Place at end
|
|
49
|
+
}, {
|
|
50
|
+
type: 'users',
|
|
51
|
+
positioning: { enabled: true }
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
2. Read (Get)
|
|
55
|
+
|
|
56
|
+
// Get by ID
|
|
57
|
+
const user = await api.get('1', {
|
|
58
|
+
type: 'users'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(user);
|
|
62
|
+
// Output: { data: { id: '1', type: 'users', attributes: { ... } } }
|
|
63
|
+
|
|
64
|
+
// Handle not found
|
|
65
|
+
try {
|
|
66
|
+
const notFound = await api.get('999', { type: 'users' });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log('User not found');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
3. Update
|
|
72
|
+
|
|
73
|
+
// Partial update
|
|
74
|
+
const updated = await api.update('1', {
|
|
75
|
+
age: 31,
|
|
76
|
+
active: false
|
|
77
|
+
}, {
|
|
78
|
+
type: 'users'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Update with validation context
|
|
82
|
+
const updateWithContext = await api.update('1', {
|
|
83
|
+
email: 'newemail@example.com'
|
|
84
|
+
}, {
|
|
85
|
+
type: 'users',
|
|
86
|
+
userId: 'admin-123', // For audit logging
|
|
87
|
+
ipAddress: '192.168.1.1'
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
4. Delete
|
|
91
|
+
|
|
92
|
+
// Delete by ID
|
|
93
|
+
await api.delete('1', {
|
|
94
|
+
type: 'users'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Delete returns { data: null } on success
|
|
98
|
+
|
|
99
|
+
5. Query (List)
|
|
100
|
+
|
|
101
|
+
// Simple query
|
|
102
|
+
const allUsers = await api.query({}, {
|
|
103
|
+
type: 'users'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Advanced query with filters
|
|
107
|
+
const activeAdults = await api.query({
|
|
108
|
+
filter: {
|
|
109
|
+
active: true,
|
|
110
|
+
age: { $gte: 18 } // If your storage plugin supports operators
|
|
111
|
+
},
|
|
112
|
+
sort: '-age,name', // Sort by age DESC, then name ASC
|
|
113
|
+
page: {
|
|
114
|
+
size: 20,
|
|
115
|
+
number: 1
|
|
116
|
+
}
|
|
117
|
+
}, {
|
|
118
|
+
type: 'users'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(activeAdults);
|
|
122
|
+
// Output: {
|
|
123
|
+
// data: [ { id: '1', type: 'users', attributes: {...} }, ... ],
|
|
124
|
+
// meta: { total: 45, pageSize: 20, pageNumber: 1, totalPages: 3 },
|
|
125
|
+
// links: { self: '...', next: '...', last: '...' }
|
|
126
|
+
// }
|
|
127
|
+
|
|
128
|
+
// Search (if configured)
|
|
129
|
+
const searchResults = await api.query({
|
|
130
|
+
search: 'john',
|
|
131
|
+
filter: { active: true }
|
|
132
|
+
}, {
|
|
133
|
+
type: 'users',
|
|
134
|
+
searchFields: ['name', 'email']
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
Advanced Usage
|
|
138
|
+
|
|
139
|
+
With MySQL Storage
|
|
140
|
+
|
|
141
|
+
import { Api, MySQLPlugin, ValidationPlugin } from
|
|
142
|
+
'./jsonrestapi/index.js';
|
|
143
|
+
|
|
144
|
+
const api = new Api()
|
|
145
|
+
.use(ValidationPlugin)
|
|
146
|
+
.use(MySQLPlugin, {
|
|
147
|
+
connection: {
|
|
148
|
+
host: 'localhost',
|
|
149
|
+
user: 'root',
|
|
150
|
+
password: 'password',
|
|
151
|
+
database: 'myapp'
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Sync schema with database
|
|
156
|
+
await api.syncSchema(userSchema, 'users');
|
|
157
|
+
|
|
158
|
+
// Use with table specification
|
|
159
|
+
const user = await api.insert({
|
|
160
|
+
name: 'Database User',
|
|
161
|
+
email: 'db@example.com'
|
|
162
|
+
}, {
|
|
163
|
+
type: 'users',
|
|
164
|
+
table: 'users', // MySQL table name
|
|
165
|
+
connection: 'default'
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
With Authentication Context
|
|
169
|
+
|
|
170
|
+
// Add security plugin
|
|
171
|
+
api.use(SecurityPlugin);
|
|
172
|
+
|
|
173
|
+
// All operations can include auth context
|
|
174
|
+
const result = await api.insert({
|
|
175
|
+
title: 'Secure Post',
|
|
176
|
+
content: 'This is secure'
|
|
177
|
+
}, {
|
|
178
|
+
type: 'posts',
|
|
179
|
+
userId: 'user-123', // From decoded JWT
|
|
180
|
+
authenticated: true,
|
|
181
|
+
permissions: ['create', 'read']
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
Batch Operations
|
|
185
|
+
|
|
186
|
+
// Insert multiple records
|
|
187
|
+
const users = [
|
|
188
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
189
|
+
{ name: 'User 2', email: 'user2@example.com' },
|
|
190
|
+
{ name: 'User 3', email: 'user3@example.com' }
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const created = await Promise.all(
|
|
194
|
+
users.map(user => api.insert(user, { type: 'users' }))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Update multiple records
|
|
198
|
+
const updates = [
|
|
199
|
+
{ id: '1', data: { active: false } },
|
|
200
|
+
{ id: '2', data: { active: false } },
|
|
201
|
+
{ id: '3', data: { active: false } }
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
await Promise.all(
|
|
205
|
+
updates.map(({ id, data }) =>
|
|
206
|
+
api.update(id, data, { type: 'users' })
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
With Transactions (MySQL)
|
|
211
|
+
|
|
212
|
+
// Get connection pool
|
|
213
|
+
const { pool } = api.getConnection();
|
|
214
|
+
const connection = await pool.getConnection();
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await connection.beginTransaction();
|
|
218
|
+
|
|
219
|
+
// Create user
|
|
220
|
+
const user = await api.insert({
|
|
221
|
+
name: 'Transaction User',
|
|
222
|
+
email: 'tx@example.com'
|
|
223
|
+
}, {
|
|
224
|
+
type: 'users',
|
|
225
|
+
connection // Pass connection for transaction
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Create related profile
|
|
229
|
+
await api.insert({
|
|
230
|
+
userId: user.data.id,
|
|
231
|
+
bio: 'Created in transaction'
|
|
232
|
+
}, {
|
|
233
|
+
type: 'profiles',
|
|
234
|
+
connection
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await connection.commit();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
await connection.rollback();
|
|
240
|
+
throw error;
|
|
241
|
+
} finally {
|
|
242
|
+
connection.release();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
With Custom Validation
|
|
246
|
+
|
|
247
|
+
// Add custom validation hook
|
|
248
|
+
api.hook('afterValidate', async (context) => {
|
|
249
|
+
if (context.options.type === 'users') {
|
|
250
|
+
const { data, method } = context;
|
|
251
|
+
|
|
252
|
+
// Check email uniqueness
|
|
253
|
+
if ((method === 'insert' || method === 'update') && data.email) {
|
|
254
|
+
const existing = await api.query({
|
|
255
|
+
filter: { email: data.email }
|
|
256
|
+
}, { type: 'users' });
|
|
257
|
+
|
|
258
|
+
if (existing.data.length > 0) {
|
|
259
|
+
context.errors.push({
|
|
260
|
+
field: 'email',
|
|
261
|
+
message: 'Email already exists',
|
|
262
|
+
code: 'DUPLICATE_EMAIL'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
Error Handling
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const user = await api.insert({
|
|
273
|
+
name: 'J', // Too short!
|
|
274
|
+
email: 'invalid-email'
|
|
275
|
+
}, { type: 'users' });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error.errors) {
|
|
278
|
+
// Validation errors
|
|
279
|
+
error.errors.forEach(err => {
|
|
280
|
+
console.log(`${err.field}: ${err.message}`);
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
// Other errors
|
|
284
|
+
console.error('Unexpected error:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Direct Storage Access
|
|
289
|
+
|
|
290
|
+
// If you need direct access to storage
|
|
291
|
+
if (api.memoryData) {
|
|
292
|
+
// Direct memory access
|
|
293
|
+
console.log('All data:', api.memoryData);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (api.mysqlPools) {
|
|
297
|
+
// Direct MySQL access
|
|
298
|
+
const { pool } = api.getConnection();
|
|
299
|
+
const [rows] = await pool.query('SELECT * FROM users WHERE active = ?',
|
|
300
|
+
[true]);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
Complete Example: Task Manager
|
|
304
|
+
|
|
305
|
+
import { createApi, Schema, PositioningPlugin } from
|
|
306
|
+
'./jsonrestapi/index.js';
|
|
307
|
+
|
|
308
|
+
// Create API
|
|
309
|
+
const api = createApi({ storage: 'memory' })
|
|
310
|
+
.use(PositioningPlugin);
|
|
311
|
+
|
|
312
|
+
// Define schema
|
|
313
|
+
const taskSchema = new Schema({
|
|
314
|
+
id: { type: 'id' },
|
|
315
|
+
title: { type: 'string', required: true, min: 1, max: 200 },
|
|
316
|
+
description: { type: 'string', max: 1000 },
|
|
317
|
+
completed: { type: 'boolean', default: false },
|
|
318
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high'], default:
|
|
319
|
+
'medium' },
|
|
320
|
+
dueDate: { type: 'date' },
|
|
321
|
+
position: { type: 'number' },
|
|
322
|
+
createdAt: { type: 'timestamp', default: () => Date.now() }
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
api.addResource('tasks', taskSchema);
|
|
326
|
+
|
|
327
|
+
// Usage
|
|
328
|
+
async function taskManager() {
|
|
329
|
+
// Create tasks
|
|
330
|
+
const task1 = await api.insert({
|
|
331
|
+
title: 'Write documentation',
|
|
332
|
+
priority: 'high',
|
|
333
|
+
dueDate: '2024-12-31'
|
|
334
|
+
}, { type: 'tasks' });
|
|
335
|
+
|
|
336
|
+
const task2 = await api.insert({
|
|
337
|
+
title: 'Review PR',
|
|
338
|
+
priority: 'medium',
|
|
339
|
+
beforeId: task1.data.id // Position before first task
|
|
340
|
+
}, {
|
|
341
|
+
type: 'tasks',
|
|
342
|
+
positioning: { enabled: true }
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// List high priority incomplete tasks
|
|
346
|
+
const urgent = await api.query({
|
|
347
|
+
filter: {
|
|
348
|
+
completed: false,
|
|
349
|
+
priority: 'high'
|
|
350
|
+
},
|
|
351
|
+
sort: 'position,dueDate'
|
|
352
|
+
}, { type: 'tasks' });
|
|
353
|
+
|
|
354
|
+
console.log(`Found ${urgent.meta.total} urgent tasks`);
|
|
355
|
+
|
|
356
|
+
// Complete a task
|
|
357
|
+
await api.update(task1.data.id, {
|
|
358
|
+
completed: true
|
|
359
|
+
}, { type: 'tasks' });
|
|
360
|
+
|
|
361
|
+
// Reorder tasks
|
|
362
|
+
await api.reposition('tasks', task2.data.id, null); // Move to end
|
|
363
|
+
}
|
|
364
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API Registry Plugin - Automatic version management
|
|
5
|
+
*/
|
|
6
|
+
export const ApiRegistryPlugin = {
|
|
7
|
+
install(api, options = {}) {
|
|
8
|
+
// Initialize the global registry if it doesn't exist
|
|
9
|
+
if (!globalThis.__jsonRestApiRegistry) {
|
|
10
|
+
globalThis.__jsonRestApiRegistry = new ApiRegistry();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const registry = globalThis.__jsonRestApiRegistry;
|
|
14
|
+
|
|
15
|
+
// Register this API instance
|
|
16
|
+
const apiName = options.name || api.options.name;
|
|
17
|
+
const apiVersion = options.version || api.options.version;
|
|
18
|
+
|
|
19
|
+
if (!apiName || !apiVersion) {
|
|
20
|
+
throw new Error('API name and version are required for registration');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
api.apiName = apiName;
|
|
24
|
+
api.apiVersion = apiVersion;
|
|
25
|
+
|
|
26
|
+
// Register in global registry
|
|
27
|
+
registry.register(apiName, apiVersion, api);
|
|
28
|
+
|
|
29
|
+
// Add version resolution to the API
|
|
30
|
+
api.apis = new Proxy({}, {
|
|
31
|
+
get(target, prop) {
|
|
32
|
+
const requestedVersion = api.apiVersion;
|
|
33
|
+
return registry.getCompatibleApi(prop, requestedVersion);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Override HTTP routes to handle version negotiation
|
|
38
|
+
if (api.router) {
|
|
39
|
+
const originalMount = api.mount;
|
|
40
|
+
api.mount = function(app, basePath = '/api') {
|
|
41
|
+
// Mount versioned routes
|
|
42
|
+
const versionPath = `${basePath}/${apiVersion}`;
|
|
43
|
+
originalMount.call(api, app, versionPath);
|
|
44
|
+
|
|
45
|
+
// Also mount on base path with version negotiation
|
|
46
|
+
app.use(`${basePath}/:apiName/*`, (req, res, next) => {
|
|
47
|
+
const requestedVersion =
|
|
48
|
+
req.headers['api-version'] ||
|
|
49
|
+
req.headers['x-api-version'] ||
|
|
50
|
+
req.query.v ||
|
|
51
|
+
'latest';
|
|
52
|
+
|
|
53
|
+
const compatibleApi = registry.getCompatibleApi(
|
|
54
|
+
req.params.apiName,
|
|
55
|
+
requestedVersion
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!compatibleApi) {
|
|
59
|
+
return res.status(400).json({
|
|
60
|
+
errors: [{
|
|
61
|
+
status: '400',
|
|
62
|
+
title: 'Version Not Found',
|
|
63
|
+
detail: `No compatible version found for ${req.params.apiName} ${requestedVersion}`
|
|
64
|
+
}]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Forward to the compatible API
|
|
69
|
+
req.url = req.url.replace(`/${req.params.apiName}`, '');
|
|
70
|
+
compatibleApi.router(req, res, next);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return api;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Global API Registry
|
|
81
|
+
*/
|
|
82
|
+
class ApiRegistry {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.apis = new Map(); // name -> version -> api
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
register(name, version, api) {
|
|
88
|
+
if (!this.apis.has(name)) {
|
|
89
|
+
this.apis.set(name, new Map());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const versions = this.apis.get(name);
|
|
93
|
+
versions.set(version, api);
|
|
94
|
+
|
|
95
|
+
// Sort versions for efficient lookup
|
|
96
|
+
const sortedVersions = Array.from(versions.entries())
|
|
97
|
+
.sort(([a], [b]) => semver.compare(b, a));
|
|
98
|
+
|
|
99
|
+
this.apis.set(name, new Map(sortedVersions));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getCompatibleApi(name, versionSpec) {
|
|
103
|
+
const versions = this.apis.get(name);
|
|
104
|
+
if (!versions) return null;
|
|
105
|
+
|
|
106
|
+
// Special case: latest
|
|
107
|
+
if (versionSpec === 'latest') {
|
|
108
|
+
return versions.values().next().value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Exact match
|
|
112
|
+
if (versions.has(versionSpec)) {
|
|
113
|
+
return versions.get(versionSpec);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Find compatible version
|
|
117
|
+
for (const [version, api] of versions) {
|
|
118
|
+
if (semver.satisfies(version, versionSpec)) {
|
|
119
|
+
return api;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback: find any version >= requested
|
|
124
|
+
if (!versionSpec.includes('>') && !versionSpec.includes('<') && !versionSpec.includes('^') && !versionSpec.includes('~')) {
|
|
125
|
+
for (const [version, api] of versions) {
|
|
126
|
+
if (semver.gte(version, versionSpec)) {
|
|
127
|
+
return api;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getAllVersions(name) {
|
|
136
|
+
const versions = this.apis.get(name);
|
|
137
|
+
return versions ? Array.from(versions.keys()) : [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Export the registry for direct access if needed
|
|
142
|
+
export const apiRegistry = globalThis.__jsonRestApiRegistry || new ApiRegistry();
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
|
|
2
|
+
1. Creating Versioned APIs
|
|
3
|
+
|
|
4
|
+
// Just specify name and version - that's it!
|
|
5
|
+
const userApi = createApi({
|
|
6
|
+
name: 'users',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
storage: 'memory'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Create a new version
|
|
12
|
+
const userApiV2 = createApi({
|
|
13
|
+
name: 'users',
|
|
14
|
+
version: '2.0.0',
|
|
15
|
+
storage: 'memory'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
2. Using APIs Programmatically
|
|
19
|
+
|
|
20
|
+
// Get latest version automatically
|
|
21
|
+
const api = Api.get('users', 'latest');
|
|
22
|
+
|
|
23
|
+
// Get specific version
|
|
24
|
+
const apiV1 = Api.get('users', '1.0.0');
|
|
25
|
+
|
|
26
|
+
// Get minimum version (2.0.0 or higher)
|
|
27
|
+
const apiV2Plus = Api.get('users', '2.0.0');
|
|
28
|
+
|
|
29
|
+
// Use it
|
|
30
|
+
const user = await api.insert({
|
|
31
|
+
name: 'John',
|
|
32
|
+
email: 'john@example.com'
|
|
33
|
+
}, { type: 'users' });
|
|
34
|
+
|
|
35
|
+
3. Cross-API Communication
|
|
36
|
+
|
|
37
|
+
APIs can access each other automatically with version compatibility:
|
|
38
|
+
|
|
39
|
+
const ordersApi = createApi({
|
|
40
|
+
name: 'orders',
|
|
41
|
+
version: '1.0.0'
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Inside orders API, access users API automatically
|
|
45
|
+
ordersApi.hook('afterInsert', async (context) => {
|
|
46
|
+
// This gets a compatible users API automatically!
|
|
47
|
+
const usersApi = ordersApi.apis.users;
|
|
48
|
+
const user = await usersApi.get(context.data.userId, { type: 'users'
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
4. HTTP Version Negotiation
|
|
53
|
+
|
|
54
|
+
The library handles all HTTP version routing automatically:
|
|
55
|
+
|
|
56
|
+
// Mount APIs - versioning is automatic
|
|
57
|
+
userApiV1.mount(app); // Available at /api/1.0.0/users
|
|
58
|
+
userApiV2.mount(app); // Available at /api/2.0.0/users
|
|
59
|
+
|
|
60
|
+
// Clients can request versions:
|
|
61
|
+
// Via header
|
|
62
|
+
fetch('/api/users', {
|
|
63
|
+
headers: { 'API-Version': '2.0.0' }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Via query
|
|
67
|
+
fetch('/api/users?v=2.0.0');
|
|
68
|
+
|
|
69
|
+
// Via path
|
|
70
|
+
fetch('/api/2.0.0/users');
|
|
71
|
+
|
|
72
|
+
5. Version Resolution Rules
|
|
73
|
+
|
|
74
|
+
The library automatically finds the right version:
|
|
75
|
+
|
|
76
|
+
- 'latest' → Newest version
|
|
77
|
+
- '2.0.0' → Exactly 2.0.0 OR the newest version ≥ 2.0.0
|
|
78
|
+
- '^2.0.0' → Any 2.x.x version (npm style)
|
|
79
|
+
- '~2.1.0' → Any 2.1.x version (npm style)
|
|
80
|
+
- '>=2.0.0' → Any version ≥ 2.0.0
|
|
81
|
+
|
|
82
|
+
6. Complete Working Example
|
|
83
|
+
|
|
84
|
+
// Define v1
|
|
85
|
+
const apiV1 = createApi({
|
|
86
|
+
name: 'products',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
storage: 'memory'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
apiV1.addResource('products', new Schema({
|
|
92
|
+
id: { type: 'id' },
|
|
93
|
+
name: { type: 'string', required: true },
|
|
94
|
+
price: { type: 'number', required: true }
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// Define v2 with new field
|
|
98
|
+
const apiV2 = createApi({
|
|
99
|
+
name: 'products',
|
|
100
|
+
version: '2.0.0',
|
|
101
|
+
storage: 'memory'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
apiV2.addResource('products', new Schema({
|
|
105
|
+
id: { type: 'id' },
|
|
106
|
+
name: { type: 'string', required: true },
|
|
107
|
+
price: { type: 'number', required: true },
|
|
108
|
+
category: { type: 'string', required: true } // New!
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Use programmatically - version selection is automatic
|
|
112
|
+
async function createProduct(data) {
|
|
113
|
+
// Automatically picks the right version based on data
|
|
114
|
+
const hasCategory = 'category' in data;
|
|
115
|
+
const minVersion = hasCategory ? '2.0.0' : '1.0.0';
|
|
116
|
+
|
|
117
|
+
const api = Api.get('products', minVersion);
|
|
118
|
+
return api.insert(data, { type: 'products' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Works with v1
|
|
122
|
+
await createProduct({
|
|
123
|
+
name: 'Widget',
|
|
124
|
+
price: 9.99
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Automatically uses v2
|
|
128
|
+
await createProduct({
|
|
129
|
+
name: 'Gadget',
|
|
130
|
+
price: 19.99,
|
|
131
|
+
category: 'Electronics'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
The library handles all the complexity - you just specify name and
|
|
135
|
+
version when creating APIs, and the library does the rest!
|