json-rest-api 1.0.13 → 1.0.15

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.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,115 +1,115 @@
1
- import { requirePackage } from 'hooked-api';
1
+ import { requirePackage } from 'hooked-api'
2
2
 
3
- const pendingBroadcasts = new WeakMap();
3
+ const pendingBroadcasts = new WeakMap()
4
4
 
5
- function normalizeAuthContext(auth) {
6
- if (!auth) return null;
7
- if (typeof auth !== 'object') return null;
8
- const normalized = { ...auth };
5
+ function normalizeAuthContext (auth) {
6
+ if (!auth) return null
7
+ if (typeof auth !== 'object') return null
8
+ const normalized = { ...auth }
9
9
  if (normalized.userId !== undefined && normalized.userId !== null) {
10
- normalized.userId = String(normalized.userId);
10
+ normalized.userId = String(normalized.userId)
11
11
  }
12
12
  if (normalized.roles && !Array.isArray(normalized.roles)) {
13
- normalized.roles = [normalized.roles];
13
+ normalized.roles = [normalized.roles]
14
14
  }
15
- return normalized;
15
+ return normalized
16
16
  }
17
17
 
18
- function matchesFilters(record, filters, searchSchemaStructure) {
19
- if (!filters || Object.keys(filters).length === 0) return true;
18
+ function matchesFilters (record, filters, searchSchemaStructure) {
19
+ if (!filters || Object.keys(filters).length === 0) return true
20
20
 
21
- if (!record) return false;
21
+ if (!record) return false
22
22
 
23
23
  if (!searchSchemaStructure || Object.keys(searchSchemaStructure).length === 0) {
24
- return false;
24
+ return false
25
25
  }
26
26
 
27
27
  for (const [filterKey, filterValue] of Object.entries(filters)) {
28
- const fieldDef = searchSchemaStructure[filterKey];
29
- if (!fieldDef) continue;
28
+ const fieldDef = searchSchemaStructure[filterKey]
29
+ if (!fieldDef) continue
30
30
 
31
- const fieldName = fieldDef.actualField || filterKey;
31
+ const fieldName = fieldDef.actualField || filterKey
32
32
 
33
- let recordValue;
33
+ let recordValue
34
34
  if (fieldDef.isRelationship) {
35
- const relationshipData = record.relationships?.[filterKey]?.data;
36
- recordValue = relationshipData?.id;
35
+ const relationshipData = record.relationships?.[filterKey]?.data
36
+ recordValue = relationshipData?.id
37
37
  } else {
38
- recordValue = record.attributes?.[fieldName] ?? record[fieldName];
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
- return false;
43
+ return false
44
44
  }
45
- continue;
45
+ continue
46
46
  }
47
47
 
48
48
  if (typeof fieldDef.filterOperator === 'function') {
49
49
  if (!fieldDef.filterRecord?.(record, filterValue)) {
50
- return false;
50
+ return false
51
51
  }
52
- continue;
52
+ continue
53
53
  }
54
54
 
55
- const operator = fieldDef.filterOperator || '=';
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
- return false;
60
+ return false
61
61
  }
62
- break;
62
+ break
63
63
  case 'in':
64
64
  if (Array.isArray(filterValue)) {
65
65
  if (!filterValue.includes(recordValue)) {
66
- return false;
66
+ return false
67
67
  }
68
68
  } else if (recordValue !== filterValue) {
69
- return false;
69
+ return false
70
70
  }
71
- break;
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
- return false;
75
+ return false
76
76
  }
77
77
  }
78
- break;
78
+ break
79
79
  case '>':
80
- if (!(recordValue > filterValue)) return false;
81
- break;
80
+ if (!(recordValue > filterValue)) return false
81
+ break
82
82
  case '>=':
83
- if (!(recordValue >= filterValue)) return false;
84
- break;
83
+ if (!(recordValue >= filterValue)) return false
84
+ break
85
85
  case '<':
86
- if (!(recordValue < filterValue)) return false;
87
- break;
86
+ if (!(recordValue < filterValue)) return false
87
+ break
88
88
  case '<=':
89
- if (!(recordValue <= filterValue)) return false;
90
- break;
89
+ if (!(recordValue <= filterValue)) return false
90
+ break
91
91
  case '!=':
