json-rest-api 1.0.23 → 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 +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- 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_Knex_Schema_And_Migrations.md +122 -2
- 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/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- 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 +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- 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/compile-schemas.js +10 -6
- 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 +7 -3
- 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/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- 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 +56 -6
- package/tests/test-development-guide.md +5 -5
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
generateKnexMigrationDiff
|
|
9
9
|
} from './lib/dbTablesOperations.js'
|
|
10
10
|
import { introspectKnexTableSnapshot } from './lib/dbIntrospection.js'
|
|
11
|
-
import { applyFieldSelectionToQuery, buildFieldSelection
|
|
12
|
-
import {
|
|
13
|
-
import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
|
|
11
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
|
|
12
|
+
import { buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
|
|
14
13
|
import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
|
|
15
14
|
import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
|
|
16
15
|
import { processIncludes } from './lib/querying/knex-process-includes.js'
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
parseSortEntry
|
|
40
39
|
} from './lib/querying/query-field-sort-helpers.js'
|
|
41
40
|
import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
|
|
41
|
+
import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
|
|
42
42
|
|
|
43
43
|
export const RestApiKnexPlugin = {
|
|
44
44
|
name: 'rest-api-knex',
|
|
@@ -46,9 +46,8 @@ export const RestApiKnexPlugin = {
|
|
|
46
46
|
|
|
47
47
|
async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
|
|
48
48
|
// Try to import knex dynamically
|
|
49
|
-
let knexLib
|
|
50
49
|
try {
|
|
51
|
-
|
|
50
|
+
await import('knex')
|
|
52
51
|
} catch (e) {
|
|
53
52
|
requirePackage('knex', 'rest-api-knex',
|
|
54
53
|
'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
|
|
@@ -100,6 +99,7 @@ export const RestApiKnexPlugin = {
|
|
|
100
99
|
|
|
101
100
|
return {
|
|
102
101
|
structure: filteredSchema,
|
|
102
|
+
storage: vars.schemaInfo?.storage,
|
|
103
103
|
indexes: vars.schemaInfo?.indexes || [],
|
|
104
104
|
foreignKeys: vars.schemaInfo?.foreignKeys || [],
|
|
105
105
|
checkConstraints: vars.schemaInfo?.checkConstraints || []
|
|
@@ -457,7 +457,14 @@ export const RestApiKnexPlugin = {
|
|
|
457
457
|
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
458
458
|
* @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
|
|
459
459
|
*/
|
|
460
|
-
helpers.dataGetMinimal = async ({
|
|
460
|
+
helpers.dataGetMinimal = async ({
|
|
461
|
+
scopeName,
|
|
462
|
+
context,
|
|
463
|
+
runHooks,
|
|
464
|
+
applyQueryFilters,
|
|
465
|
+
filters = context.queryParams?.filters,
|
|
466
|
+
queryPurpose = 'single'
|
|
467
|
+
}) => {
|
|
461
468
|
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
462
469
|
if (storageAdapter) {
|
|
463
470
|
context.storageAdapter = storageAdapter
|
|
@@ -483,29 +490,37 @@ export const RestApiKnexPlugin = {
|
|
|
483
490
|
if (typeof applyQueryFilters === 'function') {
|
|
484
491
|
const scopedQueryState = await applyQueryFilters({
|
|
485
492
|
query,
|
|
486
|
-
filters
|
|
493
|
+
filters,
|
|
487
494
|
tableName,
|
|
488
495
|
db,
|
|
489
496
|
scopeName,
|
|
497
|
+
queryPurpose,
|
|
490
498
|
storageAdapter
|
|
491
499
|
})
|
|
492
500
|
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
493
501
|
} else if (runHooks) {
|
|
502
|
+
const previousKnexQuery = context.knexQuery
|
|
494
503
|
context.knexQuery = {
|
|
495
504
|
query,
|
|
496
|
-
filters
|
|
505
|
+
filters,
|
|
497
506
|
schemaInfo: context.schemaInfo,
|
|
498
507
|
scopeName,
|
|
499
508
|
tableName,
|
|
500
509
|
db,
|
|
510
|
+
queryPurpose,
|
|
501
511
|
adapter: storageAdapter,
|
|
502
512
|
storageAdapter,
|
|
503
513
|
}
|
|
504
514
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
524
|
}
|
|
510
525
|
}
|
|
511
526
|
|
|
@@ -621,7 +636,9 @@ export const RestApiKnexPlugin = {
|
|
|
621
636
|
scopeName,
|
|
622
637
|
tableName,
|
|
623
638
|
db,
|
|
639
|
+
queryPurpose: 'collection',
|
|
624
640
|
adapter: storageAdapter,
|
|
641
|
+
storageAdapter,
|
|
625
642
|
}
|
|
626
643
|
|
|
627
644
|
log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
|
|
@@ -682,9 +699,7 @@ export const RestApiKnexPlugin = {
|
|
|
682
699
|
query
|
|
683
700
|
.limit(pageSize)
|
|
684
701
|
.offset((pageNumber - 1) * pageSize)
|
|
685
|
-
}
|
|
686
|
-
// Cursor-based pagination
|
|
687
|
-
else if (queryParams.page.after || queryParams.page.before) {
|
|
702
|
+
} else if (queryParams.page.after || queryParams.page.before) {
|
|
688
703
|
// Fetch one extra record to determine if there are more
|
|
689
704
|
query.limit(pageSize + 1)
|
|
690
705
|
|
|
@@ -711,7 +726,13 @@ export const RestApiKnexPlugin = {
|
|
|
711
726
|
sortDescriptors,
|
|
712
727
|
cursorData,
|
|
713
728
|
(direction) => (direction === 'DESC' ? '<' : '>'),
|
|
714
|
-
(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
|
+
),
|
|
715
736
|
{
|
|
716
737
|
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
717
738
|
}
|
|
@@ -739,15 +760,19 @@ export const RestApiKnexPlugin = {
|
|
|
739
760
|
sortDescriptors,
|
|
740
761
|
cursorData,
|
|
741
762
|
(direction) => (direction === 'DESC' ? '>' : '<'),
|
|
742
|
-
(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
|
+
),
|
|
743
770
|
{
|
|
744
771
|
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
745
772
|
}
|
|
746
773
|
)
|
|
747
774
|
}
|
|
748
|
-
}
|
|
749
|
-
// Default pagination if only size is specified - treat as cursor-based for "load more"
|
|
750
|
-
else {
|
|
775
|
+
} else {
|
|
751
776
|
// Fetch one extra to detect hasMore
|
|
752
777
|
query.limit(pageSize + 1)
|
|
753
778
|
}
|
|
@@ -760,38 +785,10 @@ export const RestApiKnexPlugin = {
|
|
|
760
785
|
// Execute query
|
|
761
786
|
const records = await query
|
|
762
787
|
|
|
763
|
-
// Store query string for response building
|
|
764
|
-
const queryParts = []
|
|
765
|
-
Object.entries(queryParams).forEach(([key, value]) => {
|
|
766
|
-
if (Array.isArray(value)) {
|
|
767
|
-
// Handle arrays (like sort, include)
|
|
768
|
-
if (value.length > 0) {
|
|
769
|
-
queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
|
|
770
|
-
}
|
|
771
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
772
|
-
// Handle nested objects (like filters, fields, page)
|
|
773
|
-
Object.entries(value).forEach(([subKey, subValue]) => {
|
|
774
|
-
if (subValue !== undefined && subValue !== null) {
|
|
775
|
-
if (typeof subValue === 'object' && !Array.isArray(subValue)) {
|
|
776
|
-
// Handle deeply nested objects (like filters[country][code])
|
|
777
|
-
Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
|
|
778
|
-
if (subSubValue !== undefined && subSubValue !== null) {
|
|
779
|
-
queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
|
|
780
|
-
}
|
|
781
|
-
})
|
|
782
|
-
} else {
|
|
783
|
-
queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
})
|
|
787
|
-
} else if (value !== undefined && value !== null) {
|
|
788
|
-
queryParts.push(`${key}=${encodeURIComponent(value)}`)
|
|
789
|
-
}
|
|
790
|
-
})
|
|
791
|
-
|
|
792
788
|
// Initialize returnMeta namespace for thread-safe metadata
|
|
793
789
|
context.returnMeta = context.returnMeta || {}
|
|
794
|
-
|
|
790
|
+
const queryString = serializeJsonApiQuery(queryParams)
|
|
791
|
+
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
795
792
|
|
|
796
793
|
// Execute count query for pagination if offset-based pagination is used
|
|
797
794
|
if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
@@ -801,28 +798,31 @@ export const RestApiKnexPlugin = {
|
|
|
801
798
|
// Only execute count query if enabled
|
|
802
799
|
if (scope.vars.enablePaginationCounts) {
|
|
803
800
|
// Build count query with same filters as main query
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
+
}
|
|
819
817
|
|
|
820
|
-
|
|
818
|
+
try {
|
|
821
819
|
await runHooks('knexQueryFiltering')
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (
|
|
820
|
+
countQuery = context.knexQuery?.query || countQuery
|
|
821
|
+
} finally {
|
|
822
|
+
if (previousKnexQuery === undefined) {
|
|
825
823
|
delete context.knexQuery
|
|
824
|
+
} else {
|
|
825
|
+
context.knexQuery = previousKnexQuery
|
|
826
826
|
}
|
|
827
827
|
}
|
|
828
828
|
|
|
@@ -868,7 +868,9 @@ export const RestApiKnexPlugin = {
|
|
|
868
868
|
|
|
869
869
|
const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
|
|
870
870
|
|
|
871
|
-
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields
|
|
871
|
+
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields, {
|
|
872
|
+
schemaInfo
|
|
873
|
+
})
|
|
872
874
|
const urlPrefix = getUrlPrefix(context, scope)
|
|
873
875
|
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
874
876
|
urlPrefix,
|
|
@@ -877,7 +879,10 @@ export const RestApiKnexPlugin = {
|
|
|
877
879
|
records,
|
|
878
880
|
pageSize,
|
|
879
881
|
hasMore,
|
|
880
|
-
sortFields
|
|
882
|
+
sortFields,
|
|
883
|
+
{
|
|
884
|
+
schemaInfo
|
|
885
|
+
}
|
|
881
886
|
)
|
|
882
887
|
}
|
|
883
888
|
|
|
@@ -918,7 +923,6 @@ export const RestApiKnexPlugin = {
|
|
|
918
923
|
if (storageAdapter) {
|
|
919
924
|
context.storageAdapter = storageAdapter
|
|
920
925
|
}
|
|
921
|
-
const scope = api.resources[scopeName]
|
|
922
926
|
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
923
927
|
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
924
928
|
const db = context.db || api.knex.instance
|
|
@@ -943,12 +947,26 @@ export const RestApiKnexPlugin = {
|
|
|
943
947
|
})
|
|
944
948
|
: attributes
|
|
945
949
|
|
|
950
|
+
const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
|
|
951
|
+
|
|
946
952
|
// Insert and get the new ID
|
|
947
953
|
const result = await db(tableName).insert(dbAttributes).returning(idProperty)
|
|
948
954
|
|
|
949
|
-
// Extract the ID value (
|
|
950
|
-
|
|
951
|
-
|
|
955
|
+
// Extract the ID value. Some dialects ignore .returning() for inserts and can
|
|
956
|
+
// yield 0 even when the caller explicitly supplied the primary key.
|
|
957
|
+
const returnedId = Array.isArray(result)
|
|
958
|
+
? (result[0]?.[idProperty] ?? result[0])
|
|
959
|
+
: (result?.[idProperty] ?? result)
|
|
960
|
+
|
|
961
|
+
if (
|
|
962
|
+
explicitId !== undefined &&
|
|
963
|
+
explicitId !== null &&
|
|
964
|
+
(returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
|
|
965
|
+
) {
|
|
966
|
+
return explicitId
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return returnedId
|
|
952
970
|
}
|
|
953
971
|
|
|
954
972
|
/**
|
|
@@ -1135,7 +1153,6 @@ export const RestApiKnexPlugin = {
|
|
|
1135
1153
|
if (storageAdapter) {
|
|
1136
1154
|
context.storageAdapter = storageAdapter
|
|
1137
1155
|
}
|
|
1138
|
-
const scope = api.resources[scopeName]
|
|
1139
1156
|
const id = context.id
|
|
1140
1157
|
|
|
1141
1158
|
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
@@ -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,11 +21,16 @@ 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
|
|
33
|
+
if (typeof scopeOptions.normalizeId === 'function') vars.normalizeId = scopeOptions.normalizeId
|
|
27
34
|
|
|
28
35
|
// Add validation for query limits
|
|
29
36
|
if (vars.queryDefaultLimit && vars.queryMaxLimit) {
|
|
@@ -3,8 +3,18 @@ import {
|
|
|
3
3
|
RestApiValidationError
|
|
4
4
|
} from '../../../lib/rest-api-errors.js'
|
|
5
5
|
import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
|
+
import { normalizeRelationshipIdentifiers } from '../lib/querying-writing/resource-id-normalization.js'
|
|
6
7
|
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
7
|
-
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 }
|
|
8
18
|
|
|
9
19
|
/**
|
|
10
20
|
* Gets an enhanced logger instance with full error details and stack traces
|
|
@@ -28,16 +38,6 @@ export const getEnhancedLogger = (log) => {
|
|
|
28
38
|
export const cascadeConfig = (settingName, sources, defaultValue) =>
|
|
29
39
|
sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
|
|
30
40
|
|
|
31
|
-
/**
|
|
32
|
-
* Normalizes return record settings to valid values
|
|
33
|
-
* @param {string|boolean} value - The value to normalize
|
|
34
|
-
* @returns {string} Normalized value: 'no', 'minimal', or 'full'
|
|
35
|
-
*/
|
|
36
|
-
export function normalizeReturnValue (value) {
|
|
37
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
38
|
-
return 'no' // default
|
|
39
|
-
}
|
|
40
|
-
|
|
41
41
|
/**
|
|
42
42
|
* Sets up common request context for REST API methods
|
|
43
43
|
* Handles simplified mode, transaction setup, and initial validation
|
|
@@ -75,7 +75,7 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
75
75
|
} else {
|
|
76
76
|
context.inputRecord = params
|
|
77
77
|
// Preserve returnFullRecord if specified in params
|
|
78
|
-
context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
|
|
78
|
+
context.params = params.returnFullRecord !== undefined ? { returnFullRecord: params.returnFullRecord } : {}
|
|
79
79
|
}
|
|
80
80
|
} else {
|
|
81
81
|
context.inputRecord = params.inputRecord
|
|
@@ -89,33 +89,10 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
89
89
|
const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
|
|
90
90
|
|
|
91
91
|
// Get return record setting - from params only (per-call override) or use default
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Normalize return record setting to always be an object with method keys
|
|
97
|
-
if (typeof returnFullRecordRaw === 'object' && returnFullRecordRaw !== null) {
|
|
98
|
-
// It's already an object, normalize the values
|
|
99
|
-
context.returnRecordSetting = {
|
|
100
|
-
post: normalizeReturnValue(returnFullRecordRaw.post),
|
|
101
|
-
put: normalizeReturnValue(returnFullRecordRaw.put),
|
|
102
|
-
patch: normalizeReturnValue(returnFullRecordRaw.patch)
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
// It's a single value (string or boolean), apply to all methods
|
|
106
|
-
const normalized = normalizeReturnValue(returnFullRecordRaw)
|
|
107
|
-
context.returnRecordSetting = {
|
|
108
|
-
post: normalized,
|
|
109
|
-
put: normalized,
|
|
110
|
-
patch: normalized
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Helper function to normalize return values (same as above)
|
|
115
|
-
function normalizeReturnValue (value) {
|
|
116
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
117
|
-
return 'no' // default
|
|
118
|
-
}
|
|
92
|
+
context.returnRecordSetting = normalizeReturnRecordSetting(
|
|
93
|
+
context.params.returnFullRecord,
|
|
94
|
+
defaultReturnFullRecord
|
|
95
|
+
)
|
|
119
96
|
|
|
120
97
|
// These only make sense as parameter per query, not in vars etc.
|
|
121
98
|
context.queryParams = params.queryParams || {}
|
|
@@ -160,7 +137,7 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
160
137
|
*/
|
|
161
138
|
export const handleWriteMethodError = async (error, context, method, scopeName, log, runHooks) => {
|
|
162
139
|
// Rollback transaction if we created it
|
|
163
|
-
if (context.shouldCommit) {
|
|
140
|
+
if (context.shouldCommit && !context.transactionCommitted) {
|
|
164
141
|
await context.transaction.rollback()
|
|
165
142
|
await runHooks('afterRollback')
|
|
166
143
|
}
|
|
@@ -178,6 +155,39 @@ export const handleWriteMethodError = async (error, context, method, scopeName,
|
|
|
178
155
|
throw error
|
|
179
156
|
}
|
|
180
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
|
+
|
|
181
191
|
/**
|
|
182
192
|
* Validates that a pivot resource exists for many-to-many relationships
|
|
183
193
|
*
|
|
@@ -296,16 +306,20 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
296
306
|
context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) }
|
|
297
307
|
}
|
|
298
308
|
|
|
309
|
+
const schemaStructure = context.schemaInfo.schemaStructure || {}
|
|
310
|
+
const hasLogicalIdField = Object.hasOwn(schemaStructure, 'id')
|
|
311
|
+
const resourceId = context.inputRecord?.data?.id
|
|
312
|
+
|
|
299
313
|
// Merge belongsTo updates with attributes for validation
|
|
300
314
|
const attributesToValidate = {
|
|
301
315
|
...context.inputRecord.data.attributes,
|
|
302
|
-
...belongsToUpdates
|
|
316
|
+
...belongsToUpdates,
|
|
317
|
+
...(hasLogicalIdField && resourceId !== undefined ? { id: resourceId } : {})
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
// Extract computed fields and foreign key fields before validation
|
|
306
321
|
const computedFields = {}
|
|
307
322
|
const foreignKeyFields = {}
|
|
308
|
-
const schemaStructure = context.schemaInfo.schemaStructure || {}
|
|
309
323
|
Object.entries(attributesToValidate).forEach(([key, value]) => {
|
|
310
324
|
const fieldDef = schemaStructure[key]
|
|
311
325
|
// Computed fields should be stripped from input
|
|
@@ -375,12 +389,13 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
375
389
|
const { validatedObject, errors } = await schema[validationMethod](attributesForValidation)
|
|
376
390
|
|
|
377
391
|
if (Object.keys(errors).length > 0) {
|
|
378
|
-
// --- START OF MODIFICATION ---
|
|
379
|
-
const schemaStructure = context.schemaInfo.schemaInstance.structure // Get the schema structure for lookup
|
|
380
|
-
|
|
381
392
|
const violations = Object.entries(errors).map(([field, error]) => {
|
|
382
393
|
let fieldPath = `data.attributes.${field}` // Default path for attributes
|
|
383
394
|
|
|
395
|
+
if (field === 'id' && hasLogicalIdField) {
|
|
396
|
+
fieldPath = 'data.id'
|
|
397
|
+
}
|
|
398
|
+
|
|
384
399
|
// Check if this field is a foreign key that has an 'as' alias
|
|
385
400
|
const fieldDef = schemaStructure[field]
|
|
386
401
|
if (fieldDef && fieldDef.belongsTo && fieldDef.as) {
|
|
@@ -400,7 +415,6 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
400
415
|
message: error.message
|
|
401
416
|
}
|
|
402
417
|
})
|
|
403
|
-
// --- END OF MODIFICATION ---
|
|
404
418
|
|
|
405
419
|
throw new RestApiValidationError(
|
|
406
420
|
'Schema validation failed for resource attributes',
|
|
@@ -411,9 +425,14 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
411
425
|
)
|
|
412
426
|
}
|
|
413
427
|
|
|
428
|
+
if (Object.hasOwn(validatedObject, 'id')) {
|
|
429
|
+
context.inputRecord.data.id = validatedObject.id
|
|
430
|
+
}
|
|
431
|
+
const { id: _validatedId, ...validatedAttributes } = validatedObject
|
|
432
|
+
|
|
414
433
|
// Update attributes with validated values
|
|
415
434
|
// Virtual fields have now been validated and cast properly
|
|
416
|
-
context.inputRecord.data.attributes =
|
|
435
|
+
context.inputRecord.data.attributes = validatedAttributes
|
|
417
436
|
|
|
418
437
|
await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`)
|
|
419
438
|
await runHooks('afterSchemaValidate')
|
|
@@ -434,8 +453,16 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
434
453
|
for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
|
|
435
454
|
if (!relData?.data) continue
|
|
436
455
|
|
|
456
|
+
const normalizedRelationshipData = normalizeRelationshipIdentifiers(relData.data, { api })
|
|
457
|
+
inputRecord.data.relationships[relName] = {
|
|
458
|
+
...relData,
|
|
459
|
+
data: normalizedRelationshipData
|
|
460
|
+
}
|
|
461
|
+
|
|
437
462
|
// Handle both single and array relationships
|
|
438
|
-
const relatedItems = Array.isArray(
|
|
463
|
+
const relatedItems = Array.isArray(normalizedRelationshipData)
|
|
464
|
+
? normalizedRelationshipData
|
|
465
|
+
: [normalizedRelationshipData]
|
|
439
466
|
|
|
440
467
|
for (const item of relatedItems) {
|
|
441
468
|
// Get the scope for the related resource
|
|
@@ -455,31 +482,18 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
455
482
|
isUpdate: false
|
|
456
483
|
}
|
|
457
484
|
|
|
458
|
-
const targetScope = api.resources?.[item.type]
|
|
459
|
-
const applyQueryFilters = targetScope?.applyQueryFilters
|
|
460
|
-
? async ({ query, filters, tableName, db, scopeName, storageAdapter, isAnyApi }) => {
|
|
461
|
-
const queryState = await targetScope.applyQueryFilters({
|
|
462
|
-
query,
|
|
463
|
-
filters,
|
|
464
|
-
schemaInfo: getContext.schemaInfo,
|
|
465
|
-
scopeName,
|
|
466
|
-
tableName,
|
|
467
|
-
db,
|
|
468
|
-
isAnyApi,
|
|
469
|
-
storageAdapter
|
|
470
|
-
}, getContext)
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
query: unwrapQueryBuilderState(queryState, query)
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
: undefined
|
|
477
|
-
|
|
478
485
|
// Get the minimal record
|
|
479
486
|
const record = await helpers.dataGetMinimal({
|
|
480
487
|
scopeName: item.type,
|
|
481
488
|
context: getContext,
|
|
482
|
-
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'
|
|
483
497
|
})
|
|
484
498
|
|
|
485
499
|
if (!record) {
|
|
@@ -506,6 +520,22 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
506
520
|
}
|
|
507
521
|
}
|
|
508
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
|
+
|
|
509
539
|
/**
|
|
510
540
|
* Applies field setters to transform attribute values before storage
|
|
511
541
|
* Setters are applied in dependency order to handle interdependent fields
|
|
@@ -552,8 +582,18 @@ export const applyFieldSetters = async (attributes, schemaInfo, context, api, he
|
|
|
552
582
|
setterContext
|
|
553
583
|
)
|
|
554
584
|
} catch (error) {
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
)
|
|
557
597
|
}
|
|
558
598
|
}
|
|
559
599
|
|
|
@@ -667,7 +707,7 @@ export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
|
|
|
667
707
|
}
|
|
668
708
|
|
|
669
709
|
// Then check schema fields for belongsTo relationships with matching 'as' property
|
|
670
|
-
for (const [
|
|
710
|
+
for (const [, fieldDef] of Object.entries(schemaInfo.schemaStructure || {})) {
|
|
671
711
|
if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
|
|
672
712
|
return fieldDef
|
|
673
713
|
}
|