qdadm 0.52.2 → 0.53.1
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/package.json +1 -1
- package/src/entity/EntityManager.js +144 -50
- package/src/entity/storage/ApiStorage.js +29 -15
package/package.json
CHANGED
|
@@ -324,46 +324,110 @@ export class EntityManager {
|
|
|
324
324
|
* @param {string} method - Operation: 'list', 'get', 'create', 'update', 'delete'
|
|
325
325
|
* @param {object} [context] - Routing context
|
|
326
326
|
* @param {Array<{entity: string, id: string|number}>} [context.parentChain] - Parent chain from root to direct parent
|
|
327
|
-
* @returns {{ storage
|
|
327
|
+
* @returns {string | function | { storage?: IStorage, endpoint?: string | function, params?: object }}
|
|
328
|
+
* - `string`: Full endpoint URL (uses primary storage)
|
|
329
|
+
* - `function`: Dynamic endpoint builder `(context) => string` (marks as dynamic)
|
|
330
|
+
* - `{ endpoint }`: Full endpoint URL (uses primary storage)
|
|
331
|
+
* - `{ endpoint: function }`: Dynamic endpoint builder (marks as dynamic)
|
|
332
|
+
* - `{ endpoint, params }`: Endpoint with default query params (merged with request params)
|
|
333
|
+
* - `{ storage, endpoint }`: Custom storage with full endpoint
|
|
334
|
+
* - `{ storage }`: Custom storage with its default endpoint
|
|
335
|
+
* - `undefined/null`: Use primary storage with its default endpoint
|
|
328
336
|
*
|
|
329
337
|
* @example
|
|
330
338
|
* // Single storage (default, no change needed)
|
|
331
|
-
* manager.resolveStorage('list') //
|
|
339
|
+
* manager.resolveStorage('list') // undefined = use default storage
|
|
332
340
|
*
|
|
333
341
|
* @example
|
|
334
|
-
* //
|
|
335
|
-
*
|
|
342
|
+
* // Endpoint override (simplest, recommended)
|
|
343
|
+
* resolveStorage(method, context) {
|
|
344
|
+
* const parent = context?.parentChain?.at(-1)
|
|
345
|
+
* if (parent?.entity === 'bots') {
|
|
346
|
+
* return `/api/admin/bots/${parent.id}/commands`
|
|
347
|
+
* }
|
|
348
|
+
* }
|
|
336
349
|
*
|
|
337
350
|
* @example
|
|
338
|
-
* //
|
|
351
|
+
* // Endpoint with default query params
|
|
339
352
|
* resolveStorage(method, context) {
|
|
340
353
|
* const parent = context?.parentChain?.at(-1)
|
|
341
354
|
* if (parent?.entity === 'bots') {
|
|
342
355
|
* return {
|
|
343
|
-
*
|
|
344
|
-
*
|
|
356
|
+
* endpoint: `/api/admin/bots/${parent.id}/commands`,
|
|
357
|
+
* params: { include: 'bot', status: 'pending' }
|
|
345
358
|
* }
|
|
346
359
|
* }
|
|
347
|
-
* return { storage: this.storage }
|
|
348
360
|
* }
|
|
349
361
|
*
|
|
350
362
|
* @example
|
|
351
|
-
* //
|
|
363
|
+
* // Dynamic endpoint builder (marks endpoint as context-dependent for debug panel)
|
|
352
364
|
* resolveStorage(method, context) {
|
|
353
|
-
* const
|
|
354
|
-
* if (
|
|
365
|
+
* const parent = context?.parentChain?.at(-1)
|
|
366
|
+
* if (parent?.entity === 'bots') {
|
|
367
|
+
* return (ctx) => `/api/admin/bots/${ctx.parentChain.at(-1).id}/commands`
|
|
368
|
+
* }
|
|
369
|
+
* }
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* // Different storage with endpoint
|
|
373
|
+
* resolveStorage(method, context) {
|
|
374
|
+
* if (context?.parentChain?.at(-1)?.entity === 'projects') {
|
|
355
375
|
* return {
|
|
356
|
-
* storage: this.
|
|
357
|
-
*
|
|
376
|
+
* storage: this.projectStorage,
|
|
377
|
+
* endpoint: `/api/projects/${parent.id}/tasks`
|
|
358
378
|
* }
|
|
359
379
|
* }
|
|
360
|
-
* return { storage: this.storage }
|
|
361
380
|
* }
|
|
362
381
|
*/
|
|
363
382
|
resolveStorage(method, context) {
|
|
364
383
|
return { storage: this.storage }
|
|
365
384
|
}
|
|
366
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Normalize resolveStorage() return value to standard format
|
|
388
|
+
* @param {string | function | object} result - Result from resolveStorage()
|
|
389
|
+
* @param {object} [context] - Context to pass to endpoint builder function
|
|
390
|
+
* @returns {{ storage: IStorage, endpoint?: string, params?: object, isDynamic?: boolean }}
|
|
391
|
+
* @private
|
|
392
|
+
*/
|
|
393
|
+
_normalizeResolveResult(result, context) {
|
|
394
|
+
// Function = dynamic endpoint builder
|
|
395
|
+
if (typeof result === 'function') {
|
|
396
|
+
return {
|
|
397
|
+
storage: this.storage,
|
|
398
|
+
endpoint: result(context),
|
|
399
|
+
isDynamic: true
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// String = endpoint with primary storage
|
|
404
|
+
if (typeof result === 'string') {
|
|
405
|
+
return { storage: this.storage, endpoint: result }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Null/undefined = primary storage
|
|
409
|
+
if (!result) {
|
|
410
|
+
return { storage: this.storage }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Object with endpoint function = dynamic endpoint builder
|
|
414
|
+
if (typeof result.endpoint === 'function') {
|
|
415
|
+
return {
|
|
416
|
+
storage: result.storage || this.storage,
|
|
417
|
+
endpoint: result.endpoint(context),
|
|
418
|
+
params: result.params,
|
|
419
|
+
isDynamic: true
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Object without storage = use primary storage
|
|
424
|
+
if (!result.storage) {
|
|
425
|
+
return { storage: this.storage, ...result }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return result
|
|
429
|
+
}
|
|
430
|
+
|
|
367
431
|
// ============ METADATA ACCESSORS ============
|
|
368
432
|
|
|
369
433
|
/**
|
|
@@ -712,14 +776,15 @@ export class EntityManager {
|
|
|
712
776
|
|
|
713
777
|
/**
|
|
714
778
|
* Get initial data for a new entity based on field defaults
|
|
779
|
+
* @param {object} [context] - Routing context passed to default functions
|
|
715
780
|
* @returns {object}
|
|
716
781
|
*/
|
|
717
|
-
getInitialData() {
|
|
782
|
+
getInitialData(context = null) {
|
|
718
783
|
const data = {}
|
|
719
784
|
for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
|
|
720
785
|
if (fieldConfig.default !== undefined) {
|
|
721
786
|
data[fieldName] = typeof fieldConfig.default === 'function'
|
|
722
|
-
? fieldConfig.default()
|
|
787
|
+
? fieldConfig.default(context)
|
|
723
788
|
: fieldConfig.default
|
|
724
789
|
} else {
|
|
725
790
|
// Type-based defaults
|
|
@@ -744,6 +809,26 @@ export class EntityManager {
|
|
|
744
809
|
return data
|
|
745
810
|
}
|
|
746
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Apply field defaults to data (for create operations)
|
|
814
|
+
* Only applies defaults for fields that are undefined in data.
|
|
815
|
+
* @param {object} data - Provided data
|
|
816
|
+
* @param {object} [context] - Routing context passed to default functions
|
|
817
|
+
* @returns {object} - Data with defaults applied
|
|
818
|
+
*/
|
|
819
|
+
applyDefaults(data, context = null) {
|
|
820
|
+
const result = { ...data }
|
|
821
|
+
for (const [fieldName, fieldConfig] of Object.entries(this._fields)) {
|
|
822
|
+
// Only apply default if field is undefined in data
|
|
823
|
+
if (result[fieldName] === undefined && fieldConfig.default !== undefined) {
|
|
824
|
+
result[fieldName] = typeof fieldConfig.default === 'function'
|
|
825
|
+
? fieldConfig.default(context)
|
|
826
|
+
: fieldConfig.default
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return result
|
|
830
|
+
}
|
|
831
|
+
|
|
747
832
|
/**
|
|
748
833
|
* Get field names that are required
|
|
749
834
|
* @returns {string[]}
|
|
@@ -931,7 +1016,8 @@ export class EntityManager {
|
|
|
931
1016
|
* @returns {Promise<{ items: Array, total: number, fromCache: boolean }>}
|
|
932
1017
|
*/
|
|
933
1018
|
async list(params = {}, context) {
|
|
934
|
-
const
|
|
1019
|
+
const resolved = this._normalizeResolveResult(this.resolveStorage('list', context), context)
|
|
1020
|
+
const { storage, endpoint, params: resolvedParams } = resolved
|
|
935
1021
|
if (!storage) {
|
|
936
1022
|
throw new Error(`[EntityManager:${this.name}] list() not implemented`)
|
|
937
1023
|
}
|
|
@@ -939,19 +1025,22 @@ export class EntityManager {
|
|
|
939
1025
|
// Extract internal flag and cacheSafe flag
|
|
940
1026
|
const { _internal = false, cacheSafe = false, ...queryParams } = params
|
|
941
1027
|
|
|
1028
|
+
// Merge resolved params (defaults) with query params (overrides)
|
|
1029
|
+
const mergedParams = resolvedParams ? { ...resolvedParams, ...queryParams } : queryParams
|
|
1030
|
+
|
|
942
1031
|
// Only count stats for non-internal operations
|
|
943
1032
|
if (!_internal) {
|
|
944
1033
|
this._stats.list++
|
|
945
1034
|
}
|
|
946
1035
|
|
|
947
|
-
const hasFilters =
|
|
1036
|
+
const hasFilters = mergedParams.search || Object.keys(mergedParams.filters || {}).length > 0
|
|
948
1037
|
const canUseCache = !hasFilters || cacheSafe
|
|
949
1038
|
|
|
950
1039
|
// 1. Cache valid + cacheable → use cache with local filtering
|
|
951
1040
|
if (this._cache.valid && canUseCache) {
|
|
952
1041
|
if (!_internal) this._stats.cacheHits++
|
|
953
1042
|
console.log('[cache] Using local cache for entity:', this.name)
|
|
954
|
-
const filtered = this._filterLocally(this._cache.items,
|
|
1043
|
+
const filtered = this._filterLocally(this._cache.items, mergedParams)
|
|
955
1044
|
// Update max stats
|
|
956
1045
|
if (filtered.items.length > this._stats.maxItemsSeen) {
|
|
957
1046
|
this._stats.maxItemsSeen = filtered.items.length
|
|
@@ -966,10 +1055,10 @@ export class EntityManager {
|
|
|
966
1055
|
|
|
967
1056
|
// 2. Fetch from API
|
|
968
1057
|
let response
|
|
969
|
-
if (
|
|
970
|
-
// Use request() with
|
|
971
|
-
// Build query params for GET request
|
|
972
|
-
const apiResponse = await storage.request('GET',
|
|
1058
|
+
if (endpoint && storage.request) {
|
|
1059
|
+
// Use request() with endpoint for multi-storage routing
|
|
1060
|
+
// Build query params for GET request, pass context for normalize()
|
|
1061
|
+
const apiResponse = await storage.request('GET', endpoint, { params: mergedParams, context })
|
|
973
1062
|
// Normalize response: handle both { data: [...], pagination: {...} } and { items, total }
|
|
974
1063
|
const data = apiResponse.data ?? apiResponse
|
|
975
1064
|
response = {
|
|
@@ -978,7 +1067,7 @@ export class EntityManager {
|
|
|
978
1067
|
}
|
|
979
1068
|
} else {
|
|
980
1069
|
// Standard storage.list() (normalizes response to { items, total })
|
|
981
|
-
response = await storage.list(
|
|
1070
|
+
response = await storage.list(mergedParams, context)
|
|
982
1071
|
}
|
|
983
1072
|
const items = response.items || []
|
|
984
1073
|
const total = response.total ?? items.length
|
|
@@ -1027,7 +1116,7 @@ export class EntityManager {
|
|
|
1027
1116
|
* @returns {Promise<object>}
|
|
1028
1117
|
*/
|
|
1029
1118
|
async get(id, context) {
|
|
1030
|
-
const { storage,
|
|
1119
|
+
const { storage, endpoint } = this._normalizeResolveResult(this.resolveStorage('get', context), context)
|
|
1031
1120
|
this._stats.get++
|
|
1032
1121
|
|
|
1033
1122
|
// Try cache first if valid and complete
|
|
@@ -1043,12 +1132,12 @@ export class EntityManager {
|
|
|
1043
1132
|
// Fallback to storage
|
|
1044
1133
|
this._stats.cacheMisses++
|
|
1045
1134
|
if (storage) {
|
|
1046
|
-
// Use request() with
|
|
1047
|
-
if (
|
|
1048
|
-
const response = await storage.request('GET', `${
|
|
1135
|
+
// Use request() with endpoint for multi-storage routing, otherwise use get()
|
|
1136
|
+
if (endpoint && storage.request) {
|
|
1137
|
+
const response = await storage.request('GET', `${endpoint}/${id}`, { context })
|
|
1049
1138
|
return response.data ?? response
|
|
1050
1139
|
}
|
|
1051
|
-
return storage.get(id)
|
|
1140
|
+
return storage.get(id, context)
|
|
1052
1141
|
}
|
|
1053
1142
|
throw new Error(`[EntityManager:${this.name}] get() not implemented`)
|
|
1054
1143
|
}
|
|
@@ -1060,9 +1149,10 @@ export class EntityManager {
|
|
|
1060
1149
|
* Otherwise fetches from storage (or parallel get() calls).
|
|
1061
1150
|
*
|
|
1062
1151
|
* @param {Array<string|number>} ids
|
|
1152
|
+
* @param {object} [context] - Routing context for multi-storage
|
|
1063
1153
|
* @returns {Promise<Array<object>>}
|
|
1064
1154
|
*/
|
|
1065
|
-
async getMany(ids) {
|
|
1155
|
+
async getMany(ids, context) {
|
|
1066
1156
|
if (!ids || ids.length === 0) return []
|
|
1067
1157
|
|
|
1068
1158
|
// Try cache first if valid and complete
|
|
@@ -1082,12 +1172,13 @@ export class EntityManager {
|
|
|
1082
1172
|
|
|
1083
1173
|
this._stats.cacheMisses += ids.length
|
|
1084
1174
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1175
|
+
const { storage } = this._normalizeResolveResult(this.resolveStorage('getMany', context), context)
|
|
1176
|
+
if (storage?.getMany) {
|
|
1177
|
+
return storage.getMany(ids, context)
|
|
1087
1178
|
}
|
|
1088
1179
|
// Fallback: parallel get calls (get() handles its own stats)
|
|
1089
1180
|
this._stats.cacheMisses -= ids.length // Avoid double counting
|
|
1090
|
-
return Promise.all(ids.map(id => this.get(id).catch(() => null)))
|
|
1181
|
+
return Promise.all(ids.map(id => this.get(id, context).catch(() => null)))
|
|
1091
1182
|
.then(results => results.filter(Boolean))
|
|
1092
1183
|
}
|
|
1093
1184
|
|
|
@@ -1103,17 +1194,20 @@ export class EntityManager {
|
|
|
1103
1194
|
* @returns {Promise<object>} - The created entity
|
|
1104
1195
|
*/
|
|
1105
1196
|
async create(data, context) {
|
|
1106
|
-
const { storage,
|
|
1197
|
+
const { storage, endpoint } = this._normalizeResolveResult(this.resolveStorage('create', context), context)
|
|
1107
1198
|
this._stats.create++
|
|
1108
1199
|
if (storage) {
|
|
1200
|
+
// Apply field defaults before presave hooks
|
|
1201
|
+
const dataWithDefaults = this.applyDefaults(data, context)
|
|
1202
|
+
|
|
1109
1203
|
// Invoke presave hooks (can modify data or throw to abort)
|
|
1110
|
-
const presaveContext = this._buildPresaveContext(
|
|
1204
|
+
const presaveContext = this._buildPresaveContext(dataWithDefaults, true)
|
|
1111
1205
|
await this._invokeHook('presave', presaveContext)
|
|
1112
1206
|
|
|
1113
|
-
// Use request() with
|
|
1207
|
+
// Use request() with endpoint for multi-storage routing, otherwise use create()
|
|
1114
1208
|
let result
|
|
1115
|
-
if (
|
|
1116
|
-
const response = await storage.request('POST',
|
|
1209
|
+
if (endpoint && storage.request) {
|
|
1210
|
+
const response = await storage.request('POST', endpoint, { data: presaveContext.record, context })
|
|
1117
1211
|
result = response.data ?? response
|
|
1118
1212
|
} else {
|
|
1119
1213
|
result = await storage.create(presaveContext.record)
|
|
@@ -1148,17 +1242,17 @@ export class EntityManager {
|
|
|
1148
1242
|
* @returns {Promise<object>}
|
|
1149
1243
|
*/
|
|
1150
1244
|
async update(id, data, context) {
|
|
1151
|
-
const { storage,
|
|
1245
|
+
const { storage, endpoint } = this._normalizeResolveResult(this.resolveStorage('update', context), context)
|
|
1152
1246
|
this._stats.update++
|
|
1153
1247
|
if (storage) {
|
|
1154
1248
|
// Invoke presave hooks (can modify data or throw to abort)
|
|
1155
1249
|
const presaveContext = this._buildPresaveContext(data, false, id)
|
|
1156
1250
|
await this._invokeHook('presave', presaveContext)
|
|
1157
1251
|
|
|
1158
|
-
// Use request() with
|
|
1252
|
+
// Use request() with endpoint for multi-storage routing, otherwise use update()
|
|
1159
1253
|
let result
|
|
1160
|
-
if (
|
|
1161
|
-
const response = await storage.request('PUT', `${
|
|
1254
|
+
if (endpoint && storage.request) {
|
|
1255
|
+
const response = await storage.request('PUT', `${endpoint}/${id}`, { data: presaveContext.record, context })
|
|
1162
1256
|
result = response.data ?? response
|
|
1163
1257
|
} else {
|
|
1164
1258
|
result = await storage.update(id, presaveContext.record)
|
|
@@ -1193,17 +1287,17 @@ export class EntityManager {
|
|
|
1193
1287
|
* @returns {Promise<object>}
|
|
1194
1288
|
*/
|
|
1195
1289
|
async patch(id, data, context) {
|
|
1196
|
-
const { storage,
|
|
1290
|
+
const { storage, endpoint } = this._normalizeResolveResult(this.resolveStorage('patch', context), context)
|
|
1197
1291
|
this._stats.update++ // patch counts as update
|
|
1198
1292
|
if (storage) {
|
|
1199
1293
|
// Invoke presave hooks (can modify data or throw to abort)
|
|
1200
1294
|
const presaveContext = this._buildPresaveContext(data, false, id)
|
|
1201
1295
|
await this._invokeHook('presave', presaveContext)
|
|
1202
1296
|
|
|
1203
|
-
// Use request() with
|
|
1297
|
+
// Use request() with endpoint for multi-storage routing, otherwise use patch()
|
|
1204
1298
|
let result
|
|
1205
|
-
if (
|
|
1206
|
-
const response = await storage.request('PATCH', `${
|
|
1299
|
+
if (endpoint && storage.request) {
|
|
1300
|
+
const response = await storage.request('PATCH', `${endpoint}/${id}`, { data: presaveContext.record, context })
|
|
1207
1301
|
result = response.data ?? response
|
|
1208
1302
|
} else {
|
|
1209
1303
|
result = await storage.patch(id, presaveContext.record)
|
|
@@ -1236,17 +1330,17 @@ export class EntityManager {
|
|
|
1236
1330
|
* @returns {Promise<void>}
|
|
1237
1331
|
*/
|
|
1238
1332
|
async delete(id, context) {
|
|
1239
|
-
const { storage,
|
|
1333
|
+
const { storage, endpoint } = this._normalizeResolveResult(this.resolveStorage('delete', context), context)
|
|
1240
1334
|
this._stats.delete++
|
|
1241
1335
|
if (storage) {
|
|
1242
1336
|
// Invoke predelete hooks (can throw to abort, e.g., for cascade checks)
|
|
1243
1337
|
const predeleteContext = this._buildPredeleteContext(id)
|
|
1244
1338
|
await this._invokeHook('predelete', predeleteContext)
|
|
1245
1339
|
|
|
1246
|
-
// Use request() with
|
|
1340
|
+
// Use request() with endpoint for multi-storage routing, otherwise use delete()
|
|
1247
1341
|
let result
|
|
1248
|
-
if (
|
|
1249
|
-
result = await storage.request('DELETE', `${
|
|
1342
|
+
if (endpoint && storage.request) {
|
|
1343
|
+
result = await storage.request('DELETE', `${endpoint}/${id}`, { context })
|
|
1250
1344
|
} else {
|
|
1251
1345
|
result = await storage.delete(id)
|
|
1252
1346
|
}
|
|
@@ -114,14 +114,17 @@ export class ApiStorage extends IStorage {
|
|
|
114
114
|
/**
|
|
115
115
|
* Normalize API response data to internal format
|
|
116
116
|
* @param {object|Array} data - API response data
|
|
117
|
+
* @param {object} [context] - Routing context from EntityManager
|
|
118
|
+
* @param {Array<{entity: string, id: string}>} [context.parentChain] - Parent hierarchy
|
|
119
|
+
* @param {string} [context.path] - API path used for the request
|
|
117
120
|
* @returns {object|Array} - Normalized data
|
|
118
121
|
*/
|
|
119
|
-
_normalizeData(data) {
|
|
122
|
+
_normalizeData(data, context = null) {
|
|
120
123
|
if (!this._normalize) return data
|
|
121
124
|
if (Array.isArray(data)) {
|
|
122
|
-
return data.map(item => this._normalize(item))
|
|
125
|
+
return data.map(item => this._normalize(item, context))
|
|
123
126
|
}
|
|
124
|
-
return this._normalize(data)
|
|
127
|
+
return this._normalize(data, context)
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
/**
|
|
@@ -153,9 +156,11 @@ export class ApiStorage extends IStorage {
|
|
|
153
156
|
* @param {string} [params.sort_by] - Field to sort by
|
|
154
157
|
* @param {string} [params.sort_order='asc'] - Sort order ('asc' or 'desc')
|
|
155
158
|
* @param {object} [params.filters] - Field filters { field: value }
|
|
159
|
+
* @param {object} [context] - Routing context for normalize()
|
|
160
|
+
* @param {Array<{entity: string, id: string}>} [context.parentChain] - Parent hierarchy
|
|
156
161
|
* @returns {Promise<{ items: Array, total: number }>}
|
|
157
162
|
*/
|
|
158
|
-
async list(params = {}) {
|
|
163
|
+
async list(params = {}, context = null) {
|
|
159
164
|
const { page = 1, page_size = 20, sort_by, sort_order, filters = {} } = params
|
|
160
165
|
|
|
161
166
|
// WIP: Apply param mapping to filters
|
|
@@ -168,7 +173,7 @@ export class ApiStorage extends IStorage {
|
|
|
168
173
|
const data = response.data
|
|
169
174
|
const items = data[this.responseItemsKey] || data.items || data
|
|
170
175
|
return {
|
|
171
|
-
items: this._normalizeData(items),
|
|
176
|
+
items: this._normalizeData(items, context),
|
|
172
177
|
total: data[this.responseTotalKey] || data.total || (Array.isArray(data) ? data.length : 0)
|
|
173
178
|
}
|
|
174
179
|
}
|
|
@@ -176,11 +181,12 @@ export class ApiStorage extends IStorage {
|
|
|
176
181
|
/**
|
|
177
182
|
* Get a single entity by ID
|
|
178
183
|
* @param {string|number} id
|
|
184
|
+
* @param {object} [context] - Routing context for normalize()
|
|
179
185
|
* @returns {Promise<object>}
|
|
180
186
|
*/
|
|
181
|
-
async get(id) {
|
|
187
|
+
async get(id, context = null) {
|
|
182
188
|
const response = await this.client.get(`${this.endpoint}/${id}`)
|
|
183
|
-
return this._normalizeData(response.data)
|
|
189
|
+
return this._normalizeData(response.data, context)
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
/**
|
|
@@ -230,20 +236,28 @@ export class ApiStorage extends IStorage {
|
|
|
230
236
|
/**
|
|
231
237
|
* Generic request for special operations
|
|
232
238
|
* @param {string} method - 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'
|
|
233
|
-
* @param {string}
|
|
234
|
-
*
|
|
239
|
+
* @param {string} endpoint - Full endpoint URL (starts with /) or relative path
|
|
240
|
+
* - Full endpoint (/api/...): used as-is
|
|
241
|
+
* - Relative path (no leading /): appended to this.endpoint
|
|
242
|
+
* @param {object} options - Request options
|
|
243
|
+
* @param {object} [options.data] - Request body
|
|
244
|
+
* @param {object} [options.params] - Query parameters
|
|
245
|
+
* @param {object} [options.headers] - Additional headers
|
|
246
|
+
* @param {object} [options.context] - Routing context for normalize()
|
|
247
|
+
* @param {Array<{entity: string, id: string}>} [options.context.parentChain] - Parent hierarchy
|
|
235
248
|
* @returns {Promise<any>}
|
|
236
249
|
*/
|
|
237
|
-
async request(method,
|
|
238
|
-
const
|
|
250
|
+
async request(method, endpoint, options = {}) {
|
|
251
|
+
const { context, ...requestOptions } = options
|
|
252
|
+
const url = endpoint.startsWith('/') ? endpoint : `${this.endpoint}/${endpoint}`
|
|
239
253
|
const response = await this.client.request({
|
|
240
254
|
method,
|
|
241
255
|
url,
|
|
242
|
-
data:
|
|
243
|
-
params:
|
|
244
|
-
headers:
|
|
256
|
+
data: requestOptions.data,
|
|
257
|
+
params: requestOptions.params,
|
|
258
|
+
headers: requestOptions.headers
|
|
245
259
|
})
|
|
246
|
-
return response.data
|
|
260
|
+
return this._normalizeData(response.data, context)
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
|