json-rest-api 1.0.7 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,193 @@
1
+ import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
2
+ import { validatePatchPayload } from '../lib/querying-writing/payload-validators.js';
3
+ import { processRelationships } from '../lib/writing/relationship-processor.js';
4
+ import { updateManyToManyRelationship } from '../lib/writing/many-to-many-manipulations.js';
5
+ import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js';
6
+ import {
7
+ setupCommonRequest,
8
+ validateResourceAttributesBeforeWrite,
9
+ validateRelationshipAccess,
10
+ applyFieldSetters,
11
+ validatePivotResource,
12
+ handleRecordReturnAfterWrite,
13
+ handleWriteMethodError
14
+ } from './common.js';
15
+
16
+ /**
17
+ * PATCH
18
+ * Performs a partial update on an existing resource's attributes or relationships.
19
+ * Unlike PUT, PATCH only updates the fields provided, leaving other fields unchanged.
20
+ * This method supports updating both attributes and relationships (1:1 and n:n).
21
+ * For relationships, only the ones explicitly provided will be updated.
22
+ * Just like PUT, it CANNOT have the `included` array in data.
23
+ */
24
+ export default async function patchMethod({
25
+ params,
26
+ context,
27
+ vars,
28
+ helpers,
29
+ scope,
30
+ scopes,
31
+ runHooks,
32
+ apiOptions,
33
+ pluginOptions,
34
+ scopeOptions,
35
+ scopeName,
36
+ api,
37
+ log
38
+ }) {
39
+ context.method = 'patch';
40
+
41
+ try {
42
+ const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
43
+ params,
44
+ context,
45
+ vars,
46
+ scopes,
47
+ scopeOptions,
48
+ scopeName,
49
+ api,
50
+ helpers
51
+ });
52
+ context.id = context.inputRecord.data.id;
53
+
54
+ // Run early hooks for pre-processing (e.g., file handling)
55
+ await runHooks('beforeProcessing');
56
+ await runHooks('beforeProcessingPatch');
57
+
58
+ // Validate PATCH payload to ensure the partial update actually contains changes.
59
+ // PATCH requests must include either attributes to update or relationships to modify -
60
+ // an empty PATCH is invalid. This prevents accidental no-op requests and ensures clients
61
+ // are explicit about what they want to change. Unlike PUT, PATCH preserves all fields
62
+ // not mentioned in the request.
63
+ // Example: data must have either attributes: {title: 'New'} or relationships: {author: {...}}
64
+ validatePatchPayload(context.inputRecord, scopes);
65
+
66
+ // Validate that user has read access to all related resources
67
+ // This ensures users can only create relationships to resources they can access
68
+ await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api);
69
+
70
+ // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
71
+ // Example: relationships.author -> author_id: '123' for storage
72
+ // Example: relationships.tags -> array of pivot records to create later (only for provided relationships in PATCH)
73
+ const { belongsToUpdates, manyToManyRelationships } = processRelationships(
74
+ scope,
75
+ { context }
76
+ );
77
+
78
+ await validateResourceAttributesBeforeWrite({
79
+ context,
80
+ schema,
81
+ belongsToUpdates,
82
+ runHooks,
83
+ isPartialValidation: true
84
+ });
85
+
86
+ // Fetch minimal record for authorization checks
87
+ const minimalRecord = await helpers.dataGetMinimal({
88
+ scopeName,
89
+ context,
90
+ runHooks
91
+ });
92
+
93
+ if (!minimalRecord) {
94
+ throw new RestApiResourceError(
95
+ `Resource not found: ${scopeName}/${context.id}`,
96
+ ERROR_SUBTYPES.NOT_FOUND
97
+ );
98
+ }
99
+
100
+ context.minimalRecord = minimalRecord;
101
+
102
+ // Centralised checkPermissions function
103
+ await scope.checkPermissions({
104
+ method: 'patch',
105
+ auth: context.auth,
106
+ id: context.id,
107
+ minimalRecord: context.minimalRecord,
108
+ transaction: context.transaction
109
+ });
110
+
111
+ // Merge belongsTo updates into attributes before patching the record
112
+ if (Object.keys(belongsToUpdates).length > 0) {
113
+ context.inputRecord.data.attributes = {
114
+ ...context.inputRecord.data.attributes,
115
+ ...belongsToUpdates
116
+ };
117
+ }
118
+
119
+ await runHooks('beforeDataCall');
120
+ await runHooks('beforeDataCallPatch');
121
+
122
+ // Apply field setters after validation and before storage
123
+ if (context.inputRecord?.data?.attributes) {
124
+ context.inputRecord.data.attributes = await applyFieldSetters(
125
+ context.inputRecord.data.attributes,
126
+ context.schemaInfo,
127
+ context,
128
+ api,
129
+ helpers
130
+ );
131
+ }
132
+
133
+ // Call the storage helper - should return the patched record
134
+ await helpers.dataPatch({
135
+ scopeName,
136
+ context
137
+ });
138
+
139
+ await runHooks('afterDataCallPatch');
140
+ await runHooks('afterDataCall');
141
+
142
+ // Process many-to-many relationships after main record update
143
+ // For PATCH, we only update the relationships that were explicitly provided
144
+ for (const { relName, relDef, relData } of manyToManyRelationships) {
145
+
146
+ // Validate pivot resource exists
147
+ validatePivotResource(scopes, relDef, relName);
148
+
149
+ // Update many-to-many relationships using intelligent synchronization that preserves pivot data.
150
+ // This compares current relationships with desired state: removes records no longer needed,
151
+ // adds new relationships, and crucially preserves existing pivot records with their metadata
152
+ // (like created_at timestamps or extra pivot fields). This is superior to delete-all-recreate
153
+ // because it maintains audit trails and custom pivot data.
154
+ // Example: If article has tags [1,2,3] and you update to [2,3,4], it keeps the pivot records
155
+ // for tags 2&3 (preserving their created_at), deletes tag 1, and adds new record for tag 4.
156
+ // Example: If article has tags [1,2,3] and update sends [2,3,4], tag 1 is removed, tags 2,3 kept, tag 4 added
157
+ await updateManyToManyRelationship(null, {
158
+ api,
159
+ context: {
160
+ resourceId: context.id,
161
+ relDef,
162
+ relData,
163
+ transaction: context.transaction
164
+ }
165
+ });
166
+ }
167
+
168
+ const ret = await handleRecordReturnAfterWrite({
169
+ context,
170
+ scopeName,
171
+ api,
172
+ scopes,
173
+ schemaStructure,
174
+ schemaRelationships,
175
+ scopeOptions,
176
+ vars,
177
+ runHooks,
178
+ helpers,
179
+ log
180
+ });
181
+
182
+ // Commit transaction if we created it
183
+ if (context.shouldCommit) {
184
+ await context.transaction.commit();
185
+ await runHooks('afterCommit');
186
+ }
187
+
188
+ return ret;
189
+
190
+ } catch (error) {
191
+ await handleWriteMethodError(error, context, 'PATCH', scopeName, log, runHooks);
192
+ }
193
+ }
@@ -0,0 +1,104 @@
1
+ import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
2
+ import { findRelationshipDefinition, handleWriteMethodError } from "./common.js";
3
+ import { createPivotRecords } from "../lib/writing/many-to-many-manipulations.js";
4
+ import { RestApiValidationError } from "../../../lib/rest-api-errors.js";
5
+
6
+ /**
7
+ * POST RELATIONSHIP
8
+ * Adds members to a to-many relationship
9
+ * POST /api/articles/1/relationships/tags
10
+ *
11
+ * @param {string} id - The ID of the resource
12
+ * @param {string} relationshipName - The name of the relationship
13
+ * @param {array} relationshipData - Array of resource identifiers to add
14
+ * @returns {Promise<void>} 204 No Content
15
+ */
16
+ export default async function postRelationshipMethod({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api, log }) {
17
+ context.method = 'postRelationship';
18
+ context.id = params.id;
19
+ context.relationshipName = params.relationshipName;
20
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo;
21
+
22
+ // Transaction handling
23
+ context.transaction = params.transaction ||
24
+ (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
25
+ context.shouldCommit = !params.transaction && !!context.transaction;
26
+
27
+ context.db = context.transaction || api.knex.instance
28
+
29
+ try {
30
+ // Validate
31
+ const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
32
+ if (!relDef) {
33
+ throw new RestApiResourceError(
34
+ `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
35
+ { subtype: 'relationship_not_found' }
36
+ );
37
+ }
38
+
39
+ if (!relDef.hasMany && !relDef.manyToMany) {
40
+ throw new RestApiValidationError(
41
+ `Cannot POST to to-one relationship '${context.relationshipName}'. Use PATCH instead.`,
42
+ { fields: ['data'] }
43
+ );
44
+ }
45
+
46
+ if (!Array.isArray(params.relationshipData)) {
47
+ throw new RestApiPayloadError('POST to relationship requires array of resource identifiers');
48
+ }
49
+
50
+ // Check permissions
51
+ await runHooks('checkPermissions');
52
+ await runHooks('checkPermissionsPostRelationship');
53
+
54
+ // Verify parent exists
55
+ const exists = await helpers.dataExists({
56
+ scopeName,
57
+ context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo },
58
+ });
59
+
60
+ if (!exists) {
61
+ throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
62
+ }
63
+
64
+ // Add relationships
65
+ if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
66
+ const manyToManyDef = relDef.manyToMany || {
67
+ through: relDef.through,
68
+ foreignKey: relDef.foreignKey,
69
+ otherKey: relDef.otherKey
70
+ };
71
+
72
+ await createPivotRecords(api, context.id, manyToManyDef, params.relationshipData, context.transaction);
73
+ } else {
74
+ // Update foreign keys for hasMany
75
+ const targetType = relDef.hasMany;
76
+ for (const identifier of params.relationshipData) {
77
+ await api.resources[targetType].patch({
78
+ id: identifier.id,
79
+ inputRecord: {
80
+ data: {
81
+ type: targetType,
82
+ id: identifier.id,
83
+ attributes: { [relDef.foreignKey]: context.id }
84
+ }
85
+ },
86
+ transaction: context.transaction,
87
+ simplified: false
88
+ });
89
+ }
90
+ }
91
+
92
+ await runHooks('finish');
93
+ await runHooks('finishPostRelationship');
94
+
95
+ if (context.shouldCommit) {
96
+ await context.transaction.commit();
97
+ }
98
+
99
+ return; // 204 No Content
100
+
101
+ } catch (error) {
102
+ await handleWriteMethodError(error, context, 'POST_RELATIONSHIP', scopeName, log, runHooks);
103
+ }
104
+ };
@@ -0,0 +1,157 @@
1
+ import { validatePostPayload } from '../lib/querying-writing/payload-validators.js';
2
+ import { processRelationships } from '../lib/writing/relationship-processor.js';
3
+ import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js';
4
+ import {
5
+ setupCommonRequest,
6
+ validateResourceAttributesBeforeWrite,
7
+ validateRelationshipAccess,
8
+ applyFieldSetters,
9
+ validatePivotResource,
10
+ handleRecordReturnAfterWrite,
11
+ handleWriteMethodError
12
+ } from './common.js';
13
+
14
+ /**
15
+ * POST
16
+ * Creates a new resource. The request must include a JSON:API document with a 'data' object
17
+ * containing 'type' and 'attributes'. It can also establish relationships to existing resources.
18
+ * The returned document contains the created resource with its server-assigned ID.
19
+ */
20
+ export default async function postMethod({
21
+ params,
22
+ context,
23
+ vars,
24
+ helpers,
25
+ scope,
26
+ scopes,
27
+ runHooks,
28
+ apiOptions,
29
+ pluginOptions,
30
+ scopeOptions,
31
+ scopeName,
32
+ api,
33
+ log
34
+ }) {
35
+ context.method = 'post';
36
+
37
+ try {
38
+ const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
39
+ params,
40
+ context,
41
+ vars,
42
+ scopes,
43
+ scopeOptions,
44
+ scopeName,
45
+ api,
46
+ helpers
47
+ });
48
+
49
+ // Run early hooks for pre-processing (e.g., file handling)
50
+ await runHooks('beforeProcessing');
51
+ await runHooks('beforeProcessingPost');
52
+
53
+ // Validate POST payload to ensure it follows JSON:API format and references valid resources.
54
+ // This checks the payload has required 'data' object with 'type' and 'attributes', validates
55
+ // that data.type matches a real resource type (preventing creation of non-existent resources),
56
+ // and ensures any relationships reference valid resource types with proper ID format.
57
+ // Example: data.type: 'articles' must be a registered scope, relationships.author must reference 'users'.
58
+ validatePostPayload(context.inputRecord, scopes);
59
+
60
+ // Validate that user has read access to all related resources
61
+ // This ensures users can only create relationships to resources they can access
62
+ await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api);
63
+
64
+ // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
65
+ // Example: relationships.author -> author_id: '123' for storage
66
+ // Example: relationships.tags -> array of pivot records to create later
67
+ const { belongsToUpdates, manyToManyRelationships } = await processRelationships(
68
+ scope,
69
+ { context }
70
+ );
71
+
72
+ // Merge belongsTo updates into attributes before validation (like PUT/PATCH do)
73
+ if (Object.keys(belongsToUpdates).length > 0) {
74
+ context.inputRecord.data.attributes = {
75
+ ...context.inputRecord.data.attributes,
76
+ ...belongsToUpdates
77
+ };
78
+ }
79
+
80
+ await validateResourceAttributesBeforeWrite({
81
+ context,
82
+ schema,
83
+ belongsToUpdates,
84
+ runHooks
85
+ });
86
+
87
+ // Centralised checkPermissions function
88
+ await scope.checkPermissions({
89
+ method: 'post',
90
+ auth: context.auth,
91
+ transaction: context.transaction
92
+ });
93
+
94
+ await runHooks('beforeDataCall');
95
+ await runHooks('beforeDataCallPost');
96
+
97
+ // Apply field setters after validation and before storage
98
+ if (context.inputRecord?.data?.attributes) {
99
+ context.inputRecord.data.attributes = await applyFieldSetters(
100
+ context.inputRecord.data.attributes,
101
+ context.schemaInfo,
102
+ context,
103
+ api,
104
+ helpers
105
+ );
106
+ }
107
+
108
+ // Create the main record - storage helper should return the created record with its ID
109
+ context.id = await helpers.dataPost({
110
+ scopeName,
111
+ context
112
+ });
113
+
114
+ await runHooks('afterDataCallPost');
115
+ await runHooks('afterDataCall');
116
+
117
+ // Process many-to-many relationships after main record creation
118
+ for (const { relName, relDef, relData } of manyToManyRelationships) {
119
+ // Validate pivot resource exists
120
+ validatePivotResource(scopes, relDef, relName);
121
+
122
+ // Create pivot records in the through table to establish many-to-many relationships.
123
+ // This creates records in the intermediary table (like 'article_tags') that link the main
124
+ // resource to each related resource. Before creating each pivot record, it validates that
125
+ // the related resource actually exists (preventing orphaned relationships). If validateExists
126
+ // is false, it skips validation for performance in bulk operations.
127
+ // Example: article.tags: [{id: '1'}, {id: '2'}] creates two article_tags records:
128
+ // {article_id: 100, tag_id: 1} and {article_id: 100, tag_id: 2}
129
+ await createPivotRecords(api, context.id, relDef, relData, context.transaction);
130
+ }
131
+
132
+ const ret = await handleRecordReturnAfterWrite({
133
+ context,
134
+ scopeName,
135
+ api,
136
+ scopes,
137
+ schemaStructure,
138
+ schemaRelationships,
139
+ scopeOptions,
140
+ vars,
141
+ runHooks,
142
+ helpers,
143
+ log
144
+ });
145
+
146
+ // Commit transaction if we created it
147
+ if (context.shouldCommit) {
148
+ await context.transaction.commit();
149
+ await runHooks('afterCommit');
150
+ }
151
+
152
+ return ret;
153
+
154
+ } catch (error) {
155
+ await handleWriteMethodError(error, context, 'POST', scopeName, log, runHooks);
156
+ }
157
+ }