json-rest-api 1.0.23 → 1.0.25

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 (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. package/tests/test-development-guide.md +5 -5
@@ -1,5 +1,15 @@
1
- import { RestApiResourceError, RestApiValidationError, RestApiPayloadError } from '../../../lib/rest-api-errors.js'
2
- import { findRelationshipDefinition, handleWriteMethodError } from './common.js'
1
+ import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
2
+ import {
3
+ commitOwnedTransaction,
4
+ findRelationshipDefinition,
5
+ getVisibleRelationshipParent,
6
+ handleWriteMethodError,
7
+ validateRelationshipRoutePayload
8
+ } from './common.js'
9
+ import {
10
+ normalizeRelationshipIdentifiers,
11
+ requireExistingResourceId
12
+ } from '../lib/querying-writing/resource-id-normalization.js'
3
13
 
4
14
  /**
5
15
  * DELETE RELATIONSHIP
@@ -11,9 +21,13 @@ import { findRelationshipDefinition, handleWriteMethodError } from './common.js'
11
21
  * @param {array} relationshipData - Array of resource identifiers to remove
12
22
  * @returns {Promise<void>} 204 No Content
13
23
  */
14
- export default async function deleteRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api, log }) {
24
+ export default async function deleteRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
15
25
  context.method = 'deleteRelationship'
16
- context.id = params.id
26
+ context.id = requireExistingResourceId(params.id, {
27
+ scopeOptions,
28
+ vars,
29
+ scopeName
30
+ })
17
31
  context.relationshipName = params.relationshipName
18
32
  context.schemaInfo = scopes[scopeName].vars.schemaInfo
19
33
 
@@ -40,21 +54,28 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
40
54
  )
41
55
  }
42
56
 
43
- if (!Array.isArray(params.relationshipData)) {
44
- throw new RestApiPayloadError('DELETE from relationship requires array of resource identifiers')
45
- }
57
+ params.relationshipData = validateRelationshipRoutePayload({
58
+ context,
59
+ vars,
60
+ scopeName,
61
+ operation: 'deleteRelationship',
62
+ relationshipData: params.relationshipData
63
+ })
64
+ params.relationshipData = normalizeRelationshipIdentifiers(params.relationshipData, { api })
46
65
 
47
66
  // Check permissions
48
67
  await runHooks('checkPermissions')
49
68
  await runHooks('checkPermissionsDeleteRelationship')
50
69
 
51
- // Verify parent exists
52
- const exists = await helpers.dataExists({
70
+ // Verify the parent exists in the caller's visible dataset.
71
+ const parentRecord = await getVisibleRelationshipParent({
72
+ context,
73
+ helpers,
53
74
  scopeName,
54
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
75
+ runHooks
55
76
  })
56
77
 
57
- if (!exists) {
78
+ if (!parentRecord) {
58
79
  throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
59
80
  }
60
81
 
@@ -99,16 +120,14 @@ export default async function deleteRelationshipMethod ({ params, context, vars,
99
120
  },
100
121
  transaction: context.transaction,
101
122
  simplified: false
102
- })
123
+ }, { ...context })
103
124
  }
104
125
  }
105
126
 
106
127
  await runHooks('finish')
107
128
  await runHooks('finishDeleteRelationship')
108
129
 
109
- if (context.shouldCommit) {
110
- await context.transaction.commit()
111
- }
130
+ await commitOwnedTransaction(context, runHooks)
112
131
 
113
132
  // 204 No Content
114
133
  } catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
- import { handleWriteMethodError } from './common.js'
2
+ import { commitOwnedTransaction, handleWriteMethodError } from './common.js'
3
+ import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
3
4
 
4
5
  /**
5
6
  * DELETE
@@ -26,7 +27,11 @@ export default async function deleteMethod ({
26
27
  context.method = 'delete'
27
28
 
28
29
  // Set the ID in context
29
- context.id = params.id
30
+ context.id = requireExistingResourceId(params.id, {
31
+ scopeOptions,
32
+ vars,
33
+ scopeName
34
+ })
30
35
 
31
36
  // Set scopeName in context (needed for broadcasting)
32
37
  context.scopeName = scopeName
@@ -91,11 +96,7 @@ export default async function deleteMethod ({
91
96
  await runHooks('finish')
92
97
  await runHooks('finishDelete')
93
98
 
94
- // Commit transaction if we created it
95
- if (context.shouldCommit) {
96
- await context.transaction.commit()
97
- await runHooks('afterCommit')
98
- }
99
+ await commitOwnedTransaction(context, runHooks)
99
100
 
100
101
  // DELETE typically returns void/undefined (204 No Content)
101
102
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
- import { findRelationshipDefinition } from './common.js'
2
+ import { findRelationshipDefinition, getVisibleRelationshipParent } from './common.js'
3
3
  import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
4
+ import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
4
5
 
5
6
  /**
6
7
  * GET RELATED
@@ -12,9 +13,13 @@ import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
12
13
  * @param {object} queryParams - Standard query parameters
13
14
  * @returns {Promise<object>} Related resources with full data
14
15
  */
