Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4250890b75 | |||
| 5e6e5aaaf2 | |||
| 0f6c997070 |
+2
-1
@@ -34,7 +34,8 @@
|
||||
"@types/qs": "6.9.16",
|
||||
"@wyw-in-js/transform@npm:0.6.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
||||
"@wyw-in-js/transform@npm:0.7.0": "patch:@wyw-in-js/transform@npm%3A0.7.0#~/.yarn/patches/@wyw-in-js-transform-npm-0.7.0-ba641dc99f.patch",
|
||||
"@opentelemetry/api": "1.9.1"
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"chokidar": "^3.6.0"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"nx": {},
|
||||
|
||||
@@ -76,3 +76,14 @@ export type FunctionExecutionResult = {
|
||||
stackTrace: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ChokidarFsEvent =
|
||||
| 'add'
|
||||
| 'addDir'
|
||||
| 'change'
|
||||
| 'unlink'
|
||||
| 'unlinkDir'
|
||||
| 'ready'
|
||||
| 'raw'
|
||||
| 'error'
|
||||
| 'all';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path, { relative } from 'path';
|
||||
import chokidar, { type FSWatcher } from 'chokidar';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
import { type ChokidarFsEvent } from '@/cli/types';
|
||||
|
||||
export type ManifestWatcherOptions = {
|
||||
appPath: string;
|
||||
@@ -13,7 +13,10 @@ const IGNORED_DIRECTORY_NAMES = new Set(['node_modules', 'dist', '.twenty']);
|
||||
|
||||
export class ManifestWatcher {
|
||||
private appPath: string;
|
||||
private handleChangeDetected: (filePath: string, event: EventName) => void;
|
||||
private handleChangeDetected: (
|
||||
filePath: string,
|
||||
event: ChokidarFsEvent,
|
||||
) => void;
|
||||
private verbose: boolean;
|
||||
private watcher: FSWatcher | null = null;
|
||||
|
||||
|
||||
+5
-2
@@ -10,7 +10,7 @@ import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifes
|
||||
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
|
||||
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import type { Location } from 'esbuild';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
import { type ChokidarFsEvent } from '@/cli/types';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
@@ -102,7 +102,10 @@ export class StartWatchersOrchestratorStep {
|
||||
]);
|
||||
}
|
||||
|
||||
private handleChangeDetected(sourcePath: string, event: EventName): void {
|
||||
private handleChangeDetected(
|
||||
sourcePath: string,
|
||||
event: ChokidarFsEvent,
|
||||
): void {
|
||||
this.state.addEvent({
|
||||
message: `Change detected: ${sourcePath}`,
|
||||
status: 'info',
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
|
||||
import { filterOutContactsThatBelongToSelfOrWorkspaceMembers } from 'src/modules/contact-creation-manager/utils/filter-out-contacts-that-belong-to-self-or-workspace-members.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
const account = (
|
||||
handle: string,
|
||||
handleAliases: string[] = [],
|
||||
): ConnectedAccountEntity =>
|
||||
({ handle, handleAliases }) as ConnectedAccountEntity;
|
||||
|
||||
const member = (userEmail: string): WorkspaceMemberWorkspaceEntity =>
|
||||
({ userEmail }) as unknown as WorkspaceMemberWorkspaceEntity;
|
||||
|
||||
const contact = (handle: string): Contact => ({ handle, displayName: handle });
|
||||
|
||||
describe('filterOutContactsThatBelongToSelfOrWorkspaceMembers', () => {
|
||||
it('drops contacts sharing a company domain with the connected account', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('colleague@acme.com'), contact('lead@prospect.com')],
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['lead@prospect.com']);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when the connected account is on a consumer email provider', () => {
|
||||
// gmail.com is shared by millions of unrelated people, so domain
|
||||
// equality cannot mean "teammate".
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('friend@gmail.com')],
|
||||
account('me@gmail.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when the connected account is on a university domain', () => {
|
||||
// Universities host thousands of unrelated members; treat them like
|
||||
// consumer providers and let collaborators through.
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('professor@mit.edu'), contact('lab-member@mit.edu')],
|
||||
account('researcher@mit.edu'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('drops workspace members regardless of domain', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[
|
||||
contact('teammate@partner.com'),
|
||||
contact('outsider@partner.com'),
|
||||
],
|
||||
account('me@acme.com'),
|
||||
[member('teammate@partner.com')],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['outsider@partner.com']);
|
||||
});
|
||||
|
||||
it('drops the connected account handle and any of its aliases', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[
|
||||
contact('me@acme.com'),
|
||||
contact('me+work@acme.com'),
|
||||
contact('real-contact@partner.com'),
|
||||
],
|
||||
account('me@acme.com', ['me+work@acme.com']),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result.map((c) => c.handle)).toEqual(['real-contact@partner.com']);
|
||||
});
|
||||
|
||||
it('matches handles case-insensitively', () => {
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
[contact('Teammate@Partner.com')],
|
||||
account('me@acme.com'),
|
||||
[member('teammate@partner.com')],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connect
|
||||
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
|
||||
import { getDomainNameFromHandle } from 'src/modules/contact-creation-manager/utils/get-domain-name-from-handle.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { isWorkDomain } from 'src/utils/is-work-email';
|
||||
import { isPersonalOrInstitutionalDomain } from 'src/utils/is-personal-or-institutional-email';
|
||||
|
||||
export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts: Contact[],
|
||||
@@ -41,7 +41,7 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
return contacts.filter(
|
||||
(contact) =>
|
||||
(isDifferentDomain(contact, selfDomainName) ||
|
||||
!isWorkDomain(selfDomainName)) &&
|
||||
isPersonalOrInstitutionalDomain(selfDomainName)) &&
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
!workspaceMembersMap[contact.handle.toLowerCase()] &&
|
||||
!allHandles.includes(contact.handle.toLowerCase()),
|
||||
|
||||
+37
@@ -37,6 +37,43 @@ describe('filterEmails', () => {
|
||||
expect(filteredMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should keep same-domain emails when primary handle is on a university domain', () => {
|
||||
// Universities host thousands of unrelated members across departments
|
||||
// and labs, so domain equality is not a reliable proxy for "internal
|
||||
// teammate". Treat them like consumer providers and skip the same-domain
|
||||
// filter — collaborators sharing the institution domain should be
|
||||
// imported, not silently dropped.
|
||||
const primaryHandle = 'researcher@mit.edu';
|
||||
const messages: MessageWithParticipants[] = [
|
||||
{
|
||||
externalId: 'institution-internal',
|
||||
subject: 'Research collaboration',
|
||||
receivedAt: new Date('2025-01-09T09:54:37.000Z'),
|
||||
text: 'Following up on the paper.',
|
||||
headerMessageId: '<msg@mit.edu>',
|
||||
messageThreadExternalId: 'thread-1',
|
||||
direction: MessageDirection.OUTGOING,
|
||||
participants: [
|
||||
{
|
||||
role: MessageParticipantRole.FROM,
|
||||
handle: 'researcher@mit.edu',
|
||||
displayName: 'Researcher',
|
||||
},
|
||||
{
|
||||
role: MessageParticipantRole.TO,
|
||||
handle: 'professor@mit.edu',
|
||||
displayName: 'Professor',
|
||||
},
|
||||
],
|
||||
attachments: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = filterEmails(primaryHandle, [], messages, []);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
|
||||
it('Should filter messages with participant from the blocklist', () => {
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
|
||||
+4
-4
@@ -7,7 +7,7 @@ import { filterOutIcsAttachments } from 'src/modules/messaging/message-import-ma
|
||||
import { filterOutInternals } from 'src/modules/messaging/message-import-manager/utils/filter-out-internals.util';
|
||||
import { isGroupEmail } from 'src/modules/messaging/message-import-manager/utils/is-group-email';
|
||||
import { isMessageSenderMatchingHandles } from 'src/modules/messaging/message-import-manager/utils/is-message-sender-matching-handles.util';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
import { isPersonalOrInstitutionalEmail } from 'src/utils/is-personal-or-institutional-email';
|
||||
|
||||
export const filterEmails = (
|
||||
primaryHandle: string,
|
||||
@@ -24,9 +24,9 @@ export const filterEmails = (
|
||||
blocklist,
|
||||
);
|
||||
|
||||
const messagesWithoutInternals = isWorkEmail(primaryHandle)
|
||||
? filterOutInternals(primaryHandle, messagesWithoutBlocklisted)
|
||||
: messagesWithoutBlocklisted;
|
||||
const messagesWithoutInternals = isPersonalOrInstitutionalEmail(primaryHandle)
|
||||
? messagesWithoutBlocklisted
|
||||
: filterOutInternals(primaryHandle, messagesWithoutBlocklisted);
|
||||
|
||||
if (!excludeGroupEmails) {
|
||||
return messagesWithoutInternals;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
isPersonalOrInstitutionalDomain,
|
||||
isPersonalOrInstitutionalEmail,
|
||||
} from 'src/utils/is-personal-or-institutional-email';
|
||||
|
||||
describe('isPersonalOrInstitutionalDomain', () => {
|
||||
it('returns true for consumer email providers', () => {
|
||||
expect(isPersonalOrInstitutionalDomain('gmail.com')).toBe(true);
|
||||
expect(isPersonalOrInstitutionalDomain('outlook.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for universities present in the Hipolabs dataset', () => {
|
||||
// Domains where many unrelated members share an address space.
|
||||
expect(isPersonalOrInstitutionalDomain('mit.edu')).toBe(true);
|
||||
expect(isPersonalOrInstitutionalDomain('cam.ac.uk')).toBe(true);
|
||||
expect(isPersonalOrInstitutionalDomain('ethz.ch')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for company-owned domains', () => {
|
||||
expect(isPersonalOrInstitutionalDomain('acme.com')).toBe(false);
|
||||
expect(isPersonalOrInstitutionalDomain('example.io')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPersonalOrInstitutionalEmail', () => {
|
||||
it('extracts the domain and applies the same rules as the domain check', () => {
|
||||
expect(isPersonalOrInstitutionalEmail('user@gmail.com')).toBe(true);
|
||||
expect(isPersonalOrInstitutionalEmail('researcher@mit.edu')).toBe(true);
|
||||
expect(isPersonalOrInstitutionalEmail('user@acme.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false rather than throwing on malformed input', () => {
|
||||
// Callers (filter-emails gate, contact filter) treat false as
|
||||
// "domain looks org-defining" — failing closed is the safe default.
|
||||
expect(isPersonalOrInstitutionalEmail('')).toBe(false);
|
||||
expect(isPersonalOrInstitutionalEmail('not-an-email')).toBe(false);
|
||||
expect(isPersonalOrInstitutionalEmail('missing-domain@')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { emailProvidersSet } from 'src/utils/email-providers';
|
||||
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
|
||||
import { universityDomainsSet } from 'src/utils/university-domains';
|
||||
|
||||
export const isPersonalOrInstitutionalDomain = (domain: string) =>
|
||||
emailProvidersSet.has(domain) || universityDomainsSet.has(domain);
|
||||
|
||||
export const isPersonalOrInstitutionalEmail = (email: string) => {
|
||||
try {
|
||||
return isPersonalOrInstitutionalDomain(getDomainNameByEmail(email));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25928,7 +25928,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.1, anymatch@npm:~3.1.2":
|
||||
"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
|
||||
version: 3.1.3
|
||||
resolution: "anymatch@npm:3.1.3"
|
||||
dependencies:
|
||||
@@ -28507,26 +28507,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:3.5.1":
|
||||
version: 3.5.1
|
||||
resolution: "chokidar@npm:3.5.1"
|
||||
dependencies:
|
||||
anymatch: "npm:~3.1.1"
|
||||
braces: "npm:~3.0.2"
|
||||
fsevents: "npm:~2.3.1"
|
||||
glob-parent: "npm:~5.1.0"
|
||||
is-binary-path: "npm:~2.1.0"
|
||||
is-glob: "npm:~4.0.1"
|
||||
normalize-path: "npm:~3.0.0"
|
||||
readdirp: "npm:~3.5.0"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
checksum: 10c0/894d2fdeeef6a0bc61993a20b864e29e9296f2308628b8b2edf1bef2d59ab11f21938eebbbcbf581f15d16d3e030c08860d2fb035f7b9f3baebac57049a37959
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:3.6.0, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0":
|
||||
"chokidar@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "chokidar@npm:3.6.0"
|
||||
dependencies:
|
||||
@@ -28545,15 +28526,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:4.0.3, chokidar@npm:^4.0.0, chokidar@npm:^4.0.1, chokidar@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "chokidar@npm:4.0.3"
|
||||
dependencies:
|
||||
readdirp: "npm:^4.0.1"
|
||||
checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^1.1.1":
|
||||
version: 1.1.4
|
||||
resolution: "chownr@npm:1.1.4"
|
||||
@@ -34712,7 +34684,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.1, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||
"fsevents@npm:^2.3.2, fsevents@npm:^2.3.3, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@npm:2.3.3"
|
||||
dependencies:
|
||||
@@ -34731,7 +34703,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.1#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
||||
"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||
dependencies:
|
||||
@@ -35141,7 +35113,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0, glob-parent@npm:~5.1.2":
|
||||
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
dependencies:
|
||||
@@ -48895,22 +48867,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readdirp@npm:^4.0.1":
|
||||
version: 4.1.2
|
||||
resolution: "readdirp@npm:4.1.2"
|
||||
checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readdirp@npm:~3.5.0":
|
||||
version: 3.5.0
|
||||
resolution: "readdirp@npm:3.5.0"
|
||||
dependencies:
|
||||
picomatch: "npm:^2.2.1"
|
||||
checksum: 10c0/293de2ed981884a09e76fbf90bddc7e1a87667e57e0284ddc8c177e3151b0d179a9a56441d9f2f3654423924ec100af57ba9e507086527f98fd1d21bdd041c3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readdirp@npm:~3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "readdirp@npm:3.6.0"
|
||||
|
||||
Reference in New Issue
Block a user