json-rest-api 1.0.24 → 1.0.25
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 +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +362 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +1 -0
- package/tests/test-development-guide.md +5 -5
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
parseSortEntry
|
|
39
39
|
} from './lib/querying/query-field-sort-helpers.js'
|
|
40
40
|
import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
|
|
41
|
+
import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
|
|
41
42
|
|
|
42
43
|
export const RestApiKnexPlugin = {
|
|
43
44
|
name: 'rest-api-knex',
|
|
@@ -456,7 +457,14 @@ export const RestApiKnexPlugin = {
|
|
|
456
457
|
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
457
458
|
* @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
|
|
458
459
|
*/
|
|
459
|
-
helpers.dataGetMinimal = async ({
|
|
460
|
+
helpers.dataGetMinimal = async ({
|
|
461
|
+
scopeName,
|
|
462
|
+
context,
|
|
463
|
+
runHooks,
|
|
464
|
+
applyQueryFilters,
|
|
465
|
+
filters = context.queryParams?.filters,
|
|
466
|
+
queryPurpose = 'single'
|
|
467
|
+
}) => {
|
|
460
468
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
461
469
|
if (storageAdapter) {
|
|
462
470
|
context.storageAdapter = storageAdapter
|
|
@@ -482,29 +490,37 @@ export const RestApiKnexPlugin = {
|
|
|
482
490
|
if (typeof applyQueryFilters === 'function') {
|
|
483
491
|
const scopedQueryState = await applyQueryFilters({
|
|
484
492
|
query,
|
|
485
|
-
filters
|
|
493
|
+
filters,
|
|
486
494
|
tableName,
|
|
487
495
|
db,
|
|
488
496
|
scopeName,
|
|
497
|
+
queryPurpose,
|
|
489
498
|
storageAdapter
|
|
490
499
|
})
|
|
491
500
|
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
492
501
|
} else if (runHooks) {
|
|
502
|
+
const previousKnexQuery = context.knexQuery
|
|
493
503
|
context.knexQuery = {
|
|
494
504
|
query,
|
|
495
|
-
filters
|
|
505
|
+
filters,
|
|
496
506
|
schemaInfo: context.schemaInfo,
|
|
497
507
|
scopeName,
|
|
498
508
|
tableName,
|
|
499
509
|
db,
|
|
510
|
+
queryPurpose,
|
|
500
511
|
adapter: storageAdapter,
|
|
501
512
|
storageAdapter,
|
|
502
513
|
}
|
|
503
514
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
515
|
+
try {
|
|
516
|
+
await runHooks('knexQueryFiltering')
|
|
517
|
+
query = context.knexQuery?.query || query
|
|
518
|
+
} finally {
|
|
519
|
+
if (previousKnexQuery === undefined) {
|
|
520
|
+
delete context.knexQuery
|
|
521
|
+
} else {
|
|
522
|
+
context.knexQuery = previousKnexQuery
|
|
523
|
+
}
|
|
508
524
|
}
|
|
509
525
|
}
|
|
510
526
|
|
|
@@ -620,7 +636,9 @@ export const RestApiKnexPlugin = {
|
|
|
620
636
|
scopeName,
|
|
621
637
|
tableName,
|
|
622
638
|
db,
|
|
639
|
+
queryPurpose: 'collection',
|
|
623
640
|
adapter: storageAdapter,
|
|
641
|
+
storageAdapter,
|
|
624
642
|
}
|
|
625
643
|
|
|
626
644
|
log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
|
|
@@ -708,7 +726,13 @@ export const RestApiKnexPlugin = {
|
|
|
708
726
|
sortDescriptors,
|
|
709
727
|
cursorData,
|
|
710
728
|
(direction) => (direction === 'DESC' ? '<' : '>'),
|
|
711
|
-
(builder, descriptor, operator, value) => builder.where(
|
|
729
|
+
(builder, descriptor, operator, value) => builder.where(
|
|
730
|
+
descriptor.column,
|
|
731
|
+
operator,
|
|
732
|
+
storageAdapter?.translateFilterValue
|
|
733
|
+
? storageAdapter.translateFilterValue(descriptor.field, value)
|
|
734
|
+
: value
|
|
735
|
+
),
|
|
712
736
|
{
|
|
713
737
|
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
714
738
|
}
|
|
@@ -736,7 +760,13 @@ export const RestApiKnexPlugin = {
|
|
|
736
760
|
sortDescriptors,
|
|
737
761
|
cursorData,
|
|
738
762
|
(direction) => (direction === 'DESC' ? '>' : '<'),
|
|
739
|
-
(builder, descriptor, operator, value) => builder.where(
|
|
763
|
+
(builder, descriptor, operator, value) => builder.where(
|
|
764
|
+
descriptor.column,
|
|
765
|
+
operator,
|
|
766
|
+
storageAdapter?.translateFilterValue
|
|
767
|
+
? storageAdapter.translateFilterValue(descriptor.field, value)
|
|
768
|
+
: value
|
|
769
|
+
),
|
|
740
770
|
{
|
|
741
771
|
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
742
772
|
}
|
|
@@ -755,38 +785,10 @@ export const RestApiKnexPlugin = {
|
|
|
755
785
|
// Execute query
|
|
756
786
|
const records = await query
|
|
757
787
|
|
|
758
|
-
// Store query string for response building
|
|
759
|
-
const queryParts = []
|
|
760
|
-
Object.entries(queryParams).forEach(([key, value]) => {
|
|
761
|
-
if (Array.isArray(value)) {
|
|
762
|
-
// Handle arrays (like sort, include)
|
|
763
|
-
if (value.length > 0) {
|
|
764
|
-
queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
|
|
765
|
-
}
|
|
766
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
767
|
-
// Handle nested objects (like filters, fields, page)
|
|
768
|
-
Object.entries(value).forEach(([subKey, subValue]) => {
|
|
769
|
-
if (subValue !== undefined && subValue !== null) {
|
|
770
|
-
if (typeof subValue === 'object' && !Array.isArray(subValue)) {
|
|
771
|
-
// Handle deeply nested objects (like filters[country][code])
|
|
772
|
-
Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
|
|
773
|
-
if (subSubValue !== undefined && subSubValue !== null) {
|
|
774
|
-
queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
|
|
775
|
-
}
|
|
776
|
-
})
|
|
777
|
-
} else {
|
|
778
|
-
queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
})
|
|
782
|
-
} else if (value !== undefined && value !== null) {
|
|
783
|
-
queryParts.push(`${key}=${encodeURIComponent(value)}`)
|
|
784
|
-
}
|
|
785
|
-
})
|
|
786
|
-
|
|
787
788
|
// Initialize returnMeta namespace for thread-safe metadata
|
|
788
789
|
context.returnMeta = context.returnMeta || {}
|
|
789
|
-
|
|
790
|
+
const queryString = serializeJsonApiQuery(queryParams)
|
|
791
|
+
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
790
792
|
|
|
791
793
|
// Execute count query for pagination if offset-based pagination is used
|
|
792
794
|
if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
@@ -796,28 +798,31 @@ export const RestApiKnexPlugin = {
|
|
|
796
798
|
// Only execute count query if enabled
|
|
797
799
|
if (scope.vars.enablePaginationCounts) {
|
|
798
800
|
// Build count query with same filters as main query
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
801
|
+
let countQuery = db(tableName)
|
|
802
|
+
|
|
803
|
+
// Mandatory server-side filters must also run when the client supplied no filters.
|
|
804
|
+
// Otherwise pagination metadata can disclose or count rows excluded from the page.
|
|
805
|
+
const previousKnexQuery = context.knexQuery
|
|
806
|
+
context.knexQuery = {
|
|
807
|
+
query: countQuery,
|
|
808
|
+
filters: queryParams.filters,
|
|
809
|
+
scopeName,
|
|
810
|
+
tableName,
|
|
811
|
+
schemaInfo,
|
|
812
|
+
db,
|
|
813
|
+
queryPurpose: 'count',
|
|
814
|
+
adapter: storageAdapter,
|
|
815
|
+
storageAdapter
|
|
816
|
+
}
|
|
814
817
|
|
|
815
|
-
|
|
818
|
+
try {
|
|
816
819
|
await runHooks('knexQueryFiltering')
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
+
countQuery = context.knexQuery?.query || countQuery
|
|
821
|
+
} finally {
|
|
822
|
+
if (previousKnexQuery === undefined) {
|
|
820
823
|
delete context.knexQuery
|
|
824
|
+
} else {
|
|
825
|
+
context.knexQuery = previousKnexQuery
|
|
821
826
|
}
|
|
822
827
|
}
|
|
823
828
|
|
|
@@ -863,7 +868,9 @@ export const RestApiKnexPlugin = {
|
|
|
863
868
|
|
|
864
869
|
const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
|
|
865
870
|
|
|
866
|
-
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields
|
|
871
|
+
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields, {
|
|
872
|
+
schemaInfo
|
|
873
|
+
})
|
|
867
874
|
const urlPrefix = getUrlPrefix(context, scope)
|
|
868
875
|
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
869
876
|
urlPrefix,
|
|
@@ -872,7 +879,10 @@ export const RestApiKnexPlugin = {
|
|
|
872
879
|
records,
|
|
873
880
|
pageSize,
|
|
874
881
|
hasMore,
|
|
875
|
-
sortFields
|
|
882
|
+
sortFields,
|
|
883
|
+
{
|
|
884
|
+
schemaInfo
|
|
885
|
+
}
|
|
876
886
|
)
|
|
877
887
|
}
|
|
878
888
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeReturnRecordSetting } from '../lib/querying-writing/return-record-settings.js'
|
|
2
|
+
|
|
1
3
|
export default async function turnScopeInitIntoVars ({ context, scopes, vars: apiVars }) {
|
|
2
4
|
// Refer to the scope's vars
|
|
3
5
|
const scope = scopes[context.scopeName]
|
|
@@ -19,8 +21,12 @@ export default async function turnScopeInitIntoVars ({ context, scopes, vars: ap
|
|
|
19
21
|
if (typeof scopeOptions.simplifiedTransport !== 'undefined') vars.simplifiedTransport = scopeOptions.simplifiedTransport
|
|
20
22
|
|
|
21
23
|
// Set returnRecord settings as scope vars
|
|
22
|
-
if (typeof scopeOptions.returnRecordApi !== 'undefined')
|
|
23
|
-
|
|
24
|
+
if (typeof scopeOptions.returnRecordApi !== 'undefined') {
|
|
25
|
+
vars.returnRecordApi = normalizeReturnRecordSetting(scopeOptions.returnRecordApi, vars.returnRecordApi || 'full')
|
|
26
|
+
}
|
|
27
|
+
if (typeof scopeOptions.returnRecordTransport !== 'undefined') {
|
|
28
|
+
vars.returnRecordTransport = normalizeReturnRecordSetting(scopeOptions.returnRecordTransport, vars.returnRecordTransport || 'no')
|
|
29
|
+
}
|
|
24
30
|
|
|
25
31
|
// Set idProperty as scope var
|
|
26
32
|
if (typeof scopeOptions.idProperty !== 'undefined') vars.idProperty = scopeOptions.idProperty
|
|
@@ -5,7 +5,16 @@ import {
|
|
|
5
5
|
import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
6
|
import { normalizeRelationshipIdentifiers } from '../lib/querying-writing/resource-id-normalization.js'
|
|
7
7
|
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
normalizeReturnRecordMode,
|
|
10
|
+
normalizeReturnRecordSetting
|
|
11
|
+
} from '../lib/querying-writing/return-record-settings.js'
|
|
12
|
+
import {
|
|
13
|
+
getRequestContracts,
|
|
14
|
+
validateRequestContractOrThrow
|
|
15
|
+
} from '../lib/querying-writing/request-contracts.js'
|
|
16
|
+
|
|
17
|
+
export { normalizeReturnRecordMode as normalizeReturnValue }
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Gets an enhanced logger instance with full error details and stack traces
|
|
@@ -29,16 +38,6 @@ export const getEnhancedLogger = (log) => {
|
|
|
29
38
|
export const cascadeConfig = (settingName, sources, defaultValue) =>
|
|
30
39
|
sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
|
|
31
40
|
|
|
32
|
-
/**
|
|
33
|
-
* Normalizes return record settings to valid values
|
|
34
|
-
* @param {string|boolean} value - The value to normalize
|
|
35
|
-
* @returns {string} Normalized value: 'no', 'minimal', or 'full'
|
|
36
|
-
*/
|
|
37
|
-
export function normalizeReturnValue (value) {
|
|
38
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
39
|
-
return 'no' // default
|
|
40
|
-
}
|
|
41
|
-
|
|
42
41
|
/**
|
|
43
42
|
* Sets up common request context for REST API methods
|
|
44
43
|
* Handles simplified mode, transaction setup, and initial validation
|
|
@@ -76,7 +75,7 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
76
75
|
} else {
|
|
77
76
|
context.inputRecord = params
|
|
78
77
|
// Preserve returnFullRecord if specified in params
|
|
79
|
-
context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
|
|
78
|
+
context.params = params.returnFullRecord !== undefined ? { returnFullRecord: params.returnFullRecord } : {}
|
|
80
79
|
}
|
|
81
80
|
} else {
|
|
82
81
|
context.inputRecord = params.inputRecord
|
|
@@ -90,33 +89,10 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
90
89
|
const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
|
|
91
90
|
|
|
92
91
|
// Get return record setting - from params only (per-call override) or use default
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Normalize return record setting to always be an object with method keys
|
|
98
|
-
if (typeof returnFullRecordRaw === 'object' && returnFullRecordRaw !== null) {
|
|
99
|
-
// It's already an object, normalize the values
|
|
100
|
-
context.returnRecordSetting = {
|
|
101
|
-
post: normalizeReturnValue(returnFullRecordRaw.post),
|
|
102
|
-
put: normalizeReturnValue(returnFullRecordRaw.put),
|
|
103
|
-
patch: normalizeReturnValue(returnFullRecordRaw.patch)
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
// It's a single value (string or boolean), apply to all methods
|
|
107
|
-
const normalized = normalizeReturnValue(returnFullRecordRaw)
|
|
108
|
-
context.returnRecordSetting = {
|
|
109
|
-
post: normalized,
|
|
110
|
-
put: normalized,
|
|
111
|
-
patch: normalized
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Helper function to normalize return values (same as above)
|
|
116
|
-
function normalizeReturnValue (value) {
|
|
117
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
118
|
-
return 'no' // default
|
|
119
|
-
}
|
|
92
|
+
context.returnRecordSetting = normalizeReturnRecordSetting(
|
|
93
|
+
context.params.returnFullRecord,
|
|
94
|
+
defaultReturnFullRecord
|
|
95
|
+
)
|
|
120
96
|
|
|
121
97
|
// These only make sense as parameter per query, not in vars etc.
|
|
122
98
|
context.queryParams = params.queryParams || {}
|
|
@@ -161,7 +137,7 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
161
137
|
*/
|
|
162
138
|
export const handleWriteMethodError = async (error, context, method, scopeName, log, runHooks) => {
|
|
163
139
|
// Rollback transaction if we created it
|
|
164
|
-
if (context.shouldCommit) {
|
|
140
|
+
if (context.shouldCommit && !context.transactionCommitted) {
|
|
165
141
|
await context.transaction.rollback()
|
|
166
142
|
await runHooks('afterRollback')
|
|
167
143
|
}
|
|
@@ -179,6 +155,39 @@ export const handleWriteMethodError = async (error, context, method, scopeName,
|
|
|
179
155
|
throw error
|
|
180
156
|
}
|
|
181
157
|
|
|
158
|
+
export const commitOwnedTransaction = async (context, runHooks) => {
|
|
159
|
+
if (!context.shouldCommit) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await context.transaction.commit()
|
|
164
|
+
context.transactionCommitted = true
|
|
165
|
+
await runHooks('afterCommit')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const validateRelationshipRoutePayload = ({
|
|
169
|
+
context,
|
|
170
|
+
vars,
|
|
171
|
+
scopeName,
|
|
172
|
+
operation,
|
|
173
|
+
relationshipData
|
|
174
|
+
}) => {
|
|
175
|
+
const contracts = getRequestContracts({
|
|
176
|
+
scopeName,
|
|
177
|
+
schemaInfo: context.schemaInfo,
|
|
178
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
179
|
+
sortableFields: vars.sortableFields
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const validated = validateRequestContractOrThrow(
|
|
183
|
+
contracts[operation],
|
|
184
|
+
{ data: relationshipData },
|
|
185
|
+
`${operation} relationship request body is invalid`
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return validated.data
|
|
189
|
+
}
|
|
190
|
+
|
|
182
191
|
/**
|
|
183
192
|
* Validates that a pivot resource exists for many-to-many relationships
|
|
184
193
|
*
|
|
@@ -473,31 +482,18 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
473
482
|
isUpdate: false
|
|
474
483
|
}
|
|
475
484
|
|
|
476
|
-
const targetScope = api.resources?.[item.type]
|
|
477
|
-
const applyQueryFilters = targetScope?.applyQueryFilters
|
|
478
|
-
? async ({ query, filters, tableName, db, scopeName, storageAdapter, isAnyApi }) => {
|
|
479
|
-
const queryState = await targetScope.applyQueryFilters({
|
|
480
|
-
query,
|
|
481
|
-
filters,
|
|
482
|
-
schemaInfo: getContext.schemaInfo,
|
|
483
|
-
scopeName,
|
|
484
|
-
tableName,
|
|
485
|
-
db,
|
|
486
|
-
isAnyApi,
|
|
487
|
-
storageAdapter
|
|
488
|
-
}, getContext)
|
|
489
|
-
|
|
490
|
-
return {
|
|
491
|
-
query: unwrapQueryBuilderState(queryState, query)
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
: undefined
|
|
495
|
-
|
|
496
485
|
// Get the minimal record
|
|
497
486
|
const record = await helpers.dataGetMinimal({
|
|
498
487
|
scopeName: item.type,
|
|
499
488
|
context: getContext,
|
|
500
|
-
applyQueryFilters
|
|
489
|
+
applyQueryFilters: relatedScope.applyQueryFilters
|
|
490
|
+
? (filterParams) => relatedScope.applyQueryFilters({
|
|
491
|
+
...filterParams,
|
|
492
|
+
schemaInfo: getContext.schemaInfo
|
|
493
|
+
}, getContext)
|
|
494
|
+
: undefined,
|
|
495
|
+
filters: {},
|
|
496
|
+
queryPurpose: 'relationship-validation'
|
|
501
497
|
})
|
|
502
498
|
|
|
503
499
|
if (!record) {
|
|
@@ -524,6 +520,22 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
524
520
|
}
|
|
525
521
|
}
|
|
526
522
|
|
|
523
|
+
export const getVisibleRelationshipParent = async ({
|
|
524
|
+
context,
|
|
525
|
+
helpers,
|
|
526
|
+
scopeName,
|
|
527
|
+
runHooks
|
|
528
|
+
}) => {
|
|
529
|
+
// Related-route filters belong to the target resource, not its parent.
|
|
530
|
+
return helpers.dataGetMinimal({
|
|
531
|
+
scopeName,
|
|
532
|
+
context,
|
|
533
|
+
runHooks,
|
|
534
|
+
filters: {},
|
|
535
|
+
queryPurpose: 'relationship-parent'
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
527
539
|
/**
|
|
528
540
|
* Applies field setters to transform attribute values before storage
|
|
529
541
|
* Setters are applied in dependency order to handle interdependent fields
|
|
@@ -570,8 +582,18 @@ export const applyFieldSetters = async (attributes, schemaInfo, context, api, he
|
|
|
570
582
|
setterContext
|
|
571
583
|
)
|
|
572
584
|
} catch (error) {
|
|
573
|
-
|
|
574
|
-
|
|
585
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
586
|
+
throw new RestApiValidationError(
|
|
587
|
+
`Setter for field '${fieldName}' failed: ${message}`,
|
|
588
|
+
{
|
|
589
|
+
fields: [`data.attributes.${fieldName}`],
|
|
590
|
+
violations: [{
|
|
591
|
+
field: `data.attributes.${fieldName}`,
|
|
592
|
+
rule: 'setter_failed',
|
|
593
|
+
message
|
|
594
|
+
}]
|
|
595
|
+
}
|
|
596
|
+
)
|
|
575
597
|
}
|
|
576
598
|
}
|
|
577
599
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { RestApiResourceError, RestApiValidationError
|
|
2
|
-
import {
|
|
1
|
+
import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import {
|
|
3
|
+
commitOwnedTransaction,
|
|
4
|
+
findRelationshipDefinition,
|
|
5
|
+
getVisibleRelationshipParent,
|
|
6
|
+
handleWriteMethodError,
|
|
7
|
+
validateRelationshipRoutePayload
|
|
8
|
+
} from './common.js'
|
|
3
9
|
import {
|
|
4
10
|
normalizeRelationshipIdentifiers,
|
|
5
11
|
requireExistingResourceId
|
|
@@ -48,22 +54,28 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
|
|
|
48
54
|
)
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
params.relationshipData = validateRelationshipRoutePayload({
|
|
58
|
+
context,
|
|
59
|
+
vars,
|
|
60
|
+
scopeName,
|
|
61
|
+
operation: 'deleteRelationship',
|
|
62
|
+
relationshipData: params.relationshipData
|
|
63
|
+
})
|
|
54
64
|
params.relationshipData = normalizeRelationshipIdentifiers(params.relationshipData, { api })
|
|
55
65
|
|
|
56
66
|
// Check permissions
|
|
57
67
|
await runHooks('checkPermissions')
|
|
58
68
|
await runHooks('checkPermissionsDeleteRelationship')
|
|
59
69
|
|
|
60
|
-
// Verify parent exists
|
|
61
|
-
const
|
|
70
|
+
// Verify the parent exists in the caller's visible dataset.
|
|
71
|
+
const parentRecord = await getVisibleRelationshipParent({
|
|
72
|
+
context,
|
|
73
|
+
helpers,
|
|
62
74
|
scopeName,
|
|
63
|
-
|
|
75
|
+
runHooks
|
|
64
76
|
})
|
|
65
77
|
|
|
66
|
-
if (!
|
|
78
|
+
if (!parentRecord) {
|
|
67
79
|
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
68
80
|
}
|
|
69
81
|
|
|
@@ -108,16 +120,14 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
|
|
|
108
120
|
},
|
|
109
121
|
transaction: context.transaction,
|
|
110
122
|
simplified: false
|
|
111
|
-
})
|
|
123
|
+
}, { ...context })
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
await runHooks('finish')
|
|
116
128
|
await runHooks('finishDeleteRelationship')
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
await context.transaction.commit()
|
|
120
|
-
}
|
|
130
|
+
await commitOwnedTransaction(context, runHooks)
|
|
121
131
|
|
|
122
132
|
// 204 No Content
|
|
123
133
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { handleWriteMethodError } from './common.js'
|
|
2
|
+
import { commitOwnedTransaction, handleWriteMethodError } from './common.js'
|
|
3
3
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -96,11 +96,7 @@ export default async function deleteMethod ({
|
|
|
96
96
|
await runHooks('finish')
|
|
97
97
|
await runHooks('finishDelete')
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
if (context.shouldCommit) {
|
|
101
|
-
await context.transaction.commit()
|
|
102
|
-
await runHooks('afterCommit')
|
|
103
|
-
}
|
|
99
|
+
await commitOwnedTransaction(context, runHooks)
|
|
104
100
|
|
|
105
101
|
// DELETE typically returns void/undefined (204 No Content)
|
|
106
102
|
} catch (error) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { findRelationshipDefinition } from './common.js'
|
|
2
|
+
import { findRelationshipDefinition, getVisibleRelationshipParent } from './common.js'
|
|
3
3
|
import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
|
|
4
4
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
5
5
|
|
|
@@ -57,13 +57,15 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
57
57
|
await runHooks('checkPermissions')
|
|
58
58
|
await runHooks('checkPermissionsGetRelated')
|
|
59
59
|
|
|
60
|
-
// Verify parent exists
|
|
61
|
-
const
|
|
60
|
+
// Verify the parent exists in the caller's visible dataset.
|
|
61
|
+
const parentRecord = await getVisibleRelationshipParent({
|
|
62
|
+
context,
|
|
63
|
+
helpers,
|
|
62
64
|
scopeName,
|
|
63
|
-
|
|
65
|
+
runHooks
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
if (!
|
|
68
|
+
if (!parentRecord) {
|
|
67
69
|
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -102,7 +104,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
102
104
|
transaction: context.transaction,
|
|
103
105
|
simplified: false,
|
|
104
106
|
isTransport: params.isTransport
|
|
105
|
-
})
|
|
107
|
+
}, { ...context })
|
|
106
108
|
|
|
107
109
|
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
108
110
|
if (!relatedId) {
|
|
@@ -119,7 +121,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
119
121
|
transaction: context.transaction,
|
|
120
122
|
simplified: false,
|
|
121
123
|
isTransport: params.isTransport
|
|
122
|
-
})
|
|
124
|
+
}, { ...context })
|
|
123
125
|
|
|
124
126
|
return {
|
|
125
127
|
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
@@ -140,7 +142,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
140
142
|
transaction: context.transaction,
|
|
141
143
|
simplified: false,
|
|
142
144
|
isTransport: params.isTransport
|
|
143
|
-
})
|
|
145
|
+
}, { ...context })
|
|
144
146
|
|
|
145
147
|
// Extract the related resource from the parent's relationships
|
|
146
148
|
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
@@ -194,7 +196,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
194
196
|
transaction: context.transaction,
|
|
195
197
|
simplified: false,
|
|
196
198
|
isTransport: params.isTransport
|
|
197
|
-
})
|
|
199
|
+
}, { ...context })
|
|
198
200
|
|
|
199
201
|
if (!result.links) {
|
|
200
202
|
result.links = {}
|
|
@@ -228,7 +230,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
228
230
|
transaction: context.transaction,
|
|
229
231
|
simplified: false,
|
|
230
232
|
isTransport: params.isTransport
|
|
231
|
-
})
|
|
233
|
+
}, { ...context })
|
|
232
234
|
|
|
233
235
|
if (!result.links) {
|
|
234
236
|
result.links = {}
|
|
@@ -256,7 +258,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
256
258
|
transaction: context.transaction,
|
|
257
259
|
simplified: false,
|
|
258
260
|
isTransport: params.isTransport,
|
|
259
|
-
})
|
|
261
|
+
}, { ...context })
|
|
260
262
|
if (related?.data) {
|
|
261
263
|
results.push(related.data)
|
|
262
264
|
}
|
|
@@ -307,7 +309,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
307
309
|
transaction: context.transaction,
|
|
308
310
|
simplified: false,
|
|
309
311
|
isTransport: params.isTransport
|
|
310
|
-
})
|
|
312
|
+
}, { ...context })
|
|
311
313
|
let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
|
|
312
314
|
|
|
313
315
|
if (includedResources.length === 0) {
|
|
@@ -339,7 +341,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
339
341
|
transaction: context.transaction,
|
|
340
342
|
simplified: false,
|
|
341
343
|
isTransport: params.isTransport,
|
|
342
|
-
})
|
|
344
|
+
}, { ...context })
|
|
343
345
|
if (related?.data) {
|
|
344
346
|
includedResources.push(related.data)
|
|
345
347
|
}
|
|
@@ -46,7 +46,7 @@ export default async function getRelationshipMethod ({ params, context, vars, he
|
|
|
46
46
|
transaction: context.transaction,
|
|
47
47
|
simplified: false,
|
|
48
48
|
isTransport: params.isTransport
|
|
49
|
-
})
|
|
49
|
+
}, { ...context })
|
|
50
50
|
|
|
51
51
|
if (!fullRecord || !fullRecord.data) {
|
|
52
52
|
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|