json-rest-api 1.0.21 → 1.0.22

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 (173) hide show
  1. package/audit/00-charter.md +103 -0
  2. package/audit/01-scope-map.md +84 -0
  3. package/audit/02-coverage-matrix.csv +174 -0
  4. package/audit/03-invariants.md +123 -0
  5. package/audit/04-standards-rubric.md +56 -0
  6. package/audit/05-competitor-rubric.md +77 -0
  7. package/audit/06-findings.md +160 -0
  8. package/audit/07-risk-register.csv +10 -0
  9. package/audit/08-remediation-roadmap.md +177 -0
  10. package/audit/09-signoff-check.md +23 -0
  11. package/audit/10-final-synthesis.md +29 -0
  12. package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
  13. package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
  14. package/audit/evidence/code/boundary-leak-checks.txt +72 -0
  15. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
  16. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
  17. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
  18. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
  19. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
  20. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
  21. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
  22. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
  23. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
  24. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
  25. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
  26. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
  27. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
  28. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
  29. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
  30. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
  31. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
  32. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
  33. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
  34. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
  35. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
  36. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
  37. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
  38. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
  39. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
  40. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
  41. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
  42. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
  43. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
  44. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
  45. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
  46. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
  47. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
  48. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
  49. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
  50. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
  51. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
  52. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
  53. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
  54. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
  55. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
  56. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
  57. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
  58. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
  59. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
  60. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
  61. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
  62. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
  63. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
  64. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
  65. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
  66. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
  67. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
  68. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
  69. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
  70. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
  71. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
  72. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
  73. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
  74. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
  75. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
  76. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
  77. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
  78. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
  79. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
  80. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
  81. package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
  82. package/audit/evidence/code/critical-pass1-summary.csv +67 -0
  83. package/audit/evidence/code/critical-pass2-summary.csv +67 -0
  84. package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
  85. package/audit/evidence/code/error-model-signals.txt +98 -0
  86. package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
  87. package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
  88. package/audit/evidence/code/hook-flow-signals.txt +282 -0
  89. package/audit/evidence/code/idproperty-signals.txt +157 -0
  90. package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
  91. package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
  92. package/audit/evidence/code/imports-json-rest-api.txt +354 -0
  93. package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
  94. package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
  95. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
  96. package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
  97. package/audit/evidence/code/permission-signals.txt +193 -0
  98. package/audit/evidence/code/query-include-signals.txt +687 -0
  99. package/audit/evidence/code/transaction-signals.txt +28 -0
  100. package/audit/evidence/code/validation-signals.txt +650 -0
  101. package/audit/evidence/code/write-path-signals.txt +61 -0
  102. package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
  103. package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
  104. package/audit/evidence/external/corpus-freeze.md +21 -0
  105. package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
  106. package/audit/evidence/external/npm-download-signals.txt +23 -0
  107. package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
  108. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
  109. package/audit/evidence/external/readme-fortune-signals.txt +26 -0
  110. package/audit/evidence/external/readme-fortune.md +165 -0
  111. package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
  112. package/audit/evidence/external/readme-json-api-serializer.md +690 -0
  113. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  114. package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
  115. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
  116. package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
  117. package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
  118. package/audit/evidence/external/readme-jsonapi-server.md +104 -0
  119. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
  120. package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
  121. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
  122. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
  123. package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
  124. package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
  125. package/audit/evidence/external/standards-sources.txt +6 -0
  126. package/audit/evidence/runtime/baseline-commits.txt +2 -0
  127. package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
  128. package/audit/evidence/runtime/environment.txt +4 -0
  129. package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
  130. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
  131. package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
  132. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
  133. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
  134. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
  135. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
  136. package/audit/evidence/runtime/test-exitcodes.txt +4 -0
  137. package/audit/evidence/runtime/test-summary.md +107 -0
  138. package/audit/evidence/runtime/trace-anchors.txt +61 -0
  139. package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
  140. package/audit/evidence/tests/failing-tests-summary.txt +25 -0
  141. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
  142. package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
  143. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
  144. package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
  145. package/audit/evidence/tests/query-include-test-signals.txt +948 -0
  146. package/audit/evidence/tests/relationship-test-signals.txt +223 -0
  147. package/audit/evidence/tests/skipped-tests.txt +10 -0
  148. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
  149. package/audit/evidence/tests/test-suite-list.txt +32 -0
  150. package/audit/subsystems/docs-drift.md +49 -0
  151. package/audit/subsystems/pass2-cross-review.md +131 -0
  152. package/audit/subsystems/plugins-access.md +51 -0
  153. package/audit/subsystems/plugins-bulk.md +50 -0
  154. package/audit/subsystems/plugins-cors.md +48 -0
  155. package/audit/subsystems/plugins-file.md +53 -0
  156. package/audit/subsystems/plugins-multihome.md +50 -0
  157. package/audit/subsystems/plugins-positioning.md +54 -0
  158. package/audit/subsystems/plugins-socketio.md +50 -0
  159. package/audit/subsystems/query-engine.md +61 -0
  160. package/audit/subsystems/relationships.md +55 -0
  161. package/audit/subsystems/rest-orchestration.md +66 -0
  162. package/audit/subsystems/schema-core.md +64 -0
  163. package/audit/subsystems/storage-knex.md +57 -0
  164. package/audit/subsystems/tests.md +48 -0
  165. package/audit/subsystems/transport-express.md +55 -0
  166. package/audit/traces/trace-read-get.md +22 -0
  167. package/audit/traces/trace-read-query.md +25 -0
  168. package/audit/traces/trace-relationship-endpoints.md +31 -0
  169. package/audit/traces/trace-write-delete.md +17 -0
  170. package/audit/traces/trace-write-patch.md +26 -0
  171. package/audit/traces/trace-write-post.md +22 -0
  172. package/audit/traces/trace-write-put.md +23 -0
  173. package/package.json +3 -3
