json-rest-api 1.0.29 → 2.1.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 (294) 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 +744 -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 +733 -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 +103 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. 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
  }