15
- export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
16
+ export default async function getRelatedMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api }) {
16
17
  context.method = 'getRelated'
17
- context.id = params.id
18
+ context.id = requireExistingResourceId(params.id, {
19
+ scopeOptions,
20
+ vars,
21
+ scopeName
22
+ })
18
23
  context.relationshipName = params.relationshipName
19
24
  context.queryParams = params.queryParams || {}
20
25
  context.schemaInfo = scopes[scopeName].vars.schemaInfo
@@ -52,13 +57,15 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
52
57
  await runHooks('checkPermissions')
53
58
  await runHooks('checkPermissionsGetRelated')
54
59
 
55
- // Verify parent exists
56
- const exists = await helpers.dataExists({
60
+ // Verify the parent exists in the caller's visible dataset.
61
+ const parentRecord = await getVisibleRelationshipParent({
62
+ context,
63
+ helpers,
57
64
  scopeName,
58
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
65
+ runHooks
59
66
  })
60
67
 
61
- if (!exists) {
68
+ if (!parentRecord) {
62
69
  throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
63
70
  }
64
71
 
@@ -97,7 +104,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
97
104
  transaction: context.transaction,
98
105
  simplified: false,
99
106
  isTransport: params.isTransport
100
- })
107
+ }, { ...context })
101
108
 
102
109
  const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
103
110
  if (!relatedId) {
@@ -114,7 +121,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
114
121
  transaction: context.transaction,
115
122
  simplified: false,
116
123
  isTransport: params.isTransport
117
- })
124
+ }, { ...context })
118
125
 
119
126
  return {
120
127
  links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
@@ -135,7 +142,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
135
142
  transaction: context.transaction,
136
143
  simplified: false,
137
144
  isTransport: params.isTransport
138
- })
145
+ }, { ...context })
139
146
 
140
147
  // Extract the related resource from the parent's relationships
141
148
  const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
@@ -189,7 +196,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
189
196
  transaction: context.transaction,
190
197
  simplified: false,
191
198
  isTransport: params.isTransport
192
- })
199
+ }, { ...context })
193
200
 
194
201
  if (!result.links) {
195
202
  result.links = {}
@@ -223,7 +230,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
223
230
  transaction: context.transaction,
224
231
  simplified: false,
225
232
  isTransport: params.isTransport
226
- })
233
+ }, { ...context })
227
234
 
228
235
  if (!result.links) {
229
236
  result.links = {}
@@ -251,7 +258,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
251
258
  transaction: context.transaction,
252
259
  simplified: false,
253
260
  isTransport: params.isTransport,
254
- })
261
+ }, { ...context })
255
262
  if (related?.data) {
256
263
  results.push(related.data)
257
264
  }
@@ -302,7 +309,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
302
309
  transaction: context.transaction,
303
310
  simplified: false,
304
311
  isTransport: params.isTransport
305
- })
312
+ }, { ...context })
306
313
  let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
307
314
 
308
315
  if (includedResources.length === 0) {
@@ -334,7 +341,7 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
334
341
  transaction: context.transaction,
335
342
  simplified: false,
336
343
  isTransport: params.isTransport,
337
- })
344
+ }, { ...context })
338
345
  if (related?.data) {
339
346
  includedResources.push(related.data)
340
347
  }
@@ -1,6 +1,7 @@
1
1
  import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
2
  import { findRelationshipDefinition } from './common.js'
3
3
  import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
4
+ import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
4
5
 
5
6
  /**
6
7
  * GET RELATIONSHIP
@@ -11,9 +12,13 @@ import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
11
12
  * @param {string} relationshipName - The name of the relationship
12
13
  * @returns {Promise<object>} Relationship linkage with links
13
14
  */