@@ -0,0 +1,72 @@
1
+ 1 /**
2
+ 2 * Default data helper implementations that instruct users to install a storage plugin
3
+ 3 *
4
+ 4 * @description
5
+ 5 * These default implementations ensure the REST API plugin fails gracefully
6
+ 6 * when no storage plugin is installed. Each method throws a descriptive error
7
+ 7 * guiding users to install a storage implementation.
8
+ 8 *
9
+ 9 * @example
10
+ 10 * // When no storage plugin is installed:
11
+ 11 * try {
12
+ 12 * await api.resources.articles.get({ id: 1 });
13
+ 13 * } catch (error) {
14
+ 14 * console.log(error.message);
15
+ 15 * // "No storage implementation for get. Install a storage plugin."
16
+ 16 * }
17
+ 17 *
18
+ 18 * @example
19
+ 19 * // After installing rest-api-knex-plugin:
20
+ 20 * api.use(RestApiKnexPlugin, { knex });
21
+ 21 * // Now data helpers are replaced with actual implementations
22
+ 22 * const article = await api.resources.articles.get({ id: 1 });
23
+ 23 * // Works! Returns article from database
24
+ 24 *
25
+ 25 * Used by:
26
+ 26 * - rest-api-plugin sets these as initial data helpers
27
+ 27 * - Storage plugins replace these with actual implementations
28
+ 28 *
29
+ 29 * Purpose:
30
+ 30 * - Provides clear error messages when storage is missing
31
+ 31 * - Ensures REST API plugin can be installed independently
32
+ 32 * - Allows storage plugins to be swapped without changing API
33
+ 33 *
34
+ 34 * Data flow:
35
+ 35 * 1. REST API plugin initializes with these defaults
36
+ 36 * 2. User tries to use API without storage plugin
37
+ 37 * 3. Clear error message guides them to install storage
38
+ 38 * 4. Storage plugin replaces these with real implementations
39
+ 39 */
40
+ 40 export const defaultDataHelpers = {
41
+ 41 dataExists: async function (scope, deps) {
42
+ 42 throw new Error('No storage implementation for exists. Install a storage plugin.')
43
+ 43 },
44
+ 44
45
+ 45 dataGet: async function (scope, deps) {
46
+ 46 throw new Error('No storage implementation for get. Install a storage plugin.')
47
+ 47 },
48
+ 48
49
+ 49 dataGetMinimal: async function (scope, deps) {
50
+ 50 throw new Error('No storage implementation for getMinimal. Install a storage plugin.')
51
+ 51 },
52
+ 52
53
+ 53 dataQuery: async function (scope, deps) {
54
+ 54 throw new Error('No storage implementation for query. Install a storage plugin.')
55
+ 55 },
56
+ 56
57
+ 57 dataPost: async function (scope, deps) {
58
+ 58 throw new Error('No storage implementation for post. Install a storage plugin.')
59
+ 59 },
60
+ 60
61
+ 61 dataPatch: async function (scope, deps) {
62
+ 62 throw new Error('No storage implementation for patch. Install a storage plugin.')
63
+ 63 },
64
+ 64
65
+ 65 dataPut: async function (scope, deps) {
66
+ 66 throw new Error('No storage implementation for put. Install a storage plugin.')
67
+ 67 },
68
+ 68
69
+ 69 dataDelete: async function (scope, deps) {
70
+ 70 throw new Error('No storage implementation for delete. Install a storage plugin.')
71
+ 71 }
72
+ 72 }
@@ -0,0 +1,139 @@
1
+ 1 /**
2
+ 2 * Extracts all foreign key fields from a schema definition
3
+ 3 *
4
+ 4 * @param {Object} schema - The schema definition (Schema object or plain object)
5
+ 5 * @returns {Set<string>} Set of foreign key field names
6
+ 6 *
7
+ 7 * @example
8
+ 8 * // Input: Schema with belongsTo relationships
9
+ 9 * const schema = {
10
+ 10 * title: { type: 'string' },
11
+ 11 * content: { type: 'string' },
12
+ 12 * author_id: { type: 'number', belongsTo: 'users', as: 'author' },
13
+ 13 * category_id: { type: 'number', belongsTo: 'categories', as: 'category' },
14
+ 14 * status: { type: 'string' }
15
+ 15 * };
16
+ 16 *
17
+ 17 * const foreignKeys = getForeignKeyFields(schema);
18
+ 18 *
19
+ 19 * // Output: Set containing only foreign key fields
20
+ 20 * // Set(['author_id', 'category_id'])
21
+ 21 *
22
+ 22 * @example
23
+ 23 * // Input: Works with compiled Schema objects too
24
+ 24 * const schemaObject = {
25
+ 25 * structure: {
26
+ 26 * post_id: { type: 'id', belongsTo: 'posts', as: 'post' },
27
+ 27 * user_id: { type: 'id', belongsTo: 'users', as: 'user' }
28
+ 28 * }
29
+ 29 * };
30
+ 30 *
31
+ 31 * const foreignKeys = getForeignKeyFields(schemaObject);
32
+ 32 *
33
+ 33 * // Output: Handles both formats
34
+ 34 * // Set(['post_id', 'user_id'])
35
+ 35 *
36
+ 36 * @description
37
+ 37 * Used by:
38
+ 38 * - toJsonApi to filter foreign keys from attributes
39
+ 39 * - buildFieldSelection to ensure foreign keys are always selected
40
+ 40 * - knex-json-api-helpers for field filtering
41
+ 41 *
42
+ 42 * Purpose:
43
+ 43 * - Identifies fields that should be relationships, not attributes
44
+ 44 * - Foreign keys are stored in DB but not exposed as JSON:API attributes
45
+ 45 * - Ensures these fields are included in SELECT for relationship building
46
+ 46 * - Helps transform database structure to JSON:API format
47
+ 47 *
48
+ 48 * Data flow:
49
+ 49 * 1. Accepts schema in either format (plain or with structure property)
50
+ 50 * 2. Iterates through all field definitions
51
+ 51 * 3. Collects fields with belongsTo property
52
+ 52 * 4. Returns Set for efficient lookups
53
+ 53 */
54
+ 54 export const getForeignKeyFields = (schema) => {
55
+ 55 const foreignKeys = new Set()
56
+ 56 if (!schema) return foreignKeys
57
+ 57
58
+ 58 // Handle both Schema objects and plain objects
59
+ 59 const schemaStructure = schema.structure || schema
60
+ 60
61
+ 61 Object.entries(schemaStructure).forEach(([field, def]) => {
62
+ 62 if (def.belongsTo) {
63
+ 63 foreignKeys.add(field)
64
+ 64 }
65
+ 65 })
66
+ 66 return foreignKeys
67
+ 67 }
68
+ 68
69
+ 69 /**
70
+ 70 * Filters hidden fields from attributes based on schema rules.
71
+ 71 *
72
+ 72 * This pure function removes fields marked as hidden or normallyHidden
73
+ 73 * based on the schema definition and requested fields. It ensures that
74
+ 74 * sensitive data is never exposed in API responses.
75
+ 75 *
76
+ 76 * @param {Object} attributes - The attributes object to filter
77
+ 77 * @param {Object} schema - The schema object with structure property
78
+ 78 * @param {Array<string>|string} requestedFields - Fields explicitly requested (for normallyHidden)
79
+ 79 * @returns {Object} Filtered attributes object
80
+ 80 *
81
+ 81 * @example <caption>Filtering hidden fields</caption>
82
+ 82 * const attributes = {
83
+ 83 * name: 'John',
84
+ 84 * email: 'john@example.com',
85
+ 85 * password_hash: 'xxx',
86
+ 86 * internal_id: '123'
87
+ 87 * };
88
+ 88 * const schema = {
89
+ 89 * structure: {
90
+ 90 * name: { type: 'string' },
91
+ 91 * email: { type: 'string' },
92
+ 92 * password_hash: { type: 'string', hidden: true },
93
+ 93 * internal_id: { type: 'string', normallyHidden: true }
94
+ 94 * }
95
+ 95 * };
96
+ 96 * const filtered = filterHiddenFields(attributes, schema, null);
97
+ 97 * // Returns: { name: 'John', email: 'john@example.com' }
98
+ 98 * // password_hash (hidden) and internal_id (normallyHidden) are removed
99
+ 99 *
100
+ 100 * @example <caption>Including normallyHidden fields when requested</caption>
101
+ 101 * const filtered = filterHiddenFields(attributes, schema, 'name,internal_id');
102
+ 102 * // Returns: { name: 'John', internal_id: '123' }
103
+ 103 * // internal_id is included because explicitly requested
104
+ 104 * // password_hash is still filtered (always hidden)
105
+ 105 */
106
+ 106 export const filterHiddenFields = (attributes, schema, requestedFields) => {
107
+ 107 const filtered = {}
108
+ 108
109
+ 109 // Parse requested fields if it's a string (from query params)
110
+ 110 // Example: "name,price,cost" -> ['name', 'price', 'cost']
111
+ 111 const requested = requestedFields
112
+ 112 ? (
113
+ 113 typeof requestedFields === 'string'
114
+ 114 ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
115
+ 115 : requestedFields
116
+ 116 )
117
+ 117 : null
118
+ 118
119
+ 119 Object.entries(attributes).forEach(([field, value]) => {
120
+ 120 const fieldDef = schema.structure?.[field]
121
+ 121
122
+ 122 // Never include hidden fields - these are completely invisible
123
+ 123 // Example: password_hash with hidden:true is always filtered out
124
+ 124 if (fieldDef?.hidden === true) return
125
+ 125
126
+ 126 // Include normallyHidden fields only if explicitly requested
127
+ 127 // Example: 'cost' with normallyHidden:true is only included if user requests it
128
+ 128 // via sparse fieldsets like ?fields[products]=name,cost
129
+ 129 if (fieldDef?.normallyHidden === true) {
130
+ 130 if (!requested || !requested.includes(field)) {
131
+ 131 return // Filter out normallyHidden field
132
+ 132 }
133
+ 133 }
134
+ 134
135
+ 135 filtered[field] = value
136
+ 136 })
137
+ 137
138
+ 138 return filtered
139
+ 139 }
@@ -0,0 +1,45 @@
1
+ 1 /**
2
+ 2 * @module knex-constants
3
+ 3 * @description Shared constants for the REST API Knex plugin
4
+ 4 *
5
+ 5 * This module contains all the magic strings and shared constants used
6
+ 6 * throughout the plugin to avoid repetition and improve maintainability.
7
+ 7 */
8
+ 8
9
+ 9 // Internal property names - using unique names to avoid collisions with user data
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
+ 13 export const COMPUTED_DEPENDENCIES_KEY = '__$jsonrestapi_computed_deps$__'
14
+ 14
15
+ 15 // Default query limits
16
+ 16 export const DEFAULT_QUERY_LIMIT = 20
17
+ 17 export const DEFAULT_MAX_QUERY_LIMIT = 100
18
+ 18 export const DEFAULT_MAX_INCLUDE_LIMIT = 1000
19
+ 19 export const DEFAULT_INCLUDE_DEPTH_LIMIT = 3
20
+ 20
21
+ 21 // Schema handling helpers
22
+ 22 export const getSchemaStructure = (schema) => schema?.structure || schema || {}
23
+ 23 export const getIdProperty = (schemaInfo) => schemaInfo?.idProperty || 'id'
24
+ 24
25
+ 25 // Default values
26
+ 26 export const DEFAULT_PAGE_SIZE = 20
27
+ 27 export const DEFAULT_MAX_PAGE_SIZE = 100
28
+ 28 export const DEFAULT_PAGE_NUMBER = 1
29
+ 29
30
+ 30 // Database clients
31
+ 31 export const DB_CLIENTS = {
32
+ 32 POSTGRES: 'pg',
33
+ 33 MYSQL: 'mysql',
34
+ 34 MYSQL2: 'mysql2',
35
+ 35 SQLITE: 'sqlite3',
36
+ 36 MSSQL: 'mssql'
37
+ 37 }
38
+ 38
39
+ 39 // Error subtypes
40
+ 40 export const ERROR_SUBTYPES = {
41
+ 41 NOT_FOUND: 'not_found',
42
+ 42 VALIDATION: 'validation',
43
+ 43 PERMISSION: 'permission',
44
+ 44 CONFLICT: 'conflict'
45
+ 45 }
@@ -0,0 +1,396 @@
1
+ 1 /**
2
+ 2 * Checks if a field exists only in code, not in the database
3
+ 3 *
4
+ 4 * @param {string} fieldName - The field name to check
5
+ 5 * @param {Object} schemaInfo - Schema info with computed fields and structure
6
+ 6 * @returns {boolean} True if field is computed or virtual
7
+ 7 *
8
+ 8 * @example
9
+ 9 * // Input: Check computed field
10
+ 10 * const schemaInfo = {
11
+ 11 * computed: { profit_margin: { (definition) } },
12
+ 12 * schemaStructure: { price: { type: 'number' } }
13
+ 13 * };
14
+ 14 * isNonDatabaseField('profit_margin', schemaInfo);
15
+ 15 * // Output: true (it's computed, not stored)
16
+ 16 *
17
+ 17 * @example
18
+ 18 * // Input: Check virtual field
19
+ 19 * const schemaInfo = {
20
+ 20 * computed: {},
21
+ 21 * schemaStructure: {
22
+ 22 * temp_password: { type: 'string', virtual: true }
23
+ 23 * }
24
+ 24 * };
25
+ 25 * isNonDatabaseField('temp_password', schemaInfo);
26
+ 26 * // Output: true (it's virtual, not stored)
27
+ 27 *
28
+ 28 * @description
29
+ 29 * Used by:
30
+ 30 * - buildFieldSelection to exclude from SELECT queries
31
+ 31 * - dataPost/dataPatch to skip non-database fields
32
+ 32 * - enrichAttributes to identify computed fields
33
+ 33 *
34
+ 34 * Purpose:
35
+ 35 * - Prevents SQL errors by excluding non-existent columns
36
+ 36 * - Identifies fields that need computation or special handling
37
+ 37 * - Supports virtual fields for temporary/input-only data
38
+ 38 */
39
+ 39 export const isNonDatabaseField = (fieldName, schemaInfo) => {
40
+ 40 const { computed = {}, schemaStructure = {} } = schemaInfo
41
+ 41 if (fieldName in computed) return true
42
+ 42 const fieldDef = schemaStructure[fieldName]
43
+ 43 return fieldDef && fieldDef.virtual === true
44
+ 44 }
45
+ 45
46
+ 46 /**
47
+ 47 * Builds the field selection list for database queries with sparse fieldset support
48
+ 48 *
49
+ 49 * @param {Object} scope - Resource scope with schema information
50
+ 50 * @param {Object} deps - Dependencies object
51
+ 51 * @param {Object} deps.context - Request context with queryParams and schemaInfo
52
+ 52 * @returns {Promise<Object>} Field selection information
53
+ 53 *
54
+ 54 * @example
55
+ 55 * // Input: Basic sparse fieldset request
56
+ 56 * const deps = {
57
+ 57 * context: {
58
+ 58 * scopeName: 'articles',
59
+ 59 * queryParams: { fields: { articles: 'title,body' } },
60
+ 60 * schemaInfo: { idProperty: 'id', ... }
61
+ 61 * }
62
+ 62 * };
63
+ 63 * const result = await buildFieldSelection(scope, deps);
64
+ 64 *
65
+ 65 * // Output: Includes requested fields plus required fields
66
+ 66 * // {
67
+ 67 * // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
68
+ 68 * // requestedFields: ['title', 'body'],
69
+ 69 * // computedDependencies: [],
70
+ 70 * // idProperty: 'id'
71
+ 71 * // }
72
+ 72 * // Note: Foreign keys (author_id, category_id) always included for relationships
73
+ 73 *
74
+ 74 * @example
75
+ 75 * // Input: Computed field with hidden dependencies
76
+ 76 * const schemaInfo = {
77
+ 77 * computed: {
78
+ 78 * profit_margin: {
79
+ 79 * compute: (record) => (record.price - record.cost) / record.price,
80
+ 80 * dependencies: ['price', 'cost']
81
+ 81 * }
82
+ 82 * },
83
+ 83 * schemaStructure: {
84
+ 84 * name: { type: 'string' },
85
+ 85 * price: { type: 'number' },
86
+ 86 * cost: { type: 'number', normallyHidden: true }
87
+ 87 * }
88
+ 88 * };
89
+ 89 * const deps = {
90
+ 90 * context: {
91
+ 91 * queryParams: { fields: { products: 'name,profit_margin' } }
92
+ 92 * }
93
+ 93 * };
94
+ 94 *
95
+ 95 * // Output: Fetches hidden dependency for computation
96
+ 96 * // {
97
+ 97 * // fieldsToSelect: ['id', 'name', 'price', 'cost'],
98
+ 98 * // requestedFields: ['name', 'profit_margin'],
99
+ 99 * // computedDependencies: ['cost'], // Will be removed after computation
100
+ 100 * // idProperty: 'id'
101
+ 101 * // }
102
+ 102 *
103
+ 103 * @example
104
+ 104 * // Input: No sparse fieldsets (returns all visible fields)
105
+ 105 * const deps = {
106
+ 106 * context: {
107
+ 107 * queryParams: {}, // No fields parameter
108
+ 108 * schemaInfo: { ... }
109
+ 109 * }
110
+ 110 * };
111
+ 111 *
112
+ 112 * // Output: All non-hidden, non-virtual fields
113
+ 113 * // {
114
+ 114 * // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
115
+ 115 * // requestedFields: null,
116
+ 116 * // computedDependencies: [],
117
+ 117 * // idProperty: 'id'
118
+ 118 * // }
119
+ 119 *
120
+ 120 * @description
121
+ 121 * Used by:
122
+ 122 * - dataGet to build SELECT query
123
+ 123 * - dataQuery to build SELECT query
124
+ 124 * - buildQuerySelection as the main field resolver
125
+ 125 *
126
+ 126 * Purpose:
127
+ 127 * - Implements JSON:API sparse fieldsets specification
128
+ 128 * - Ensures relationships work by including foreign keys
129
+ 129 * - Handles computed field dependencies intelligently
130
+ 130 * - Respects field visibility rules (hidden/normallyHidden)
131
+ 131 * - Supports custom ID column names
132
+ 132 *
133
+ 133 * Data flow:
134
+ 134 * 1. Always includes ID (with aliasing if needed)
135
+ 135 * 2. Parses requested fields from query string
136
+ 136 * 3. Validates requested fields exist in schema
137
+ 137 * 4. Adds dependencies for computed fields
138
+ 138 * 5. Includes all foreign keys for relationships
139
+ 139 * 6. Returns complete field list for SQL SELECT
140
+ 140 */
141
+ 141 export const buildFieldSelection = async (scope, deps) => {
142
+ 142 const fieldsToSelect = new Set()
143
+ 143 const computedDependencies = new Set()
144
+ 144
145
+ 145 // Extract values from scope
146
+ 146 const {
147
+ 147 vars: {
148
+ 148 schemaInfo: { schemaInstance, computed: computedFields = {}, schemaStructure }
149
+ 149 }
150
+ 150 } = scope
151
+ 151
152
+ 152 // Extract values from deps
153
+ 153 const { context } = deps
154
+ 154 const scopeName = context.scopeName
155
+ 155 const requestedFields = context.queryParams?.fields?.[scopeName]
156
+ 156 const idProperty = context.schemaInfo.idProperty
157
+ 157
158
+ 158 // Always include the ID field - required for JSON:API
159
+ 159 // Handle aliasing if idProperty is not 'id'
160
+ 160 if (idProperty !== 'id') {
161
+ 161 fieldsToSelect.add(`${idProperty} as id`)
162
+ 162 } else {
163
+ 163 fieldsToSelect.add('id')
164
+ 164 }
165
+ 165
166
+ 166 // Handle both Schema objects and plain objects
167
+ 167 if (!schemaStructure) {
168
+ 168 schemaStructure = schemaInstance?.structure || schemaInstance || {}
169
+ 169 }
170
+ 170
171
+ 171 // Get computed fields and virtual fields from schema
172
+ 172 const computedFieldNames = new Set(Object.keys(computedFields))
173
+ 173
174
+ 174 // Find fields marked as virtual in the schema
175
+ 175 const virtualFieldNames = new Set()
176
+ 176 Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
177
+ 177 if (fieldDef.virtual === true) {
178
+ 178 virtualFieldNames.add(fieldName)
179
+ 179 }
180
+ 180 })
181
+ 181
182
+ 182 const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
183
+ 183
184
+ 184 // Parse requested fields
185
+ 185 const requested = requestedFields
186
+ 186 ? (
187
+ 187 typeof requestedFields === 'string'
188
+ 188 ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
189
+ 189 : requestedFields
190
+ 190 )
191
+ 191 : null
192
+ 192
193
+ 193 if (requested && requested.length > 0) {
194
+ 194 // Sparse fieldsets requested - only select specified fields
195
+ 195 // Example: ?fields[products]=name,price,profit_margin
196
+ 196 requested.forEach(field => {
197
+ 197 // Skip computed and virtual fields - they don't exist in database
198
+ 198 // Computed fields will be calculated later in enrichAttributes
199
+ 199 // Virtual fields are handled separately (from request input)
200
+ 200 if (nonDatabaseFields.has(field)) return
201
+ 201
202
+ 202 const fieldDef = schemaStructure[field]
203
+ 203 if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`)
204
+ 204
205
+ 205 if (fieldDef.belongsToPolymorphic) {
206
+ 206 return
207
+ 207 }
208
+ 208
209
+ 209 // NEVER include hidden fields, even if explicitly requested
210
+ 210 // Example: password_hash with hidden:true is never returned
211
+ 211 if (fieldDef.hidden === true) return
212
+ 212
213
+ 213 fieldsToSelect.add(field)
214
+ 214 })
215
+ 215
216
+ 216 // Handle computed field dependencies - fetch fields needed for calculations
217
+ 217 // Example: User requests 'profit_margin' which depends on 'price' and 'cost'
218
+ 218 // We need to fetch price and cost from DB even if not explicitly requested
219
+ 219 const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
220
+ 220 for (const computedField of requestedComputedFields) {
221
+ 221 const fieldDef = computedFields[computedField]
222
+ 222 if (fieldDef.dependencies) {
223
+ 223 for (const dep of fieldDef.dependencies) {
224
+ 224 const depFieldDef = schemaStructure[dep]
225
+ 225 // Only add dependency if it exists and isn't hidden
226
+ 226 if (depFieldDef && depFieldDef.hidden !== true) {
227
+ 227 fieldsToSelect.add(dep)
228
+ 228 // Track dependencies that weren't explicitly requested
229
+ 229 // These will be removed from the final response
230
+ 230 if (!requested.includes(dep)) {
231
+ 231 computedDependencies.add(dep)
232
+ 232 }
233
+ 233 }
234
+ 234 }
235
+ 235 }
236
+ 236 }
237
+ 237
238
+ 238 // Still handle normallyHidden fields for backward compatibility
239
+ 239 if (requestedComputedFields.length > 0) {
240
+ 240 Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
241
+ 241 if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
242
+ 242 // Only add if not already handled by dependencies
243
+ 243 if (!fieldsToSelect.has(field)) {
244
+ 244 fieldsToSelect.add(field)
245
+ 245 if (!requested.includes(field)) {
246
+ 246 computedDependencies.add(field)
247
+ 247 }
248
+ 248 }
249
+ 249 }
250
+ 250 })
251
+ 251 }
252
+ 252 } else {
253
+ 253 // No sparse fieldsets - return all visible fields
254
+ 254 // This is the default behavior when no ?fields parameter is provided
255
+ 255 Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
256
+ 256 // Skip virtual fields - they don't exist in database
257
+ 257 if (fieldDef.virtual === true) return
258
+ 258
259
+ 259 // Skip hidden fields - these are NEVER returned
260
+ 260 // Example: password_hash with hidden:true
261
+ 261 if (fieldDef.hidden === true) return
262
+ 262
263
+ 263 // Skip polymorphic placeholder fields - handled via type/id columns
264
+ 264 if (fieldDef.belongsToPolymorphic) return
265
+ 265
266
+ 266 // Skip normallyHidden fields - these are hidden by default
267
+ 267 // Example: cost with normallyHidden:true (only returned when explicitly requested)
268
+ 268 if (fieldDef.normallyHidden === true) return
269
+ 269
270
+ 270 fieldsToSelect.add(field)
271
+ 271 })
272
+ 272
273
+ 273 // When no sparse fieldsets, we compute all computed fields
274
+ 274 // So we need to include their dependencies even if normallyHidden
275
+ 275 // Example: profit_margin depends on 'cost' which is normallyHidden
276
+ 276 // We fetch 'cost' for calculation but don't return it in response
277
+ 277 for (const [fieldName, fieldDef] of Object.entries(computedFields)) {
278
+ 278 if (fieldDef.dependencies) {
279
+ 279 for (const dep of fieldDef.dependencies) {
280
+ 280 const depFieldDef = schemaStructure[dep]
281
+ 281 if (depFieldDef && depFieldDef.hidden !== true) {
282
+ 282 fieldsToSelect.add(dep)
283
+ 283 // Track normallyHidden dependencies for later removal
284
+ 284 // These are fetched for computation but not returned
285
+ 285 if (depFieldDef.normallyHidden === true) {
286
+ 286 computedDependencies.add(dep)
287
+ 287 }
288
+ 288 }
289
+ 289 }
290
+ 290 }
291
+ 291 }
292
+ 292 }
293
+ 293
294
+ 294 // Always include foreign keys for relationships (unless hidden)
295
+ 295 Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
296
+ 296 if (fieldDef.belongsTo && fieldDef.hidden !== true) {
297
+ 297 fieldsToSelect.add(field)
298
+ 298 }
299
+ 299 })
300
+ 300
301
+ 301 // Always include polymorphic type and id fields from relationships
302
+ 302 try {
303
+ 303 const relationships = scope.vars.schemaInfo.schemaRelationships
304
+ 304 Object.entries(relationships || {}).forEach(([relName, relDef]) => {
305
+ 305 if (relDef.belongsToPolymorphic) {
306
+ 306 if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
307
+ 307 if (relDef.idField) fieldsToSelect.add(relDef.idField)
308
+ 308 }
309
+ 309 })
310
+ 310 } catch (e) {
311
+ 311 // Scope might not have relationships
312
+ 312 }
313
+ 313
314
+ 314 // Return detailed information about field selection
315
+ 315 // This info is used by:
316
+ 316 // 1. SQL query builder to SELECT the right columns
317
+ 317 // 2. enrichAttributes to know which computed fields to calculate
318
+ 318 // 3. enrichAttributes to remove dependencies from final response
319
+ 319 return {
320
+ 320 fieldsToSelect: Array.from(fieldsToSelect), // Fields to SELECT from database
321
+ 321 requestedFields: requested, // Fields explicitly requested by user
322
+ 322 computedDependencies: Array.from(computedDependencies), // Dependencies to remove from response
323
+ 323 idProperty // Pass idProperty for reference
324
+ 324 }
325
+ 325 }
326
+ 326
327
+ 327 /**
328
+ 328 * Determines which computed fields to calculate based on sparse fieldsets
329
+ 329 *
330
+ 330 * @param {string} scopeName - Resource name (e.g., 'products')
331
+ 331 * @param {Array<string>|string} requestedFields - Requested fields from query
332
+ 332 * @param {Object} computedFields - Computed field definitions from schema
333
+ 333 * @returns {Array<string>} Names of computed fields to calculate
334
+ 334 *
335
+ 335 * @example
336
+ 336 * // Input: No sparse fieldsets (calculate all computed fields)
337
+ 337 * const computed = {
338
+ 338 * full_name: { compute: (r) => `${r.first} ${r.last}` },
339
+ 339 * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
340
+ 340 * };
341
+ 341 * getRequestedComputedFields('users', null, computed);
342
+ 342 * // Output: ['full_name', 'age'] (all computed fields)
343
+ 343 *
344
+ 344 * @example
345
+ 345 * // Input: Sparse fieldsets with some computed fields
346
+ 346 * const requestedFields = 'first_name,full_name,email';
347
+ 347 * const computed = {
348
+ 348 * full_name: { compute: (r) => `${r.first} ${r.last}` },
349
+ 349 * age: { compute: (r) => new Date().getFullYear() - r.birth_year }
350
+ 350 * };
351
+ 351 * getRequestedComputedFields('users', requestedFields, computed);
352
+ 352 * // Output: ['full_name'] (only requested computed field)
353
+ 353 *
354
+ 354 * @example
355
+ 355 * // Input: Sparse fieldsets with no computed fields
356
+ 356 * const requestedFields = ['first_name', 'email'];
357
+ 357 * const computed = {
358
+ 358 * full_name: { compute: (r) => `${r.first} ${r.last}` }
359
+ 359 * };
360
+ 360 * getRequestedComputedFields('users', requestedFields, computed);
361
+ 361 * // Output: [] (no computed fields requested)
362
+ 362 *
363
+ 363 * @description
364
+ 364 * Used by:
365
+ 365 * - enrichAttributes to determine which computations to run
366
+ 366 * - Applied after fetching data from database
367
+ 367 *
368
+ 368 * Purpose:
369
+ 369 * - Optimizes performance by only computing requested fields
370
+ 370 * - Supports JSON:API sparse fieldsets for computed fields
371
+ 371 * - Handles both string and array input formats
372
+ 372 *
373
+ 373 * Data flow:
374
+ 374 * 1. Checks if sparse fieldsets are specified
375
+ 375 * 2. If not, returns all computed field names
376
+ 376 * 3. If yes, filters to only requested computed fields
377
+ 377 * 4. Normalizes string input to array format
378
+ 378 */
379
+ 379 export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
380
+ 380 if (!computedFields) return []
381
+ 381
382
+ 382 const allComputedFields = Object.keys(computedFields)
383
+ 383
384
+ 384 if (!requestedFields || requestedFields.length === 0) {
385
+ 385 // No sparse fieldsets - return all computed fields
386
+ 386 return allComputedFields
387
+ 387 }
388
+ 388
389
+ 389 // Parse requested fields if it's a string
390
+ 390 const requested = typeof requestedFields === 'string'
391
+ 391 ? requestedFields.split(',').map(f => f.trim()).filter(f => f)
392
+ 392 : requestedFields
393
+ 393
394
+ 394 // Return only requested computed fields that exist
395
+ 395 return requested.filter(field => allComputedFields.includes(field))
396
+ 396 }