json-rest-api 1.0.7 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -1,109 +1,114 @@
1
1
  /**
2
- * @module connectors-query-parser
3
- * @description Shared JSON:API query parameter parser for connector plugins
4
- *
5
- * This module provides utilities for parsing URL query strings into JSON:API
6
- * compliant parameter objects. It handles the standard JSON:API query parameter
7
- * patterns including bracket notation for filters, fields, and pagination.
8
- * This parser is shared between HTTP, Express, and WebSocket connectors to
9
- * ensure consistent query parameter handling across all transport layers.
10
- *
11
- * Why this is useful upstream:
12
- * - Ensures consistent JSON:API query parsing across all connectors
13
- * - Handles bracket notation parsing (e.g., filter[status]=published)
14
- * - Converts comma-separated values to arrays for include/sort
15
- * - Maintains fields as strings for REST API validation
16
- * - Automatically converts numeric page values
17
- * - Ignores non-JSON:API parameters safely
18
- */
19
-
20
- /**
21
- * Parses URL query strings into JSON:API compliant parameter objects.
22
- *
23
- * This function takes a raw query string and converts it into a structured
24
- * object following JSON:API conventions. It handles all standard JSON:API
25
- * query parameter patterns including filters with bracket notation, sparse
26
- * fieldsets, sorting, pagination, and relationship inclusion.
2
+ * Parses URL query strings into JSON:API compliant parameter objects
27
3
  *
28
4
  * @param {string} queryString - The query string part of URL (without ?)
29
5
  * @returns {object} Parsed query parameters in JSON:API format
30
6
  *
31
- * @example <caption>Basic query parsing</caption>
7
+ * @example
8
+ * // Input: Basic include and sort
32
9
  * const query = parseJsonApiQuery('include=author&sort=-created_at');
33
- * // Returns:
10
+ *
11
+ * // Output: Parsed into arrays
34
12
  * // {
35
- * // include: ['author'],
13
+ * // include: ['author'], // Split by comma
36
14
  * // fields: {},
37
15
  * // filters: {},
38
- * // sort: ['-created_at'],
16
+ * // sort: ['-created_at'], // Split by comma
39
17
  * // page: {}
40
18
  * // }
41
19
  *
42
- * @example <caption>Filter parameters with bracket notation</caption>
20
+ * @example
21
+ * // Input: Filters with bracket notation
43
22
  * const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
44
- * // Returns:
23
+ *
24
+ * // Output: Extracted filter keys
45
25
  * // {
46
26
  * // include: [],
47
27
  * // fields: {},
48
28
  * // filters: {
49
- * // status: 'published',
50
- * // author_id: '123'
29
+ * // status: 'published', // filter[status] → filters.status
30
+ * // author_id: '123' // Values kept as strings
51
31
  * // },
52
32
  * // sort: [],
53
33
  * // page: {}
54
34
  * // }
55
35
  *
56
- * @example <caption>Sparse fieldsets</caption>
36
+ * @example
37
+ * // Input: Sparse fieldsets
57
38
  * const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
58
- * // Returns:
39
+ *
40
+ * // Output: Fields kept as comma-separated strings
59
41
  * // {
60
42
  * // include: [],
61
43
  * // fields: {
62
- * // articles: 'title,body', // Kept as string for validation
63
- * // users: 'name'
44
+ * // articles: 'title,body', // NOT split into array
45
+ * // users: 'name' // Validation expects strings
64
46
  * // },
65
47
  * // filters: {},
66
48
  * // sort: [],
67
49
  * // page: {}
68
50
  * // }
69
51
  *
70
- * @example <caption>Pagination parameters</caption>
71
- * const query = parseJsonApiQuery('page[size]=20&page[number]=3');
72
- * // Returns:
52
+ * @example
53
+ * // Input: Pagination with numeric conversion
54
+ * const query = parseJsonApiQuery('page[size]=20&page[number]=3&page[cursor]=abc123');
55
+ *
56
+ * // Output: Numbers converted, strings preserved
73
57
  * // {
74
58
  * // include: [],
75
59
  * // fields: {},
76
60
  * // filters: {},
77
61
  * // sort: [],
78
62
  * // page: {
79
- * // size: 20, // Converted to number
80
- * // number: 3 // Converted to number
63
+ * // size: 20, // "20" → 20 (numeric)
64
+ * // number: 3, // "3" → 3 (numeric)
65
+ * // cursor: 'abc123' // Non-numeric stays string
81
66
  * // }
82
67
  * // }
83
68
  *
84
- * @example <caption>Complex query with all parameter types</caption>
69
+ * @example
70
+ * // Input: Complex real-world query
85
71
  * const query = parseJsonApiQuery(
86
72
  * 'include=author,comments.user&' +
87
73
  * 'filter[status]=published&' +
88
- * 'filter[created_after]=2024-01-01&' +
74
+ * 'filter[created_after]=2024-01-01&' +
89
75
  * 'fields[articles]=title,summary&' +
90
76
  * 'sort=-created_at,title&' +
91
- * 'page[size]=10&page[number]=1'
77
+ * 'page[size]=10'
92
78
  * );
93
- * // Returns complete parsed structure with all parameters
94
79
  *
95
- * @example <caption>Empty or invalid input</caption>
96
- * parseJsonApiQuery(''); // Returns default structure with empty values
97
- * parseJsonApiQuery(null); // Returns default structure with empty values
98
- * parseJsonApiQuery('foo=bar'); // Ignores non-JSON:API parameters
80
+ * // Output: All parameters properly categorized
81
+ * // {
82
+ * // include: ['author', 'comments.user'],
83
+ * // fields: { articles: 'title,summary' },
84
+ * // filters: {
85
+ * // status: 'published',
86
+ * // created_after: '2024-01-01'
87
+ * // },
88
+ * // sort: ['-created_at', 'title'],
89
+ * // page: { size: 10 }
90
+ * // }
91
+ *
92
+ * @description
93
+ * Used by:
94
+ * - express-plugin parses req.query with this
95
+ * - http-plugin parses URL query strings with this
96
+ * - websocket-plugin parses message query parameters with this
97
+ *
98
+ * Purpose:
99
+ * - Provides uniform JSON:API query parsing across all transports
100
+ * - Handles bracket notation without regex complexity
101
+ * - Converts data types appropriately (numbers for pagination)
102
+ * - Ignores non-JSON:API parameters gracefully
103
+ * - Returns consistent structure even for empty input
99
104
  *
100
- * @example <caption>Why this is useful upstream</caption>
101
- * // Connector plugins use this to:
102
- * // 1. Convert raw URL queries to structured JSON:API format
103
- * // 2. Handle bracket notation without custom regex parsing
104
- * // 3. Ensure consistent behavior across HTTP/Express/WebSocket
105
- * // 4. Prepare query parameters for REST API validation
106
- * // 5. Support all JSON:API query features uniformly
105
+ * Data flow:
106
+ * 1. Uses URLSearchParams for reliable parsing
107
+ * 2. Categorizes parameters by their prefix pattern
108
+ * 3. Extracts bracketed keys (filter[x] → x)
109
+ * 4. Splits comma-separated values for include/sort
110
+ * 5. Converts numeric strings for pagination
111
+ * 6. Returns normalized structure for REST API
107
112
  */
