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,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) {
@@ -76,12 +77,12 @@ function buildFieldMapJsonSchemaFragment () {
76
77
 
77
78
  function buildKnownPageSchema () {
78
79
  return createSchema({
79
- number: { type: 'number' },
80
- size: { type: 'number' },
81
- limit: { type: 'number' },
82
- offset: { type: 'number' },
83
- after: { type: 'string' },
84
- before: { type: 'string' },
80
+ number: { type: 'integer', min: 1 },
81
+ size: { type: 'integer', min: 1 },
82
+ limit: { type: 'integer', min: 1 },
83
+ offset: { type: 'integer', min: 0 },
84
+ after: { type: 'string', min: 1 },
85
+ before: { type: 'string', min: 1 },
85
86
  cursor: { type: 'string' }
86
87
  })
87
88
  }
@@ -313,6 +314,16 @@ function installRequestContractSupport () {
313
314
  const firstError = pickFirstError(errors)
314
315
  context.throwParamError(firstError?.code || 'INVALID_PAGE', firstError?.message || 'Invalid page parameters.')
315
316
  }
317
+ for (const direction of ['after', 'before']) {
318
+ if (Object.hasOwn(knownPageParams, direction) && !validatedObject[direction]?.trim()) {
319
+ context.throwParamError('INVALID_PAGE', `page[${direction}] must contain a cursor.`)
320
+ }
321
+ }
322
+
323
+ if ((validatedObject.after !== undefined && validatedObject.before !== undefined) ||
324
+ (validatedObject.number !== undefined && (validatedObject.after !== undefined || validatedObject.before !== undefined))) {
325
+ context.throwParamError('INVALID_PAGE', 'Use either page[number], page[after], or page[before].')
326
+ }
316
327
 
317
328
  return {
318
329
  ...context.value,
@@ -335,18 +346,9 @@ function installRequestContractSupport () {
335
346
 
336
347
  function buildWritableAttributesStructure (schemaInfo = {}) {
337
348
  const schemaStructure = schemaInfo.schemaStructure || {}
338
- const schemaRelationships = schemaInfo.schemaRelationships || {}
339
349
  const idProperty = schemaInfo.idProperty || 'id'
340
350
  const attributesStructure = {}
341
- const excludedFieldNames = new Set()
342
-
343
- for (const relDef of Object.values(schemaRelationships)) {
344
- if (!relDef?.belongsToPolymorphic) continue
345
-
346
- const { typeField, idField } = relDef.belongsToPolymorphic
347
- if (typeField) excludedFieldNames.add(typeField)
348
- if (idField) excludedFieldNames.add(idField)
349
- }
351
+ const excludedFieldNames = schemaInfo.foreignKeyFields
350
352
 
351
353
  for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
352
354
  if (!fieldDef) continue
@@ -354,7 +356,6 @@ function buildWritableAttributesStructure (schemaInfo = {}) {
354
356
  if (excludedFieldNames.has(fieldName)) continue
355
357
  if (fieldDef.computed === true) continue
356
358
  if (fieldDef.type === undefined) continue
357
- if (fieldDef.belongsTo && fieldDef.as) continue
358
359
  attributesStructure[fieldName] = { ...fieldDef }
359
360
  }
360
361
 
@@ -376,14 +377,14 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
376
377
  return relDef.belongsToPolymorphic.types
377
378
  }
378
379
 
379
- if (relDef?.target) {
380
- return [relDef.target]
381
- }
382
-
383
380
  if (relDef?.belongsTo) {
384
381
  return [relDef.belongsTo]
385
382
  }
386
383
 
384
+ if (relDef?.target) {
385
+ return [relDef.target]
386
+ }
387
+
387
388
  if (relDef?.type === 'manyToMany' || relDef?.type === 'hasMany' || relDef?.type === 'hasOne') {
388
389
  return [relDef.target || relName]
389
390
  }
@@ -392,28 +393,9 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
392
393
  }
393
394
 
394
395
  function buildRelationshipStructure (schemaInfo = {}) {
395
- const schemaStructure = schemaInfo.schemaStructure || {}
396
- const schemaRelationships = schemaInfo.schemaRelationships || {}
397
396
  const relationshipStructure = {}
398
397
 
399
- for (const [, fieldDef] of Object.entries(schemaStructure)) {
400
- if (!fieldDef?.belongsTo || !fieldDef?.as) continue
401
-
402
- relationshipStructure[fieldDef.as] = {
403
- type: 'object',
404
- schema: createSchema({
405
- data: {
406
- type: 'jsonApiRelationshipData',
407
- required: true,
408
- nullable: true,
409
- cardinality: 'one',
410
- allowedTypes: [fieldDef.belongsTo]
411
- }
412
- })
413
- }
414
- }
415
-
416
- for (const [relName, relDef] of Object.entries(schemaRelationships)) {
398
+ for (const [relName, relDef] of Object.entries(schemaInfo.outputRelationships || {})) {
417
399
  const cardinality = getRelationshipCardinality(relDef)
418
400
  relationshipStructure[relName] = {
419
401
  type: 'object',
@@ -473,7 +455,10 @@ function buildWriteDocumentContract (scopeName, schemaInfo, mode) {
473
455
  }
474
456
 
475
457
  const schema = createSchema({
476
- data: dataFieldDefinition
458
+ data: dataFieldDefinition,
459
+ meta: { type: 'object', additionalProperties: true },
460
+ links: { type: 'object', additionalProperties: true },
461
+ jsonapi: { type: 'object', additionalProperties: true }
477
462
  })
478
463
 
479
464
  return {
@@ -583,17 +568,19 @@ function buildRequestContracts ({ scopeName, schemaInfo, includeDepthLimit, sort
583
568
  }
584
569
  }
585
570
 
586
- function buildContractCacheKey ({ includeDepthLimit, sortableFields }) {
571
+ function buildContractCacheKey ({ scopeName, includeDepthLimit, sortableFields }) {
587
572
  return JSON.stringify({
573
+ scopeName,
588
574
  includeDepthLimit,
589
575
  sortableFields: Array.isArray(sortableFields) ? [...sortableFields].sort() : []
590
576
  })
591
577
  }
592
578
 
579
+ /** @param {{ scopeName: string, schemaInfo: object, includeDepthLimit?: number, sortableFields?: string[] }} options */
593
580
  export function getRequestContracts ({ scopeName, schemaInfo, includeDepthLimit = 3, sortableFields = [] }) {
594
581
  installRequestContractSupport()
595
582
 
596
- const cacheKey = buildContractCacheKey({ includeDepthLimit, sortableFields })
583
+ const cacheKey = buildContractCacheKey({ scopeName, includeDepthLimit, sortableFields })
597
584
  if (schemaInfo.requestContracts && schemaInfo.requestContractsCacheKey === cacheKey) {
598
585
  return schemaInfo.requestContracts
599
586
  }
@@ -619,6 +606,10 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
619
606
  throw new Error('Invalid request contract.')
620
607
  }
621
608
 
609
+ if (!isPlainObject(payload)) {
610
+ throw new RestApiValidationError('document must be an object', { fields: ['document'] })
611
+ }
612
+
622
613
  const { validatedObject, errors } = schema[mode](payload)
623
614
  if (Object.keys(errors).length === 0) {
624
615
  return validatedObject
@@ -637,3 +628,77 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
637
628
  violations
638
629
  })
639
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
+ }