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.
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 +99 -33
  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
@@ -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
- };