json-rest-api 1.0.10 → 1.0.11
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/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -167,16 +167,18 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
|
|
|
167
167
|
* // }
|
|
168
168
|
*
|
|
169
169
|
* @example
|
|
170
|
-
* // Input:
|
|
170
|
+
* // Input: Explicit searchSchema takes precedence
|
|
171
171
|
* const schema = {
|
|
172
172
|
* email: { type: 'string', search: true }
|
|
173
173
|
* };
|
|
174
174
|
* const explicitSearchSchema = {
|
|
175
175
|
* email: { type: 'string', filterOperator: 'contains' }
|
|
176
176
|
* };
|
|
177
|
-
* generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
178
|
-
* //
|
|
179
|
-
* //
|
|
177
|
+
* const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
178
|
+
* // Output: Explicit searchSchema wins
|
|
179
|
+
* // {
|
|
180
|
+
* // email: { type: 'string', filterOperator: 'contains' } // Uses explicit definition
|
|
181
|
+
* // }
|
|
180
182
|
*
|
|
181
183
|
* @example
|
|
182
184
|
* // Input: Virtual search fields
|
|
@@ -215,15 +217,16 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
|
|
|
215
217
|
* - Provides flexible search configuration options
|
|
216
218
|
* - Enables virtual fields for cross-table searches
|
|
217
219
|
* - Supports range queries (before/after patterns)
|
|
218
|
-
* -
|
|
220
|
+
* - Merges search:true fields with explicit searchSchema
|
|
221
|
+
* - Explicit searchSchema always takes precedence
|
|
219
222
|
* - Allows storage plugins to optimize queries
|
|
220
223
|
*
|
|
221
224
|
* Data flow:
|
|
222
225
|
* 1. Starts with explicit searchSchema (if any)
|
|
223
226
|
* 2. Processes schema fields with 'search' property
|
|
224
|
-
* 3.
|
|
225
|
-
* 4.
|
|
226
|
-
* 5.
|
|
227
|
+
* 3. Skips fields already in explicit searchSchema (no conflicts)
|
|
228
|
+
* 4. Handles multiple filters from single field
|
|
229
|
+
* 5. Processes virtual search definitions
|
|
227
230
|
* 6. Returns merged searchSchema or null
|
|
228
231
|
*/
|
|
229
232
|
export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
|
|
@@ -240,28 +243,32 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
240
243
|
|
|
241
244
|
if (effectiveSearch) {
|
|
242
245
|
if (effectiveSearch === true) {
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
rule: 'duplicate_search_field',
|
|
252
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
253
|
-
}]
|
|
254
|
-
}
|
|
255
|
-
);
|
|
246
|
+
// For belongsTo relationships, use the relationship name (as property) instead of the field name
|
|
247
|
+
const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
|
|
248
|
+
|
|
249
|
+
// Check if field already exists in explicit searchSchema
|
|
250
|
+
if (searchSchema[searchFieldName]) {
|
|
251
|
+
// Skip - explicit searchSchema takes precedence
|
|
252
|
+
// This allows searchSchema to override fields marked with search:true
|
|
253
|
+
return;
|
|
256
254
|
}
|
|
257
255
|
|
|
258
256
|
// Simple boolean - copy entire field definition (except search) and add filterOperator
|
|
259
257
|
const { search, ...fieldDefWithoutSearch } = fieldDef;
|
|
260
|
-
|
|
258
|
+
const searchEntry = {
|
|
261
259
|
...fieldDefWithoutSearch,
|
|
262
260
|
// Preserve existing filterOperator or default to '='
|
|
263
261
|
filterOperator: fieldDefWithoutSearch.filterOperator || '='
|
|
264
262
|
};
|
|
263
|
+
|
|
264
|
+
// For belongsTo relationships, add metadata to map back to the actual field
|
|
265
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
266
|
+
searchEntry.actualField = fieldName; // Maps to the DB column
|
|
267
|
+
searchEntry.isRelationship = true;
|
|
268
|
+
searchEntry.targetResource = fieldDef.belongsTo;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
searchSchema[searchFieldName] = searchEntry;
|
|
265
272
|
} else if (typeof effectiveSearch === 'object') {
|
|
266
273
|
// Check if search defines multiple filter fields
|
|
267
274
|
const hasNestedFilters = Object.values(effectiveSearch).some(
|
|
@@ -271,19 +278,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
271
278
|
if (hasNestedFilters) {
|
|
272
279
|
// Multiple filters from one field (like published_after/before)
|
|
273
280
|
Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
|
|
274
|
-
// Check
|
|
281
|
+
// Check if filter already exists in explicit searchSchema
|
|
275
282
|
if (searchSchema[filterName]) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
fields: [filterName],
|
|
280
|
-
violations: [{
|
|
281
|
-
field: filterName,
|
|
282
|
-
rule: 'duplicate_search_field',
|
|
283
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
284
|
-
}]
|
|
285
|
-
}
|
|
286
|
-
);
|
|
283
|
+
// Skip - explicit searchSchema takes precedence
|
|
284
|
+
return;
|
|
287
285
|
}
|
|
288
286
|
|
|
289
287
|
searchSchema[filterName] = {
|
|
@@ -293,26 +291,29 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
293
291
|
};
|
|
294
292
|
});
|
|
295
293
|
} else {
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
field: fieldName,
|
|
304
|
-
rule: 'duplicate_search_field',
|
|
305
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
306
|
-
}]
|
|
307
|
-
}
|
|
308
|
-
);
|
|
294
|
+
// For belongsTo relationships, use the relationship name (as property) instead of the field name
|
|
295
|
+
const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
|
|
296
|
+
|
|
297
|
+
// Check if field already exists in explicit searchSchema
|
|
298
|
+
if (searchSchema[searchFieldName]) {
|
|
299
|
+
// Skip - explicit searchSchema takes precedence
|
|
300
|
+
return;
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
// Single filter with config
|
|
312
|
-
|
|
304
|
+
const searchEntry = {
|
|
313
305
|
type: fieldDef.type,
|
|
314
306
|
...effectiveSearch
|
|
315
307
|
};
|
|
308
|
+
|
|
309
|
+
// For belongsTo relationships, add metadata to map back to the actual field
|
|
310
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
311
|
+
searchEntry.actualField = fieldName; // Maps to the DB column
|
|
312
|
+
searchEntry.isRelationship = true;
|
|
313
|
+
searchEntry.targetResource = fieldDef.belongsTo;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
searchSchema[searchFieldName] = searchEntry;
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
}
|
|
@@ -321,19 +322,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
321
322
|
// Handle _virtual search definitions
|
|
322
323
|
if (schema._virtual?.search) {
|
|
323
324
|
Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
|
|
324
|
-
// Check
|
|
325
|
+
// Check if filter already exists in explicit searchSchema
|
|
325
326
|
if (searchSchema[filterName]) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
{
|
|
329
|
-
fields: [filterName],
|
|
330
|
-
violations: [{
|
|
331
|
-
field: filterName,
|
|
332
|
-
rule: 'duplicate_search_field',
|
|
333
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
334
|
-
}]
|
|
335
|
-
}
|
|
336
|
-
);
|
|
327
|
+
// Skip - explicit searchSchema takes precedence
|
|
328
|
+
return;
|
|
337
329
|
}
|
|
338
330
|
|
|
339
331
|
searchSchema[filterName] = filterDef;
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
* scopeOptions: {
|
|
75
75
|
* relationships: {
|
|
76
76
|
* posts: {
|
|
77
|
-
*
|
|
77
|
+
* type: 'hasMany',
|
|
78
|
+
* target: 'posts'
|
|
78
79
|
* // Missing: foreignKey
|
|
79
80
|
* }
|
|
80
81
|
* }
|
|
@@ -154,8 +155,8 @@ export function validateRelationships({ context, scopes }) {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
// Validate
|
|
158
|
-
if (relDef.hasMany) {
|
|
158
|
+
// Validate based on relationship type
|
|
159
|
+
if (relDef.type === 'hasMany') {
|
|
159
160
|
const validation = validateHasManyRelationship(relDef, relName, scopeName);
|
|
160
161
|
if (!validation.valid) {
|
|
161
162
|
throw new Error(
|
|
@@ -164,8 +165,16 @@ export function validateRelationships({ context, scopes }) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if (relDef.type === 'hasOne') {
|
|
169
|
+
const validation = validateHasOneRelationship(relDef, relName, scopeName);
|
|
170
|
+
if (!validation.valid) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Invalid hasOne relationship '${relName}' in scope '${scopeName}': ${validation.error}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (relDef.type === 'manyToMany') {
|
|
169
178
|
const validation = validateManyToManyRelationship(relDef, relName, scopeName);
|
|
170
179
|
if (!validation.valid) {
|
|
171
180
|
throw new Error(
|
|
@@ -290,7 +299,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
|
|
|
290
299
|
* @example
|
|
291
300
|
* // Input: Valid hasMany configuration
|
|
292
301
|
* const relDef = {
|
|
293
|
-
*
|
|
302
|
+
* type: 'hasMany',
|
|
303
|
+
* target: 'posts',
|
|
294
304
|
* foreignKey: 'author_id' // Required!
|
|
295
305
|
* };
|
|
296
306
|
* validateHasManyRelationship(relDef, 'posts', 'users');
|
|
@@ -299,7 +309,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
|
|
|
299
309
|
* @example
|
|
300
310
|
* // Input: Missing required foreignKey
|
|
301
311
|
* const relDef = {
|
|
302
|
-
*
|
|
312
|
+
* type: 'hasMany',
|
|
313
|
+
* target: 'posts'
|
|
303
314
|
* // Missing: foreignKey
|
|
304
315
|
* };
|
|
305
316
|
* validateHasManyRelationship(relDef, 'posts', 'users');
|
|
@@ -311,7 +322,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
|
|
|
311
322
|
* @example
|
|
312
323
|
* // Input: Polymorphic hasMany with 'via' (doesn't need foreignKey)
|
|
313
324
|
* const relDef = {
|
|
314
|
-
*
|
|
325
|
+
* type: 'hasMany',
|
|
326
|
+
* target: 'comments',
|
|
315
327
|
* via: 'commentable' // Uses polymorphic relationship
|
|
316
328
|
* };
|
|
317
329
|
* validateHasManyRelationship(relDef, 'comments', 'posts');
|
|
@@ -320,6 +332,14 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
|
|
|
320
332
|
* @private
|
|
321
333
|
*/
|
|
322
334
|
const validateHasManyRelationship = (relDef, relName, scopeName) => {
|
|
335
|
+
// Validate target is specified
|
|
336
|
+
if (!relDef.target) {
|
|
337
|
+
return {
|
|
338
|
+
valid: false,
|
|
339
|
+
error: `hasMany relationship requires 'target' to be specified.`
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
323
343
|
// Polymorphic hasMany relationships using 'via' don't need foreignKey
|
|
324
344
|
// They use the polymorphic fields (typeField, idField) from the belongsToPolymorphic relationship
|
|
325
345
|
if (relDef.via) {
|
|
@@ -340,11 +360,49 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
|
|
|
340
360
|
};
|
|
341
361
|
}
|
|
342
362
|
|
|
343
|
-
|
|
344
|
-
|
|
363
|
+
return { valid: true };
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Validates a hasOne relationship definition
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} relDef - Relationship definition with hasOne
|
|
370
|
+
* @param {string} relName - Relationship name
|
|
371
|
+
* @param {string} scopeName - Scope being registered
|
|
372
|
+
* @returns {Object} Validation result {valid: boolean, error?: string}
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* // Input: Valid hasOne configuration
|
|
376
|
+
* const relDef = {
|
|
377
|
+
* type: 'hasOne',
|
|
378
|
+
* target: 'profile',
|
|
379
|
+
* foreignKey: 'user_id' // Required!
|
|
380
|
+
* };
|
|
381
|
+
* validateHasOneRelationship(relDef, 'profile', 'users');
|
|
382
|
+
* // Output: { valid: true }
|
|
383
|
+
*
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
const validateHasOneRelationship = (relDef, relName, scopeName) => {
|
|
387
|
+
// Validate target is specified
|
|
388
|
+
if (!relDef.target) {
|
|
389
|
+
return {
|
|
390
|
+
valid: false,
|
|
391
|
+
error: `hasOne relationship requires 'target' to be specified.`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!relDef.foreignKey) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
error: `hasOne relationship requires foreignKey to be specified. Add foreignKey: '<field_name>' to the relationship definition.`
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (typeof relDef.foreignKey !== 'string') {
|
|
345
403
|
return {
|
|
346
404
|
valid: false,
|
|
347
|
-
error: `
|
|
405
|
+
error: `hasOne relationship foreignKey must be a string, got ${typeof relDef.foreignKey}`
|
|
348
406
|
};
|
|
349
407
|
}
|
|
350
408
|
|
|
@@ -362,11 +420,10 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
|
|
|
362
420
|
* @example
|
|
363
421
|
* // Input: Valid manyToMany configuration
|
|
364
422
|
* const relDef = {
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* }
|
|
423
|
+
* type: 'manyToMany',
|
|
424
|
+
* through: 'article_tags', // Pivot table
|
|
425
|
+
* foreignKey: 'article_id', // This scope's FK
|
|
426
|
+
* otherKey: 'tag_id' // Other scope's FK
|
|
370
427
|
* };
|
|
371
428
|
* validateManyToManyRelationship(relDef, 'tags', 'articles');
|
|
372
429
|
* // Output: { valid: true }
|
|
@@ -374,11 +431,10 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
|
|
|
374
431
|
* @example
|
|
375
432
|
* // Input: Missing required fields
|
|
376
433
|
* const relDef = {
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
* }
|
|
434
|
+
* type: 'manyToMany',
|
|
435
|
+
* through: 'article_tags',
|
|
436
|
+
* foreignKey: 'article_id'
|
|
437
|
+
* // Missing: otherKey
|
|
382
438
|
* };
|
|
383
439
|
* validateManyToManyRelationship(relDef, 'tags', 'articles');
|
|
384
440
|
* // Output: {
|
|
@@ -389,7 +445,7 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
|
|
|
389
445
|
* @private
|
|
390
446
|
*/
|
|
391
447
|
const validateManyToManyRelationship = (relDef, relName, scopeName) => {
|
|
392
|
-
const { through, foreignKey, otherKey } = relDef
|
|
448
|
+
const { through, foreignKey, otherKey } = relDef;
|
|
393
449
|
|
|
394
450
|
if (!through) {
|
|
395
451
|
return {
|