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,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
  }