     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	 * Validates all relationships in a scope configuration
    14	 *
    15	 * @param {Object} params - Validation parameters
    16	 * @param {Object} params.context - Event context with scopeName and scopeOptions
    17	 * @param {Object} params.scopes - All registered scopes for validation
    18	 * @throws {Error} If any relationship is misconfigured
    19	 *
    20	 * @example
    21	 * // Input: Valid polymorphic relationship
    22	 * validateRelationships({
    23	 *   context: {
    24	 *     scopeName: 'comments',
    25	 *     scopeOptions: {
    26	 *       schema: {
    27	 *         commentable_type: { type: 'string' },
    28	 *         commentable_id: { type: 'integer' }
    29	 *       },
    30	 *       relationships: {
    31	 *         commentable: {
    32	 *           belongsToPolymorphic: {
    33	 *             types: ['posts', 'videos'],     // Must exist
    34	 *             typeField: 'commentable_type',
    35	 *             idField: 'commentable_id'
    36	 *           }
    37	 *         }
    38	 *       }
    39	 *     }
    40	 *   },
    41	 *   scopes: { posts: {}, videos: {} }         // Referenced types exist
    42	 * });
    43	 * // Output: No error (valid configuration)
    44	 *
    45	 * @example
    46	 * // Input: Missing required field
    47	 * validateRelaationships({
    48	 *   context: {
    49	 *     scopeName: 'attachments',
    50	 *     scopeOptions: {
    51	 *       relationships: {
    52	 *         attachable: {
    53	 *           belongsToPolymorphic: {
    54	 *             types: ['documents'],
    55	 *             typeField: 'attachable_type'
    56	 *             // Missing: idField
    57	 *           }
    58	 *         }
    59	 *       }
    60	 *     }
    61	 *   },
    62	 *   scopes: { documents: {} }
    63	 * });
    64	 * // Throws: Error
    65	 * // "Invalid polymorphic relationship 'attachable' in scope 'attachments':
    66	 * //  belongsToPolymorphic.idField must be specified"
    67	 *
    68	 * @example
    69	 * // Input: HasMany without foreignKey
    70	 * validateRelationships({
    71	 *   context: {
    72	 *     scopeName: 'users',
    73	 *     scopeOptions: {
    74	 *       relationships: {
    75	 *         posts: {
    76	 *           type: 'hasMany',
    77	 *           target: '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 based on relationship type
   158	    if (relDef.type === '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	    if (relDef.type === 'hasOne') {
   168	      const validation = validateHasOneRelationship(relDef, relName, scopeName)
   169	      if (!validation.valid) {
   170	        throw new Error(
   171	          `Invalid hasOne relationship '${relName}' in scope '${scopeName}': ${validation.error}`
   172	        )
   173	      }
   174	    }
   175	
   176	    if (relDef.type === 'manyToMany') {
   177	      const validation = validateManyToManyRelationship(relDef, relName, scopeName)
   178	      if (!validation.valid) {
   179	        throw new Error(
   180	          `Invalid manyToMany relationship '${relName}' in scope '${scopeName}': ${validation.error}`
   181	        )
   182	      }
   183	    }
   184	  }
   185	}
   186	
   187	/**
   188	 * Validates a polymorphic relationship definition
   189	 *
   190	 * @param {Object} relDef - Relationship definition with belongsToPolymorphic
   191	 * @param {string} scopeName - Scope being registered (for error messages)
   192	 * @param {Object} scopes - All registered scopes for validation
   193	 * @returns {Object} Validation result {valid: boolean, error?: string}
   194	 *
   195	 * @example
   196	 * // Input: Valid polymorphic configuration
   197	 * const relDef = {
   198	 *   belongsToPolymorphic: {
   199	 *     types: ['articles', 'videos', 'products'],
   200	 *     typeField: 'commentable_type',
   201	 *     idField: 'commentable_id'
   202	 *   }
   203	 * };
   204	 * validatePolymorphicRelationship(relDef, 'comments', scopes);
   205	 * // Output: { valid: true }
   206	 *
   207	 * @example
   208	 * // Input: Missing required types array
   209	 * const relDef = {
   210	 *   belongsToPolymorphic: {
   211	 *     typeField: 'commentable_type',
   212	 *     idField: 'commentable_id'
   213	 *     // Missing: types
   214	 *   }
   215	 * };
   216	 * validatePolymorphicRelationship(relDef, 'comments', scopes);
   217	 * // Output: {
   218	 * //   valid: false,
   219	 * //   error: 'belongsToPolymorphic.types must be a non-empty array'
   220	 * // }
   221	 *
   222	 * @example
   223	 * // Input: References non-existent scope
   224	 * const relDef = {
   225	 *   belongsToPolymorphic: {
   226	 *     types: ['articles', 'unicorns'],    // 'unicorns' doesn't exist
   227	 *     typeField: 'attachable_type',
   228	 *     idField: 'attachable_id'
   229	 *   }
   230	 * };
   231	 * validatePolymorphicRelationship(relDef, 'attachments', scopes);
   232	 * // Output: {
   233	 * //   valid: false,
   234	 * //   error: "Polymorphic type 'unicorns' is not a registered scope"
   235	 * // }
   236	 *
   237	 * @private
   238	 */
   239	const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
   240	  // Validation logic:
   241	  // 1. Check relDef.belongsToPolymorphic exists
   242	  // 2. Validate required properties: types, typeField, idField
   243	  // 3. Ensure types is non-empty array
   244	  // 4. Verify all types are registered scopes
   245	  // 5. Check that typeField and idField exist in the schema
   246	
   247	  const { belongsToPolymorphic } = relDef
   248	
   249	  if (!belongsToPolymorphic) {
   250	    return { valid: false, error: 'Missing belongsToPolymorphic definition' }
   251	  }
   252	
   253	  const { types, typeField, idField } = belongsToPolymorphic
   254	
   255	  if (!types || !Array.isArray(types) || types.length === 0) {
   256	    return {
   257	      valid: false,
   258	      error: 'belongsToPolymorphic.types must be a non-empty array'
   259	    }
   260	  }
   261	
   262	  if (!typeField || typeof typeField !== 'string') {
   263	    return {
   264	      valid: false,
   265	      error: 'belongsToPolymorphic.typeField must be specified'
   266	    }
   267	  }
   268	
   269	  if (!idField || typeof idField !== 'string') {
   270	    return {
   271	      valid: false,
   272	      error: 'belongsToPolymorphic.idField must be specified'
   273	    }
   274	  }
   275	
   276	  // Check that all types are valid scopes
   277	  for (const type of types) {
   278	    if (!scopes[type]) {
   279	      return {
   280	        valid: false,
   281	        error: `Polymorphic type '${type}' is not a registered scope`
   282	      }
   283	    }
   284	  }
   285	
   286	  return { valid: true }
   287	}
   288	
   289	/**
   290	 * Validates a hasMany relationship definition
   291	 *
   292	 * @param {Object} relDef - Relationship definition with hasMany
   293	 * @param {string} relName - Relationship name
   294	 * @param {string} scopeName - Scope being registered
   295	 * @returns {Object} Validation result {valid: boolean, error?: string}
   296	 *
   297	 * @example
   298	 * // Input: Valid hasMany configuration
   299	 * const relDef = {
   300	 *   type: 'hasMany',
   301	 *   target: 'posts',
   302	 *   foreignKey: 'author_id'              // Required!
   303	 * };
   304	 * validateHasManyRelationship(relDef, 'posts', 'users');
   305	 * // Output: { valid: true }
   306	 *
   307	 * @example
   308	 * // Input: Missing required foreignKey
   309	 * const relDef = {
   310	 *   type: 'hasMany',
   311	 *   target: 'posts'
   312	 *   // Missing: foreignKey
   313	 * };
   314	 * validateHasManyRelationship(relDef, 'posts', 'users');
   315	 * // Output: {
   316	 * //   valid: false,
   317	 * //   error: 'hasMany relationship requires foreignKey to be specified...'
   318	 * // }
   319	 *
   320	 * @example
   321	 * // Input: Polymorphic hasMany with 'via' (doesn't need foreignKey)
   322	 * const relDef = {
   323	 *   type: 'hasMany',
   324	 *   target: 'comments',
   325	 *   via: 'commentable'                   // Uses polymorphic relationship
   326	 * };
   327	 * validateHasManyRelationship(relDef, 'comments', 'posts');
   328	 * // Output: { valid: true }
   329	 *
   330	 * @private
   331	 */
   332	const validateHasManyRelationship = (relDef, relName, scopeName) => {
   333	  // Validate target is specified
   334	  if (!relDef.target) {
   335	    return {
   336	      valid: false,
   337	      error: 'hasMany relationship requires \'target\' to be specified.'
   338	    }
   339	  }
   340	
   341	  // Polymorphic hasMany relationships using 'via' don't need foreignKey
   342	  // They use the polymorphic fields (typeField, idField) from the belongsToPolymorphic relationship
   343	  if (relDef.via) {
   344	    return { valid: true }
   345	  }
   346	
   347	  if (!relDef.foreignKey) {
   348	    return {
   349	      valid: false,
   350	      error: 'hasMany relationship requires foreignKey to be specified. Add foreignKey: \'<field_name>\' to the relationship definition.'
   351	    }
   352	  }
   353	
   354	  if (typeof relDef.foreignKey !== 'string') {
   355	    return {
   356	      valid: false,
   357	      error: `hasMany relationship foreignKey must be a string, got ${typeof relDef.foreignKey}`
   358	    }
   359	  }
   360	
   361	  return { valid: true }
   362	}
   363	
   364	/**
   365	 * Validates a hasOne relationship definition
   366	 *
   367	 * @param {Object} relDef - Relationship definition with hasOne
   368	 * @param {string} relName - Relationship name
   369	 * @param {string} scopeName - Scope being registered
   370	 * @returns {Object} Validation result {valid: boolean, error?: string}
   371	 *
   372	 * @example
   373	 * // Input: Valid hasOne configuration
   374	 * const relDef = {
   375	 *   type: 'hasOne',
   376	 *   target: 'profile',
   377	 *   foreignKey: 'user_id'              // Required!
   378	 * };
   379	 * validateHasOneRelationship(relDef, 'profile', 'users');
   380	 * // Output: { valid: true }
   381	 *
   382	 * @private
   383	 */
   384	const validateHasOneRelationship = (relDef, relName, scopeName) => {
   385	  // Validate target is specified
   386	  if (!relDef.target) {
   387	    return {
   388	      valid: false,
   389	      error: 'hasOne relationship requires \'target\' to be specified.'
   390	    }
   391	  }
   392	
   393	  if (!relDef.foreignKey) {
   394	    return {
   395	      valid: false,
   396	      error: 'hasOne relationship requires foreignKey to be specified. Add foreignKey: \'<field_name>\' to the relationship definition.'
   397	    }
   398	  }
   399	
   400	  if (typeof relDef.foreignKey !== 'string') {
   401	    return {
   402	      valid: false,
   403	      error: `hasOne relationship foreignKey must be a string, got ${typeof relDef.foreignKey}`
   404	    }
   405	  }
   406	
   407	  return { valid: true }
   408	}
   409	
   410	/**
   411	 * Validates a manyToMany relationship definition
   412	 *
   413	 * @param {Object} relDef - Relationship definition with manyToMany
   414	 * @param {string} relName - Relationship name
   415	 * @param {string} scopeName - Scope being registered
   416	 * @returns {Object} Validation result {valid: boolean, error?: string}
   417	 *
   418	 * @example
   419	 * // Input: Valid manyToMany configuration
   420	 * const relDef = {
   421	 *   type: 'manyToMany',
   422	 *   through: 'article_tags',           // Pivot table
   423	 *   foreignKey: 'article_id',          // This scope's FK
   424	 *   otherKey: 'tag_id'                 // Other scope's FK
   425	 * };
   426	 * validateManyToManyRelationship(relDef, 'tags', 'articles');
   427	 * // Output: { valid: true }
   428	 *
   429	 * @example
   430	 * // Input: Missing required fields
   431	 * const relDef = {
   432	 *   type: 'manyToMany',
   433	 *   through: 'article_tags',
   434	 *   foreignKey: 'article_id'
   435	 *   // Missing: otherKey
   436	 * };
   437	 * validateManyToManyRelationship(relDef, 'tags', 'articles');
   438	 * // Output: {
   439	 * //   valid: false,
   440	 * //   error: 'manyToMany relationship requires otherKey to be specified...'
   441	 * // }
   442	 *
   443	 * @private
   444	 */
   445	const validateManyToManyRelationship = (relDef, relName, scopeName) => {
   446	  const { through, foreignKey, otherKey } = relDef
   447	
   448	  if (!through) {
   449	    return {
   450	      valid: false,
   451	      error: 'manyToMany relationship requires \'through\' table to be specified'
   452	    }
   453	  }
   454	
   455	  if (!foreignKey) {
   456	    return {
   457	      valid: false,
   458	      error: 'manyToMany relationship requires foreignKey to be specified. Add foreignKey: \'<field_name>\' to the manyToMany configuration.'
   459	    }
   460	  }
   461	
   462	  if (!otherKey) {
   463	    return {
   464	      valid: false,
   465	      error: 'manyToMany relationship requires otherKey to be specified. Add otherKey: \'<field_name>\' to the manyToMany configuration.'
   466	    }
   467	  }
   468	
   469	  if (typeof foreignKey !== 'string') {
   470	    return {
   471	      valid: false,
   472	      error: `manyToMany relationship foreignKey must be a string, got ${typeof foreignKey}`
   473	    }
   474	  }
   475	
   476	  if (typeof otherKey !== 'string') {
   477	    return {
   478	      valid: false,
   479	      error: `manyToMany relationship otherKey must be a string, got ${typeof otherKey}`
   480	    }
   481	  }
   482	
   483	  return { valid: true }
   484	}