14
- export default async function getRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
15
+ export default async function getRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api }) {
15
16
  context.method = 'getRelationship'
16
- context.id = params.id
17
+ context.id = requireExistingResourceId(params.id, {
18
+ scopeOptions,
19
+ vars,
20
+ scopeName
21
+ })
17
22
  context.relationshipName = params.relationshipName
18
23
  context.schemaInfo = scopes[scopeName].vars.schemaInfo
19
24
 
@@ -41,7 +46,7 @@ export default async function getRelationshipMethod ({ params, context, vars, he
41
46
  transaction: context.transaction,
42
47
  simplified: false,
43
48
  isTransport: params.isTransport
44
- })
49
+ }, { ...context })
45
50
 
46
51
  if (!fullRecord || !fullRecord.data) {
47
52
  throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
@@ -3,6 +3,7 @@ import { normalizeRecordAttributes } from '../lib/querying-writing/database-valu
3
3
  import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
4
4
  import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
5
5
  import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
6
+ import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
6
7
  import { cascadeConfig } from './common.js'
7
8
 
8
9
  /**
@@ -62,10 +63,15 @@ export default async function getMethod ({
62
63
  includeDepthLimit: vars.includeDepthLimit,
63
64
  sortableFields: vars.sortableFields
64
65
  })
66
+ const normalizedId = requireExistingResourceId(params.id, {
67
+ scopeOptions,
68
+ vars,
69
+ scopeName
70
+ })
65
71
  const validatedRequest = validateRequestContractOrThrow(
66
72
  requestContracts.get,
67
73
  {
68
- id: params.id,
74
+ id: normalizedId,
69
75
  queryParams: context.queryParams
70
76
  },
71
77
  'GET request parameters are invalid'
@@ -1,3 +1,10 @@
1
+ import {
2
+ commitOwnedTransaction,
3
+ handleWriteMethodError,
4
+ validateRelationshipRoutePayload
5
+ } from './common.js'
6
+ import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
7
+
1
8
  /**
2
9
  * PATCH RELATIONSHIP
3
10
  * Completely replaces a relationship
@@ -8,10 +15,15 @@
8
15
  * @param {object|array} relationshipData - New relationship data
9
16
  * @returns {Promise<void>} 204 No Content
10
17
  */
11
- export default async function patchRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
18
+ export default async function patchRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
12
19
  context.method = 'patchRelationship'
13
- context.id = params.id
20
+ context.id = requireExistingResourceId(params.id, {
21
+ scopeOptions,
22
+ vars,
23
+ scopeName
24
+ })
14
25
  context.relationshipName = params.relationshipName
26
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo
15
27
 
16
28
  // Transaction handling
17
29
  context.transaction = params.transaction ||
@@ -24,6 +36,14 @@ export default async function patchRelationshipMethod ({ params, context, vars,
24
36
  await runHooks('checkPermissions')
25
37
  await runHooks('checkPermissionsPatchRelationship')
26
38
 
39
+ const relationshipData = validateRelationshipRoutePayload({
40
+ context,
41
+ vars,
42
+ scopeName,
43
+ operation: 'patchRelationship',
44
+ relationshipData: params.relationshipData
45
+ })
46
+
27
47
  // Reuse existing patch with relationship data
28
48
  await scope.patch({
29
49
  id: context.id,
@@ -32,24 +52,22 @@ export default async function patchRelationshipMethod ({ params, context, vars,
32
52
  type: scopeName,
33
53
  id: context.id,
34
54
  relationships: {
35
- [params.relationshipName]: { data: params.relationshipData }
55
+ [params.relationshipName]: { data: relationshipData }
36
56
  }
37
57
  }
38
58
  },
39
59
  transaction: context.transaction,
40
60
  simplified: false,
41
61
  isTransport: params.isTransport
42
- })
62
+ }, { ...context })
43
63
 
44
64
  await runHooks('finish')
45
65
  await runHooks('finishPatchRelationship')
46
66
 
47
- if (context.shouldCommit) {
48
- await context.transaction.commit()
49
- }
67
+ await commitOwnedTransaction(context, runHooks)
50
68
 
51
69
  // 204 No Content
52
70
  } catch (error) {
53
- await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log)
71
+ await handleWriteMethodError(error, context, 'PATCH_RELATIONSHIP', scopeName, log, runHooks)
54
72
  }
55
73
  }
@@ -3,6 +3,10 @@ import { processRelationships } from '../lib/writing/relationship-processor.js'
3
3
  import { updateManyToManyRelationship } from '../lib/writing/many-to-many-manipulations.js'
4
4
  import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
5
5
  import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
