json-rest-api 1.0.6 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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
|
+
}
|