json-rest-api 1.0.22 → 1.0.24
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/README.md +22 -1
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +196 -38
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +111 -5
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +370 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +97 -190
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1111 -160
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -3
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +21 -76
- package/plugins/core/lib/querying/knex-relationship-includes.js +468 -215
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +20 -5
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -14
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +133 -0
- package/plugins/core/lib/storage/storage-adapter.js +82 -4
- package/plugins/core/lib/storage/storage-mapping.js +274 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +531 -312
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +271 -226
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +148 -92
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +29 -14
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +56 -12
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +21 -8
- package/plugins/core/rest-api-plugin-methods/put.js +73 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +6 -0
- package/plugins/core/rest-api-positioning-plugin.js +2 -5
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -4
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +521 -0
- package/tests/db-table-operations.test.js +786 -0
- package/tests/fastify-plugin.test.js +156 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +278 -176
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +259 -0
- package/audit/00-charter.md +0 -103
- package/audit/01-scope-map.md +0 -84
- package/audit/02-coverage-matrix.csv +0 -174
- package/audit/03-invariants.md +0 -123
- package/audit/04-standards-rubric.md +0 -56
- package/audit/05-competitor-rubric.md +0 -77
- package/audit/06-findings.md +0 -160
- package/audit/07-risk-register.csv +0 -10
- package/audit/08-remediation-roadmap.md +0 -177
- package/audit/09-signoff-check.md +0 -23
- package/audit/10-final-synthesis.md +0 -29
- package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
- package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
- package/audit/evidence/code/boundary-leak-checks.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
- package/audit/evidence/code/critical-pass1-summary.csv +0 -67
- package/audit/evidence/code/critical-pass2-summary.csv +0 -67
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
- package/audit/evidence/code/error-model-signals.txt +0 -98
- package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
- package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
- package/audit/evidence/code/hook-flow-signals.txt +0 -282
- package/audit/evidence/code/idproperty-signals.txt +0 -157
- package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
- package/audit/evidence/code/imports-json-rest-api.txt +0 -354
- package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
- package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
- package/audit/evidence/code/permission-signals.txt +0 -193
- package/audit/evidence/code/query-include-signals.txt +0 -687
- package/audit/evidence/code/transaction-signals.txt +0 -28
- package/audit/evidence/code/validation-signals.txt +0 -650
- package/audit/evidence/code/write-path-signals.txt +0 -61
- package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
- package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
- package/audit/evidence/external/corpus-freeze.md +0 -21
- package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
- package/audit/evidence/external/npm-download-signals.txt +0 -23
- package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
- package/audit/evidence/external/readme-fortune-signals.txt +0 -26
- package/audit/evidence/external/readme-fortune.md +0 -165
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
- package/audit/evidence/external/readme-json-api-serializer.md +0 -690
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
- package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
- package/audit/evidence/external/readme-jsonapi-server.md +0 -104
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
- package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
- package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
- package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
- package/audit/evidence/external/standards-sources.txt +0 -6
- package/audit/evidence/runtime/baseline-commits.txt +0 -2
- package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
- package/audit/evidence/runtime/environment.txt +0 -4
- package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
- package/audit/evidence/runtime/test-exitcodes.txt +0 -4
- package/audit/evidence/runtime/test-summary.md +0 -107
- package/audit/evidence/runtime/trace-anchors.txt +0 -61
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
- package/audit/evidence/tests/failing-tests-summary.txt +0 -25
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
- package/audit/evidence/tests/query-include-test-signals.txt +0 -948
- package/audit/evidence/tests/relationship-test-signals.txt +0 -223
- package/audit/evidence/tests/skipped-tests.txt +0 -10
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
- package/audit/evidence/tests/test-suite-list.txt +0 -32
- package/audit/subsystems/docs-drift.md +0 -49
- package/audit/subsystems/pass2-cross-review.md +0 -131
- package/audit/subsystems/plugins-access.md +0 -51
- package/audit/subsystems/plugins-bulk.md +0 -50
- package/audit/subsystems/plugins-cors.md +0 -48
- package/audit/subsystems/plugins-file.md +0 -53
- package/audit/subsystems/plugins-multihome.md +0 -50
- package/audit/subsystems/plugins-positioning.md +0 -54
- package/audit/subsystems/plugins-socketio.md +0 -50
- package/audit/subsystems/query-engine.md +0 -61
- package/audit/subsystems/relationships.md +0 -55
- package/audit/subsystems/rest-orchestration.md +0 -66
- package/audit/subsystems/schema-core.md +0 -64
- package/audit/subsystems/storage-knex.md +0 -57
- package/audit/subsystems/tests.md +0 -48
- package/audit/subsystems/transport-express.md +0 -55
- package/audit/traces/trace-read-get.md +0 -22
- package/audit/traces/trace-read-query.md +0 -25
- package/audit/traces/trace-relationship-endpoints.md +0 -31
- package/audit/traces/trace-write-delete.md +0 -17
- package/audit/traces/trace-write-patch.md +0 -26
- package/audit/traces/trace-write-post.md +0 -22
- package/audit/traces/trace-write-put.md +0 -23
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt
DELETED
|
@@ -1,1135 +0,0 @@
|
|
|
1
|
-
1 import { requirePackage } from 'hooked-api'
|
|
2
|
-
2 import { createSchema } from 'json-rest-schema'
|
|
3
|
-
3 import { createKnexTable, addKnexFields, alterKnexFields } from './lib/dbTablesOperations.js'
|
|
4
|
-
4 import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js'
|
|
5
|
-
5 import { getForeignKeyFields } from './lib/querying-writing/field-utils.js'
|
|
6
|
-
6 import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js'
|
|
7
|
-
7 import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js'
|
|
8
|
-
8 import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js'
|
|
9
|
-
9 import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js'
|
|
10
|
-
10 import { processIncludes } from './lib/querying/knex-process-includes.js'
|
|
11
|
-
11 import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
|
|
12
|
-
12 import {
|
|
13
|
-
13 polymorphicFiltersHook,
|
|
14
|
-
14 crossTableFiltersHook,
|
|
15
|
-
15 basicFiltersHook
|
|
16
|
-
16 } from './lib/querying/knex-query-helpers.js'
|
|
17
|
-
17 import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
18
|
-
18 import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
|
|
19
|
-
19 import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
|
|
20
|
-
20 import {
|
|
21
|
-
21 calculatePaginationMeta,
|
|
22
|
-
22 generatePaginationLinks,
|
|
23
|
-
23 generateCursorPaginationLinks,
|
|
24
|
-
24 buildCursorMeta,
|
|
25
|
-
25 parseCursor
|
|
26
|
-
26 } from './lib/querying/knex-pagination-helpers.js'
|
|
27
|
-
27 import { getUrlPrefix } from './lib/querying/url-helpers.js'
|
|
28
|
-
28 import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
29
|
-
29
|
|
30
|
-
30 /**
|
|
31
|
-
31 * Strips non-database fields (computed and virtual) from attributes before database operations
|
|
32
|
-
32 * @param {Object} attributes - The attributes object
|
|
33
|
-
33 * @param {Object} schemaInfo - The schema info containing computed field definitions and schema structure
|
|
34
|
-
34 * @returns {Object} Attributes with computed and virtual fields removed
|
|
35
|
-
35 */
|
|
36
|
-
36 const stripNonDatabaseFields = (attributes, schemaInfo) => {
|
|
37
|
-
37 if (!attributes || !schemaInfo) return attributes || {}
|
|
38
|
-
38
|
|
39
|
-
39 const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
40
|
-
40 return Object.entries(attributes)
|
|
41
|
-
41 .filter(([key]) => {
|
|
42
|
-
42 // Remove computed fields
|
|
43
|
-
43 if (key in computed) return false
|
|
44
|
-
44 // Remove fields marked as virtual in the schema
|
|
45
|
-
45 const fieldDef = schemaStructure[key]
|
|
46
|
-
46 if (fieldDef && fieldDef.virtual === true) {
|
|
47
|
-
47 return false
|
|
48
|
-
48 }
|
|
49
|
-
49 return true
|
|
50
|
-
50 })
|
|
51
|
-
51 .reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {})
|
|
52
|
-
52 }
|
|
53
|
-
53
|
|
54
|
-
54 export const RestApiKnexPlugin = {
|
|
55
|
-
55 name: 'rest-api-knex',
|
|
56
|
-
56 dependencies: ['rest-api'],
|
|
57
|
-
57
|
|
58
|
-
58 async install ({ helpers, vars, pluginOptions, api, log, scopes, addHook, addScopeMethod }) {
|
|
59
|
-
59 // Try to import knex dynamically
|
|
60
|
-
60 let knexLib
|
|
61
|
-
61 try {
|
|
62
|
-
62 knexLib = await import('knex')
|
|
63
|
-
63 } catch (e) {
|
|
64
|
-
64 requirePackage('knex', 'rest-api-knex',
|
|
65
|
-
65 'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
|
|
66
|
-
66 }
|
|
67
|
-
67
|
|
68
|
-
68 // Get Knex instance from plugin options
|
|
69
|
-
69 const knexOptions = pluginOptions || {}
|
|
70
|
-
70 const knex = knexOptions.knex
|
|
71
|
-
71
|
|
72
|
-
72 // Expose Knex instance and helpers in a structured way
|
|
73
|
-
73 api.knex = {
|
|
74
|
-
74 instance: knex,
|
|
75
|
-
75 helpers: {}
|
|
76
|
-
76 }
|
|
77
|
-
77
|
|
78
|
-
78 const storageAdapters = new Map()
|
|
79
|
-
79
|
|
80
|
-
80 const getScopeStorageAdapter = (scopeName) => {
|
|
81
|
-
81 if (!scopeName) return null
|
|
82
|
-
82 const resource = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
83
|
-
83 const schemaInfo = resource?.vars?.schemaInfo
|
|
84
|
-
84 if (!schemaInfo) return null
|
|
85
|
-
85
|
|
86
|
-
86 const cached = storageAdapters.get(scopeName)
|
|
87
|
-
87 if (cached && cached.schemaInfo === schemaInfo) {
|
|
88
|
-
88 return cached.adapter
|
|
89
|
-
89 }
|
|
90
|
-
90
|
|
91
|
-
91 const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
92
|
-
92 storageAdapters.set(scopeName, { adapter, schemaInfo })
|
|
93
|
-
93 if (resource?.vars) {
|
|
94
|
-
94 resource.vars.storageAdapter = adapter
|
|
95
|
-
95 }
|
|
96
|
-
96 return adapter
|
|
97
|
-
97 }
|
|
98
|
-
98
|
|
99
|
-
99 api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
100
|
-
100 helpers.getStorageAdapter = getScopeStorageAdapter
|
|
101
|
-
101
|
|
102
|
-
102 const createSelectTranslator = (adapter) => {
|
|
103
|
-
103 if (!adapter) return null
|
|
104
|
-
104 return (field, alias) => {
|
|
105
|
-
105 if (field === '*') {
|
|
106
|
-
106 return alias ? `${alias}.*` : '*'
|
|
107
|
-
107 }
|
|
108
|
-
108 const translated = adapter.translateColumn(field)
|
|
109
|
-
109 const result = translated || field
|
|
110
|
-
110 if (!alias) {
|
|
111
|
-
111 return result
|
|
112
|
-
112 }
|
|
113
|
-
113 if (result.includes('.')) {
|
|
114
|
-
114 return result
|
|
115
|
-
115 }
|
|
116
|
-
116 return `${alias}.${result}`
|
|
117
|
-
117 }
|
|
118
|
-
118 }
|
|
119
|
-
119
|
|
120
|
-
120 // Check database capabilities
|
|
121
|
-
121 const hasWindowFunctions = await supportsWindowFunctions(knex)
|
|
122
|
-
122 const dbInfo = await getDatabaseInfo(knex)
|
|
123
|
-
123
|
|
124
|
-
124 // Store capabilities in API instance for access throughout
|
|
125
|
-
125 api.knex.capabilities = {
|
|
126
|
-
126 windowFunctions: hasWindowFunctions,
|
|
127
|
-
127 dbInfo
|
|
128
|
-
128 }
|
|
129
|
-
129
|
|
130
|
-
130 log.info('Database capabilities detected:', {
|
|
131
|
-
131 database: dbInfo.client,
|
|
132
|
-
132 version: dbInfo.version,
|
|
133
|
-
133 windowFunctions: hasWindowFunctions
|
|
134
|
-
134 })
|
|
135
|
-
135
|
|
136
|
-
136 // Cross-table search functions are now imported directly and used with full signatures
|
|
137
|
-
137
|
|
138
|
-
138 /* ╔═════════════════════════════════════════════════════════════════════╗
|
|
139
|
-
139 * ║ MAIN QUERY FILTERING HOOK ║
|
|
140
|
-
140 * ║ This is the heart of the filtering system. It processes searchSchema ║
|
|
141
|
-
141 * ║ filters and builds SQL WHERE conditions with proper JOINs ║
|
|
142
|
-
142 * ╚═════════════════════════════════════════════════════════════════════╝ */
|
|
143
|
-
143
|
|
144
|
-
144 // Register the three separate filter hooks
|
|
145
|
-
145 // Dependencies object for the hooks
|
|
146
|
-
146 const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
147
|
-
147
|
|
148
|
-
148 // Register in specific order: polymorphic → cross-table → basic
|
|
149
|
-
149 // This ensures proper field qualification when JOINs are present
|
|
150
|
-
150
|
|
151
|
-
151 // 1. Polymorphic filters (adds JOINs for polymorphic relationships)
|
|
152
|
-
152 addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
|
|
153
|
-
153 async (hookParams) => polymorphicFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
154
|
-
154 )
|
|
155
|
-
155
|
|
156
|
-
156 // 2. Cross-table filters (adds JOINs for cross-table fields)
|
|
157
|
-
157 addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
|
|
158
|
-
158 async (hookParams) => crossTableFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
159
|
-
159 )
|
|
160
|
-
160
|
|
161
|
-
161 // 3. Basic filters (processes simple main table filters)
|
|
162
|
-
162 addHook('knexQueryFiltering', 'basicFiltersHook', {},
|
|
163
|
-
163 async (hookParams) => basicFiltersHook(hookParams, polymorphicFiltersHookParams)
|
|
164
|
-
164 )
|
|
165
|
-
165
|
|
166
|
-
166 // 3. Basic filters (processes simple main table filters)
|
|
167
|
-
167 addHook('release', 'releaseHook', {},
|
|
168
|
-
168 async ({ api }) => api.knex.instance.destroy()
|
|
169
|
-
169 )
|
|
170
|
-
170
|
|
171
|
-
171 // Helper scope method to get all schema-related information
|
|
172
|
-
172 addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
|
|
173
|
-
173 // Create a filtered schema that excludes virtual fields
|
|
174
|
-
174 const schemaStructure = vars.schemaInfo.schemaStructure || {}
|
|
175
|
-
175 const filteredSchema = {}
|
|
176
|
-
176
|
|
177
|
-
177 // Copy only non-virtual fields to the filtered schema
|
|
178
|
-
178 Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
179
|
-
179 if (!fieldDef.virtual) {
|
|
180
|
-
180 filteredSchema[fieldName] = fieldDef
|
|
181
|
-
181 }
|
|
182
|
-
182 })
|
|
183
|
-
183
|
|
184
|
-
184 // Create schema object from filtered fields
|
|
185
|
-
185 const tableSchemaInstance = createSchema(filteredSchema)
|
|
186
|
-
186
|
|
187
|
-
187 await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
|
|
188
|
-
188 })
|
|
189
|
-
189
|
|
190
|
-
190 // Helper scope method to alter existing fields in a table
|
|
191
|
-
191 addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
192
|
-
192 // Validate required parameters
|
|
193
|
-
193 if (!params.fields || typeof params.fields !== 'object') {
|
|
194
|
-
194 throw new Error('fields parameter is required for alterKnexFields')
|
|
195
|
-
195 }
|
|
196
|
-
196
|
|
197
|
-
197 await alterKnexFields(
|
|
198
|
-
198 api.knex.instance,
|
|
199
|
-
199 vars.schemaInfo.tableName,
|
|
200
|
-
200 params.fields,
|
|
201
|
-
201 params.options // Pass through any additional options
|
|
202
|
-
202 )
|
|
203
|
-
203 })
|
|
204
|
-
204
|
|
205
|
-
205 // Helper scope method to add a field to an existing table
|
|
206
|
-
206 addScopeMethod('addKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
207
|
-
207 // Create schema object from filtered fields
|
|
208
|
-
208 const partialTableSchema = createSchema(params.fields)
|
|
209
|
-
209
|
|
210
|
-
210 await addKnexFields(api.knex.instance, vars.schemaInfo.tableName, partialTableSchema)
|
|
211
|
-
211 })
|
|
212
|
-
212
|
|
213
|
-
213 helpers.newTransaction = async () => {
|
|
214
|
-
214 return knex.transaction()
|
|
215
|
-
215 }
|
|
216
|
-
216
|
|
217
|
-
217 /* ╔═════════════════════════════════════════════════════════════════════╗
|
|
218
|
-
218 * ║ DATA OPERATION METHODS ║
|
|
219
|
-
219 * ║ Implementation of the storage interface required by REST API plugin║
|
|
220
|
-
220 * ╚═════════════════════════════════════════════════════════════════════╝ */
|
|
221
|
-
221
|
|
222
|
-
222 /**
|
|
223
|
-
223 * Checks if a resource exists in the database
|
|
224
|
-
224 *
|
|
225
|
-
225 * @param {Object} params - The parameters object
|
|
226
|
-
226 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
227
|
-
227 * @param {Object} params.context - The context object containing request-specific data
|
|
228
|
-
228 * @param {string|number} params.context.id - The ID of the resource to check for existence
|
|
229
|
-
229 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
230
|
-
230 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
231
|
-
231 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
232
|
-
232 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
233
|
-
233 * @returns {Promise<boolean>} True if the resource exists, false otherwise
|
|
234
|
-
234 */
|
|
235
|
-
235 helpers.dataExists = async ({ scopeName, context }) => {
|
|
236
|
-
236 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
237
|
-
237 if (storageAdapter) {
|
|
238
|
-
238 context.storageAdapter = storageAdapter
|
|
239
|
-
239 }
|
|
240
|
-
240
|
|
241
|
-
241 const id = context.id
|
|
242
|
-
242
|
|
243
|
-
243 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
244
|
-
244 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
245
|
-
245 const db = context.db || api.knex.instance
|
|
246
|
-
246
|
|
247
|
-
247 log.debug(`[Knex] EXISTS ${tableName}/${id}`)
|
|
248
|
-
248
|
|
249
|
-
249 const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
|
|
250
|
-
250
|
|
251
|
-
251 const record = await db(tableName)
|
|
252
|
-
252 .where(idProperty, id)
|
|
253
|
-
253 .select(selectClause)
|
|
254
|
-
254 .first()
|
|
255
|
-
255
|
|
256
|
-
256 return !!record
|
|
257
|
-
257 }
|
|
258
|
-
258
|
|
259
|
-
259 /**
|
|
260
|
-
260 * Retrieves a single resource by ID with support for sparse fieldsets and includes
|
|
261
|
-
261 *
|
|
262
|
-
262 * @param {Object} params - The parameters object
|
|
263
|
-
263 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
264
|
-
264 * @param {Object} params.context - The context object containing request-specific data
|
|
265
|
-
265 * @param {string|number} params.context.id - The ID of the resource to retrieve
|
|
266
|
-
266 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
267
|
-
267 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
268
|
-
268 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
269
|
-
269 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
270
|
-
270 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
271
|
-
271 * @param {Object} [params.context.queryParams] - Query parameters for sparse fieldsets and includes
|
|
272
|
-
272 * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
|
|
273
|
-
273 * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
|
|
274
|
-
274 * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
|
|
275
|
-
275 * @returns {Promise<Object>} JSON:API formatted response with data and optional included resources
|
|
276
|
-
276 * @throws {RestApiResourceError} When the resource is not found
|
|
277
|
-
277 */
|
|
278
|
-
278 helpers.dataGet = async ({ scopeName, context, runHooks }) => {
|
|
279
|
-
279 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
280
|
-
280 if (storageAdapter) {
|
|
281
|
-
281 context.storageAdapter = storageAdapter
|
|
282
|
-
282 }
|
|
283
|
-
283 const scope = api.resources[scopeName]
|
|
284
|
-
284 if (!scope) {
|
|
285
|
-
285 log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
|
|
286
|
-
286 throw new Error(`Scope '${scopeName}' not found in api.resources`)
|
|
287
|
-
287 }
|
|
288
|
-
288 if (!scope.scopeName && !scope.name) {
|
|
289
|
-
289 log.debug('[DATA-GET] Scope structure:', {
|
|
290
|
-
290 scopeKeys: Object.keys(scope),
|
|
291
|
-
291 scopeName,
|
|
292
|
-
292 hasVars: !!scope.vars,
|
|
293
|
-
293 varKeys: scope.vars ? Object.keys(scope.vars) : []
|
|
294
|
-
294 })
|
|
295
|
-
295 }
|
|
296
|
-
296 const id = context.id
|
|
297
|
-
297 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
298
|
-
298 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
299
|
-
299 const db = context.db || api.knex.instance
|
|
300
|
-
300
|
|
301
|
-
301 log.debug(`[Knex] GET ${tableName}/${id}`)
|
|
302
|
-
302
|
|
303
|
-
303 // Build field selection for sparse fieldsets
|
|
304
|
-
304 // This determines which fields to SELECT from database
|
|
305
|
-
305 // and tracks dependencies needed for computed fields
|
|
306
|
-
306 const fieldSelectionInfo = await buildFieldSelection(
|
|
307
|
-
307 scope,
|
|
308
|
-
308 { context }
|
|
309
|
-
309 )
|
|
310
|
-
310
|
|
311
|
-
311 // Store dependency info in context for enrichAttributes
|
|
312
|
-
312 // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
313
|
-
313 // The REST API plugin will use this to remove 'cost' from response if not requested
|
|
314
|
-
314 context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
315
|
-
315
|
|
316
|
-
316 // Build query - no filtering hooks for single records
|
|
317
|
-
317 // Permission checks will handle access control
|
|
318
|
-
318 let query = db(tableName).where(idProperty, id)
|
|
319
|
-
319
|
|
320
|
-
320 // Apply field selection
|
|
321
|
-
321 const selectTranslator = createSelectTranslator(storageAdapter)
|
|
322
|
-
322 query = buildQuerySelection(
|
|
323
|
-
323 query,
|
|
324
|
-
324 tableName,
|
|
325
|
-
325 fieldSelectionInfo.fieldsToSelect,
|
|
326
|
-
326 false,
|
|
327
|
-
327 selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
328
|
-
328 )
|
|
329
|
-
329
|
|
330
|
-
330 const record = await query.first()
|
|
331
|
-
331
|
|
332
|
-
332 if (!record) {
|
|
333
|
-
333 throw new RestApiResourceError(
|
|
334
|
-
334 'Resource not found',
|
|
335
|
-
335 {
|
|
336
|
-
336 subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
337
|
-
337 resourceType: scopeName,
|
|
338
|
-
338 resourceId: id
|
|
339
|
-
339 }
|
|
340
|
-
340 )
|
|
341
|
-
341 }
|
|
342
|
-
342
|
|
343
|
-
343 // Load relationship identifiers for all hasMany relationships
|
|
344
|
-
344 const records = [record] // Wrap in array for processing
|
|
345
|
-
345 await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
346
|
-
346
|
|
347
|
-
347 // Process includes
|
|
348
|
-
348 const included = await processIncludes(scope, records, {
|
|
349
|
-
349 log,
|
|
350
|
-
350 scopes,
|
|
351
|
-
351 knex,
|
|
352
|
-
352 context
|
|
353
|
-
353 })
|
|
354
|
-
354
|
|
355
|
-
355 // Build and return response
|
|
356
|
-
356 return buildJsonApiResponse(scope, records, included, true, scopeName, context)
|
|
357
|
-
357 }
|
|
358
|
-
358
|
|
359
|
-
359 /**
|
|
360
|
-
360 * Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
|
|
361
|
-
361 *
|
|
362
|
-
362 * @param {Object} params - The parameters object
|
|
363
|
-
363 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
364
|
-
364 * @param {Object} params.context - The context object containing request-specific data
|
|
365
|
-
365 * @param {string|number} params.context.id - The ID of the resource to retrieve
|
|
366
|
-
366 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
367
|
-
367 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
368
|
-
368 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
369
|
-
369 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
370
|
-
370 * @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
|
|
371
|
-
371 */
|
|
372
|
-
372 helpers.dataGetMinimal = async ({ scopeName, context }) => {
|
|
373
|
-
373 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
374
|
-
374 if (storageAdapter) {
|
|
375
|
-
375 context.storageAdapter = storageAdapter
|
|
376
|
-
376 }
|
|
377
|
-
377 const scope = api.resources[scopeName]
|
|
378
|
-
378 const id = context.id
|
|
379
|
-
379 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
380
|
-
380 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
381
|
-
381 const db = context.db || api.knex.instance
|
|
382
|
-
382
|
|
383
|
-
383 log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
|
|
384
|
-
384
|
|
385
|
-
385 // Build query - no filtering hooks for single records
|
|
386
|
-
386 // Permission checks will handle access control
|
|
387
|
-
387 let query = db(tableName).where(idProperty, id)
|
|
388
|
-
388
|
|
389
|
-
389 // Add alias if idProperty is not 'id'
|
|
390
|
-
390 if (idProperty !== 'id') {
|
|
391
|
-
391 query = query.select('*', `${idProperty} as id`)
|
|
392
|
-
392 }
|
|
393
|
-
393
|
|
394
|
-
394 // Execute query
|
|
395
|
-
395 const record = await query.first()
|
|
396
|
-
396
|
|
397
|
-
397 if (!record) {
|
|
398
|
-
398 return null
|
|
399
|
-
399 }
|
|
400
|
-
400
|
|
401
|
-
401 // Transform to JSON:API format with belongsTo relationships
|
|
402
|
-
402 return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
|
|
403
|
-
403 }
|
|
404
|
-
404
|
|
405
|
-
405 /**
|
|
406
|
-
406 * Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
|
|
407
|
-
407 *
|
|
408
|
-
408 * @param {Object} params - The parameters object
|
|
409
|
-
409 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
410
|
-
410 * @param {Object} params.context - The context object containing request-specific data
|
|
411
|
-
411 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
412
|
-
412 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
413
|
-
413 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
414
|
-
414 * @param {Object} params.context.schemaInfo.searchSchemaInstance - Search schema for filtering capabilities
|
|
415
|
-
415 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
416
|
-
416 * @param {Object} params.context.queryParams - Query parameters object
|
|
417
|
-
417 * @param {Object} [params.context.queryParams.filters] - Filter conditions
|
|
418
|
-
418 * @param {Array<string>} [params.context.queryParams.sort] - Sort fields (prefix with - for DESC)
|
|
419
|
-
419 * @param {Object} [params.context.queryParams.page] - Pagination parameters
|
|
420
|
-
420 * @param {number} [params.context.queryParams.page.size] - Page size
|
|
421
|
-
421 * @param {number} [params.context.queryParams.page.number] - Page number (offset pagination)
|
|
422
|
-
422 * @param {string} [params.context.queryParams.page.after] - Cursor for forward pagination
|
|
423
|
-
423 * @param {string} [params.context.queryParams.page.before] - Cursor for backward pagination
|
|
424
|
-
424 * @param {Array<string>} [params.context.queryParams.include] - Related resources to include
|
|
425
|
-
425 * @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
|
|
426
|
-
426 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
427
|
-
427 * @param {Array<string>} params.context.sortableFields - Array of fields that can be sorted
|
|
428
|
-
428 * @param {Object} [params.context.knexQuery] - Temporarily set during hooks for query building
|
|
429
|
-
429 * @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
|
|
430
|
-
430 * @param {Function} params.runHooks - Function to run hooks (e.g., 'knexQueryFiltering')
|
|
431
|
-
431 * @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
|
|
432
|
-
432 */
|
|
433
|
-
433 helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
434
|
-
434 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
435
|
-
435 if (storageAdapter) {
|
|
436
|
-
436 context.storageAdapter = storageAdapter
|
|
437
|
-
437 }
|
|
438
|
-
438 const scope = api.resources[scopeName]
|
|
439
|
-
439 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
440
|
-
440 const schemaInfo = context.schemaInfo
|
|
441
|
-
441 const queryParams = context.queryParams
|
|
442
|
-
442 const db = context.db || api.knex.instance
|
|
443
|
-
443 const sortableFields = context.sortableFields
|
|
444
|
-
444
|
|
445
|
-
445 log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
|
|
446
|
-
446 log.debug(`[Knex] QUERY ${tableName}`, queryParams)
|
|
447
|
-
447
|
|
448
|
-
448 // Build field selection for sparse fieldsets
|
|
449
|
-
449 // This determines which fields to SELECT from database
|
|
450
|
-
450 // and tracks dependencies needed for computed fields
|
|
451
|
-
451 const fieldSelectionInfo = await buildFieldSelection(
|
|
452
|
-
452 scope,
|
|
453
|
-
453 { context }
|
|
454
|
-
454 )
|
|
455
|
-
455
|
|
456
|
-
456 // Store dependency info in context for enrichAttributes
|
|
457
|
-
457 // Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
458
|
-
458 // The REST API plugin will use this to remove 'cost' from response if not requested
|
|
459
|
-
459 context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
460
|
-
460
|
|
461
|
-
461 // Start building query with table prefix (for JOIN support)
|
|
462
|
-
462 let query = db(tableName)
|
|
463
|
-
463
|
|
464
|
-
464 const selectTranslator = createSelectTranslator(storageAdapter)
|
|
465
|
-
465 query = buildQuerySelection(
|
|
466
|
-
466 query,
|
|
467
|
-
467 tableName,
|
|
468
|
-
468 fieldSelectionInfo.fieldsToSelect,
|
|
469
|
-
469 true,
|
|
470
|
-
470 selectTranslator ? { translateColumn: selectTranslator } : undefined
|
|
471
|
-
471 )
|
|
472
|
-
472
|
|
473
|
-
473 /* ═══════════════════════════════════════════════════════════════════
|
|
474
|
-
474 * FILTERING HOOKS
|
|
475
|
-
475 * This is where the magic happens. The knexQueryFiltering hook is called
|
|
476
|
-
476 * to apply all filter conditions. The searchSchemaFilter hook (registered
|
|
477
|
-
477 * above) will process the searchSchema and apply filters with JOINs.
|
|
478
|
-
478 *
|
|
479
|
-
479 * IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
|
|
480
|
-
480 * to ensure proper grouping and prevent accidental filter bypass.
|
|
481
|
-
481 * ═══════════════════════════════════════════════════════════════════ */
|
|
482
|
-
482
|
|
483
|
-
483 log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
|
|
484
|
-
484
|
|
485
|
-
485 log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
|
|
486
|
-
486
|
|
487
|
-
487 log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
|
|
488
|
-
488
|
|
489
|
-
489 // YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
|
|
490
|
-
490
|
|
491
|
-
491 // Store the query data in context where hooks can access it
|
|
492
|
-
492 // This is the proper way to share data between methods and hooks
|
|
493
|
-
493 if (context) {
|
|
494
|
-
494 context.knexQuery = {
|
|
495
|
-
495 query,
|
|
496
|
-
496 filters: queryParams.filters,
|
|
497
|
-
497 schemaInfo,
|
|
498
|
-
498 scopeName,
|
|
499
|
-
499 tableName,
|
|
500
|
-
500 db,
|
|
501
|
-
501 adapter: storageAdapter,
|
|
502
|
-
502 }
|
|
503
|
-
503
|
|
504
|
-
504 log.trace('[DATA-QUERY] Stored data in context', { hasStoredData: !!context.knexQuery, filters: queryParams.filters })
|
|
505
|
-
505 }
|
|
506
|
-
506
|
|
507
|
-
507 await runHooks('knexQueryFiltering')
|
|
508
|
-
508
|
|
509
|
-
509 // Clean up after hook execution
|
|
510
|
-
510 if (context && context.knexQuery) {
|
|
511
|
-
511 delete context.knexQuery
|
|
512
|
-
512 }
|
|
513
|
-
513
|
|
514
|
-
514 log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
|
|
515
|
-
515
|
|
516
|
-
516 // Apply sorting directly (no hooks)
|
|
517
|
-
517 if (queryParams.sort && queryParams.sort.length > 0) {
|
|
518
|
-
518 queryParams.sort.forEach(sortField => {
|
|
519
|
-
519 const desc = sortField.startsWith('-')
|
|
520
|
-
520 const field = desc ? sortField.substring(1) : sortField
|
|
521
|
-
521
|
|
522
|
-
522 // Check if field is sortable
|
|
523
|
-
523 if (sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
|
|
524
|
-
524 log.warn(`Ignoring non-sortable field: ${field}`)
|
|
525
|
-
525 return // Skip non-sortable fields
|
|
526
|
-
526 }
|
|
527
|
-
527
|
|
528
|
-
528 // Map relationship names to actual database columns for sorting
|
|
529
|
-
529 // Check if this is a relationship field that needs to be mapped
|
|
530
|
-
530 let dbField = field
|
|
531
|
-
531 const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
532
|
-
532 if (searchField?.actualField) {
|
|
533
|
-
533 // This is a relationship field, use the actual database column
|
|
534
|
-
534 dbField = searchField.actualField
|
|
535
|
-
535 }
|
|
536
|
-
536 const translatedField = storageAdapter?.translateColumn
|
|
537
|
-
537 ? storageAdapter.translateColumn(dbField)
|
|
538
|
-
538 : dbField
|
|
539
|
-
539
|
|
540
|
-
540 query.orderBy(translatedField, desc ? 'desc' : 'asc')
|
|
541
|
-
541 })
|
|
542
|
-
542 }
|
|
543
|
-
543
|
|
544
|
-
544 // Apply pagination
|
|
545
|
-
545 // Check if page object has any actual pagination parameters
|
|
546
|
-
546 const hasPageParams = queryParams.page &&
|
|
547
|
-
547 (queryParams.page.size !== undefined ||
|
|
548
|
-
548 queryParams.page.number !== undefined ||
|
|
549
|
-
549 queryParams.page.after !== undefined ||
|
|
550
|
-
550 queryParams.page.before !== undefined)
|
|
551
|
-
551
|
|
552
|
-
552 if (hasPageParams) {
|
|
553
|
-
553 const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
554
|
-
554 const pageSize = Math.min(
|
|
555
|
-
555 requestedSize,
|
|
556
|
-
556 scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
557
|
-
557 )
|
|
558
|
-
558
|
|
559
|
-
559 // Validate page size
|
|
560
|
-
560 if (requestedSize <= 0) {
|
|
561
|
-
561 throw new RestApiValidationError(
|
|
562
|
-
562 'Page size must be greater than 0',
|
|
563
|
-
563 {
|
|
564
|
-
564 fields: ['page.size'],
|
|
565
|
-
565 violations: [{
|
|
566
|
-
566 field: 'page.size',
|
|
567
|
-
567 rule: 'min_value',
|
|
568
|
-
568 message: 'Page size must be a positive number'
|
|
569
|
-
569 }]
|
|
570
|
-
570 }
|
|
571
|
-
571 )
|
|
572
|
-
572 }
|
|
573
|
-
573
|
|
574
|
-
574 // Offset-based pagination
|
|
575
|
-
575 if (queryParams.page.number !== undefined) {
|
|
576
|
-
576 const pageNumber = queryParams.page.number || 1
|
|
577
|
-
577 query
|
|
578
|
-
578 .limit(pageSize)
|
|
579
|
-
579 .offset((pageNumber - 1) * pageSize)
|
|
580
|
-
580 }
|
|
581
|
-
581 // Cursor-based pagination
|
|
582
|
-
582 else if (queryParams.page.after || queryParams.page.before) {
|
|
583
|
-
583 // Fetch one extra record to determine if there are more
|
|
584
|
-
584 query.limit(pageSize + 1)
|
|
585
|
-
585
|
|
586
|
-
586 // Get the configured ID property
|
|
587
|
-
587 const idProperty = context.schemaInfo.idProperty || 'id'
|
|
588
|
-
588
|
|
589
|
-
589 if (queryParams.page.after) {
|
|
590
|
-
590 let cursorData
|
|
591
|
-
591 try {
|
|
592
|
-
592 cursorData = parseCursor(queryParams.page.after)
|
|
593
|
-
593 } catch (error) {
|
|
594
|
-
594 throw new RestApiValidationError(
|
|
595
|
-
595 'Invalid cursor format in page[after] parameter',
|
|
596
|
-
596 {
|
|
597
|
-
597 fields: ['page.after'],
|
|
598
|
-
598 violations: [{
|
|
599
|
-
599 field: 'page.after',
|
|
600
|
-
600 rule: 'invalid_cursor',
|
|
601
|
-
601 message: 'The cursor value is not valid'
|
|
602
|
-
602 }]
|
|
603
|
-
603 }
|
|
604
|
-
604 )
|
|
605
|
-
605 }
|
|
606
|
-
606
|
|
607
|
-
607 // Build sort fields array with directions
|
|
608
|
-
608 const sortFields = []
|
|
609
|
-
609 if (queryParams.sort && queryParams.sort.length > 0) {
|
|
610
|
-
610 queryParams.sort.forEach(sortField => {
|
|
611
|
-
611 const desc = sortField.startsWith('-')
|
|
612
|
-
612 const field = desc ? sortField.substring(1) : sortField
|
|
613
|
-
613 sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
614
|
-
614 })
|
|
615
|
-
615 } else if (scope.vars.defaultSort) {
|
|
616
|
-
616 sortFields.push({
|
|
617
|
-
617 field: scope.vars.defaultSort.field || idProperty,
|
|
618
|
-
618 direction: scope.vars.defaultSort.direction || 'ASC'
|
|
619
|
-
619 })
|
|
620
|
-
620 } else {
|
|
621
|
-
621 sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
622
|
-
622 }
|
|
623
|
-
623
|
|
624
|
-
624 // Build compound WHERE clause for multi-field cursor
|
|
625
|
-
625 query.where(function () {
|
|
626
|
-
626 sortFields.forEach((sortInfo, index) => {
|
|
627
|
-
627 const { field, direction } = sortInfo
|
|
628
|
-
628 const qualifiedField = `${tableName}.${field}`
|
|
629
|
-
629 const cursorValue = cursorData[field]
|
|
630
|
-
630
|
|
631
|
-
631 if (cursorValue === undefined) {
|
|
632
|
-
632 log.warn(`Cursor missing value for sort field: ${field}`)
|
|
633
|
-
633 return
|
|
634
|
-
634 }
|
|
635
|
-
635
|
|
636
|
-
636 // Build condition for this level and all previous levels
|
|
637
|
-
637 this.orWhere(function () {
|
|
638
|
-
638 // All previous fields must be equal
|
|
639
|
-
639 for (let i = 0; i < index; i++) {
|
|
640
|
-
640 const prevField = sortFields[i].field
|
|
641
|
-
641 const prevQualifiedField = `${tableName}.${prevField}`
|
|
642
|
-
642 const prevValue = cursorData[prevField]
|
|
643
|
-
643 if (prevValue !== undefined) {
|
|
644
|
-
644 this.where(prevQualifiedField, '=', prevValue)
|
|
645
|
-
645 }
|
|
646
|
-
646 }
|
|
647
|
-
647
|
|
648
|
-
648 // Current field must be greater/less than cursor value
|
|
649
|
-
649 if (direction === 'DESC') {
|
|
650
|
-
650 this.where(qualifiedField, '<', cursorValue)
|
|
651
|
-
651 } else {
|
|
652
|
-
652 this.where(qualifiedField, '>', cursorValue)
|
|
653
|
-
653 }
|
|
654
|
-
654 })
|
|
655
|
-
655 })
|
|
656
|
-
656 })
|
|
657
|
-
657 } else if (queryParams.page.before) {
|
|
658
|
-
658 let cursorData
|
|
659
|
-
659 try {
|
|
660
|
-
660 cursorData = parseCursor(queryParams.page.before)
|
|
661
|
-
661 } catch (error) {
|
|
662
|
-
662 throw new RestApiValidationError(
|
|
663
|
-
663 'Invalid cursor format in page[before] parameter',
|
|
664
|
-
664 {
|
|
665
|
-
665 fields: ['page.before'],
|
|
666
|
-
666 violations: [{
|
|
667
|
-
667 field: 'page.before',
|
|
668
|
-
668 rule: 'invalid_cursor',
|
|
669
|
-
669 message: 'The cursor value is not valid'
|
|
670
|
-
670 }]
|
|
671
|
-
671 }
|
|
672
|
-
672 )
|
|
673
|
-
673 }
|
|
674
|
-
674
|
|
675
|
-
675 // Build sort fields array with directions
|
|
676
|
-
676 const sortFields = []
|
|
677
|
-
677 if (queryParams.sort && queryParams.sort.length > 0) {
|
|
678
|
-
678 queryParams.sort.forEach(sortField => {
|
|
679
|
-
679 const desc = sortField.startsWith('-')
|
|
680
|
-
680 const field = desc ? sortField.substring(1) : sortField
|
|
681
|
-
681 sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' })
|
|
682
|
-
682 })
|
|
683
|
-
683 } else if (scope.vars.defaultSort) {
|
|
684
|
-
684 sortFields.push({
|
|
685
|
-
685 field: scope.vars.defaultSort.field || idProperty,
|
|
686
|
-
686 direction: scope.vars.defaultSort.direction || 'ASC'
|
|
687
|
-
687 })
|
|
688
|
-
688 } else {
|
|
689
|
-
689 sortFields.push({ field: idProperty, direction: 'ASC' })
|
|
690
|
-
690 }
|
|
691
|
-
691
|
|
692
|
-
692 // Build compound WHERE clause for multi-field cursor (reversed for before)
|
|
693
|
-
693 query.where(function () {
|
|
694
|
-
694 sortFields.forEach((sortInfo, index) => {
|
|
695
|
-
695 const { field, direction } = sortInfo
|
|
696
|
-
696 const qualifiedField = `${tableName}.${field}`
|
|
697
|
-
697 const cursorValue = cursorData[field]
|
|
698
|
-
698
|
|
699
|
-
699 if (cursorValue === undefined) {
|
|
700
|
-
700 log.warn(`Cursor missing value for sort field: ${field}`)
|
|
701
|
-
701 return
|
|
702
|
-
702 }
|
|
703
|
-
703
|
|
704
|
-
704 // Build condition for this level and all previous levels
|
|
705
|
-
705 this.orWhere(function () {
|
|
706
|
-
706 // All previous fields must be equal
|
|
707
|
-
707 for (let i = 0; i < index; i++) {
|
|
708
|
-
708 const prevField = sortFields[i].field
|
|
709
|
-
709 const prevQualifiedField = `${tableName}.${prevField}`
|
|
710
|
-
710 const prevValue = cursorData[prevField]
|
|
711
|
-
711 if (prevValue !== undefined) {
|
|
712
|
-
712 this.where(prevQualifiedField, '=', prevValue)
|
|
713
|
-
713 }
|
|
714
|
-
714 }
|
|
715
|
-
715
|
|
716
|
-
716 // Current field must be less/greater than cursor value (reversed)
|
|
717
|
-
717 if (direction === 'DESC') {
|
|
718
|
-
718 this.where(qualifiedField, '>', cursorValue)
|
|
719
|
-
719 } else {
|
|
720
|
-
720 this.where(qualifiedField, '<', cursorValue)
|
|
721
|
-
721 }
|
|
722
|
-
722 })
|
|
723
|
-
723 })
|
|
724
|
-
724 })
|
|
725
|
-
725 }
|
|
726
|
-
726 }
|
|
727
|
-
727 // Default pagination if only size is specified - treat as cursor-based for "load more"
|
|
728
|
-
728 else {
|
|
729
|
-
729 // Fetch one extra to detect hasMore
|
|
730
|
-
730 query.limit(pageSize + 1)
|
|
731
|
-
731 }
|
|
732
|
-
732 } else {
|
|
733
|
-
733 // No pagination params provided - apply default limit
|
|
734
|
-
734 const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
735
|
-
735 query.limit(defaultLimit)
|
|
736
|
-
736 }
|
|
737
|
-
737
|
|
738
|
-
738 // Execute query
|
|
739
|
-
739 const records = await query
|
|
740
|
-
740
|
|
741
|
-
741 // Store query string for response building
|
|
742
|
-
742 const queryParts = []
|
|
743
|
-
743 Object.entries(queryParams).forEach(([key, value]) => {
|
|
744
|
-
744 if (Array.isArray(value)) {
|
|
745
|
-
745 // Handle arrays (like sort, include)
|
|
746
|
-
746 if (value.length > 0) {
|
|
747
|
-
747 queryParts.push(`${key}=${value.map(v => encodeURIComponent(v)).join(',')}`)
|
|
748
|
-
748 }
|
|
749
|
-
749 } else if (typeof value === 'object' && value !== null) {
|
|
750
|
-
750 // Handle nested objects (like filters, fields, page)
|
|
751
|
-
751 Object.entries(value).forEach(([subKey, subValue]) => {
|
|
752
|
-
752 if (subValue !== undefined && subValue !== null) {
|
|
753
|
-
753 if (typeof subValue === 'object' && !Array.isArray(subValue)) {
|
|
754
|
-
754 // Handle deeply nested objects (like filters[country][code])
|
|
755
|
-
755 Object.entries(subValue).forEach(([subSubKey, subSubValue]) => {
|
|
756
|
-
756 if (subSubValue !== undefined && subSubValue !== null) {
|
|
757
|
-
757 queryParts.push(`${key}[${subKey}][${subSubKey}]=${encodeURIComponent(subSubValue)}`)
|
|
758
|
-
758 }
|
|
759
|
-
759 })
|
|
760
|
-
760 } else {
|
|
761
|
-
761 queryParts.push(`${key}[${subKey}]=${encodeURIComponent(subValue)}`)
|
|
762
|
-
762 }
|
|
763
|
-
763 }
|
|
764
|
-
764 })
|
|
765
|
-
765 } else if (value !== undefined && value !== null) {
|
|
766
|
-
766 queryParts.push(`${key}=${encodeURIComponent(value)}`)
|
|
767
|
-
767 }
|
|
768
|
-
768 })
|
|
769
|
-
769
|
|
770
|
-
770 // Initialize returnMeta namespace for thread-safe metadata
|
|
771
|
-
771 context.returnMeta = context.returnMeta || {}
|
|
772
|
-
772 context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
|
773
|
-
773
|
|
774
|
-
774 // Execute count query for pagination if offset-based pagination is used
|
|
775
|
-
775 if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
776
|
-
776 const page = parseInt(queryParams.page?.number) || 1
|
|
777
|
-
777 const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
778
|
-
778
|
|
779
|
-
779 // Only execute count query if enabled
|
|
780
|
-
780 if (scope.vars.enablePaginationCounts) {
|
|
781
|
-
781 // Build count query with same filters as main query
|
|
782
|
-
782 const countQuery = db(tableName)
|
|
783
|
-
783
|
|
784
|
-
784 // Apply filters through hooks (same as main query)
|
|
785
|
-
785 if (queryParams.filters && Object.keys(queryParams.filters).length > 0) {
|
|
786
|
-
786 // Store query data in context for hooks
|
|
787
|
-
787 if (context) {
|
|
788
|
-
788 context.knexQuery = {
|
|
789
|
-
789 query: countQuery,
|
|
790
|
-
790 filters: queryParams.filters,
|
|
791
|
-
791 scopeName,
|
|
792
|
-
792 tableName,
|
|
793
|
-
793 schemaInfo,
|
|
794
|
-
794 db
|
|
795
|
-
795 }
|
|
796
|
-
796 }
|
|
797
|
-
797
|
|
798
|
-
798 // Run the same filtering hooks
|
|
799
|
-
799 await runHooks('knexQueryFiltering')
|
|
800
|
-
800
|
|
801
|
-
801 // Clean up
|
|
802
|
-
802 if (context && context.knexQuery) {
|
|
803
|
-
803 delete context.knexQuery
|
|
804
|
-
804 }
|
|
805
|
-
805 }
|
|
806
|
-
806
|
|
807
|
-
807 // Get total count
|
|
808
|
-
808 const countResult = await countQuery.count('* as total').first()
|
|
809
|
-
809 const total = parseInt(countResult.total)
|
|
810
|
-
810
|
|
811
|
-
811 // Calculate pagination metadata with total
|
|
812
|
-
812 context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
813
|
-
813 } else {
|
|
814
|
-
814 // Without count, we can still provide basic pagination info
|
|
815
|
-
815 context.returnMeta.paginationMeta = {
|
|
816
|
-
816 page,
|
|
817
|
-
817 pageSize
|
|
818
|
-
818 // No total, pageCount, or hasMore when counts are disabled
|
|
819
|
-
819 }
|
|
820
|
-
820 }
|
|
821
|
-
821
|
|
822
|
-
822 // Generate links
|
|
823
|
-
823 const urlPrefix = getUrlPrefix(context, scope)
|
|
824
|
-
824 context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
825
|
-
825 urlPrefix,
|
|
826
|
-
826 scopeName,
|
|
827
|
-
827 queryParams,
|
|
828
|
-
828 context.returnMeta.paginationMeta
|
|
829
|
-
829 )
|
|
830
|
-
830 }
|
|
831
|
-
831
|
|
832
|
-
832 // Handle cursor-based pagination meta
|
|
833
|
-
833 // Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
|
|
834
|
-
834 if (queryParams.page?.after || queryParams.page?.before ||
|
|
835
|
-
835 (queryParams.page?.size && queryParams.page?.number === undefined)) {
|
|
836
|
-
836 const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
837
|
-
837
|
|
838
|
-
838 // Check if there are more records
|
|
839
|
-
839 // We fetched pageSize + 1 records to detect if there are more
|
|
840
|
-
840 const hasMore = records.length > pageSize
|
|
841
|
-
841
|
|
842
|
-
842 // Remove the extra record if present
|
|
843
|
-
843 if (hasMore) {
|
|
844
|
-
844 records.pop()
|
|
845
|
-
845 }
|
|
846
|
-
846
|
|
847
|
-
847 // Determine sort fields for cursor
|
|
848
|
-
848 const idProperty = context.schemaInfo.idProperty || 'id'
|
|
849
|
-
849 let sortFields = [idProperty]
|
|
850
|
-
850 if (queryParams.sort && queryParams.sort.length > 0) {
|
|
851
|
-
851 sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s)
|
|
852
|
-
852 }
|
|
853
|
-
853
|
|
854
|
-
854 context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields)
|
|
855
|
-
855 const urlPrefix = getUrlPrefix(context, scope)
|
|
856
|
-
856 context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
857
|
-
857 urlPrefix,
|
|
858
|
-
858 scopeName,
|
|
859
|
-
859 queryParams,
|
|
860
|
-
860 records,
|
|
861
|
-
861 pageSize,
|
|
862
|
-
862 hasMore,
|
|
863
|
-
863 sortFields
|
|
864
|
-
864 )
|
|
865
|
-
865 }
|
|
866
|
-
866
|
|
867
|
-
867 // Load relationship identifiers for all hasMany relationships
|
|
868
|
-
868 await loadRelationshipIdentifiers(records, scopeName, scopes, db)
|
|
869
|
-
869
|
|
870
|
-
870 // Process includes
|
|
871
|
-
871 const included = await processIncludes(scope, records, {
|
|
872
|
-
872 log,
|
|
873
|
-
873 scopes,
|
|
874
|
-
874 knex,
|
|
875
|
-
875 context,
|
|
876
|
-
876 api
|
|
877
|
-
877 })
|
|
878
|
-
878
|
|
879
|
-
879 // Build and return response
|
|
880
|
-
880 return buildJsonApiResponse(scope, records, included, false, scopeName, context)
|
|
881
|
-
881 }
|
|
882
|
-
882
|
|
883
|
-
883 /**
|
|
884
|
-
884 * Creates a new resource in the database
|
|
885
|
-
885 *
|
|
886
|
-
886 * @param {Object} params - The parameters object
|
|
887
|
-
887 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
888
|
-
888 * @param {Object} params.context - The context object containing request-specific data
|
|
889
|
-
889 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
890
|
-
890 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
891
|
-
891 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
892
|
-
892 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
893
|
-
893 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
894
|
-
894 * @param {Object} params.context.inputRecord - JSON:API formatted input record
|
|
895
|
-
895 * @param {Object} params.context.inputRecord.data - The resource data
|
|
896
|
-
896 * @param {Object} params.context.inputRecord.data.attributes - The resource attributes to insert
|
|
897
|
-
897 * @returns {Promise<string|number>} The ID of the newly created resource
|
|
898
|
-
898 */
|
|
899
|
-
899 helpers.dataPost = async ({ scopeName, context }) => {
|
|
900
|
-
900 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
901
|
-
901 if (storageAdapter) {
|
|
902
|
-
902 context.storageAdapter = storageAdapter
|
|
903
|
-
903 }
|
|
904
|
-
904 const scope = api.resources[scopeName]
|
|
905
|
-
905 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
906
|
-
906 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
907
|
-
907 const db = context.db || api.knex.instance
|
|
908
|
-
908 const inputRecord = context.inputRecord
|
|
909
|
-
909
|
|
910
|
-
910 log.debug(`[Knex] POST ${tableName}`, inputRecord)
|
|
911
|
-
911
|
|
912
|
-
912 // Extract attributes from JSON:API format
|
|
913
|
-
913 const attributes = inputRecord.data.attributes
|
|
914
|
-
914
|
|
915
|
-
915 // Strip non-database fields (computed and virtual) before insert
|
|
916
|
-
916 const dbAttributes = stripNonDatabaseFields(attributes, context.schemaInfo)
|
|
917
|
-
917
|
|
918
|
-
918 // Insert and get the new ID
|
|
919
|
-
919 const result = await db(tableName).insert(dbAttributes).returning(idProperty)
|
|
920
|
-
920
|
|
921
|
-
921 // Extract the ID value (SQLite returns array of objects)
|
|
922
|
-
922 const id = result[0]?.[idProperty] || result[0]
|
|
923
|
-
923 return id
|
|
924
|
-
924 }
|
|
925
|
-
925
|
|
926
|
-
926 /**
|
|
927
|
-
927 * Replaces an entire resource (PUT operation) or creates it with a specific ID
|
|
928
|
-
928 *
|
|
929
|
-
929 * @param {Object} params - The parameters object
|
|
930
|
-
930 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
931
|
-
931 * @param {Object} params.context - The context object containing request-specific data
|
|
932
|
-
932 * @param {string|number} params.context.id - The ID of the resource to replace or create
|
|
933
|
-
933 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
934
|
-
934 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
935
|
-
935 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
936
|
-
936 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
937
|
-
937 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
938
|
-
938 * @param {Object} params.context.inputRecord - JSON:API formatted input record
|
|
939
|
-
939 * @param {Object} params.context.inputRecord.data - The resource data
|
|
940
|
-
940 * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes
|
|
941
|
-
941 * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
|
|
942
|
-
942 * @param {boolean} params.context.isCreate - Whether this is a create operation (true) or update (false)
|
|
943
|
-
943 * @returns {Promise<void>} Resolves when the operation is complete
|
|
944
|
-
944 * @throws {RestApiResourceError} When updating and the resource is not found
|
|
945
|
-
945 */
|
|
946
|
-
946 helpers.dataPut = async ({ scopeName, context }) => {
|
|
947
|
-
947 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
948
|
-
948 if (storageAdapter) {
|
|
949
|
-
949 context.storageAdapter = storageAdapter
|
|
950
|
-
950 }
|
|
951
|
-
951 const scope = api.resources[scopeName]
|
|
952
|
-
952 const id = context.id
|
|
953
|
-
953 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
954
|
-
954 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
955
|
-
955 const db = context.db || api.knex.instance
|
|
956
|
-
956 const inputRecord = context.inputRecord
|
|
957
|
-
957 const isCreate = context.isCreate
|
|
958
|
-
958
|
|
959
|
-
959 log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
|
|
960
|
-
960
|
|
961
|
-
961 // Extract attributes and process relationships using helper
|
|
962
|
-
962 const attributes = inputRecord.data.attributes || {}
|
|
963
|
-
963 const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
964
|
-
964 const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
965
|
-
965
|
|
966
|
-
966 // Strip non-database fields (computed and virtual) before database operation
|
|
967
|
-
967 const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
968
|
-
968
|
|
969
|
-
969 // Map 'id' to actual idProperty if needed (for PUT with specific ID)
|
|
970
|
-
970 if (idProperty !== 'id' && inputRecord.data.id) {
|
|
971
|
-
971 finalAttributes[idProperty] = inputRecord.data.id
|
|
972
|
-
972 }
|
|
973
|
-
973
|
|
974
|
-
974 if (isCreate) {
|
|
975
|
-
975 // Create mode - insert new record with specified ID
|
|
976
|
-
976 const recordData = {
|
|
977
|
-
977 ...finalAttributes,
|
|
978
|
-
978 [idProperty]: id
|
|
979
|
-
979 }
|
|
980
|
-
980
|
|
981
|
-
981 await db(tableName).insert(recordData)
|
|
982
|
-
982 } else {
|
|
983
|
-
983 // Update mode - check if record exists first
|
|
984
|
-
984 const exists = await db(tableName)
|
|
985
|
-
985 .where(idProperty, id)
|
|
986
|
-
986 .first()
|
|
987
|
-
987
|
|
988
|
-
988 if (!exists) {
|
|
989
|
-
989 throw new RestApiResourceError(
|
|
990
|
-
990 'Resource not found',
|
|
991
|
-
991 {
|
|
992
|
-
992 subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
993
|
-
993 resourceType: scopeName,
|
|
994
|
-
994 resourceId: id
|
|
995
|
-
995 }
|
|
996
|
-
996 )
|
|
997
|
-
997 }
|
|
998
|
-
998
|
|
999
|
-
999 // Remove the idProperty from attributes to prevent updating the primary key
|
|
1000
|
-
1000 delete finalAttributes[idProperty]
|
|
1001
|
-
1001
|
|
1002
|
-
1002 // Update the record (replace all fields)
|
|
1003
|
-
1003 if (Object.keys(finalAttributes).length > 0) {
|
|
1004
|
-
1004 await db(tableName)
|
|
1005
|
-
1005 .where(idProperty, id)
|
|
1006
|
-
1006 .update(finalAttributes)
|
|
1007
|
-
1007 }
|
|
1008
|
-
1008 }
|
|
1009
|
-
1009 }
|
|
1010
|
-
1010
|
|
1011
|
-
1011 /**
|
|
1012
|
-
1012 * Partially updates a resource (PATCH operation)
|
|
1013
|
-
1013 *
|
|
1014
|
-
1014 * @param {Object} params - The parameters object
|
|
1015
|
-
1015 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1016
|
-
1016 * @param {Object} params.context - The context object containing request-specific data
|
|
1017
|
-
1017 * @param {string|number} params.context.id - The ID of the resource to update
|
|
1018
|
-
1018 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
1019
|
-
1019 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
1020
|
-
1020 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
1021
|
-
1021 * @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
1022
|
-
1022 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
1023
|
-
1023 * @param {Object} params.context.inputRecord - JSON:API formatted input record with partial updates
|
|
1024
|
-
1024 * @param {Object} params.context.inputRecord.data - The resource data
|
|
1025
|
-
1025 * @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes to update
|
|
1026
|
-
1026 * @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
|
|
1027
|
-
1027 * @returns {Promise<void>} Resolves when the update is complete
|
|
1028
|
-
1028 * @throws {RestApiResourceError} When the resource is not found
|
|
1029
|
-
1029 */
|
|
1030
|
-
1030 helpers.dataPatch = async ({ scopeName, context }) => {
|
|
1031
|
-
1031 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1032
|
-
1032 if (storageAdapter) {
|
|
1033
|
-
1033 context.storageAdapter = storageAdapter
|
|
1034
|
-
1034 }
|
|
1035
|
-
1035 const scope = api.resources[scopeName]
|
|
1036
|
-
1036 const id = context.id
|
|
1037
|
-
1037 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1038
|
-
1038 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1039
|
-
1039 const db = context.db || api.knex.instance
|
|
1040
|
-
1040 const inputRecord = context.inputRecord
|
|
1041
|
-
1041
|
|
1042
|
-
1042 log.debug(`[Knex] PATCH ${tableName}/${id}`)
|
|
1043
|
-
1043
|
|
1044
|
-
1044 // Check if record exists
|
|
1045
|
-
1045 const exists = await db(tableName)
|
|
1046
|
-
1046 .where(idProperty, id)
|
|
1047
|
-
1047 .first()
|
|
1048
|
-
1048
|
|
1049
|
-
1049 if (!exists) {
|
|
1050
|
-
1050 throw new RestApiResourceError(
|
|
1051
|
-
1051 'Resource not found',
|
|
1052
|
-
1052 {
|
|
1053
|
-
1053 subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1054
|
-
1054 resourceType: scopeName,
|
|
1055
|
-
1055 resourceId: id
|
|
1056
|
-
1056 }
|
|
1057
|
-
1057 )
|
|
1058
|
-
1058 }
|
|
1059
|
-
1059
|
|
1060
|
-
1060 // Extract attributes and process relationships using helper
|
|
1061
|
-
1061 const attributes = inputRecord.data.attributes || {}
|
|
1062
|
-
1062 const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
1063
|
-
1063 const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
1064
|
-
1064
|
|
1065
|
-
1065 // Strip non-database fields (computed and virtual) before database operation
|
|
1066
|
-
1066 const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo)
|
|
1067
|
-
1067
|
|
1068
|
-
1068 log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
|
|
1069
|
-
1069
|
|
1070
|
-
1070 // Remove the idProperty from attributes to prevent updating the primary key
|
|
1071
|
-
1071 delete finalAttributes[idProperty]
|
|
1072
|
-
1072
|
|
1073
|
-
1073 // Update only if there are changes
|
|
1074
|
-
1074 if (Object.keys(finalAttributes).length > 0) {
|
|
1075
|
-
1075 await db(tableName)
|
|
1076
|
-
1076 .where(idProperty, id)
|
|
1077
|
-
1077 .update(finalAttributes)
|
|
1078
|
-
1078 }
|
|
1079
|
-
1079 }
|
|
1080
|
-
1080
|
|
1081
|
-
1081 /**
|
|
1082
|
-
1082 * Deletes a resource from the database
|
|
1083
|
-
1083 *
|
|
1084
|
-
1084 * @param {Object} params - The parameters object
|
|
1085
|
-
1085 * @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1086
|
-
1086 * @param {Object} params.context - The context object containing request-specific data
|
|
1087
|
-
1087 * @param {string|number} params.context.id - The ID of the resource to delete
|
|
1088
|
-
1088 * @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
1089
|
-
1089 * @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
1090
|
-
1090 * @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
1091
|
-
1091 * @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
1092
|
-
1092 * @returns {Promise<Object>} Returns { success: true } when deletion is successful
|
|
1093
|
-
1093 * @throws {RestApiResourceError} When the resource is not found
|
|
1094
|
-
1094 */
|
|
1095
|
-
1095 helpers.dataDelete = async ({ scopeName, context }) => {
|
|
1096
|
-
1096 const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1097
|
-
1097 if (storageAdapter) {
|
|
1098
|
-
1098 context.storageAdapter = storageAdapter
|
|
1099
|
-
1099 }
|
|
1100
|
-
1100 const scope = api.resources[scopeName]
|
|
1101
|
-
1101 const id = context.id
|
|
1102
|
-
1102
|
|
1103
|
-
1103 const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1104
|
-
1104 const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1105
|
-
1105 const db = context.db || api.knex.instance
|
|
1106
|
-
1106
|
|
1107
|
-
1107 log.debug(`[Knex] DELETE ${tableName}/${id}`)
|
|
1108
|
-
1108
|
|
1109
|
-
1109 // Check if record exists
|
|
1110
|
-
1110 const exists = await db(tableName)
|
|
1111
|
-
1111 .where(idProperty, id)
|
|
1112
|
-
1112 .first()
|
|
1113
|
-
1113
|
|
1114
|
-
1114 if (!exists) {
|
|
1115
|
-
1115 throw new RestApiResourceError(
|
|
1116
|
-
1116 'Resource not found',
|
|
1117
|
-
1117 {
|
|
1118
|
-
1118 subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1119
|
-
1119 resourceType: scopeName,
|
|
1120
|
-
1120 resourceId: id
|
|
1121
|
-
1121 }
|
|
1122
|
-
1122 )
|
|
1123
|
-
1123 }
|
|
1124
|
-
1124
|
|
1125
|
-
1125 // Delete the record
|
|
1126
|
-
1126 await db(tableName)
|
|
1127
|
-
1127 .where(idProperty, id)
|
|
1128
|
-
1128 .delete()
|
|
1129
|
-
1129
|
|
1130
|
-
1130 return { success: true }
|
|
1131
|
-
1131 }
|
|
1132
|
-
1132
|
|
1133
|
-
1133 log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
|
|
1134
|
-
1134 }
|
|
1135
|
-
1135 }
|