json-rest-api 1.0.28 → 2.0.0

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 (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,3 +1,6 @@
1
+ import { createSchema } from 'json-rest-schema'
2
+ import { getForeignKeyFields, assertFieldName, assertFieldNameMap } from './field-utils.js'
3
+
1
4
  /**
2
5
  * Schema processing utilities for search and field dependencies
3
6
  *
@@ -10,79 +13,15 @@
10
13
  */
11
14
 
12
15
  /**
13
- * Marks all fields in a searchSchema as indexed for database optimization
14
- *
15
- * @param {Object} searchSchema - Search schema object to process
16
- * @returns {void} Modifies the searchSchema in-place
16
+ * Mark object-valued search definitions indexed in place, overriding false.
17
+ * This is search metadata, not a DDL operation or a query-performance guarantee.
17
18
  *
19
+ * @param {Object} searchSchema - Search definitions, if available
20
+ * @returns {void}
18
21
  * @example
19
- * // Input: Search fields without index flags
20
- * const searchSchema = {
21
- * title: { type: 'string', filterOperator: 'contains' },
22
- * status: { type: 'string', filterOperator: '=' },
23
- * created_at: { type: 'datetime', filterOperator: '>=' }
24
- * };
25
- * ensureSearchFieldsAreIndexed(searchSchema);
26
- *
27
- * // Output: All fields marked as indexed
28
- * // {
29
- * // title: { type: 'string', filterOperator: 'contains', indexed: true },
30
- * // status: { type: 'string', filterOperator: '=', indexed: true },
31
- * // created_at: { type: 'datetime', filterOperator: '>=', indexed: true }
32
- * // }
33
- *
34
- * @example
35
- * // Input: Virtual field for cross-table search
36
- * const searchSchema = {
37
- * name: { type: 'string', filterOperator: 'contains' },
38
- * author_name: { // Virtual field
39
- * type: 'string',
40
- * filterOperator: 'contains',
41
- * virtualField: {
42
- * joinTo: 'users',
43
- * joinOn: ['author_id', 'id'],
44
- * searchField: 'name'
45
- * }
46
- * }
47
- * };
48
- * ensureSearchFieldsAreIndexed(searchSchema);
49
- *
50
- * // Output: Both regular and virtual fields indexed
51
- * // {
52
- * // name: { ..., indexed: true },
53
- * // author_name: { ..., indexed: true } // Enables efficient JOIN
54
- * // }
55
- *
56
- * @example
57
- * // Input: Already indexed or explicitly false
58
- * const searchSchema = {
59
- * email: { type: 'string', indexed: false }, // Explicitly false
60
- * username: { type: 'string', indexed: true } // Already true
61
- * };
62
- * ensureSearchFieldsAreIndexed(searchSchema);
63
- *
64
- * // Output: All forced to indexed: true
65
- * // {
66
- * // email: { type: 'string', indexed: true }, // Overridden
67
- * // username: { type: 'string', indexed: true } // Unchanged
68
- * // }
69
- *
70
- * @description
71
- * Used by:
72
- * - rest-api-plugin during scope initialization
73
- * - Applied to all searchSchema fields before storage setup
74
- *
75
- * Purpose:
76
- * - Signals storage plugins which fields need database indexes
77
- * - Enables efficient filtering without full table scans
78
- * - Supports cross-table searches via indexed JOINs
79
- * - Improves query performance for filtered API requests
80
- *
81
- * Data flow:
82
- * 1. Receives searchSchema object (or null)
83
- * 2. Iterates through each field definition
84
- * 3. Sets indexed: true on all fields
85
- * 4. Storage plugins use this flag for index creation
22
+ * const fields = { title: { type: 'string', indexed: false } }
23
+ * ensureSearchFieldsAreIndexed(fields)
24
+ * // fields.title.indexed === true
86
25
  */
87
26
  export function ensureSearchFieldsAreIndexed (searchSchema) {
88
27
  if (!searchSchema) return
@@ -118,7 +57,8 @@ function createRelationshipSearchEntries (fieldName, fieldDef, searchEntry) {
118
57
 
119
58
  function registerSearchEntries (searchSchema, entries = []) {
120
59
  for (const [searchFieldName, searchEntry] of entries) {
121
- if (!searchFieldName || searchSchema[searchFieldName]) {
60
+ assertFieldName(searchFieldName, 'generated search schema')
61
+ if (!searchFieldName || Object.hasOwn(searchSchema, searchFieldName)) {
122
62
  continue
123
63
  }
124
64
 
@@ -127,137 +67,21 @@ function registerSearchEntries (searchSchema, entries = []) {
127
67
  }
128
68
 
129
69
  /**
130
- * Generates complete searchSchema by merging schema search definitions with explicit searchSchema
131
- *
132
- * @param {Object} schema - Main resource schema with optional 'search' properties
133
- * @param {Object} explicitSearchSchema - Optional explicit searchSchema to merge
134
- * @returns {Object|null} Merged searchSchema or null if no search fields
135
- * @throws {RestApiValidationError} If same field defined in multiple places
136
- *
137
- * @example
138
- * // Input: Simple search fields in schema
139
- * const schema = {
140
- * title: { type: 'string', search: true }, // Simple
141
- * content: { type: 'text' }, // Not searchable
142
- * status: { type: 'string', search: { filterOperator: '=' } } // Custom
143
- * };
144
- * const result = generateSearchSchemaFromSchema(schema, null);
145
- *
146
- * // Output: Generated search schema
147
- * // {
148
- * // title: { type: 'string' }, // No operator set; resolver applies defaults
149
- * // status: { type: 'string', filterOperator: '=' } // Specified operator
150
- * // }
151
- *
152
- * @example
153
- * // Input: Multiple filters from one field
154
- * const schema = {
155
- * published_at: {
156
- * type: 'datetime',
157
- * search: {
158
- * published_after: { filterOperator: '>=', type: 'datetime' },
159
- * published_before: { filterOperator: '<=', type: 'datetime' }
160
- * }
161
- * }
162
- * };
163
- * const result = generateSearchSchemaFromSchema(schema, null);
164
- *
165
- * // Output: Two search fields from one database field
166
- * // {
167
- * // published_after: {
168
- * // type: 'datetime',
169
- * // actualField: 'published_at', // Maps to real field
170
- * // filterOperator: '>='
171
- * // },
172
- * // published_before: {
173
- * // type: 'datetime',
174
- * // actualField: 'published_at', // Same field, different operator
175
- * // filterOperator: '<='
176
- * // }
177
- * // }
178
- *
179
- * @example
180
- * // Input: Merging with explicit searchSchema
181
- * const schema = {
182
- * name: { type: 'string', search: true }
183
- * };
184
- * const explicitSearchSchema = {
185
- * email: { type: 'string', filterOperator: '=' },
186
- * age: { type: 'number', filterOperator: '>=' }
187
- * };
188
- * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
189
- *
190
- * // Output: Combined from both sources
191
- * // {
192
- * // email: { type: 'string', filterOperator: '=' }, // Explicit
193
- * // age: { type: 'number', filterOperator: '>=' }, // Explicit
194
- * // name: { type: 'string' } // From schema (no operator set)
195
- * // }
196
- *
70
+ * Merge generated field filters into an explicit search schema.
71
+ * Explicit names win. Boolean search copies the field declaration; configured
72
+ * search may define one filter or named filters with actualField mappings.
73
+ * belongsTo aliases and physical-field search entries share the target metadata.
74
+ * Operator defaults are resolved later, not hard-coded by this generator.
75
+ *
76
+ * @param {Object} schema - Resource field definitions
77
+ * @param {Object} explicitSearchSchema - Explicit filters taking precedence
78
+ * @returns {Object|null} Merged definitions, or null when none exist
197
79
  * @example
198
- * // Input: Explicit searchSchema takes precedence
199
- * const schema = {
200
- * email: { type: 'string', search: true }
201
- * };
202
- * const explicitSearchSchema = {
203
- * email: { type: 'string', filterOperator: 'contains' }
204
- * };
205
- * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
206
- * // Output: Explicit searchSchema wins
207
- * // {
208
- * // email: { type: 'string', filterOperator: 'contains' } // Uses explicit definition
209
- * // }
210
- *
211
- * @example
212
- * // Input: Virtual search fields
213
- * const schema = {
214
- * title: { type: 'string' },
215
- * _virtual: {
216
- * search: {
217
- * category_name: { // Doesn't exist in DB
218
- * type: 'string',
219
- * filterOperator: 'contains',
220
- * virtualField: {
221
- * joinTo: 'categories',
222
- * joinOn: ['category_id', 'id'], // JOIN condition
223
- * searchField: 'name' // Field in related table
224
- * }
225
- * }
226
- * }
227
- * }
228
- * };
229
- *
230
- * // Output: Virtual field for cross-table search
231
- * // {
232
- * // category_name: {
233
- * // type: 'string',
234
- * // filterOperator: 'contains',
235
- * // virtualField: { ... join config ... }
236
- * // }
237
- * // }
238
- *
239
- * @description
240
- * Used by:
241
- * - rest-api-plugin during scope initialization
242
- * - Generates searchSchema from various sources
243
- *
244
- * Purpose:
245
- * - Provides flexible search configuration options
246
- * - Enables virtual fields for cross-table searches
247
- * - Supports range queries (before/after patterns)
248
- * - Merges search:true fields with explicit searchSchema
249
- * - Explicit searchSchema always takes precedence
250
- * - Allows storage plugins to optimize queries
251
- *
252
- * Data flow:
253
- * 1. Starts with explicit searchSchema (if any)
254
- * 2. Processes schema fields with 'search' property
255
- * 3. Skips fields already in explicit searchSchema (no conflicts)
256
- * 4. Handles multiple filters from single field
257
- * 5. Processes virtual search definitions
258
- * 6. Returns merged searchSchema or null
80
+ * generateSearchSchemaFromSchema({ title: { type: 'string', search: true } }, {})
81
+ * // { title: { type: 'string' } }
259
82
  */
260
83
  export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
84
+ assertFieldNameMap(explicitSearchSchema, 'explicit search schema')
261
85
  // Start with explicit searchSchema or empty object
262
86
  const searchSchema = explicitSearchSchema ? { ...explicitSearchSchema } : {}
263
87
 
@@ -278,6 +102,7 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
278
102
  // Do not set filterOperator here; centralized resolver will apply sensible defaults
279
103
  }))
280
104
  } else if (typeof effectiveSearch === 'object') {
105
+ assertFieldNameMap(effectiveSearch, `search declaration for '${fieldName}'`)
281
106
  // Check if search defines multiple filter fields
282
107
  const hasNestedFilters = Object.values(effectiveSearch).some(
283
108
  v => typeof v === 'object' && v.filterOperator
@@ -287,7 +112,7 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
287
112
  // Multiple filters from one field (like published_after/before)
288
113
  Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
289
114
  // Check if filter already exists in explicit searchSchema
290
- if (searchSchema[filterName]) {
115
+ if (Object.hasOwn(searchSchema, filterName)) {
291
116
  // Skip - explicit searchSchema takes precedence
292
117
  return
293
118
  }
@@ -311,9 +136,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
311
136
 
312
137
  // Handle _virtual search definitions
313
138
  if (schema._virtual?.search) {
139
+ assertFieldNameMap(schema._virtual.search, 'virtual search schema')
314
140
  Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
315
141
  // Check if filter already exists in explicit searchSchema
316
- if (searchSchema[filterName]) {
142
+ if (Object.hasOwn(searchSchema, filterName)) {
317
143
  // Skip - explicit searchSchema takes precedence
318
144
  return
319
145
  }
@@ -326,52 +152,21 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
326
152
  }
327
153
 
328
154
  /**
329
- * Generic topological sort for handling dependencies
330
- *
331
- * @param {Array} items - Array of items to sort
332
- * @param {Function} getDependencies - Function that returns dependencies for an item
333
- * @returns {Array} Sorted array respecting dependencies
334
- * @throws {Error} If circular dependencies or unknown dependencies detected
335
- *
336
- * @example
337
- * // Input: Simple dependency chain
338
- * const items = ['a', 'b', 'c'];
339
- * const deps = { a: ['b'], b: ['c'], c: [] };
340
- * const sorted = topologicalSort(items, item => deps[item]);
341
- * // Output: ['c', 'b', 'a']
342
- * // c first (no deps), then b (depends on c), then a (depends on b)
343
- *
344
- * @example
345
- * // Input: Circular dependency
346
- * const items = ['a', 'b'];
347
- * const deps = { a: ['b'], b: ['a'] }; // Circular!
348
- * topologicalSort(items, item => deps[item]);
349
- * // Throws: Error "Circular dependency detected: b"
350
- *
351
- * @example
352
- * // Input: Unknown dependency
353
- * const items = ['a', 'b'];
354
- * const deps = { a: ['c'], b: [] }; // 'c' not in items
355
- * topologicalSort(items, item => deps[item]);
356
- * // Throws: Error "Unknown dependency 'c' for 'a'"
357
- *
358
- * @description
359
- * Used by:
360
- * - sortFieldsByDependencies for ordering field operations
361
- * - Any code needing dependency-based ordering
362
- *
363
- * Purpose:
364
- * - Orders items so dependencies come before dependents
365
- * - Detects circular dependencies early
366
- * - Validates all dependencies exist
367
- * - Uses depth-first search algorithm
155
+ * Sort a fixed field graph dependency-first, preserving traversal order for ties.
156
+ * Membership is indexed once; cycles and references outside the graph reject.
157
+ * @template T
158
+ * @param {readonly T[]} items
159
+ * @param {(item: T) => Iterable<T> | null | undefined} getDependencies
160
+ * @returns {T[]}
368
161
  */
369
162
  export function topologicalSort (items, getDependencies) {
163
+ const itemNames = new Set(items)
370
164
  const sorted = []
371
165
  const visited = new Set()
372
166
  const visiting = new Set()
167
+ const stack = []
373
168
 
374
- function visit (item) {
169
+ function enter (item) {
375
170
  if (visited.has(item)) return
376
171
 
377
172
  if (visiting.has(item)) {
@@ -380,100 +175,184 @@ export function topologicalSort (items, getDependencies) {
380
175
 
381
176
  visiting.add(item)
382
177
 
383
- const dependencies = getDependencies(item) || []
384
- for (const dep of dependencies) {
385
- if (!items.includes(dep)) {
386
- throw new Error(`Unknown dependency '${dep}' for '${item}'`)
178
+ stack.push({ item, iterator: (getDependencies(item) || [])[Symbol.iterator]() })
179
+ }
180
+
181
+ try {
182
+ for (const item of items) {
183
+ enter(item)
184
+ while (stack.length) {
185
+ const frame = stack[stack.length - 1]
186
+ let next
187
+ try {
188
+ next = frame.iterator.next()
189
+ } catch (error) {
190
+ stack.pop()
191
+ throw error
192
+ }
193
+ if (next.done) {
194
+ visiting.delete(frame.item)
195
+ visited.add(frame.item)
196
+ sorted.push(frame.item)
197
+ stack.pop()
198
+ continue
199
+ }
200
+ if (!itemNames.has(next.value)) {
201
+ throw new Error(`Unknown dependency '${next.value}' for '${frame.item}'`)
202
+ }
203
+ enter(next.value)
204
+ }
205
+ }
206
+ } catch (error) {
207
+ // Match nested for-of cleanup while retaining the original graph failure.
208
+ for (let index = stack.length - 1; index >= 0; index--) {
209
+ try { stack[index].iterator.return?.() } catch {}
210
+ }
211
+ throw error
212
+ }
213
+
214
+ return sorted
215
+ }
216
+
217
+ /** Validate callback dependencies once; retain direct read edges and execution orders. */
218
+ export function compileFieldDependencies ({ schemaStructure, computedFields, queryFields = {}, schemaRelationships = {}, idProperty = 'id', scopeName }) {
219
+ const inputNames = new Set([...Object.keys(schemaStructure), 'id', idProperty])
220
+ const getterNames = new Set([...inputNames, ...Object.keys(queryFields)])
221
+ const readNames = new Set([...getterNames, ...Object.keys(computedFields)])
222
+ const relationshipFields = getForeignKeyFields(schemaStructure, schemaRelationships)
223
+ const getterDependencies = new Set([...getterNames].filter(name => !relationshipFields.has(name)))
224
+ const computedDependencies = new Set([...readNames].filter(name => !relationshipFields.has(name)))
225
+ const fieldGetters = Object.create(null)
226
+ const fieldSetters = Object.create(null)
227
+ const readDependencies = Object.fromEntries([...readNames].map(name => [name, []]))
228
+
229
+ const dependencies = (name, definition, property, kind, available) => {
230
+ const declared = definition[property]
231
+ if (declared === undefined) return []
232
+ if (!Array.isArray(declared) || declared.some(dep => typeof dep !== 'string' || dep.length === 0)) {
233
+ throw new Error(`Field '${name}' in resource '${scopeName}' must declare ${property} as an array of nonempty field names`)
234
+ }
235
+ for (const dep of declared) {
236
+ if (!available.has(dep)) {
237
+ throw new Error(`Field '${name}' in resource '${scopeName}' has ${kind} dependency '${dep}' that does not exist in schema or is unavailable at that stage`)
387
238
  }
388
- visit(dep)
389
239
  }
240
+ return [...new Set(declared)]
241
+ }
390
242
 
391
- visiting.delete(item)
392
- visited.add(item)
393
- sorted.push(item)
243
+ for (const [name, definition] of Object.entries({ ...schemaStructure, ...queryFields })) {
244
+ for (const [callback, property, available, target] of [
245
+ ['getter', 'runGetterAfter', getterDependencies, fieldGetters],
246
+ ['setter', 'runSetterAfter', inputNames, fieldSetters]
247
+ ]) {
248
+ if (callback === 'setter' && Object.hasOwn(queryFields, name)) {
249
+ if (definition.setter !== undefined || definition.runSetterAfter !== undefined) {
250
+ throw new Error(`Query field '${name}' in resource '${scopeName}' cannot declare a setter or runSetterAfter; projections are read-only`)
251
+ }
252
+ continue
253
+ }
254
+ if (definition[callback] !== undefined && typeof definition[callback] !== 'function') {
255
+ throw new Error(`Field '${name}' in resource '${scopeName}' has invalid ${callback} function`)
256
+ }
257
+ if (definition[callback] === undefined) {
258
+ if (definition[property] !== undefined) {
259
+ throw new Error(`Field '${name}' in resource '${scopeName}' declares ${property} without a ${callback} function`)
260
+ }
261
+ continue
262
+ }
263
+ const after = dependencies(name, definition, property, callback, available)
264
+ target[name] = { [callback]: definition[callback], [property]: after, fieldDef: definition }
265
+ if (callback === 'getter') readDependencies[name] = after
266
+ }
394
267
  }
395
268
 
396
- for (const item of items) {
397
- visit(item)
269
+ for (const [name, definition] of Object.entries(computedFields)) {
270
+ readDependencies[name] = dependencies(name, definition, 'dependencies', 'computed', computedDependencies)
398
271
  }
399
272
 
400
- return sorted
273
+ let sortedGetterFields, sortedSetterFields, sortedComputedFields
274
+ try {
275
+ sortedGetterFields = topologicalSort([...getterNames], name => fieldGetters[name]?.runGetterAfter || [])
276
+ .filter(name => Object.hasOwn(fieldGetters, name))
277
+ } catch (error) {
278
+ throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`, { cause: error })
279
+ }
280
+ try {
281
+ sortedSetterFields = topologicalSort([...inputNames], name => fieldSetters[name]?.runSetterAfter || [])
282
+ .filter(name => Object.hasOwn(fieldSetters, name))
283
+ } catch (error) {
284
+ throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`, { cause: error })
285
+ }
286
+ try {
287
+ sortedComputedFields = topologicalSort([...readNames], name => readDependencies[name])
288
+ .filter(name => Object.hasOwn(computedFields, name))
289
+ } catch (error) {
290
+ throw new Error(`Invalid computed dependencies in ${scopeName}: ${error.message}`, { cause: error })
291
+ }
292
+
293
+ return { fieldGetters, sortedGetterFields, fieldSetters, sortedSetterFields, readDependencies, sortedComputedFields, foreignKeyFields: relationshipFields }
401
294
  }
402
295
 
403
- /**
404
- * Sorts fields by their dependencies using topological sort
405
- *
406
- * @param {Object} fields - Object with field definitions
407
- * @param {string} dependencyProperty - Property name containing dependencies
408
- * @returns {Array} Field names sorted by dependencies
409
- * @throws {Error} If circular dependencies or unknown fields detected
410
- *
411
- * @example
412
- * // Input: Getter dependencies (fullName needs firstName and lastName)
413
- * const fields = {
414
- * firstName: { getter: v => v.trim() },
415
- * lastName: { getter: v => v.trim() },
416
- * fullName: {
417
- * getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
418
- * runGetterAfter: ['firstName', 'lastName']
419
- * }
420
- * };
421
- * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
422
- * // Output: ['firstName', 'lastName', 'fullName']
423
- * // Ensures firstName/lastName getters run before fullName
424
- *
425
- * @example
426
- * // Input: Complex dependency chain
427
- * const fields = {
428
- * a: { runGetterAfter: ['b', 'c'] }, // a needs b and c
429
- * b: { runGetterAfter: ['d'] }, // b needs d
430
- * c: { runGetterAfter: ['d'] }, // c needs d
431
- * d: { runGetterAfter: [] } // d needs nothing
432
- * };
433
- * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
434
- * // Output: ['d', 'b', 'c', 'a']
435
- * // d first, then b/c (both need d), then a (needs b/c)
436
- *
437
- * @example
438
- * // Input: Circular dependency error
439
- * const fields = {
440
- * a: { runGetterAfter: ['b'] },
441
- * b: { runGetterAfter: ['a'] } // Circular!
442
- * };
443
- * sortFieldsByDependencies(fields, 'runGetterAfter');
444
- * // Throws: Error "Circular dependency detected: a in runGetterAfter"
445
- *
446
- * @description
447
- * Used by:
448
- * - Schema processing for getter/setter ordering
449
- * - Ensures dependent fields process after dependencies
450
- *
451
- * Purpose:
452
- * - Orders field operations by dependencies
453
- * - Enables computed fields that depend on other fields
454
- * - Validates dependency graph is acyclic
455
- * - Provides clear error messages for debugging
456
- *
457
- * Data flow:
458
- * 1. Extracts field names from object
459
- * 2. Calls topologicalSort with dependency function
460
- * 3. Returns ordered field names
461
- * 4. Enhances error messages with property name
296
+ /** Find only the dependencies needed by this read; ordering is already compiled.
297
+ * @param {{ readDependencies?: Record<string, readonly string[]> }} schemaInfo
298
+ * @param {Iterable<string>} fields
299
+ * @returns {Set<string>}
462
300
  */
463
- export function sortFieldsByDependencies (fields, dependencyProperty) {
464
- const fieldNames = Object.keys(fields)
301
+ export function getFieldDependencyClosure (schemaInfo, fields) {
302
+ const selected = new Set(fields)
303
+ const pending = [...selected]
304
+ const dependencies = schemaInfo.readDependencies || {}
305
+ while (pending.length) {
306
+ const name = pending.pop()
307
+ if (!Object.hasOwn(dependencies, name)) continue
308
+ for (const dependency of dependencies[name]) {
309
+ if (selected.has(dependency)) continue
310
+ selected.add(dependency)
311
+ pending.push(dependency)
312
+ }
313
+ }
314
+ return selected
315
+ }
465
316
 
466
- if (fieldNames.length === 0) return []
317
+ /** Copy declaration data without freezing caller-owned values or cloning callbacks. */
318
+ export function snapshotResourceConfiguration (value, seen = new WeakMap()) {
319
+ if (value === null || typeof value !== 'object') return value
320
+ if (seen.has(value)) return seen.get(value)
467
321
 
468
- try {
469
- return topologicalSort(fieldNames, (fieldName) => {
470
- const field = fields[fieldName]
471
- return field[dependencyProperty] || []
472
- })
473
- } catch (error) {
474
- if (error.message.includes('Circular dependency')) {
475
- throw new Error(`${error.message} in ${dependencyProperty}`)
322
+ let copy
323
+ if (value instanceof Date) copy = new Date(value.getTime())
324
+ else if (value instanceof RegExp) {
325
+ copy = new RegExp(value.source, value.flags)
326
+ copy.lastIndex = value.lastIndex
327
+ } else if (typeof value.validateWith === 'function' && typeof value.getFieldDefinitions === 'function') {
328
+ const factory = createSchema.createFactory(value)
329
+ copy = factory({}, { operations: value.operations })
330
+ seen.set(value, copy)
331
+ copy.structure = snapshotResourceConfiguration(value.structure, seen)
332
+ return copy
333
+ } else if (Array.isArray(value)) copy = new Array(value.length)
334
+ else {
335
+ const prototype = Object.getPrototypeOf(value)
336
+ if (prototype !== Object.prototype && prototype !== null) return value
337
+ copy = Object.create(prototype)
338
+ }
339
+ seen.set(value, copy)
340
+ if (value instanceof Date || value instanceof RegExp) return copy
341
+ for (const key of Object.keys(value)) {
342
+ // File backends are external handles; their state and method receiver stay intact.
343
+ const isFileBackend = key === 'storage' && value.type === 'file' && typeof value[key]?.upload === 'function'
344
+ let propertyValue
345
+ if (isFileBackend) {
346
+ propertyValue = value[key]
347
+ } else {
348
+ propertyValue = snapshotResourceConfiguration(value[key], seen)
476
349
  }
477
- throw error
350
+ Object.defineProperty(copy, key, {
351
+ value: propertyValue,
352
+ enumerable: true,
353
+ configurable: true,
354
+ writable: true
355
+ })
478
356
  }
357
+ return copy
479
358
  }