Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bd39067
feat(server): introducing flat entity superset
prastoin Jan 26, 2026
0c60a54
fixup
prastoin Jan 26, 2026
e028f60
wip
prastoin Jan 27, 2026
04cc808
refacotr(server): field
prastoin Jan 27, 2026
fe4d3a9
chore(server): remove expand
prastoin Jan 27, 2026
a6ed777
clean
prastoin Jan 27, 2026
34a7e07
fix
prastoin Jan 27, 2026
a386595
finally
prastoin Jan 27, 2026
920f234
refactor(server): flat object
prastoin Jan 27, 2026
7325eb6
lint
prastoin Jan 27, 2026
4530bc8
feat(server): role target
prastoin Jan 27, 2026
17f78b4
wip
prastoin Jan 27, 2026
33a2d45
fix
prastoin Jan 27, 2026
cf351b4
feat(server): views
prastoin Jan 27, 2026
b40d1da
fix
prastoin Jan 27, 2026
c63e91e
lint
prastoin Jan 27, 2026
5c59ead
feat(server): more entities
prastoin Jan 27, 2026
5f31a81
feat(server): more enttiies
prastoin Jan 27, 2026
5faf9fb
chore
prastoin Jan 27, 2026
dd524a2
more entities
prastoin Jan 27, 2026
da6fb16
lint
prastoin Jan 27, 2026
0079a1b
feat(server): page layout widget dynamic settings WIP need nested sup…
prastoin Jan 27, 2026
bac1d9d
feat(server): rebase and fix string indexed record type issue
prastoin Jan 28, 2026
0852807
fix(server): navigation menu
prastoin Jan 28, 2026
7457cf1
tsc
prastoin Jan 28, 2026
6c1c95c
feat(server): page layout and page layout tab
prastoin Jan 28, 2026
a2690fd
refactor(server): from entity to flat entity arg type
prastoin Jan 28, 2026
2f932c8
refactor(server): from to type args
prastoin Jan 28, 2026
cb710f9
lint
prastoin Jan 28, 2026
ae96a02
feat(server): webhook and logic-function
prastoin Jan 28, 2026
faa5569
chore
prastoin Jan 28, 2026
2c43489
lint
prastoin Jan 28, 2026
1e126ec
fix
prastoin Jan 28, 2026
250666c
fix module injection
prastoin Jan 28, 2026
2a6ef2d
unit
prastoin Jan 28, 2026
898831a
fix(server): role
prastoin Jan 28, 2026
8a718a5
lint
prastoin Jan 28, 2026
52e31da
fix(server): by definition serialized relation are nullable
prastoin Jan 28, 2026
d06a137
lint
prastoin Jan 28, 2026
9d9cda3
comment
prastoin Jan 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ModelId,
} from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { FormatRecordSerializedRelationProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-record-serialized-relation-properties.type';
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';

@Entity('agent')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
type AllFieldMetadataSettings,
type FieldMetadataSettingsMapping,
FieldMetadataType,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';

export const isFieldMetadataSettingsOfType = <
T extends keyof FieldMetadataSettingsMapping,
>(
settings: AllFieldMetadataSettings | null,
fieldMetadataType: T,
): settings is FieldMetadataSettingsMapping[T] => {
// Settings don't have a discriminator - the type is determined by fieldMetadataType
// For required settings types (RELATION, MORPH_RELATION, FILES), ensure settings is defined
if (
fieldMetadataType === FieldMetadataType.RELATION ||
fieldMetadataType === FieldMetadataType.MORPH_RELATION ||
fieldMetadataType === FieldMetadataType.FILES
) {
return isDefined(settings);
}

return true;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { Repository } from 'typeorm';

import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';

import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgentMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-agent-maps.type';
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';

@Injectable()
Expand All @@ -18,20 +20,35 @@ export class WorkspaceFlatAgentMapCacheService extends WorkspaceCacheProvider<Fl
constructor(
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super();
}

async computeForCache(workspaceId: string): Promise<FlatAgentMaps> {
const agents = await this.agentRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [agents, applications] = await Promise.all([
this.agentRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);

const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);

const flatAgentMaps = createEmptyFlatEntityMaps();

for (const agentEntity of agents) {
const flatAgent = transformAgentEntityToFlatAgent(agentEntity);
const flatAgent = transformAgentEntityToFlatAgent({
agentEntity,
applicationIdToUniversalIdentifierMap,
});

addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatAgent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,67 @@ import { IsNull, Not, Repository } from 'typeorm';

import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';

import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type';
import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { fromRoleTargetEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';

@Injectable()
@WorkspaceCache('flatRoleTargetByAgentIdMaps')
export class WorkspaceFlatRoleTargetByAgentIdService extends WorkspaceCacheProvider<FlatRoleTargetByAgentIdMaps> {
constructor(
@InjectRepository(RoleTargetEntity)
private readonly roleTargetRepository: Repository<RoleTargetEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
) {
super();
}

async computeForCache(
workspaceId: string,
): Promise<FlatRoleTargetByAgentIdMaps> {
const roleTargetEntities = await this.roleTargetRepository.find({
where: {
workspaceId,
agentId: Not(IsNull()),
},
withDeleted: true,
});
const [roleTargetEntities, applications, roles] = await Promise.all([
this.roleTargetRepository.find({
where: {
workspaceId,
agentId: Not(IsNull()),
},
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.roleRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
]);

const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const roleIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(roles);

const flatRoleTargetByAgentIdMaps: FlatRoleTargetByAgentIdMaps = {};

for (const roleTargetEntity of roleTargetEntities as Array<
Omit<RoleTargetEntity, 'agentId'> &
NonNullableRequired<Pick<RoleTargetEntity, 'agentId'>>
>) {
const flatRoleTarget =
fromRoleTargetsEntityToFlatRoleTarget(roleTargetEntity);
const flatRoleTarget = fromRoleTargetEntityToFlatRoleTarget({
roleTargetEntity,
applicationIdToUniversalIdentifierMap,
roleIdToUniversalIdentifierMap,
});

flatRoleTargetByAgentIdMaps[roleTargetEntity.agentId] = flatRoleTarget;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { isDefined } from 'twenty-shared/utils';

import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';

export const transformAgentEntityToFlatAgent = ({
agentEntity,
applicationIdToUniversalIdentifierMap,
}: {
agentEntity: AgentEntity;
applicationIdToUniversalIdentifierMap: Map<string, string>;
}): FlatAgent => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(agentEntity.applicationId);

if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${agentEntity.applicationId} not found for agent ${agentEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}

export const transformAgentEntityToFlatAgent = (
agentEntity: AgentEntity,
): FlatAgent => {
return {
createdAt: agentEntity.createdAt.toISOString(),
deletedAt: agentEntity.deletedAt?.toISOString() ?? null,
Expand All @@ -23,5 +43,11 @@ export const transformAgentEntityToFlatAgent = (
applicationId: agentEntity.applicationId,
modelConfiguration: agentEntity.modelConfiguration,
evaluationInputs: agentEntity.evaluationInputs,
__universal: {
universalIdentifier: agentEntity.universalIdentifier,
applicationUniversalIdentifier,
responseFormat: agentEntity.responseFormat,
modelConfiguration: agentEntity.modelConfiguration,
},
};
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { WorkspaceFlatCommandMenuItemMapCacheService } from 'src/engine/metadata-modules/flat-command-menu-item/services/workspace-flat-command-menu-item-map-cache.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';

@Module({
imports: [
TypeOrmModule.forFeature([CommandMenuItemEntity]),
TypeOrmModule.forFeature([
CommandMenuItemEntity,
ApplicationEntity,
ObjectMetadataEntity,
]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [WorkspaceFlatCommandMenuItemMapCacheService],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { Repository } from 'typeorm';

import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';

import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItemMaps } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item-maps.type';
import { fromCommandMenuItemEntityToFlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-entity-to-flat-command-menu-item.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';

@Injectable()
Expand All @@ -18,21 +21,48 @@ export class WorkspaceFlatCommandMenuItemMapCacheService extends WorkspaceCacheP
constructor(
@InjectRepository(CommandMenuItemEntity)
private readonly commandMenuItemRepository: Repository<CommandMenuItemEntity>,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super();
}

async computeForCache(workspaceId: string): Promise<FlatCommandMenuItemMaps> {
const commandMenuItems = await this.commandMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
});
const [commandMenuItems, applications, objectMetadatas] = await Promise.all(
[
this.commandMenuItemRepository.find({
where: { workspaceId },
withDeleted: true,
}),
this.applicationRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
this.objectMetadataRepository.find({
where: { workspaceId },
select: ['id', 'universalIdentifier'],
withDeleted: true,
}),
],
);

const applicationIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(applications);
const objectMetadataIdToUniversalIdentifierMap =
createIdToUniversalIdentifierMap(objectMetadatas);

const flatCommandMenuItemMaps = createEmptyFlatEntityMaps();

for (const commandMenuItemEntity of commandMenuItems) {
const flatCommandMenuItem =
fromCommandMenuItemEntityToFlatCommandMenuItem(commandMenuItemEntity);
fromCommandMenuItemEntityToFlatCommandMenuItem({
commandMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
});

addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatCommandMenuItem,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
import { isDefined } from 'twenty-shared/utils';

import { type CommandMenuItemEntity } from 'src/engine/metadata-modules/command-menu-item/entities/command-menu-item.entity';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type EntityWithRegroupedOneToManyRelations } from 'src/engine/workspace-cache/types/entity-with-regrouped-one-to-many-relations.type';

type FromCommandMenuItemEntityToFlatCommandMenuItemArgs = {
commandMenuItemEntity: EntityWithRegroupedOneToManyRelations<CommandMenuItemEntity>;
applicationIdToUniversalIdentifierMap: Map<string, string>;
objectMetadataIdToUniversalIdentifierMap: Map<string, string>;
};

export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
commandMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
}: FromCommandMenuItemEntityToFlatCommandMenuItemArgs): FlatCommandMenuItem => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
commandMenuItemEntity.applicationId,
);

if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${commandMenuItemEntity.applicationId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}

let availabilityObjectMetadataUniversalIdentifier: string | null = null;

if (isDefined(commandMenuItemEntity.availabilityObjectMetadataId)) {
availabilityObjectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
commandMenuItemEntity.availabilityObjectMetadataId,
) ?? null;

if (!isDefined(availabilityObjectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${commandMenuItemEntity.availabilityObjectMetadataId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}

export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
commandMenuItemEntity: CommandMenuItemEntity,
): FlatCommandMenuItem => {
return {
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
Expand All @@ -18,5 +61,10 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
applicationId: commandMenuItemEntity.applicationId,
createdAt: commandMenuItemEntity.createdAt.toISOString(),
updatedAt: commandMenuItemEntity.updatedAt.toISOString(),
__universal: {
universalIdentifier: commandMenuItemEntity.universalIdentifier,
applicationUniversalIdentifier,
availabilityObjectMetadataUniversalIdentifier,
},
};
};
Loading
Loading