Skip to content

Commit d330fd3

Browse files
authored
feat: migrate to the deepnote/database-integrations package (#158)
* refactor: rename IntegrationConfig to LegacyIntegrationConfig * replace notebook scanning for integrations with full project integration list * move duckdb integration inclusion from env var provider to integration storage * fix: scope integrations for env vars by the open project * consolidate duckdb and other integration processing * add `@deepnote/database-integrations` package * fix import restriction violations in integration env var logic * rename IntegrationType to LegacyIntegrationType * add legacy integration config upgrading logic * prefix integration config types with "legacy" to differentiate from new * migrate integration storage to format of deepnote/database-integrations * replace integration env var logic with deepnote/database-integrations * prevent saving and loading of duckdb integrations via integration storage * migrate all components to the new database integration config format * localize "unsupported integration type" message * test: add tests for updated sql env var provider * test: add tests for updated integrationStorage * test: add tests for legacy->new config conversions * fix tests * fix postgres ssl flag migration from legacy config * use node assert in SQL env var tests * update INTEGRATIONS_CREDENTIALS document * upgrade deepnote/database-integrations (fix snowflake URL construction)
1 parent 4e1645d commit d330fd3

27 files changed

+2647
-1721
lines changed

INTEGRATIONS_CREDENTIALS.md

Lines changed: 280 additions & 63 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,6 +2455,7 @@
24552455
"@c4312/evt": "^0.1.1",
24562456
"@deepnote/blocks": "^1.3.5",
24572457
"@deepnote/convert": "^1.2.0",
2458+
"@deepnote/database-integrations": "^1.1.1",
24582459
"@enonic/fnv-plus": "^1.3.0",
24592460
"@jupyter-widgets/base": "^6.0.8",
24602461
"@jupyter-widgets/controls": "^5.0.9",

src/messageTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export type LocalizedMessages = {
235235
integrationsRequiredField: string;
236236
integrationsOptionalField: string;
237237
integrationsUnnamedIntegration: string;
238+
integrationsUnsupportedIntegrationType: string;
238239
// Select input settings strings
239240
selectInputSettingsTitle: string;
240241
allowMultipleValues: string;

src/notebooks/deepnote/integrations/integrationDetector.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@ import { inject, injectable } from 'inversify';
22

33
import { logger } from '../../../platform/logging';
44
import { IDeepnoteNotebookManager } from '../../types';
5-
import {
6-
DATAFRAME_SQL_INTEGRATION_ID,
7-
DEEPNOTE_TO_INTEGRATION_TYPE,
8-
IntegrationStatus,
9-
IntegrationWithStatus,
10-
RawIntegrationType
11-
} from '../../../platform/notebooks/deepnote/integrationTypes';
5+
import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
126
import { IIntegrationDetector, IIntegrationStorage } from './types';
7+
import { DatabaseIntegrationType, databaseIntegrationTypes } from '@deepnote/database-integrations';
138

149
/**
1510
* Service for detecting integrations used in Deepnote notebooks
@@ -40,39 +35,25 @@ export class IntegrationDetector implements IIntegrationDetector {
4035
const integrations = new Map<string, IntegrationWithStatus>();
4136

4237
// Use the project's integrations field as the source of truth
43-
const projectIntegrations = project.project.integrations || [];
38+
const projectIntegrations = project.project.integrations?.slice() ?? [];
4439
logger.debug(`IntegrationDetector: Found ${projectIntegrations.length} integrations in project.integrations`);
4540

4641
for (const projectIntegration of projectIntegrations) {
4742
const integrationId = projectIntegration.id;
48-
49-
// Skip the internal DuckDB integration
50-
if (integrationId === DATAFRAME_SQL_INTEGRATION_ID) {
51-
continue;
52-
}
53-
54-
logger.debug(`IntegrationDetector: Found integration: ${integrationId} (${projectIntegration.type})`);
55-
56-
// Map the Deepnote integration type to our IntegrationType
57-
const integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType];
58-
59-
// Skip unknown integration types
60-
if (!integrationType) {
61-
logger.warn(
62-
`IntegrationDetector: Unknown integration type '${projectIntegration.type}' for integration ID '${integrationId}'. Skipping.`
63-
);
43+
const integrationType = projectIntegration.type;
44+
if (!(databaseIntegrationTypes as readonly string[]).includes(integrationType)) {
45+
logger.debug(`IntegrationDetector: Skipping unsupported integration type: ${integrationType}`);
6446
continue;
6547
}
6648

6749
// Check if the integration is configured
6850
const config = await this.integrationStorage.getIntegrationConfig(integrationId);
69-
7051
const status: IntegrationWithStatus = {
71-
config: config || null,
52+
config: config ?? null,
7253
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected,
7354
// Include integration metadata from project for prefilling when config is null
7455
integrationName: projectIntegration.name,
75-
integrationType: integrationType
56+
integrationType: integrationType as DatabaseIntegrationType
7657
};
7758

7859
integrations.set(integrationId, status);

src/notebooks/deepnote/integrations/integrationManager.ts

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import { inject, injectable } from 'inversify';
2-
import { commands, l10n, NotebookDocument, window, workspace } from 'vscode';
2+
import { commands, l10n, window, workspace } from 'vscode';
33

44
import { IExtensionContext } from '../../../platform/common/types';
55
import { Commands } from '../../../platform/common/constants';
66
import { logger } from '../../../platform/logging';
77
import { IIntegrationDetector, IIntegrationManager, IIntegrationStorage, IIntegrationWebviewProvider } from './types';
8-
import {
9-
DEEPNOTE_TO_INTEGRATION_TYPE,
10-
IntegrationStatus,
11-
IntegrationType,
12-
IntegrationWithStatus,
13-
RawIntegrationType
14-
} from '../../../platform/notebooks/deepnote/integrationTypes';
15-
import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils';
8+
import { IntegrationStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
169
import { IDeepnoteNotebookManager } from '../../types';
10+
import { DatabaseIntegrationType, databaseIntegrationTypes } from '@deepnote/database-integrations';
1711

1812
/**
1913
* Manages integration UI and commands for Deepnote notebooks
@@ -143,14 +137,6 @@ export class IntegrationManager implements IIntegrationManager {
143137

144138
// First try to detect integrations from the stored project
145139
let integrations = await this.integrationDetector.detectIntegrations(projectId);
146-
147-
// If no integrations found in stored project, scan cells directly
148-
// This handles the case where the notebook was already open when the extension loaded
149-
if (integrations.size === 0) {
150-
logger.debug(`IntegrationManager: No integrations found in stored project, scanning cells directly`);
151-
integrations = await this.detectIntegrationsFromCells(activeNotebook);
152-
}
153-
154140
logger.debug(`IntegrationManager: Found ${integrations.size} integrations`);
155141

156142
// If a specific integration was requested (e.g., from status bar click),
@@ -164,20 +150,15 @@ export class IntegrationManager implements IIntegrationManager {
164150
const projectIntegration = project?.project.integrations?.find((i) => i.id === selectedIntegrationId);
165151

166152
let integrationName: string | undefined;
167-
let integrationType: IntegrationType | undefined;
153+
let integrationType: DatabaseIntegrationType | undefined;
168154

169-
if (projectIntegration) {
155+
// Validate that projectIntegration.type against supported types
156+
if (
157+
projectIntegration &&
158+
(databaseIntegrationTypes as readonly string[]).includes(projectIntegration.type)
159+
) {
170160
integrationName = projectIntegration.name;
171-
172-
// Validate that projectIntegration.type exists in the mapping before lookup
173-
if (projectIntegration.type in DEEPNOTE_TO_INTEGRATION_TYPE) {
174-
// Map the Deepnote integration type to our IntegrationType
175-
integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType];
176-
} else {
177-
logger.warn(
178-
`IntegrationManager: Unknown integration type '${projectIntegration.type}' for integration ID '${selectedIntegrationId}' in project '${projectId}'. Integration type will be undefined.`
179-
);
180-
}
161+
integrationType = projectIntegration.type as DatabaseIntegrationType;
181162
}
182163

183164
integrations.set(selectedIntegrationId, {
@@ -196,35 +177,4 @@ export class IntegrationManager implements IIntegrationManager {
196177
// Show the webview with optional selected integration
197178
await this.webviewProvider.show(projectId, integrations, selectedIntegrationId);
198179
}
199-
200-
/**
201-
* Detect integrations by scanning cells directly (fallback method)
202-
* This is used when the project isn't stored in the notebook manager
203-
*/
204-
private async detectIntegrationsFromCells(notebook: NotebookDocument): Promise<Map<string, IntegrationWithStatus>> {
205-
// Collect all cells with SQL integration metadata
206-
const blocksWithIntegrations: BlockWithIntegration[] = [];
207-
208-
for (const cell of notebook.getCells()) {
209-
const metadata = cell.metadata;
210-
logger.trace(`IntegrationManager: Cell ${cell.index} metadata:`, metadata);
211-
212-
// Check cell metadata for sql_integration_id
213-
if (metadata && typeof metadata === 'object') {
214-
const integrationId = (metadata as Record<string, unknown>).sql_integration_id;
215-
if (typeof integrationId === 'string') {
216-
logger.debug(`IntegrationManager: Found integration ${integrationId} in cell ${cell.index}`);
217-
blocksWithIntegrations.push({
218-
id: `cell-${cell.index}`,
219-
sql_integration_id: integrationId
220-
});
221-
}
222-
}
223-
}
224-
225-
logger.debug(`IntegrationManager: Found ${blocksWithIntegrations.length} cells with integrations`);
226-
227-
// Use the shared utility to scan blocks and build the status map
228-
return scanBlocksForIntegrations(blocksWithIntegrations, this.integrationStorage, 'IntegrationManager');
229-
}
230180
}

src/notebooks/deepnote/integrations/integrationUtils.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

src/notebooks/deepnote/integrations/integrationWebview.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@ import { logger } from '../../../platform/logging';
77
import { LocalizedMessages, SharedMessages } from '../../../messageTypes';
88
import { IDeepnoteNotebookManager, ProjectIntegration } from '../../types';
99
import { IIntegrationStorage, IIntegrationWebviewProvider } from './types';
10-
import {
11-
INTEGRATION_TYPE_TO_DEEPNOTE,
12-
IntegrationConfig,
13-
IntegrationStatus,
14-
IntegrationWithStatus,
15-
RawIntegrationType
16-
} from '../../../platform/notebooks/deepnote/integrationTypes';
10+
import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
11+
import { DatabaseIntegrationConfig } from '@deepnote/database-integrations';
1712

1813
/**
1914
* Manages the webview panel for integration configuration
@@ -182,7 +177,8 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
182177
integrationsSnowflakeRolePlaceholder: localize.Integrations.snowflakeRolePlaceholder,
183178
integrationsSnowflakeWarehouseLabel: localize.Integrations.snowflakeWarehouseLabel,
184179
integrationsSnowflakeWarehousePlaceholder: localize.Integrations.snowflakeWarehousePlaceholder,
185-
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}')
180+
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}'),
181+
integrationsUnsupportedIntegrationType: localize.Integrations.unsupportedIntegrationType('{0}')
186182
};
187183

188184
await this.currentPanel.webview.postMessage({
@@ -221,7 +217,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
221217
private async handleMessage(message: {
222218
type: string;
223219
integrationId?: string;
224-
config?: IntegrationConfig;
220+
config?: DatabaseIntegrationConfig;
225221
}): Promise<void> {
226222
switch (message.type) {
227223
case 'configure':
@@ -263,7 +259,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
263259
/**
264260
* Save the configuration for an integration
265261
*/
266-
private async saveConfiguration(integrationId: string, config: IntegrationConfig): Promise<void> {
262+
private async saveConfiguration(integrationId: string, config: DatabaseIntegrationConfig): Promise<void> {
267263
try {
268264
await this.integrationStorage.save(config);
269265

@@ -349,17 +345,16 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
349345
return null;
350346
}
351347

352-
// Map to Deepnote integration type
353-
const deepnoteType: RawIntegrationType | undefined = INTEGRATION_TYPE_TO_DEEPNOTE[type];
354-
if (!deepnoteType) {
355-
logger.warn(`IntegrationWebviewProvider: Cannot map type ${type} for integration ${id}, skipping`);
348+
// Skip DuckDB integration (internal, not a real Deepnote integration)
349+
if (type === 'pandas-dataframe') {
350+
logger.trace(`IntegrationWebviewProvider: Skipping internal DuckDB integration ${id}`);
356351
return null;
357352
}
358353

359354
return {
360355
id,
361356
name: integration.config?.name || integration.integrationName || id,
362-
type: deepnoteType
357+
type
363358
};
364359
})
365360
.filter((integration): integration is ProjectIntegration => integration !== null);

0 commit comments

Comments
 (0)