json-rest-api 1.0.29 → 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 (292) 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 +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} +74 -30
  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 -177
  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 +48 -317
  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 +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
  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 +100 -290
  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 +93 -38
  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 +128 -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 -2485
  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 -1077
  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 -395
  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 -148
  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 +36 -68
  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 +37 -81
  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 -1247
  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 -148
  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 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  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-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. 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
+ }