Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Timeline #4936

Merged
merged 17 commits into from
Apr 19, 2024
Prev Previous commit
Next Next commit
Improve activity logs
  • Loading branch information
FelixMalfait committed Apr 10, 2024
commit 6939bd27baaa61b0bea9064486a80ff5bd949c82
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const useTimelineActivities = (
orderBy: {
createdAt: 'DescNullsFirst',
},
fetchPolicy: 'network-only',
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
import { isNonEmptyArray } from '@apollo/client/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
Expand Down Expand Up @@ -32,6 +32,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
skip,
depth = 1,
queryFields,
fetchPolicy,
FelixMalfait marked this conversation as resolved.
Show resolved Hide resolved
}: ObjectMetadataItemIdentifier &
ObjectRecordQueryVariables & {
onCompleted?: (
Expand All @@ -44,6 +45,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
skip?: boolean;
depth?: number;
queryFields?: Record<string, any>;
fetchPolicy?: WatchQueryFetchPolicy;
}) => {
const findManyQueryStateIdentifier =
objectNameSingular +
Expand Down Expand Up @@ -83,6 +85,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
limit,
orderBy,
},
fetchPolicy: fetchPolicy,
onCompleted: (data) => {
if (!isDefined(data)) {
onCompleted?.([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class EntityEventsToDbListener {
payload.details.diff = objectRecordChangedValues(
payload.details.before,
payload.details.after,
payload.objectMetadata,
);

return this.handle(payload);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter/utils/object-record-changed-values';

const mockObjectMetadata: ObjectMetadataInterface = {
id: '1',
nameSingular: 'Object',
namePlural: 'Objects',
labelSingular: 'Object',
labelPlural: 'Objects',
description: 'Test object metadata',
targetTableName: 'test_table',
fromRelations: [],
toRelations: [],
fields: [],
isSystem: false,
isCustom: false,
isActive: true,
isRemote: false,
};

describe('objectRecordChangedValues', () => {
it('detects changes in scalar values correctly', () => {
const oldRecord = { id: 1, name: 'Original Name', updatedAt: new Date() };
const newRecord = { id: 1, name: 'Updated Name', updatedAt: new Date() };

const result = objectRecordChangedValues(oldRecord, newRecord);
const result = objectRecordChangedValues(
oldRecord,
newRecord,
mockObjectMetadata,
);

expect(result).toEqual({
name: { before: 'Original Name', after: 'Updated Name' },
});
});
});

it('ignores changes in properties that are objects', () => {
const oldRecord = { id: 1, details: { age: 20 } };
const newRecord = { id: 1, details: { age: 21 } };

const result = objectRecordChangedValues(oldRecord, newRecord);

expect(result).toEqual({});
});

it('ignores changes to the updatedAt field', () => {
const oldRecord = { id: 1, updatedAt: new Date('2020-01-01') };
const newRecord = { id: 1, updatedAt: new Date('2024-01-01') };

const result = objectRecordChangedValues(oldRecord, newRecord);
const result = objectRecordChangedValues(
oldRecord,
newRecord,
mockObjectMetadata,
);

expect(result).toEqual({});
});
Expand All @@ -35,7 +53,11 @@ it('returns an empty object when there are no changes', () => {
const oldRecord = { id: 1, name: 'Name', value: 100 };
const newRecord = { id: 1, name: 'Name', value: 100 };

const result = objectRecordChangedValues(oldRecord, newRecord);
const result = objectRecordChangedValues(
oldRecord,
newRecord,
mockObjectMetadata,
);

expect(result).toEqual({});
});
Expand All @@ -59,7 +81,11 @@ it('correctly handles a mix of changed, unchanged, and special case values', ()
name: { before: 'Original', after: 'Updated' },
};

const result = objectRecordChangedValues(oldRecord, newRecord);
const result = objectRecordChangedValues(
oldRecord,
newRecord,
mockObjectMetadata,
);

expect(result).toEqual(expectedChanges);
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import deepEqual from 'deep-equal';

import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';

import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';

export const objectRecordChangedValues = (
oldRecord: Record<string, any>,
newRecord: Record<string, any>,
objectMetadata: ObjectMetadataInterface,
FelixMalfait marked this conversation as resolved.
Show resolved Hide resolved
) => {
const isObject = (value: any) => {
return typeof value === 'object' && value !== null && !Array.isArray(value);
};

const changedValues = Object.keys(newRecord).reduce(
(acc, key) => {
// Discard if values are objects (e.g. we don't want Company.AccountOwner ; we have AccountOwnerId already)
if (isObject(oldRecord[key]) || isObject(newRecord[key])) {
// return acc;
if (
objectMetadata.fields.find(
(field) =>
field.type === FieldMetadataType.RELATION && field.name === key,
FelixMalfait marked this conversation as resolved.
Show resolved Hide resolved
) ||
(objectMetadata.nameSingular === 'activity' && key === 'body')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo need to hand diff for activity.body (thought I had done it but somehow got lost)

) {
return acc;
}
console.log(oldRecord);
console.log('----');
console.log(newRecord);

if (!deepEqual(oldRecord[key], newRecord[key]) && key != 'updatedAt') {
if (!deepEqual(oldRecord[key], newRecord[key]) && key !== 'updatedAt') {
acc[key] = { before: oldRecord[key], after: newRecord[key] };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export function objectRecordDiffMerge(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great to test this function!

oldRecord: Record<string, any>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised we don't have a record typing in the backend (that would be Record<string, any> & { id: string }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using it a lot in this file, it would be useful

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Weiko do you know if we have ever discussed this?

newRecord: Record<string, any>,
): Record<string, any> {
const result: Record<string, any> = { diff: {} };

// Iterate over the keys in the oldRecord diff
Object.keys(oldRecord.diff).forEach((key) => {
if (newRecord.diff[key]) {
// If the key also exists in the newRecord, merge the 'before' from the oldRecord and the 'after' from the newRecord
result.diff[key] = {
before: oldRecord.diff[key].before,
after: newRecord.diff[key].after,
};
} else {
// If the key does not exist in the newRecord, copy it as is from the oldRecord
result.diff[key] = oldRecord.diff[key];
}
});

// Iterate over the keys in the newRecord diff to catch any that weren't in the oldRecord
Object.keys(newRecord.diff).forEach((key) => {
if (!result.diff[key]) {
// If the key was not already added from the oldRecord, add it from the newRecord
result.diff[key] = newRecord.diff[key];
}
});

return result;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';

import { objectRecordDiffMerge } from 'src/engine/integrations/event-emitter/utils/object-record-diff-merge';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';

@Injectable()
Expand All @@ -10,20 +11,50 @@ export class TimelineActivityRepository {

public async upsert(
name: string,
properties: string,
properties: Record<string, any>,
workspaceMemberId: string | null,
objectName: string,
objectId: string,
workspaceId: string,
): Promise<void> {
if (['activity', 'messageParticipant'].includes(objectName)) {
return await this.handleSecondOrderObjects(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this actually looks more as a service than a repository!

name,
properties,
workspaceMemberId,
objectName,
objectId,
workspaceId,
);
}

return await this.upsertOne(
name,
'object',
properties,
workspaceMemberId,
objectName,
objectId,
workspaceId,
);
}

private async handleSecondOrderObjects(
name: string,
properties: Record<string, any>,
workspaceMemberId: string | null,
objectName: string,
objectId: string,
workspaceId: string,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);

if (objectName === 'activity') {
const activityTargets =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."activityTarget"
WHERE "activityId" = $1`,
WHERE "activityId" = $1`,
[objectId],
workspaceId,
);
Expand All @@ -35,6 +66,7 @@ export class TimelineActivityRepository {
if (columnName === 'activityId' || !columnName.endsWith('Id')) return;
if (columnValue === null) return;
this.upsertOne(
activityTargets[0].id,
name,
properties,
workspaceMemberId,
Expand All @@ -45,11 +77,13 @@ export class TimelineActivityRepository {
},
);
}
// Todo handle message participants
}

private async upsertOne(
name: string,
properties: string,
type: string,
properties: Record<string, any>,
workspaceMemberId: string | null,
objectName: string,
objectId: string,
Expand All @@ -61,17 +95,32 @@ export class TimelineActivityRepository {
const recentActivity =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."timelineActivity"
WHERE "${objectName}Id" = $1 AND "createdAt" >= NOW() - interval '10 minutes'`,
[objectId],
WHERE "${objectName}Id" = $1
AND ("name" = $2 OR "name" = $3)
AND "workspaceMemberId" = $4
AND "createdAt" >= NOW() - interval '10 minutes'`,
[
objectId,
name,
name.replace(/\.updated$/, '.created'),
workspaceMemberId,
],
workspaceId,
);

if (recentActivity.length !== 0) {
// Now we need to merge the .diff

const newProps = objectRecordDiffMerge(
recentActivity[0].properties,
properties,
);

await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."timelineActivity"
SET "properties" = $2, "workspaceMemberId" = $3
WHERE "id" = $1`,
[recentActivity[0].id, properties, workspaceMemberId],
[recentActivity[0].id, newProps, workspaceMemberId],
workspaceId,
);

Expand Down