feat (search): add dynamic search field metadata foundation for database-driven search configuration (#14469)

## Summary

Implements the foundation for dynamic search field configuration from
[issue #1428](https://github.com/twentyhq/core-team-issues/issues/1428).

## Changes

- Add `SearchFieldMetadataEntity` junction table for storing searchable
field configurations
- Add `SearchFieldMetadataService` with core CRUD operations  
- Add `SearchFieldMetadataModule` following existing patterns  
- Add `IS_DYNAMIC_SEARCH_FIELDS_ENABLED` feature flag (defaults to
`false`)
- Database migration with proper indexes and foreign keys  

## Architecture

Uses a junction table where **record existence = field is searchable**:

**Table: `searchFieldMetadata(objectMetadataId, fieldMetadataId,
workspaceId)`**

- Unique constraint on `(objectMetadataId, fieldMetadataId)`  
- `ON DELETE CASCADE` on foreign keys  

## Testing

- [x] Migration runs successfully  
- [x] Table created with correct schema  
- [x] Feature flag seeded properly  
- [x] Database reset works correctly  

## Next Steps

Future PRs will handle:

- Data migration from existing hardcoded search field configs  
- Integration with search services  

 No breaking changes — fully backward compatible.
This commit is contained in:
Abdullah.
2025-09-19 21:35:51 +05:00
committed by GitHub
parent a89cc3b234
commit ff3467c18e
10 changed files with 138 additions and 0 deletions
@@ -7,6 +7,7 @@ import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadat
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { SearchFieldMetadataModule } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
@@ -17,6 +18,7 @@ import { RouteModule } from 'src/engine/metadata-modules/route/route.module';
DataSourceModule,
FieldMetadataModule,
ObjectMetadataModule,
SearchFieldMetadataModule,
ServerlessFunctionModule,
AgentModule,
WorkspaceMetadataVersionModule,
@@ -31,6 +33,7 @@ import { RouteModule } from 'src/engine/metadata-modules/route/route.module';
DataSourceModule,
FieldMetadataModule,
ObjectMetadataModule,
SearchFieldMetadataModule,
ServerlessFunctionModule,
AgentModule,
RemoteServerModule,
@@ -0,0 +1,55 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity('searchFieldMetadata')
@Unique('IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE', [
'objectMetadataId',
'fieldMetadataId',
])
@Index('IDX_SEARCH_FIELD_METADATA_WORKSPACE_ID', ['workspaceId'])
@Index('IDX_SEARCH_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId'])
export class SearchFieldMetadataEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
objectMetadataId: string;
@ManyToOne(() => ObjectMetadataEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'objectMetadataId' })
objectMetadata: Relation<ObjectMetadataEntity>;
@Column({ nullable: false, type: 'uuid' })
fieldMetadataId: string;
@ManyToOne(() => FieldMetadataEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'fieldMetadataId' })
fieldMetadata: Relation<FieldMetadataEntity>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@ManyToOne(() => Workspace, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
@Module({
imports: [TypeOrmModule.forFeature([SearchFieldMetadataEntity])],
providers: [],
exports: [],
})
export class SearchFieldMetadataModule {}