json-rest-api 1.0.6 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -1,109 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
7
|
+
* @example
|
|
8
|
+
* // Input: Basic include and sort
|
|
32
9
|
* const query = parseJsonApiQuery('include=author&sort=-created_at');
|
|
33
|
-
*
|
|
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
|
|
20
|
+
* @example
|
|
21
|
+
* // Input: Filters with bracket notation
|
|
43
22
|
* const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
|
|
44
|
-
*
|
|
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
|
|
36
|
+
* @example
|
|
37
|
+
* // Input: Sparse fieldsets
|
|
57
38
|
* const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
|
|
58
|
-
*
|
|
39
|
+
*
|
|
40
|
+
* // Output: Fields kept as comma-separated strings
|
|
59
41
|
* // {
|
|
60
42
|
* // include: [],
|
|
61
43
|
* // fields: {
|
|
62
|
-
* // articles: 'title,body', //
|
|
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
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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,
|
|
80
|
-
* // number: 3
|
|
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
|
|
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
|
|
77
|
+
* 'page[size]=10'
|
|
92
78
|
* );
|
|
93
|
-
* // Returns complete parsed structure with all parameters
|
|
94
79
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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) {
|
package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js}
RENAMED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
+
}
|