json-rest-api 1.0.28 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
package/tests/socketio.test.js
DELETED
|
@@ -1,860 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { io as ioClient } from 'socket.io-client'
|
|
5
|
-
import { SignJWT } from 'jose'
|
|
6
|
-
import {
|
|
7
|
-
validateJsonApiStructure,
|
|
8
|
-
cleanTables,
|
|
9
|
-
createJsonApiDocument,
|
|
10
|
-
assertResourceAttributes,
|
|
11
|
-
createRelationship,
|
|
12
|
-
resourceIdentifier
|
|
13
|
-
} from './helpers/test-utils.js'
|
|
14
|
-
import { createWebSocketApi } from './fixtures/api-configs.js'
|
|
15
|
-
|
|
16
|
-
// Create JWT token using jose
|
|
17
|
-
async function createToken (payload = {}, secret = 'test-secret-key') {
|
|
18
|
-
const encoder = new TextEncoder()
|
|
19
|
-
const key = encoder.encode(secret)
|
|
20
|
-
|
|
21
|
-
const jwt = new SignJWT({
|
|
22
|
-
sub: '123',
|
|
23
|
-
email: 'test@example.com',
|
|
24
|
-
roles: ['user'],
|
|
25
|
-
...payload,
|
|
26
|
-
jti: `test-${Date.now()}`
|
|
27
|
-
})
|
|
28
|
-
.setProtectedHeader({ alg: 'HS256' })
|
|
29
|
-
.setIssuedAt()
|
|
30
|
-
.setExpirationTime('1h')
|
|
31
|
-
|
|
32
|
-
return await jwt.sign(key)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Create Knex instance for tests
|
|
36
|
-
const knex = knexLib({
|
|
37
|
-
client: 'better-sqlite3',
|
|
38
|
-
connection: {
|
|
39
|
-
filename: ':memory:'
|
|
40
|
-
},
|
|
41
|
-
useNullAsDefault: true
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
// API instance that persists across ALL tests
|
|
45
|
-
let api
|
|
46
|
-
let server
|
|
47
|
-
|
|
48
|
-
// Helper function to wait for socket event with timeout
|
|
49
|
-
function waitForSocketEvent (socket, eventName, timeout = 1000) {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const timer = setTimeout(() => {
|
|
52
|
-
reject(new Error(`Timeout waiting for ${eventName}`))
|
|
53
|
-
}, timeout)
|
|
54
|
-
|
|
55
|
-
socket.once(eventName, (data) => {
|
|
56
|
-
clearTimeout(timer)
|
|
57
|
-
resolve(data)
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe('WebSocket/Socket.IO Plugin', () => {
|
|
63
|
-
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
64
|
-
before(async () => {
|
|
65
|
-
// Create API instance ONCE with WebSocket support
|
|
66
|
-
const result = await createWebSocketApi(knex)
|
|
67
|
-
api = result.api
|
|
68
|
-
server = result.server
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// IMPORTANT: after() cleans up resources
|
|
72
|
-
after(async () => {
|
|
73
|
-
// Skip cleanup if nothing was initialized
|
|
74
|
-
if (!api || !server) {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Close Socket.IO server first
|
|
79
|
-
if (api.vars.socketIO) {
|
|
80
|
-
// Disconnect all connected sockets
|
|
81
|
-
await api.vars.socketIO.disconnectSockets()
|
|
82
|
-
|
|
83
|
-
// Close the Socket.IO server
|
|
84
|
-
await new Promise((resolve) => {
|
|
85
|
-
api.vars.socketIO.close(() => {
|
|
86
|
-
resolve()
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Close Redis clients if they exist
|
|
92
|
-
if (api.vars.socketIORedisClients) {
|
|
93
|
-
await api.vars.socketIORedisClients.pubClient.quit()
|
|
94
|
-
await api.vars.socketIORedisClients.subClient.quit()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Close HTTP server
|
|
98
|
-
if (server) {
|
|
99
|
-
await new Promise((resolve) => {
|
|
100
|
-
server.close(resolve)
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Clean up JWT plugin resources
|
|
105
|
-
if (api.helpers?.auth?.cleanup) {
|
|
106
|
-
api.helpers.auth.cleanup()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Always destroy knex connection to allow tests to exit
|
|
110
|
-
await knex.destroy()
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
114
|
-
beforeEach(async () => {
|
|
115
|
-
// Clean all tables
|
|
116
|
-
await cleanTables(knex, [
|
|
117
|
-
'basic_countries',
|
|
118
|
-
'basic_publishers',
|
|
119
|
-
'basic_authors',
|
|
120
|
-
'basic_books',
|
|
121
|
-
'basic_book_authors'
|
|
122
|
-
])
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
describe('Basic Subscription and Notifications', () => {
|
|
126
|
-
it('should receive minimal notifications for subscribed resources', async () => {
|
|
127
|
-
// Generate a real JWT token for testing
|
|
128
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
129
|
-
|
|
130
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
131
|
-
path: '/api/socket.io',
|
|
132
|
-
auth: { token }
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
// Wait for connection
|
|
137
|
-
await new Promise((resolve, reject) => {
|
|
138
|
-
socket.on('connect', resolve)
|
|
139
|
-
socket.on('connect_error', reject)
|
|
140
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// Subscribe to posts with status filter
|
|
144
|
-
const subResponse = await new Promise((resolve) => {
|
|
145
|
-
socket.emit('subscribe', {
|
|
146
|
-
resource: 'books',
|
|
147
|
-
filters: { title: 'Test Book' } // Exact match
|
|
148
|
-
}, resolve)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
assert(subResponse.success, 'Subscription should succeed')
|
|
152
|
-
assert(subResponse.data.subscriptionId, 'Should return subscription ID')
|
|
153
|
-
|
|
154
|
-
// Listen for updates
|
|
155
|
-
const updatePromise = waitForSocketEvent(socket, 'subscription.update')
|
|
156
|
-
|
|
157
|
-
// Create a country first (required for books)
|
|
158
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
159
|
-
name: 'Test Country',
|
|
160
|
-
code: 'TC'
|
|
161
|
-
})
|
|
162
|
-
const countryResult = await api.resources.countries.post({
|
|
163
|
-
inputRecord: countryDoc,
|
|
164
|
-
simplified: false
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Create a book using API
|
|
168
|
-
const bookDoc = createJsonApiDocument('books',
|
|
169
|
-
{ title: 'Test Book' },
|
|
170
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
const createResult = await api.resources.books.post({
|
|
174
|
-
inputRecord: bookDoc,
|
|
175
|
-
simplified: false
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// Check notification
|
|
179
|
-
const notification = await updatePromise
|
|
180
|
-
assert.equal(notification.type, 'resource.postd')
|
|
181
|
-
assert.equal(notification.resource, 'books')
|
|
182
|
-
assert.equal(String(notification.id), String(createResult.data.id))
|
|
183
|
-
assert.equal(notification.action, 'post')
|
|
184
|
-
assert(!notification.data, 'Should not include data in notification')
|
|
185
|
-
} finally {
|
|
186
|
-
socket.close()
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('should not receive notifications for non-matching filters', async () => {
|
|
191
|
-
// Generate a real JWT token for testing
|
|
192
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
193
|
-
|
|
194
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
195
|
-
path: '/api/socket.io',
|
|
196
|
-
auth: { token }
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
// Wait for connection
|
|
201
|
-
await new Promise((resolve, reject) => {
|
|
202
|
-
socket.on('connect', () => {
|
|
203
|
-
console.log('Socket connected successfully')
|
|
204
|
-
resolve()
|
|
205
|
-
})
|
|
206
|
-
socket.on('connect_error', (error) => {
|
|
207
|
-
console.error('Connection error:', error.message, error.type)
|
|
208
|
-
reject(error)
|
|
209
|
-
})
|
|
210
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
// Subscribe with specific filter
|
|
214
|
-
await new Promise((resolve) => {
|
|
215
|
-
socket.emit('subscribe', {
|
|
216
|
-
resource: 'books',
|
|
217
|
-
filters: { title: 'Specific Title' }
|
|
218
|
-
}, resolve)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// Create a country first (required for books)
|
|
222
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
223
|
-
name: 'Test Country',
|
|
224
|
-
code: 'TC'
|
|
225
|
-
})
|
|
226
|
-
const countryResult = await api.resources.countries.post({
|
|
227
|
-
inputRecord: countryDoc,
|
|
228
|
-
simplified: false
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
// Create a book that doesn't match filter
|
|
232
|
-
const bookDoc = createJsonApiDocument('books',
|
|
233
|
-
{ title: 'Different Book' },
|
|
234
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
await api.resources.books.post({
|
|
238
|
-
inputRecord: bookDoc,
|
|
239
|
-
simplified: false
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// Should not receive update
|
|
243
|
-
await assert.rejects(
|
|
244
|
-
waitForSocketEvent(socket, 'subscription.update', 500),
|
|
245
|
-
{ message: /Timeout/ }
|
|
246
|
-
)
|
|
247
|
-
} finally {
|
|
248
|
-
socket.close()
|
|
249
|
-
}
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
describe('Filter Validation', () => {
|
|
254
|
-
it('should validate filters against searchSchema', async () => {
|
|
255
|
-
// Generate a real JWT token for testing
|
|
256
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
257
|
-
|
|
258
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
259
|
-
path: '/api/socket.io',
|
|
260
|
-
auth: { token }
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
// Wait for connection
|
|
265
|
-
await new Promise((resolve, reject) => {
|
|
266
|
-
socket.on('connect', resolve)
|
|
267
|
-
socket.on('connect_error', reject)
|
|
268
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
269
|
-
})
|
|
270
|
-
// Try invalid filter
|
|
271
|
-
const response = await new Promise((resolve) => {
|
|
272
|
-
socket.emit('subscribe', {
|
|
273
|
-
resource: 'books',
|
|
274
|
-
filters: { invalid_field: 'value' }
|
|
275
|
-
}, resolve)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
assert(response.error, 'Should return error')
|
|
279
|
-
assert.equal(response.error.code, 'INVALID_FILTERS')
|
|
280
|
-
} finally {
|
|
281
|
-
socket.close()
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('should reject function filters without filterRecord', async () => {
|
|
286
|
-
// Generate a real JWT token for testing
|
|
287
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
288
|
-
|
|
289
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
290
|
-
path: '/api/socket.io',
|
|
291
|
-
auth: { token }
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
// Wait for connection
|
|
296
|
-
await new Promise((resolve, reject) => {
|
|
297
|
-
socket.on('connect', resolve)
|
|
298
|
-
socket.on('connect_error', reject)
|
|
299
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
300
|
-
})
|
|
301
|
-
// Create a country first
|
|
302
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
303
|
-
name: 'Test Country',
|
|
304
|
-
code: 'TC'
|
|
305
|
-
})
|
|
306
|
-
const countryResult = await api.resources.countries.post({
|
|
307
|
-
inputRecord: countryDoc,
|
|
308
|
-
simplified: false
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
// Assuming we add a complex filter to the schema that uses filterOperator function
|
|
312
|
-
// For now, test with country_id which is a simple filter
|
|
313
|
-
const response = await new Promise((resolve) => {
|
|
314
|
-
socket.emit('subscribe', {
|
|
315
|
-
resource: 'books',
|
|
316
|
-
filters: { country: countryResult.data.id }
|
|
317
|
-
}, resolve)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
// Should succeed for simple filters
|
|
321
|
-
assert(response.success, 'Should succeed for simple filters')
|
|
322
|
-
} finally {
|
|
323
|
-
socket.close()
|
|
324
|
-
}
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe('Transaction Safety', () => {
|
|
329
|
-
// These tests verify that broadcasts are properly deferred until after transaction commit
|
|
330
|
-
// The implementation uses:
|
|
331
|
-
// - WeakMap to store pending broadcasts per transaction
|
|
332
|
-
// - afterCommit hook to broadcast after successful transactions
|
|
333
|
-
// - afterRollback hook to clean up after failed transactions
|
|
334
|
-
// - No mutation of transaction objects
|
|
335
|
-
|
|
336
|
-
it('should not broadcast when operations fail', async () => {
|
|
337
|
-
// This test verifies that broadcasts don't happen when operations fail
|
|
338
|
-
// The library should rollback transactions and not send notifications
|
|
339
|
-
|
|
340
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
341
|
-
|
|
342
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
343
|
-
path: '/api/socket.io',
|
|
344
|
-
auth: { token }
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
// Wait for connection
|
|
349
|
-
await new Promise((resolve, reject) => {
|
|
350
|
-
socket.on('connect', resolve)
|
|
351
|
-
socket.on('connect_error', reject)
|
|
352
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
// Subscribe to all books
|
|
356
|
-
await new Promise((resolve) => {
|
|
357
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
// Track notifications
|
|
361
|
-
const notifications = []
|
|
362
|
-
socket.on('subscription.update', (notification) => {
|
|
363
|
-
notifications.push(notification)
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
// Try to create a book without required country relationship
|
|
367
|
-
const bookDoc = createJsonApiDocument('books',
|
|
368
|
-
{ title: 'Invalid Book' }
|
|
369
|
-
// Missing required country relationship
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
await api.resources.books.post({
|
|
374
|
-
inputRecord: bookDoc,
|
|
375
|
-
simplified: false
|
|
376
|
-
})
|
|
377
|
-
assert.fail('Should have thrown an error')
|
|
378
|
-
} catch (error) {
|
|
379
|
-
// Expected to fail due to missing required relationship
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Wait a bit to ensure no broadcast happens
|
|
383
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
384
|
-
|
|
385
|
-
// Should not have received any notification
|
|
386
|
-
assert.equal(notifications.length, 0, 'Should not receive any notifications for failed operations')
|
|
387
|
-
} finally {
|
|
388
|
-
socket.close()
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
it('should broadcast after successful operations', async () => {
|
|
393
|
-
// This test verifies that broadcasts happen after successful operations
|
|
394
|
-
// The library handles transactions internally and uses afterCommit hook
|
|
395
|
-
// to ensure broadcasts happen only after successful commit
|
|
396
|
-
|
|
397
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
398
|
-
|
|
399
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
400
|
-
path: '/api/socket.io',
|
|
401
|
-
auth: { token }
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
try {
|
|
405
|
-
// Wait for connection
|
|
406
|
-
await new Promise((resolve, reject) => {
|
|
407
|
-
socket.on('connect', resolve)
|
|
408
|
-
socket.on('connect_error', reject)
|
|
409
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
// Subscribe to all books
|
|
413
|
-
await new Promise((resolve) => {
|
|
414
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
// Create a country first (required for books)
|
|
418
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
419
|
-
name: 'Test Country',
|
|
420
|
-
code: 'TC'
|
|
421
|
-
})
|
|
422
|
-
const countryResult = await api.resources.countries.post({
|
|
423
|
-
inputRecord: countryDoc,
|
|
424
|
-
simplified: false
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
// Track notifications
|
|
428
|
-
const notifications = []
|
|
429
|
-
socket.on('subscription.update', (notification) => {
|
|
430
|
-
notifications.push(notification)
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
// Create book - the library will handle transaction internally if configured
|
|
434
|
-
const bookDoc = createJsonApiDocument('books',
|
|
435
|
-
{ title: 'Broadcast Test Book' },
|
|
436
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
const createResult = await api.resources.books.post({
|
|
440
|
-
inputRecord: bookDoc,
|
|
441
|
-
simplified: false
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
// Wait a bit to ensure broadcast happens (setImmediate)
|
|
445
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
446
|
-
|
|
447
|
-
// Should have received the notification
|
|
448
|
-
assert.equal(notifications.length, 1, 'Should receive one notification')
|
|
449
|
-
assert.equal(notifications[0].type, 'resource.postd')
|
|
450
|
-
assert.equal(notifications[0].resource, 'books')
|
|
451
|
-
assert.equal(String(notifications[0].id), String(createResult.data.id))
|
|
452
|
-
} finally {
|
|
453
|
-
socket.close()
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
|
|
457
|
-
it('should broadcast deferred patch notifications from relationship PATCH', async () => {
|
|
458
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
459
|
-
|
|
460
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
461
|
-
path: '/api/socket.io',
|
|
462
|
-
auth: { token }
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
await new Promise((resolve, reject) => {
|
|
467
|
-
socket.on('connect', resolve)
|
|
468
|
-
socket.on('connect_error', reject)
|
|
469
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
const country = await api.resources.countries.post({
|
|
473
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
474
|
-
name: 'Original Country',
|
|
475
|
-
code: 'OC'
|
|
476
|
-
}),
|
|
477
|
-
simplified: false
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
const nextCountry = await api.resources.countries.post({
|
|
481
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
482
|
-
name: 'Next Country',
|
|
483
|
-
code: 'NC'
|
|
484
|
-
}),
|
|
485
|
-
simplified: false
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
const book = await api.resources.books.post({
|
|
489
|
-
inputRecord: createJsonApiDocument(
|
|
490
|
-
'books',
|
|
491
|
-
{ title: 'Relationship Patch Broadcast' },
|
|
492
|
-
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
493
|
-
),
|
|
494
|
-
simplified: false
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
await new Promise((resolve) => {
|
|
498
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
const updatePromise = waitForSocketEvent(socket, 'subscription.update', 1000)
|
|
502
|
-
|
|
503
|
-
await api.resources.books.patchRelationship({
|
|
504
|
-
id: book.data.id,
|
|
505
|
-
relationshipName: 'country',
|
|
506
|
-
relationshipData: resourceIdentifier('countries', nextCountry.data.id)
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
const notification = await updatePromise
|
|
510
|
-
assert.equal(notification.type, 'resource.patchd')
|
|
511
|
-
assert.equal(notification.resource, 'books')
|
|
512
|
-
assert.equal(String(notification.id), String(book.data.id))
|
|
513
|
-
assert.equal(notification.action, 'patch')
|
|
514
|
-
} finally {
|
|
515
|
-
socket.close()
|
|
516
|
-
}
|
|
517
|
-
})
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
describe('Multiple Subscriptions', () => {
|
|
521
|
-
it('should handle multiple subscriptions from same client', async () => {
|
|
522
|
-
// Generate a real JWT token for testing
|
|
523
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
524
|
-
|
|
525
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
526
|
-
path: '/api/socket.io',
|
|
527
|
-
auth: { token }
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
// Wait for connection
|
|
532
|
-
await new Promise((resolve, reject) => {
|
|
533
|
-
socket.on('connect', resolve)
|
|
534
|
-
socket.on('connect_error', reject)
|
|
535
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
536
|
-
})
|
|
537
|
-
// Subscribe to books and countries
|
|
538
|
-
const bookSubResponse = await new Promise((resolve) => {
|
|
539
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
const countrySubResponse = await new Promise((resolve) => {
|
|
543
|
-
socket.emit('subscribe', { resource: 'countries' }, resolve)
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
assert(bookSubResponse.success)
|
|
547
|
-
assert(countrySubResponse.success)
|
|
548
|
-
assert.notEqual(bookSubResponse.data.subscriptionId, countrySubResponse.data.subscriptionId)
|
|
549
|
-
|
|
550
|
-
// Create both resources
|
|
551
|
-
const notifications = []
|
|
552
|
-
socket.on('subscription.update', (notification) => {
|
|
553
|
-
notifications.push(notification)
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
// Create a country first (required for books)
|
|
557
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Multi Test Country', code: 'MT' })
|
|
558
|
-
const countryResult = await api.resources.countries.post({ inputRecord: countryDoc, simplified: false })
|
|
559
|
-
|
|
560
|
-
const bookDoc = createJsonApiDocument('books',
|
|
561
|
-
{ title: 'Multi Test Book' },
|
|
562
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
563
|
-
)
|
|
564
|
-
await api.resources.books.post({ inputRecord: bookDoc, simplified: false })
|
|
565
|
-
|
|
566
|
-
const countryDoc2 = createJsonApiDocument('countries', { name: 'Multi Test Country 2', code: 'MT2' })
|
|
567
|
-
await api.resources.countries.post({ inputRecord: countryDoc2, simplified: false })
|
|
568
|
-
|
|
569
|
-
// Wait a bit for notifications
|
|
570
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
571
|
-
|
|
572
|
-
assert.equal(notifications.length, 3, 'Should receive 3 notifications')
|
|
573
|
-
assert.equal(notifications.filter(n => n.resource === 'books').length, 1, 'Should receive 1 book notification')
|
|
574
|
-
assert.equal(notifications.filter(n => n.resource === 'countries').length, 2, 'Should receive 2 country notifications')
|
|
575
|
-
} finally {
|
|
576
|
-
socket.close()
|
|
577
|
-
}
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
it('should unsubscribe correctly', async () => {
|
|
581
|
-
// Generate a real JWT token for testing
|
|
582
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
583
|
-
|
|
584
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
585
|
-
path: '/api/socket.io',
|
|
586
|
-
auth: { token }
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
try {
|
|
590
|
-
// Wait for connection
|
|
591
|
-
await new Promise((resolve, reject) => {
|
|
592
|
-
socket.on('connect', resolve)
|
|
593
|
-
socket.on('connect_error', reject)
|
|
594
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
595
|
-
})
|
|
596
|
-
// Subscribe
|
|
597
|
-
const subResponse = await new Promise((resolve) => {
|
|
598
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
const subscriptionId = subResponse.data.subscriptionId
|
|
602
|
-
|
|
603
|
-
// Unsubscribe
|
|
604
|
-
const unsubResponse = await new Promise((resolve) => {
|
|
605
|
-
socket.emit('unsubscribe', { subscriptionId }, resolve)
|
|
606
|
-
})
|
|
607
|
-
|
|
608
|
-
assert(unsubResponse.success)
|
|
609
|
-
|
|
610
|
-
// Create a country first (required for books)
|
|
611
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
612
|
-
name: 'Test Country',
|
|
613
|
-
code: 'TC'
|
|
614
|
-
})
|
|
615
|
-
const countryResult = await api.resources.countries.post({
|
|
616
|
-
inputRecord: countryDoc,
|
|
617
|
-
simplified: false
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
// Create a book
|
|
621
|
-
const bookDoc = createJsonApiDocument('books',
|
|
622
|
-
{ title: 'After Unsub Book' },
|
|
623
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
624
|
-
)
|
|
625
|
-
await api.resources.books.post({ inputRecord: bookDoc, simplified: false })
|
|
626
|
-
|
|
627
|
-
// Should not receive notification
|
|
628
|
-
await assert.rejects(
|
|
629
|
-
waitForSocketEvent(socket, 'subscription.update', 500),
|
|
630
|
-
{ message: /Timeout/ }
|
|
631
|
-
)
|
|
632
|
-
} finally {
|
|
633
|
-
socket.close()
|
|
634
|
-
}
|
|
635
|
-
})
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
describe('Update and Delete Notifications', () => {
|
|
639
|
-
it('should receive notifications for updates', async () => {
|
|
640
|
-
// Generate a real JWT token for testing
|
|
641
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
642
|
-
|
|
643
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
644
|
-
path: '/api/socket.io',
|
|
645
|
-
auth: { token }
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
// Wait for connection
|
|
650
|
-
await new Promise((resolve, reject) => {
|
|
651
|
-
socket.on('connect', resolve)
|
|
652
|
-
socket.on('connect_error', reject)
|
|
653
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
654
|
-
})
|
|
655
|
-
// Create a country first (required for books)
|
|
656
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
657
|
-
name: 'Test Country',
|
|
658
|
-
code: 'TC'
|
|
659
|
-
})
|
|
660
|
-
const countryResult = await api.resources.countries.post({
|
|
661
|
-
inputRecord: countryDoc,
|
|
662
|
-
simplified: false
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
// Create a book first
|
|
666
|
-
const bookDoc = createJsonApiDocument('books',
|
|
667
|
-
{ title: 'Original Title' },
|
|
668
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
669
|
-
)
|
|
670
|
-
const createResult = await api.resources.books.post({
|
|
671
|
-
inputRecord: bookDoc,
|
|
672
|
-
simplified: false
|
|
673
|
-
})
|
|
674
|
-
const bookId = createResult.data.id
|
|
675
|
-
|
|
676
|
-
// Subscribe to books
|
|
677
|
-
await new Promise((resolve) => {
|
|
678
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
679
|
-
})
|
|
680
|
-
|
|
681
|
-
// Update the book
|
|
682
|
-
const updatePromise = waitForSocketEvent(socket, 'subscription.update')
|
|
683
|
-
|
|
684
|
-
const patchDoc = {
|
|
685
|
-
data: {
|
|
686
|
-
type: 'books',
|
|
687
|
-
id: String(bookId),
|
|
688
|
-
attributes: { title: 'Updated Title' }
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
await api.resources.books.patch({
|
|
693
|
-
id: bookId,
|
|
694
|
-
inputRecord: patchDoc,
|
|
695
|
-
simplified: false
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
// Check notification
|
|
699
|
-
const notification = await updatePromise
|
|
700
|
-
assert.equal(notification.type, 'resource.patchd')
|
|
701
|
-
assert.equal(notification.resource, 'books')
|
|
702
|
-
assert.equal(String(notification.id), String(bookId))
|
|
703
|
-
assert.equal(notification.action, 'patch')
|
|
704
|
-
} finally {
|
|
705
|
-
socket.close()
|
|
706
|
-
}
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
it('should receive notifications for deletes', async () => {
|
|
710
|
-
// Generate a real JWT token for testing
|
|
711
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
712
|
-
|
|
713
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
714
|
-
path: '/api/socket.io',
|
|
715
|
-
auth: { token }
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
try {
|
|
719
|
-
// Wait for connection
|
|
720
|
-
await new Promise((resolve, reject) => {
|
|
721
|
-
socket.on('connect', resolve)
|
|
722
|
-
socket.on('connect_error', reject)
|
|
723
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
724
|
-
})
|
|
725
|
-
// Create a country first (required for books)
|
|
726
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
727
|
-
name: 'Test Country',
|
|
728
|
-
code: 'TC'
|
|
729
|
-
})
|
|
730
|
-
const countryResult = await api.resources.countries.post({
|
|
731
|
-
inputRecord: countryDoc,
|
|
732
|
-
simplified: false
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
// Create a book first
|
|
736
|
-
const bookDoc = createJsonApiDocument('books',
|
|
737
|
-
{ title: 'To Delete' },
|
|
738
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
739
|
-
)
|
|
740
|
-
const createResult = await api.resources.books.post({
|
|
741
|
-
inputRecord: bookDoc,
|
|
742
|
-
simplified: false
|
|
743
|
-
})
|
|
744
|
-
const bookId = createResult.data.id
|
|
745
|
-
|
|
746
|
-
// Subscribe to books
|
|
747
|
-
await new Promise((resolve) => {
|
|
748
|
-
socket.emit('subscribe', { resource: 'books' }, resolve)
|
|
749
|
-
})
|
|
750
|
-
|
|
751
|
-
// Delete the book
|
|
752
|
-
const deletePromise = waitForSocketEvent(socket, 'subscription.update')
|
|
753
|
-
|
|
754
|
-
await api.resources.books.delete({ id: bookId })
|
|
755
|
-
|
|
756
|
-
// Check notification
|
|
757
|
-
const notification = await deletePromise
|
|
758
|
-
assert.equal(notification.type, 'resource.deleted')
|
|
759
|
-
assert.equal(notification.resource, 'books')
|
|
760
|
-
assert.equal(String(notification.id), String(bookId))
|
|
761
|
-
assert.equal(notification.action, 'delete')
|
|
762
|
-
} finally {
|
|
763
|
-
socket.close()
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
describe('Relationship Filters', () => {
|
|
769
|
-
it('should filter by relationship fields', async () => {
|
|
770
|
-
// Generate a real JWT token for testing
|
|
771
|
-
const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
|
|
772
|
-
|
|
773
|
-
const socket = ioClient(`http://localhost:${server.address().port}`, {
|
|
774
|
-
path: '/api/socket.io',
|
|
775
|
-
auth: { token }
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
try {
|
|
779
|
-
// Wait for connection
|
|
780
|
-
await new Promise((resolve, reject) => {
|
|
781
|
-
socket.on('connect', resolve)
|
|
782
|
-
socket.on('connect_error', reject)
|
|
783
|
-
setTimeout(() => reject(new Error('Connection timeout')), 5000)
|
|
784
|
-
})
|
|
785
|
-
// Create test data
|
|
786
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Filter Country', code: 'FC' })
|
|
787
|
-
const countryResult = await api.resources.countries.post({
|
|
788
|
-
inputRecord: countryDoc,
|
|
789
|
-
simplified: false
|
|
790
|
-
})
|
|
791
|
-
|
|
792
|
-
const publisherDoc = createJsonApiDocument('publishers',
|
|
793
|
-
{ name: 'Filter Publisher' },
|
|
794
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
795
|
-
)
|
|
796
|
-
const publisherResult = await api.resources.publishers.post({
|
|
797
|
-
inputRecord: publisherDoc,
|
|
798
|
-
simplified: false
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
// Subscribe to books filtered by publisher
|
|
802
|
-
await new Promise((resolve) => {
|
|
803
|
-
socket.emit('subscribe', {
|
|
804
|
-
resource: 'books',
|
|
805
|
-
filters: { publisher: publisherResult.data.id }
|
|
806
|
-
}, resolve)
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
// Create book with matching publisher
|
|
810
|
-
const matchingBookPromise = waitForSocketEvent(socket, 'subscription.update')
|
|
811
|
-
|
|
812
|
-
const matchingBookDoc = createJsonApiDocument('books',
|
|
813
|
-
{ title: 'Matching Book' },
|
|
814
|
-
{
|
|
815
|
-
country: createRelationship(resourceIdentifier('countries', countryResult.data.id)),
|
|
816
|
-
publisher: createRelationship(resourceIdentifier('publishers', publisherResult.data.id))
|
|
817
|
-
}
|
|
818
|
-
)
|
|
819
|
-
await api.resources.books.post({
|
|
820
|
-
inputRecord: matchingBookDoc,
|
|
821
|
-
simplified: false
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
// Should receive notification
|
|
825
|
-
const notification = await matchingBookPromise
|
|
826
|
-
assert.equal(notification.resource, 'books')
|
|
827
|
-
|
|
828
|
-
// Create book with different publisher
|
|
829
|
-
const otherPublisherDoc = createJsonApiDocument('publishers',
|
|
830
|
-
{ name: 'Other Publisher' },
|
|
831
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
832
|
-
)
|
|
833
|
-
const otherPublisherResult = await api.resources.publishers.post({
|
|
834
|
-
inputRecord: otherPublisherDoc,
|
|
835
|
-
simplified: false
|
|
836
|
-
})
|
|
837
|
-
|
|
838
|
-
const nonMatchingBookDoc = createJsonApiDocument('books',
|
|
839
|
-
{ title: 'Non-Matching Book' },
|
|
840
|
-
{
|
|
841
|
-
country: createRelationship(resourceIdentifier('countries', countryResult.data.id)),
|
|
842
|
-
publisher: createRelationship(resourceIdentifier('publishers', otherPublisherResult.data.id))
|
|
843
|
-
}
|
|
844
|
-
)
|
|
845
|
-
await api.resources.books.post({
|
|
846
|
-
inputRecord: nonMatchingBookDoc,
|
|
847
|
-
simplified: false
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
// Should not receive notification for non-matching book
|
|
851
|
-
await assert.rejects(
|
|
852
|
-
waitForSocketEvent(socket, 'subscription.update', 500),
|
|
853
|
-
{ message: /Timeout/ }
|
|
854
|
-
)
|
|
855
|
-
} finally {
|
|
856
|
-
socket.close()
|
|
857
|
-
}
|
|
858
|
-
})
|
|
859
|
-
})
|
|
860
|
-
})
|