json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- 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,77 @@ 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
|
-
scopeName
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
scopeName,
|
|
188
|
+
scopeOptions: scope.scopeOptions || {}
|
|
189
|
+
}
|
|
190
|
+
await runHooks('schema:enrich', schemaContext)
|
|
191
|
+
|
|
192
192
|
// Create schema object
|
|
193
|
-
const schemaObject = createSchema(schemaContext.fields)
|
|
194
|
-
|
|
193
|
+
const schemaObject = createSchema(schemaContext.fields)
|
|
194
|
+
|
|
195
195
|
// Generate searchSchema by merging explicit searchSchema with fields marked search:true.
|
|
196
196
|
// This allows two ways to define searchable fields: either mark fields with search:true
|
|
197
197
|
// in the main schema, or provide an explicit searchSchema with more control over filtering.
|
|
@@ -200,11 +200,11 @@ export async function compileSchemas(scope, deps) {
|
|
|
200
200
|
// searchSchema will be added automatically.
|
|
201
201
|
// Example: title: {search: true} auto-generates a searchable field with sensible defaults,
|
|
202
202
|
// while searchSchema can specify filterOperator: 'contains' or complex join configurations.
|
|
203
|
-
|
|
203
|
+
const rawSearchFields = generateSearchSchemaFromSchema(
|
|
204
204
|
schemaContext.fields,
|
|
205
205
|
scope.scopeOptions.searchSchema
|
|
206
|
-
)
|
|
207
|
-
|
|
206
|
+
)
|
|
207
|
+
|
|
208
208
|
if (rawSearchFields) {
|
|
209
209
|
// Mark all search fields as indexed for database optimization.
|
|
210
210
|
// This ensures that any field used for filtering will have a database index created,
|
|
@@ -212,106 +212,106 @@ export async function compileSchemas(scope, deps) {
|
|
|
212
212
|
// would require full table scans. The storage plugin uses these hints to create indexes.
|
|
213
213
|
// Example: A status field marked for search gets indexed: true, enabling efficient
|
|
214
214
|
// WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
|
|
215
|
-
ensureSearchFieldsAreIndexed(rawSearchFields)
|
|
216
|
-
|
|
215
|
+
ensureSearchFieldsAreIndexed(rawSearchFields)
|
|
216
|
+
|
|
217
217
|
// Hook: searchSchema:enrich
|
|
218
218
|
const searchSchemaContext = {
|
|
219
219
|
fields: schemaContext.fields, // Mutable
|
|
220
220
|
originalFields: rawSearchFields, // Read-only enriched schema
|
|
221
221
|
scopeName
|
|
222
|
-
}
|
|
223
|
-
await runHooks('searchSchema:enrich', searchSchemaContext)
|
|
224
|
-
|
|
222
|
+
}
|
|
223
|
+
await runHooks('searchSchema:enrich', searchSchemaContext)
|
|
224
|
+
|
|
225
225
|
// Create searchSchema object
|
|
226
|
-
var searchSchemaObject = createSchema(rawSearchFields)
|
|
226
|
+
var searchSchemaObject = createSchema(rawSearchFields)
|
|
227
227
|
} else {
|
|
228
|
-
var searchSchemaObject = createSchema({})
|
|
228
|
+
var searchSchemaObject = createSchema({})
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// Build schemaRelationships including polymorphic fields from schema
|
|
232
|
-
const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) }
|
|
233
|
-
|
|
232
|
+
const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) }
|
|
233
|
+
|
|
234
234
|
// Validate belongsTo fields
|
|
235
235
|
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
236
236
|
// Validate that belongsTo fields have 'as' property
|
|
237
237
|
if (fieldDef.belongsTo && !fieldDef.as) {
|
|
238
238
|
throw new Error(
|
|
239
239
|
`Field '${fieldName}' in resource '${scopeName}' has belongsTo: '${fieldDef.belongsTo}' but is missing the required 'as' property. ` +
|
|
240
|
-
|
|
240
|
+
'The \'as\' property defines the relationship name used in JSON:API payloads. ' +
|
|
241
241
|
`Example: ${fieldName}: { type: 'number', belongsTo: '${fieldDef.belongsTo}', as: '${fieldName.replace(/_id$/, '')}' }`
|
|
242
|
-
)
|
|
242
|
+
)
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
// Extract and validate getter definitions
|
|
247
|
-
const fieldGetters = {}
|
|
248
|
-
const getterFields = []
|
|
249
|
-
|
|
247
|
+
const fieldGetters = {}
|
|
248
|
+
const getterFields = []
|
|
249
|
+
|
|
250
250
|
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
251
251
|
if (fieldDef.getter && typeof fieldDef.getter === 'function') {
|
|
252
252
|
fieldGetters[fieldName] = {
|
|
253
253
|
getter: fieldDef.getter,
|
|
254
254
|
runGetterAfter: fieldDef.runGetterAfter || [],
|
|
255
|
-
fieldDef
|
|
256
|
-
}
|
|
257
|
-
getterFields.push(fieldName)
|
|
258
|
-
|
|
255
|
+
fieldDef
|
|
256
|
+
}
|
|
257
|
+
getterFields.push(fieldName)
|
|
258
|
+
|
|
259
259
|
// Validate dependencies exist
|
|
260
260
|
if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
|
|
261
261
|
for (const dep of fieldDef.runGetterAfter) {
|
|
262
262
|
if (!schemaContext.fields[dep]) {
|
|
263
263
|
throw new Error(
|
|
264
264
|
`Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
|
|
265
|
-
)
|
|
265
|
+
)
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
// Sort getters by dependencies
|
|
273
|
-
let sortedGetterFields = []
|
|
273
|
+
let sortedGetterFields = []
|
|
274
274
|
if (Object.keys(fieldGetters).length > 0) {
|
|
275
275
|
try {
|
|
276
|
-
sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
|
|
276
|
+
sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
|
|
277
277
|
} catch (error) {
|
|
278
|
-
throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
|
|
278
|
+
throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// Extract and validate setter definitions
|
|
283
|
-
const fieldSetters = {}
|
|
284
|
-
const setterFields = []
|
|
285
|
-
|
|
283
|
+
const fieldSetters = {}
|
|
284
|
+
const setterFields = []
|
|
285
|
+
|
|
286
286
|
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
287
287
|
if (fieldDef.setter && typeof fieldDef.setter === 'function') {
|
|
288
288
|
fieldSetters[fieldName] = {
|
|
289
289
|
setter: fieldDef.setter,
|
|
290
290
|
runSetterAfter: fieldDef.runSetterAfter || [],
|
|
291
|
-
fieldDef
|
|
292
|
-
}
|
|
293
|
-
setterFields.push(fieldName)
|
|
294
|
-
|
|
291
|
+
fieldDef
|
|
292
|
+
}
|
|
293
|
+
setterFields.push(fieldName)
|
|
294
|
+
|
|
295
295
|
// Validate dependencies exist
|
|
296
296
|
if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
|
|
297
297
|
for (const dep of fieldDef.runSetterAfter) {
|
|
298
298
|
if (!schemaContext.fields[dep]) {
|
|
299
299
|
throw new Error(
|
|
300
300
|
`Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
|
|
301
|
-
)
|
|
301
|
+
)
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
// Sort setters by dependencies
|
|
309
|
-
let sortedSetterFields = []
|
|
309
|
+
let sortedSetterFields = []
|
|
310
310
|
if (Object.keys(fieldSetters).length > 0) {
|
|
311
311
|
try {
|
|
312
|
-
sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
|
|
312
|
+
sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
|
|
313
313
|
} catch (error) {
|
|
314
|
-
throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
|
|
314
|
+
throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
@@ -323,14 +323,14 @@ export async function compileSchemas(scope, deps) {
|
|
|
323
323
|
|
|
324
324
|
searchSchemaInstance: searchSchemaObject,
|
|
325
325
|
searchSchemaStructure: searchSchemaObject.structure,
|
|
326
|
-
|
|
326
|
+
|
|
327
327
|
computed: computedFields,
|
|
328
|
-
schemaRelationships
|
|
328
|
+
schemaRelationships,
|
|
329
329
|
tableName: scope.scopeOptions.tableName || scopeName,
|
|
330
330
|
idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
|
|
331
|
-
fieldGetters
|
|
332
|
-
sortedGetterFields
|
|
333
|
-
fieldSetters
|
|
334
|
-
sortedSetterFields
|
|
335
|
-
}
|
|
336
|
-
}
|
|
331
|
+
fieldGetters,
|
|
332
|
+
sortedGetterFields,
|
|
333
|
+
fieldSetters,
|
|
334
|
+
sortedSetterFields
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -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)
|
|
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)
|
|
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)
|
|
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
|
+
}
|