Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -0,0 +1,81 @@
const { IntegrationBase } = require('../../integration-base');

class ConfigCapturingModule {
static definition = {
getName: () => 'config-capturing-module'
};
}

class ConfigCapturingIntegration extends IntegrationBase {
static Definition = {
name: 'config-capturing',
version: '1.0.0',
modules: {
primary: ConfigCapturingModule
},
display: {
label: 'Config Capturing Integration',
description: 'Test double for capturing config state during updates',
detailsUrl: 'https://example.com',
icon: 'test-icon'
}
};

static _capturedOnUpdateState = null;

static resetCaptures() {
this._capturedOnUpdateState = null;
}

static getCapturedOnUpdateState() {
return this._capturedOnUpdateState;
}

constructor(params) {
super(params);
this.integrationRepository = {
updateIntegrationById: jest.fn().mockResolvedValue({}),
findIntegrationById: jest.fn().mockResolvedValue({}),
};
this.updateIntegrationStatus = {
execute: jest.fn().mockResolvedValue({})
};
this.updateIntegrationMessages = {
execute: jest.fn().mockResolvedValue({})
};
}

async initialize() {
this.registerEventHandlers();
}

async onUpdate(params) {
ConfigCapturingIntegration._capturedOnUpdateState = {
thisConfig: JSON.parse(JSON.stringify(this.config)),
paramsConfig: params.config
};

this.config = this._deepMerge(this.config, params.config);
}

_deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
target[key] !== null &&
typeof target[key] === 'object' &&
!Array.isArray(target[key])
) {
result[key] = this._deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
}

module.exports = { ConfigCapturingIntegration };
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class DummyIntegration extends IntegrationBase {
async send(event, data) {
this.sendSpy(event, data);
this.eventCallHistory.push({ event, data, timestamp: Date.now() });
if (event === 'ON_UPDATE') {
await this.onUpdate(data);
}
return { event, data };
}

Expand All @@ -68,7 +71,26 @@ class DummyIntegration extends IntegrationBase {
}

async onUpdate(params) {
return;
this.config = this._deepMerge(this.config, params.config);
}

_deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (
source[key] !== null &&
typeof source[key] === 'object' &&
!Array.isArray(source[key]) &&
target[key] !== null &&
typeof target[key] === 'object' &&
!Array.isArray(target[key])
) {
result[key] = this._deepMerge(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}

async onDelete(params) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { UpdateIntegration } = require('../../use-cases/update-integration');
const { TestIntegrationRepository } = require('../doubles/test-integration-repository');
const { TestModuleFactory } = require('../../../modules/tests/doubles/test-module-factory');
const { DummyIntegration } = require('../doubles/dummy-integration-class');
const { ConfigCapturingIntegration } = require('../doubles/config-capturing-integration');

describe('UpdateIntegration Use-Case', () => {
let integrationRepository;
Expand Down Expand Up @@ -121,7 +122,7 @@ describe('UpdateIntegration Use-Case', () => {
expect(dto.config.bar).toBeUndefined();
});

it('handles deeply nested config updates', async () => {
it('handles deeply nested config updates with merge semantics', async () => {
const record = await integrationRepository.createIntegration(['e1'], 'user-1', { type: 'dummy', nested: { old: 'value' } });

const newConfig = {
Expand All @@ -135,7 +136,73 @@ describe('UpdateIntegration Use-Case', () => {

expect(dto.config.nested.new).toBe('value');
expect(dto.config.nested.deep.level).toBe('test');
expect(dto.config.nested.old).toBeUndefined();
expect(dto.config.nested.old).toBe('value');
});
});

describe('partial config update semantics (issue #514)', () => {
let configCapturingUseCase;

beforeEach(() => {
ConfigCapturingIntegration.resetCaptures();
configCapturingUseCase = new UpdateIntegration({
integrationRepository,
integrationClasses: [ConfigCapturingIntegration],
moduleFactory,
});
});

it('passes existing database config to integration constructor', async () => {
const existingConfig = { type: 'config-capturing', a: 1, b: 2, c: 3 };
const record = await integrationRepository.createIntegration(
['e1'],
'user-1',
existingConfig
);

const partialUpdateConfig = { type: 'config-capturing', a: 10 };
await configCapturingUseCase.execute(record.id, 'user-1', partialUpdateConfig);

const captured = ConfigCapturingIntegration.getCapturedOnUpdateState();
expect(captured.thisConfig).toEqual(existingConfig);
expect(captured.paramsConfig).toEqual(partialUpdateConfig);
});

it('allows onUpdate to merge partial config with existing config', async () => {
const existingConfig = { type: 'config-capturing', a: 1, b: 2, c: 3 };
const record = await integrationRepository.createIntegration(
['e1'],
'user-1',
existingConfig
);

const partialUpdateConfig = { type: 'config-capturing', a: 10 };
const dto = await configCapturingUseCase.execute(record.id, 'user-1', partialUpdateConfig);

expect(dto.config).toEqual({ type: 'config-capturing', a: 10, b: 2, c: 3 });
});

it('preserves nested existing values during partial update', async () => {
const existingConfig = {
type: 'config-capturing',
settings: { theme: 'dark', notifications: true },
credentials: { apiKey: 'secret123' }
};
const record = await integrationRepository.createIntegration(
['e1'],
'user-1',
existingConfig
);

const partialUpdateConfig = {
type: 'config-capturing',
settings: { theme: 'light' }
};
const dto = await configCapturingUseCase.execute(record.id, 'user-1', partialUpdateConfig);

expect(dto.config.settings.theme).toBe('light');
expect(dto.config.settings.notifications).toBe(true);
expect(dto.config.credentials.apiKey).toBe('secret123');
});
});
});
4 changes: 2 additions & 2 deletions packages/core/integrations/use-cases/update-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ class UpdateIntegration {
modules.push(moduleInstance);
}

// 4. Create the Integration domain entity with modules and updated config
// 4. Create the Integration domain entity with modules and existing config
const integrationInstance = new integrationClass({
id: integrationRecord.id,
userId: integrationRecord.userId,
entities: integrationRecord.entitiesIds,
config: config,
config: integrationRecord.config,
status: integrationRecord.status,
version: integrationRecord.version,
messages: integrationRecord.messages,
Expand Down
Loading