json-rest-api 1.0.14 → 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.
- 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 +97 -98
- 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 +8 -8
- 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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
327
|
+
schemaRelationships,
|
|
329
328
|
tableName: scope.scopeOptions.tableName || scopeName,
|
|
330
329
|
idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
|
|
331
|
-
fieldGetters
|
|
332
|
-
sortedGetterFields
|
|
333
|
-
fieldSetters
|
|
334
|
-
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)
|
|
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
|
+
}
|