6
+ import {
7
+ requireDocumentResourceId,
8
+ requireExistingResourceId
9
+ } from '../lib/querying-writing/resource-id-normalization.js'
6
10
  import {
7
11
  setupCommonRequest,
8
12
  validateResourceAttributesBeforeWrite,
@@ -10,7 +14,8 @@ import {
10
14
  applyFieldSetters,
11
15
  validatePivotResource,
12
16
  handleRecordReturnAfterWrite,
13
- handleWriteMethodError
17
+ handleWriteMethodError,
18
+ commitOwnedTransaction
14
19
  } from './common.js'
15
20
 
16
21
  /**
@@ -59,14 +64,37 @@ export default async function patchMethod ({
59
64
  includeDepthLimit: vars.includeDepthLimit,
60
65
  sortableFields: vars.sortableFields
61
66
  })
67
+ const normalizedPathId = params.id === undefined
68
+ ? null
69
+ : requireExistingResourceId(params.id, {
70
+ scopeOptions,
71
+ vars,
72
+ scopeName
73
+ })
74
+
75
+ if (normalizedPathId && !context.inputRecord?.data?.id) {
76
+ context.inputRecord = {
77
+ ...context.inputRecord,
78
+ data: {
79
+ ...(context.inputRecord?.data || {}),
80
+ id: normalizedPathId
81
+ }
82
+ }
83
+ }
84
+
62
85
  context.inputRecord = validateRequestContractOrThrow(
63
86
  requestContracts.patch,
64
87
  context.inputRecord,
65
88
  'PATCH request body is invalid'
66
89
  )
67
- if (params.id && String(params.id) !== String(context.inputRecord.data.id)) {
90
+ const normalizedBodyId = requireDocumentResourceId(context.inputRecord.data.id, {
91
+ scopeOptions,
92
+ vars
93
+ })
94
+
95
+ if (normalizedPathId && normalizedPathId !== normalizedBodyId) {
68
96
  throw new RestApiValidationError(
69
- `ID mismatch. URL path ID '${params.id}' does not match request body ID '${context.inputRecord.data.id}'`,
97
+ `ID mismatch. URL path ID '${normalizedPathId}' does not match request body ID '${normalizedBodyId}'`,
70
98
  {
71
99
  fields: ['data.id'],
72
100
  violations: [{
@@ -77,7 +105,8 @@ export default async function patchMethod ({
77
105
  }
78
106
  )
79
107
  }
80
- context.id = params.id || context.inputRecord.data.id
108
+ context.inputRecord.data.id = normalizedBodyId
109
+ context.id = normalizedPathId || normalizedBodyId
81
110
 
82
111
  // Validate that user has read access to all related resources
83
112
  // This ensures users can only create relationships to resources they can access
@@ -194,11 +223,7 @@ export default async function patchMethod ({
194
223
  log
195
224
  })
196
225
 
197
- // Commit transaction if we created it
198
- if (context.shouldCommit) {
199
- await context.transaction.commit()
200
- await runHooks('afterCommit')
201
- }
226
+ await commitOwnedTransaction(context, runHooks)
202
227
 
203
228
  return ret
204
229
  } catch (error) {
@@ -1,6 +1,16 @@
1
- import { RestApiResourceError, RestApiValidationError, RestApiPayloadError } from '../../../lib/rest-api-errors.js'
2
- import { findRelationshipDefinition, handleWriteMethodError } from './common.js'
1
+ import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
2
+ import {
3
+ commitOwnedTransaction,
4
+ findRelationshipDefinition,
5
+ getVisibleRelationshipParent,
6
+ handleWriteMethodError,
7
+ validateRelationshipRoutePayload
8
+ } from './common.js'
3
9
  import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
10
+ import {
11
+ normalizeRelationshipIdentifiers,
12
+ requireExistingResourceId
13
+ } from '../lib/querying-writing/resource-id-normalization.js'
4
14
 
5
15
  /**
6
16
  * POST RELATIONSHIP
@@ -12,9 +22,13 @@ import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js
12
22
  * @param {array} relationshipData - Array of resource identifiers to add
13
23
  * @returns {Promise<void>} 204 No Content
14
24
  */
15
- export default async function postRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api, log }) {
25
+ export default async function postRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api, log }) {
16
26
  context.method = 'postRelationship'
17
- context.id = params.id
27
+ context.id = requireExistingResourceId(params.id, {
28
+ scopeOptions,
29
+ vars,
30
+ scopeName
31
+ })
18
32
  context.relationshipName = params.relationshipName
19
33
  context.schemaInfo = scopes[scopeName].vars.schemaInfo
20
34
 
@@ -42,21 +56,28 @@ export default async function postRelationshipMethod ({ params, context, vars, h
42
56
  )
43
57
  }
44
58
 
45
- if (!Array.isArray(params.relationshipData)) {
46
- throw new RestApiPayloadError('POST to relationship requires array of resource identifiers')
47
- }
59
+ params.relationshipData = validateRelationshipRoutePayload({
60
+ context,
61
+ vars,
62
+ scopeName,
63
+ operation: 'postRelationship',
64
+ relationshipData: params.relationshipData
65
+ })
66
+ params.relationshipData = normalizeRelationshipIdentifiers(params.relationshipData, { api })
48
67
 
49
68
  // Check permissions
50
69
  await runHooks('checkPermissions')
51
70
  await runHooks('checkPermissionsPostRelationship')
52
71
 
53
- // Verify parent exists
54
- const exists = await helpers.dataExists({
72
+ // Verify the parent exists in the caller's visible dataset.
73
+ const parentRecord = await getVisibleRelationshipParent({
74
+ context,
75
+ helpers,
55
76
  scopeName,
56
- context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo },
77
+ runHooks
57
78
  })
58
79
 
59
- if (!exists) {
80
+ if (!parentRecord) {
60
81
  throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
61
82
  }
62
83
 
@@ -75,7 +96,7 @@ export default async function postRelationshipMethod ({ params, context, vars, h
75
96
  through: relDef.through,
76
97
  foreignKey: relDef.foreignKey,
77
98
  otherKey: relDef.otherKey
78
- }, params.relationshipData, context.transaction)
99
+ }, params.relationshipData, context.transaction, context)
79
100
  }