92
92
  case '<>':
93
- if (recordValue === filterValue) return false;
94
- break;
93
+ if (recordValue === filterValue) return false
94
+ break
95
95
  case '=':
96
96
  default:
97
97
  if (fieldDef.isRelationship) {
98
98
  if (String(recordValue) !== String(filterValue)) {
99
- return false;
99
+ return false
100
100
  }
101
101
  } else if (recordValue !== filterValue) {
102
- return false;
102
+ return false
103
103
  }
104
104
  }
105
105
  }
106
106
 
107
- return true;
107
+ return true
108
108
  }
109
109
 
110
- function buildConfig(pluginOptions = {}) {
111
- const authOptions = pluginOptions.auth || {};
112
- const subscriptionOptions = pluginOptions.subscriptions || {};
110
+ function buildConfig (pluginOptions = {}) {
111
+ const authOptions = pluginOptions.auth || {}
112
+ const subscriptionOptions = pluginOptions.subscriptions || {}
113
113
 
114
114
  return {
115
115
  auth: {
@@ -123,42 +123,42 @@ function buildConfig(pluginOptions = {}) {
123
123
  maxPerSocket: subscriptionOptions.maxPerSocket ?? 100
124
124
  },
125
125
  transport: pluginOptions.transport || {}
126
- };
126
+ }
127
127
  }
128
128
 
129
- async function authenticateSocket({ socket, api, helpers, log, config }) {
130
- const { authenticate, allowClientProvidedAuth, anonymousContext, requireAuth, onAuthenticationFailed } = config.auth;
129
+ async function authenticateSocket ({ socket, api, helpers, log, config }) {
130
+ const { authenticate, allowClientProvidedAuth, anonymousContext, requireAuth, onAuthenticationFailed } = config.auth
131
131
 
132
132
  try {
133
- let authContext = null;
133
+ let authContext = null
134
134
 
135
135
  if (authenticate) {
136
- authContext = await authenticate({ socket, api, helpers, log });
136
+ authContext = await authenticate({ socket, api, helpers, log })
137
137
  } else if (allowClientProvidedAuth && socket.handshake.auth && typeof socket.handshake.auth === 'object') {
138
- authContext = socket.handshake.auth;
138
+ authContext = socket.handshake.auth
139
139
  } else if (anonymousContext) {
140
- authContext = anonymousContext;
140
+ authContext = anonymousContext
141
141
  }
142
142
 
143
- const normalized = normalizeAuthContext(authContext);
143
+ const normalized = normalizeAuthContext(authContext)
144
144
  if (!normalized && requireAuth) {
145
- throw new Error('Authentication required');
145
+ throw new Error('Authentication required')
146
146
  }
147
147
 
148
- return normalized;
148
+ return normalized
149
149
  } catch (error) {
150
150
  if (onAuthenticationFailed) {
151
151
  try {
152
- await onAuthenticationFailed({ socket, error, log });
152
+ await onAuthenticationFailed({ socket, error, log })
153
153
  } catch (hookError) {
154
- log.error('socketio auth failure handler threw error', hookError);
154
+ log.error('socketio auth failure handler threw error', hookError)
155
155
  }
156
156
  }
157
- throw error;
157
+ throw error
158
158
  }
159
159
  }
160
160
 
161
- async function registerSubscription({
161
+ async function registerSubscription ({
162
162
  socket,
163
163
  data,
164
164
  scopes,
@@ -166,94 +166,94 @@ async function registerSubscription({
166
166
  log,
167
167
  config
168
168
  }) {
169
- const { resource, filters = {}, include, fields, subscriptionId } = data || {};
169
+ const { resource, filters = {}, include, fields, subscriptionId } = data || {}
170
170
 
171
171
  if (!resource || typeof resource !== 'string') {
172
- throw Object.assign(new Error('Resource is required'), { code: 'RESOURCE_REQUIRED' });
172
+ throw Object.assign(new Error('Resource is required'), { code: 'RESOURCE_REQUIRED' })
173
173
  }
174
174
 
175
- const scope = scopes[resource];
175
+ const scope = scopes[resource]
176
176
  if (!scope) {
177
- throw Object.assign(new Error(`Resource '${resource}' not found`), { code: 'RESOURCE_NOT_FOUND' });
177
+ throw Object.assign(new Error(`Resource '${resource}' not found`), { code: 'RESOURCE_NOT_FOUND' })
178
178
  }
179
179
 
180
- const auth = socket.data.auth || null;
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
- const schemaInfo = scope.vars?.schemaInfo;
190
- const searchSchemaStructure = schemaInfo?.searchSchemaStructure;
191
- const searchSchemaInstance = schemaInfo?.searchSchemaInstance;
189
+ const schemaInfo = scope.vars?.schemaInfo
190
+ const searchSchemaStructure = schemaInfo?.searchSchemaStructure
191
+ const searchSchemaInstance = schemaInfo?.searchSchemaInstance
192
192
 
193
- const validatedFilters = { ...filters };
193
+ const validatedFilters = { ...filters }
194
194
  if (Object.keys(validatedFilters).length > 0) {
195
195
  if (!searchSchemaStructure || Object.keys(searchSchemaStructure).length === 0 || !searchSchemaInstance) {
196
- throw Object.assign(new Error(`Filtering is not enabled for resource '${resource}'`), { code: 'FILTERING_NOT_ENABLED' });
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
- const fieldDef = searchSchemaStructure[filterKey];
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
- const error = new Error('Invalid filter values');
215
- error.code = 'INVALID_FILTERS';
216
- error.details = errors;
217
- throw error;
214
+ const error = new Error('Invalid filter values')
215
+ error.code = 'INVALID_FILTERS'
216
+ error.details = errors
217
+ throw error
218
218
  }
219
219
 
220
- Object.assign(validatedFilters, validatedObject);
220
+ Object.assign(validatedFilters, validatedObject)
221
221
  }
222
222
 
223
223
  if (include !== undefined) {
224
224
  if (!Array.isArray(include)) {
225
- throw Object.assign(new Error('Include parameter must be an array'), { code: 'INVALID_INCLUDE' });
225
+ throw Object.assign(new Error('Include parameter must be an array'), { code: 'INVALID_INCLUDE' })
226
226
  }
227
227
 
228
- const relationships = schemaInfo?.schemaRelationships || {};
228
+ const relationships = schemaInfo?.schemaRelationships || {}
229
229
  for (const includePath of include) {
230
- const baseName = includePath.split('.')[0];
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
- throw Object.assign(new Error('Fields parameter must be an object'), { code: 'INVALID_FIELDS' });
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
- throw Object.assign(new Error(`Fields for '${resourceType}' must be an array`), { code: 'INVALID_FIELDS' });
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
- socket.data.subscriptions = new Map();
252
+ socket.data.subscriptions = new Map()
253
253
  }
254
254
 
255
255
  if (socket.data.subscriptions.size >= config.subscriptions.maxPerSocket) {
256
- throw Object.assign(new Error('Subscription limit reached for this connection'), { code: 'SUBSCRIPTION_LIMIT' });
256
+ throw Object.assign(new Error('Subscription limit reached for this connection'), { code: 'SUBSCRIPTION_LIMIT' })
257
257
  }
258
258
 
259
259
  const subscription = {
@@ -264,18 +264,18 @@ async function registerSubscription({
264
264
  fields: fields || {},
265
265
  auth,
266
266
  createdAt: new Date()
267
- };
267
+ }
268
268
 
269
- await runHooks('subscriptionFilters', { subscription, auth });
269
+ await runHooks('subscriptionFilters', { subscription, auth })
270
270
 
271
- const roomName = `${resource}:updates`;
272
- socket.join(roomName);
273
- socket.data.subscriptions.set(subscription.id, subscription);
271
+ const roomName = `${resource}:updates`
272
+ socket.join(roomName)
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,
@@ -284,12 +284,12 @@ async function registerSubscription({
284
284
  include: subscription.include,
285
285
  fields: subscription.fields,
286
286
  status: 'active'
287
- };
287
+ }
288
288
  }
289
289
 
290
- async function handleRestoreSubscriptions({ socket, subscriptions, scopes, runHooks, log, config }) {
291
- const restored = [];
292
- const failed = [];
290
+ async function handleRestoreSubscriptions ({ socket, subscriptions, scopes, runHooks, log, config }) {
291
+ const restored = []
292
+ const failed = []
293
293
 
294
294
  for (const sub of subscriptions) {
295
295
  try {
@@ -300,8 +300,8 @@ async function handleRestoreSubscriptions({ socket, subscriptions, scopes, runHo
300
300
  runHooks,
301
301
  log,
302
302
  config
303
- });
304
- restored.push(response.subscriptionId);
303
+ })
304
+ restored.push(response.subscriptionId)
305
305
  } catch (error) {
306
306
  failed.push({
307
307
  subscriptionId: sub?.subscriptionId || null,
@@ -309,47 +309,47 @@ async function handleRestoreSubscriptions({ socket, subscriptions, scopes, runHo
309
309
  code: error.code || 'SUBSCRIBE_ERROR',
310
310
  message: error.message
311
311
  }
312
- });
312
+ })
313
313
  }
314
314
  }
315
315
 
316
- return { restored, failed };
316
+ return { restored, failed }
317
317
  }
318
318
 
319
- function getRecordForFiltering(context) {
320
- return context?.minimalRecord || context?.originalMinimalRecord || null;
319
+ function getRecordForFiltering (context) {
320
+ return context?.minimalRecord || context?.originalMinimalRecord || null
321
321
  }
322
322
 
323
- async function performBroadcast({ method, scopeName, id, context, api, io, log }) {
324
- if (!io) return;
323
+ async function performBroadcast ({ method, scopeName, id, context, api, io, log }) {
324
+ if (!io) return
325
325
 
326
- const scope = api.resources[scopeName];
326
+ const scope = api.resources[scopeName]
327
327
  if (!scope) {
328
- log.warn(`Socket.IO broadcast skipped: unknown resource ${scopeName}`);
329
- return;
328
+ log.warn(`Socket.IO broadcast skipped: unknown resource ${scopeName}`)
329
+ return
330
330
  }
331
331
 
332
- const roomName = `${scopeName}:updates`;
333
- const socketsInRoom = await io.in(roomName).fetchSockets();
332
+ const roomName = `${scopeName}:updates`
333
+ const socketsInRoom = await io.in(roomName).fetchSockets()
334
334
  if (!socketsInRoom || socketsInRoom.length === 0) {
335
- log.debug(`Socket.IO broadcast skipped: no subscribers for ${roomName}`);
336
- return;
335
+ log.debug(`Socket.IO broadcast skipped: no subscribers for ${roomName}`)
336
+ return
337
337
  }
338
338
 
339
- const schemaInfo = scope.vars?.schemaInfo;
340
- const searchSchemaStructure = schemaInfo?.searchSchemaStructure;
341
- const recordForFiltering = getRecordForFiltering(context);
339
+ const schemaInfo = scope.vars?.schemaInfo
340
+ const searchSchemaStructure = schemaInfo?.searchSchemaStructure
341
+ const recordForFiltering = getRecordForFiltering(context)
342
342
 
343
343
  for (const socket of socketsInRoom) {
344
- const subscriptions = socket.data.subscriptions;
345
- if (!subscriptions || subscriptions.size === 0) continue;
344
+ const subscriptions = socket.data.subscriptions
345
+ if (!subscriptions || subscriptions.size === 0) continue
346
346
 
347
347
  for (const subscription of subscriptions.values()) {
348
- if (subscription.resource !== scopeName) continue;
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
- continue;
352
+ continue
353
353
  }
354
354
  }
355
355
 
@@ -362,15 +362,15 @@ async function performBroadcast({ method, scopeName, id, context, api, io, log }
362
362
  meta: {
363
363
  timestamp: new Date().toISOString()
364
364
  }
365
- };
365
+ }
366
366
 
367
367
  if (method === 'delete') {
368
- notification.deletedRecord = { id };
368
+ notification.deletedRecord = { id }
369
369
  }
370
370
 
371
- socket.emit('subscription.update', notification);
372
- log.debug(`Socket.IO broadcast: ${scopeName}/${id} -> socket ${socket.id}`);
373
- break;
371
+ socket.emit('subscription.update', notification)
372
+ log.debug(`Socket.IO broadcast: ${scopeName}/${id} -> socket ${socket.id}`)
373
+ break
374
374
  }
375
375
  }
376
376
  }
@@ -379,91 +379,91 @@ export const SocketIOPlugin = {
379
379
  name: 'socketio',
380
380
  dependencies: ['rest-api'],
381
381
 
382
- async install({ api, addHook, log, scopes, helpers, vars, runHooks, pluginOptions = {} }) {
383
- const config = buildConfig(pluginOptions);
382
+ async install ({ api, addHook, log, scopes, helpers, vars, runHooks, pluginOptions = {} }) {
383
+ const config = buildConfig(pluginOptions)
384
384
 
385
- let Server;
385
+ let Server
386
386
  try {
387
- ({ Server } = await import('socket.io'));
387
+ ({ Server } = await import('socket.io'))
388
388
  } catch (error) {
389
- requirePackage('socket.io', 'socketio', 'Socket.IO is required for WebSocket support. This is a peer dependency.');
390
- throw error;
389
+ requirePackage('socket.io', 'socketio', 'Socket.IO is required for WebSocket support. This is a peer dependency.')
390
+ throw error
391
391
  }
392
392
 
393
- let createAdapter;
394
- let createClient;
395
- let io;
393
+ let createAdapter
394
+ let createClient
395
+ let io
396
396
 
397
397
  api.startSocketServer = async (server, startOptions = {}) => {
398
- const transportConfig = { ...config.transport, ...startOptions.transport };
399
- const authConfig = { ...config.auth, ...(startOptions.auth || {}) };
400
- const redisConfig = startOptions.redis ?? pluginOptions.redis ?? null;
398
+ const transportConfig = { ...config.transport, ...startOptions.transport }
399
+ const authConfig = { ...config.auth, ...(startOptions.auth || {}) }
400
+ const redisConfig = startOptions.redis ?? pluginOptions.redis ?? null
401
401
 
402
- config.auth = authConfig;
402
+ config.auth = authConfig
403
403
 
404
- const defaultPath = vars.transport?.mountPath ? `${vars.transport.mountPath}/socket.io` : '/socket.io';
405
- const path = startOptions.path || config.transport.path || defaultPath;
406
- const cors = startOptions.cors || config.transport.cors || { origin: '*', methods: ['GET', 'POST'] };
404
+ const defaultPath = vars.transport?.mountPath ? `${vars.transport.mountPath}/socket.io` : '/socket.io'
405
+ const path = startOptions.path || config.transport.path || defaultPath
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
- vars.socketIO = io;
415
- api.io = io;
414
+ vars.socketIO = io
415
+ api.io = io
416
416
 
417
417
  if (redisConfig) {
418
418
  try {
419
- ({ createClient } = await import('redis'));
419
+ ({ createClient } = await import('redis'))
420
420
  } catch (error) {
421
- requirePackage('redis', 'socketio', 'Redis is required for Socket.IO horizontal scaling. This is a peer dependency.');
422
- throw error;
421
+ requirePackage('redis', 'socketio', 'Redis is required for Socket.IO horizontal scaling. This is a peer dependency.')
422
+ throw error
423
423
  }
424
424
 
425
425
  try {
426
- ({ createAdapter } = await import('@socket.io/redis-adapter'));
426
+ ({ createAdapter } = await import('@socket.io/redis-adapter'))
427
427
  } catch (error) {
428
428
  requirePackage('@socket.io/redis-adapter', 'socketio',
429
- 'Socket.IO Redis adapter is required for horizontal scaling. This is a peer dependency.');
430
- throw error;
429
+ 'Socket.IO Redis adapter is required for horizontal scaling. This is a peer dependency.')
430
+ throw error
431
431
  }
432
432
 
433
- const pubClient = createClient(redisConfig);
434
- const subClient = pubClient.duplicate();
433
+ const pubClient = createClient(redisConfig)
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
- io.adapter(createAdapter(pubClient, subClient));
442
- vars.socketIORedisClients = { pubClient, subClient };
443
- log.info('Socket.IO configured with Redis adapter');
441
+ io.adapter(createAdapter(pubClient, subClient))
442
+ vars.socketIORedisClients = { pubClient, subClient }
443
+ log.info('Socket.IO configured with Redis adapter')
444
444
  }
445
445
 
446
446
  io.use(async (socket, next) => {
447
447
  try {
448
- const authContext = await authenticateSocket({ socket, api, helpers, log, config });
449
- socket.data.auth = authContext;
450
- socket.data.subscriptions = new Map();
451
- next();
448
+ const authContext = await authenticateSocket({ socket, api, helpers, log, config })
449
+ socket.data.auth = authContext
450
+ socket.data.subscriptions = new Map()
451
+ next()
452
452
  } catch (error) {
453
- log.warn('Socket.IO authentication failed', error);
454
- next(new Error(error.message || 'Authentication failed'));
453
+ log.warn('Socket.IO authentication failed', error)
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 {
@@ -474,66 +474,68 @@ export const SocketIOPlugin = {
474
474
  runHooks,
475
475
  log,
476
476
  config
477
- });
477
+ })
478
478
 
479
- if (callback) callback({ success: true, data: result });
480
- else socket.emit('subscription.created', result);
479
+ if (callback) callback({ success: true, data: result })
480
+ else socket.emit('subscription.created', result)
481
481
  } catch (error) {
482
- log.error('Socket.IO subscribe error', error);
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
- };
489
- if (callback) callback(response);
490
- else socket.emit('subscription.error', response.error);
488
+ }
489
+ if (callback) callback(response)
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
- const subscriptionId = payload?.subscriptionId;
496
+ const subscriptionId = payload?.subscriptionId
497
497
  if (!subscriptionId) {
498
- const error = { code: 'MISSING_SUBSCRIPTION_ID', message: 'Subscription ID is required' };
499
- if (callback) callback({ error });
500
- return;
498
+ const error = { code: 'MISSING_SUBSCRIPTION_ID', message: 'Subscription ID is required' }
499
+ if (callback) callback({ error })
500
+ return
501
501
  }
502
502
 
503
- const subscription = socket.data.subscriptions?.get(subscriptionId);
503
+ const subscription = socket.data.subscriptions?.get(subscriptionId)
504
504
  if (!subscription) {
505
- const error = { code: 'SUBSCRIPTION_NOT_FOUND', message: 'Subscription not found' };
506
- if (callback) callback({ error });
507
- return;
505
+ const error = { code: 'SUBSCRIPTION_NOT_FOUND', message: 'Subscription not found' }
506
+ if (callback) callback({ error })
507
+ return
508
508
  }
509
509
 
510
- socket.data.subscriptions.delete(subscriptionId);
510
+ socket.data.subscriptions.delete(subscriptionId)
511
511
  const hasOther = Array.from(socket.data.subscriptions.values())
512
- .some((sub) => sub.resource === subscription.resource);
512
+ .some((sub) => sub.resource === subscription.resource)
513
513
  if (!hasOther) {
514
- socket.leave(`${subscription.resource}:updates`);
514
+ socket.leave(`${subscription.resource}:updates`)
515
515
  }
516
516
 
517
- if (callback) callback({ success: true });
518
- log.info(`Socket ${socket.id} unsubscribed from ${subscription.resource}`, { subscriptionId });
517
+ if (callback) callback({ success: true })
518
+ log.info(`Socket ${socket.id} unsubscribed from ${subscription.resource}`, { subscriptionId })
519
519
  } catch (error) {
520
- log.error('Socket.IO unsubscribe error', error);
521
- if (callback) callback({
522
- error: {
523
- code: 'UNSUBSCRIBE_ERROR',
524
- message: error.message
525
- }
526
- });
520
+ log.error('Socket.IO unsubscribe error', error)
521
+ if (callback) {
522
+ callback({
523
+ error: {
524
+ code: 'UNSUBSCRIBE_ERROR',
525
+ message: error.message
526
+ }
527
+ })
528
+ }
527
529
  }
528
- });
530
+ })
529
531
 
530
532
  socket.on('restore-subscriptions', async (payload, callback) => {
531
533
  try {
532
- const subscriptions = payload?.subscriptions;
534
+ const subscriptions = payload?.subscriptions
533
535
  if (!Array.isArray(subscriptions)) {
534
- const error = { code: 'INVALID_DATA', message: 'Subscriptions must be an array' };
535
- if (callback) callback({ error });
536
- return;
536
+ const error = { code: 'INVALID_DATA', message: 'Subscriptions must be an array' }
537
+ if (callback) callback({ error })
538
+ return
537
539
  }
538
540
 
539
541
  const result = await handleRestoreSubscriptions({
@@ -543,75 +545,77 @@ export const SocketIOPlugin = {
543
545
  runHooks,
544
546
  log,
545
547
  config
546
- });
548
+ })
547
549
 
548
- if (callback) callback({ success: true, ...result });
550
+ if (callback) callback({ success: true, ...result })
549
551
  } catch (error) {
550
- log.error('Socket.IO restore subscriptions error', error);
551
- if (callback) callback({
552
- error: {
553
- code: 'RESTORE_ERROR',
554
- message: error.message
555
- }
556
- });
552
+ log.error('Socket.IO restore subscriptions error', error)
553
+ if (callback) {
554
+ callback({
555
+ error: {
556
+ code: 'RESTORE_ERROR',
557
+ message: error.message
558
+ }
559
+ })
560
+ }
557
561
  }
558
- });
562
+ })
559
563
 
