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.
- 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 +90 -32
- 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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default data helper implementations that instruct users to install a storage plugin
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* These default implementations ensure the REST API plugin fails gracefully
|
|
6
|
+
* when no storage plugin is installed. Each method throws a descriptive error
|
|
7
|
+
* guiding users to install a storage implementation.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // When no storage plugin is installed:
|
|
11
|
+
* try {
|
|
12
|
+
* await api.resources.articles.get({ id: 1 });
|
|
13
|
+
* } catch (error) {
|
|
14
|
+
* console.log(error.message);
|
|
15
|
+
* // "No storage implementation for get. Install a storage plugin."
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // After installing rest-api-knex-plugin:
|
|
20
|
+
* api.use(RestApiKnexPlugin, { knex });
|
|
21
|
+
* // Now data helpers are replaced with actual implementations
|
|
22
|
+
* const article = await api.resources.articles.get({ id: 1 });
|
|
23
|
+
* // Works! Returns article from database
|
|
24
|
+
*
|
|
25
|
+
* Used by:
|
|
26
|
+
* - rest-api-plugin sets these as initial data helpers
|
|
27
|
+
* - Storage plugins replace these with actual implementations
|
|
28
|
+
*
|
|
29
|
+
* Purpose:
|
|
30
|
+
* - Provides clear error messages when storage is missing
|
|
31
|
+
* - Ensures REST API plugin can be installed independently
|
|
32
|
+
* - Allows storage plugins to be swapped without changing API
|
|
33
|
+
*
|
|
34
|
+
* Data flow:
|
|
35
|
+
* 1. REST API plugin initializes with these defaults
|
|
36
|
+
* 2. User tries to use API without storage plugin
|
|
37
|
+
* 3. Clear error message guides them to install storage
|
|
38
|
+
* 4. Storage plugin replaces these with real implementations
|
|
39
|
+
*/
|
|
40
|
+
export const defaultDataHelpers = {
|
|
41
|
+
dataExists: async function(scope, deps) {
|
|
42
|
+
throw new Error(`No storage implementation for exists. Install a storage plugin.`);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
dataGet: async function(scope, deps) {
|
|
46
|
+
throw new Error(`No storage implementation for get. Install a storage plugin.`);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
dataGetMinimal: async function(scope, deps) {
|
|
50
|
+
throw new Error(`No storage implementation for getMinimal. Install a storage plugin.`);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
dataQuery: async function(scope, deps) {
|
|
54
|
+
throw new Error(`No storage implementation for query. Install a storage plugin.`);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
dataPost: async function(scope, deps) {
|
|
58
|
+
throw new Error(`No storage implementation for post. Install a storage plugin.`);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
dataPatch: async function(scope, deps) {
|
|
62
|
+
throw new Error(`No storage implementation for patch. Install a storage plugin.`);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
dataPut: async function(scope, deps) {
|
|
66
|
+
throw new Error(`No storage implementation for put. Install a storage plugin.`);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
dataDelete: async function(scope, deps) {
|
|
70
|
+
throw new Error(`No storage implementation for delete. Install a storage plugin.`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -1,45 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description Pure utility functions for field and schema manipulation
|
|
2
|
+
* Extracts all foreign key fields from a schema definition
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* that don't require scope context or dependencies. These functions can be
|
|
7
|
-
* used independently for schema inspection and field filtering.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Extracts all foreign key fields from a schema definition.
|
|
12
|
-
*
|
|
13
|
-
* This function identifies fields that represent foreign keys based on belongsTo
|
|
14
|
-
* relationships in the schema. These fields need special handling because they're
|
|
15
|
-
* stored in the database but not exposed as attributes in JSON:API responses.
|
|
16
|
-
* Instead, they're represented as relationships.
|
|
17
|
-
*
|
|
18
|
-
* @param {Object} schema - The schema definition (can be Schema object or plain object)
|
|
4
|
+
* @param {Object} schema - The schema definition (Schema object or plain object)
|
|
19
5
|
* @returns {Set<string>} Set of foreign key field names
|
|
20
6
|
*
|
|
21
|
-
* @example
|
|
7
|
+
* @example
|
|
8
|
+
* // Input: Schema with belongsTo relationships
|
|
22
9
|
* const schema = {
|
|
23
10
|
* title: { type: 'string' },
|
|
11
|
+
* content: { type: 'string' },
|
|
24
12
|
* author_id: { type: 'number', belongsTo: 'users', as: 'author' },
|
|
25
|
-
* category_id: { type: 'number', belongsTo: 'categories', as: 'category' }
|
|
13
|
+
* category_id: { type: 'number', belongsTo: 'categories', as: 'category' },
|
|
14
|
+
* status: { type: 'string' }
|
|
26
15
|
* };
|
|
16
|
+
*
|
|
27
17
|
* const foreignKeys = getForeignKeyFields(schema);
|
|
28
|
-
* // Returns Set(['author_id', 'category_id'])
|
|
29
18
|
*
|
|
30
|
-
*
|
|
31
|
-
* //
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
19
|
+
* // Output: Set containing only foreign key fields
|
|
20
|
+
* // Set(['author_id', 'category_id'])
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Input: Works with compiled Schema objects too
|
|
24
|
+
* const schemaObject = {
|
|
25
|
+
* structure: {
|
|
26
|
+
* post_id: { type: 'id', belongsTo: 'posts', as: 'post' },
|
|
27
|
+
* user_id: { type: 'id', belongsTo: 'users', as: 'user' }
|
|
28
|
+
* }
|
|
29
|
+
* };
|
|
30
|
+
*
|
|
31
|
+
* const foreignKeys = getForeignKeyFields(schemaObject);
|
|
32
|
+
*
|
|
33
|
+
* // Output: Handles both formats
|
|
34
|
+
* // Set(['post_id', 'user_id'])
|
|
35
|
+
*
|
|
36
|
+
* @description
|
|
37
|
+
* Used by:
|
|
38
|
+
* - toJsonApi to filter foreign keys from attributes
|
|
39
|
+
* - buildFieldSelection to ensure foreign keys are always selected
|
|
40
|
+
* - knex-json-api-helpers for field filtering
|
|
41
|
+
*
|
|
42
|
+
* Purpose:
|
|
43
|
+
* - Identifies fields that should be relationships, not attributes
|
|
44
|
+
* - Foreign keys are stored in DB but not exposed as JSON:API attributes
|
|
45
|
+
* - Ensures these fields are included in SELECT for relationship building
|
|
46
|
+
* - Helps transform database structure to JSON:API format
|
|
36
47
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* // 4. Optimize queries by knowing which fields are joins
|
|
48
|
+
* Data flow:
|
|
49
|
+
* 1. Accepts schema in either format (plain or with structure property)
|
|
50
|
+
* 2. Iterates through all field definitions
|
|
51
|
+
* 3. Collects fields with belongsTo property
|
|
52
|
+
* 4. Returns Set for efficient lookups
|
|
43
53
|
*/
|
|
44
54
|
export const getForeignKeyFields = (schema) => {
|
|
45
55
|
const foreignKeys = new Set();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export const RELATIONSHIPS_KEY = '__$jsonrestapi_relationships$__';
|
|
11
11
|
export const RELATIONSHIP_METADATA_KEY = '__$jsonrestapi_metadata$__';
|
|
12
12
|
export const ROW_NUMBER_KEY = '__$jsonrestapi_rn$__';
|
|
13
|
+
export const COMPUTED_DEPENDENCIES_KEY = '__$jsonrestapi_computed_deps$__';
|
|
13
14
|
|
|
14
15
|
// Default query limits
|
|
15
16
|
export const DEFAULT_QUERY_LIMIT = 20;
|
|
@@ -1,79 +1,151 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @description Context-dependent field helpers for Knex operations
|
|
2
|
+
* Checks if a field exists only in code, not in the database
|
|
4
3
|
*
|
|
5
|
-
* This module contains field helper functions that require scope context and
|
|
6
|
-
* dependencies. Pure utility functions have been moved to utils/field-utils.js.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { getForeignKeyFields, filterHiddenFields } from '../utils/field-utils.js';
|
|
10
|
-
|
|
11
|
-
// Re-export pure utility functions from field-utils
|
|
12
|
-
export { getForeignKeyFields, filterHiddenFields } from '../utils/field-utils.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Helper function to check if a field should be excluded from database operations
|
|
16
4
|
* @param {string} fieldName - The field name to check
|
|
17
|
-
* @param {Object} schemaInfo -
|
|
18
|
-
* @returns {boolean} True if
|
|
5
|
+
* @param {Object} schemaInfo - Schema info with computed fields and structure
|
|
6
|
+
* @returns {boolean} True if field is computed or virtual
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Input: Check computed field
|
|
10
|
+
* const schemaInfo = {
|
|
11
|
+
* computed: { profit_margin: { (definition) } },
|
|
12
|
+
* schemaStructure: { price: { type: 'number' } }
|
|
13
|
+
* };
|
|
14
|
+
* isNonDatabaseField('profit_margin', schemaInfo);
|
|
15
|
+
* // Output: true (it's computed, not stored)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Input: Check virtual field
|
|
19
|
+
* const schemaInfo = {
|
|
20
|
+
* computed: {},
|
|
21
|
+
* schemaStructure: {
|
|
22
|
+
* temp_password: { type: 'string', virtual: true }
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
* isNonDatabaseField('temp_password', schemaInfo);
|
|
26
|
+
* // Output: true (it's virtual, not stored)
|
|
27
|
+
*
|
|
28
|
+
* @description
|
|
29
|
+
* Used by:
|
|
30
|
+
* - buildFieldSelection to exclude from SELECT queries
|
|
31
|
+
* - dataPost/dataPatch to skip non-database fields
|
|
32
|
+
* - enrichAttributes to identify computed fields
|
|
33
|
+
*
|
|
34
|
+
* Purpose:
|
|
35
|
+
* - Prevents SQL errors by excluding non-existent columns
|
|
36
|
+
* - Identifies fields that need computation or special handling
|
|
37
|
+
* - Supports virtual fields for temporary/input-only data
|
|
19
38
|
*/
|
|
20
39
|
export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
21
|
-
const { computed = {},
|
|
22
|
-
|
|
40
|
+
const { computed = {}, schemaStructure = {} } = schemaInfo;
|
|
41
|
+
if (fieldName in computed) return true;
|
|
42
|
+
const fieldDef = schemaStructure[fieldName];
|
|
43
|
+
return fieldDef && fieldDef.virtual === true;
|
|
23
44
|
};
|
|
24
45
|
|
|
25
46
|
/**
|
|
26
|
-
* Builds the field selection list for database queries
|
|
27
|
-
*
|
|
28
|
-
* This function constructs the SELECT clause fields based on JSON:API sparse fieldsets
|
|
29
|
-
* while ensuring all necessary fields are included for proper relationship handling.
|
|
30
|
-
* It always includes ID and foreign key fields even if not explicitly requested,
|
|
31
|
-
* because these are needed for JSON:API relationship links.
|
|
47
|
+
* Builds the field selection list for database queries with sparse fieldset support
|
|
32
48
|
*
|
|
33
|
-
* @param {Object} scope -
|
|
49
|
+
* @param {Object} scope - Resource scope with schema information
|
|
34
50
|
* @param {Object} deps - Dependencies object
|
|
35
|
-
* @param {Object} deps.context - Request context
|
|
36
|
-
* @returns {Promise<Object>}
|
|
51
|
+
* @param {Object} deps.context - Request context with queryParams and schemaInfo
|
|
52
|
+
* @returns {Promise<Object>} Field selection information
|
|
37
53
|
*
|
|
38
|
-
* @example
|
|
39
|
-
*
|
|
54
|
+
* @example
|
|
55
|
+
* // Input: Basic sparse fieldset request
|
|
40
56
|
* const deps = {
|
|
41
57
|
* context: {
|
|
42
58
|
* scopeName: 'articles',
|
|
43
59
|
* queryParams: { fields: { articles: 'title,body' } },
|
|
44
|
-
* schemaInfo:
|
|
60
|
+
* schemaInfo: { idProperty: 'id', ... }
|
|
45
61
|
* }
|
|
46
62
|
* };
|
|
47
|
-
* const
|
|
48
|
-
*
|
|
63
|
+
* const result = await buildFieldSelection(scope, deps);
|
|
64
|
+
*
|
|
65
|
+
* // Output: Includes requested fields plus required fields
|
|
66
|
+
* // {
|
|
49
67
|
* // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
|
|
50
68
|
* // requestedFields: ['title', 'body'],
|
|
51
|
-
* // computedDependencies: []
|
|
69
|
+
* // computedDependencies: [],
|
|
70
|
+
* // idProperty: 'id'
|
|
52
71
|
* // }
|
|
72
|
+
* // Note: Foreign keys (author_id, category_id) always included for relationships
|
|
53
73
|
*
|
|
54
|
-
* @example
|
|
74
|
+
* @example
|
|
75
|
+
* // Input: Computed field with hidden dependencies
|
|
76
|
+
* const schemaInfo = {
|
|
77
|
+
* computed: {
|
|
78
|
+
* profit_margin: {
|
|
79
|
+
* compute: (record) => (record.price - record.cost) / record.price,
|
|
80
|
+
* dependencies: ['price', 'cost']
|
|
81
|
+
* }
|
|
82
|
+
* },
|
|
83
|
+
* schemaStructure: {
|
|
84
|
+
* name: { type: 'string' },
|
|
85
|
+
* price: { type: 'number' },
|
|
86
|
+
* cost: { type: 'number', normallyHidden: true }
|
|
87
|
+
* }
|
|
88
|
+
* };
|
|
55
89
|
* const deps = {
|
|
56
90
|
* context: {
|
|
57
|
-
*
|
|
58
|
-
* queryParams: { fields: { products: 'name,profit_margin' } },
|
|
59
|
-
* schemaInfo: scope.vars.schemaInfo
|
|
91
|
+
* queryParams: { fields: { products: 'name,profit_margin' } }
|
|
60
92
|
* }
|
|
61
93
|
* };
|
|
62
|
-
*
|
|
63
|
-
* //
|
|
64
|
-
* //
|
|
94
|
+
*
|
|
95
|
+
* // Output: Fetches hidden dependency for computation
|
|
96
|
+
* // {
|
|
97
|
+
* // fieldsToSelect: ['id', 'name', 'price', 'cost'],
|
|
65
98
|
* // requestedFields: ['name', 'profit_margin'],
|
|
66
|
-
* // computedDependencies: ['cost'] //
|
|
99
|
+
* // computedDependencies: ['cost'], // Will be removed after computation
|
|
100
|
+
* // idProperty: 'id'
|
|
101
|
+
* // }
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Input: No sparse fieldsets (returns all visible fields)
|
|
105
|
+
* const deps = {
|
|
106
|
+
* context: {
|
|
107
|
+
* queryParams: {}, // No fields parameter
|
|
108
|
+
* schemaInfo: { ... }
|
|
109
|
+
* }
|
|
110
|
+
* };
|
|
111
|
+
*
|
|
112
|
+
* // Output: All non-hidden, non-virtual fields
|
|
113
|
+
* // {
|
|
114
|
+
* // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
|
|
115
|
+
* // requestedFields: null,
|
|
116
|
+
* // computedDependencies: [],
|
|
117
|
+
* // idProperty: 'id'
|
|
67
118
|
* // }
|
|
119
|
+
*
|
|
120
|
+
* @description
|
|
121
|
+
* Used by:
|
|
122
|
+
* - dataGet to build SELECT query
|
|
123
|
+
* - dataQuery to build SELECT query
|
|
124
|
+
* - buildQuerySelection as the main field resolver
|
|
125
|
+
*
|
|
126
|
+
* Purpose:
|
|
127
|
+
* - Implements JSON:API sparse fieldsets specification
|
|
128
|
+
* - Ensures relationships work by including foreign keys
|
|
129
|
+
* - Handles computed field dependencies intelligently
|
|
130
|
+
* - Respects field visibility rules (hidden/normallyHidden)
|
|
131
|
+
* - Supports custom ID column names
|
|
132
|
+
*
|
|
133
|
+
* Data flow:
|
|
134
|
+
* 1. Always includes ID (with aliasing if needed)
|
|
135
|
+
* 2. Parses requested fields from query string
|
|
136
|
+
* 3. Validates requested fields exist in schema
|
|
137
|
+
* 4. Adds dependencies for computed fields
|
|
138
|
+
* 5. Includes all foreign keys for relationships
|
|
139
|
+
* 6. Returns complete field list for SQL SELECT
|
|
68
140
|
*/
|
|
69
141
|
export const buildFieldSelection = async (scope, deps) => {
|
|
70
142
|
let fieldsToSelect = new Set();
|
|
71
143
|
let computedDependencies = new Set();
|
|
72
|
-
|
|
144
|
+
|
|
73
145
|
// Extract values from scope
|
|
74
146
|
const {
|
|
75
147
|
vars: {
|
|
76
|
-
schemaInfo: { schema, computed: computedFields = {},
|
|
148
|
+
schemaInfo: { schema, computed: computedFields = {}, schemaStructure }
|
|
77
149
|
}
|
|
78
150
|
} = scope;
|
|
79
151
|
|
|
@@ -84,17 +156,30 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
84
156
|
const idProperty = context.schemaInfo.idProperty;
|
|
85
157
|
|
|
86
158
|
// Always include the ID field - required for JSON:API
|
|
87
|
-
|
|
159
|
+
// Handle aliasing if idProperty is not 'id'
|
|
160
|
+
if (idProperty !== 'id') {
|
|
161
|
+
fieldsToSelect.add(`${idProperty} as id`);
|
|
162
|
+
} else {
|
|
163
|
+
fieldsToSelect.add('id');
|
|
164
|
+
}
|
|
88
165
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
166
|
+
// Handle both Schema objects and plain objects
|
|
167
|
+
if (!schemaStructure) {
|
|
168
|
+
schemaStructure = schema?.structure || schema || {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get computed fields and virtual fields from schema
|
|
92
172
|
const computedFieldNames = new Set(Object.keys(computedFields));
|
|
93
|
-
const virtualFieldNames = new Set(Object.keys(virtualFields));
|
|
94
|
-
const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames]);
|
|
95
173
|
|
|
96
|
-
//
|
|
97
|
-
const
|
|
174
|
+
// Find fields marked as virtual in the schema
|
|
175
|
+
const virtualFieldNames = new Set();
|
|
176
|
+
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
177
|
+
if (fieldDef.virtual === true) {
|
|
178
|
+
virtualFieldNames.add(fieldName);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames]);
|
|
98
183
|
|
|
99
184
|
// Parse requested fields
|
|
100
185
|
const requested = requestedFields ? (
|
|
@@ -113,7 +198,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
113
198
|
if (nonDatabaseFields.has(field)) return;
|
|
114
199
|
|
|
115
200
|
const fieldDef = schemaStructure[field];
|
|
116
|
-
if (!fieldDef)
|
|
201
|
+
if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`);
|
|
117
202
|
|
|
118
203
|
// NEVER include hidden fields, even if explicitly requested
|
|
119
204
|
// Example: password_hash with hidden:true is never returned
|
|
@@ -162,6 +247,9 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
162
247
|
// No sparse fieldsets - return all visible fields
|
|
163
248
|
// This is the default behavior when no ?fields parameter is provided
|
|
164
249
|
Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
|
|
250
|
+
// Skip virtual fields - they don't exist in database
|
|
251
|
+
if (fieldDef.virtual === true) return;
|
|
252
|
+
|
|
165
253
|
// Skip hidden fields - these are NEVER returned
|
|
166
254
|
// Example: password_hash with hidden:true
|
|
167
255
|
if (fieldDef.hidden === true) return;
|
|
@@ -222,17 +310,62 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
222
310
|
return {
|
|
223
311
|
fieldsToSelect: Array.from(fieldsToSelect), // Fields to SELECT from database
|
|
224
312
|
requestedFields: requested, // Fields explicitly requested by user
|
|
225
|
-
computedDependencies: Array.from(computedDependencies) // Dependencies to remove from response
|
|
313
|
+
computedDependencies: Array.from(computedDependencies), // Dependencies to remove from response
|
|
314
|
+
idProperty // Pass idProperty for reference
|
|
226
315
|
};
|
|
227
316
|
};
|
|
228
317
|
|
|
229
318
|
/**
|
|
230
|
-
*
|
|
319
|
+
* Determines which computed fields to calculate based on sparse fieldsets
|
|
320
|
+
*
|
|
321
|
+
* @param {string} scopeName - Resource name (e.g., 'products')
|
|
322
|
+
* @param {Array<string>|string} requestedFields - Requested fields from query
|
|
323
|
+
* @param {Object} computedFields - Computed field definitions from schema
|
|
324
|
+
* @returns {Array<string>} Names of computed fields to calculate
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // Input: No sparse fieldsets (calculate all computed fields)
|
|
328
|
+
* const computed = {
|
|
329
|
+
* full_name: { compute: (r) => `${r.first} ${r.last}` },
|
|
330
|
+
* age: { compute: (r) => new Date().getFullYear() - r.birth_year }
|
|
331
|
+
* };
|
|
332
|
+
* getRequestedComputedFields('users', null, computed);
|
|
333
|
+
* // Output: ['full_name', 'age'] (all computed fields)
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* // Input: Sparse fieldsets with some computed fields
|
|
337
|
+
* const requestedFields = 'first_name,full_name,email';
|
|
338
|
+
* const computed = {
|
|
339
|
+
* full_name: { compute: (r) => `${r.first} ${r.last}` },
|
|
340
|
+
* age: { compute: (r) => new Date().getFullYear() - r.birth_year }
|
|
341
|
+
* };
|
|
342
|
+
* getRequestedComputedFields('users', requestedFields, computed);
|
|
343
|
+
* // Output: ['full_name'] (only requested computed field)
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* // Input: Sparse fieldsets with no computed fields
|
|
347
|
+
* const requestedFields = ['first_name', 'email'];
|
|
348
|
+
* const computed = {
|
|
349
|
+
* full_name: { compute: (r) => `${r.first} ${r.last}` }
|
|
350
|
+
* };
|
|
351
|
+
* getRequestedComputedFields('users', requestedFields, computed);
|
|
352
|
+
* // Output: [] (no computed fields requested)
|
|
353
|
+
*
|
|
354
|
+
* @description
|
|
355
|
+
* Used by:
|
|
356
|
+
* - enrichAttributes to determine which computations to run
|
|
357
|
+
* - Applied after fetching data from database
|
|
358
|
+
*
|
|
359
|
+
* Purpose:
|
|
360
|
+
* - Optimizes performance by only computing requested fields
|
|
361
|
+
* - Supports JSON:API sparse fieldsets for computed fields
|
|
362
|
+
* - Handles both string and array input formats
|
|
231
363
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
364
|
+
* Data flow:
|
|
365
|
+
* 1. Checks if sparse fieldsets are specified
|
|
366
|
+
* 2. If not, returns all computed field names
|
|
367
|
+
* 3. If yes, filters to only requested computed fields
|
|
368
|
+
* 4. Normalizes string input to array format
|
|
236
369
|
*/
|
|
237
370
|
export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
|
|
238
371
|
if (!computedFields) return [];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { toJsonApiRecord } from '../querying/knex-json-api-transformers-querying.js';
|
|
2
|
+
import { getSchemaStructure } from './knex-constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts database record to JSON:API format with belongsTo relationships
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} scope - Resource scope with schema and relationship info
|
|
8
|
+
* @param {Object} record - Raw database record
|
|
9
|
+
* @param {string} scopeName - Resource type name
|
|
10
|
+
* @returns {Object} JSON:API resource with belongsTo relationships
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Input: Database record with foreign keys
|
|
14
|
+
* const dbRecord = {
|
|
15
|
+
* id: 1,
|
|
16
|
+
* title: 'Hello World',
|
|
17
|
+
* author_id: 2,
|
|
18
|
+
* publisher_id: 3,
|
|
19
|
+
* category_id: null
|
|
20
|
+
* };
|
|
21
|
+
* const result = toJsonApiRecordWithBelongsTo(scope, dbRecord, 'articles');
|
|
22
|
+
*
|
|
23
|
+
* // Output: JSON:API with relationship objects
|
|
24
|
+
* // {
|
|
25
|
+
* // type: 'articles',
|
|
26
|
+
* // id: '1',
|
|
27
|
+
* // attributes: {
|
|
28
|
+
* // title: 'Hello World'
|
|
29
|
+
* // // Note: author_id, publisher_id, category_id removed
|
|
30
|
+
* // },
|
|
31
|
+
* // relationships: {
|
|
32
|
+
* // author: {
|
|
33
|
+
* // data: { type: 'authors', id: '2' }
|
|
34
|
+
* // },
|
|
35
|
+
* // publisher: {
|
|
36
|
+
* // data: { type: 'publishers', id: '3' }
|
|
37
|
+
* // },
|
|
38
|
+
* // category: {
|
|
39
|
+
* // data: null // Explicit null for empty relationship
|
|
40
|
+
* // }
|
|
41
|
+
* // }
|
|
42
|
+
* // }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Input: Polymorphic relationships
|
|
46
|
+
* const dbRecord = {
|
|
47
|
+
* id: 5,
|
|
48
|
+
* content: 'Great post!',
|
|
49
|
+
* commentable_type: 'articles',
|
|
50
|
+
* commentable_id: 10
|
|
51
|
+
* };
|
|
52
|
+
* // With schema defining polymorphic relationship:
|
|
53
|
+
* // relationships: {
|
|
54
|
+
* // commentable: {
|
|
55
|
+
* // belongsToPolymorphic: true,
|
|
56
|
+
* // typeField: 'commentable_type',
|
|
57
|
+
* // idField: 'commentable_id'
|
|
58
|
+
* // }
|
|
59
|
+
* // }
|
|
60
|
+
*
|
|
61
|
+
* // Output: Polymorphic relationship in JSON:API
|
|
62
|
+
* // {
|
|
63
|
+
* // type: 'comments',
|
|
64
|
+
* // id: '5',
|
|
65
|
+
* // attributes: { content: 'Great post!' },
|
|
66
|
+
* // relationships: {
|
|
67
|
+
* // commentable: {
|
|
68
|
+
* // data: { type: 'articles', id: '10' }
|
|
69
|
+
* // }
|
|
70
|
+
* // }
|
|
71
|
+
* // }
|
|
72
|
+
*
|
|
73
|
+
* @description
|
|
74
|
+
* Used by:
|
|
75
|
+
* - dataGetMinimal for lightweight single record fetches
|
|
76
|
+
* - dataDelete to return deleted record structure
|
|
77
|
+
* - Internal operations needing quick JSON:API conversion
|
|
78
|
+
*
|
|
79
|
+
* Purpose:
|
|
80
|
+
* - Provides JSON:API structure without loading related data
|
|
81
|
+
* - Transforms foreign keys into relationship objects
|
|
82
|
+
* - Handles both regular and polymorphic belongsTo
|
|
83
|
+
* - Maintains explicit nulls for empty relationships
|
|
84
|
+
*
|
|
85
|
+
* Data flow:
|
|
86
|
+
* 1. Calls toJsonApiRecord for basic transformation
|
|
87
|
+
* 2. Scans schema for belongsTo field definitions
|
|
88
|
+
* 3. Converts foreign key values to relationship objects
|
|
89
|
+
* 4. Processes polymorphic relationships separately
|
|
90
|
+
* 5. Returns complete JSON:API resource object
|
|
91
|
+
*/
|
|
92
|
+
export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
|
|
93
|
+
if (!record) return null;
|
|
94
|
+
|
|
95
|
+
// Get the basic JSON:API structure (without relationships)
|
|
96
|
+
const jsonApiRecord = toJsonApiRecord(scope, record, scopeName);
|
|
97
|
+
|
|
98
|
+
// Extract schema info from scope
|
|
99
|
+
const {
|
|
100
|
+
vars: {
|
|
101
|
+
schemaInfo: { schema, schemaRelationships: relationships, idProperty }
|
|
102
|
+
}
|
|
103
|
+
} = scope;
|
|
104
|
+
|
|
105
|
+
const idField = idProperty || 'id';
|
|
106
|
+
|
|
107
|
+
// Get schema structure
|
|
108
|
+
const schemaStructure = getSchemaStructure(schema);
|
|
109
|
+
|
|
110
|
+
// Initialize relationships object
|
|
111
|
+
jsonApiRecord.relationships = {};
|
|
112
|
+
|
|
113
|
+
// Process regular belongsTo relationships from schema
|
|
114
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
115
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
116
|
+
const foreignKeyValue = record[fieldName];
|
|
117
|
+
|
|
118
|
+
if (foreignKeyValue !== null && foreignKeyValue !== undefined) {
|
|
119
|
+
jsonApiRecord.relationships[fieldDef.as] = {
|
|
120
|
+
data: {
|
|
121
|
+
type: fieldDef.belongsTo,
|
|
122
|
+
id: String(foreignKeyValue)
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} else {
|
|
126
|
+
// Explicitly null relationship
|
|
127
|
+
jsonApiRecord.relationships[fieldDef.as] = {
|
|
128
|
+
data: null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Process polymorphic belongsTo relationships
|
|
135
|
+
Object.entries(relationships || {}).forEach(([relName, relDef]) => {
|
|
136
|
+
if (relDef.belongsToPolymorphic) {
|
|
137
|
+
const typeValue = record[relDef.typeField];
|
|
138
|
+
const idValue = record[relDef.idField];
|
|
139
|
+
|
|
140
|
+
if (typeValue && idValue) {
|
|
141
|
+
jsonApiRecord.relationships[relName] = {
|
|
142
|
+
data: {
|
|
143
|
+
type: typeValue,
|
|
144
|
+
id: String(idValue)
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
// Explicitly null relationship
|
|
149
|
+
jsonApiRecord.relationships[relName] = {
|
|
150
|
+
data: null
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Remove relationships object if empty
|
|
157
|
+
if (Object.keys(jsonApiRecord.relationships).length === 0) {
|
|
158
|
+
delete jsonApiRecord.relationships;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return jsonApiRecord;
|
|
162
|
+
};
|