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,153 +1,53 @@
1
1
  import { createSchema } from 'json-rest-schema'
2
- import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js'
2
+ import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, compileFieldDependencies, snapshotResourceConfiguration } from './schema-helpers.js'
3
3
  import { buildStorageInfo, normalizeStorageConfig } from '../storage/storage-mapping.js'
4
+ import { compileQueryFields } from './query-field-helpers.js'
5
+ import { buildEffectiveSortList } from '../querying/query-field-sort-helpers.js'
6
+ import { assertScalarQueryField, assertFieldNameMap } from './field-utils.js'
7
+ import { validateRelationshipFieldNames, validateSchemaFieldNames } from './scope-validations.js'
4
8
 
5
9
  /**
6
- * Compiles and enriches schemas for a resource scope
7
- *
8
- * @param {Object} scope - Scope containing raw schema options
9
- * @param {Object} deps - Dependencies with context and hooks
10
- * @returns {Promise<void>} Resolves when compilation complete
11
- *
12
- * @example
13
- * // Input: Raw schema with computed field
14
- * const rawFields = {
15
- * title: { type: 'string', required: true },
16
- * content: { type: 'string' },
17
- * author_id: { belongsTo: 'users', as: 'author' }, // Missing type
18
- * word_count: {
19
- * type: 'number',
20
- * computed: true,
21
- * compute: (record) => record.content.split(' ').length
22
- * }
23
- * };
24
- *
25
- * await compileSchemas(scope, deps);
26
- *
27
- * // Output in scope.vars.schemaInfo:
28
- * // {
29
- * // schema: Schema {}, // json-rest-schema instance
30
- * // schemaStructure: {
31
- * // title: { type: 'string', required: true },
32
- * // content: { type: 'string' },
33
- * // author_id: { type: 'id', belongsTo: 'users', as: 'author' } // Type added!
34
- * // // Note: word_count removed (it's computed)
35
- * // },
36
- * // computed: {
37
- * // word_count: { type: 'number', computed: true, compute: [Function] }
38
- * // }
39
- * // }
10
+ * Compile one resource and publish its completed metadata in vars.schemaInfo.
11
+ * Raw configuration is snapshotted; computed fields are kept out of the
12
+ * writable schema. Plugins enrich attribute, search and computed definitions
13
+ * before callback dependencies, output membership and storage mappings compile.
14
+ * schema:compiled runs before the completed metadata snapshot is published.
40
15
  *
16
+ * @param {Object} scope
17
+ * @param {Object} scope.scopeOptions - Resource configuration
18
+ * @param {Object} scope.vars - Receives the compiled schemaInfo
19
+ * @param {Object} deps
20
+ * @param {Object} deps.context
21
+ * @param {string} deps.context.scopeName - Resource name
22
+ * @param {Function} deps.runHooks - Awaited schema enrichment/publication hooks
23
+ * @returns {Promise<void>}
41
24
  * @example
42
- * // Input: Schema with search fields
43
- * const rawFields = {
44
- * title: { type: 'string', search: true }, // Searchable
45
- * status: { type: 'string' }, // Not searchable
46
- * author_id: { belongsTo: 'users', as: 'author' }
47
- * };
48
- *
49
- * // Output: Auto-generated searchSchema
50
- * // searchSchemaObject contains:
51
- * // {
52
- * // title: {
53
- * // type: 'string',
54
- * // indexed: true, // Added for DB optimization
55
- * // filterOperator: '=' // Default operator
56
- * // }
57
- * // }
58
- *
59
- * @example
60
- * // Input: Pivot table auto-detection
61
- * const pivotSchema = {
62
- * article_id: { belongsTo: 'articles', as: 'article' },
63
- * tag_id: { belongsTo: 'tags', as: 'tag' },
64
- * display_order: { type: 'number' }
65
- * };
66
- *
67
- * // Output: Both foreign keys become searchable
68
- * // schemaStructure will have:
69
- * // {
70
- * // article_id: { type: 'id', belongsTo: 'articles', as: 'article', search: true },
71
- * // tag_id: { type: 'id', belongsTo: 'tags', as: 'tag', search: true },
72
- * // display_order: { type: 'number' }
73
- * // }
74
- * // This enables efficient many-to-many filtering!
75
- *
76
- * @example
77
- * // Input: Schema with dependent getters
78
- * const schema = {
79
- * first_name: { type: 'string' },
80
- * last_name: { type: 'string' },
81
- * full_name: {
82
- * type: 'string',
83
- * getter: (record) => `${record.first_name} ${record.last_name}`,
84
- * runGetterAfter: ['first_name', 'last_name'] // Dependencies
85
- * },
86
- * email: {
87
- * type: 'string',
88
- * setter: (value) => value.toLowerCase().trim()
89
- * }
90
- * };
91
- *
92
- * // Output includes dependency-sorted fields:
93
- * // {
94
- * // fieldGetters: { full_name: { getter: [Function], runGetterAfter: [...] } },
95
- * // sortedGetterFields: ['full_name'], // Topologically sorted
96
- * // fieldSetters: { email: { setter: [Function], runSetterAfter: [] } },
97
- * // sortedSetterFields: ['email']
98
- * // }
99
- *
100
- * @description
101
- * Used by:
102
- * - Every REST method calls this before any operations
103
- * - Compilation happens once per scope (cached)
104
- *
105
- * Purpose:
106
- * - Separates computed fields from database fields
107
- * - Adds missing types to belongsTo relationships
108
- * - Auto-detects pivot tables for many-to-many
109
- * - Generates search schemas from field markers
110
- * - Validates getter/setter dependencies
111
- * - Provides hooks for schema enrichment
112
- * - Caches everything for performance
113
- *
114
- * Data flow:
115
- * 1. Extracts computed fields to separate object
116
- * 2. Adds type:'id' to belongsTo fields
117
- * 3. Detects pivot tables (2+ foreign keys, 40%+ of fields)
118
- * 4. Runs schema:enrich hook for plugins
119
- * 5. Creates json-rest-schema validation instance
120
- * 6. Generates searchSchema from search:true fields
121
- * 7. Runs searchSchema:enrich hook
122
- * 8. Extracts and topologically sorts getters/setters
123
- * 9. Caches all results in scope.vars.schemaInfo
25
+ * // A title field remains in schemaInfo.schemaStructure and is validated by
26
+ * // schemaInfo.schemaInstance. A computed wordCount instead lives in
27
+ * // schemaInfo.computed; schemaInfo.outputFields contains both definitions.
28
+ * // The search schema, dependency orders and storageInfo share this publication.
124
29
  */
