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,28 +1,28 @@
1
- import { createSchema } from 'json-rest-schema';
2
- import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js';
1
+ import { createSchema } from 'json-rest-schema'
2
+ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js'
3
3
 
4
4
  /**
5
5
  * Compiles and enriches schemas for a resource scope
6
- *
6
+ *
7
7
  * @param {Object} scope - Scope containing raw schema options
8
8
  * @param {Object} deps - Dependencies with context and hooks
9
9
  * @returns {Promise<void>} Resolves when compilation complete
10
- *
10
+ *
11
11
  * @example
12
12
  * // Input: Raw schema with computed field
13
13
  * const rawFields = {
14
14
  * title: { type: 'string', required: true },
15
15
  * content: { type: 'string' },
16
16
  * author_id: { belongsTo: 'users', as: 'author' }, // Missing type
17
- * word_count: {
18
- * type: 'number',
17
+ * word_count: {
18
+ * type: 'number',
19
19
  * computed: true,
20
20
  * compute: (record) => record.content.split(' ').length
21
21
  * }
22
22
  * };
23
- *
23
+ *
24
24
  * await compileSchemas(scope, deps);
25
- *
25
+ *
26
26
  * // Output in scope.vars.schemaInfo:
27
27
  * // {
28
28
  * // schema: Schema {}, // json-rest-schema instance
@@ -36,7 +36,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
36
36
  * // word_count: { type: 'number', computed: true, compute: [Function] }
37
37
  * // }
38
38
  * // }
39
- *
39
+ *
40
40
  * @example
41
41
  * // Input: Schema with search fields
42
42
  * const rawFields = {
@@ -44,17 +44,17 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
44
44
  * status: { type: 'string' }, // Not searchable
45
45
  * author_id: { belongsTo: 'users', as: 'author' }
46
46
  * };
47
- *
47
+ *
48
48
  * // Output: Auto-generated searchSchema
49
49
  * // searchSchemaObject contains:
50
50
  * // {
51
- * // title: {
52
- * // type: 'string',
51
+ * // title: {
52
+ * // type: 'string',
53
53
  * // indexed: true, // Added for DB optimization
54
54
  * // filterOperator: '=' // Default operator
55
55
  * // }
56
56
  * // }
57
- *
57
+ *
58
58
  * @example
59
59
  * // Input: Pivot table auto-detection
60
60
  * const pivotSchema = {
@@ -62,7 +62,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
62
62
  * tag_id: { belongsTo: 'tags', as: 'tag' },
63
63
  * display_order: { type: 'number' }
64
64
  * };
65
- *
65
+ *
66
66
  * // Output: Both foreign keys become searchable
67
67
  * // schemaStructure will have:
68
68
  * // {
@@ -71,8 +71,8 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
71
71
  * // display_order: { type: 'number' }
72
72
  * // }
73
73
  * // This enables efficient many-to-many filtering!
74
- *
75
- * @example
74
+ *
75
+ * @example
76
76
  * // Input: Schema with dependent getters
77
77
  * const schema = {
78
78
  * first_name: { type: 'string' },
@@ -87,7 +87,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
87
87
  * setter: (value) => value.toLowerCase().trim()
88
88
  * }
89
89
  * };
90
- *
90
+ *
91
91
  * // Output includes dependency-sorted fields:
92
92
  * // {
93
93
  * // fieldGetters: { full_name: { getter: [Function], runGetterAfter: [...] } },
@@ -95,12 +95,12 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
95
95
  * // fieldSetters: { email: { setter: [Function], runSetterAfter: [] } },
96
96
  * // sortedSetterFields: ['email']
97
97
  * // }
98
- *
98
+ *
99
99
  * @description
100
100
  * Used by:
101
101
  * - Every REST method calls this before any operations
102
102
  * - Compilation happens once per scope (cached)
103
- *
103
+ *
104
104
  * Purpose:
105
105
  * - Separates computed fields from database fields
106
106
  * - Adds missing types to belongsTo relationships
@@ -109,7 +109,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
109
109
  * - Validates getter/setter dependencies
110
110
  * - Provides hooks for schema enrichment
111
111
  * - Caches everything for performance
112
- *
112
+ *
113
113
  * Data flow:
114
114
  * 1. Extracts computed fields to separate object
115
115
  * 2. Adds type:'id' to belongsTo fields
@@ -121,77 +121,76 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
121
121
  * 8. Extracts and topologically sorts getters/setters
122
122
  * 9. Caches all results in scope.vars.schemaInfo
123
123
  */