80
101
  } else if (relDef.type === 'hasMany') {
81
102
  // Update foreign keys for hasMany
@@ -92,16 +113,14 @@ export default async function postRelationshipMethod ({ params, context, vars, h
92
113
  },
93
114
  transaction: context.transaction,
94
115
  simplified: false
95
- })
116
+ }, { ...context })
96
117
  }
97
118
  }
98
119
 
99
120
  await runHooks('finish')
100
121
  await runHooks('finishPostRelationship')
101
122
 
102
- if (context.shouldCommit) {
103
- await context.transaction.commit()
104
- }
123
+ await commitOwnedTransaction(context, runHooks)
105
124
 
106
125
  // 204 No Content
107
126
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { processRelationships } from '../lib/writing/relationship-processor.js'
2
2
  import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
3
3
  import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
4
+ import { requireDocumentResourceId } from '../lib/querying-writing/resource-id-normalization.js'
4
5
  import {
5
6
  setupCommonRequest,
6
7
  validateResourceAttributesBeforeWrite,
@@ -8,7 +9,8 @@ import {
8
9
  applyFieldSetters,
9
10
  validatePivotResource,
10
11
  handleRecordReturnAfterWrite,
11
- handleWriteMethodError
12
+ handleWriteMethodError,
13
+ commitOwnedTransaction
12
14
  } from './common.js'
13
15
 
14
16
  /**
@@ -62,6 +64,13 @@ export default async function postMethod ({
62
64
  'POST request body is invalid'
63
65
  )
64
66
 
67
+ if (context.inputRecord?.data?.id !== undefined) {
68
+ context.inputRecord.data.id = requireDocumentResourceId(context.inputRecord.data.id, {
69
+ scopeOptions,
70
+ vars
71
+ })
72
+ }
73
+
65
74
  // Validate that user has read access to all related resources
66
75
  // This ensures users can only create relationships to resources they can access
67
76
  await validateRelationshipAccess(context, context.inputRecord, helpers, api)
@@ -143,7 +152,7 @@ export default async function postMethod ({
143
152
 
144
153
  // Validate pivot resource exists
145
154
  validatePivotResource(scopes, relDef, relName)
146
- await createPivotRecords(api, context.id, relDef, relData, context.transaction)
155
+ await createPivotRecords(api, context.id, relDef, relData, context.transaction, context)
147
156
  }
148
157
 
149
158
  const ret = await handleRecordReturnAfterWrite({
@@ -160,11 +169,7 @@ export default async function postMethod ({
160
169
  log
161
170
  })
162
171
 
163
- // Commit transaction if we created it
164
- if (context.shouldCommit) {
165
- await context.transaction.commit()
166
- await runHooks('afterCommit')
167
- }
172
+ await commitOwnedTransaction(context, runHooks)
168
173
 
169
174
  return ret
170
175
  } catch (error) {