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.
- 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 +90 -32
- 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
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
|
|
2
|
-
import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_MAX_INCLUDE_LIMIT } from '../utils/knex-constants.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Builds a window function query for limited includes
|
|
6
|
-
*
|
|
7
|
-
* @param {Object} knex - Knex instance
|
|
8
|
-
* @param {string} tableName - Target table name
|
|
9
|
-
* @param {string} foreignKey - Foreign key field
|
|
10
|
-
* @param {Array} parentIds - Parent record IDs
|
|
11
|
-
* @param {Array} fieldsToSelect - Fields to select
|
|
12
|
-
* @param {Object} includeConfig - Include configuration (limit, orderBy)
|
|
13
|
-
* @param {Object} capabilities - Database capabilities
|
|
14
|
-
* @returns {Object} Knex query
|
|
15
|
-
*/
|
|
16
|
-
export const buildWindowedIncludeQuery = (
|
|
17
|
-
knex,
|
|
18
|
-
tableName,
|
|
19
|
-
foreignKey,
|
|
20
|
-
parentIds,
|
|
21
|
-
fieldsToSelect,
|
|
22
|
-
includeConfig,
|
|
23
|
-
capabilities,
|
|
24
|
-
scopeVars = {}
|
|
25
|
-
) => {
|
|
26
|
-
const { orderBy = [] } = includeConfig;
|
|
27
|
-
|
|
28
|
-
// Apply defaults for limit
|
|
29
|
-
const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT;
|
|
30
|
-
|
|
31
|
-
// Validate against max
|
|
32
|
-
if (scopeVars.queryMaxLimit && effectiveLimit > scopeVars.queryMaxLimit) {
|
|
33
|
-
throw new RestApiResourceError({
|
|
34
|
-
title: 'Include Limit Exceeds Maximum',
|
|
35
|
-
detail: `Requested include limit (${effectiveLimit}) exceeds queryMaxLimit (${scopeVars.queryMaxLimit})`,
|
|
36
|
-
status: 400
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if window functions are supported
|
|
41
|
-
if (!capabilities.windowFunctions) {
|
|
42
|
-
const { dbInfo } = capabilities;
|
|
43
|
-
throw new RestApiResourceError(
|
|
44
|
-
`Include limits require window function support. Your database (${dbInfo.client} ${dbInfo.version}) does not support this feature. ` +
|
|
45
|
-
`Window functions are supported in: PostgreSQL 8.4+, MySQL 8.0+, MariaDB 10.2+, SQLite 3.25+, SQL Server 2005+`,
|
|
46
|
-
{
|
|
47
|
-
subtype: 'unsupported_operation',
|
|
48
|
-
database: dbInfo.client,
|
|
49
|
-
version: dbInfo.version,
|
|
50
|
-
requiredFeature: 'window_functions'
|
|
51
|
-
}
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Build the window function query
|
|
56
|
-
// This creates a subquery that partitions by the foreign key and numbers rows
|
|
57
|
-
const subquery = knex(tableName)
|
|
58
|
-
.select('*')
|
|
59
|
-
.select(
|
|
60
|
-
knex.raw(
|
|
61
|
-
'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
62
|
-
buildOrderByClause(orderBy) +
|
|
63
|
-
') as ' + ROW_NUMBER_KEY,
|
|
64
|
-
[foreignKey]
|
|
65
|
-
)
|
|
66
|
-
)
|
|
67
|
-
.whereIn(foreignKey, parentIds);
|
|
68
|
-
|
|
69
|
-
// Apply field selection if specified
|
|
70
|
-
if (fieldsToSelect !== '*' && Array.isArray(fieldsToSelect)) {
|
|
71
|
-
// Include the foreign key and row number in selection
|
|
72
|
-
const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])];
|
|
73
|
-
subquery.select(fieldsWithFK);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Wrap in outer query to filter by row number
|
|
77
|
-
const query = knex
|
|
78
|
-
.select('*')
|
|
79
|
-
.from(subquery.as('_windowed'))
|
|
80
|
-
.where(ROW_NUMBER_KEY, '<=', effectiveLimit);
|
|
81
|
-
|
|
82
|
-
return query;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Build ORDER BY clause from array of sort fields
|
|
87
|
-
*/
|
|
88
|
-
export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
89
|
-
if (!orderBy || orderBy.length === 0) {
|
|
90
|
-
const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id';
|
|
91
|
-
return `${defaultField} ASC`; // Default ordering
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return orderBy.map(field => {
|
|
95
|
-
const isDesc = field.startsWith('-');
|
|
96
|
-
const fieldName = isDesc ? field.substring(1) : field;
|
|
97
|
-
const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName;
|
|
98
|
-
return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}`;
|
|
99
|
-
}).join(', ');
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Apply standard (non-windowed) include configuration
|
|
104
|
-
* Used for fallback or when window strategy not requested
|
|
105
|
-
*/
|
|
106
|
-
export const applyStandardIncludeConfig = (query, includeConfig, scopeVars, log) => {
|
|
107
|
-
const { orderBy = [] } = includeConfig;
|
|
108
|
-
|
|
109
|
-
// Apply ordering
|
|
110
|
-
orderBy.forEach(field => {
|
|
111
|
-
const desc = field.startsWith('-');
|
|
112
|
-
const column = desc ? field.substring(1) : field;
|
|
113
|
-
query = query.orderBy(column, desc ? 'desc' : 'asc');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Apply limit with defaults
|
|
117
|
-
const requestedLimit = includeConfig.limit;
|
|
118
|
-
const defaultLimit = scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT;
|
|
119
|
-
const limit = requestedLimit ?? defaultLimit;
|
|
120
|
-
|
|
121
|
-
// Allow explicit null/false to mean no limit
|
|
122
|
-
if (limit !== null && limit !== false) {
|
|
123
|
-
const maxInclude = scopeVars.maxIncludeLimit || DEFAULT_MAX_INCLUDE_LIMIT;
|
|
124
|
-
const maxQuery = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT;
|
|
125
|
-
const effectiveMax = Math.min(maxInclude, maxQuery);
|
|
126
|
-
const effectiveLimit = Math.min(limit, effectiveMax);
|
|
127
|
-
|
|
128
|
-
query = query.limit(effectiveLimit);
|
|
129
|
-
|
|
130
|
-
log.debug('Applied include limit:', {
|
|
131
|
-
requested: requestedLimit,
|
|
132
|
-
default: defaultLimit,
|
|
133
|
-
effective: effectiveLimit,
|
|
134
|
-
maxAllowed: effectiveMax,
|
|
135
|
-
note: requestedLimit !== undefined ? 'Using explicit limit' : 'Using default limit'
|
|
136
|
-
});
|
|
137
|
-
} else {
|
|
138
|
-
log.debug('No limit applied to include query (explicitly disabled)');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return query;
|
|
142
|
-
};
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module manyToManyManipulations
|
|
3
|
-
* @description Many-to-many relationship manipulation functions for REST API plugin
|
|
4
|
-
*
|
|
5
|
-
* This module provides sophisticated handling of many-to-many relationships through
|
|
6
|
-
* pivot tables. It implements intelligent syncing that preserves pivot table metadata
|
|
7
|
-
* while efficiently updating relationships. This is crucial for maintaining data
|
|
8
|
-
* integrity and audit trails in complex relational systems.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
|
|
12
|
-
import { transformSimplifiedToJsonApi } from './simplified-helpers.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Updates many-to-many relationships intelligently by synchronizing pivot table records
|
|
16
|
-
* while preserving any additional pivot data (like timestamps or extra attributes).
|
|
17
|
-
*
|
|
18
|
-
* This function implements a "sync" operation that:
|
|
19
|
-
* 1. Fetches all existing pivot records for the resource
|
|
20
|
-
* 2. Compares with the desired state from the input
|
|
21
|
-
* 3. Deletes pivot records that should no longer exist
|
|
22
|
-
* 4. Creates new pivot records for new relationships
|
|
23
|
-
* 5. Preserves existing pivot records (with their extra data) that remain
|
|
24
|
-
*
|
|
25
|
-
* This approach is superior to delete-all-then-recreate because it:
|
|
26
|
-
* - Preserves pivot table metadata (created_at, updated_at, extra fields)
|
|
27
|
-
* - Minimizes database operations
|
|
28
|
-
* - Maintains referential integrity
|
|
29
|
-
* - Provides better audit trails
|
|
30
|
-
*
|
|
31
|
-
* @param {Object} scope - The scope object (not used directly, for consistency)
|
|
32
|
-
* @param {Object} deps - Dependencies object
|
|
33
|
-
* @param {Object} deps.api - The API instance with access to resources
|
|
34
|
-
* @param {Object} deps.context - Request context
|
|
35
|
-
* @param {string|number} deps.context.resourceId - The ID of the resource being updated
|
|
36
|
-
* @param {Object} deps.context.relDef - The relationship definition object
|
|
37
|
-
* @param {string} deps.context.relDef.through - The pivot table/scope name
|
|
38
|
-
* @param {string} deps.context.relDef.foreignKey - The foreign key field pointing to this resource
|
|
39
|
-
* @param {string} deps.context.relDef.otherKey - The foreign key field pointing to the related resource
|
|
40
|
-
* @param {boolean} [deps.context.relDef.validateExists=true] - Whether to validate related resources exist
|
|
41
|
-
* @param {Array} deps.context.relData - Array of relationship data objects with type and id
|
|
42
|
-
* @param {Object} deps.context.transaction - Database transaction object
|
|
43
|
-
* @throws {RestApiResourceError} If a related resource doesn't exist when validation is enabled
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* // Example 1: Updating article tags (some added, some removed, some kept)
|
|
47
|
-
* const deps = {
|
|
48
|
-
* api,
|
|
49
|
-
* context: {
|
|
50
|
-
* resourceId: '100',
|
|
51
|
-
* relDef: {
|
|
52
|
-
* through: 'article_tags',
|
|
53
|
-
* foreignKey: 'article_id',
|
|
54
|
-
* otherKey: 'tag_id',
|
|
55
|
-
* validateExists: true
|
|
56
|
-
* },
|
|
57
|
-
* relData: [
|
|
58
|
-
* { type: 'tags', id: '1' }, // Existing - will be kept
|
|
59
|
-
* { type: 'tags', id: '3' }, // New - will be added
|
|
60
|
-
* // Tag 2 was in the relationship but not in relData - will be removed
|
|
61
|
-
* ],
|
|
62
|
-
* transaction: trx
|
|
63
|
-
* }
|
|
64
|
-
* };
|
|
65
|
-
* await updateManyToManyRelationship(null, deps);
|
|
66
|
-
* // Result in article_tags table:
|
|
67
|
-
* // - Record linking article 100 to tag 1: Preserved with original created_at
|
|
68
|
-
* // - Record linking article 100 to tag 2: Deleted
|
|
69
|
-
* // - Record linking article 100 to tag 3: Created new
|
|
70
|
-
*
|
|
71
|
-
* @example
|
|
72
|
-
* // Example 2: Clearing all relationships
|
|
73
|
-
* const deps = {
|
|
74
|
-
* api,
|
|
75
|
-
* context: {
|
|
76
|
-
* resourceId: '100',
|
|
77
|
-
* relDef,
|
|
78
|
-
* relData: [],
|
|
79
|
-
* transaction: trx
|
|
80
|
-
* }
|
|
81
|
-
* };
|
|
82
|
-
* await updateManyToManyRelationship(null, deps);
|
|
83
|
-
* // All pivot records for article 100 will be deleted
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* // Example 3: Pivot table with extra data preservation
|
|
87
|
-
* // Assume article_tags has extra fields like 'display_order' and 'featured'
|
|
88
|
-
* // Existing pivot record: { article_id: 100, tag_id: 1, display_order: 1, featured: true }
|
|
89
|
-
* const deps = {
|
|
90
|
-
* api,
|
|
91
|
-
* context: {
|
|
92
|
-
* resourceId: '100',
|
|
93
|
-
* relDef,
|
|
94
|
-
* relData: [
|
|
95
|
-
* { type: 'tags', id: '1' }, // This tag stays
|
|
96
|
-
* { type: 'tags', id: '2' } // New tag added
|
|
97
|
-
* ],
|
|
98
|
-
* transaction: trx
|
|
99
|
-
* }
|
|
100
|
-
* };
|
|
101
|
-
* await updateManyToManyRelationship(null, deps);
|
|
102
|
-
* // Result:
|
|
103
|
-
* // - Tag 1 pivot: display_order and featured are preserved
|
|
104
|
-
* // - Tag 2 pivot: created with default values for extra fields
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* // Example 4: Skip validation for performance (when you know resources exist)
|
|
108
|
-
* const deps = {
|
|
109
|
-
* api,
|
|
110
|
-
* context: {
|
|
111
|
-
* resourceId: userId,
|
|
112
|
-
* relDef: {
|
|
113
|
-
* through: 'user_roles',
|
|
114
|
-
* foreignKey: 'user_id',
|
|
115
|
-
* otherKey: 'role_id',
|
|
116
|
-
* validateExists: false // Skip GET requests for each role
|
|
117
|
-
* },
|
|
118
|
-
* relData: roleData,
|
|
119
|
-
* transaction: trx
|
|
120
|
-
* }
|
|
121
|
-
* };
|
|
122
|
-
* await updateManyToManyRelationship(null, deps);
|
|
123
|
-
* // Faster execution, but could create orphaned relationships if roles don't exist
|
|
124
|
-
*
|
|
125
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
126
|
-
* // The REST API plugin uses this to:
|
|
127
|
-
* // 1. Intelligently sync many-to-many relationships without data loss
|
|
128
|
-
* // 2. Preserve pivot table metadata (timestamps, extra fields)
|
|
129
|
-
* // 3. Minimize database operations (only delete/create what changed)
|
|
130
|
-
* // 4. Maintain audit trails by preserving original created_at times
|
|
131
|
-
* // 5. Support complex pivot tables with additional attributes
|
|
132
|
-
* // 6. Provide atomic updates within transactions
|
|
133
|
-
*/
|
|
134
|
-
export const updateManyToManyRelationship = async (scope, deps) => {
|
|
135
|
-
// Extract values from deps
|
|
136
|
-
const { api, context } = deps;
|
|
137
|
-
const { resourceId, relDef, relData, transaction: trx } = context;
|
|
138
|
-
|
|
139
|
-
// Get the knex instance from the pivot scope
|
|
140
|
-
const pivotScope = api.resources[relDef.through];
|
|
141
|
-
if (!pivotScope) {
|
|
142
|
-
throw new Error(`Pivot table resource '${relDef.through}' not found`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Get the actual database table name (might be different from scope name)
|
|
146
|
-
const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
|
|
147
|
-
|
|
148
|
-
// Get existing pivot records directly from database
|
|
149
|
-
const existingRecords = await trx(tableName)
|
|
150
|
-
.where(relDef.foreignKey, resourceId)
|
|
151
|
-
.select(relDef.otherKey);
|
|
152
|
-
|
|
153
|
-
// Create sets for efficient comparison
|
|
154
|
-
const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])));
|
|
155
|
-
const newIds = new Set(relData.map(r => String(r.id)));
|
|
156
|
-
|
|
157
|
-
// Determine what to delete and add
|
|
158
|
-
const toDelete = [...existingIds].filter(id => !newIds.has(id));
|
|
159
|
-
const toAdd = [...newIds].filter(id => !existingIds.has(id));
|
|
160
|
-
|
|
161
|
-
// Validate related resources exist if needed (do this before any changes)
|
|
162
|
-
if (relDef.validateExists !== false && toAdd.length > 0) {
|
|
163
|
-
for (const relatedId of toAdd) {
|
|
164
|
-
const related = relData.find(r => String(r.id) === relatedId);
|
|
165
|
-
try {
|
|
166
|
-
await api.resources[related.type].get({
|
|
167
|
-
id: related.id,
|
|
168
|
-
transaction: trx
|
|
169
|
-
});
|
|
170
|
-
} catch (error) {
|
|
171
|
-
throw new RestApiResourceError(
|
|
172
|
-
`Related ${related.type} with id ${related.id} not found`,
|
|
173
|
-
{
|
|
174
|
-
subtype: 'not_found',
|
|
175
|
-
resourceType: related.type,
|
|
176
|
-
resourceId: related.id
|
|
177
|
-
}
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Bulk delete records that should be removed
|
|
184
|
-
if (toDelete.length > 0) {
|
|
185
|
-
await trx(tableName)
|
|
186
|
-
.where(relDef.foreignKey, resourceId)
|
|
187
|
-
.whereIn(relDef.otherKey, toDelete)
|
|
188
|
-
.delete();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Bulk insert new records
|
|
192
|
-
if (toAdd.length > 0) {
|
|
193
|
-
const recordsToInsert = toAdd.map(relatedId => ({
|
|
194
|
-
[relDef.foreignKey]: resourceId,
|
|
195
|
-
[relDef.otherKey]: relatedId
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
await trx(tableName).insert(recordsToInsert);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Records that exist in both are automatically preserved with their pivot data
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
// Note: deleteExistingPivotRecords has been removed in favor of using
|
|
205
|
-
// updateManyToManyRelationship for all sync operations (including PUT).
|
|
206
|
-
// This aligns with industry standards where ORMs use intelligent sync
|
|
207
|
-
// rather than delete-all-then-recreate patterns.
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Creates new pivot table records for many-to-many relationships.
|
|
211
|
-
*
|
|
212
|
-
* This function handles the creation of pivot table records that link resources
|
|
213
|
-
* in many-to-many relationships. It includes optional validation to ensure that
|
|
214
|
-
* the related resources actually exist before creating the pivot records.
|
|
215
|
-
*
|
|
216
|
-
* Key features:
|
|
217
|
-
* - Validates related resources exist (optional but recommended)
|
|
218
|
-
* - Creates minimal pivot records with just the foreign keys
|
|
219
|
-
* - Works within database transactions for consistency
|
|
220
|
-
* - Throws descriptive errors if related resources are missing
|
|
221
|
-
*
|
|
222
|
-
* @param {Object} api - The API instance with access to resources
|
|
223
|
-
* @param {string|number} resourceId - The ID of the primary resource
|
|
224
|
-
* @param {Object} relDef - The relationship definition
|
|
225
|
-
* @param {string} relDef.through - The pivot table/scope name
|
|
226
|
-
* @param {string} relDef.foreignKey - Field name for the primary resource's ID
|
|
227
|
-
* @param {string} relDef.otherKey - Field name for the related resource's ID
|
|
228
|
-
* @param {boolean} [relDef.validateExists=true] - Whether to validate resources exist
|
|
229
|
-
* @param {Array} relData - Array of related resources to link
|
|
230
|
-
* @param {string} relData[].type - The type of the related resource
|
|
231
|
-
* @param {string} relData[].id - The ID of the related resource
|
|
232
|
-
* @param {Object} trx - Database transaction object
|
|
233
|
-
* @throws {RestApiResourceError} If a related resource doesn't exist
|
|
234
|
-
*
|
|
235
|
-
* @example
|
|
236
|
-
* // Example 1: Creating article-tag relationships
|
|
237
|
-
* const relDef = {
|
|
238
|
-
* through: 'article_tags',
|
|
239
|
-
* foreignKey: 'article_id',
|
|
240
|
-
* otherKey: 'tag_id'
|
|
241
|
-
* };
|
|
242
|
-
* const relData = [
|
|
243
|
-
* { type: 'tags', id: '10' },
|
|
244
|
-
* { type: 'tags', id: '20' },
|
|
245
|
-
* { type: 'tags', id: '30' }
|
|
246
|
-
* ];
|
|
247
|
-
* await createPivotRecords(api, '100', relDef, relData, trx);
|
|
248
|
-
* // Creates 3 records in article_tags:
|
|
249
|
-
* // { article_id: 100, tag_id: 10 }
|
|
250
|
-
* // { article_id: 100, tag_id: 20 }
|
|
251
|
-
* // { article_id: 100, tag_id: 30 }
|
|
252
|
-
*
|
|
253
|
-
* @example
|
|
254
|
-
* // Example 2: With validation - ensures tags exist
|
|
255
|
-
* const relData = [
|
|
256
|
-
* { type: 'tags', id: '999' } // Non-existent tag
|
|
257
|
-
* ];
|
|
258
|
-
* try {
|
|
259
|
-
* await createPivotRecords(api, '100', relDef, relData, trx);
|
|
260
|
-
* } catch (error) {
|
|
261
|
-
* // RestApiResourceError: "Related tags with id 999 not found"
|
|
262
|
-
* // No pivot record is created
|
|
263
|
-
* }
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* // Example 3: Skip validation for bulk operations
|
|
267
|
-
* const relDef = {
|
|
268
|
-
* through: 'user_permissions',
|
|
269
|
-
* foreignKey: 'user_id',
|
|
270
|
-
* otherKey: 'permission_id',
|
|
271
|
-
* validateExists: false // Trust that permissions exist
|
|
272
|
-
* };
|
|
273
|
-
* const permissions = [
|
|
274
|
-
* { type: 'permissions', id: '1' },
|
|
275
|
-
* { type: 'permissions', id: '2' },
|
|
276
|
-
* // ... hundreds more
|
|
277
|
-
* ];
|
|
278
|
-
* await createPivotRecords(api, userId, relDef, permissions, trx);
|
|
279
|
-
* // Much faster without validation, but could create invalid relationships
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* // Example 4: Used after clearing relationships
|
|
283
|
-
* // Common pattern for "replace all" operations:
|
|
284
|
-
* await deleteExistingPivotRecords(api, articleId, relDef, trx);
|
|
285
|
-
* await createPivotRecords(api, articleId, relDef, newTags, trx);
|
|
286
|
-
* // Old relationships removed, new ones created
|
|
287
|
-
*
|
|
288
|
-
* @example
|
|
289
|
-
* // Example 5: Empty relData creates no records
|
|
290
|
-
* await createPivotRecords(api, '100', relDef, [], trx);
|
|
291
|
-
* // No operations performed, no errors thrown
|
|
292
|
-
*
|
|
293
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
294
|
-
* // The REST API plugin uses this to:
|
|
295
|
-
* // 1. Create pivot records after relationship validation
|
|
296
|
-
* // 2. Ensure referential integrity by validating resources exist
|
|
297
|
-
* // 3. Support bulk relationship creation in POST operations
|
|
298
|
-
* // 4. Work within transactions for atomic operations
|
|
299
|
-
* // 5. Provide clear error messages when relationships are invalid
|
|
300
|
-
* // 6. Allow performance optimization by skipping validation when safe
|
|
301
|
-
*/
|
|
302
|
-
export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
|
|
303
|
-
if (relData.length === 0) return; // Early exit if nothing to create
|
|
304
|
-
|
|
305
|
-
// Get pivot table info
|
|
306
|
-
const pivotScope = api.resources[relDef.through];
|
|
307
|
-
if (!pivotScope) {
|
|
308
|
-
throw new Error(`Pivot table resource '${relDef.through}' not found`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Get the actual database table name (might be different from scope name)
|
|
312
|
-
const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
|
|
313
|
-
|
|
314
|
-
// Validate all related resources exist if needed (do this before any inserts)
|
|
315
|
-
if (relDef.validateExists !== false) {
|
|
316
|
-
for (const related of relData) {
|
|
317
|
-
try {
|
|
318
|
-
await api.resources[related.type].get({
|
|
319
|
-
id: related.id,
|
|
320
|
-
transaction: trx
|
|
321
|
-
});
|
|
322
|
-
} catch (error) {
|
|
323
|
-
throw new RestApiResourceError(
|
|
324
|
-
`Related ${related.type} with id ${related.id} not found`,
|
|
325
|
-
{
|
|
326
|
-
subtype: 'not_found',
|
|
327
|
-
resourceType: related.type,
|
|
328
|
-
resourceId: related.id
|
|
329
|
-
}
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Prepare records for bulk insert
|
|
336
|
-
const recordsToInsert = relData.map(related => ({
|
|
337
|
-
[relDef.foreignKey]: resourceId,
|
|
338
|
-
[relDef.otherKey]: related.id
|
|
339
|
-
}));
|
|
340
|
-
|
|
341
|
-
// Bulk insert all pivot records in a single query
|
|
342
|
-
await trx(tableName).insert(recordsToInsert);
|
|
343
|
-
};
|