124
- export async function compileSchemas(scope, deps) {
124
+ export async function compileSchemas (scope, deps) {
125
125
  // Extract scopeName from context
126
- const { context, runHooks } = deps;
127
- const scopeName = context.scopeName;
126
+ const { context, runHooks } = deps
127
+ const scopeName = context.scopeName
128
128
 
129
129
  // Get raw schema
130
- const rawFields = scope.scopeOptions?.schema || {};
131
-
130
+ const rawFields = scope.scopeOptions?.schema || {}
131
+
132
132
  // Extract computed fields from schema and build enriched schema
133
- const computedFields = {};
134
- const enrichedFields = {};
135
-
133
+ const computedFields = {}
134
+ const enrichedFields = {}
135
+
136
136
  for (const [fieldName, fieldDef] of Object.entries(rawFields)) {
137
137
  if (fieldDef.computed === true) {
138
138
  // Extract computed field - copy entire definition
139
- computedFields[fieldName] = { ...fieldDef };
140
-
139
+ computedFields[fieldName] = { ...fieldDef }
140
+
141
141
  // Validate computed field
142
142
  if (!fieldDef.type) {
143
- throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`);
143
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
144
144
  }
145
-
145
+
146
146
  if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
147
- throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`);
147
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
148
148
  }
149
-
149
+
150
150
  // Don't include computed fields in the validation schema
151
151
  } else {
152
- enrichedFields[fieldName] = { ...fieldDef };
152
+ enrichedFields[fieldName] = { ...fieldDef }
153
153
  }
154
154
  }
155
-
155
+
156
156
  // Default enrichment: Add type to belongsTo fields
157
157
  for (const [fieldName, fieldDef] of Object.entries(enrichedFields)) {
158
158
  if (fieldDef.belongsTo && !fieldDef.type) {
159
- fieldDef.type = 'id';
159
+ fieldDef.type = 'id'
160
160
  }
161
161
  }
162
-
162
+
163
163
  // ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
164
164
  // Auto-detect pivot tables and make their foreign key fields searchable
165
165
  // This is critical for many-to-many relationship operations which need to filter pivot records
166
- const fields = Object.entries(enrichedFields);
167
- const belongsToFields = fields.filter(([_, def]) => def.belongsTo);
168
-
166
+ const fields = Object.entries(enrichedFields)
167
+ const belongsToFields = fields.filter(([_, def]) => def.belongsTo)
168
+
169
169
  // If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
170
- const nonIdFields = fields.filter(([name, _]) => name !== 'id');
171
- const isProbablyPivot = belongsToFields.length >= 2 &&
172
- (nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4);
173
-
170
+ const nonIdFields = fields.filter(([name, _]) => name !== 'id')
171
+ const isProbablyPivot = belongsToFields.length >= 2 &&
172
+ (nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4)
173
+
174
174
  if (isProbablyPivot) {
175
175
  // Make all belongsTo fields searchable for pivot operations
176
176
  for (const [fieldName, fieldDef] of belongsToFields) {
177
177
  if (!fieldDef.search) {
178
- fieldDef.search = true;
178
+ fieldDef.search = true
179
179
  }
180
180
  }
181
181
  }
182
182
 
183
-
184
183
  // Hook: schema:enrich
185
184
  const schemaContext = {
186
185
  fields: enrichedFields, // Mutable
187
186
  originalFields: rawFields, // Read-only
188
187
  scopeName
189
- };
190
- await runHooks('schema:enrich', schemaContext);
191
-
188
+ }
189
+ await runHooks('schema:enrich', schemaContext)
190
+
192
191
  // Create schema object
193
- const schemaObject = createSchema(schemaContext.fields);
194
-
192
+ const schemaObject = createSchema(schemaContext.fields)
193
+
195
194
  // Generate searchSchema by merging explicit searchSchema with fields marked search:true.
196
195
  // This allows two ways to define searchable fields: either mark fields with search:true
197
196
  // in the main schema, or provide an explicit searchSchema with more control over filtering.
