Compare commits

...

3 Commits

Author SHA1 Message Date
Paul Rastoin 914cf5aafc [REQUIRES_FIELDS_CACHE_FLUSH]Fix field entity to flat field transpiler (#17096)
Following https://github.com/twentyhq/twenty/pull/16981

UniversalIdentifier is now required in the entity so no need to fallback
it
2026-01-12 14:07:15 +01:00
Paul Rastoin a9a8ed7d6e Identify standard field do deploy until IS_WORKSPACE_CREATION_V2_ENABLED is enabled in prod (#16981)
fixes https://github.com/twentyhq/twenty/issues/16905

Do not merge until `IS_WORKSPACE_CREATION_V2_ENABLED` has been activated
by default, and so sync metadata has been deprecated by doing so. As the
sync metadata will attempt to insert `null` `applicationId` and
`universalIdentifier` values while creating a workspace

In this PR we're introducing a new `SyncableEntityRequired` which
enforces the non nullable `applicationId` and `universalIdentifier` on
extending entity

In this PR we also migrate the field metadata entity to extend the
required

This command will search for workspace field metadata entities that
aren't associated to an applicationId, dispatch them to either the
workspace-custom `applicationId` or the twenty-standard `applicationId`.
For the standard entities it will also set their universal identifier
based on the `STANDARD_OBJECTS` const hashmap

As the non nullable `applicationId` and `universalIdentifier`migration
won't pass in the first we've been using the save point and upgrade
command migration fallback pattern

Tested the command on a prod extract locally

Both `twenty-eng` and `twenty-for-twenty` have unexpected standard
objects
Please note that we will deprecate the `isCustom` and `standardId` col
later in the future

```ts
[Nest] 98971  - 01/01/2026, 3:18:00 PM     LOG [IdentifyStandardEntitiesCommand] Successfully validated 600/600 field metadata update(s) for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee (309 custom, 291 standard)
[Nest] 98971  - 01/01/2026, 3:18:00 PM    WARN [IdentifyStandardEntitiesCommand] Found 35 warning(s) while processing field metadata for workspace 9870323e-22c3-4d14-9b7f-5bdc84f7d6ee. These fields will become custom.
```
2026-01-12 11:54:00 +01:00
Lucas Bordeau a31d7d33e5 Made the code more resilient to stale value prop in date filter (#17038)
Fixes : https://github.com/twentyhq/twenty/issues/17035

There was a bad pattern which risked introduce this bug in
`turnRecordFilterIntoRecordGqlOperationFilter` after recent refactor of
date logic and date filters, which didn't create any bug during dev time
because it wasn't tested with value combinations that existed before the
refactor.

Code has been re-organized to make it resilient to any wrong value
combination.

Also fixed a small bug that appeared during QA with `DATE` filter type,
which was blocking the save button from disappearing after a save.
2026-01-12 11:34:38 +01:00
18 changed files with 648 additions and 146 deletions
@@ -1,6 +1,6 @@
import { type ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterOperand } from 'twenty-shared/types';
import { areViewFiltersEqual } from '@/views/utils/areViewFiltersEqual';
import { ViewFilterOperand } from 'twenty-shared/types';
describe('areViewFiltersEqual', () => {
const baseFilter: ViewFilter = {
@@ -21,13 +21,6 @@ describe('areViewFiltersEqual', () => {
expect(areViewFiltersEqual(filterA, filterB)).toBe(true);
});
it('should return false when displayValue is different', () => {
const filterA = { ...baseFilter };
const filterB = { ...baseFilter, displayValue: 'different' };
expect(areViewFiltersEqual(filterA, filterB)).toBe(false);
});
it('should return false when fieldMetadataId is different', () => {
const filterA = { ...baseFilter };
const filterB = { ...baseFilter, fieldMetadataId: 'field-2' };
@@ -10,7 +10,6 @@ export const areViewFiltersEqual = (
'viewFilterGroupId',
'positionInViewFilterGroup',
'value',
'displayValue',
'operand',
'subFieldName',
];
@@ -0,0 +1,240 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { IsNull, Repository } from 'typeorm';
import { v4 } from 'uuid';
import {
RunOnWorkspaceArgs,
WorkspacesMigrationCommandRunner,
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standard-metadata.util';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { STANDARD_OBJECTS } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant';
type CustomFieldMetadata = {
fieldMetadataEntity: FieldMetadataEntity;
fromStandard: boolean;
};
type StandardFieldMetadata = {
fieldMetadataEntity: FieldMetadataEntity;
universalIdentifier: string;
};
type AllWarnings = 'unknown_standard_id';
type FieldMetadataWarning = {
fieldMetadataEntity: FieldMetadataEntity;
warning: AllWarnings;
};
type AllExceptions = 'existing_universal_id_mismatch';
type FieldMetadataException = {
fieldMetadataEntity: FieldMetadataEntity;
exception: AllExceptions;
};
@Command({
name: 'upgrade:1-16:identify-field-metadata',
description: 'Identify standard field metadata',
})
export class IdentifyFieldMetadataCommand extends WorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
protected readonly applicationService: ApplicationService,
protected readonly workspaceCacheService: WorkspaceCacheService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService, [
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
WorkspaceActivationStatus.ONGOING_CREATION,
WorkspaceActivationStatus.PENDING_CREATION,
]);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running identify standard field metadata for workspace ${workspaceId}`,
);
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const allFieldMetadataEntities = await this.fieldMetadataRepository.find({
select: {
id: true,
universalIdentifier: true,
applicationId: true,
name: true,
standardId: true,
object: {
nameSingular: true,
},
isCustom: true,
},
relations: ['object'],
where: {
workspaceId,
applicationId: IsNull(),
},
});
const customFieldMetadataEntities: CustomFieldMetadata[] = [];
const standardFieldMetadataEntities: StandardFieldMetadata[] = [];
const warnings: FieldMetadataWarning[] = [];
const exceptions: FieldMetadataException[] = [];
for (const fieldMetadataEntity of allFieldMetadataEntities) {
const isStandardMetadataResult = isStandardMetadata(fieldMetadataEntity);
if (!isStandardMetadataResult) {
customFieldMetadataEntities.push({
fieldMetadataEntity,
fromStandard: false,
});
continue;
}
const objectConfig =
STANDARD_OBJECTS[
fieldMetadataEntity.object
.nameSingular as keyof typeof STANDARD_OBJECTS
];
const universalIdentifier =
objectConfig?.fields[
fieldMetadataEntity.name as keyof typeof objectConfig.fields
]?.universalIdentifier;
if (!isDefined(universalIdentifier)) {
warnings.push({
fieldMetadataEntity,
warning: 'unknown_standard_id',
});
customFieldMetadataEntities.push({
fieldMetadataEntity,
fromStandard: true,
});
continue;
}
if (
isDefined(fieldMetadataEntity.universalIdentifier) &&
fieldMetadataEntity.universalIdentifier !== universalIdentifier
) {
exceptions.push({
fieldMetadataEntity,
exception: 'existing_universal_id_mismatch',
});
continue;
}
standardFieldMetadataEntities.push({
fieldMetadataEntity,
universalIdentifier:
fieldMetadataEntity.universalIdentifier ?? universalIdentifier,
});
}
const totalUpdates =
customFieldMetadataEntities.length + standardFieldMetadataEntities.length;
if (warnings.length > 0) {
this.logger.warn(
`Found ${warnings.length} warning(s) while processing field metadata for workspace ${workspaceId}. These fields will become custom.`,
);
for (const { fieldMetadataEntity, warning } of warnings) {
this.logger.warn(
`Warning for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${warning}`,
);
}
}
if (exceptions.length > 0) {
this.logger.error(
`Found ${exceptions.length} exception(s) while processing field metadata for workspace ${workspaceId}. No updates will be applied.`,
);
for (const { fieldMetadataEntity, exception } of exceptions) {
this.logger.error(
`Exception for field "${fieldMetadataEntity.name}" on object "${fieldMetadataEntity.object.nameSingular}" (id=${fieldMetadataEntity.id} standardId=${fieldMetadataEntity.standardId}): ${exception}`,
);
}
throw new Error(
`Aborting migration for workspace ${workspaceId} due to ${exceptions.length} exception(s). See logs above for details.`,
);
}
this.logger.log(
`Successfully validated ${totalUpdates}/${allFieldMetadataEntities.length} field metadata update(s) for workspace ${workspaceId} (${customFieldMetadataEntities.length} custom, ${standardFieldMetadataEntities.length} standard)`,
);
if (!options.dryRun) {
const customUpdates = customFieldMetadataEntities.map(
({ fieldMetadataEntity }) => ({
id: fieldMetadataEntity.id,
universalIdentifier: fieldMetadataEntity.universalIdentifier ?? v4(),
applicationId: workspaceCustomFlatApplication.id,
}),
);
const standardUpdates = standardFieldMetadataEntities.map(
({ fieldMetadataEntity, universalIdentifier }) => ({
id: fieldMetadataEntity.id,
universalIdentifier,
applicationId: twentyStandardFlatApplication.id,
}),
);
await this.fieldMetadataRepository.save([
...customUpdates,
...standardUpdates,
]);
const relatedMetadataNames =
getMetadataRelatedMetadataNames('fieldMetadata');
const cacheKeysToInvalidate = relatedMetadataNames.map(
getMetadataFlatEntityMapsKey,
);
this.logger.log(
`Invalidating caches: ${cacheKeysToInvalidate.join(' ')}`,
);
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
);
this.logger.log(
`Applied ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`,
);
} else {
this.logger.log(
`Dry run: would apply ${totalUpdates} field metadata update(s) for workspace ${workspaceId}`,
);
}
}
}
@@ -0,0 +1,71 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@Command({
name: 'upgrade:1-16:make-field-metadata-universal-identifier-and-application-id-not-nullable-migration',
description:
'Make universalIdentifier and applicationId columns NOT NULL on fieldMetadata table',
})
export class MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.warn(
'Skipping has already been run once MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
return;
}
if (options.dryRun) {
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries(
queryRunner,
);
await queryRunner.commitTransaction();
this.logger.log(
'Successfully run MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand',
);
this.hasRunOnce = true;
} catch (error) {
await queryRunner.rollbackTransaction();
this.logger.log(
`Rollbacking MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: ${error.message}`,
);
} finally {
await queryRunner.release();
}
}
}
@@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity, FieldMetadataEntity]),
DataSourceModule,
WorkspaceCacheModule,
FieldMetadataModule,
ApplicationModule,
GlobalWorkspaceDataSourceModule,
TwentyStandardApplicationModule,
],
providers: [
IdentifyFieldMetadataCommand,
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
],
exports: [
IdentifyFieldMetadataCommand,
MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
],
})
export class V1_16_UpgradeVersionCommandModule {}
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
import { V1_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-14/1-14-upgrade-version-command.module';
import { V1_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-15/1-15-upgrade-version-command.module';
import { V1_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -14,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
V1_13_UpgradeVersionCommandModule,
V1_14_UpgradeVersionCommandModule,
V1_15_UpgradeVersionCommandModule,
V1_16_UpgradeVersionCommandModule,
DataSourceModule,
],
providers: [UpgradeCommand],
@@ -22,6 +22,8 @@ import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/u
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-backfill-updated-by-field.command';
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
import { IdentifyFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-identify-field-metadata.command';
import { MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -59,6 +61,10 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly fixNanPositionValuesInNotesCommand: FixNanPositionValuesInNotesCommand,
protected readonly backfillUpdatedByFieldCommand: BackfillUpdatedByFieldCommand,
protected readonly addWorkspaceForeignKeysMigrationCommand: AddWorkspaceForeignKeysMigrationCommand,
// 1.16 Commands
protected readonly identifyFieldMetadataCommand: IdentifyFieldMetadataCommand,
protected readonly makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
) {
super(
workspaceRepository,
@@ -92,6 +98,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.addWorkspaceForeignKeysMigrationCommand,
];
const commands_1160: VersionCommands = [
this.identifyFieldMetadataCommand,
this
.makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
];
this.allCommands = {
'1.12.0': commands_1120,
'1.13.0': commands_1130,
@@ -0,0 +1,64 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
import { makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries } from 'src/database/typeorm/core/migrations/utils/1767277454048-makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable.util';
export class MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048
implements MigrationInterface
{
name =
'MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048';
public async up(queryRunner: QueryRunner): Promise<void> {
const savepointName =
'sp_make_field_metadata_universal_identifier_and_application_id_not_nullable';
try {
await queryRunner.query(`SAVEPOINT ${savepointName}`);
await makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries(
queryRunner,
);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (e) {
try {
await queryRunner.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
await queryRunner.query(`RELEASE SAVEPOINT ${savepointName}`);
} catch (rollbackError) {
// eslint-disable-next-line no-console
console.error(
'Failed to rollback to savepoint in MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048',
rollbackError,
);
throw rollbackError;
}
// eslint-disable-next-line no-console
console.error(
'Swallowing MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullable1767277454048 error',
e,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "FK_05453a954e458e3d91f2ff5043f"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_f1c88fdfc3ad8910b17fc1fd73"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "applicationId" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "universalIdentifier" DROP NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_f1c88fdfc3ad8910b17fc1fd73" ON "core"."fieldMetadata" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_05453a954e458e3d91f2ff5043f" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}
@@ -0,0 +1,23 @@
import { type QueryRunner } from 'typeorm';
export const makeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableQueries =
async (queryRunner: QueryRunner): Promise<void> => {
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" DROP CONSTRAINT "FK_05453a954e458e3d91f2ff5043f"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_f1c88fdfc3ad8910b17fc1fd73"`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "universalIdentifier" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ALTER COLUMN "applicationId" SET NOT NULL`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_f1c88fdfc3ad8910b17fc1fd73" ON "core"."fieldMetadata" ("workspaceId", "universalIdentifier") `,
);
await queryRunner.query(
`ALTER TABLE "core"."fieldMetadata" ADD CONSTRAINT "FK_05453a954e458e3d91f2ff5043f" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
};
@@ -33,13 +33,17 @@ export class ApplicationService {
workspaceId: string;
workspace?: never;
}
| { workspace: WorkspaceEntity; workspaceId?: never }) {
| {
workspace: WorkspaceEntity;
workspaceId?: never;
}) {
const workspace = isDefined(workspaceInput)
? workspaceInput
: await this.workspaceRepository.findOne({
where: {
id: workspaceId,
},
withDeleted: true,
});
if (!isDefined(workspace)) {
@@ -161,6 +161,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
@Field()
updatedAt: Date;
// TODO prastoin make non nullable once MakeFieldMetadataUniversalIdentifierAndApplicationIdNotNullableMigrationCommand has passed in production @Field(() => UUIDScalarType, { nullable: true })
@Field(() => UUIDScalarType, { nullable: true })
applicationId?: string;
}
@@ -30,7 +30,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
import { SyncableEntityRequired } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity-required.interface';
@Entity('fieldMetadata')
@Check(
@@ -56,7 +56,7 @@ import { SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/type
export class FieldMetadataEntity<
TFieldMetadataType extends FieldMetadataType = FieldMetadataType,
>
extends SyncableEntity
extends SyncableEntityRequired
implements Required<FieldMetadataEntity>
{
@PrimaryGeneratedColumn('uuid')
@@ -18,6 +18,10 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
return {
...fieldMetadataWithoutRelations,
// TODO remove once 1-16-make-field-metadata-universal-identifier-and-application-id-not-nullable-migration.command.ts has been run once
universalIdentifier:
fieldMetadataWithoutRelations.universalIdentifier ??
fieldMetadataWithoutRelations.id,
createdAt: fieldMetadataWithoutRelations.createdAt.toISOString(),
updatedAt: fieldMetadataWithoutRelations.updatedAt.toISOString(),
kanbanAggregateOperationViewIds:
@@ -28,8 +32,5 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
[],
viewFieldIds: fieldMetadataEntity.viewFields.map(({ id }) => id),
viewFilterIds: fieldMetadataEntity.viewFilters.map(({ id }) => id),
universalIdentifier:
fieldMetadataWithoutRelations.standardId ??
fieldMetadataWithoutRelations.id,
};
};
@@ -1,4 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
FieldMetadataType,
type NonNullableRequired,
} from 'twenty-shared/types';
import { v4 } from 'uuid';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
@@ -11,7 +14,9 @@ import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-mana
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
workspaceId: string;
flatObjectMetadata: Pick<FlatObjectMetadata, 'id' | 'applicationId'>;
flatObjectMetadata: NonNullableRequired<
Pick<FlatObjectMetadata, 'id' | 'applicationId'>
>;
};
export type DefaultFlatFieldForCustomObjectMaps = ReturnType<
@@ -57,7 +62,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const nameFieldId = v4();
@@ -94,7 +99,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const createdAtFieldId = v4();
@@ -131,7 +136,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const updatedAtFieldId = v4();
@@ -168,7 +173,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const deletedAtFieldId = v4();
@@ -205,7 +210,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const createdByFieldId = v4();
@@ -241,7 +246,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const updatedByFieldId = v4();
@@ -277,7 +282,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const positionFieldId = v4();
@@ -314,7 +319,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
relationTargetObjectMetadataId: null,
settings: null,
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
const searchVectorFieldId = v4();
@@ -354,7 +359,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
generatedType: 'STORED',
},
morphId: null,
applicationId: applicationId ?? null,
applicationId,
};
return {
@@ -161,7 +161,7 @@ describe('WorkspaceEntityManager', () => {
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
morphId: null,
applicationId: null,
applicationId: 'application-id',
};
const flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata> = {
@@ -89,7 +89,7 @@ describe('WorkspaceRepository', () => {
morphId: null,
standardId: null,
standardOverrides: null,
applicationId: null,
applicationId: 'application-id',
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
calendarViewIds: [],
@@ -0,0 +1,22 @@
import { Column, Index, JoinColumn, ManyToOne, type Relation } from 'typeorm';
import type { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/workspace-sync/types/workspace-related-entity';
@Index(['workspaceId', 'universalIdentifier'], {
unique: true,
})
export abstract class SyncableEntityRequired extends WorkspaceRelatedEntity {
@Column({ nullable: false, type: 'uuid' })
universalIdentifier: string;
@Column({ nullable: false, type: 'uuid' })
applicationId: string;
@ManyToOne('ApplicationEntity', {
onDelete: 'CASCADE',
nullable: false,
})
@JoinColumn({ name: 'applicationId' })
application: Relation<ApplicationEntity>;
}
@@ -168,7 +168,10 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
);
}
case 'DATE': {
if (recordFilter.operand === RecordFilterOperand.IS_RELATIVE) {
const itsARelativeDateFilter =
recordFilter.operand === RecordFilterOperand.IS_RELATIVE;
if (itsARelativeDateFilter) {
const relativeDateFilterValue = resolveRelativeDateFilterStringified(
recordFilter.value,
);
@@ -204,62 +207,75 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
}
const nowAsPlainDate = Temporal.Now.plainDateISO(
filterValueDependencies.timeZone,
).toString();
const operandIsTakingNowAsReference =
recordFilter.operand === RecordFilterOperand.IS_TODAY ||
recordFilter.operand === RecordFilterOperand.IS_IN_PAST ||
recordFilter.operand === RecordFilterOperand.IS_IN_FUTURE;
const plainDateFilter = recordFilter.value;
if (operandIsTakingNowAsReference) {
const nowAsPlainDate = Temporal.Now.plainDateISO(
filterValueDependencies.timeZone,
).toString();
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
gte: plainDateFilter,
} as DateFilter,
};
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
lt: plainDateFilter,
} as DateFilter,
};
switch (recordFilter.operand) {
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
lt: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
gte: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_TODAY: {
return {
[correspondingFieldMetadataItem.name]: {
eq: nowAsPlainDate,
} as DateFilter,
};
}
}
} else {
const plainDateFilter = recordFilter.value;
case RecordFilterOperand.IS: {
return {
[correspondingFieldMetadataItem.name]: {
eq: plainDateFilter,
} as DateFilter,
};
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
gte: plainDateFilter,
} as DateFilter,
};
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
lt: plainDateFilter,
} as DateFilter,
};
}
case RecordFilterOperand.IS: {
return {
[correspondingFieldMetadataItem.name]: {
eq: plainDateFilter,
} as DateFilter,
};
}
}
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
lt: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
gte: nowAsPlainDate,
} as DateFilter,
};
case RecordFilterOperand.IS_TODAY: {
return {
[correspondingFieldMetadataItem.name]: {
eq: nowAsPlainDate,
} as DateFilter,
};
}
default:
throw new Error(
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
);
}
throw new Error(
`Unknown operand ${recordFilter.operand} for ${filterType} filter`,
);
}
case 'DATE_TIME': {
if (recordFilter.operand === RecordFilterOperand.IS_RELATIVE) {
const itsARelativeDateTimeFilter =
recordFilter.operand === RecordFilterOperand.IS_RELATIVE;
if (itsARelativeDateTimeFilter) {
const resolvedFilterValue = resolveDateTimeFilter(recordFilter);
const parsedRelativeDateFilterValue =
@@ -307,81 +323,96 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
};
}
if (!isNonEmptyString(recordFilter.value)) {
throw new Error(`Date filter is empty`);
}
const operandIsTakingNowAsReference =
recordFilter.operand === RecordFilterOperand.IS_TODAY ||
recordFilter.operand === RecordFilterOperand.IS_IN_PAST ||
recordFilter.operand === RecordFilterOperand.IS_IN_FUTURE;
const resolvedDateTime = Temporal.Instant.from(recordFilter.value);
if (operandIsTakingNowAsReference) {
const now = Temporal.Now.zonedDateTimeISO(
filterValueDependencies.timeZone,
);
const now = Temporal.Now.zonedDateTimeISO(
filterValueDependencies.timeZone,
);
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
gte: resolvedDateTime.toString(),
} as DateTimeFilter,
};
switch (recordFilter.operand) {
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
lt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
gt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
case RecordFilterOperand.IS_TODAY: {
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
],
};
}
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
lt: resolvedDateTime.toString(),
} as DateTimeFilter,
};
} else {
if (!isNonEmptyString(recordFilter.value)) {
throw new Error(`Date filter is empty`);
}
case RecordFilterOperand.IS: {
const start = resolvedDateTime
.toZonedDateTimeISO('UTC')
.with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
.toInstant();
const end = start.add({ minutes: 1 });
const resolvedDateTime = Temporal.Instant.from(recordFilter.value);
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
lt: end.toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
gte: start.toString(),
} as DateTimeFilter,
},
],
};
}
case RecordFilterOperand.IS_IN_PAST:
return {
[correspondingFieldMetadataItem.name]: {
lt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
case RecordFilterOperand.IS_IN_FUTURE:
return {
[correspondingFieldMetadataItem.name]: {
gt: now.toInstant().round('minute').toString(),
} as DateTimeFilter,
};
case RecordFilterOperand.IS_TODAY: {
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
gte: getPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
lt: getNextPeriodStart(now, 'DAY').toInstant().toString(),
} as DateTimeFilter,
},
],
};
switch (recordFilter.operand) {
case RecordFilterOperand.IS_AFTER: {
return {
[correspondingFieldMetadataItem.name]: {
gte: resolvedDateTime.toString(),
} as DateTimeFilter,
};
}
case RecordFilterOperand.IS_BEFORE: {
return {
[correspondingFieldMetadataItem.name]: {
lt: resolvedDateTime.toString(),
} as DateTimeFilter,
};
}
case RecordFilterOperand.IS: {
const start = resolvedDateTime
.toZonedDateTimeISO('UTC')
.with({
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0,
})
.toInstant();
const end = start.add({ minutes: 1 });
return {
and: [
{
[correspondingFieldMetadataItem.name]: {
lt: end.toString(),
} as DateTimeFilter,
},
{
[correspondingFieldMetadataItem.name]: {
gte: start.toString(),
} as DateTimeFilter,
},
],
};
}
}
}