Skip to content

Commit 0407e10

Browse files
committed
migrate integration storage to format of deepnote/database-integrations
1 parent 42da69c commit 0407e10

File tree

2 files changed

+87
-24
lines changed

2 files changed

+87
-24
lines changed

src/platform/notebooks/deepnote/integrationStorage.ts

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,36 @@ import { EventEmitter } from 'vscode';
44
import { IEncryptedStorage } from '../../common/application/types';
55
import { IAsyncDisposableRegistry } from '../../common/types';
66
import { logger } from '../../logging';
7-
import { LegacyIntegrationConfig, LegacyIntegrationType } from './integrationTypes';
87
import { IIntegrationStorage } from './types';
8+
import { upgradeLegacyIntegrationConfig } from './legacyIntegrationConfigUtils';
9+
import {
10+
DatabaseIntegrationConfig,
11+
DatabaseIntegrationType,
12+
databaseIntegrationTypes,
13+
databaseMetadataSchemasByType
14+
} from '@deepnote/database-integrations';
915

1016
const INTEGRATION_SERVICE_NAME = 'deepnote-integrations';
1117

18+
// NOTE: We need a way to upgrade existing configurations to the new format of deepnote/database-integrations.
19+
type VersionedDatabaseIntegrationConfig = DatabaseIntegrationConfig & { version: 1 };
20+
21+
function storeEncryptedIntegrationConfig(
22+
encryptedStorage: IEncryptedStorage,
23+
integrationId: string,
24+
config: VersionedDatabaseIntegrationConfig
25+
): Promise<void> {
26+
return encryptedStorage.store(INTEGRATION_SERVICE_NAME, integrationId, JSON.stringify(config));
27+
}
28+
1229
/**
1330
* Storage service for integration configurations.
1431
* Uses VSCode's SecretStorage API to securely store credentials.
1532
* Storage is scoped to the user's machine and shared across all deepnote projects.
1633
*/
1734
@injectable()
1835
export class IntegrationStorage implements IIntegrationStorage {
19-
private readonly cache: Map<string, LegacyIntegrationConfig> = new Map();
36+
private readonly cache: Map<string, DatabaseIntegrationConfig> = new Map();
2037

2138
private cacheLoaded = false;
2239

@@ -35,15 +52,15 @@ export class IntegrationStorage implements IIntegrationStorage {
3552
/**
3653
* Get all stored integration configurations.
3754
*/
38-
async getAll(): Promise<LegacyIntegrationConfig[]> {
55+
async getAll(): Promise<DatabaseIntegrationConfig[]> {
3956
await this.ensureCacheLoaded();
4057
return Array.from(this.cache.values());
4158
}
4259

4360
/**
4461
* Get a specific integration configuration by ID
4562
*/
46-
async getIntegrationConfig(integrationId: string): Promise<LegacyIntegrationConfig | undefined> {
63+
async getIntegrationConfig(integrationId: string): Promise<DatabaseIntegrationConfig | undefined> {
4764
await this.ensureCacheLoaded();
4865
return this.cache.get(integrationId);
4966
}
@@ -56,28 +73,18 @@ export class IntegrationStorage implements IIntegrationStorage {
5673
async getProjectIntegrationConfig(
5774
_projectId: string,
5875
integrationId: string
59-
): Promise<LegacyIntegrationConfig | undefined> {
76+
): Promise<DatabaseIntegrationConfig | undefined> {
6077
return this.getIntegrationConfig(integrationId);
6178
}
6279

63-
/**
64-
* Get all integrations of a specific type
65-
*/
66-
async getByType(type: LegacyIntegrationType): Promise<LegacyIntegrationConfig[]> {
67-
await this.ensureCacheLoaded();
68-
return Array.from(this.cache.values()).filter((config) => config.type === type);
69-
}
70-
7180
/**
7281
* Save or update an integration configuration
7382
*/
74-
async save(config: LegacyIntegrationConfig): Promise<void> {
83+
async save(config: DatabaseIntegrationConfig): Promise<void> {
7584
await this.ensureCacheLoaded();
7685

7786
// Store the configuration as JSON in encrypted storage
78-
const configJson = JSON.stringify(config);
79-
await this.encryptedStorage.store(INTEGRATION_SERVICE_NAME, config.id, configJson);
80-
87+
await storeEncryptedIntegrationConfig(this.encryptedStorage, config.id, { ...config, version: 1 });
8188
// Update cache
8289
this.cache.set(config.id, config);
8390

@@ -154,19 +161,72 @@ export class IntegrationStorage implements IIntegrationStorage {
154161

155162
try {
156163
const integrationIds: string[] = JSON.parse(indexJson);
164+
const idsToDelete: string[] = [];
157165

158166
// Load each integration configuration
159167
for (const id of integrationIds) {
160168
const configJson = await this.encryptedStorage.retrieve(INTEGRATION_SERVICE_NAME, id);
161169
if (configJson) {
162170
try {
163-
const config: LegacyIntegrationConfig = JSON.parse(configJson);
164-
this.cache.set(id, config);
171+
const parsedData = JSON.parse(configJson);
172+
173+
// Check if this is a legacy config (missing 'version' field)
174+
if (!('version' in parsedData)) {
175+
logger.info(`Upgrading legacy integration config for ${id}`);
176+
177+
// Attempt to upgrade the legacy config
178+
const upgradedConfig = await upgradeLegacyIntegrationConfig(parsedData);
179+
180+
if (upgradedConfig) {
181+
// Successfully upgraded - save the new config
182+
logger.info(`Successfully upgraded integration config for ${id}`);
183+
await storeEncryptedIntegrationConfig(this.encryptedStorage, id, {
184+
...upgradedConfig,
185+
version: 1
186+
});
187+
this.cache.set(id, upgradedConfig);
188+
} else {
189+
// Upgrade failed - mark for deletion
190+
logger.warn(`Failed to upgrade integration ${id}, marking for deletion`);
191+
idsToDelete.push(id);
192+
}
193+
} else {
194+
// Already versioned config - validate against current schema
195+
const { version: _version, ...rawConfig } = parsedData;
196+
const config = databaseIntegrationTypes.includes(rawConfig.type)
197+
? (rawConfig as DatabaseIntegrationConfig)
198+
: null;
199+
const validMetadata = config
200+
? databaseMetadataSchemasByType[config.type].safeParse(config.metadata).data
201+
: null;
202+
if (config && validMetadata) {
203+
this.cache.set(
204+
id,
205+
// NOTE: We must cast here because there is no union-wide schema parser at the moment.
206+
{ ...config, metadata: validMetadata } as DatabaseIntegrationConfig
207+
);
208+
} else {
209+
logger.warn(`Invalid integration config for ${id}, marking for deletion`);
210+
idsToDelete.push(id);
211+
}
212+
}
165213
} catch (error) {
166214
logger.error(`Failed to parse integration config for ${id}:`, error);
215+
// Mark corrupted configs for deletion
216+
idsToDelete.push(id);
167217
}
168218
}
169219
}
220+
221+
// Delete any configs that failed to upgrade or were corrupted
222+
if (idsToDelete.length > 0) {
223+
logger.info(`Deleting ${idsToDelete.length} invalid integration config(s)`);
224+
for (const id of idsToDelete) {
225+
await this.encryptedStorage.store(INTEGRATION_SERVICE_NAME, id, undefined);
226+
}
227+
// Update the index to remove deleted IDs
228+
await this.updateIndex();
229+
}
170230
} catch (error) {
171231
logger.error('Failed to parse integration index:', error);
172232
}

src/platform/notebooks/deepnote/types.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { CancellationToken, Event, NotebookDocument, Uri } from 'vscode';
22
import { IDisposable, Resource } from '../../common/types';
33
import { EnvironmentVariables } from '../../common/variables/types';
4-
import { LegacyIntegrationConfig } from './integrationTypes';
54
import { DeepnoteProject } from '../../deepnote/deepnoteTypes';
5+
import { DatabaseIntegrationConfig } from '@deepnote/database-integrations';
66

77
/**
88
* Settings for select input blocks
@@ -31,7 +31,7 @@ export interface IIntegrationStorage extends IDisposable {
3131
*/
3232
readonly onDidChangeIntegrations: Event<void>;
3333

34-
getAll(): Promise<LegacyIntegrationConfig[]>;
34+
getAll(): Promise<DatabaseIntegrationConfig[]>;
3535

3636
/**
3737
* Retrieves the global (non-project-scoped) integration configuration by integration ID.
@@ -49,14 +49,17 @@ export interface IIntegrationStorage extends IDisposable {
4949
* - The `IntegrationConfig` object if a global configuration exists for the given ID
5050
* - `undefined` if no global configuration exists for the given integration ID
5151
*/
52-
getIntegrationConfig(integrationId: string): Promise<LegacyIntegrationConfig | undefined>;
52+
getIntegrationConfig(integrationId: string): Promise<DatabaseIntegrationConfig | undefined>;
5353

5454
/**
5555
* Get integration configuration for a specific project and integration
5656
*/
57-
getProjectIntegrationConfig(projectId: string, integrationId: string): Promise<LegacyIntegrationConfig | undefined>;
57+
getProjectIntegrationConfig(
58+
projectId: string,
59+
integrationId: string
60+
): Promise<DatabaseIntegrationConfig | undefined>;
5861

59-
save(config: LegacyIntegrationConfig): Promise<void>;
62+
save(config: DatabaseIntegrationConfig): Promise<void>;
6063
delete(integrationId: string): Promise<void>;
6164
exists(integrationId: string): Promise<boolean>;
6265
clear(): Promise<void>;

0 commit comments

Comments
 (0)