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
@@ -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
- * @module field-utils
3
- * @description Pure utility functions for field and schema manipulation
2
+ * Extracts all foreign key fields from a schema definition
4
3
  *
5
- * This module contains pure functions extracted from knex-field-helpers.js
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 <caption>Basic foreign key extraction</caption>
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
- * @example <caption>Schema object vs plain object</caption>
31
- * // Works with both compiled Schema objects and plain schema definitions
32
- * const schemaObject = createSchema(schema);
33
- * const foreignKeys1 = getForeignKeyFields(schemaObject); // Schema object
34
- * const foreignKeys2 = getForeignKeyFields(schema); // Plain object
35
- * // Both return the same Set
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
- * @example <caption>Why this is useful upstream</caption>
38
- * // The REST API Knex plugin uses this to:
39
- * // 1. Filter out foreign keys when building JSON:API attributes
40
- * // 2. Include foreign keys in SELECT even with sparse fieldsets
41
- * // 3. Identify which fields need relationship transformation
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
- * @module knex-field-helpers
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 - The schema info object containing computed and virtual fields
18
- * @returns {boolean} True if the field is non-database (computed or virtual)
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 = {}, virtual = {} } = schemaInfo;
22
- return fieldName in computed || fieldName in virtual;
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, handling sparse fieldsets.
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 - The scope object containing schema and configuration
49
+ * @param {Object} scope - Resource scope with schema information
34
50
  * @param {Object} deps - Dependencies object
35
- * @param {Object} deps.context - Request context containing queryParams and schemaInfo
36
- * @returns {Promise<Object>} Object with fieldsToSelect, requestedFields, and computedDependencies
51
+ * @param {Object} deps.context - Request context with queryParams and schemaInfo
52
+ * @returns {Promise<Object>} Field selection information
37
53
  *
38
- * @example <caption>Sparse fieldset with automatic foreign key inclusion</caption>
39
- * const scope = api.resources['articles'];
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: scope.vars.schemaInfo
60
+ * schemaInfo: { idProperty: 'id', ... }
45
61
  * }
46
62
  * };
47
- * const fieldInfo = await buildFieldSelection(scope, deps);
48
- * // Returns {
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 <caption>With computed fields and dependencies</caption>
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
- * scopeName: 'products',
58
- * queryParams: { fields: { products: 'name,profit_margin' } },
59
- * schemaInfo: scope.vars.schemaInfo
91
+ * queryParams: { fields: { products: 'name,profit_margin' } }
60
92
  * }
61
93
  * };
62
- * const fieldInfo = await buildFieldSelection(scope, deps);
63
- * // Returns {
64
- * // fieldsToSelect: ['id', 'name', 'price', 'cost'], // cost needed for profit_margin
94
+ *
95
+ * // Output: Fetches hidden dependency for computation
96
+ * // {
97
+ * // fieldsToSelect: ['id', 'name', 'price', 'cost'],
65
98
  * // requestedFields: ['name', 'profit_margin'],
66
- * // computedDependencies: ['cost'] // will be removed from final response
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 = {}, virtual: virtualFields = {} }
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
- fieldsToSelect.add(idProperty);
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
- // Get computed and virtual fields - these are not stored in the database
90
- // Computed: { profit_margin: { type: 'number', dependencies: ['price', 'cost'], compute: ... } }
91
- // Virtual: { passwordConfirmation: { type: 'string' } }
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
- // Handle both Schema objects and plain objects
97
- const schemaStructure = schema?.structure || schema || {};
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) return; // Unknown field, skip
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
- * Gets the list of requested computed fields based on sparse fieldsets.
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
- * @param {string} scopeName - The name of the scope/resource
233
- * @param {Array<string>|string} requestedFields - Array or comma-separated string of requested fields
234
- * @param {Object} computedFields - Object containing computed field definitions
235
- * @returns {Array<string>} Array of computed field names to calculate
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
+ };