json-rest-api 1.0.26 → 1.0.28
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/docs/API.md +5 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -2
- package/docs/GUIDE/GUIDE_Y_Appendices.md +42 -29
- package/index.js +2 -0
- package/lib/error-context.js +21 -0
- package/lib/rest-api-errors.js +70 -0
- package/package.json +2 -2
- package/plugins/core/connectors/lib/transport-http-helpers.js +21 -0
- package/plugins/core/lib/anyapi/schema-utils.js +2 -1
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -2
- package/plugins/core/lib/dbTablesOperations.js +20 -10
- package/plugins/core/lib/querying/knex-pagination-helpers.js +11 -1
- package/plugins/core/lib/querying/knex-process-includes.js +11 -5
- package/plugins/core/lib/querying/knex-relationship-includes.js +63 -46
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +231 -143
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -1
- package/plugins/core/lib/storage/canonical-storage-mapping.js +11 -1
- package/plugins/core/lib/storage/storage-adapter.js +4 -2
- package/plugins/core/lib/storage/storage-mapping.js +7 -1
- package/plugins/core/rest-api-plugin-methods/common.js +5 -0
- package/plugins/core/rest-api-plugin-methods/get.js +6 -0
- package/plugins/core/rest-api-plugin-methods/query.js +6 -0
- package/tests/computed-fields.test.js +90 -2
- package/tests/db-table-operations.test.js +13 -1
- package/tests/fixtures/api-configs.js +83 -3
- package/tests/includes.test.js +49 -0
- package/tests/pagination-cursor-multifield.test.js +4 -0
- package/tests/relationship-include-storage-adapter.test.js +56 -0
- package/tests/storage-mapping.test.js +112 -10
|
@@ -84,14 +84,26 @@ import {
|
|
|
84
84
|
import { unwrapQueryBuilderState } from './query-builder-utils.js'
|
|
85
85
|
import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, DEFAULT_QUERY_LIMIT } from '../querying-writing/knex-constants.js'
|
|
86
86
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
87
|
+
import { isRestApiError, wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
87
88
|
import {
|
|
88
89
|
getFieldValue as getStorageFieldValue,
|
|
89
90
|
getIdColumn as getStorageIdColumn,
|
|
90
91
|
getStorageColumn as getMappedStorageColumn,
|
|
91
92
|
} from '../storage/storage-mapping.js'
|
|
93
|
+
import { createStorageAdapter } from '../storage/storage-adapter.js'
|
|
92
94
|
|
|
93
95
|
const getScopeStorageAdapter = (scopes, scopeName) => scopes[scopeName]?.vars?.storageAdapter || null
|
|
94
96
|
|
|
97
|
+
const resolveScopeStorageAdapter = (scopes, scopeName, knex) => {
|
|
98
|
+
const configuredAdapter = getScopeStorageAdapter(scopes, scopeName)
|
|
99
|
+
if (configuredAdapter) return configuredAdapter
|
|
100
|
+
|
|
101
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
102
|
+
if (!schemaInfo) return null
|
|
103
|
+
|
|
104
|
+
return createStorageAdapter({ knex, schemaInfo })
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
const getIdColumnForScope = (scopes, scopeName) => {
|
|
96
108
|
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
97
109
|
if (adapter?.getIdColumn) {
|
|
@@ -161,7 +173,7 @@ const applyScopeFiltersToIncludeQuery = async ({
|
|
|
161
173
|
return { query }
|
|
162
174
|
}
|
|
163
175
|
|
|
164
|
-
const storageAdapter =
|
|
176
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, scopeName, db)
|
|
165
177
|
|
|
166
178
|
const queryState = await scopeObject.applyQueryFilters({
|
|
167
179
|
query,
|
|
@@ -415,14 +427,18 @@ const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
|
415
427
|
}
|
|
416
428
|
})
|
|
417
429
|
} catch (error) {
|
|
418
|
-
|
|
430
|
+
if (isRestApiError(error)) throw error
|
|
431
|
+
|
|
419
432
|
const errorContext = {
|
|
420
433
|
scopeName,
|
|
421
434
|
recordCount: records?.length || 0,
|
|
422
435
|
error: error.message
|
|
423
436
|
}
|
|
424
437
|
console.error('[loadRelationshipMetadata] Error loading relationship metadata:', errorContext)
|
|
425
|
-
throw
|
|
438
|
+
throw wrapUnexpectedError(error, {
|
|
439
|
+
message: `Failed to load relationship metadata for scope '${scopeName}'`,
|
|
440
|
+
context: errorContext
|
|
441
|
+
})
|
|
426
442
|
}
|
|
427
443
|
}
|
|
428
444
|
|
|
@@ -498,7 +514,7 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
498
514
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
499
515
|
// Build field selection for sparse fieldsets
|
|
500
516
|
const targetScopeObject = scopes[targetScope]
|
|
501
|
-
const storageAdapter =
|
|
517
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
502
518
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
503
519
|
? await buildFieldSelection(targetScopeObject, {
|
|
504
520
|
context: {
|
|
@@ -626,6 +642,8 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
626
642
|
}
|
|
627
643
|
}
|
|
628
644
|
} catch (error) {
|
|
645
|
+
if (isRestApiError(error)) throw error
|
|
646
|
+
|
|
629
647
|
// Log error with detailed context
|
|
630
648
|
log.error('[INCLUDE] Error loading belongsTo relationship:', {
|
|
631
649
|
fieldName,
|
|
@@ -636,13 +654,10 @@ export const loadBelongsTo = async (scope, deps) => {
|
|
|
636
654
|
stack: error.stack
|
|
637
655
|
})
|
|
638
656
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
)
|
|
643
|
-
enhancedError.originalError = error
|
|
644
|
-
enhancedError.context = { fieldName, includeName, targetScope: fieldDef.belongsTo }
|
|
645
|
-
throw enhancedError
|
|
657
|
+
throw wrapUnexpectedError(error, {
|
|
658
|
+
message: `Failed to load belongsTo relationship '${includeName}' for field '${fieldName}'`,
|
|
659
|
+
context: { fieldName, includeName, targetScope: fieldDef.belongsTo }
|
|
660
|
+
})
|
|
646
661
|
}
|
|
647
662
|
}
|
|
648
663
|
|
|
@@ -741,7 +756,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
741
756
|
|
|
742
757
|
// Step 3: Build field selection for sparse fieldsets
|
|
743
758
|
const targetScopeObject = scopes[targetScope]
|
|
744
|
-
const storageAdapter =
|
|
759
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
745
760
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
746
761
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
747
762
|
? await buildFieldSelection(targetScopeObject, {
|
|
@@ -970,7 +985,7 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
970
985
|
|
|
971
986
|
// Build field selection for sparse fieldsets
|
|
972
987
|
const targetScopeObject = scopes[targetScope]
|
|
973
|
-
const storageAdapter =
|
|
988
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
974
989
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
975
990
|
? await buildFieldSelection(targetScopeObject, {
|
|
976
991
|
context: {
|
|
@@ -1159,6 +1174,8 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1159
1174
|
}
|
|
1160
1175
|
}
|
|
1161
1176
|
} catch (error) {
|
|
1177
|
+
if (isRestApiError(error)) throw error
|
|
1178
|
+
|
|
1162
1179
|
// Log error with detailed context
|
|
1163
1180
|
log.error('[INCLUDE] Error loading hasMany relationship:', {
|
|
1164
1181
|
scopeName,
|
|
@@ -1169,13 +1186,10 @@ export const loadHasMany = async (scope, deps) => {
|
|
|
1169
1186
|
stack: error.stack
|
|
1170
1187
|
})
|
|
1171
1188
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
)
|
|
1176
|
-
enhancedError.originalError = error
|
|
1177
|
-
enhancedError.context = { scopeName, includeName, hasThrough: !!relDef.through }
|
|
1178
|
-
throw enhancedError
|
|
1189
|
+
throw wrapUnexpectedError(error, {
|
|
1190
|
+
message: `Failed to load hasMany relationship '${includeName}' for scope '${scopeName}'`,
|
|
1191
|
+
context: { scopeName, includeName, hasThrough: !!relDef.through }
|
|
1192
|
+
})
|
|
1179
1193
|
}
|
|
1180
1194
|
}
|
|
1181
1195
|
|
|
@@ -1221,7 +1235,7 @@ export const loadHasOne = async (scope, deps) => {
|
|
|
1221
1235
|
|
|
1222
1236
|
// Build query for hasOne - expects single result per parent
|
|
1223
1237
|
const targetScopeObject = scopes[targetScope]
|
|
1224
|
-
const storageAdapter =
|
|
1238
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
1225
1239
|
|
|
1226
1240
|
// Build field selection for sparse fieldsets
|
|
1227
1241
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
@@ -1393,7 +1407,7 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1393
1407
|
|
|
1394
1408
|
// Build field selection for sparse fieldsets
|
|
1395
1409
|
const targetScopeObject = scopes[targetType]
|
|
1396
|
-
const storageAdapter =
|
|
1410
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetType, knex)
|
|
1397
1411
|
const fieldSelectionInfo = fields?.[targetType]
|
|
1398
1412
|
? await buildFieldSelection(targetScopeObject, {
|
|
1399
1413
|
context: {
|
|
@@ -1539,6 +1553,8 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1539
1553
|
}
|
|
1540
1554
|
})
|
|
1541
1555
|
} catch (error) {
|
|
1556
|
+
if (isRestApiError(error)) throw error
|
|
1557
|
+
|
|
1542
1558
|
// Log error with detailed context
|
|
1543
1559
|
log.error('[INCLUDE] Error loading polymorphic belongsTo relationship:', {
|
|
1544
1560
|
relName,
|
|
@@ -1548,13 +1564,10 @@ export const loadPolymorphicBelongsTo = async (scope, deps) => {
|
|
|
1548
1564
|
stack: error.stack
|
|
1549
1565
|
})
|
|
1550
1566
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
)
|
|
1555
|
-
enhancedError.originalError = error
|
|
1556
|
-
enhancedError.context = { relName, types: relDef?.belongsToPolymorphic?.types }
|
|
1557
|
-
throw enhancedError
|
|
1567
|
+
throw wrapUnexpectedError(error, {
|
|
1568
|
+
message: `Failed to load polymorphic belongsTo relationship '${relName}'`,
|
|
1569
|
+
context: { relName, types: relDef?.belongsToPolymorphic?.types }
|
|
1570
|
+
})
|
|
1558
1571
|
}
|
|
1559
1572
|
}
|
|
1560
1573
|
|
|
@@ -1622,7 +1635,7 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1622
1635
|
|
|
1623
1636
|
// Build field selection for sparse fieldsets
|
|
1624
1637
|
const targetScopeObject = scopes[targetScope]
|
|
1625
|
-
const storageAdapter =
|
|
1638
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
1626
1639
|
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
1627
1640
|
const fieldSelectionInfo = fields?.[targetScope]
|
|
1628
1641
|
? await buildFieldSelection(targetScopeObject, {
|
|
@@ -1759,6 +1772,8 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1759
1772
|
}
|
|
1760
1773
|
}
|
|
1761
1774
|
} catch (error) {
|
|
1775
|
+
if (isRestApiError(error)) throw error
|
|
1776
|
+
|
|
1762
1777
|
// Log error with detailed context
|
|
1763
1778
|
log.error('[INCLUDE] Error loading reverse polymorphic relationship:', {
|
|
1764
1779
|
scopeName,
|
|
@@ -1770,13 +1785,10 @@ export const loadReversePolymorphic = async (scope, deps) => {
|
|
|
1770
1785
|
stack: error.stack
|
|
1771
1786
|
})
|
|
1772
1787
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
)
|
|
1777
|
-
enhancedError.originalError = error
|
|
1778
|
-
enhancedError.context = { scopeName, includeName, via: relDef?.via, targetScope: relDef?.target }
|
|
1779
|
-
throw enhancedError
|
|
1788
|
+
throw wrapUnexpectedError(error, {
|
|
1789
|
+
message: `Failed to load reverse polymorphic relationship '${includeName}' via '${relDef?.via}' for scope '${scopeName}'`,
|
|
1790
|
+
context: { scopeName, includeName, via: relDef?.via, targetScope: relDef?.target }
|
|
1791
|
+
})
|
|
1780
1792
|
}
|
|
1781
1793
|
}
|
|
1782
1794
|
|
|
@@ -1897,6 +1909,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1897
1909
|
}
|
|
1898
1910
|
}
|
|
1899
1911
|
} catch (includeError) {
|
|
1912
|
+
if (isRestApiError(includeError)) throw includeError
|
|
1913
|
+
|
|
1900
1914
|
// Log specific include error and continue with other includes
|
|
1901
1915
|
log.error('[INCLUDE] Error processing include:', {
|
|
1902
1916
|
scopeName,
|
|
@@ -1918,6 +1932,8 @@ export const processIncludes = async (scope, deps) => {
|
|
|
1918
1932
|
}
|
|
1919
1933
|
}
|
|
1920
1934
|
} catch (error) {
|
|
1935
|
+
if (isRestApiError(error)) throw error
|
|
1936
|
+
|
|
1921
1937
|
// Log error with full context
|
|
1922
1938
|
log.error('[INCLUDE] Error in processIncludes:', {
|
|
1923
1939
|
scopeName,
|
|
@@ -2019,6 +2035,8 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
2019
2035
|
recordsWithRelationships: records
|
|
2020
2036
|
}
|
|
2021
2037
|
} catch (error) {
|
|
2038
|
+
if (isRestApiError(error)) throw error
|
|
2039
|
+
|
|
2022
2040
|
// Log comprehensive error information
|
|
2023
2041
|
log.error('[INCLUDE] Failed to build included resources:', {
|
|
2024
2042
|
scopeName,
|
|
@@ -2028,15 +2046,14 @@ export const buildIncludedResources = async (scope, deps) => {
|
|
|
2028
2046
|
stack: error.stack
|
|
2029
2047
|
})
|
|
2030
2048
|
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2039
|
-
throw enhancedError
|
|
2049
|
+
throw wrapUnexpectedError(error, {
|
|
2050
|
+
message: `Failed to build included resources for scope '${scopeName}'`,
|
|
2051
|
+
context: {
|
|
2052
|
+
scopeName,
|
|
2053
|
+
includeParam,
|
|
2054
|
+
recordCount: records?.length || 0
|
|
2055
|
+
}
|
|
2056
|
+
})
|
|
2040
2057
|
}
|
|
2041
2058
|
}
|
|
2042
2059
|
|