@@ -402,6 +402,165 @@ describe('CRM Metadata Spec Compliance', () => {
402402 }
403403 } ) ;
404404 } ) ;
405+
406+ // ------------------------------------------------------------------
407+ // Enterprise Lookup Field Configuration
408+ // ------------------------------------------------------------------
409+
410+ describe ( 'Enterprise Lookup Metadata' , ( ) => {
411+ /** Extract all lookup fields from an object definition */
412+ function getLookupFields ( obj : Record < string , any > ) : Array < [ string , Record < string , any > ] > {
413+ return Object . entries ( obj . fields ) . filter (
414+ ( [ , f ] : [ string , any ] ) => f . type === 'lookup' || f . type === 'master_detail' ,
415+ ) as Array < [ string , Record < string , any > ] > ;
416+ }
417+
418+ it ( 'every CRM lookup field has lookup_columns configured' , ( ) => {
419+ for ( const obj of allObjects ) {
420+ const lookups = getLookupFields ( obj ) ;
421+ for ( const [ fieldName , field ] of lookups ) {
422+ expect ( field . lookup_columns , `${ obj . name } .${ fieldName } missing lookup_columns` ) . toBeDefined ( ) ;
423+ expect ( Array . isArray ( field . lookup_columns ) ) . toBe ( true ) ;
424+ expect ( field . lookup_columns . length ) . toBeGreaterThanOrEqual ( 2 ) ;
425+ }
426+ }
427+ } ) ;
428+
429+ it ( 'every CRM lookup field has lookup_filters configured' , ( ) => {
430+ for ( const obj of allObjects ) {
431+ const lookups = getLookupFields ( obj ) ;
432+ for ( const [ fieldName , field ] of lookups ) {
433+ expect ( field . lookup_filters , `${ obj . name } .${ fieldName } missing lookup_filters` ) . toBeDefined ( ) ;
434+ expect ( Array . isArray ( field . lookup_filters ) ) . toBe ( true ) ;
435+ expect ( field . lookup_filters . length ) . toBeGreaterThanOrEqual ( 1 ) ;
436+ }
437+ }
438+ } ) ;
439+
440+ it ( 'every CRM lookup field has description_field configured' , ( ) => {
441+ for ( const obj of allObjects ) {
442+ const lookups = getLookupFields ( obj ) ;
443+ for ( const [ fieldName , field ] of lookups ) {
444+ expect ( field . description_field , `${ obj . name } .${ fieldName } missing description_field` ) . toBeDefined ( ) ;
445+ expect ( typeof field . description_field ) . toBe ( 'string' ) ;
446+ }
447+ }
448+ } ) ;
449+
450+ it ( 'lookup_columns include at least one column with a type hint for cell rendering' , ( ) => {
451+ for ( const obj of allObjects ) {
452+ const lookups = getLookupFields ( obj ) ;
453+ for ( const [ fieldName , field ] of lookups ) {
454+ const cols = field . lookup_columns as Array < string | Record < string , any > > ;
455+ const typedCols = cols . filter (
456+ ( c ) => typeof c === 'object' && c . type ,
457+ ) ;
458+ expect (
459+ typedCols . length ,
460+ `${ obj . name } .${ fieldName } has no typed columns for cell rendering` ,
461+ ) . toBeGreaterThanOrEqual ( 1 ) ;
462+ }
463+ }
464+ } ) ;
465+
466+ it ( 'lookup_columns cover diverse cell types (select, currency, boolean, date)' , ( ) => {
467+ const allTypedColumns : string [ ] = [ ] ;
468+ for ( const obj of allObjects ) {
469+ const lookups = getLookupFields ( obj ) ;
470+ for ( const [ , field ] of lookups ) {
471+ const cols = field . lookup_columns as Array < string | Record < string , any > > ;
472+ for ( const c of cols ) {
473+ if ( typeof c === 'object' && c . type ) {
474+ allTypedColumns . push ( c . type ) ;
475+ }
476+ }
477+ }
478+ }
479+ const uniqueTypes = new Set ( allTypedColumns ) ;
480+ expect ( uniqueTypes . has ( 'select' ) ) . toBe ( true ) ;
481+ expect ( uniqueTypes . has ( 'currency' ) ) . toBe ( true ) ;
482+ expect ( uniqueTypes . has ( 'boolean' ) ) . toBe ( true ) ;
483+ expect ( uniqueTypes . has ( 'date' ) ) . toBe ( true ) ;
484+ } ) ;
485+
486+ it ( 'lookup_filters have valid operator values' , ( ) => {
487+ const validOperators = [ 'eq' , 'ne' , 'gt' , 'lt' , 'gte' , 'lte' , 'contains' , 'in' , 'notIn' ] ;
488+ for ( const obj of allObjects ) {
489+ const lookups = getLookupFields ( obj ) ;
490+ for ( const [ fieldName , field ] of lookups ) {
491+ for ( const filter of field . lookup_filters ) {
492+ expect ( filter ) . toHaveProperty ( 'field' ) ;
493+ expect ( filter ) . toHaveProperty ( 'operator' ) ;
494+ expect ( filter ) . toHaveProperty ( 'value' ) ;
495+ expect (
496+ validOperators ,
497+ `${ obj . name } .${ fieldName } filter operator "${ filter . operator } " invalid` ,
498+ ) . toContain ( filter . operator ) ;
499+ }
500+ }
501+ }
502+ } ) ;
503+
504+ it ( 'lookup_filters cover diverse operators (eq, ne, in, notIn)' , ( ) => {
505+ const allOperators : string [ ] = [ ] ;
506+ for ( const obj of allObjects ) {
507+ const lookups = getLookupFields ( obj ) ;
508+ for ( const [ , field ] of lookups ) {
509+ for ( const filter of field . lookup_filters ) {
510+ allOperators . push ( filter . operator ) ;
511+ }
512+ }
513+ }
514+ const uniqueOps = new Set ( allOperators ) ;
515+ expect ( uniqueOps . has ( 'eq' ) ) . toBe ( true ) ;
516+ expect ( uniqueOps . has ( 'in' ) ) . toBe ( true ) ;
517+ expect ( uniqueOps . has ( 'notIn' ) ) . toBe ( true ) ;
518+ expect ( uniqueOps . has ( 'ne' ) ) . toBe ( true ) ;
519+ } ) ;
520+
521+ it ( 'account.owner references user with active-only filter' , ( ) => {
522+ const owner = ( AccountObject . fields as any ) . owner ;
523+ expect ( owner . reference ) . toBe ( 'user' ) ;
524+ expect ( owner . description_field ) . toBe ( 'email' ) ;
525+ expect ( owner . lookup_filters ) . toEqual ( [ { field : 'active' , operator : 'eq' , value : true } ] ) ;
526+ } ) ;
527+
528+ it ( 'opportunity.account references account with type filter' , ( ) => {
529+ const account = ( OpportunityObject . fields as any ) . account ;
530+ expect ( account . reference ) . toBe ( 'account' ) ;
531+ expect ( account . description_field ) . toBe ( 'industry' ) ;
532+ const typeFilter = account . lookup_filters . find ( ( f : any ) => f . field === 'type' ) ;
533+ expect ( typeFilter ) . toBeDefined ( ) ;
534+ expect ( typeFilter . operator ) . toBe ( 'in' ) ;
535+ expect ( typeFilter . value ) . toContain ( 'Customer' ) ;
536+ } ) ;
537+
538+ it ( 'order_item.product filters active products only' , ( ) => {
539+ const product = ( OrderItemObject . fields as any ) . product ;
540+ expect ( product . reference ) . toBe ( 'product' ) ;
541+ expect ( product . description_field ) . toBe ( 'sku' ) ;
542+ expect ( product . lookup_filters ) . toEqual ( [ { field : 'is_active' , operator : 'eq' , value : true } ] ) ;
543+ const cols = product . lookup_columns as Array < Record < string , any > > ;
544+ expect ( cols . find ( ( c ) => c . field === 'price' ) ?. type ) . toBe ( 'currency' ) ;
545+ expect ( cols . find ( ( c ) => c . field === 'stock' ) ?. type ) . toBe ( 'number' ) ;
546+ expect ( cols . find ( ( c ) => c . field === 'is_active' ) ?. type ) . toBe ( 'boolean' ) ;
547+ } ) ;
548+
549+ it ( 'opportunity_contact.opportunity filters out closed stages' , ( ) => {
550+ const opp = ( OpportunityContactObject . fields as any ) . opportunity ;
551+ expect ( opp . reference ) . toBe ( 'opportunity' ) ;
552+ const stageFilter = opp . lookup_filters . find ( ( f : any ) => f . field === 'stage' ) ;
553+ expect ( stageFilter ) . toBeDefined ( ) ;
554+ expect ( stageFilter . operator ) . toBe ( 'notIn' ) ;
555+ expect ( stageFilter . value ) . toContain ( 'closed_won' ) ;
556+ expect ( stageFilter . value ) . toContain ( 'closed_lost' ) ;
557+ } ) ;
558+
559+ it ( 'opportunity.contacts has lookup_page_size for multi-select' , ( ) => {
560+ const contacts = ( OpportunityObject . fields as any ) . contacts ;
561+ expect ( contacts . lookup_page_size ) . toBe ( 15 ) ;
562+ } ) ;
563+ } ) ;
405564} ) ;
406565
407566// ====================================================================
0 commit comments