diff --git a/packages/nocodb-sdk/src/lib/enums.ts b/packages/nocodb-sdk/src/lib/enums.ts index 5cd28fa926..08bc8232fb 100644 --- a/packages/nocodb-sdk/src/lib/enums.ts +++ b/packages/nocodb-sdk/src/lib/enums.ts @@ -553,3 +553,19 @@ export enum ViewSettingOverrideOptions { GROUP = 'group', ROW_COLORING = 'rowColoring', } + +export enum MetaEventType { + COLUMN_ADDED = 'COLUMN_ADDED', + COLUMN_UPDATED = 'COLUMN_UPDATED', + COLUMN_DELETED = 'COLUMN_DELETED', +} + +export enum MetaEntityType { + BASE = 'BASE', + TABLE = 'TABLE', + COLUMN = 'COLUMN', + VIEW = 'VIEW', + FILTER = 'FILTER', + SORT = 'SORT', + VIEW_ROW_COLOR = 'VIEW_ROW_COLOR', +} diff --git a/packages/nocodb-sdk/src/lib/globals.ts b/packages/nocodb-sdk/src/lib/globals.ts index 13bb9fa3d3..59d56a6bba 100644 --- a/packages/nocodb-sdk/src/lib/globals.ts +++ b/packages/nocodb-sdk/src/lib/globals.ts @@ -1,9 +1,5 @@ import { ColumnType, FilterType } from './Api'; -import { - OrgUserRoles, - ProjectRoles, - WorkspaceUserRoles, -} from './enums'; +import { OrgUserRoles, ProjectRoles, WorkspaceUserRoles } from './enums'; import { PlanTitles } from './payment'; export const enumColors = { @@ -239,7 +235,6 @@ export enum NcErrorType { ERR_SUBSCRIPTION_CREATE_FAILED = 'ERR_SUBSCRIPTION_CREATE_FAILED', ERR_STRIPE_WEBHOOK_VERIFICATION_FAILED = 'ERR_STRIPE_WEBHOOK_VERIFICATION_FAILED', ERR_API_CLIENT_NOT_FOUND = 'ERR_API_CLIENT_NOT_FOUND', - } export enum ROW_COLORING_MODE { diff --git a/packages/nocodb-sdk/src/lib/ncTypes.ts b/packages/nocodb-sdk/src/lib/ncTypes.ts index 26c467697c..64c1c92868 100644 --- a/packages/nocodb-sdk/src/lib/ncTypes.ts +++ b/packages/nocodb-sdk/src/lib/ncTypes.ts @@ -16,6 +16,7 @@ export interface NcContext { socket_id?: string; nc_site_url?: string; timezone?: string; + suppressDependencyEvaluation?: boolean; } export interface NcRequest extends Partial { diff --git a/packages/nocodb/src/ee/services/columns.service.ts b/packages/nocodb/src/ee/services/columns.service.ts index 520298b6d8..7393fba6a3 100644 --- a/packages/nocodb/src/ee/services/columns.service.ts +++ b/packages/nocodb/src/ee/services/columns.service.ts @@ -47,6 +47,7 @@ import { getUniqueColumnAliasName } from '~/helpers/getUniqueName'; import ProjectMgrv2 from '~/db/sql-mgr/v2/ProjectMgrv2'; import { ViewRowColorService } from '~/services/view-row-color.service'; import { FiltersService } from '~/services/filters.service'; +import { MetaDependencyEventHandler } from '~/services/meta-dependency/event-handler.service'; @Injectable() export class ColumnsService extends ColumnsServiceCE { @@ -57,6 +58,7 @@ export class ColumnsService extends ColumnsServiceCE { protected readonly formulaColumnTypeChanger, protected readonly viewRowColorService: ViewRowColorService, protected readonly filtersService: FiltersService, + protected readonly metaDependencyEventHandler: MetaDependencyEventHandler, ) { super( metaService, @@ -64,6 +66,7 @@ export class ColumnsService extends ColumnsServiceCE { formulaColumnTypeChanger, formulaColumnTypeChanger, filtersService, + metaDependencyEventHandler, ); } diff --git a/packages/nocodb/src/models/Filter.ts b/packages/nocodb/src/models/Filter.ts index 82bc4b093b..acbfe3eea4 100644 --- a/packages/nocodb/src/models/Filter.ts +++ b/packages/nocodb/src/models/Filter.ts @@ -353,11 +353,13 @@ export default class Filter implements FilterType { id, ); - await NocoCache.update( - context, - `${CacheScope.FILTER_EXP}:${id}`, - updateObj, - ); + ncMeta.knex.attachToTransaction(async () => { + await NocoCache.update( + context, + `${CacheScope.FILTER_EXP}:${id}`, + updateObj, + ); + }); // on update delete any optimised single query cache { diff --git a/packages/nocodb/src/modules/noco.module.ts b/packages/nocodb/src/modules/noco.module.ts index 7cf1e02b3d..0e3f8daa75 100644 --- a/packages/nocodb/src/modules/noco.module.ts +++ b/packages/nocodb/src/modules/noco.module.ts @@ -4,6 +4,7 @@ import { Module } from '@nestjs/common'; import { MulterModule } from '@nestjs/platform-express'; import multer from 'multer'; import { NotFoundHandlerModule } from './not-found-handler.module'; +import { MetaDependencyEventHandler } from '~/services/meta-dependency/event-handler.service'; import { ViewsV3Service } from '~/services/v3/views-v3.service'; import { EventEmitterModule } from '~/modules/event-emitter/event-emitter.module'; import { JobsModule } from '~/modules/jobs/jobs.module'; @@ -161,6 +162,10 @@ import { InternalApiModuleProvider, InternalApiModules, } from '~/controllers/internal/provider'; +import { + MetaDependencyModuleProvider, + MetaDependencyServices, +} from '~/services/meta-dependency/meta-dependency.provider'; export const nocoModuleMetadata = { imports: [ @@ -358,6 +363,11 @@ export const nocoModuleMetadata = { ...InternalApiModules, InternalApiModuleProvider, + + /* Dependency handler */ + MetaDependencyEventHandler, + ...MetaDependencyServices, + MetaDependencyModuleProvider, ], exports: [ /* Generic */ @@ -407,6 +417,8 @@ export const nocoModuleMetadata = { AttachmentUrlUploadHandler, ...InternalApiModules, + MetaDependencyEventHandler, + ...MetaDependencyServices, ], }; diff --git a/packages/nocodb/src/services/columns.service.ts b/packages/nocodb/src/services/columns.service.ts index 53a2e5669e..c025c24808 100644 --- a/packages/nocodb/src/services/columns.service.ts +++ b/packages/nocodb/src/services/columns.service.ts @@ -12,6 +12,7 @@ import { isSystemColumn, isVirtualCol, LongTextAiMetaProp, + MetaEventType, NcApiVersion, NcBaseError, ncIsNull, @@ -103,6 +104,7 @@ import { MetaTable } from '~/utils/globals'; import { parseMetaProp } from '~/utils/modelUtils'; import NocoSocket from '~/socket/NocoSocket'; import { DBErrorExtractor } from '~/helpers/db-error/extractor'; +import { MetaDependencyEventHandler } from '~/services/meta-dependency/event-handler.service'; export type { ReusableParams } from '~/services/columns.service.type'; @@ -253,6 +255,7 @@ export class ColumnsService implements IColumnsService { protected readonly formulaColumnTypeChanger: IFormulaColumnTypeChanger, protected readonly viewRowColorService: ViewRowColorService, protected readonly filtersService: FiltersService, + protected readonly metaDependencyEventHandler: MetaDependencyEventHandler, ) {} async updateFormulas( @@ -2056,6 +2059,15 @@ export class ColumnsService implements IColumnsService { context, columns: table.columns, }); + await this.metaDependencyEventHandler.handleEvent( + context, + { + eventType: MetaEventType.COLUMN_UPDATED, + oldEntity: oldColumn, + newEntity: updatedColumn, + }, + ncMeta, + ); NocoSocket.broadcastEvent( context, diff --git a/packages/nocodb/src/services/meta-dependency/event-handler.service.ts b/packages/nocodb/src/services/meta-dependency/event-handler.service.ts new file mode 100644 index 0000000000..2e0f1deade --- /dev/null +++ b/packages/nocodb/src/services/meta-dependency/event-handler.service.ts @@ -0,0 +1,76 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + META_DEPENDENCY_MODULE_PROVIDER_KEY, + type MetaDependencyEventRequest, + type MetaEventHandler, +} from './types'; +import type { MetaEventType, NcContext } from 'nocodb-sdk'; +import type { MetaService } from '~/meta/meta.service'; +import Noco from '~/Noco'; + +@Injectable() +export class MetaDependencyEventHandler { + constructor( + @Inject(META_DEPENDENCY_MODULE_PROVIDER_KEY) + protected readonly metaEventHandlers: MetaEventHandler[], + ) { + this.registerEvents(metaEventHandlers); + } + + metaEventHandlerMap: Record = { + COLUMN_ADDED: [], + COLUMN_DELETED: [], + COLUMN_UPDATED: [], + }; + + registerEvents(metaEventHandler: MetaEventHandler[]) { + for (const each of metaEventHandler) { + for (const eachType of each.triggerMetaEvents) { + this.metaEventHandlerMap[eachType] = + this.metaEventHandlerMap[eachType] ?? []; + this.metaEventHandlerMap[eachType].push(each); + } + } + } + + async handleEvent( + context: NcContext, + param: MetaDependencyEventRequest, + ncMeta = Noco.ncMeta, + ) { + // if suppressed, do not make further evaluation + if (context.suppressDependencyEvaluation) { + return; + } + // next context will have suppressDependencyEvaluation as true by default unless modules override it. + const nextContext = { + ...context, + suppressDependencyEvaluation: true, + } as NcContext; + let trxNcMeta: MetaService; + try { + for (const handler of this.metaEventHandlerMap[param.eventType] ?? []) { + const affectedDependencies = await handler.getAffectedDependency( + nextContext, + param, + trxNcMeta ?? ncMeta, + ); + if (affectedDependencies) { + trxNcMeta = trxNcMeta ?? (await ncMeta.startTransaction()); + await handler.handle( + nextContext, + { + ...param, + affectedDependencyResult: affectedDependencies, + }, + trxNcMeta, + ); + } + } + await trxNcMeta?.commit(); + } catch (ex) { + await trxNcMeta?.rollback(); + throw ex; + } + } +} diff --git a/packages/nocodb/src/services/meta-dependency/handler/column/column-timezone-update.handler.ts b/packages/nocodb/src/services/meta-dependency/handler/column/column-timezone-update.handler.ts new file mode 100644 index 0000000000..0daf1f44e0 --- /dev/null +++ b/packages/nocodb/src/services/meta-dependency/handler/column/column-timezone-update.handler.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; +import { MetaEventType, parseProp, UITypes } from 'nocodb-sdk'; +import type { NcContext } from 'nocodb-sdk'; +import type { + AffectedDependencyResult, + MetaDependencyEventRequest, + MetaEventHandler, +} from '../../types'; +import { Filter } from '~/models'; +import { MetaTable } from '~/cli'; +import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils'; +import Noco from '~/Noco'; + +/** + * @class ColumnTimezoneUpdateDependencyHandler + * @description Handles updates to column timezones and propagates these changes to dependent filters. + * This class is responsible for identifying when a column's timezone changes (for DateTime, Date, or Formula types) + * and then updating any associated filter expressions that rely on that column's timezone. + */ +@Injectable() +export class ColumnTimezoneUpdateDependencyHandler implements MetaEventHandler { + triggerMetaEvents: MetaEventType[] = [MetaEventType.COLUMN_UPDATED]; + async getAffectedDependency( + context: NcContext, + param: MetaDependencyEventRequest, + ncMeta = Noco.ncMeta, + ): Promise { + let validForProcess = false; + const affectedColumnIds: string[] = []; + const newEntityMeta = parseProp(param.newEntity.meta); + const oldEntityMeta = parseProp(param.oldEntity.meta); + if ( + [UITypes.DateTime, UITypes.Date].includes(param.newEntity.uidt) && + // we leave it as is if the new meta timezone empty / not set + newEntityMeta.timezone && + newEntityMeta.timezone !== oldEntityMeta.timezone + ) { + validForProcess = true; + affectedColumnIds.push(param.newEntity.id); + } else if ( + [UITypes.Formula].includes(param.newEntity.uidt) && + newEntityMeta.display_column_meta?.timezone && + newEntityMeta.display_column_meta?.timezone !== + oldEntityMeta.display_column_meta?.timezone + ) { + validForProcess = true; + affectedColumnIds.push(param.newEntity.id); + } + + if (validForProcess) { + return { + filters: await ncMeta.metaList2( + context.workspace_id, + context.base_id, + MetaTable.FILTER_EXP, + { + xcCondition: (qb) => qb.whereIn('fk_column_id', affectedColumnIds), + }, + ), + }; + } + return undefined; + } + + async handle( + context: NcContext, + param: MetaDependencyEventRequest & { + affectedDependencyResult: AffectedDependencyResult; + }, + ncMeta = Noco.ncMeta, + ): Promise { + if (!param.affectedDependencyResult.filters?.length) { + return; + } + let timezone: string; + if ( + [UITypes.DateTime, UITypes.Date].includes(param.newEntity.uidt) && + parseProp(param.newEntity.meta).timezone + ) { + timezone = parseProp(param.newEntity.meta).timezone; + } else if ( + [UITypes.Formula].includes(param.newEntity.uidt) && + parseProp(param.newEntity.meta).display_column_meta?.timezone + ) { + timezone = parseProp(param.newEntity.meta).display_column_meta?.timezone; + } + for (const each of param.affectedDependencyResult.filters as Filter[]) { + each.meta = parseMetaProp(each); + each.meta.timezone = timezone; + await Filter.update( + { + ...context, + base_id: each.base_id, + workspace_id: each.fk_workspace_id, + }, + each.id, + { ...each, meta: stringifyMetaProp(each) }, + ncMeta, + ); + } + } +} diff --git a/packages/nocodb/src/services/meta-dependency/meta-dependency.provider.ts b/packages/nocodb/src/services/meta-dependency/meta-dependency.provider.ts new file mode 100644 index 0000000000..57bd5f881d --- /dev/null +++ b/packages/nocodb/src/services/meta-dependency/meta-dependency.provider.ts @@ -0,0 +1,13 @@ +import { + META_DEPENDENCY_MODULE_PROVIDER_KEY, + type MetaEventHandler, +} from './types'; +import { ColumnTimezoneUpdateDependencyHandler } from '~/services/meta-dependency/handler/column/column-timezone-update.handler'; + +export const MetaDependencyServices = [ColumnTimezoneUpdateDependencyHandler]; + +export const MetaDependencyModuleProvider = { + provide: META_DEPENDENCY_MODULE_PROVIDER_KEY, + useFactory: (...internalApiModules: MetaEventHandler[]) => internalApiModules, + inject: MetaDependencyServices, +}; diff --git a/packages/nocodb/src/services/meta-dependency/types.ts b/packages/nocodb/src/services/meta-dependency/types.ts new file mode 100644 index 0000000000..4affb37082 --- /dev/null +++ b/packages/nocodb/src/services/meta-dependency/types.ts @@ -0,0 +1,41 @@ +import type { MetaEntityType, MetaEventType, NcContext } from 'nocodb-sdk'; +import type { MetaService } from '~/meta/meta.service'; + +export const META_DEPENDENCY_MODULE_PROVIDER_KEY = 'META_DEPENDENCY'; + +export interface AffectedDependencyResult { + bases?: any[]; + models?: any[]; + filters?: any[]; + columns?: any[]; + views?: any[]; + sorts?: any[]; +} + +export interface MetaDependencyEventRequest { + eventType: MetaEventType; + oldEntity?: any; + newEntity?: any; +} + +export interface MetaEventHandler { + triggerMetaEvents: MetaEventType[]; + getAffectedDependency( + context: NcContext, + param: MetaDependencyEventRequest, + ncMeta?: MetaService, + ): Promise; + handle( + context: NcContext, + param: MetaDependencyEventRequest & { + affectedDependencyResult: AffectedDependencyResult; + }, + ncMeta?: MetaService, + ): Promise; +} + +export interface MetaEvent { + eventType: MetaEventType; + entityType: MetaEntityType; + entity: T; +}