108
113
  export function parseJsonApiQuery(queryString) {
109
114
  if (!queryString) {
@@ -1,9 +1,23 @@
1
1
  /**
2
- * Detects database capabilities based on client type and version
3
- */
4
-
5
- /**
6
- * Parse version string to comparable format
2
+ * Parses database version string to comparable format
3
+ *
4
+ * @param {string} versionString - Raw version string from database
5
+ * @returns {Object|null} Parsed version with major, minor, patch
6
+ *
7
+ * @example
8
+ * // Input: MySQL version
9
+ * parseVersion("8.0.33");
10
+ * // Output: { major: 8, minor: 0, patch: 33 }
11
+ *
12
+ * @example
13
+ * // Input: PostgreSQL version with extra info
14
+ * parseVersion("14.5 (Ubuntu 14.5-1.pgdg20.04+1)");
15
+ * // Output: { major: 14, minor: 5, patch: 0 }
16
+ *
17
+ * @example
18
+ * // Input: MariaDB version
19
+ * parseVersion("10.6.12-MariaDB");
20
+ * // Output: { major: 10, minor: 6, patch: 12 }
7
21
  */
8
22
  const parseVersion = (versionString) => {
9
23
  // MySQL: "8.0.33"
@@ -22,7 +36,55 @@ const parseVersion = (versionString) => {
22
36
  };
23
37
 
24
38
  /**
25
- * Check if database supports window functions
39
+ * Checks if the database supports window functions based on version
40
+ *
41
+ * @param {Object} knex - Knex instance
42
+ * @returns {Promise<boolean>} True if window functions are supported
43
+ *
44
+ * @example
45
+ * // Input: MySQL 8.0.33
46
+ * const supports = await supportsWindowFunctions(knex);
47
+ * // Output: true (MySQL 8.0+ supports window functions)
48
+ *
49
+ * @example
50
+ * // Input: MySQL 5.7.42
51
+ * const supports = await supportsWindowFunctions(knex);
52
+ * // Output: false (MySQL < 8.0 doesn't support window functions)
53
+ *
54
+ * @example
55
+ * // Input: PostgreSQL (any modern version)
56
+ * const supports = await supportsWindowFunctions(knex);
57
+ * // Output: true (PostgreSQL 8.4+ supports window functions)
58
+ *
59
+ * @example
60
+ * // Input: SQLite 3.24.0
61
+ * const supports = await supportsWindowFunctions(knex);
62
+ * // Output: false (SQLite needs 3.25.0+)
63
+ *
64
+ * @description
65
+ * Used by:
66
+ * - buildWindowedIncludeQuery to validate before using window functions
67
+ * - rest-api-knex-plugin to determine query strategies
68
+ *
69
+ * Purpose:
70
+ * - Prevents runtime errors by checking feature support
71
+ * - Enables fallback strategies for older databases
72
+ * - Provides clear error messages about version requirements
73
+ *
74
+ * Data flow:
75
+ * 1. Identifies database client type from Knex config
76
+ * 2. For version-dependent databases, queries version string
77
+ * 3. Parses version and compares against known thresholds
78
+ * 4. Returns boolean indicating support
79
+ * 5. Falls back to false if version can't be determined
80
+ *
81
+ * Version requirements:
82
+ * - PostgreSQL: 8.4+ (all modern versions)
83
+ * - MySQL: 8.0+
84
+ * - MariaDB: 10.2+
85
+ * - SQLite: 3.25.0+
86
+ * - SQL Server: 2005+ (all versions)
87
+ * - Oracle: Supported (long-standing feature)
26
88
  */
27
89
  export const supportsWindowFunctions = async (knex) => {
28
90
  try {
@@ -100,7 +162,51 @@ export const supportsWindowFunctions = async (knex) => {
100
162
  };
101
163
 
102
164
  /**
103
- * Get database info for error messages
165
+ * Gets database info for error messages and capability checks
166
+ *
167
+ * @param {Object} knex - Knex instance
168
+ * @returns {Promise<Object>} Database info with client and version
169
+ *
170
+ * @example
171
+ * // Input: MySQL connection
172
+ * const info = await getDatabaseInfo(knex);
173
+ * // Output: { client: 'MySQL', version: '8.0.33' }
174
+ *
175
+ * @example
176
+ * // Input: PostgreSQL connection
177
+ * const info = await getDatabaseInfo(knex);
178
+ * // Output: {
179
+ * // client: 'PostgreSQL',
180
+ * // version: 'PostgreSQL 14.5 (Ubuntu 14.5-1.pgdg20.04+1)'
181
+ * // }
182
+ *
183
+ * @example
184
+ * // Input: Connection error case
185
+ * const info = await getDatabaseInfo(knexWithError);
186
+ * // Output: {
187
+ * // client: 'MySQL',
188
+ * // version: 'unknown',
189
+ * // error: 'Connection refused'
190
+ * // }
191
+ *
192
+ * @description
193
+ * Used by:
194
+ * - Error messages to show which database/version lacks a feature
195
+ * - Capability detection for feature flags
196
+ * - Debugging connection issues
197
+ *
198
+ * Purpose:
199
+ * - Provides human-readable database identification
200
+ * - Helps users understand version requirements
201
+ * - Enables detailed error messages
202
+ * - Gracefully handles query failures
203
+ *
204
+ * Data flow:
205
+ * 1. Identifies client type from Knex config
206
+ * 2. Runs version query specific to each database
207
+ * 3. Returns formatted info object
208
+ * 4. Includes error details if version query fails
209
+ * 5. Falls back to 'unknown' for unsupported databases
104
210
  */
105
211
  export const getDatabaseInfo = async (knex) => {
106
212
  try {
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Normalizes a date value to appropriate format based on field type
3
+ *
4
+ * @param {*} value - The date value from the database
5
+ * @param {string} type - The field type ('date', 'dateTime', or 'time')
6
+ * @returns {Date|string|null} Date object for date/dateTime, string for time, or null
7
+ *
8
+ * @example
9
+ * // Input: Already a Date object
10
+ * normalizeDateValue(new Date('2024-01-15T10:30:00Z'), 'dateTime');
11
+ * // Output: Date object (unchanged)
12
+ *
13
+ * @example
14
+ * // Input: MySQL datetime string without timezone
15
+ * normalizeDateValue('2024-01-15 10:30:00', 'dateTime');
16
+ * // Output: Date object parsed as UTC (2024-01-15T10:30:00Z)
17
+ *
18
+ * @example
19
+ * // Input: Date-only string
20
+ * normalizeDateValue('2024-01-15', 'date');
21
+ * // Output: Date object at UTC midnight (2024-01-15T00:00:00Z)
22
+ *
23
+ * @example
24
+ * // Input: Time field (always returns string)
25
+ * normalizeDateValue('14:30:45', 'time');
26
+ * // Output: "14:30:45" (string)
27
+ *
28
+ * @example
29
+ * // Input: Date object for time field
30
+ * normalizeDateValue(new Date('2024-01-15T14:30:45Z'), 'time');
31
+ * // Output: "14:30:45" (extracts time portion)
32
+ *
33
+ * @description
34
+ * Used by:
35
+ * - normalizeAttributes to process individual field values
36
+ * - Applied to all date/time fields fetched from database
37
+ *
38
+ * Purpose:
39
+ * - Handles database-specific date formats (especially MySQL)
40
+ * - Prevents timezone shifts for date-only fields
41
+ * - Ensures consistent Date objects across databases
42
+ * - Keeps time fields as HH:MM:SS strings
43
+ *
44
+ * Data flow:
45
+ * 1. Checks for null/undefined (returns null)
46
+ * 2. For time fields: ensures HH:MM:SS string format
47
+ * 3. For date/dateTime: converts to Date objects
48
+ * 4. Handles MySQL datetime format (no T separator)
49
+ * 5. Forces UTC interpretation to prevent timezone issues
50
+ */
51
+ function normalizeDateValue(value, type) {
52
+ // Handle null/undefined
53
+ if (value === null || value === undefined) {
54
+ return null;
55
+ }
56
+
57
+ // Handle time type specially - always return as string
58
+ if (type === 'time') {
59
+ // If it's already a properly formatted time string, return as-is
60
+ if (typeof value === 'string' && /^\d{2}:\d{2}:\d{2}/.test(value)) {
61
+ return value;
62
+ }
63
+ // If we got a Date object for a time field, extract the time portion
64
+ if (value instanceof Date) {
65
+ return value.toISOString().slice(11, 19); // Extract HH:MM:SS
66
+ }
67
+ // Try to parse and extract time
68
+ if (typeof value === 'string' || typeof value === 'number') {
69
+ const d = new Date(value);
70
+ if (!isNaN(d.getTime())) {
71
+ return d.toISOString().slice(11, 19);
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ // For date and dateTime: Already a Date object? Return as-is
78
+ if (value instanceof Date) {
79
+ return value;
80
+ }
81
+
82
+ // Handle string values
83
+ if (typeof value === 'string') {
84
+ // Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
85
+ // These have no T separator and no timezone indicator
86
+ const isMySQLDateTime = type === 'dateTime' &&
87
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(value) &&
88
+ !value.includes('T') &&
89
+ !value.includes('Z') &&
90
+ !value.includes('+') &&
91
+ !value.includes('-', 10); // Don't match date separators
92
+
93
+ if (isMySQLDateTime) {
94
+ // Convert to ISO format and force UTC interpretation
95
+ // '2024-01-15 10:30:00' becomes '2024-01-15T10:30:00Z'
96
+ return new Date(value.replace(' ', 'T') + 'Z');
97
+ }
98
+
99
+ // For date-only fields, ensure parsing at UTC midnight
100
+ // This prevents timezone shifts when parsing dates
101
+ if (type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
102
+ return new Date(value + 'T00:00:00Z');
103
+ }
104
+ }
105
+
106
+ // Handle numeric values (Unix timestamps)
107
+ if (typeof value === 'number') {
108
+ return new Date(value);
109
+ }
110
+
111
+ // Try to parse any other format
112
+ const dateObj = new Date(value);
113
+
114
+ // Check if date is valid
115
+ if (isNaN(dateObj.getTime())) {
116
+ console.warn(`Invalid date value for field type '${type}': ${value}`);
117
+ return null; // Return null for invalid dates
118
+ }
119
+
120
+ return dateObj;
121
+ }
122
+
123
+ /**
124
+ * Normalizes database values in an attributes object
125
+ *
126
+ * @param {Object} attributes - The attributes object to normalize
127
+ * @param {Object} schemaStructure - The schema structure defining field types
128
+ * @returns {Object} The normalized attributes object
129
+ *
130
+ * @example
131
+ * // Input: Raw database values
132
+ * const attributes = {
133
+ * id: 1,
134
+ * is_active: 1, // Boolean as 0/1
135
+ * created_at: '2024-01-15 10:30:00', // MySQL datetime
136
+ * birth_date: '1990-05-20', // Date string
137
+ * shift_time: '09:00:00' // Time string
138
+ * };
139
+ *
140
+ * const schema = {
141
+ * is_active: { type: 'boolean' },
142
+ * created_at: { type: 'dateTime' },
143
+ * birth_date: { type: 'date' },
144
+ * shift_time: { type: 'time' }
145
+ * };
146
+ *
147
+ * const normalized = normalizeAttributes(attributes, schema);
148
+ *
149
+ * // Output: Properly typed values
150
+ * // {
151
+ * // id: 1,
152
+ * // is_active: true, // 1 → true
153
+ * // created_at: Date('2024-01-15T10:30:00Z'), // Date object
154
+ * // birth_date: Date('1990-05-20T00:00:00Z'), // Date at midnight UTC
155
+ * // shift_time: '09:00:00' // String unchanged
156
+ * // }
157
+ *
158
+ * @example
159
+ * // Input: Boolean edge cases
160
+ * const attributes = {
161
+ * flag1: 0, // Number 0
162
+ * flag2: '1', // String '1'
163
+ * flag3: true, // Already boolean
164
+ * flag4: null // Null value
165
+ * };
166
+ *
167
+ * const normalized = normalizeAttributes(attributes, {
168
+ * flag1: { type: 'boolean' },
169
+ * flag2: { type: 'boolean' },
170
+ * flag3: { type: 'boolean' },
171
+ * flag4: { type: 'boolean' }
172
+ * });
173
+ *
174
+ * // Output:
175
+ * // {
176
+ * // flag1: false, // 0 → false
177
+ * // flag2: true, // '1' → true
178
+ * // flag3: true, // unchanged
179
+ * // flag4: null // null preserved
180
+ * // }
181
+ *
182
+ * @description
183
+ * Used by:
184
+ * - normalizeRecordAttributes for each record's attributes
185
+ * - Applied after fetching data from database
186
+ *
187
+ * Purpose:
188
+ * - Handles databases without native boolean support (SQLite, older MySQL)
189
+ * - Normalizes date formats from different databases
190
+ * - Ensures consistent data types in API responses
191
+ * - Preserves null values appropriately
192
+ *
193
+ * Data flow:
194
+ * 1. Creates copy of attributes to avoid mutation
195
+ * 2. Iterates through each field with schema definition
196
+ * 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
197
+ * 4. Normalizes dates using normalizeDateValue
198
+ * 5. Returns new object with normalized values
199
+ */
200
+ export function normalizeAttributes(attributes, schemaStructure) {
201
+ if (!attributes || !schemaStructure) {
202
+ return attributes;
203
+ }
204
+
205
+ const normalized = { ...attributes };
206
+
207
+ // Iterate through each attribute
208
+ for (const [fieldName, value] of Object.entries(attributes)) {
209
+ const fieldDef = schemaStructure[fieldName];
210
+
211
+ // Skip if no field definition found
212
+ if (!fieldDef) continue;
213
+
214
+ // Normalize boolean values
215
+ if (fieldDef.type === 'boolean') {
216
+ if (value === 1 || value === '1') {
217
+ normalized[fieldName] = true;
218
+ } else if (value === 0 || value === '0') {
219
+ normalized[fieldName] = false;
220
+ }
221
+ // null/undefined remain as-is
222
+ }
223
+
224
+ // Normalize date/dateTime/time values
225
+ if (fieldDef.type === 'date' || fieldDef.type === 'dateTime' || fieldDef.type === 'time') {
226
+ normalized[fieldName] = normalizeDateValue(value, fieldDef.type);
227
+ }
228
+ }
229
+
230
+ return normalized;
231
+ }
232
+
233
+ /**
234
+ * Normalizes all records in a JSON:API response
235
+ *
236
+ * @param {Object} record - The JSON:API response object
237
+ * @param {Object} scopes - All available scopes for schema lookup
238
+ * @returns {Object} The response with normalized values
239
+ *
240
+ * @example
241
+ * // Input: JSON:API response with various data types
242
+ * const response = {
243
+ * data: {
244
+ * type: 'articles',
245
+ * id: '1',
246
+ * attributes: {
247
+ * title: 'My Article',
248
+ * is_published: 1, // Boolean as number
249
+ * published_at: '2024-01-15 10:00:00' // MySQL datetime
250
+ * }
251
+ * },
252
+ * included: [{
253
+ * type: 'users',
254
+ * id: '10',
255
+ * attributes: {
256
+ * name: 'John',
257
+ * is_admin: 0, // Boolean as number
258
+ * last_login: '2024-01-14 15:30:00'
259
+ * }
260
+ * }]
261
+ * };
262
+ *
263
+ * const normalized = normalizeRecordAttributes(response, scopes);
264
+ *
265
+ * // Output: All booleans and dates normalized
266
+ * // {
267
+ * // data: {
268
+ * // type: 'articles',
269
+ * // id: '1',
270
+ * // attributes: {
271
+ * // title: 'My Article',
272
+ * // is_published: true, // 1 → true
273
+ * // published_at: Date('2024-01-15T10:00:00Z') // Date object
274
+ * // }
275
+ * // },
276
+ * // included: [{
277
+ * // type: 'users',
278
+ * // id: '10',
279
+ * // attributes: {
280
+ * // name: 'John',
281
+ * // is_admin: false, // 0 → false
282
+ * // last_login: Date('2024-01-14T15:30:00Z') // Date object
283
+ * // }
284
+ * // }]
285
+ * // }
286
+ *
287
+ * @example
288
+ * // Input: Collection response (array of records)
289
+ * const response = {
290
+ * data: [
291
+ * {
292
+ * type: 'comments',
293
+ * id: '1',
294
+ * attributes: { approved: 1, created_at: '2024-01-01 09:00:00' }
295
+ * },
296
+ * {
297
+ * type: 'comments',
298
+ * id: '2',
299
+ * attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
300
+ * }
301
+ * ]
302
+ * };
303
+ *
304
+ * // Each record in the array is normalized
305
+ *
306
+ * @description
307
+ * Used by:
308
+ * - rest-api-knex-plugin after fetching data, before sending response
309
+ * - Applied to both GET (single) and QUERY (collection) responses
310
+ *
311
+ * Purpose:
312
+ * - Ensures consistent data types in API responses
313
+ * - Handles both primary data and included resources
314
+ * - Works with single records and collections
315
+ * - Uses schema definitions to determine field types
316
+ *
317
+ * Data flow:
318
+ * 1. Checks if data is array (collection) or object (single)
319
+ * 2. For each record, looks up its schema by type
320
+ * 3. Calls normalizeAttributes with schema structure
321
+ * 4. Processes included array the same way
322
+ * 5. Returns complete response with normalized values
323
+ */
324
+ export function normalizeRecordAttributes(record, scopes) {
325
+ if (!record || !scopes) {
326
+ return record;
327
+ }
328
+
329
+ // Normalize main data records
330
+ if (record.data) {
331
+ if (Array.isArray(record.data)) {
332
+ // Handle array of records (query result)
333
+ for (const entry of record.data) {
334
+ const scope = scopes[entry.type];
335
+ if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
336
+ entry.attributes = normalizeAttributes(
337
+ entry.attributes,
338
+ scope.vars.schemaInfo.schemaStructure
339
+ );
340
+ }
341
+ }
342
+ } else {
343
+ // Handle single record (get result)
344
+ const entry = record.data;
345
+ const scope = scopes[entry.type];
346
+ if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
347
+ entry.attributes = normalizeAttributes(
348
+ entry.attributes,
349
+ scope.vars.schemaInfo.schemaStructure
350
+ );
351
+ }
352
+ }
353
+ }
354
+
355
+ // Normalize included records
356
+ if (record.included && Array.isArray(record.included)) {
357
+ for (const entry of record.included) {
358
+ const scope = scopes[entry.type];
359
+ if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
360
+ entry.attributes = normalizeAttributes(
361
+ entry.attributes,
362
+ scope.vars.schemaInfo.schemaStructure
363
+ );
364
+ }
365
+ }
366
+ }
367
+
368
+ return record;
369
+ }