json-rest-api 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Scope configuration validators for relationship definitions
3
+ *
4
+ * @description
5
+ * These validators run at scope registration time to ensure:
6
+ * - Polymorphic relationships are properly configured
7
+ * - HasMany relationships specify required foreign keys
8
+ * - ManyToMany relationships have pivot table settings
9
+ * - Referenced scopes actually exist in the system
10
+ */
11
+
12
+
13
+ /**
14
+ * Validates all relationships in a scope configuration
15
+ *
16
+ * @param {Object} params - Validation parameters
17
+ * @param {Object} params.context - Event context with scopeName and scopeOptions
18
+ * @param {Object} params.scopes - All registered scopes for validation
19
+ * @throws {Error} If any relationship is misconfigured
20
+ *
21
+ * @example
22
+ * // Input: Valid polymorphic relationship
23
+ * validateRelationships({
24
+ * context: {
25
+ * scopeName: 'comments',
26
+ * scopeOptions: {
27
+ * schema: {
28
+ * commentable_type: { type: 'string' },
29
+ * commentable_id: { type: 'integer' }
30
+ * },
31
+ * relationships: {
32
+ * commentable: {
33
+ * belongsToPolymorphic: {
34
+ * types: ['posts', 'videos'], // Must exist
35
+ * typeField: 'commentable_type',
36
+ * idField: 'commentable_id'
37
+ * }
38
+ * }
39
+ * }
40
+ * }
41
+ * },
42
+ * scopes: { posts: {}, videos: {} } // Referenced types exist
43
+ * });
44
+ * // Output: No error (valid configuration)
45
+ *
46
+ * @example
47
+ * // Input: Missing required field
48
+ * validateRelaationships({
49
+ * context: {
50
+ * scopeName: 'attachments',
51
+ * scopeOptions: {
52
+ * relationships: {
53
+ * attachable: {
54
+ * belongsToPolymorphic: {
55
+ * types: ['documents'],
56
+ * typeField: 'attachable_type'
57
+ * // Missing: idField
58
+ * }
59
+ * }
60
+ * }
61
+ * }
62
+ * },
63
+ * scopes: { documents: {} }
64
+ * });
65
+ * // Throws: Error
66
+ * // "Invalid polymorphic relationship 'attachable' in scope 'attachments':
67
+ * // belongsToPolymorphic.idField must be specified"
68
+ *
69
+ * @example
70
+ * // Input: HasMany without foreignKey
71
+ * validateRelationships({
72
+ * context: {
73
+ * scopeName: 'users',
74
+ * scopeOptions: {
75
+ * relationships: {
76
+ * posts: {
77
+ * hasMany: 'posts'
78
+ * // Missing: foreignKey
79
+ * }
80
+ * }
81
+ * }
82
+ * },
83
+ * scopes: { posts: {} }
84
+ * });
85
+ * // Throws: Error
86
+ * // "Invalid hasMany relationship 'posts' in scope 'users':
87
+ * // hasMany relationship requires foreignKey to be specified"
88
+ *
89
+ * @example
90
+ * // Input: Complex activity feed with multiple polymorphic relationships
91
+ * validateRelationships({
92
+ * context: {
93
+ * scopeName: 'activities',
94
+ * scopeOptions: {
95
+ * schema: {
96
+ * trackable_type: { type: 'string' },
97
+ * trackable_id: { type: 'integer' },
98
+ * actor_type: { type: 'string' },
99
+ * actor_id: { type: 'integer' }
100
+ * },
101
+ * relationships: {
102
+ * trackable: { // What was changed
103
+ * belongsToPolymorphic: {
104
+ * types: ['posts', 'comments', 'users'],
105
+ * typeField: 'trackable_type',
106
+ * idField: 'trackable_id'
107
+ * }
108
+ * },
109
+ * actor: { // Who made the change
110
+ * belongsToPolymorphic: {
111
+ * types: ['users', 'api_clients'],
112
+ * typeField: 'actor_type',
113
+ * idField: 'actor_id'
114
+ * }
115
+ * }
116
+ * }
117
+ * }
118
+ * },
119
+ * scopes: { posts: {}, comments: {}, users: {}, api_clients: {} }
120
+ * });
121
+ * // Output: No error (both polymorphic relationships valid)
122
+ *
123
+ * @description
124
+ * Used by:
125
+ * - rest-api-plugin on 'scope:added' event
126
+ * - Runs automatically when scopes are registered
127
+ *
128
+ * Purpose:
129
+ * - Catches configuration errors at startup, not runtime
130
+ * - Validates polymorphic relationship structure
131
+ * - Ensures referenced scope types exist
132
+ * - Validates hasMany/manyToMany foreign keys
133
+ * - Provides clear error messages for debugging
134
+ *
135
+ * Data flow:
136
+ * 1. Extracts relationships from scope options
137
+ * 2. For each relationship, checks its type
138
+ * 3. Validates polymorphic: types, typeField, idField
139
+ * 4. Validates hasMany: foreignKey (unless via)
140
+ * 5. Validates manyToMany: through, foreignKey, otherKey
141
+ * 6. Throws descriptive error on first validation failure
142
+ */
143
+ export function validateRelationships({ context, scopes }) {
144
+ const { scopeName, scopeOptions } = context;
145
+ const relationships = scopeOptions.relationships || {};
146
+
147
+ for (const [relName, relDef] of Object.entries(relationships)) {
148
+ if (relDef.belongsToPolymorphic) {
149
+ const validation = validatePolymorphicRelationship(relDef, scopeName, scopes);
150
+ if (!validation.valid) {
151
+ throw new Error(
152
+ `Invalid polymorphic relationship '${relName}' in scope '${scopeName}': ${validation.error}`
153
+ );
154
+ }
155
+ }
156
+
157
+ // Validate hasMany relationships require foreignKey
158
+ if (relDef.hasMany) {
159
+ const validation = validateHasManyRelationship(relDef, relName, scopeName);
160
+ if (!validation.valid) {
161
+ throw new Error(
162
+ `Invalid hasMany relationship '${relName}' in scope '${scopeName}': ${validation.error}`
163
+ );
164
+ }
165
+ }
166
+
167
+ // Validate manyToMany relationships require both foreignKey and otherKey
168
+ if (relDef.manyToMany) {
169
+ const validation = validateManyToManyRelationship(relDef, relName, scopeName);
170
+ if (!validation.valid) {
171
+ throw new Error(
172
+ `Invalid manyToMany relationship '${relName}' in scope '${scopeName}': ${validation.error}`
173
+ );
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+
180
+ /**
181
+ * Validates a polymorphic relationship definition
182
+ *
183
+ * @param {Object} relDef - Relationship definition with belongsToPolymorphic
184
+ * @param {string} scopeName - Scope being registered (for error messages)
185
+ * @param {Object} scopes - All registered scopes for validation
186
+ * @returns {Object} Validation result {valid: boolean, error?: string}
187
+ *
188
+ * @example
189
+ * // Input: Valid polymorphic configuration
190
+ * const relDef = {
191
+ * belongsToPolymorphic: {
192
+ * types: ['articles', 'videos', 'products'],
193
+ * typeField: 'commentable_type',
194
+ * idField: 'commentable_id'
195
+ * }
196
+ * };
197
+ * validatePolymorphicRelationship(relDef, 'comments', scopes);
198
+ * // Output: { valid: true }
199
+ *
200
+ * @example
201
+ * // Input: Missing required types array
202
+ * const relDef = {
203
+ * belongsToPolymorphic: {
204
+ * typeField: 'commentable_type',
205
+ * idField: 'commentable_id'
206
+ * // Missing: types
207
+ * }
208
+ * };
209
+ * validatePolymorphicRelationship(relDef, 'comments', scopes);
210
+ * // Output: {
211
+ * // valid: false,
212
+ * // error: 'belongsToPolymorphic.types must be a non-empty array'
213
+ * // }
214
+ *
215
+ * @example
216
+ * // Input: References non-existent scope
217
+ * const relDef = {
218
+ * belongsToPolymorphic: {
219
+ * types: ['articles', 'unicorns'], // 'unicorns' doesn't exist
220
+ * typeField: 'attachable_type',
221
+ * idField: 'attachable_id'
222
+ * }
223
+ * };
224
+ * validatePolymorphicRelationship(relDef, 'attachments', scopes);
225
+ * // Output: {
226
+ * // valid: false,
227
+ * // error: "Polymorphic type 'unicorns' is not a registered scope"
228
+ * // }
229
+ *
230
+ * @private
231
+ */
232
+ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
233
+ // Validation logic:
234
+ // 1. Check relDef.belongsToPolymorphic exists
235
+ // 2. Validate required properties: types, typeField, idField
236
+ // 3. Ensure types is non-empty array
237
+ // 4. Verify all types are registered scopes
238
+ // 5. Check that typeField and idField exist in the schema
239
+
240
+ const { belongsToPolymorphic } = relDef;
241
+
242
+ if (!belongsToPolymorphic) {
243
+ return { valid: false, error: 'Missing belongsToPolymorphic definition' };
244
+ }
245
+
246
+ const { types, typeField, idField } = belongsToPolymorphic;
247
+
248
+ if (!types || !Array.isArray(types) || types.length === 0) {
249
+ return {
250
+ valid: false,
251
+ error: 'belongsToPolymorphic.types must be a non-empty array'
252
+ };
253
+ }
254
+
255
+ if (!typeField || typeof typeField !== 'string') {
256
+ return {
257
+ valid: false,
258
+ error: 'belongsToPolymorphic.typeField must be specified'
259
+ };
260
+ }
261
+
262
+ if (!idField || typeof idField !== 'string') {
263
+ return {
264
+ valid: false,
265
+ error: 'belongsToPolymorphic.idField must be specified'
266
+ };
267
+ }
268
+
269
+ // Check that all types are valid scopes
270
+ for (const type of types) {
271
+ if (!scopes[type]) {
272
+ return {
273
+ valid: false,
274
+ error: `Polymorphic type '${type}' is not a registered scope`
275
+ };
276
+ }
277
+ }
278
+
279
+ return { valid: true };
280
+ };
281
+
282
+ /**
283
+ * Validates a hasMany relationship definition
284
+ *
285
+ * @param {Object} relDef - Relationship definition with hasMany
286
+ * @param {string} relName - Relationship name
287
+ * @param {string} scopeName - Scope being registered
288
+ * @returns {Object} Validation result {valid: boolean, error?: string}
289
+ *
290
+ * @example
291
+ * // Input: Valid hasMany configuration
292
+ * const relDef = {
293
+ * hasMany: 'posts',
294
+ * foreignKey: 'author_id' // Required!
295
+ * };
296
+ * validateHasManyRelationship(relDef, 'posts', 'users');
297
+ * // Output: { valid: true }
298
+ *
299
+ * @example
300
+ * // Input: Missing required foreignKey
301
+ * const relDef = {
302
+ * hasMany: 'posts'
303
+ * // Missing: foreignKey
304
+ * };
305
+ * validateHasManyRelationship(relDef, 'posts', 'users');
306
+ * // Output: {
307
+ * // valid: false,
308
+ * // error: 'hasMany relationship requires foreignKey to be specified...'
309
+ * // }
310
+ *
311
+ * @example
312
+ * // Input: Polymorphic hasMany with 'via' (doesn't need foreignKey)
313
+ * const relDef = {
314
+ * hasMany: 'comments',
315
+ * via: 'commentable' // Uses polymorphic relationship
316
+ * };
317
+ * validateHasManyRelationship(relDef, 'comments', 'posts');
318
+ * // Output: { valid: true }
319
+ *
320
+ * @private
321
+ */
322
+ const validateHasManyRelationship = (relDef, relName, scopeName) => {
323
+ // Polymorphic hasMany relationships using 'via' don't need foreignKey
324
+ // They use the polymorphic fields (typeField, idField) from the belongsToPolymorphic relationship
325
+ if (relDef.via) {
326
+ return { valid: true };
327
+ }
328
+
329
+ if (!relDef.foreignKey) {
330
+ return {
331
+ valid: false,
332
+ error: `hasMany relationship requires foreignKey to be specified. Add foreignKey: '<field_name>' to the relationship definition.`
333
+ };
334
+ }
335
+
336
+ if (typeof relDef.foreignKey !== 'string') {
337
+ return {
338
+ valid: false,
339
+ error: `hasMany relationship foreignKey must be a string, got ${typeof relDef.foreignKey}`
340
+ };
341
+ }
342
+
343
+ // If through is specified, also require otherKey for many-to-many
344
+ if (relDef.through && !relDef.otherKey) {
345
+ return {
346
+ valid: false,
347
+ error: `hasMany relationship with 'through' requires both foreignKey and otherKey to be specified`
348
+ };
349
+ }
350
+
351
+ return { valid: true };
352
+ };
353
+
354
+ /**
355
+ * Validates a manyToMany relationship definition
356
+ *
357
+ * @param {Object} relDef - Relationship definition with manyToMany
358
+ * @param {string} relName - Relationship name
359
+ * @param {string} scopeName - Scope being registered
360
+ * @returns {Object} Validation result {valid: boolean, error?: string}
361
+ *
362
+ * @example
363
+ * // Input: Valid manyToMany configuration
364
+ * const relDef = {
365
+ * manyToMany: {
366
+ * through: 'article_tags', // Pivot table
367
+ * foreignKey: 'article_id', // This scope's FK
368
+ * otherKey: 'tag_id' // Other scope's FK
369
+ * }
370
+ * };
371
+ * validateManyToManyRelationship(relDef, 'tags', 'articles');
372
+ * // Output: { valid: true }
373
+ *
374
+ * @example
375
+ * // Input: Missing required fields
376
+ * const relDef = {
377
+ * manyToMany: {
378
+ * through: 'article_tags',
379
+ * foreignKey: 'article_id'
380
+ * // Missing: otherKey
381
+ * }
382
+ * };
383
+ * validateManyToManyRelationship(relDef, 'tags', 'articles');
384
+ * // Output: {
385
+ * // valid: false,
386
+ * // error: 'manyToMany relationship requires otherKey to be specified...'
387
+ * // }
388
+ *
389
+ * @private
390
+ */
391
+ const validateManyToManyRelationship = (relDef, relName, scopeName) => {
392
+ const { through, foreignKey, otherKey } = relDef.manyToMany;
393
+
394
+ if (!through) {
395
+ return {
396
+ valid: false,
397
+ error: `manyToMany relationship requires 'through' table to be specified`
398
+ };
399
+ }
400
+
401
+ if (!foreignKey) {
402
+ return {
403
+ valid: false,
404
+ error: `manyToMany relationship requires foreignKey to be specified. Add foreignKey: '<field_name>' to the manyToMany configuration.`
405
+ };
406
+ }
407
+
408
+ if (!otherKey) {
409
+ return {
410
+ valid: false,
411
+ error: `manyToMany relationship requires otherKey to be specified. Add otherKey: '<field_name>' to the manyToMany configuration.`
412
+ };
413
+ }
414
+
415
+ if (typeof foreignKey !== 'string') {
416
+ return {
417
+ valid: false,
418
+ error: `manyToMany relationship foreignKey must be a string, got ${typeof foreignKey}`
419
+ };
420
+ }
421
+
422
+ if (typeof otherKey !== 'string') {
423
+ return {
424
+ valid: false,
425
+ error: `manyToMany relationship otherKey must be a string, got ${typeof otherKey}`
426
+ };
427
+ }
428
+
429
+ return { valid: true };
430
+ };
431
+