@@ -200,11 +199,11 @@ export async function compileSchemas(scope, deps) {
200
199
  // searchSchema will be added automatically.
201
200
  // Example: title: {search: true} auto-generates a searchable field with sensible defaults,
202
201
  // while searchSchema can specify filterOperator: 'contains' or complex join configurations.
203
- let rawSearchFields = generateSearchSchemaFromSchema(
202
+ const rawSearchFields = generateSearchSchemaFromSchema(
204
203
  schemaContext.fields,
205
204
  scope.scopeOptions.searchSchema
206
- );
207
-
205
+ )
206
+
208
207
  if (rawSearchFields) {
209
208
  // Mark all search fields as indexed for database optimization.
210
209
  // This ensures that any field used for filtering will have a database index created,
@@ -212,106 +211,106 @@ export async function compileSchemas(scope, deps) {
212
211
  // would require full table scans. The storage plugin uses these hints to create indexes.
213
212
  // Example: A status field marked for search gets indexed: true, enabling efficient
214
213
  // WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
215
- ensureSearchFieldsAreIndexed(rawSearchFields);
216
-
214
+ ensureSearchFieldsAreIndexed(rawSearchFields)
215
+
217
216
  // Hook: searchSchema:enrich
218
217
  const searchSchemaContext = {
219
218
  fields: schemaContext.fields, // Mutable
220
219
  originalFields: rawSearchFields, // Read-only enriched schema
221
220
  scopeName
222
- };
223
- await runHooks('searchSchema:enrich', searchSchemaContext);
224
-
221
+ }
222
+ await runHooks('searchSchema:enrich', searchSchemaContext)
223
+
225
224
  // Create searchSchema object
226
- var searchSchemaObject = createSchema(rawSearchFields);
225
+ var searchSchemaObject = createSchema(rawSearchFields)
227
226
  } else {
228
- var searchSchemaObject = createSchema({});
227
+ var searchSchemaObject = createSchema({})
229
228
  }
230
229
 
231
230
  // Build schemaRelationships including polymorphic fields from schema
232
- const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) };
233
-
231
+ const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) }
232
+
234
233
  // Validate belongsTo fields
235
234
  for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
236
235
  // Validate that belongsTo fields have 'as' property
237
236
  if (fieldDef.belongsTo && !fieldDef.as) {
238
237
  throw new Error(
239
238
  `Field '${fieldName}' in resource '${scopeName}' has belongsTo: '${fieldDef.belongsTo}' but is missing the required 'as' property. ` +
240
- `The 'as' property defines the relationship name used in JSON:API payloads. ` +
239
+ 'The \'as\' property defines the relationship name used in JSON:API payloads. ' +
241
240
  `Example: ${fieldName}: { type: 'number', belongsTo: '${fieldDef.belongsTo}', as: '${fieldName.replace(/_id$/, '')}' }`
242
- );
241
+ )
243
242
  }
244
243
  }
245
244
 
246
245
  // Extract and validate getter definitions
247
- const fieldGetters = {};
248
- const getterFields = [];
249
-
246
+ const fieldGetters = {}
247
+ const getterFields = []
248
+
250
249
  for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
251
250
  if (fieldDef.getter && typeof fieldDef.getter === 'function') {
252
251
  fieldGetters[fieldName] = {
253
252
  getter: fieldDef.getter,
254
253
  runGetterAfter: fieldDef.runGetterAfter || [],
255
- fieldDef: fieldDef
256
- };
257
- getterFields.push(fieldName);
258
-
254
+ fieldDef
255
+ }
256
+ getterFields.push(fieldName)
257
+
259
258
  // Validate dependencies exist
260
259
  if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
261
260
  for (const dep of fieldDef.runGetterAfter) {
262
261
  if (!schemaContext.fields[dep]) {
263
262
  throw new Error(
264
263
  `Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
265
- );
264
+ )
266
265
  }
267
266
  }
268
267
  }
269
268
  }
270
269
  }
271
-
270
+
272
271
  // Sort getters by dependencies
273
- let sortedGetterFields = [];
272
+ let sortedGetterFields = []
274
273
  if (Object.keys(fieldGetters).length > 0) {
275
274
  try {
276
- sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter');
275
+ sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
277
276
  } catch (error) {
278
- throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`);
277
+ throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
279
278
  }
280
279
  }
281
280
 
282
281
  // Extract and validate setter definitions
283
- const fieldSetters = {};
284
- const setterFields = [];
285
-
282
+ const fieldSetters = {}
283
+ const setterFields = []
284
+
286
285
  for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