125
30
  export async function compileSchemas (scope, deps) {
126
31
  // Extract scopeName from context
127
32
  const { context, runHooks } = deps
128
33
  const scopeName = context.scopeName
129
34
 
35
+ const scopeOptions = snapshotResourceConfiguration(scope.scopeOptions || {})
36
+
130
37
  // Get raw schema
131
- const rawFields = scope.scopeOptions?.schema || {}
38
+ const rawFields = scopeOptions.schema || {}
39
+ validateSchemaFieldNames(rawFields, scopeName)
40
+ validateRelationshipFieldNames(scopeOptions.relationships, scopeName)
132
41
 
133
42
  // Extract computed fields from schema and build enriched schema
134
43
  const computedFields = {}
135
44
  const enrichedFields = {}
136
45
 
137
- for (const [fieldName, fieldDef] of Object.entries(rawFields)) {
46
+ for (const [fieldName, fieldDef] of Object.entries(snapshotResourceConfiguration(rawFields))) {
138
47
  if (fieldDef.computed === true) {
139
48
  // Extract computed field - copy entire definition
140
49
  computedFields[fieldName] = { ...fieldDef }
141
50
 
142
- // Validate computed field
143
- if (!fieldDef.type) {
144
- throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
145
- }
146
-
147
- if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
148
- throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
149
- }
150
-
151
51
  // Don't include computed fields in the validation schema
152
52
  } else {
153
53
  enrichedFields[fieldName] = { ...fieldDef }
@@ -161,34 +61,17 @@ export async function compileSchemas (scope, deps) {
161
61
  }
162
62
  }
163
63
 
164
- // ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
165
- // Auto-detect pivot tables and make their foreign key fields searchable
166
- // This is critical for many-to-many relationship operations which need to filter pivot records
167
- const fields = Object.entries(enrichedFields)
168
- const belongsToFields = fields.filter(([_, def]) => def.belongsTo)
169
-
170
- // If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
171
- const nonIdFields = fields.filter(([name, _]) => name !== 'id')
172
- const isProbablyPivot = belongsToFields.length >= 2 &&
173
- (nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4)
174
-
175
- if (isProbablyPivot) {
176
- // Make all belongsTo fields searchable for pivot operations
177
- for (const [, fieldDef] of belongsToFields) {
178
- if (!fieldDef.search) {
179
- fieldDef.search = true
180
- }
181
- }
182
- }
183
-
184
64
  // Hook: schema:enrich
185
65
  const schemaContext = {
186
66
  fields: enrichedFields, // Mutable
187
67
  originalFields: rawFields, // Read-only
68
+ queryFields: {},
188
69
  scopeName,
189
- scopeOptions: scope.scopeOptions || {}
70
+ scopeOptions
190
71
  }
191
72
  await runHooks('schema:enrich', schemaContext)
73
+ validateSchemaFieldNames(schemaContext.fields, scopeName)
74
+ assertFieldNameMap(schemaContext.queryFields, `query fields in '${scopeName}'`)
192
75
 
193
76
  // Create schema object
194
77
  const schemaObject = createSchema(schemaContext.fields)
@@ -203,38 +86,62 @@ export async function compileSchemas (scope, deps) {
203
86
  // while searchSchema can specify filterOperator: 'contains' or complex join configurations.
204
87
  const rawSearchFields = generateSearchSchemaFromSchema(
205
88
  schemaContext.fields,
206
- scope.scopeOptions.searchSchema
207
- )
89
+ scopeOptions.searchSchema
90
+ ) || {}
208
91
 
209
- let searchSchemaObject
210
- if (rawSearchFields) {
211
- // Mark all search fields as indexed for database optimization.
212
- // This ensures that any field used for filtering will have a database index created,
213
- // dramatically improving query performance. Without indexes, filtering large tables
214
- // would require full table scans. The storage plugin uses these hints to create indexes.
215
- // Example: A status field marked for search gets indexed: true, enabling efficient
216
- // WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
217
- ensureSearchFieldsAreIndexed(rawSearchFields)
218
-
219
- // Hook: searchSchema:enrich
220
- const searchSchemaContext = {
221
- fields: schemaContext.fields, // Mutable
222
- originalFields: rawSearchFields, // Read-only enriched schema
223
- scopeName
92
+ const searchSchemaContext = {
93
+ fields: snapshotResourceConfiguration(Object.fromEntries(Object.entries(rawSearchFields).map(([name, definition]) => [name, { ...definition }]))),
94
+ originalFields: rawSearchFields,
95
+ scopeName
96
+ }
97
+ await runHooks('searchSchema:enrich', searchSchemaContext)
98
+ assertFieldNameMap(searchSchemaContext.fields, `search schema in '${scopeName}'`)
99
+ ensureSearchFieldsAreIndexed(searchSchemaContext.fields)
100
+ const searchSchemaObject = createSchema(searchSchemaContext.fields)
101
+
102
+ // Derived fields must be rebuilt from the current candidate before publication.
103
+ const idProperty = scopeOptions.idProperty || scope.vars.idProperty || 'id'
104
+ const schemaRelationships = { ...(scopeOptions.relationships || {}) }
105
+ const computedSchemaContext = {
106
+ fields: snapshotResourceConfiguration(Object.fromEntries(Object.entries(computedFields).map(([name, definition]) => [name, { ...definition }]))),
107
+ originalFields: computedFields,
108
+ schemaStructure: schemaObject.structure,
109
+ searchSchemaStructure: searchSchemaObject.structure,
110
+ schemaRelationships,
111
+ idProperty,
112
+ scopeName,
113
+ scopeOptions
114
+ }
115
+ await runHooks('computedSchema:enrich', computedSchemaContext)
116
+ assertFieldNameMap(computedSchemaContext.fields, `computed fields in '${scopeName}'`)
117
+ validateRelationshipFieldNames(schemaRelationships, scopeName)
118
+ for (const [fieldName, fieldDef] of Object.entries(computedSchemaContext.fields)) {
119
+ if (fieldDef?.computed !== true) {
120
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must set computed: true`)
121
+ }
122
+ if (!fieldDef.type) {
123
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
124
+ }
125
+ if (fieldDef.compute !== undefined && typeof fieldDef.compute !== 'function') {
126
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
127
+ }
128
+ if (Object.hasOwn(schemaObject.structure, fieldName)) {
129
+ throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' conflicts with an attribute field`)
224
130
  }
225
- await runHooks('searchSchema:enrich', searchSchemaContext)
226
-
227
- // Create searchSchema object
228
- searchSchemaObject = createSchema(rawSearchFields)
229
- } else {
230
- searchSchemaObject = createSchema({})
231
131
  }
232
132
 
233
- // Build schemaRelationships including polymorphic fields from schema
234
- const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) }
235
-
236
- // Validate belongsTo fields
133
+ // Each public relationship name has one declaration.
134
+ const relationshipNames = new Set(Object.keys(schemaRelationships))
237
135
  for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
136
+ if (fieldDef.belongsToPolymorphic !== undefined) {
137
+ throw new Error(`Field '${fieldName}' in resource '${scopeName}' cannot declare belongsToPolymorphic; declare it in relationships`)
138
+ }
139
+ if (fieldDef.belongsTo && fieldDef.as) {
140
+ if (relationshipNames.has(fieldDef.as)) {
141
+ throw new Error(`Relationship name '${fieldDef.as}' in resource '${scopeName}' is declared more than once`)
142
+ }
143
+ relationshipNames.add(fieldDef.as)
144
+ }
238
145
  // Validate that belongsTo fields have 'as' property
239
146
  if (fieldDef.belongsTo && !fieldDef.as) {
240
147
  throw new Error(
@@ -245,88 +152,77 @@ export async function compileSchemas (scope, deps) {
245
152
  }
246
153
  }
247
154
 
248
- // Extract and validate getter definitions
249
- const fieldGetters = {}
250
- const getterFields = []
251
-
252
- for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
253
- if (fieldDef.getter && typeof fieldDef.getter === 'function') {
254
- fieldGetters[fieldName] = {
255
- getter: fieldDef.getter,
256
- runGetterAfter: fieldDef.runGetterAfter || [],
257
- fieldDef
258
- }
259
- getterFields.push(fieldName)
260
-
261
- // Validate dependencies exist
262
- if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
263
- for (const dep of fieldDef.runGetterAfter) {
264
- if (!schemaContext.fields[dep]) {
265
- throw new Error(
266
- `Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
267
- )
268
- }
269
- }
155
+ const queryFields = compileQueryFields(schemaContext.queryFields, {
156
+ scopeName,
157
+ schemaStructure: schemaObject.structure,
158
+ computed: computedSchemaContext.fields,
159
+ schemaRelationships,
160
+ idProperty
161
+ })
162
+ for (const [kind, definitions] of [['Computed', computedSchemaContext.fields], ['Query', queryFields]]) {
163
+ for (const [name, definition] of Object.entries(definitions)) {
164
+ if (definition.belongsTo !== undefined || definition.belongsToPolymorphic !== undefined) {
165
+ throw new Error(`${kind} field '${name}' in scope '${scopeName}' cannot declare relationships; use a stored backing field or the resource relationships map`)
270
166
  }
271
167
  }
272
168
  }
169
+ const fieldDependencies = compileFieldDependencies({
170
+ schemaStructure: schemaObject.structure,
171
+ computedFields: computedSchemaContext.fields,
172
+ queryFields,
173
+ schemaRelationships,
174
+ idProperty,
175
+ scopeName
176
+ })
273
177
 
274
- // Sort getters by dependencies
275
- let sortedGetterFields = []
276
- if (Object.keys(fieldGetters).length > 0) {
277
- try {
278
- sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
279
- } catch (error) {
280
- throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
178
+ const identityFields = new Set([...fieldDependencies.foreignKeyFields, 'id', idProperty])
179
+ for (const fieldName of identityFields) {
180
+ if (schemaObject.structure[fieldName]?.storage?.serialize !== undefined) {
181
+ throw new Error(`Field '${fieldName}' in scope '${scopeName}' cannot use storage.serialize: identity fields must preserve resource IDs and relationship types. Use normalizeId for resource ID canonicalization.`)
281
182
  }
282
183
  }
283
184
 
284
- // Extract and validate setter definitions
285
- const fieldSetters = {}
286
- const setterFields = []
287
-
288
- for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
289
- if (fieldDef.setter && typeof fieldDef.setter === 'function') {
290
- fieldSetters[fieldName] = {
291
- setter: fieldDef.setter,
292
- runSetterAfter: fieldDef.runSetterAfter || [],
293
- fieldDef
294
- }
295
- setterFields.push(fieldName)
296
-
297
- // Validate dependencies exist
298
- if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
299
- for (const dep of fieldDef.runSetterAfter) {
300
- if (!schemaContext.fields[dep]) {
301
- throw new Error(
302
- `Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
303
- )
304
- }
305
- }
306
- }
185
+ const querySchema = { schemaStructure: schemaObject.structure, searchSchemaStructure: searchSchemaObject.structure, queryFields }
186
+ for (const [filterName, definition] of Object.entries(searchSchemaObject.structure)) {
187
+ if (typeof definition.applyFilter === 'function' && !definition.oneOf) continue
188
+ for (const target of definition.oneOf || [definition.actualField || filterName]) {
189
+ assertScalarQueryField(schemaObject.structure[target] || queryFields[target], target, 'filter')
307
190
  }
308
191
  }
309
-
310
- // Sort setters by dependencies
311
- let sortedSetterFields = []
312
- if (Object.keys(fieldSetters).length > 0) {
313
- try {
314
- sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
315
- } catch (error) {
316
- throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
317
- }
192
+ buildEffectiveSortList(scopeOptions.sortableFields, { schemaInfo: querySchema })
193
+ buildEffectiveSortList([], { defaultSort: scopeOptions.defaultSort, schemaInfo: querySchema })
194
+ for (const [fieldName, definition] of Object.entries(queryFields)) {
195
+ if (definition.sortable) buildEffectiveSortList([fieldName], { schemaInfo: querySchema })
318
196
  }
319
197
 
320
- const idProperty = scope.scopeOptions.idProperty || scope.vars.idProperty || 'id'
321
- const storage = normalizeStorageConfig(scope.scopeOptions.storage)
198
+ const storage = normalizeStorageConfig(scopeOptions.storage)
322
199
  const storageInfo = buildStorageInfo({
323
200
  schemaStructure: schemaObject.structure,
324
201
  idProperty,
325
202
  storage
326
203
  })
327
204
 
205
+ const outputFields = { ...schemaObject.structure, ...computedSchemaContext.fields, ...queryFields }
206
+ const outputRelationships = { ...schemaRelationships }
207
+ for (const definition of Object.values(schemaObject.structure)) {
208
+ if (definition.as && definition.belongsTo) {
209
+ outputRelationships[definition.as] = definition
210
+ }
211
+ }
212
+
213
+ const versionField = scopeOptions.versionField
214
+ if (versionField !== undefined) {
215
+ const definition = typeof versionField === 'string' && Object.hasOwn(schemaObject.structure, versionField)
216
+ ? schemaObject.structure[versionField]
217
+ : undefined
218
+ if (!versionField || versionField === idProperty || !definition || definition.type !== 'string' ||
219
+ definition.computed || definition.virtual || definition.belongsTo || definition.getter || definition.setter || definition.storage?.serialize) {
220
+ throw new Error(`versionField in '${scopeName}' must name a stored string attribute without getters, setters, serializers or relationships, distinct from the primary ID`)
221
+ }
222
+ }
223
+
328
224
  // Cache everything
329
- scope.vars.schemaInfo = {
225
+ const schemaInfo = {
330
226
 
331
227
  schemaInstance: schemaObject,
332
228
  schemaStructure: schemaObject.structure,
@@ -334,18 +230,21 @@ export async function compileSchemas (scope, deps) {
334
230
  searchSchemaInstance: searchSchemaObject,
335
231
  searchSchemaStructure: searchSchemaObject.structure,
336
232
 
337
- computed: computedFields,
233
+ computed: computedSchemaContext.fields,
234
+ queryFields,
338
235
  schemaRelationships,
339
- tableName: scope.scopeOptions.tableName || scopeName,
236
+ outputFields,
237
+ outputRelationships,
238
+ ...(versionField === undefined ? {} : { versionField }),
239
+ tableName: scopeOptions.tableName || scopeName,
340
240
  idProperty,
341
241
  storage,
342
242
  storageInfo,
343
- indexes: Array.isArray(scope.scopeOptions.indexes) ? [...scope.scopeOptions.indexes] : [],
344
- foreignKeys: Array.isArray(scope.scopeOptions.foreignKeys) ? [...scope.scopeOptions.foreignKeys] : [],
345
- checkConstraints: Array.isArray(scope.scopeOptions.checkConstraints) ? [...scope.scopeOptions.checkConstraints] : [],
346
- fieldGetters,
347
- sortedGetterFields,
348
- fieldSetters,
349
- sortedSetterFields
243
+ indexes: Array.isArray(scopeOptions.indexes) ? [...scopeOptions.indexes] : [],
244
+ foreignKeys: Array.isArray(scopeOptions.foreignKeys) ? [...scopeOptions.foreignKeys] : [],
245
+ checkConstraints: Array.isArray(scopeOptions.checkConstraints) ? [...scopeOptions.checkConstraints] : [],
246
+ ...fieldDependencies
350
247
  }
248
+ await runHooks('schema:compiled', { scopeName, scopeOptions, schemaInfo })
249
+ scope.vars.schemaInfo = snapshotResourceConfiguration(schemaInfo)
351
250
  }