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,5 +1,6 @@
1
1
  import { addType, addValidator, createSchema } from 'json-rest-schema'
2
2
  import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
3
+ import { requireDocumentResourceId, requireExistingResourceId } from './resource-id-normalization.js'
3
4
  import { getRelationshipCardinality } from './relationship-contracts.js'
4
5
 
5
6
  function isPlainObject (value) {
@@ -345,18 +346,9 @@ function installRequestContractSupport () {
345
346
 
346
347
  function buildWritableAttributesStructure (schemaInfo = {}) {
347
348
  const schemaStructure = schemaInfo.schemaStructure || {}
348
- const schemaRelationships = schemaInfo.schemaRelationships || {}
349
349
  const idProperty = schemaInfo.idProperty || 'id'
350
350
  const attributesStructure = {}
351
- const excludedFieldNames = new Set()
352
-
353
- for (const relDef of Object.values(schemaRelationships)) {
354
- if (!relDef?.belongsToPolymorphic) continue
355
-
356
- const { typeField, idField } = relDef.belongsToPolymorphic
357
- if (typeField) excludedFieldNames.add(typeField)
358
- if (idField) excludedFieldNames.add(idField)
359
- }
351
+ const excludedFieldNames = schemaInfo.foreignKeyFields
360
352
 
361
353
  for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
362
354
  if (!fieldDef) continue
@@ -364,7 +356,6 @@ function buildWritableAttributesStructure (schemaInfo = {}) {
364
356
  if (excludedFieldNames.has(fieldName)) continue
365
357
  if (fieldDef.computed === true) continue
366
358
  if (fieldDef.type === undefined) continue
367
- if (fieldDef.belongsTo && fieldDef.as) continue
368
359
  attributesStructure[fieldName] = { ...fieldDef }
369
360
  }
370
361
 
@@ -386,14 +377,14 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
386
377
  return relDef.belongsToPolymorphic.types
387
378
  }
388
379
 
389
- if (relDef?.target) {
390
- return [relDef.target]
391
- }
392
-
393
380
  if (relDef?.belongsTo) {
394
381
  return [relDef.belongsTo]
395
382
  }
396
383
 
384
+ if (relDef?.target) {
385
+ return [relDef.target]
386
+ }
387
+
397
388
  if (relDef?.type === 'manyToMany' || relDef?.type === 'hasMany' || relDef?.type === 'hasOne') {
398
389
  return [relDef.target || relName]
399
390
  }
@@ -402,28 +393,9 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
402
393
  }
403
394
 
404
395
  function buildRelationshipStructure (schemaInfo = {}) {
405
- const schemaStructure = schemaInfo.schemaStructure || {}
406
- const schemaRelationships = schemaInfo.schemaRelationships || {}
407
396
  const relationshipStructure = {}
408
397
 
409
- for (const [, fieldDef] of Object.entries(schemaStructure)) {
410
- if (!fieldDef?.belongsTo || !fieldDef?.as) continue
411
-
412
- relationshipStructure[fieldDef.as] = {
413
- type: 'object',
414
- schema: createSchema({
415
- data: {
416
- type: 'jsonApiRelationshipData',
417
- required: true,
418
- nullable: true,
419
- cardinality: 'one',
420
- allowedTypes: [fieldDef.belongsTo]
421
- }
422
- })
423
- }
424
- }
425
-
426
- for (const [relName, relDef] of Object.entries(schemaRelationships)) {
398
+ for (const [relName, relDef] of Object.entries(schemaInfo.outputRelationships || {})) {
427
399
  const cardinality = getRelationshipCardinality(relDef)
428
400
  relationshipStructure[relName] = {
429
401
  type: 'object',
@@ -483,7 +455,10 @@ function buildWriteDocumentContract (scopeName, schemaInfo, mode) {
483
455
  }
484
456
 
485
457
  const schema = createSchema({
486
- data: dataFieldDefinition
458
+ data: dataFieldDefinition,
459
+ meta: { type: 'object', additionalProperties: true },
460
+ links: { type: 'object', additionalProperties: true },
461
+ jsonapi: { type: 'object', additionalProperties: true }
487
462
  })
488
463
 
489
464
  return {
@@ -593,17 +568,19 @@ function buildRequestContracts ({ scopeName, schemaInfo, includeDepthLimit, sort
593
568
  }
594
569
  }
595
570
 
596
- function buildContractCacheKey ({ includeDepthLimit, sortableFields }) {
571
+ function buildContractCacheKey ({ scopeName, includeDepthLimit, sortableFields }) {
597
572
  return JSON.stringify({
573
+ scopeName,
598
574
  includeDepthLimit,
599
575
  sortableFields: Array.isArray(sortableFields) ? [...sortableFields].sort() : []
600
576
  })
601
577
  }
602
578
 
579
+ /** @param {{ scopeName: string, schemaInfo: object, includeDepthLimit?: number, sortableFields?: string[] }} options */
603
580
  export function getRequestContracts ({ scopeName, schemaInfo, includeDepthLimit = 3, sortableFields = [] }) {
604
581
  installRequestContractSupport()
605
582
 
606
- const cacheKey = buildContractCacheKey({ includeDepthLimit, sortableFields })
583
+ const cacheKey = buildContractCacheKey({ scopeName, includeDepthLimit, sortableFields })
607
584
  if (schemaInfo.requestContracts && schemaInfo.requestContractsCacheKey === cacheKey) {
608
585
  return schemaInfo.requestContracts
609
586
  }
@@ -629,6 +606,10 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
629
606
  throw new Error('Invalid request contract.')
630
607
  }
631
608
 
609
+ if (!isPlainObject(payload)) {
610
+ throw new RestApiValidationError('document must be an object', { fields: ['document'] })
611
+ }
612
+
632
613
  const { validatedObject, errors } = schema[mode](payload)
633
614
  if (Object.keys(errors).length === 0) {
634
615
  return validatedObject
@@ -647,3 +628,77 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
647
628
  violations
648
629
  })
649
630
  }
631
+
632
+ /** Select input by argument name, independently of the response format. */
633
+ export function selectWriteInput (params) {
634
+ const hasData = Object.hasOwn(params, 'data')
635
+ const hasDocument = Object.hasOwn(params, 'document')
636
+ if (hasData === hasDocument) {
637
+ throw new RestApiValidationError('Supply exactly one of data or document', { fields: ['data', 'document'] })
638
+ }
639
+ if (hasDocument) {
640
+ if (!isPlainObject(params.document)) {
641
+ throw new RestApiValidationError('document must be an object', { fields: ['document'] })
642
+ }
643
+ const unsupported = ['errors', 'included'].filter(key => Object.hasOwn(params.document, key))
644
+ if (unsupported.length) {
645
+ throw new RestApiValidationError('Write documents do not support errors or included resources', {
646
+ fields: unsupported.map(key => `document.${key}`)
647
+ })
648
+ }
649
+ }
650
+ return hasDocument ? 'document' : 'data'
651
+ }
652
+
653
+ /** Validate PUT/PATCH identity and body, then store the normalized document and ID on context. */
654
+ export function validateUpdateRequest ({ method, params, context, vars, scopeOptions, scopeName }) {
655
+ const requestContracts = getRequestContracts({
656
+ scopeName,
657
+ schemaInfo: context.schemaInfo,
658
+ includeDepthLimit: vars.includeDepthLimit,
659
+ sortableFields: vars.sortableFields
660
+ })
661
+ const normalizedPathId = params.id === undefined
662
+ ? null
663
+ : requireExistingResourceId(params.id, {
664
+ scopeOptions,
665
+ vars,
666
+ scopeName
667
+ })
668
+
669
+ if (normalizedPathId && context.inputRecord?.data?.id === undefined) {
670
+ context.inputRecord = {
671
+ ...context.inputRecord,
672
+ data: {
673
+ ...(context.inputRecord?.data || {}),
674
+ id: normalizedPathId
675
+ }
676
+ }
677
+ }
678
+
679
+ context.inputRecord = validateRequestContractOrThrow(
680
+ requestContracts[method],
681
+ context.inputRecord,
682
+ `${method.toUpperCase()} request body is invalid`
683
+ )
684
+ const normalizedBodyId = requireDocumentResourceId(context.inputRecord.data.id, {
685
+ scopeOptions,
686
+ vars
687
+ })
688
+
689
+ if (normalizedPathId && normalizedPathId !== normalizedBodyId) {
690
+ throw new RestApiValidationError(
691
+ `ID mismatch. URL path ID '${normalizedPathId}' does not match request body ID '${normalizedBodyId}'`,
692
+ {
693
+ fields: ['data.id'],
694
+ violations: [{
695
+ field: 'data.id',
696
+ rule: 'id_consistency',
697
+ message: 'Request body ID must match URL path ID when both are provided'
698
+ }]
699
+ }
700
+ )
701
+ }
702
+ context.inputRecord.data.id = normalizedBodyId
703
+ context.id = normalizedPathId || normalizedBodyId
704
+ }
@@ -1,8 +1,17 @@
1
+ // @ts-check
2
+
3
+ /** @import { RestApiPluginOptions } from '../../../../types/plugin-options.js' */
4
+ /** @typedef {Pick<RestApiPluginOptions, 'normalizeId'>} NormalizerConfig */
5
+ /** @typedef {{ scopeOptions?: NormalizerConfig | null, vars?: NormalizerConfig | null, scopeName?: string }} NormalizationOptions */
6
+ /** @typedef {{ resources?: Record<string, { scopeOptions?: NormalizerConfig | null, _scopeOptions?: NormalizerConfig | null, vars?: NormalizerConfig | null }> }} NormalizationApi */
7
+ /** @typedef {{ api?: NormalizationApi }} ReferenceOptions */
8
+
1
9
  import {
2
10
  RestApiResourceError,
3
11
  RestApiValidationError
4
12
  } from '../../../../lib/rest-api-errors.js'
5
13
 
14
+ /** @param {unknown} value @returns {string | null} */
6
15
  export function defaultNormalizeResourceId (value) {
7
16
  if (value === null || value === undefined) {
8
17
  return null
@@ -21,10 +30,10 @@ export function defaultNormalizeResourceId (value) {
21
30
  return String(value)
22
31
  }
23
32
 
24
- const normalized = String(value).trim()
25
- return normalized || null
33
+ return null
26
34
  }
27
35
 
36
+ /** @param {NormalizationOptions} [options] @returns {NonNullable<RestApiPluginOptions["normalizeId"]>} */
28
37
  export function resolveResourceIdNormalizer ({ scopeOptions = null, vars = null } = {}) {
29
38
  if (typeof scopeOptions?.normalizeId === 'function') {
30
39
  return scopeOptions.normalizeId
@@ -37,11 +46,13 @@ export function resolveResourceIdNormalizer ({ scopeOptions = null, vars = null
37
46
  return defaultNormalizeResourceId
38
47
  }
39
48
 
49
+ /** @param {unknown} value @param {NormalizationOptions} [options] @returns {string | null} */
40
50
  export function normalizeResourceId (value, options = {}) {
41
51
  const normalizer = resolveResourceIdNormalizer(options)
42
52
  return defaultNormalizeResourceId(normalizer(value))
43
53
  }
44
54
 
55
+ /** @param {unknown} value @param {NormalizationOptions} [options] @returns {string} */
45
56
  export function requireExistingResourceId (value, {
46
57
  scopeOptions = null,
47
58
  vars = null,
@@ -57,11 +68,12 @@ export function requireExistingResourceId (value, {
57
68
  {
58
69
  subtype: 'not_found',
59
70
  resourceType: scopeName,
60
- resourceId: value == null ? null : String(value)
71
+ resourceId: typeof value === 'string' ? value : defaultNormalizeResourceId(value)
61
72
  }
62
73
  )
63
74
  }
64
75
 
76
+ /** @param {unknown} value @param {NormalizationOptions} [options] @returns {string} */
65
77
  export function requireDocumentResourceId (value, {
66
78
  scopeOptions = null,
67
79
  vars = null
@@ -84,6 +96,7 @@ export function requireDocumentResourceId (value, {
84
96
  )
85
97
  }
86
98
 
99
+ /** @param {string} resourceType @param {ReferenceOptions} [options] @returns {NormalizationOptions} */
87
100
  function resolveResourceNormalizationOptions (resourceType, { api } = {}) {
88
101
  const scope = api?.resources?.[resourceType]
89
102
  return {
@@ -93,6 +106,7 @@ function resolveResourceNormalizationOptions (resourceType, { api } = {}) {
93
106
  }
94
107
  }
95
108
 
109
+ /** @param {string} resourceType @param {unknown} value @param {ReferenceOptions} [options] @returns {string} */
96
110
  export function requireReferencedResourceId (resourceType, value, { api } = {}) {
97
111
  return requireExistingResourceId(
98
112
  value,
@@ -100,12 +114,15 @@ export function requireReferencedResourceId (resourceType, value, { api } = {})
100
114
  )
101
115
  }
102
116
 
117
+ /** @param {unknown} identifier @param {ReferenceOptions} [options] @returns {unknown} */
103
118
  function normalizeRelationshipIdentifier (identifier, { api } = {}) {
104
119
  if (!identifier || typeof identifier !== 'object' || Array.isArray(identifier)) {
105
120
  return identifier
106
121
  }
107
122
 
108
- const type = typeof identifier.type === 'string' ? identifier.type : ''
123
+ // Object shape is known here; its property values remain unvalidated.
124
+ const record = /** @type {Record<string, unknown>} */ (identifier)
125
+ const type = typeof record.type === 'string' ? record.type : ''
109
126
  if (!type || !Object.hasOwn(identifier, 'id')) {
110
127
  return {
111
128
  ...identifier
@@ -114,10 +131,11 @@ function normalizeRelationshipIdentifier (identifier, { api } = {}) {
114
131
 
115
132
  return {
116
133
  ...identifier,
117
- id: requireReferencedResourceId(type, identifier.id, { api })
134
+ id: requireReferencedResourceId(type, record.id, { api })
118
135
  }
119
136
  }
120
137
 
138
+ /** @param {unknown} relationshipData @param {ReferenceOptions} [options] @returns {unknown} */
121
139
  export function normalizeRelationshipIdentifiers (relationshipData, { api } = {}) {
122
140
  if (Array.isArray(relationshipData)) {
123
141
  return relationshipData.map((identifier) => normalizeRelationshipIdentifier(identifier, { api }))
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
3
+
4
+ /** @import { ResourceFormat, WriteReturning } from '../../../../types/representations.js' */
5
+
6
+ /** @param {unknown} [value] @param {unknown} [fallback] @returns {ResourceFormat} */
7
+ export function resolveFormat (value, fallback = 'plain') {
8
+ const format = value === undefined ? fallback : value
9
+ if (format !== 'plain' && format !== 'jsonapi') {
10
+ throw new RestApiValidationError("format must be 'plain' or 'jsonapi'", { fields: ['format'] })
11
+ }
12
+ return format
13
+ }
14
+
15
+ /** @param {unknown} [value] @param {unknown} [fallback] @returns {WriteReturning} */
16
+ export function resolveReturning (value, fallback = 'full') {
17
+ const returning = value === undefined ? fallback : value
18
+ if (returning !== 'none' && returning !== 'minimal' && returning !== 'full') {
19
+ throw new RestApiValidationError("returning must be 'none', 'minimal', or 'full'", { fields: ['returning'] })
20
+ }
21
+ return returning
22
+ }
23
+
24
+ /** @param {object} options @returns {void} */
25
+ export function rejectRemovedOptions (options) {
26
+ const removedInput = ['inputRecord', 'inputRecords'].filter(name => Object.hasOwn(options, name))
27
+ if (removedInput.length) {
28
+ throw new RestApiValidationError(`Removed input options: ${removedInput.join(', ')}. Use data or document.`, { fields: removedInput })
29
+ }
30
+ const fields = ['simplified', 'simplifiedApi', 'simplifiedTransport', 'returnFullRecord', 'returnRecordApi', 'returnRecordTransport', 'isTransport']
31
+ .filter(name => Object.hasOwn(options, name))
32
+ if (fields.length) {
33
+ throw new RestApiValidationError(`Removed options: ${fields.join(', ')}. Use format and returning.`, { fields })
34
+ }
35
+ }