560
564
  socket.on('disconnect', (reason) => {
561
565
  log.info(`Socket disconnected: ${socket.id}`, {
562
566
  reason,
563
567
  subscriptionCount: socket.data.subscriptions?.size || 0
564
- });
565
- });
566
- });
568
+ })
569
+ })
570
+ })
567
571
 
568
- log.info('Socket.IO server started', { path });
569
- return io;
570
- };
572
+ log.info('Socket.IO server started', { path })
573
+ return io
574
+ }
571
575
 
572
576
  addHook('finish', 'socketio-broadcast', {}, async ({ context }) => {
573
- const { method, scopeName, id } = context;
577
+ const { method, scopeName, id } = context
574
578
 
575
579
  if (!io) {
576
- log.debug('Socket.IO broadcast skipped: server not started');
577
- return;
580
+ log.debug('Socket.IO broadcast skipped: server not started')
581
+ return
578
582
  }
579
583
 
580
584
  if (!['post', 'put', 'patch', 'delete'].includes(method)) {
581
- return;
585
+ return
582
586
  }
583
587
 
584
588
  if (!id && method !== 'delete') {
585
- log.debug('Socket.IO broadcast skipped: missing record id');
586
- return;
589
+ log.debug('Socket.IO broadcast skipped: missing record id')
590
+ return
587
591
  }
588
592
 
589
593
  if (context.transaction) {
590
594
  if (!pendingBroadcasts.has(context.transaction)) {
591
- pendingBroadcasts.set(context.transaction, []);
595
+ pendingBroadcasts.set(context.transaction, [])
592
596
  }
593
- pendingBroadcasts.get(context.transaction).push({ method, scopeName, id, context });
594
- return;
597
+ pendingBroadcasts.get(context.transaction).push({ method, scopeName, id, context })
598
+ return
595
599
  }
596
600
 
597
- await performBroadcast({ method, scopeName, id, context, api, io, log });
598
- });
601
+ await performBroadcast({ method, scopeName, id, context, api, io, log })
602
+ })
599
603
 
600
604
  addHook('afterCommit', 'socketio-broadcast-deferred', {}, async ({ context }) => {
601
- if (!context?.transaction) return;
602
- const broadcasts = pendingBroadcasts.get(context.transaction);
603
- if (!broadcasts) return;
605
+ if (!context?.transaction) return
606
+ const broadcasts = pendingBroadcasts.get(context.transaction)
607
+ if (!broadcasts) return
604
608
 
605
609
  for (const broadcast of broadcasts) {
606
- await performBroadcast({ ...broadcast, api, io, log });
610
+ await performBroadcast({ ...broadcast, api, io, log })
607
611
  }
608
612
 
609
- pendingBroadcasts.delete(context.transaction);
610
- });
613
+ pendingBroadcasts.delete(context.transaction)
614
+ })
611
615
 
612
616
  addHook('afterRollback', 'socketio-cleanup-broadcasts', {}, async ({ context }) => {
613
- if (!context?.transaction) return;
614
- pendingBroadcasts.delete(context.transaction);
615
- });
617
+ if (!context?.transaction) return
618
+ pendingBroadcasts.delete(context.transaction)
619
+ })
616
620
  }
617
- };
621
+ }