json-rest-api 1.0.10 → 1.0.11
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/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Centralized functions for URL generation to avoid code duplication
|
|
5
|
+
* and support flexible URL prefix override capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the complete URL prefix, handling all calculation and override logic
|
|
10
|
+
*
|
|
11
|
+
* Priority order:
|
|
12
|
+
* 1. context.urlPrefixOverride - Explicit override (from hooks or middleware)
|
|
13
|
+
* 2. context.urlPrefix - Pre-calculated URL prefix
|
|
14
|
+
* 3. Calculate from request if provided
|
|
15
|
+
* 4. scope.vars.transport?.mountPath - Fallback to mount path
|
|
16
|
+
* 5. '' - Empty string as final fallback
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} context - Request context (may have urlPrefixOverride or urlPrefix)
|
|
19
|
+
* @param {Object} scope - Scope/resource object with transport vars
|
|
20
|
+
* @param {Object} req - Optional Express request object for auto-calculation
|
|
21
|
+
* @returns {string} The final URL prefix to use
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // With full context
|
|
25
|
+
* const urlPrefix = getUrlPrefix(context, scope);
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // With request object for auto-calculation
|
|
29
|
+
* const urlPrefix = getUrlPrefix({}, api, req);
|
|
30
|
+
*/
|
|
31
|
+
export function getUrlPrefix(context, scope, req = null) {
|
|
32
|
+
// Priority 1: Explicit override
|
|
33
|
+
if (context?.urlPrefixOverride) {
|
|
34
|
+
return context.urlPrefixOverride;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Priority 2: Pre-calculated urlPrefix
|
|
38
|
+
if (context?.urlPrefix) {
|
|
39
|
+
return context.urlPrefix;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Priority 3: Calculate from request if provided
|
|
43
|
+
if (req) {
|
|
44
|
+
const protocol = req.get?.('x-forwarded-proto') || req.protocol || 'http';
|
|
45
|
+
const host = req.get?.('x-forwarded-host') || req.get?.('host');
|
|
46
|
+
const mountPath = scope?.vars?.transport?.mountPath || '';
|
|
47
|
+
|
|
48
|
+
if (host) {
|
|
49
|
+
return `${protocol}://${host}${mountPath}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Priority 4: Fallback to mount path
|
|
54
|
+
return scope?.vars?.transport?.mountPath || '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build a complete resource URL
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} context - Request context
|
|
61
|
+
* @param {Object} scope - Scope containing transport vars
|
|
62
|
+
* @param {string} scopeName - Resource type name
|
|
63
|
+
* @param {string} id - Resource ID
|
|
64
|
+
* @returns {string} Complete resource URL
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const url = buildResourceUrl(context, scope, 'books', '123');
|
|
68
|
+
* // Returns: "https://api.example.com/api/books/123"
|
|
69
|
+
*/
|
|
70
|
+
export function buildResourceUrl(context, scope, scopeName, id) {
|
|
71
|
+
const urlPrefix = getUrlPrefix(context, scope);
|
|
72
|
+
return `${urlPrefix}/${scopeName}/${id}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a relationship URL
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} context - Request context
|
|
79
|
+
* @param {Object} scope - Scope containing transport vars
|
|
80
|
+
* @param {string} scopeName - Resource type name
|
|
81
|
+
* @param {string} id - Resource ID
|
|
82
|
+
* @param {string} relationshipName - Name of the relationship
|
|
83
|
+
* @param {boolean} isRelationshipEndpoint - If true, builds /relationships/ URL
|
|
84
|
+
* @returns {string} Complete relationship URL
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* // For relationship linkage endpoint
|
|
88
|
+
* buildRelationshipUrl(context, scope, 'books', '1', 'author', true);
|
|
89
|
+
* // Returns: "https://api.example.com/api/books/1/relationships/author"
|
|
90
|
+
*
|
|
91
|
+
* // For related resource endpoint
|
|
92
|
+
* buildRelationshipUrl(context, scope, 'books', '1', 'author', false);
|
|
93
|
+
* // Returns: "https://api.example.com/api/books/1/author"
|
|
94
|
+
*/
|
|
95
|
+
export function buildRelationshipUrl(context, scope, scopeName, id, relationshipName, isRelationshipEndpoint = false) {
|
|
96
|
+
const urlPrefix = getUrlPrefix(context, scope);
|
|
97
|
+
if (isRelationshipEndpoint) {
|
|
98
|
+
return `${urlPrefix}/${scopeName}/${id}/relationships/${relationshipName}`;
|
|
99
|
+
}
|
|
100
|
+
return `${urlPrefix}/${scopeName}/${id}/${relationshipName}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a collection URL with optional query string
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} context - Request context
|
|
107
|
+
* @param {Object} scope - Scope containing transport vars
|
|
108
|
+
* @param {string} scopeName - Resource type name
|
|
109
|
+
* @param {string} queryString - Optional query string (with leading ?)
|
|
110
|
+
* @returns {string} Complete collection URL
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* buildCollectionUrl(context, scope, 'books', '?page[size]=10');
|
|
114
|
+
* // Returns: "https://api.example.com/api/books?page[size]=10"
|
|
115
|
+
*/
|
|
116
|
+
export function buildCollectionUrl(context, scope, scopeName, queryString = '') {
|
|
117
|
+
const urlPrefix = getUrlPrefix(context, scope);
|
|
118
|
+
return `${urlPrefix}/${scopeName}${queryString}`;
|
|
119
|
+
}
|
|
@@ -10,7 +10,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
|
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* // Input: Raw schema with computed field
|
|
13
|
-
* const
|
|
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
|
|
@@ -39,7 +39,7 @@ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortField
|
|
|
39
39
|
*
|
|
40
40
|
* @example
|
|
41
41
|
* // Input: Schema with search fields
|
|
42
|
-
* const
|
|
42
|
+
* const rawFields = {
|
|
43
43
|
* title: { type: 'string', search: true }, // Searchable
|
|
44
44
|
* status: { type: 'string' }, // Not searchable
|
|
45
45
|
* author_id: { belongsTo: 'users', as: 'author' }
|
|
@@ -127,13 +127,13 @@ export async function compileSchemas(scope, deps) {
|
|
|
127
127
|
const scopeName = context.scopeName;
|
|
128
128
|
|
|
129
129
|
// Get raw schema
|
|
130
|
-
const
|
|
130
|
+
const rawFields = scope.scopeOptions?.schema || {};
|
|
131
131
|
|
|
132
132
|
// Extract computed fields from schema and build enriched schema
|
|
133
133
|
const computedFields = {};
|
|
134
|
-
const
|
|
134
|
+
const enrichedFields = {};
|
|
135
135
|
|
|
136
|
-
for (const [fieldName, fieldDef] of Object.entries(
|
|
136
|
+
for (const [fieldName, fieldDef] of Object.entries(rawFields)) {
|
|
137
137
|
if (fieldDef.computed === true) {
|
|
138
138
|
// Extract computed field - copy entire definition
|
|
139
139
|
computedFields[fieldName] = { ...fieldDef };
|
|
@@ -149,12 +149,12 @@ export async function compileSchemas(scope, deps) {
|
|
|
149
149
|
|
|
150
150
|
// Don't include computed fields in the validation schema
|
|
151
151
|
} else {
|
|
152
|
-
|
|
152
|
+
enrichedFields[fieldName] = { ...fieldDef };
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// Default enrichment: Add type to belongsTo fields
|
|
157
|
-
for (const [fieldName, fieldDef] of Object.entries(
|
|
157
|
+
for (const [fieldName, fieldDef] of Object.entries(enrichedFields)) {
|
|
158
158
|
if (fieldDef.belongsTo && !fieldDef.type) {
|
|
159
159
|
fieldDef.type = 'id';
|
|
160
160
|
}
|
|
@@ -163,7 +163,7 @@ export async function compileSchemas(scope, deps) {
|
|
|
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(
|
|
166
|
+
const fields = Object.entries(enrichedFields);
|
|
167
167
|
const belongsToFields = fields.filter(([_, def]) => def.belongsTo);
|
|
168
168
|
|
|
169
169
|
// If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
|
|
@@ -183,52 +183,56 @@ export async function compileSchemas(scope, deps) {
|
|
|
183
183
|
|
|
184
184
|
// Hook: schema:enrich
|
|
185
185
|
const schemaContext = {
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
fields: enrichedFields, // Mutable
|
|
187
|
+
originalFields: rawFields, // Read-only
|
|
188
188
|
scopeName
|
|
189
189
|
};
|
|
190
190
|
await runHooks('schema:enrich', schemaContext);
|
|
191
191
|
|
|
192
192
|
// Create schema object
|
|
193
|
-
const schemaObject = createSchema(schemaContext.
|
|
193
|
+
const schemaObject = createSchema(schemaContext.fields);
|
|
194
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.
|
|
198
|
-
// The explicit searchSchema takes precedence
|
|
198
|
+
// The explicit searchSchema takes precedence when there are conflicts - it can override
|
|
199
|
+
// fields marked with search:true. Fields with search:true that are NOT in the explicit
|
|
200
|
+
// searchSchema will be added automatically.
|
|
199
201
|
// Example: title: {search: true} auto-generates a searchable field with sensible defaults,
|
|
200
202
|
// while searchSchema can specify filterOperator: 'contains' or complex join configurations.
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
+
let rawSearchFields = generateSearchSchemaFromSchema(
|
|
204
|
+
schemaContext.fields,
|
|
205
|
+
scope.scopeOptions.searchSchema
|
|
206
|
+
);
|
|
203
207
|
|
|
204
|
-
if (
|
|
208
|
+
if (rawSearchFields) {
|
|
205
209
|
// Mark all search fields as indexed for database optimization.
|
|
206
210
|
// This ensures that any field used for filtering will have a database index created,
|
|
207
211
|
// dramatically improving query performance. Without indexes, filtering large tables
|
|
208
212
|
// would require full table scans. The storage plugin uses these hints to create indexes.
|
|
209
213
|
// Example: A status field marked for search gets indexed: true, enabling efficient
|
|
210
214
|
// WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
|
|
211
|
-
ensureSearchFieldsAreIndexed(
|
|
215
|
+
ensureSearchFieldsAreIndexed(rawSearchFields);
|
|
212
216
|
|
|
213
217
|
// Hook: searchSchema:enrich
|
|
214
218
|
const searchSchemaContext = {
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
fields: schemaContext.fields, // Mutable
|
|
220
|
+
originalFields: rawSearchFields, // Read-only enriched schema
|
|
217
221
|
scopeName
|
|
218
222
|
};
|
|
219
223
|
await runHooks('searchSchema:enrich', searchSchemaContext);
|
|
220
224
|
|
|
221
225
|
// Create searchSchema object
|
|
222
|
-
var searchSchemaObject = createSchema(
|
|
226
|
+
var searchSchemaObject = createSchema(rawSearchFields);
|
|
223
227
|
} else {
|
|
224
|
-
var searchSchemaObject =
|
|
228
|
+
var searchSchemaObject = createSchema({});
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
// Build schemaRelationships including polymorphic fields from schema
|
|
228
232
|
const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) };
|
|
229
233
|
|
|
230
234
|
// Validate belongsTo fields
|
|
231
|
-
for (const [fieldName, fieldDef] of Object.entries(schemaContext.
|
|
235
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
232
236
|
// Validate that belongsTo fields have 'as' property
|
|
233
237
|
if (fieldDef.belongsTo && !fieldDef.as) {
|
|
234
238
|
throw new Error(
|
|
@@ -243,7 +247,7 @@ export async function compileSchemas(scope, deps) {
|
|
|
243
247
|
const fieldGetters = {};
|
|
244
248
|
const getterFields = [];
|
|
245
249
|
|
|
246
|
-
for (const [fieldName, fieldDef] of Object.entries(schemaContext.
|
|
250
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
247
251
|
if (fieldDef.getter && typeof fieldDef.getter === 'function') {
|
|
248
252
|
fieldGetters[fieldName] = {
|
|
249
253
|
getter: fieldDef.getter,
|
|
@@ -255,7 +259,7 @@ export async function compileSchemas(scope, deps) {
|
|
|
255
259
|
// Validate dependencies exist
|
|
256
260
|
if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
|
|
257
261
|
for (const dep of fieldDef.runGetterAfter) {
|
|
258
|
-
if (!schemaContext.
|
|
262
|
+
if (!schemaContext.fields[dep]) {
|
|
259
263
|
throw new Error(
|
|
260
264
|
`Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
|
|
261
265
|
);
|
|
@@ -279,7 +283,7 @@ export async function compileSchemas(scope, deps) {
|
|
|
279
283
|
const fieldSetters = {};
|
|
280
284
|
const setterFields = [];
|
|
281
285
|
|
|
282
|
-
for (const [fieldName, fieldDef] of Object.entries(schemaContext.
|
|
286
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
283
287
|
if (fieldDef.setter && typeof fieldDef.setter === 'function') {
|
|
284
288
|
fieldSetters[fieldName] = {
|
|
285
289
|
setter: fieldDef.setter,
|
|
@@ -291,7 +295,7 @@ export async function compileSchemas(scope, deps) {
|
|
|
291
295
|
// Validate dependencies exist
|
|
292
296
|
if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
|
|
293
297
|
for (const dep of fieldDef.runSetterAfter) {
|
|
294
|
-
if (!schemaContext.
|
|
298
|
+
if (!schemaContext.fields[dep]) {
|
|
295
299
|
throw new Error(
|
|
296
300
|
`Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
|
|
297
301
|
);
|
|
@@ -313,10 +317,14 @@ export async function compileSchemas(scope, deps) {
|
|
|
313
317
|
|
|
314
318
|
// Cache everything
|
|
315
319
|
scope.vars.schemaInfo = {
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
|
|
321
|
+
schemaInstance: schemaObject,
|
|
322
|
+
schemaStructure: schemaObject.structure,
|
|
323
|
+
|
|
324
|
+
searchSchemaInstance: searchSchemaObject,
|
|
325
|
+
searchSchemaStructure: searchSchemaObject.structure,
|
|
326
|
+
|
|
318
327
|
computed: computedFields,
|
|
319
|
-
searchSchema: searchSchemaObject,
|
|
320
328
|
schemaRelationships: schemaRelationships,
|
|
321
329
|
tableName: scope.scopeOptions.tableName || scopeName,
|
|
322
330
|
idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
|
|
@@ -101,8 +101,9 @@ export const supportsWindowFunctions = async (knex) => {
|
|
|
101
101
|
case 'mysql':
|
|
102
102
|
case 'mysql2':
|
|
103
103
|
try {
|
|
104
|
-
|
|
105
|
-
const mysqlVersion = parseVersion(
|
|
104
|
+
const row = await knex.first(knex.raw('VERSION() as version'));
|
|
105
|
+
const mysqlVersion = parseVersion(row.version);
|
|
106
|
+
|
|
106
107
|
return mysqlVersion && mysqlVersion.major >= 8;
|
|
107
108
|
} catch (dbError) {
|
|
108
109
|
console.warn(`[supportsWindowFunctions] Failed to get MySQL version: ${dbError.message}`);
|
|
@@ -216,13 +217,13 @@ export const getDatabaseInfo = async (knex) => {
|
|
|
216
217
|
switch (client) {
|
|
217
218
|
case 'mysql':
|
|
218
219
|
case 'mysql2':
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
220
|
+
try {
|
|
221
|
+
const row = await knex.first(knex.raw('VERSION() as version'));
|
|
222
|
+
return { client: 'MySQL', version: row.version };
|
|
223
|
+
} catch (dbError) {
|
|
224
|
+
console.warn(`[getDatabaseInfo] Failed to get MySQL version: ${dbError.message}`);
|
|
225
|
+
return { client: 'MySQL', version: 'unknown', error: dbError.message };
|
|
226
|
+
}
|
|
226
227
|
|
|
227
228
|
case 'sqlite3':
|
|
228
229
|
case 'better-sqlite3':
|
|
@@ -145,7 +145,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
145
145
|
// Extract values from scope
|
|
146
146
|
const {
|
|
147
147
|
vars: {
|
|
148
|
-
schemaInfo: {
|
|
148
|
+
schemaInfo: { schemaInstance: schemaInstance, computed: computedFields = {}, schemaStructure }
|
|
149
149
|
}
|
|
150
150
|
} = scope;
|
|
151
151
|
|
|
@@ -165,7 +165,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
165
165
|
|
|
166
166
|
// Handle both Schema objects and plain objects
|
|
167
167
|
if (!schemaStructure) {
|
|
168
|
-
schemaStructure =
|
|
168
|
+
schemaStructure = schemaInstance?.structure || schemaInstance || {};
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Get computed fields and virtual fields from schema
|
|
@@ -199,6 +199,10 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
199
199
|
|
|
200
200
|
const fieldDef = schemaStructure[field];
|
|
201
201
|
if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`);
|
|
202
|
+
|
|
203
|
+
if (fieldDef.belongsToPolymorphic) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
202
206
|
|
|
203
207
|
// NEVER include hidden fields, even if explicitly requested
|
|
204
208
|
// Example: password_hash with hidden:true is never returned
|
|
@@ -254,6 +258,9 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
254
258
|
// Example: password_hash with hidden:true
|
|
255
259
|
if (fieldDef.hidden === true) return;
|
|
256
260
|
|
|
261
|
+
// Skip polymorphic placeholder fields - handled via type/id columns
|
|
262
|
+
if (fieldDef.belongsToPolymorphic) return;
|
|
263
|
+
|
|
257
264
|
// Skip normallyHidden fields - these are hidden by default
|
|
258
265
|
// Example: cost with normallyHidden:true (only returned when explicitly requested)
|
|
259
266
|
if (fieldDef.normallyHidden === true) return;
|
|
@@ -385,4 +392,3 @@ export const getRequestedComputedFields = (scopeName, requestedFields, computedF
|
|
|
385
392
|
// Return only requested computed fields that exist
|
|
386
393
|
return requested.filter(field => allComputedFields.includes(field));
|
|
387
394
|
};
|
|
388
|
-
|
|
@@ -98,14 +98,14 @@ export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
|
|
|
98
98
|
// Extract schema info from scope
|
|
99
99
|
const {
|
|
100
100
|
vars: {
|
|
101
|
-
schemaInfo: {
|
|
101
|
+
schemaInfo: { schemaInstance: schemaInstance, schemaRelationships: relationships, idProperty }
|
|
102
102
|
}
|
|
103
103
|
} = scope;
|
|
104
104
|
|
|
105
105
|
const idField = idProperty || 'id';
|
|
106
106
|
|
|
107
107
|
// Get schema structure
|
|
108
|
-
const schemaStructure = getSchemaStructure(
|
|
108
|
+
const schemaStructure = getSchemaStructure(schemaInstance);
|
|
109
109
|
|
|
110
110
|
// Initialize relationships object
|
|
111
111
|
jsonApiRecord.relationships = {};
|
|
@@ -5,13 +5,13 @@ import { RestApiValidationError, RestApiPayloadError } from '../../../../lib/res
|
|
|
5
5
|
*
|
|
6
6
|
* @param {string[]} includes - Array of include paths to validate
|
|
7
7
|
* @param {number} maxDepth - Maximum allowed depth
|
|
8
|
-
* @returns {
|
|
8
|
+
* @returns {void}
|
|
9
9
|
* @throws {RestApiValidationError} If depth is exceeded
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* // Input: Valid include paths
|
|
13
13
|
* validateIncludeDepth(['author', 'comments.author'], 3);
|
|
14
|
-
* // Output:
|
|
14
|
+
* // Output: Validation succeeds (no error thrown)
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* // Input: Path exceeding depth limit
|
|
@@ -23,7 +23,7 @@ import { RestApiValidationError, RestApiPayloadError } from '../../../../lib/res
|
|
|
23
23
|
*/
|
|
24
24
|
function validateIncludeDepth(includes, maxDepth) {
|
|
25
25
|
if (!includes || !Array.isArray(includes)) {
|
|
26
|
-
return
|
|
26
|
+
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
for (const includePath of includes) {
|
|
@@ -46,8 +46,6 @@ function validateIncludeDepth(includes, maxDepth) {
|
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
return true;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
@@ -140,8 +138,6 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
|
|
|
140
138
|
{ path: `${context}.id`, expected: 'string, number, or null', received: typeof identifier.id }
|
|
141
139
|
);
|
|
142
140
|
}
|
|
143
|
-
|
|
144
|
-
return true;
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
/**
|
|
@@ -150,7 +146,7 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
|
|
|
150
146
|
* @param {Object} relationship - Relationship object to validate
|
|
151
147
|
* @param {string} relationshipName - Name for error context
|
|
152
148
|
* @param {Object} scopes - Scopes proxy to verify resource types
|
|
153
|
-
* @returns {
|
|
149
|
+
* @returns {void}
|
|
154
150
|
* @throws {RestApiPayloadError} If validation fails
|
|
155
151
|
*
|
|
156
152
|
* @example
|
|
@@ -160,7 +156,7 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
|
|
|
160
156
|
* 'author',
|
|
161
157
|
* scopes
|
|
162
158
|
* );
|
|
163
|
-
* // Output:
|
|
159
|
+
* // Output: Validation succeeds (no error thrown)
|
|
164
160
|
*
|
|
165
161
|
* @example
|
|
166
162
|
* // Input: To-many relationship
|
|
@@ -174,7 +170,7 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
|
|
|
174
170
|
* 'tags',
|
|
175
171
|
* scopes
|
|
176
172
|
* );
|
|
177
|
-
* // Output:
|
|
173
|
+
* // Output: Validation succeeds (no error thrown)
|
|
178
174
|
*
|
|
179
175
|
* @example
|
|
180
176
|
* // Input: Null relationship (remove association)
|
|
@@ -183,7 +179,7 @@ function validateResourceIdentifier(identifier, context, scopes = null) {
|
|
|
183
179
|
* 'featuredImage',
|
|
184
180
|
* scopes
|
|
185
181
|
* );
|
|
186
|
-
* // Output:
|
|
182
|
+
* // Output: Validation succeeds (null is valid for clearing)
|
|
187
183
|
*
|
|
188
184
|
* @example
|
|
189
185
|
* // Input: Missing data property
|
|
@@ -228,25 +224,23 @@ function validateRelationship(relationship, relationshipName, scopes = null) {
|
|
|
228
224
|
|
|
229
225
|
// data can be null (empty to-one relationship)
|
|
230
226
|
if (data === null) {
|
|
231
|
-
return
|
|
227
|
+
return;
|
|
232
228
|
}
|
|
233
229
|
|
|
234
230
|
// data can be a single resource identifier (to-one)
|
|
235
231
|
if (!Array.isArray(data)) {
|
|
236
232
|
validateResourceIdentifier(data, `Relationship '${relationshipName}'`, scopes);
|
|
237
|
-
return
|
|
233
|
+
return;
|
|
238
234
|
}
|
|
239
235
|
|
|
240
236
|
// data can be an array of resource identifiers (to-many)
|
|
241
237
|
if (data.length === 0) {
|
|
242
|
-
return
|
|
238
|
+
return; // Empty to-many relationship is valid
|
|
243
239
|
}
|
|
244
240
|
|
|
245
241
|
data.forEach((identifier, index) => {
|
|
246
242
|
validateResourceIdentifier(identifier, `Relationship '${relationshipName}[${index}]'`, scopes);
|
|
247
243
|
});
|
|
248
|
-
|
|
249
|
-
return true;
|
|
250
244
|
}
|
|
251
245
|
|
|
252
246
|
/**
|