@@ -4,19 +4,36 @@ import { EventEmitter } from 'vscode';
44import { IEncryptedStorage } from '../../common/application/types' ;
55import { IAsyncDisposableRegistry } from '../../common/types' ;
66import { logger } from '../../logging' ;
7- import { LegacyIntegrationConfig , LegacyIntegrationType } from './integrationTypes' ;
87import { IIntegrationStorage } from './types' ;
8+ import { upgradeLegacyIntegrationConfig } from './legacyIntegrationConfigUtils' ;
9+ import {
10+ DatabaseIntegrationConfig ,
11+ DatabaseIntegrationType ,
12+ databaseIntegrationTypes ,
13+ databaseMetadataSchemasByType
14+ } from '@deepnote/database-integrations' ;
915
1016const 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 ( )
1835export 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 }
0 commit comments