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
package/api.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
// Global registry for all APIs
|
|
4
|
+
const globalRegistry = new Map();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Core API class with plugin system for JSON REST APIs
|
|
8
|
+
*/
|
|
9
|
+
export class Api {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
|
|
12
|
+
// Core properties
|
|
13
|
+
this.options = {
|
|
14
|
+
idProperty: 'id',
|
|
15
|
+
name: null,
|
|
16
|
+
version: null,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Plugin system
|
|
21
|
+
this.plugins = [];
|
|
22
|
+
this.hooks = new Map();
|
|
23
|
+
|
|
24
|
+
// Storage for different implementations
|
|
25
|
+
this.implementers = new Map();
|
|
26
|
+
|
|
27
|
+
// Initialize hook points
|
|
28
|
+
this._initializeHooks();
|
|
29
|
+
|
|
30
|
+
// Auto-register if name and version provided
|
|
31
|
+
if (this.options.name && this.options.version) {
|
|
32
|
+
this.register();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register this API in the global registry
|
|
38
|
+
*/
|
|
39
|
+
register() {
|
|
40
|
+
const { name, version } = this.options;
|
|
41
|
+
|
|
42
|
+
if (!name || !version) {
|
|
43
|
+
throw new Error('API name and version required for registration');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!globalRegistry.has(name)) {
|
|
47
|
+
globalRegistry.set(name, new Map());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
globalRegistry.get(name).set(version, this);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get all versions of an API by name
|
|
56
|
+
*/
|
|
57
|
+
static getVersions(name) {
|
|
58
|
+
const versions = globalRegistry.get(name);
|
|
59
|
+
return versions ? Array.from(versions.keys()) : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get a compatible API version
|
|
64
|
+
*/
|
|
65
|
+
static get(name, version = 'latest') {
|
|
66
|
+
const versions = globalRegistry.get(name);
|
|
67
|
+
if (!versions) return null;
|
|
68
|
+
|
|
69
|
+
// Sort versions for lookup
|
|
70
|
+
const sortedVersions = Array.from(versions.entries())
|
|
71
|
+
.sort(([a], [b]) => semver.compare(b, a));
|
|
72
|
+
|
|
73
|
+
// Latest version
|
|
74
|
+
if (version === 'latest') {
|
|
75
|
+
return sortedVersions[0]?.[1];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Exact match
|
|
79
|
+
if (versions.has(version)) {
|
|
80
|
+
return versions.get(version);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Compatible version
|
|
84
|
+
for (const [ver, api] of sortedVersions) {
|
|
85
|
+
// If no operators, treat as minimum version
|
|
86
|
+
if (!version.match(/[<>^~]/)) {
|
|
87
|
+
if (semver.gte(ver, version)) {
|
|
88
|
+
return api;
|
|
89
|
+
}
|
|
90
|
+
} else if (semver.satisfies(ver, version)) {
|
|
91
|
+
return api;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get all registered APIs
|
|
100
|
+
*/
|
|
101
|
+
static getRegistry() {
|
|
102
|
+
const registry = {};
|
|
103
|
+
for (const [name, versions] of globalRegistry) {
|
|
104
|
+
registry[name] = Array.from(versions.keys()).sort(semver.rcompare);
|
|
105
|
+
}
|
|
106
|
+
return registry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Access other APIs by name
|
|
111
|
+
*/
|
|
112
|
+
get apis() {
|
|
113
|
+
const self = this;
|
|
114
|
+
return new Proxy({}, {
|
|
115
|
+
get(target, prop) {
|
|
116
|
+
// Get a compatible API based on current version
|
|
117
|
+
return Api.get(prop, self.options.version);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Add a plugin to the API
|
|
124
|
+
*/
|
|
125
|
+
use(plugin, options = {}) {
|
|
126
|
+
if (typeof plugin.install === 'function') {
|
|
127
|
+
plugin.install(this, options);
|
|
128
|
+
}
|
|
129
|
+
this.plugins.push({ plugin, options });
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if a plugin is already installed
|
|
135
|
+
*/
|
|
136
|
+
hasPlugin(plugin) {
|
|
137
|
+
return this.plugins.some(p => p.plugin === plugin);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Register a hook handler
|
|
142
|
+
*/
|
|
143
|
+
hook(name, handler, priority = 50) {
|
|
144
|
+
if (!this.hooks.has(name)) {
|
|
145
|
+
this.hooks.set(name, []);
|
|
146
|
+
}
|
|
147
|
+
const handlers = this.hooks.get(name);
|
|
148
|
+
handlers.push({ handler, priority });
|
|
149
|
+
handlers.sort((a, b) => a.priority - b.priority);
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Execute hooks
|
|
155
|
+
*/
|
|
156
|
+
async executeHook(name, context) {
|
|
157
|
+
const handlers = this.hooks.get(name) || [];
|
|
158
|
+
for (const { handler } of handlers) {
|
|
159
|
+
const result = await handler(context);
|
|
160
|
+
if (result === false) break; // Allow hooks to stop the chain
|
|
161
|
+
}
|
|
162
|
+
return context;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Register an implementation for a method
|
|
167
|
+
*/
|
|
168
|
+
implement(method, handler) {
|
|
169
|
+
this.implementers.set(method, handler);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Initialize core hooks
|
|
175
|
+
*/
|
|
176
|
+
_initializeHooks() {
|
|
177
|
+
const hookNames = [
|
|
178
|
+
'beforeValidate',
|
|
179
|
+
'afterValidate',
|
|
180
|
+
'beforeGet',
|
|
181
|
+
'afterGet',
|
|
182
|
+
'beforeQuery',
|
|
183
|
+
'afterQuery',
|
|
184
|
+
'beforeInsert',
|
|
185
|
+
'afterInsert',
|
|
186
|
+
'beforeUpdate',
|
|
187
|
+
'afterUpdate',
|
|
188
|
+
'beforeDelete',
|
|
189
|
+
'afterDelete',
|
|
190
|
+
'beforeSend',
|
|
191
|
+
'transformResult'
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
hookNames.forEach(name => this.hooks.set(name, []));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get a single resource
|
|
199
|
+
*/
|
|
200
|
+
async get(id, options = {}) {
|
|
201
|
+
const context = {
|
|
202
|
+
api: this,
|
|
203
|
+
method: 'get',
|
|
204
|
+
id,
|
|
205
|
+
options: { ...this.options, ...options },
|
|
206
|
+
result: null,
|
|
207
|
+
errors: []
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Execute before hook
|
|
211
|
+
await this.executeHook('beforeGet', context);
|
|
212
|
+
|
|
213
|
+
// Get the implementation
|
|
214
|
+
const impl = this.implementers.get('get');
|
|
215
|
+
if (!impl) {
|
|
216
|
+
throw new Error('No implementation for get method');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Execute the implementation
|
|
220
|
+
context.result = await impl(context);
|
|
221
|
+
|
|
222
|
+
// Execute after hook
|
|
223
|
+
await this.executeHook('afterGet', context);
|
|
224
|
+
|
|
225
|
+
// Transform result
|
|
226
|
+
await this.executeHook('transformResult', context);
|
|
227
|
+
|
|
228
|
+
return this._formatResponse(context);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Query multiple resources
|
|
233
|
+
*/
|
|
234
|
+
async query(params = {}, options = {}) {
|
|
235
|
+
const context = {
|
|
236
|
+
api: this,
|
|
237
|
+
method: 'query',
|
|
238
|
+
params,
|
|
239
|
+
options: { ...this.options, ...options },
|
|
240
|
+
results: [],
|
|
241
|
+
meta: {},
|
|
242
|
+
errors: []
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Execute before hook
|
|
246
|
+
await this.executeHook('beforeQuery', context);
|
|
247
|
+
|
|
248
|
+
// Get the implementation
|
|
249
|
+
const impl = this.implementers.get('query');
|
|
250
|
+
if (!impl) {
|
|
251
|
+
throw new Error('No implementation for query method');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Execute the implementation
|
|
255
|
+
const queryResult = await impl(context);
|
|
256
|
+
context.results = queryResult.results || [];
|
|
257
|
+
context.meta = queryResult.meta || {};
|
|
258
|
+
|
|
259
|
+
// Execute after hook
|
|
260
|
+
await this.executeHook('afterQuery', context);
|
|
261
|
+
|
|
262
|
+
// Transform results
|
|
263
|
+
for (let i = 0; i < context.results.length; i++) {
|
|
264
|
+
const itemContext = { ...context, result: context.results[i] };
|
|
265
|
+
await this.executeHook('transformResult', itemContext);
|
|
266
|
+
context.results[i] = itemContext.result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return this._formatQueryResponse(context);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Insert a new resource
|
|
274
|
+
*/
|
|
275
|
+
async insert(data, options = {}) {
|
|
276
|
+
const context = {
|
|
277
|
+
api: this,
|
|
278
|
+
method: 'insert',
|
|
279
|
+
data,
|
|
280
|
+
options: { ...this.options, ...options },
|
|
281
|
+
result: null,
|
|
282
|
+
errors: []
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Validation
|
|
286
|
+
await this.executeHook('beforeValidate', context);
|
|
287
|
+
await this.executeHook('afterValidate', context);
|
|
288
|
+
|
|
289
|
+
if (context.errors.length > 0) {
|
|
290
|
+
throw this._createValidationError(context.errors);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Execute before hook
|
|
294
|
+
await this.executeHook('beforeInsert', context);
|
|
295
|
+
|
|
296
|
+
// Get the implementation
|
|
297
|
+
const impl = this.implementers.get('insert');
|
|
298
|
+
if (!impl) {
|
|
299
|
+
throw new Error('No implementation for insert method');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Execute the implementation
|
|
303
|
+
context.result = await impl(context);
|
|
304
|
+
|
|
305
|
+
// Execute after hook
|
|
306
|
+
await this.executeHook('afterInsert', context);
|
|
307
|
+
|
|
308
|
+
// Transform result
|
|
309
|
+
await this.executeHook('transformResult', context);
|
|
310
|
+
|
|
311
|
+
return this._formatResponse(context);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Update an existing resource
|
|
316
|
+
*/
|
|
317
|
+
async update(id, data, options = {}) {
|
|
318
|
+
const context = {
|
|
319
|
+
api: this,
|
|
320
|
+
method: 'update',
|
|
321
|
+
id,
|
|
322
|
+
data,
|
|
323
|
+
options: { ...this.options, ...options },
|
|
324
|
+
result: null,
|
|
325
|
+
errors: []
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Validation
|
|
329
|
+
await this.executeHook('beforeValidate', context);
|
|
330
|
+
await this.executeHook('afterValidate', context);
|
|
331
|
+
|
|
332
|
+
if (context.errors.length > 0) {
|
|
333
|
+
throw this._createValidationError(context.errors);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Execute before hook
|
|
337
|
+
await this.executeHook('beforeUpdate', context);
|
|
338
|
+
|
|
339
|
+
// Get the implementation
|
|
340
|
+
const impl = this.implementers.get('update');
|
|
341
|
+
if (!impl) {
|
|
342
|
+
throw new Error('No implementation for update method');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Execute the implementation
|
|
346
|
+
context.result = await impl(context);
|
|
347
|
+
|
|
348
|
+
// Execute after hook
|
|
349
|
+
await this.executeHook('afterUpdate', context);
|
|
350
|
+
|
|
351
|
+
// Transform result
|
|
352
|
+
await this.executeHook('transformResult', context);
|
|
353
|
+
|
|
354
|
+
return this._formatResponse(context);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Delete a resource
|
|
359
|
+
*/
|
|
360
|
+
async delete(id, options = {}) {
|
|
361
|
+
const context = {
|
|
362
|
+
api: this,
|
|
363
|
+
method: 'delete',
|
|
364
|
+
id,
|
|
365
|
+
options: { ...this.options, ...options },
|
|
366
|
+
result: null,
|
|
367
|
+
errors: []
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// Execute before hook
|
|
371
|
+
await this.executeHook('beforeDelete', context);
|
|
372
|
+
|
|
373
|
+
// Get the implementation
|
|
374
|
+
const impl = this.implementers.get('delete');
|
|
375
|
+
if (!impl) {
|
|
376
|
+
throw new Error('No implementation for delete method');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Execute the implementation
|
|
380
|
+
await impl(context);
|
|
381
|
+
|
|
382
|
+
// Execute after hook
|
|
383
|
+
await this.executeHook('afterDelete', context);
|
|
384
|
+
|
|
385
|
+
return { data: null };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Format a single resource response
|
|
390
|
+
*/
|
|
391
|
+
_formatResponse(context) {
|
|
392
|
+
if (!context.result) {
|
|
393
|
+
return { data: null };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
data: this._formatResource(context.result, context.options.type)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Format a query response
|
|
403
|
+
*/
|
|
404
|
+
_formatQueryResponse(context) {
|
|
405
|
+
return {
|
|
406
|
+
data: context.results.map(item =>
|
|
407
|
+
this._formatResource(item, context.options.type)
|
|
408
|
+
),
|
|
409
|
+
meta: context.meta,
|
|
410
|
+
links: context.links || {}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Format a resource according to JSON:API
|
|
416
|
+
*/
|
|
417
|
+
_formatResource(resource, type) {
|
|
418
|
+
if (!resource) return null;
|
|
419
|
+
|
|
420
|
+
const { [this.options.idProperty]: id, ...attributes } = resource;
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
id: String(id),
|
|
424
|
+
type: type || 'resource',
|
|
425
|
+
attributes
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Create a validation error
|
|
431
|
+
*/
|
|
432
|
+
_createValidationError(errors) {
|
|
433
|
+
const error = new Error('Validation failed');
|
|
434
|
+
error.status = 422;
|
|
435
|
+
error.errors = errors.map(err => ({
|
|
436
|
+
status: '422',
|
|
437
|
+
title: 'Validation Error',
|
|
438
|
+
detail: err.message,
|
|
439
|
+
source: { pointer: `/data/attributes/${err.field}` }
|
|
440
|
+
}));
|
|
441
|
+
return error;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Create a search schema from an existing schema
|
|
446
|
+
*/
|
|
447
|
+
createSearchSchema(schema, fields) {
|
|
448
|
+
const searchStructure = {};
|
|
449
|
+
|
|
450
|
+
// Include only specified fields
|
|
451
|
+
fields.forEach(field => {
|
|
452
|
+
if (schema.structure[field]) {
|
|
453
|
+
searchStructure[field] = { ...schema.structure[field] };
|
|
454
|
+
// Make search fields optional
|
|
455
|
+
delete searchStructure[field].required;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return new schema.constructor(searchStructure);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Register a resource type with schema and hooks
|
|
464
|
+
*/
|
|
465
|
+
addResource(type, schema, hooks = {}) {
|
|
466
|
+
if (!type || typeof type !== 'string') {
|
|
467
|
+
throw new Error('Resource type must be a non-empty string');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!schema || typeof schema.validate !== 'function') {
|
|
471
|
+
throw new Error('Schema must have a validate method');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!this.schemas) {
|
|
475
|
+
this.schemas = new Map();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!this.resourceHooks) {
|
|
479
|
+
this.resourceHooks = new Map();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!this._resourceProxies) {
|
|
483
|
+
this._resourceProxies = new Map();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.schemas.set(type, schema);
|
|
487
|
+
|
|
488
|
+
// Register hooks for this resource type
|
|
489
|
+
if (hooks && typeof hooks === 'object') {
|
|
490
|
+
for (const [hookName, handler] of Object.entries(hooks)) {
|
|
491
|
+
if (typeof handler === 'function') {
|
|
492
|
+
// Create a wrapper that only runs for this resource type
|
|
493
|
+
this.hook(hookName, async (context) => {
|
|
494
|
+
if (context.options.type === type) {
|
|
495
|
+
await handler.call(this, context);
|
|
496
|
+
}
|
|
497
|
+
}, 10); // Higher priority to run before other hooks
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
this.resourceHooks.set(type, hooks);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Create resource proxy for intuitive access
|
|
504
|
+
const resourceProxy = this._createResourceProxy(type);
|
|
505
|
+
this._resourceProxies.set(type, resourceProxy);
|
|
506
|
+
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Create a resource proxy for intuitive API access
|
|
512
|
+
*/
|
|
513
|
+
_createResourceProxy(type) {
|
|
514
|
+
const api = this;
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
// Get a single resource
|
|
518
|
+
get: (id, options = {}) => api.get(id, { ...options, type }),
|
|
519
|
+
|
|
520
|
+
// Query resources
|
|
521
|
+
query: (params = {}, options = {}) => api.query(params, { ...options, type }),
|
|
522
|
+
|
|
523
|
+
// Create a new resource
|
|
524
|
+
post: (data, options = {}) => api.insert(data, { ...options, type }),
|
|
525
|
+
create: (data, options = {}) => api.insert(data, { ...options, type }), // Alias
|
|
526
|
+
|
|
527
|
+
// Update a resource
|
|
528
|
+
put: (id, data, options = {}) => api.update(id, data, { ...options, type }),
|
|
529
|
+
update: (id, data, options = {}) => api.update(id, data, { ...options, type }), // Alias
|
|
530
|
+
|
|
531
|
+
// Delete a resource
|
|
532
|
+
delete: (id, options = {}) => api.delete(id, { ...options, type }),
|
|
533
|
+
remove: (id, options = {}) => api.delete(id, { ...options, type }), // Alias
|
|
534
|
+
|
|
535
|
+
// Access schema
|
|
536
|
+
get schema() {
|
|
537
|
+
return api.schemas.get(type);
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
// Access hooks
|
|
541
|
+
get hooks() {
|
|
542
|
+
return api.resourceHooks.get(type);
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
// Versioned access
|
|
546
|
+
version: (ver) => {
|
|
547
|
+
// Get or create versioned API
|
|
548
|
+
const versionedApi = Api.get(api.options.name, ver);
|
|
549
|
+
if (!versionedApi) {
|
|
550
|
+
throw new Error(`No API version ${ver} found for ${api.options.name}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Return the resource proxy from the versioned API
|
|
554
|
+
const versionedResource = versionedApi._resourceProxies?.get(type);
|
|
555
|
+
if (!versionedResource) {
|
|
556
|
+
throw new Error(`Resource ${type} not found in API version ${ver}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return versionedResource;
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
// Batch operations
|
|
563
|
+
batch: {
|
|
564
|
+
create: (items, options = {}) =>
|
|
565
|
+
Promise.all(items.map(item => api.post(item, { ...options, type }))),
|
|
566
|
+
|
|
567
|
+
update: (updates, options = {}) =>
|
|
568
|
+
Promise.all(updates.map(({ id, data }) =>
|
|
569
|
+
api.update(id, data, { ...options, type })
|
|
570
|
+
)),
|
|
571
|
+
|
|
572
|
+
delete: (ids, options = {}) =>
|
|
573
|
+
Promise.all(ids.map(id => api.delete(id, { ...options, type })))
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get resources accessor
|
|
580
|
+
*/
|
|
581
|
+
get resources() {
|
|
582
|
+
const self = this;
|
|
583
|
+
|
|
584
|
+
if (!this._resourcesProxy) {
|
|
585
|
+
this._resourcesProxy = new Proxy({}, {
|
|
586
|
+
get(target, prop) {
|
|
587
|
+
// Check if resource exists
|
|
588
|
+
const resourceProxy = self._resourceProxies?.get(prop);
|
|
589
|
+
if (!resourceProxy) {
|
|
590
|
+
throw new Error(`Resource '${prop}' not found. Did you forget to call addResource('${prop}', schema)?`);
|
|
591
|
+
}
|
|
592
|
+
return resourceProxy;
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
has(target, prop) {
|
|
596
|
+
return self._resourceProxies?.has(prop) || false;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
ownKeys(target) {
|
|
600
|
+
return Array.from(self._resourceProxies?.keys() || []);
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
604
|
+
if (self._resourceProxies?.has(prop)) {
|
|
605
|
+
return {
|
|
606
|
+
enumerable: true,
|
|
607
|
+
configurable: true,
|
|
608
|
+
value: self._resourceProxies.get(prop)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return this._resourcesProxy;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Alternative syntax - direct property access
|
|
620
|
+
*/
|
|
621
|
+
resource(type) {
|
|
622
|
+
const resourceProxy = this._resourceProxies?.get(type);
|
|
623
|
+
if (!resourceProxy) {
|
|
624
|
+
throw new Error(`Resource '${type}' not found. Did you forget to call addResource('${type}', schema)?`);
|
|
625
|
+
}
|
|
626
|
+
return resourceProxy;
|
|
627
|
+
}
|
|
628
|
+
}
|