json-rest-api 1.0.10 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. 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 rawSchema = {
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 rawSchema = {
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 rawSchema = scope.scopeOptions?.schema || {};
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 enrichedSchema = {};
134
+ const enrichedFields = {};
135
135
 
136
- for (const [fieldName, fieldDef] of Object.entries(rawSchema)) {
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
- enrichedSchema[fieldName] = { ...fieldDef };
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(enrichedSchema)) {
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(enrichedSchema);
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
- schema: enrichedSchema, // Mutable
187
- originalSchema: rawSchema, // Read-only
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.schema);
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 and can define virtual fields for joins.
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 rawSearchSchema = scope.scopeOptions.searchSchema ||
202
- generateSearchSchemaFromSchema(schemaContext.schema);
203
+ let rawSearchFields = generateSearchSchemaFromSchema(
204
+ schemaContext.fields,
205
+ scope.scopeOptions.searchSchema
206
+ );
203
207
 
204
- if (rawSearchSchema) {
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(rawSearchSchema);
215
+ ensureSearchFieldsAreIndexed(rawSearchFields);
212
216
 
213
217
  // Hook: searchSchema:enrich
214
218
  const searchSchemaContext = {
215
- searchSchema: rawSearchSchema, // Mutable
216
- schema: schemaContext.schema, // Read-only enriched schema
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(searchSchemaContext.searchSchema);
226
+ var searchSchemaObject = createSchema(rawSearchFields);
223
227
  } else {
224
- var searchSchemaObject = null;
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.schema)) {
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.schema)) {
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.schema[dep]) {
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.schema)) {
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.schema[dep]) {
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
- schema: schemaObject,
317
- schemaStructure: schemaContext.schema,
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
- versionString = await knex.raw('SELECT VERSION() as version');
105
- const mysqlVersion = parseVersion(versionString[0].version);
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
- try {
220
- versionString = await knex.raw('SELECT VERSION() as version');
221
- return { client: 'MySQL', version: versionString[0].version };
222
- } catch (dbError) {
223
- console.warn(`[getDatabaseInfo] Failed to get MySQL version: ${dbError.message}`);
224
- return { client: 'MySQL', version: 'unknown', error: dbError.message };
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: { schema, computed: computedFields = {}, schemaStructure }
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 = schema?.structure || schema || {};
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: { schema, schemaRelationships: relationships, idProperty }
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(schema);
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 {boolean} True if valid
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: true (depths are 1 and 2, both under limit)
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 true;
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 {boolean} True if valid
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: true
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: true
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: true (null is valid for clearing)
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 true;
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 true;
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 true; // Empty to-many relationship is valid
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
  /**