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__socketio-plugin.js.txt
DELETED
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
1 import { requirePackage } from 'hooked-api'
|
|
2
|
-
2
|
|
3
|
-
3 const pendingBroadcasts = new WeakMap()
|
|
4
|
-
4
|
|
5
|
-
5 function normalizeAuthContext (auth) {
|
|
6
|
-
6 if (!auth) return null
|
|
7
|
-
7 if (typeof auth !== 'object') return null
|
|
8
|
-
8 const normalized = { ...auth }
|
|
9
|
-
9 if (normalized.userId !== undefined && normalized.userId !== null) {
|
|
10
|
-
10 normalized.userId = String(normalized.userId)
|
|
11
|
-
11 }
|
|
12
|
-
12 if (normalized.roles && !Array.isArray(normalized.roles)) {
|
|
13
|
-
13 normalized.roles = [normalized.roles]
|
|
14
|
-
14 }
|
|
15
|
-
15 return normalized
|
|
16
|
-
16 }
|
|
17
|
-
17
|
|
18
|
-
18 function matchesFilters (record, filters, searchSchemaStructure) {
|
|
19
|
-
19 if (!filters || Object.keys(filters).length === 0) return true
|
|
20
|
-
20
|
|
21
|
-
21 if (!record) return false
|
|
22
|
-
22
|
|
23
|
-
23 if (!searchSchemaStructure || Object.keys(searchSchemaStructure).length === 0) {
|
|
24
|
-
24 return false
|
|
25
|
-
25 }
|
|
26
|
-
26
|
|
27
|
-
27 for (const [filterKey, filterValue] of Object.entries(filters)) {
|
|
28
|
-
28 const fieldDef = searchSchemaStructure[filterKey]
|
|
29
|
-
29 if (!fieldDef) continue
|
|
30
|
-
30
|
|
31
|
-
31 const fieldName = fieldDef.actualField || filterKey
|
|
32
|
-
32
|
|
33
|
-
33 let recordValue
|
|
34
|
-
34 if (fieldDef.isRelationship) {
|
|
35
|
-
35 const relationshipData = record.relationships?.[filterKey]?.data
|
|
36
|
-
36 recordValue = relationshipData?.id
|
|
37
|
-
37 } else {
|
|
38
|
-
38 recordValue = record.attributes?.[fieldName] ?? record[fieldName]
|
|
39
|
-
39 }
|
|
40
|
-
40
|
|
41
|
-
41 if (recordValue === null || recordValue === undefined) {
|
|
42
|
-
42 if (filterValue !== null && filterValue !== undefined) {
|
|
43
|
-
43 return false
|
|
44
|
-
44 }
|
|
45
|
-
45 continue
|
|
46
|
-
46 }
|
|
47
|
-
47
|
|
48
|
-
48 if (typeof fieldDef.filterOperator === 'function') {
|
|
49
|
-
49 if (!fieldDef.filterRecord?.(record, filterValue)) {
|
|
50
|
-
50 return false
|
|
51
|
-
51 }
|
|
52
|
-
52 continue
|
|
53
|
-
53 }
|
|
54
|
-
54
|
|
55
|
-
55 const operator = fieldDef.filterOperator || '='
|
|
56
|
-
56
|
|
57
|
-
57 switch (operator) {
|
|
58
|
-
58 case 'like':
|
|
59
|
-
59 if (!String(recordValue).toLowerCase().includes(String(filterValue).toLowerCase())) {
|
|
60
|
-
60 return false
|
|
61
|
-
61 }
|
|
62
|
-
62 break
|
|
63
|
-
63 case 'in':
|
|
64
|
-
64 if (Array.isArray(filterValue)) {
|
|
65
|
-
65 if (!filterValue.includes(recordValue)) {
|
|
66
|
-
66 return false
|
|
67
|
-
67 }
|
|
68
|
-
68 } else if (recordValue !== filterValue) {
|
|
69
|
-
69 return false
|
|
70
|
-
70 }
|
|
71
|
-
71 break
|
|
72
|
-
72 case 'between':
|
|
73
|
-
73 if (Array.isArray(filterValue) && filterValue.length === 2) {
|
|
74
|
-
74 if (recordValue < filterValue[0] || recordValue > filterValue[1]) {
|
|
75
|
-
75 return false
|
|
76
|
-
76 }
|
|
77
|
-
77 }
|
|
78
|
-
78 break
|
|
79
|
-
79 case '>':
|
|
80
|
-
80 if (!(recordValue > filterValue)) return false
|
|
81
|
-
81 break
|
|
82
|
-
82 case '>=':
|
|
83
|
-
83 if (!(recordValue >= filterValue)) return false
|
|
84
|
-
84 break
|
|
85
|
-
85 case '<':
|
|
86
|
-
86 if (!(recordValue < filterValue)) return false
|
|
87
|
-
87 break
|
|
88
|
-
88 case '<=':
|
|
89
|
-
89 if (!(recordValue <= filterValue)) return false
|
|
90
|
-
90 break
|
|
91
|
-
91 case '!=':
|
|
92
|
-
92 case '<>':
|
|
93
|
-
93 if (recordValue === filterValue) return false
|
|
94
|
-
94 break
|
|
95
|
-
95 case '=':
|
|
96
|
-
96 default:
|
|
97
|
-
97 if (fieldDef.isRelationship) {
|
|
98
|
-
98 if (String(recordValue) !== String(filterValue)) {
|
|
99
|
-
99 return false
|
|
100
|
-
100 }
|
|
101
|
-
101 } else if (recordValue !== filterValue) {
|
|
102
|
-
102 return false
|
|
103
|
-
103 }
|
|
104
|
-
104 }
|
|
105
|
-
105 }
|
|
106
|
-
106
|
|
107
|
-
107 return true
|
|
108
|
-
108 }
|
|
109
|
-
109
|
|
110
|
-
110 function buildConfig (pluginOptions = {}) {
|
|
111
|
-
111 const authOptions = pluginOptions.auth || {}
|
|
112
|
-
112 const subscriptionOptions = pluginOptions.subscriptions || {}
|
|
113
|
-
113
|
|
114
|
-
114 return {
|
|
115
|
-
115 auth: {
|
|
116
|
-
116 authenticate: authOptions.authenticate || null,
|
|
117
|
-
117 requireAuth: authOptions.requireAuth === true,
|
|
118
|
-
118 allowClientProvidedAuth: authOptions.allowClientProvidedAuth === true,
|
|
119
|
-
119 anonymousContext: authOptions.anonymousContext || null,
|
|
120
|
-
120 onAuthenticationFailed: authOptions.onAuthenticationFailed || null
|
|
121
|
-
121 },
|
|
122
|
-
122 subscriptions: {
|
|
123
|
-
123 maxPerSocket: subscriptionOptions.maxPerSocket ?? 100
|
|
124
|
-
124 },
|
|
125
|
-
125 transport: pluginOptions.transport || {}
|
|
126
|
-
126 }
|
|
127
|
-
127 }
|
|
128
|
-
128
|
|
129
|
-
129 async function authenticateSocket ({ socket, api, helpers, log, config }) {
|
|
130
|
-
130 const { authenticate, allowClientProvidedAuth, anonymousContext, requireAuth, onAuthenticationFailed } = config.auth
|
|
131
|
-
131
|
|
132
|
-
132 try {
|
|
133
|
-
133 let authContext = null
|
|
134
|
-
134
|
|
135
|
-
135 if (authenticate) {
|
|
136
|
-
136 authContext = await authenticate({ socket, api, helpers, log })
|
|
137
|
-
137 } else if (allowClientProvidedAuth && socket.handshake.auth && typeof socket.handshake.auth === 'object') {
|
|
138
|
-
138 authContext = socket.handshake.auth
|
|
139
|
-
139 } else if (anonymousContext) {
|
|
140
|
-
140 authContext = anonymousContext
|
|
141
|
-
141 }
|
|
142
|
-
142
|
|
143
|
-
143 const normalized = normalizeAuthContext(authContext)
|
|
144
|
-
144 if (!normalized && requireAuth) {
|
|
145
|
-
145 throw new Error('Authentication required')
|
|
146
|
-
146 }
|
|
147
|
-
147
|
|
148
|
-
148 return normalized
|
|
149
|
-
149 } catch (error) {
|
|
150
|
-
150 if (onAuthenticationFailed) {
|
|
151
|
-
151 try {
|
|
152
|
-
152 await onAuthenticationFailed({ socket, error, log })
|
|
153
|
-
153 } catch (hookError) {
|
|
154
|
-
154 log.error('socketio auth failure handler threw error', hookError)
|
|
155
|
-
155 }
|
|
156
|
-
156 }
|
|
157
|
-
157 throw error
|
|
158
|
-
158 }
|
|
159
|
-
159 }
|
|
160
|
-
160
|
|
161
|
-
161 async function registerSubscription ({
|
|
162
|
-
162 socket,
|
|
163
|
-
163 data,
|
|
164
|
-
164 scopes,
|
|
165
|
-
165 runHooks,
|
|
166
|
-
166 log,
|
|
167
|
-
167 config
|
|
168
|
-
168 }) {
|
|
169
|
-
169 const { resource, filters = {}, include, fields, subscriptionId } = data || {}
|
|
170
|
-
170
|
|
171
|
-
171 if (!resource || typeof resource !== 'string') {
|
|
172
|
-
172 throw Object.assign(new Error('Resource is required'), { code: 'RESOURCE_REQUIRED' })
|
|
173
|
-
173 }
|
|
174
|
-
174
|
|
175
|
-
175 const scope = scopes[resource]
|
|
176
|
-
176 if (!scope) {
|
|
177
|
-
177 throw Object.assign(new Error(`Resource '${resource}' not found`), { code: 'RESOURCE_NOT_FOUND' })
|
|
178
|
-
178 }
|
|
179
|
-
179
|
|
180
|
-
180 const auth = socket.data.auth || null
|
|
181
|
-
181
|
|
182
|
-
182 if (scope.checkPermissions) {
|
|
183
|
-
183 await scope.checkPermissions({
|
|
184
|
-
184 method: 'query',
|
|
185
|
-
185 originalContext: { auth }
|
|
186
|
-
186 })
|
|
187
|
-
187 }
|
|
188
|
-
188
|
|
189
|
-
189 const schemaInfo = scope.vars?.schemaInfo
|
|
190
|
-
190 const searchSchemaStructure = schemaInfo?.searchSchemaStructure
|
|
191
|
-
191 const searchSchemaInstance = schemaInfo?.searchSchemaInstance
|
|
192
|
-
192
|
|
193
|
-
193 const validatedFilters = { ...filters }
|
|
194
|
-
194 if (Object.keys(validatedFilters).length > 0) {
|
|
195
|
-
195 if (!searchSchemaStructure || Object.keys(searchSchemaStructure).length === 0 || !searchSchemaInstance) {
|
|
196
|
-
196 throw Object.assign(new Error(`Filtering is not enabled for resource '${resource}'`), { code: 'FILTERING_NOT_ENABLED' })
|
|
197
|
-
197 }
|
|
198
|
-
198
|
|
199
|
-
199 for (const filterKey of Object.keys(validatedFilters)) {
|
|
200
|
-
200 const fieldDef = searchSchemaStructure[filterKey]
|
|
201
|
-
201 if (fieldDef && typeof fieldDef.filterOperator === 'function' && !fieldDef.filterRecord) {
|
|
202
|
-
202 throw Object.assign(
|
|
203
|
-
203 new Error(`Filter '${filterKey}' uses custom SQL logic and requires 'filterRecord' for real-time subscriptions`),
|
|
204
|
-
204 { code: 'UNSUPPORTED_FILTER' }
|
|
205
|
-
205 )
|
|
206
|
-
206 }
|
|
207
|
-
207 }
|
|
208
|
-
208
|
|
209
|
-
209 const { validatedObject, errors } = await searchSchemaInstance.validate(validatedFilters, {
|
|
210
|
-
210 onlyObjectValues: true
|
|
211
|
-
211 })
|
|
212
|
-
212
|
|
213
|
-
213 if (errors && Object.keys(errors).length > 0) {
|
|
214
|
-
214 const error = new Error('Invalid filter values')
|
|
215
|
-
215 error.code = 'INVALID_FILTERS'
|
|
216
|
-
216 error.details = errors
|
|
217
|
-
217 throw error
|
|
218
|
-
218 }
|
|
219
|
-
219
|
|
220
|
-
220 Object.assign(validatedFilters, validatedObject)
|
|
221
|
-
221 }
|
|
222
|
-
222
|
|
223
|
-
223 if (include !== undefined) {
|
|
224
|
-
224 if (!Array.isArray(include)) {
|
|
225
|
-
225 throw Object.assign(new Error('Include parameter must be an array'), { code: 'INVALID_INCLUDE' })
|
|
226
|
-
226 }
|
|
227
|
-
227
|
|
228
|
-
228 const relationships = schemaInfo?.schemaRelationships || {}
|
|
229
|
-
229 for (const includePath of include) {
|
|
230
|
-
230 const baseName = includePath.split('.')[0]
|
|
231
|
-
231 if (!relationships[baseName]) {
|
|
232
|
-
232 throw Object.assign(new Error(`Invalid relationship '${baseName}' for resource '${resource}'`), {
|
|
233
|
-
233 code: 'INVALID_INCLUDE'
|
|
234
|
-
234 })
|
|
235
|
-
235 }
|
|
236
|
-
236 }
|
|
237
|
-
237 }
|
|
238
|
-
238
|
|
239
|
-
239 if (fields !== undefined) {
|
|
240
|
-
240 if (typeof fields !== 'object' || Array.isArray(fields) || fields === null) {
|
|
241
|
-
241 throw Object.assign(new Error('Fields parameter must be an object'), { code: 'INVALID_FIELDS' })
|
|
242
|
-
242 }
|
|
243
|
-
243
|
|
244
|
-
244 for (const [resourceType, fieldList] of Object.entries(fields)) {
|
|
245
|
-
245 if (!Array.isArray(fieldList)) {
|
|
246
|
-
246 throw Object.assign(new Error(`Fields for '${resourceType}' must be an array`), { code: 'INVALID_FIELDS' })
|
|
247
|
-
247 }
|
|
248
|
-
248 }
|
|
249
|
-
249 }
|
|
250
|
-
250
|
|
251
|
-
251 if (!socket.data.subscriptions) {
|
|
252
|
-
252 socket.data.subscriptions = new Map()
|
|
253
|
-
253 }
|
|
254
|
-
254
|
|
255
|
-
255 if (socket.data.subscriptions.size >= config.subscriptions.maxPerSocket) {
|
|
256
|
-
256 throw Object.assign(new Error('Subscription limit reached for this connection'), { code: 'SUBSCRIPTION_LIMIT' })
|
|
257
|
-
257 }
|
|
258
|
-
258
|
|
259
|
-
259 const subscription = {
|
|
260
|
-
260 id: subscriptionId || `${resource}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
261
|
-
261 resource,
|
|
262
|
-
262 filters: validatedFilters,
|
|
263
|
-
263 include: include || [],
|
|
264
|
-
264 fields: fields || {},
|
|
265
|
-
265 auth,
|
|
266
|
-
266 createdAt: new Date()
|
|
267
|
-
267 }
|
|
268
|
-
268
|
|
269
|
-
269 await runHooks('subscriptionFilters', { subscription, auth })
|
|
270
|
-
270
|
|
271
|
-
271 const roomName = `${resource}:updates`
|
|
272
|
-
272 socket.join(roomName)
|
|
273
|
-
273 socket.data.subscriptions.set(subscription.id, subscription)
|
|
274
|
-
274
|
|
275
|
-
275 log.info(`Socket ${socket.id} subscribed to ${resource}`, {
|
|
276
|
-
276 subscriptionId: subscription.id,
|
|
277
|
-
277 filters: subscription.filters
|
|
278
|
-
278 })
|
|
279
|
-
279
|
|
280
|
-
280 return {
|
|
281
|
-
281 subscriptionId: subscription.id,
|
|
282
|
-
282 resource: subscription.resource,
|
|
283
|
-
283 filters: subscription.filters,
|
|
284
|
-
284 include: subscription.include,
|
|
285
|
-
285 fields: subscription.fields,
|
|
286
|
-
286 status: 'active'
|
|
287
|
-
287 }
|
|
288
|
-
288 }
|
|
289
|
-
289
|
|
290
|
-
290 async function handleRestoreSubscriptions ({ socket, subscriptions, scopes, runHooks, log, config }) {
|
|
291
|
-
291 const restored = []
|
|
292
|
-
292 const failed = []
|
|
293
|
-
293
|
|
294
|
-
294 for (const sub of subscriptions) {
|
|
295
|
-
295 try {
|
|
296
|
-
296 const response = await registerSubscription({
|
|
297
|
-
297 socket,
|
|
298
|
-
298 data: sub,
|
|
299
|
-
299 scopes,
|
|
300
|
-
300 runHooks,
|
|
301
|
-
301 log,
|
|
302
|
-
302 config
|
|
303
|
-
303 })
|
|
304
|
-
304 restored.push(response.subscriptionId)
|
|
305
|
-
305 } catch (error) {
|
|
306
|
-
306 failed.push({
|
|
307
|
-
307 subscriptionId: sub?.subscriptionId || null,
|
|
308
|
-
308 error: {
|
|
309
|
-
309 code: error.code || 'SUBSCRIBE_ERROR',
|
|
310
|
-
310 message: error.message
|
|
311
|
-
311 }
|
|
312
|
-
312 })
|
|
313
|
-
313 }
|
|
314
|
-
314 }
|
|
315
|
-
315
|
|
316
|
-
316 return { restored, failed }
|
|
317
|
-
317 }
|
|
318
|
-
318
|
|
319
|
-
319 function getRecordForFiltering (context) {
|
|
320
|
-
320 return context?.minimalRecord || context?.originalMinimalRecord || null
|
|
321
|
-
321 }
|
|
322
|
-
322
|
|
323
|
-
323 async function performBroadcast ({ method, scopeName, id, context, api, io, log }) {
|
|
324
|
-
324 if (!io) return
|
|
325
|
-
325
|
|
326
|
-
326 const scope = api.resources[scopeName]
|
|
327
|
-
327 if (!scope) {
|
|
328
|
-
328 log.warn(`Socket.IO broadcast skipped: unknown resource ${scopeName}`)
|
|
329
|
-
329 return
|
|
330
|
-
330 }
|
|
331
|
-
331
|
|
332
|
-
332 const roomName = `${scopeName}:updates`
|
|
333
|
-
333 const socketsInRoom = await io.in(roomName).fetchSockets()
|
|
334
|
-
334 if (!socketsInRoom || socketsInRoom.length === 0) {
|
|
335
|
-
335 log.debug(`Socket.IO broadcast skipped: no subscribers for ${roomName}`)
|
|
336
|
-
336 return
|
|
337
|
-
337 }
|
|
338
|
-
338
|
|
339
|
-
339 const schemaInfo = scope.vars?.schemaInfo
|
|
340
|
-
340 const searchSchemaStructure = schemaInfo?.searchSchemaStructure
|
|
341
|
-
341 const recordForFiltering = getRecordForFiltering(context)
|
|
342
|
-
342
|
|
343
|
-
343 for (const socket of socketsInRoom) {
|
|
344
|
-
344 const subscriptions = socket.data.subscriptions
|
|
345
|
-
345 if (!subscriptions || subscriptions.size === 0) continue
|
|
346
|
-
346
|
|
347
|
-
347 for (const subscription of subscriptions.values()) {
|
|
348
|
-
348 if (subscription.resource !== scopeName) continue
|
|
349
|
-
349
|
|
350
|
-
350 if (recordForFiltering && subscription.filters && Object.keys(subscription.filters).length > 0) {
|
|
351
|
-
351 if (!matchesFilters(recordForFiltering, subscription.filters, searchSchemaStructure)) {
|
|
352
|
-
352 continue
|
|
353
|
-
353 }
|
|
354
|
-
354 }
|
|
355
|
-
355
|
|
356
|
-
356 const notification = {
|
|
357
|
-
357 type: `resource.${method}d`,
|
|
358
|
-
358 resource: scopeName,
|
|
359
|
-
359 id,
|
|
360
|
-
360 action: method,
|
|
361
|
-
361 subscriptionId: subscription.id,
|
|
362
|
-
362 meta: {
|
|
363
|
-
363 timestamp: new Date().toISOString()
|
|
364
|
-
364 }
|
|
365
|
-
365 }
|
|
366
|
-
366
|
|
367
|
-
367 if (method === 'delete') {
|
|
368
|
-
368 notification.deletedRecord = { id }
|
|
369
|
-
369 }
|
|
370
|
-
370
|
|
371
|
-
371 socket.emit('subscription.update', notification)
|
|
372
|
-
372 log.debug(`Socket.IO broadcast: ${scopeName}/${id} -> socket ${socket.id}`)
|
|
373
|
-
373 break
|
|
374
|
-
374 }
|
|
375
|
-
375 }
|
|
376
|
-
376 }
|
|
377
|
-
377
|
|
378
|
-
378 export const SocketIOPlugin = {
|
|
379
|
-
379 name: 'socketio',
|
|
380
|
-
380 dependencies: ['rest-api'],
|
|
381
|
-
381
|
|
382
|
-
382 async install ({ api, addHook, log, scopes, helpers, vars, runHooks, pluginOptions = {} }) {
|
|
383
|
-
383 const config = buildConfig(pluginOptions)
|
|
384
|
-
384
|
|
385
|
-
385 let Server
|
|
386
|
-
386 try {
|
|
387
|
-
387 ({ Server } = await import('socket.io'))
|
|
388
|
-
388 } catch (error) {
|
|
389
|
-
389 requirePackage('socket.io', 'socketio', 'Socket.IO is required for WebSocket support. This is a peer dependency.')
|
|
390
|
-
390 throw error
|
|
391
|
-
391 }
|
|
392
|
-
392
|
|
393
|
-
393 let createAdapter
|
|
394
|
-
394 let createClient
|
|
395
|
-
395 let io
|
|
396
|
-
396
|
|
397
|
-
397 api.startSocketServer = async (server, startOptions = {}) => {
|
|
398
|
-
398 const transportConfig = { ...config.transport, ...startOptions.transport }
|
|
399
|
-
399 const authConfig = { ...config.auth, ...(startOptions.auth || {}) }
|
|
400
|
-
400 const redisConfig = startOptions.redis ?? pluginOptions.redis ?? null
|
|
401
|
-
401
|
|
402
|
-
402 config.auth = authConfig
|
|
403
|
-
403
|
|
404
|
-
404 const defaultPath = vars.transport?.mountPath ? `${vars.transport.mountPath}/socket.io` : '/socket.io'
|
|
405
|
-
405 const path = startOptions.path || config.transport.path || defaultPath
|
|
406
|
-
406 const cors = startOptions.cors || config.transport.cors || { origin: '*', methods: ['GET', 'POST'] }
|
|
407
|
-
407
|
|
408
|
-
408 io = new Server(server, {
|
|
409
|
-
409 path,
|
|
410
|
-
410 cors,
|
|
411
|
-
411 transports: ['websocket', 'polling']
|
|
412
|
-
412 })
|
|
413
|
-
413
|
|
414
|
-
414 vars.socketIO = io
|
|
415
|
-
415 api.io = io
|
|
416
|
-
416
|
|
417
|
-
417 if (redisConfig) {
|
|
418
|
-
418 try {
|
|
419
|
-
419 ({ createClient } = await import('redis'))
|
|
420
|
-
420 } catch (error) {
|
|
421
|
-
421 requirePackage('redis', 'socketio', 'Redis is required for Socket.IO horizontal scaling. This is a peer dependency.')
|
|
422
|
-
422 throw error
|
|
423
|
-
423 }
|
|
424
|
-
424
|
|
425
|
-
425 try {
|
|
426
|
-
426 ({ createAdapter } = await import('@socket.io/redis-adapter'))
|
|
427
|
-
427 } catch (error) {
|
|
428
|
-
428 requirePackage('@socket.io/redis-adapter', 'socketio',
|
|
429
|
-
429 'Socket.IO Redis adapter is required for horizontal scaling. This is a peer dependency.')
|
|
430
|
-
430 throw error
|
|
431
|
-
431 }
|
|
432
|
-
432
|
|
433
|
-
433 const pubClient = createClient(redisConfig)
|
|
434
|
-
434 const subClient = pubClient.duplicate()
|
|
435
|
-
435
|
|
436
|
-
436 await Promise.all([
|
|
437
|
-
437 pubClient.connect(),
|
|
438
|
-
438 subClient.connect()
|
|
439
|
-
439 ])
|
|
440
|
-
440
|
|
441
|
-
441 io.adapter(createAdapter(pubClient, subClient))
|
|
442
|
-
442 vars.socketIORedisClients = { pubClient, subClient }
|
|
443
|
-
443 log.info('Socket.IO configured with Redis adapter')
|
|
444
|
-
444 }
|
|
445
|
-
445
|
|
446
|
-
446 io.use(async (socket, next) => {
|
|
447
|
-
447 try {
|
|
448
|
-
448 const authContext = await authenticateSocket({ socket, api, helpers, log, config })
|
|
449
|
-
449 socket.data.auth = authContext
|
|
450
|
-
450 socket.data.subscriptions = new Map()
|
|
451
|
-
451 next()
|
|
452
|
-
452 } catch (error) {
|
|
453
|
-
453 log.warn('Socket.IO authentication failed', error)
|
|
454
|
-
454 next(new Error(error.message || 'Authentication failed'))
|
|
455
|
-
455 }
|
|
456
|
-
456 })
|
|
457
|
-
457
|
|
458
|
-
458 io.on('connection', (socket) => {
|
|
459
|
-
459 log.info(`Socket connected: ${socket.id}`, {
|
|
460
|
-
460 userId: socket.data.auth?.userId ?? null
|
|
461
|
-
461 })
|
|
462
|
-
462
|
|
463
|
-
463 socket.emit('connected', {
|
|
464
|
-
464 socketId: socket.id,
|
|
465
|
-
465 serverTime: new Date().toISOString()
|
|
466
|
-
466 })
|
|
467
|
-
467
|
|
468
|
-
468 socket.on('subscribe', async (payload, callback) => {
|
|
469
|
-
469 try {
|
|
470
|
-
470 const result = await registerSubscription({
|
|
471
|
-
471 socket,
|
|
472
|
-
472 data: payload,
|
|
473
|
-
473 scopes,
|
|
474
|
-
474 runHooks,
|
|
475
|
-
475 log,
|
|
476
|
-
476 config
|
|
477
|
-
477 })
|
|
478
|
-
478
|
|
479
|
-
479 if (callback) callback({ success: true, data: result })
|
|
480
|
-
480 else socket.emit('subscription.created', result)
|
|
481
|
-
481 } catch (error) {
|
|
482
|
-
482 log.error('Socket.IO subscribe error', error)
|
|
483
|
-
483 const response = {
|
|
484
|
-
484 error: {
|
|
485
|
-
485 code: error.code || 'SUBSCRIBE_ERROR',
|
|
486
|
-
486 message: error.message
|
|
487
|
-
487 }
|
|
488
|
-
488 }
|
|
489
|
-
489 if (callback) callback(response)
|
|
490
|
-
490 else socket.emit('subscription.error', response.error)
|
|
491
|
-
491 }
|
|
492
|
-
492 })
|
|
493
|
-
493
|
|
494
|
-
494 socket.on('unsubscribe', (payload, callback) => {
|
|
495
|
-
495 try {
|
|
496
|
-
496 const subscriptionId = payload?.subscriptionId
|
|
497
|
-
497 if (!subscriptionId) {
|
|
498
|
-
498 const error = { code: 'MISSING_SUBSCRIPTION_ID', message: 'Subscription ID is required' }
|
|
499
|
-
499 if (callback) callback({ error })
|
|
500
|
-
500 return
|
|
501
|
-
501 }
|
|
502
|
-
502
|
|
503
|
-
503 const subscription = socket.data.subscriptions?.get(subscriptionId)
|
|
504
|
-
504 if (!subscription) {
|
|
505
|
-
505 const error = { code: 'SUBSCRIPTION_NOT_FOUND', message: 'Subscription not found' }
|
|
506
|
-
506 if (callback) callback({ error })
|
|
507
|
-
507 return
|
|
508
|
-
508 }
|
|
509
|
-
509
|
|
510
|
-
510 socket.data.subscriptions.delete(subscriptionId)
|
|
511
|
-
511 const hasOther = Array.from(socket.data.subscriptions.values())
|
|
512
|
-
512 .some((sub) => sub.resource === subscription.resource)
|
|
513
|
-
513 if (!hasOther) {
|
|
514
|
-
514 socket.leave(`${subscription.resource}:updates`)
|
|
515
|
-
515 }
|
|
516
|
-
516
|
|
517
|
-
517 if (callback) callback({ success: true })
|
|
518
|
-
518 log.info(`Socket ${socket.id} unsubscribed from ${subscription.resource}`, { subscriptionId })
|
|
519
|
-
519 } catch (error) {
|
|
520
|
-
520 log.error('Socket.IO unsubscribe error', error)
|
|
521
|
-
521 if (callback) {
|
|
522
|
-
522 callback({
|
|
523
|
-
523 error: {
|
|
524
|
-
524 code: 'UNSUBSCRIBE_ERROR',
|
|
525
|
-
525 message: error.message
|
|
526
|
-
526 }
|
|
527
|
-
527 })
|
|
528
|
-
528 }
|
|
529
|
-
529 }
|
|
530
|
-
530 })
|
|
531
|
-
531
|
|
532
|
-
532 socket.on('restore-subscriptions', async (payload, callback) => {
|
|
533
|
-
533 try {
|
|
534
|
-
534 const subscriptions = payload?.subscriptions
|
|
535
|
-
535 if (!Array.isArray(subscriptions)) {
|
|
536
|
-
536 const error = { code: 'INVALID_DATA', message: 'Subscriptions must be an array' }
|
|
537
|
-
537 if (callback) callback({ error })
|
|
538
|
-
538 return
|
|
539
|
-
539 }
|
|
540
|
-
540
|
|
541
|
-
541 const result = await handleRestoreSubscriptions({
|
|
542
|
-
542 socket,
|
|
543
|
-
543 subscriptions,
|
|
544
|
-
544 scopes,
|
|
545
|
-
545 runHooks,
|
|
546
|
-
546 log,
|
|
547
|
-
547 config
|
|
548
|
-
548 })
|
|
549
|
-
549
|
|
550
|
-
550 if (callback) callback({ success: true, ...result })
|
|
551
|
-
551 } catch (error) {
|
|
552
|
-
552 log.error('Socket.IO restore subscriptions error', error)
|
|
553
|
-
553 if (callback) {
|
|
554
|
-
554 callback({
|
|
555
|
-
555 error: {
|
|
556
|
-
556 code: 'RESTORE_ERROR',
|
|
557
|
-
557 message: error.message
|
|
558
|
-
558 }
|
|
559
|
-
559 })
|
|
560
|
-
560 }
|
|
561
|
-
561 }
|
|
562
|
-
562 })
|
|
563
|
-
563
|
|
564
|
-
564 socket.on('disconnect', (reason) => {
|
|
565
|
-
565 log.info(`Socket disconnected: ${socket.id}`, {
|
|
566
|
-
566 reason,
|
|
567
|
-
567 subscriptionCount: socket.data.subscriptions?.size || 0
|
|
568
|
-
568 })
|
|
569
|
-
569 })
|
|
570
|
-
570 })
|
|
571
|
-
571
|
|
572
|
-
572 log.info('Socket.IO server started', { path })
|
|
573
|
-
573 return io
|
|
574
|
-
574 }
|
|
575
|
-
575
|
|
576
|
-
576 addHook('finish', 'socketio-broadcast', {}, async ({ context }) => {
|
|
577
|
-
577 const { method, scopeName, id } = context
|
|
578
|
-
578
|
|
579
|
-
579 if (!io) {
|
|
580
|
-
580 log.debug('Socket.IO broadcast skipped: server not started')
|
|
581
|
-
581 return
|
|
582
|
-
582 }
|
|
583
|
-
583
|
|
584
|
-
584 if (!['post', 'put', 'patch', 'delete'].includes(method)) {
|
|
585
|
-
585 return
|
|
586
|
-
586 }
|
|
587
|
-
587
|
|
588
|
-
588 if (!id && method !== 'delete') {
|
|
589
|
-
589 log.debug('Socket.IO broadcast skipped: missing record id')
|
|
590
|
-
590 return
|
|
591
|
-
591 }
|
|
592
|
-
592
|
|
593
|
-
593 if (context.transaction) {
|
|
594
|
-
594 if (!pendingBroadcasts.has(context.transaction)) {
|
|
595
|
-
595 pendingBroadcasts.set(context.transaction, [])
|
|
596
|
-
596 }
|
|
597
|
-
597 pendingBroadcasts.get(context.transaction).push({ method, scopeName, id, context })
|
|
598
|
-
598 return
|
|
599
|
-
599 }
|
|
600
|
-
600
|
|
601
|
-
601 await performBroadcast({ method, scopeName, id, context, api, io, log })
|
|
602
|
-
602 })
|
|
603
|
-
603
|
|
604
|
-
604 addHook('afterCommit', 'socketio-broadcast-deferred', {}, async ({ context }) => {
|
|
605
|
-
605 if (!context?.transaction) return
|
|
606
|
-
606 const broadcasts = pendingBroadcasts.get(context.transaction)
|
|
607
|
-
607 if (!broadcasts) return
|
|
608
|
-
608
|
|
609
|
-
609 for (const broadcast of broadcasts) {
|
|
610
|
-
610 await performBroadcast({ ...broadcast, api, io, log })
|
|
611
|
-
611 }
|
|
612
|
-
612
|
|
613
|
-
613 pendingBroadcasts.delete(context.transaction)
|
|
614
|
-
614 })
|
|
615
|
-
615
|
|
616
|
-
616 addHook('afterRollback', 'socketio-cleanup-broadcasts', {}, async ({ context }) => {
|
|
617
|
-
617 if (!context?.transaction) return
|
|
618
|
-
618 pendingBroadcasts.delete(context.transaction)
|
|
619
|
-
619 })
|
|
620
|
-
620 }
|
|
621
|
-
621 }
|