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,115 @@
|
|
|
1
|
+
|
|
2
|
+
import { RestApiResourceError } from "../../../lib/rest-api-errors.js";
|
|
3
|
+
import { findRelationshipDefinition } from "./common.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DELETE RELATIONSHIP
|
|
7
|
+
* Removes specific members from a to-many relationship
|
|
8
|
+
* DELETE /api/articles/1/relationships/tags
|
|
9
|
+
*
|
|
10
|
+
* @param {string} id - The ID of the resource
|
|
11
|
+
* @param {string} relationshipName - The name of the relationship
|
|
12
|
+
* @param {array} relationshipData - Array of resource identifiers to remove
|
|
13
|
+
* @returns {Promise<void>} 204 No Content
|
|
14
|
+
*/
|
|
15
|
+
export default async function deleteRelationshipMethod({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api }) {
|
|
16
|
+
context.method = 'deleteRelationship';
|
|
17
|
+
context.id = params.id;
|
|
18
|
+
context.relationshipName = params.relationshipName;
|
|
19
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo;
|
|
20
|
+
|
|
21
|
+
// Transaction handling
|
|
22
|
+
context.transaction = params.transaction ||
|
|
23
|
+
(helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
|
|
24
|
+
context.shouldCommit = !params.transaction && !!context.transaction;
|
|
25
|
+
context.db = context.transaction || api.knex.instance
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Validate
|
|
29
|
+
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName);
|
|
30
|
+
if (!relDef) {
|
|
31
|
+
throw new RestApiResourceError(
|
|
32
|
+
`Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
|
|
33
|
+
{ subtype: 'relationship_not_found' }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!relDef.hasMany && !relDef.manyToMany) {
|
|
38
|
+
throw new RestApiValidationError(
|
|
39
|
+
`Cannot DELETE from to-one relationship '${context.relationshipName}'`,
|
|
40
|
+
{ fields: ['data'] }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(params.relationshipData)) {
|
|
45
|
+
throw new RestApiPayloadError('DELETE from relationship requires array of resource identifiers');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check permissions
|
|
49
|
+
await runHooks('checkPermissions');
|
|
50
|
+
await runHooks('checkPermissionsDeleteRelationship');
|
|
51
|
+
|
|
52
|
+
// Verify parent exists
|
|
53
|
+
const exists = await helpers.dataExists({
|
|
54
|
+
scopeName,
|
|
55
|
+
context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!exists) {
|
|
59
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Remove relationships
|
|
63
|
+
if (relDef.manyToMany || (relDef.hasMany && relDef.through)) {
|
|
64
|
+
const manyToManyDef = relDef.manyToMany || {
|
|
65
|
+
through: relDef.through,
|
|
66
|
+
foreignKey: relDef.foreignKey,
|
|
67
|
+
otherKey: relDef.otherKey
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const knex = api.knex?.instance || helpers.db;
|
|
71
|
+
const pivotResource = manyToManyDef.through;
|
|
72
|
+
const pivotScope = api.resources[pivotResource];
|
|
73
|
+
const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || pivotResource;
|
|
74
|
+
const localKey = manyToManyDef.foreignKey;
|
|
75
|
+
const foreignKey = manyToManyDef.otherKey;
|
|
76
|
+
|
|
77
|
+
for (const identifier of params.relationshipData) {
|
|
78
|
+
await knex(pivotTable)
|
|
79
|
+
.where(localKey, context.id)
|
|
80
|
+
.where(foreignKey, identifier.id)
|
|
81
|
+
.delete()
|
|
82
|
+
.transacting(context.transaction);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Null out foreign keys for hasMany
|
|
86
|
+
const targetType = relDef.hasMany;
|
|
87
|
+
for (const identifier of params.relationshipData) {
|
|
88
|
+
await api.resources[targetType].patch({
|
|
89
|
+
id: identifier.id,
|
|
90
|
+
inputRecord: {
|
|
91
|
+
data: {
|
|
92
|
+
type: targetType,
|
|
93
|
+
id: identifier.id,
|
|
94
|
+
attributes: { [relDef.foreignKey]: null }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
transaction: context.transaction,
|
|
98
|
+
simplified: false
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await runHooks('finish');
|
|
104
|
+
await runHooks('finishDeleteRelationship');
|
|
105
|
+
|
|
106
|
+
if (context.shouldCommit) {
|
|
107
|
+
await context.transaction.commit();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return; // 204 No Content
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await handleWriteMethodError(error, context, 'DELETE_RELATIONSHIP', scopeName, log);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
|
|
2
|
+
import { handleWriteMethodError } from './common.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DELETE
|
|
6
|
+
* Permanently deletes a resource.
|
|
7
|
+
* Returns 204 No Content on success, indicating the resource has been removed.
|
|
8
|
+
* This method does not return the deleted resource.
|
|
9
|
+
*/
|
|
10
|
+
export default async function deleteMethod({
|
|
11
|
+
params,
|
|
12
|
+
context,
|
|
13
|
+
vars,
|
|
14
|
+
helpers,
|
|
15
|
+
scope,
|
|
16
|
+
scopes,
|
|
17
|
+
runHooks,
|
|
18
|
+
apiOptions,
|
|
19
|
+
pluginOptions,
|
|
20
|
+
scopeOptions,
|
|
21
|
+
scopeName,
|
|
22
|
+
api,
|
|
23
|
+
log
|
|
24
|
+
}) {
|
|
25
|
+
// Make the method available to all hooks
|
|
26
|
+
context.method = 'delete';
|
|
27
|
+
|
|
28
|
+
// Set the ID in context
|
|
29
|
+
context.id = params.id;
|
|
30
|
+
|
|
31
|
+
// Set scopeName in context (needed for broadcasting)
|
|
32
|
+
context.scopeName = scopeName;
|
|
33
|
+
|
|
34
|
+
// Set schema info even for DELETE (needed by storage layer)
|
|
35
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo;
|
|
36
|
+
|
|
37
|
+
// Transaction handling
|
|
38
|
+
context.transaction = params.transaction ||
|
|
39
|
+
(helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null);
|
|
40
|
+
context.shouldCommit = !params.transaction && !!context.transaction;
|
|
41
|
+
context.db = context.transaction || api.knex.instance;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// No payload validation needed for DELETE
|
|
45
|
+
|
|
46
|
+
// Fetch minimal record for authorization and logging
|
|
47
|
+
const minimalRecord = await helpers.dataGetMinimal({
|
|
48
|
+
scopeName,
|
|
49
|
+
context,
|
|
50
|
+
runHooks
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!minimalRecord) {
|
|
54
|
+
throw new RestApiResourceError(
|
|
55
|
+
`Resource not found`,
|
|
56
|
+
{
|
|
57
|
+
subtype: 'not_found',
|
|
58
|
+
resourceType: scopeName,
|
|
59
|
+
resourceId: context.id
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
context.originalMinimalRecord = minimalRecord;
|
|
65
|
+
context.minimalRecord = minimalRecord;
|
|
66
|
+
|
|
67
|
+
// Centralised checkPermissions function
|
|
68
|
+
await scope.checkPermissions({
|
|
69
|
+
method: 'delete',
|
|
70
|
+
auth: context.auth,
|
|
71
|
+
id: context.id,
|
|
72
|
+
minimalRecord: context.minimalRecord,
|
|
73
|
+
transaction: context.transaction
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Before data operations
|
|
77
|
+
await runHooks('beforeDataCall');
|
|
78
|
+
await runHooks('beforeDataCallDelete');
|
|
79
|
+
|
|
80
|
+
// Initialize record context for hooks
|
|
81
|
+
context.record = {};
|
|
82
|
+
|
|
83
|
+
// Call the storage helper
|
|
84
|
+
await helpers.dataDelete({
|
|
85
|
+
scopeName,
|
|
86
|
+
context
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await runHooks('afterDataCallDelete');
|
|
90
|
+
await runHooks('afterDataCall');
|
|
91
|
+
|
|
92
|
+
// No return record for DELETE (204 No Content)
|
|
93
|
+
|
|
94
|
+
await runHooks('finish');
|
|
95
|
+
await runHooks('finishDelete');
|
|
96
|
+
|
|
97
|
+
// Commit transaction if we created it
|
|
98
|
+
if (context.shouldCommit) {
|
|
99
|
+
await context.transaction.commit();
|
|
100
|
+
await runHooks('afterCommit');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// DELETE typically returns void/undefined (204 No Content)
|
|
104
|
+
return;
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await handleWriteMethodError(error, context, 'DELETE', scopeName, log, runHooks);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { filterHiddenFields } from '../lib/querying-writing/field-utils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* enrichAttributes
|
|
5
|
+
* Runs the enrichAttributes hook for a specific scope to allow plugins to modify attributes
|
|
6
|
+
* before they are returned to the client. This is a scope method so each resource type
|
|
7
|
+
* can have its own attribute enrichment logic.
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export default async function enrichAttributesMethod({ context, params, runHooks, scopeName, scopes, api, helpers }) {
|
|
12
|
+
// Extract parameters passed to enrichAttributes
|
|
13
|
+
// - attributes: The raw attributes from database
|
|
14
|
+
// - parentContext: The context from the calling method (has queryParams, transaction, etc.)
|
|
15
|
+
// - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
|
|
16
|
+
// - isMainResource: Whether this is the main resource or an included one
|
|
17
|
+
// - computedDependencies: Fields fetched only for computation (to be removed)
|
|
18
|
+
// Extract parameters passed to enrichAttributes
|
|
19
|
+
// - attributes: The raw attributes from database
|
|
20
|
+
// - parentContext: The context from the calling method (has queryParams, transaction, etc.)
|
|
21
|
+
// - requestedComputedFields: Which computed fields to calculate (from sparse fieldsets)
|
|
22
|
+
// - isMainResource: Whether this is the main resource or an included one
|
|
23
|
+
// - computedDependencies: Fields fetched only for computation (to be removed)
|
|
24
|
+
const { attributes, parentContext, requestedComputedFields, isMainResource, computedDependencies } = params || {};
|
|
25
|
+
|
|
26
|
+
// Return empty object if no attributes provided
|
|
27
|
+
if (!attributes) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get schema, computed field definitions
|
|
32
|
+
const schemaStructure = scopes[scopeName]?.vars?.schemaInfo?.schemaStructure || {};
|
|
33
|
+
const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {};
|
|
34
|
+
const fieldGetters = scopes[scopeName]?.vars?.schemaInfo?.fieldGetters || {};
|
|
35
|
+
const sortedGetterFields = scopes[scopeName]?.vars?.schemaInfo?.sortedGetterFields || [];
|
|
36
|
+
|
|
37
|
+
// Make a copy of attributes for transformation
|
|
38
|
+
let transformedAttributes = { ...attributes };
|
|
39
|
+
|
|
40
|
+
// STEP 1: Apply field getters in dependency order (before filtering)
|
|
41
|
+
// This ensures getters see all fields including hidden ones
|
|
42
|
+
for (const fieldName of sortedGetterFields) {
|
|
43
|
+
// Only process if field exists in attributes
|
|
44
|
+
if (fieldName in transformedAttributes) {
|
|
45
|
+
const getterInfo = fieldGetters[fieldName];
|
|
46
|
+
try {
|
|
47
|
+
const getterContext = {
|
|
48
|
+
attributes: transformedAttributes, // Current state with previous getters applied
|
|
49
|
+
fieldName,
|
|
50
|
+
originalValue: attributes[fieldName], // Original untransformed value
|
|
51
|
+
originalAttributes: attributes, // All original attributes
|
|
52
|
+
record: transformedAttributes, // Alias for compatibility
|
|
53
|
+
parentContext,
|
|
54
|
+
scopeName,
|
|
55
|
+
api,
|
|
56
|
+
helpers,
|
|
57
|
+
isMainResource: isMainResource !== false
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Apply getter (can be async)
|
|
61
|
+
transformedAttributes[fieldName] = await getterInfo.getter(
|
|
62
|
+
transformedAttributes[fieldName],
|
|
63
|
+
getterContext
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Error in getter for field '${fieldName}' in ${scopeName}:`, error);
|
|
67
|
+
// Keep current value on error (don't break the whole request)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Filter hidden fields from attributes based on visibility rules
|
|
73
|
+
// This removes hidden:true fields and normallyHidden:true fields (unless requested)
|
|
74
|
+
const requestedFields = parentContext?.queryParams?.fields?.[scopeName];
|
|
75
|
+
const filteredAttributes = filterHiddenFields(transformedAttributes, { structure: schemaStructure }, requestedFields);
|
|
76
|
+
|
|
77
|
+
// Handle virtual fields - they need to be added from parent context if available
|
|
78
|
+
// Virtual fields come from input and need to be preserved in responses
|
|
79
|
+
if (parentContext && parentContext.inputRecord?.data?.attributes) {
|
|
80
|
+
const inputAttrs = parentContext.inputRecord.data.attributes;
|
|
81
|
+
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
82
|
+
if (fieldDef.virtual === true && fieldName in inputAttrs && inputAttrs[fieldName] !== undefined && inputAttrs[fieldName] !== null) {
|
|
83
|
+
// Add virtual field to filtered attributes if not already there
|
|
84
|
+
if (!(fieldName in filteredAttributes)) {
|
|
85
|
+
filteredAttributes[fieldName] = inputAttrs[fieldName];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Now handle sparse fieldsets for virtual fields
|
|
92
|
+
if (requestedFields && requestedFields.length > 0) {
|
|
93
|
+
const requestedFieldsList = typeof requestedFields === 'string' ?
|
|
94
|
+
requestedFields.split(',').map(f => f.trim()) : requestedFields;
|
|
95
|
+
|
|
96
|
+
// Remove virtual fields that weren't requested
|
|
97
|
+
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
98
|
+
if (fieldDef.virtual === true && fieldName in filteredAttributes) {
|
|
99
|
+
if (!requestedFieldsList.includes(fieldName)) {
|
|
100
|
+
delete filteredAttributes[fieldName];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Determine which computed fields to calculate
|
|
107
|
+
// We only compute fields that are requested to optimize performance
|
|
108
|
+
let fieldsToCompute = [];
|
|
109
|
+
if (requestedComputedFields) {
|
|
110
|
+
// Explicit list provided (from sparse fieldsets)
|
|
111
|
+
// Example: ?fields[products]=name,profit_margin -> only compute profit_margin
|
|
112
|
+
fieldsToCompute = requestedComputedFields;
|
|
113
|
+
} else if (isMainResource || !parentContext?.queryParams?.fields) {
|
|
114
|
+
// No sparse fieldsets or this is the main resource - compute all fields
|
|
115
|
+
// This ensures all computed fields are available when no filtering is applied
|
|
116
|
+
fieldsToCompute = Object.keys(computedFields);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create compute context with all available resources
|
|
120
|
+
// IMPORTANT: We pass the TRANSFORMED attributes (after getters) to compute functions
|
|
121
|
+
// This ensures computed fields see the getter-processed values
|
|
122
|
+
// Example: if price getter converts string to number, profit_margin sees the number
|
|
123
|
+
const computeContext = {
|
|
124
|
+
attributes: transformedAttributes, // Transformed attributes after getters
|
|
125
|
+
record: { ...transformedAttributes }, // Full record for convenience
|
|
126
|
+
context: parentContext,
|
|
127
|
+
helpers, // API helpers for complex operations
|
|
128
|
+
api, // Full API instance
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Auto-compute fields that have compute functions
|
|
132
|
+
for (const fieldName of fieldsToCompute) {
|
|
133
|
+
const fieldDef = computedFields[fieldName];
|
|
134
|
+
if (fieldDef && fieldDef.compute) {
|
|
135
|
+
try {
|
|
136
|
+
// Call the compute function with full context
|
|
137
|
+
// Example: profit_margin compute gets { attributes: { price: 100, cost: 60 } }
|
|
138
|
+
// and returns: ((100 - 60) / 100 * 100) = "40.00"
|
|
139
|
+
filteredAttributes[fieldName] = await fieldDef.compute(computeContext);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Log error but don't fail the request - computed fields shouldn't break API
|
|
142
|
+
console.error(`Error computing field '${fieldName}' for ${scopeName}:`, error);
|
|
143
|
+
filteredAttributes[fieldName] = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove fields that were only fetched as dependencies
|
|
149
|
+
// This is the key to the dependency resolution feature:
|
|
150
|
+
// 1. We fetched dependencies from DB (e.g., 'cost' for profit_margin)
|
|
151
|
+
// 2. We used them in compute functions
|
|
152
|
+
// 3. Now we remove them if they weren't explicitly requested
|
|
153
|
+
// Example: User requests profit_margin, we fetch cost, compute, then remove cost
|
|
154
|
+
const finalAttributes = { ...filteredAttributes };
|
|
155
|
+
if (requestedFields && computedDependencies && computedDependencies.length > 0) {
|
|
156
|
+
// Parse requested fields if it's a string
|
|
157
|
+
const requested = typeof requestedFields === 'string'
|
|
158
|
+
? requestedFields.split(',').map(f => f.trim()).filter(f => f)
|
|
159
|
+
: requestedFields;
|
|
160
|
+
|
|
161
|
+
for (const dep of computedDependencies) {
|
|
162
|
+
// Only remove if it wasn't explicitly requested
|
|
163
|
+
// Example: 'cost' is removed unless user explicitly asked for it
|
|
164
|
+
if (!requested.includes(dep)) {
|
|
165
|
+
delete finalAttributes[dep];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create context for enrichAttributes hooks
|
|
171
|
+
Object.assign(context, {
|
|
172
|
+
parentContext,
|
|
173
|
+
attributes: finalAttributes, // Use the final attributes after dependency removal
|
|
174
|
+
computedFields,
|
|
175
|
+
requestedComputedFields: fieldsToCompute,
|
|
176
|
+
scopeName,
|
|
177
|
+
helpers,
|
|
178
|
+
api
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Run enrichAttributes hooks for additional/override computations
|
|
182
|
+
await runHooks('enrichAttributes');
|
|
183
|
+
|
|
184
|
+
// Return the attributes from context, which hooks may have modified
|
|
185
|
+
return context.attributes;
|
|
186
|
+
};
|