287
286
  if (fieldDef.setter && typeof fieldDef.setter === 'function') {
288
287
  fieldSetters[fieldName] = {
289
288
  setter: fieldDef.setter,
290
289
  runSetterAfter: fieldDef.runSetterAfter || [],
291
- fieldDef: fieldDef
292
- };
293
- setterFields.push(fieldName);
294
-
290
+ fieldDef
291
+ }
292
+ setterFields.push(fieldName)
293
+
295
294
  // Validate dependencies exist
296
295
  if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
297
296
  for (const dep of fieldDef.runSetterAfter) {
298
297
  if (!schemaContext.fields[dep]) {
299
298
  throw new Error(
300
299
  `Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
301
- );
300
+ )
302
301
  }
303
302
  }
304
303
  }
305
304
  }
306
305
  }
307
-
306
+
308
307
  // Sort setters by dependencies
309
- let sortedSetterFields = [];
308
+ let sortedSetterFields = []
310
309
  if (Object.keys(fieldSetters).length > 0) {
311
310
  try {
312
- sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter');
311
+ sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
313
312
  } catch (error) {
314
- throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`);
313
+ throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
315
314
  }
316
315
  }
317
316
 
@@ -323,14 +322,14 @@ export async function compileSchemas(scope, deps) {
323
322
 
324
323
  searchSchemaInstance: searchSchemaObject,
325
324
  searchSchemaStructure: searchSchemaObject.structure,
326
-
325
+
327
326
  computed: computedFields,
328
- schemaRelationships: schemaRelationships,
327
+ schemaRelationships,
329
328
  tableName: scope.scopeOptions.tableName || scopeName,
330
329
  idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
331
- fieldGetters: fieldGetters,
332
- sortedGetterFields: sortedGetterFields,
333
- fieldSetters: fieldSetters,
334
- sortedSetterFields: sortedSetterFields
335
- };
336
- }
330
+ fieldGetters,
331
+ sortedGetterFields,
332
+ fieldSetters,
333
+ sortedSetterFields
334
+ }
335
+ }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Parses URL query strings into JSON:API compliant parameter objects
3
- *
3
+ *
4
4
  * @param {string} queryString - The query string part of URL (without ?)
5
5
  * @returns {object} Parsed query parameters in JSON:API format
6
- *
6
+ *
7
7
  * @example
8
8
  * // Input: Basic include and sort
9
9
  * const query = parseJsonApiQuery('include=author&sort=-created_at');
10
- *
10
+ *
11
11
  * // Output: Parsed into arrays
12
12
  * // {
13
13
  * // include: ['author'], // Split by comma
@@ -16,11 +16,11 @@
16
16
  * // sort: ['-created_at'], // Split by comma
17
17
  * // page: {}
18
18
  * // }
19
- *
19
+ *
20
20
  * @example
21
21
  * // Input: Filters with bracket notation
22
22
  * const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
23
- *
23
+ *
24
24
  * // Output: Extracted filter keys
25
25
  * // {
26
26
  * // include: [],
@@ -32,11 +32,11 @@
32
32
  * // sort: [],
33
33
  * // page: {}
34
34
  * // }
35
- *
35
+ *
36
36
  * @example
37
- * // Input: Sparse fieldsets
37
+ * // Input: Sparse fieldsets
38
38
  * const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
39
- *
39
+ *
40
40
  * // Output: Fields kept as comma-separated strings
41
41
  * // {
42
42
  * // include: [],
@@ -48,11 +48,11 @@
48
48
  * // sort: [],
49
49
  * // page: {}
50
50
  * // }
51
- *
51
+ *
52
52
  * @example
53
53
  * // Input: Pagination with numeric conversion
54
54
  * const query = parseJsonApiQuery('page[size]=20&page[number]=3&page[cursor]=abc123');
55
- *
55
+ *
56
56
  * // Output: Numbers converted, strings preserved
57
57
  * // {
58
58
  * // include: [],
@@ -65,43 +65,43 @@
65
65
  * // cursor: 'abc123' // Non-numeric stays string
66
66
  * // }
67
67
  * // }
68
- *
68
+ *
69
69
  * @example
70
70
  * // Input: Complex real-world query
71
71
  * const query = parseJsonApiQuery(
72
72
  * 'include=author,comments.user&' +
73
73
  * 'filter[status]=published&' +
74
- * 'filter[created_after]=2024-01-01&' +
74
+ * 'filter[created_after]=2024-01-01&' +
75
75
  * 'fields[articles]=title,summary&' +
76
76
  * 'sort=-created_at,title&' +
77
77
  * 'page[size]=10'
78
78
  * );
79
- *
79
+ *
80
80
  * // Output: All parameters properly categorized
81
81
  * // {
82
82
  * // include: ['author', 'comments.user'],
83
83
  * // fields: { articles: 'title,summary' },
84
- * // filters: {
84
+ * // filters: {
85
85
  * // status: 'published',
86
86
  * // created_after: '2024-01-01'
87
87
  * // },
88
88
  * // sort: ['-created_at', 'title'],
89
89
  * // page: { size: 10 }
90
90
  * // }
91
- *
91
+ *
92
92
  * @description
93
93
  * Used by:
94
94
  * - express-plugin parses req.query with this
95
95
  * - http-plugin parses URL query strings with this
96
96
  * - websocket-plugin parses message query parameters with this
97
- *
97
+ *
98
98
  * Purpose:
99
99
  * - Provides uniform JSON:API query parsing across all transports
100
100
  * - Handles bracket notation without regex complexity
101
101
  * - Converts data types appropriately (numbers for pagination)
102
102
  * - Ignores non-JSON:API parameters gracefully
103
103
  * - Returns consistent structure even for empty input
104
- *
104
+ *
105
105
  * Data flow:
106
106
  * 1. Uses URLSearchParams for reliable parsing
107
107
  * 2. Categorizes parameters by their prefix pattern
@@ -110,7 +110,7 @@
110
110
  * 5. Converts numeric strings for pagination
111
111
  * 6. Returns normalized structure for REST API
112
112
  */
113
- export function parseJsonApiQuery(queryString) {
113
+ export function parseJsonApiQuery (queryString) {
114
114
  if (!queryString) {
115
115
  return {
116
116
  include: [],
@@ -118,48 +118,48 @@ export function parseJsonApiQuery(queryString) {
118
118
  filters: {},
119
119
  sort: [],
120
120
  page: {}
121
- };
121
+ }
122
122
  }
123
123
 
124
- const params = new URLSearchParams(queryString);
124
+ const params = new URLSearchParams(queryString)
125
125
  const result = {
126
126
  include: [],
127
127
  fields: {},
128
128
  filters: {},
129
129
  sort: [],
130
130
  page: {}
131
- };
131
+ }
132
132
 
133
133
  for (const [key, value] of params) {
134
134
  if (key === 'include') {
135
135
  // Parse include (comma-separated string to array)
136
- result.include = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
136
+ result.include = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
137
137
  } else if (key === 'sort') {
138
138
  // Parse sort (comma-separated string to array)
139
- result.sort = value.split(',').map(s => s.trim()).filter(s => s.length > 0);
139
+ result.sort = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
140
140
  } else if (key.startsWith('filter[') && key.endsWith(']')) {
141
141
  // Parse filter[key] = value into filters: { key: value }
142
- const filterKey = key.slice(7, -1); // Remove 'filter[' and ']'
142
+ const filterKey = key.slice(7, -1) // Remove 'filter[' and ']'
143
143
  if (filterKey) {
144
- result.filters[filterKey] = value;
144
+ result.filters[filterKey] = value
145
145
  }
146
146
  } else if (key.startsWith('fields[') && key.endsWith(']')) {
147
147
  // Parse fields[type] = fields into fields: { type: "field1,field2" }
148
148
  // Keep as comma-separated string to match REST API validation expectations
149
- const fieldType = key.slice(7, -1); // Remove 'fields[' and ']'
149
+ const fieldType = key.slice(7, -1) // Remove 'fields[' and ']'
150
150
  if (fieldType) {
151
- result.fields[fieldType] = value;
151
+ result.fields[fieldType] = value
152
152
  }
153
153
  } else if (key.startsWith('page[') && key.endsWith(']')) {
154
154
  // Parse page[size] = 10 into page: { size: 10 }
155
- const pageKey = key.slice(5, -1); // Remove 'page[' and ']'
155
+ const pageKey = key.slice(5, -1) // Remove 'page[' and ']'
156
156
  if (pageKey) {
157
157
  // Convert to number if it's a valid number, otherwise keep as string
158
- result.page[pageKey] = isNaN(value) ? value : parseInt(value, 10);
158
+ result.page[pageKey] = isNaN(value) ? value : parseInt(value, 10)
159
159
  }
160
160
  }
161
161
  // Ignore other query parameters that don't match JSON:API patterns
162
162
  }
163
163
 
164
- return result;
165
- }
164
+ return result
165
+ }