diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index d220d0a8ac..e10d0e58c6 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -249,25 +249,53 @@ export class DatabaseImpl implements Database { }) } + async _sqlQuery( + url: string, + method: "POST" | "GET", + body?: any + ): Promise { + const args: { url: string; method: string; cookie: string; body?: any } = { + url: `${this.couchInfo.sqlUrl}/${url}`, + method, + cookie: this.couchInfo.cookie, + } + if (body) { + args.body = body + } + const response = await directCouchUrlCall(body) + if (response.status > 300) { + throw new Error(await response.text()) + } + return (await response.json()) as T + } + async sql( sql: string, parameters?: SqlQueryBinding ): Promise { const dbName = this.name const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}` - const response = await directCouchUrlCall({ - url: `${this.couchInfo.sqlUrl}/${url}`, - method: "POST", - cookie: this.couchInfo.cookie, - body: { - query: sql, - args: parameters, - }, + return await this._sqlQuery(url, "POST", { + query: sql, + args: parameters, }) - if (response.status > 300) { - throw new Error(await response.text()) + } + + // checks design document is accurate (cleans up tables) + async sqlCleanup(): Promise { + const dbName = this.name + const url = `/${dbName}/_cleanup` + return await this._sqlQuery(url, "POST") + } + + // removes a document from sqlite + async sqlPurge(docIds: string[] | string): Promise { + if (!Array.isArray(docIds)) { + docIds = [docIds] } - return (await response.json()) as T[] + const dbName = this.name + const url = `/${dbName}/_purge` + return await this._sqlQuery(url, "POST", { docs: docIds }) } async query( diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 32ba81ebd8..715985766b 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -160,4 +160,18 @@ export class DDInstrumentedDatabase implements Database { return this.db.sql(sql, parameters) }) } + + sqlPurge(docIds: string[] | string): Promise { + return tracer.trace("db.sqlPurge", span => { + span?.addTags({ db_name: this.name }) + return this.db.sqlPurge(docIds) + }) + } + + sqlCleanup(): Promise { + return tracer.trace("db.sqlCleanup", span => { + span?.addTags({ db_name: this.name }) + return this.db.sqlCleanup() + }) + } } diff --git a/packages/server/src/sdk/app/tables/internal/sqs.ts b/packages/server/src/sdk/app/tables/internal/sqs.ts index 0726c94962..51e3f17bca 100644 --- a/packages/server/src/sdk/app/tables/internal/sqs.ts +++ b/packages/server/src/sdk/app/tables/internal/sqs.ts @@ -118,7 +118,7 @@ export async function addTableToSqlite(table: Table) { const db = context.getAppDB() let definition: SQLiteDefinition try { - definition = await db.get(SQLITE_DESIGN_DOC_ID) + definition = await db.get(SQLITE_DESIGN_DOC_ID) } catch (err) { definition = await buildBaseDefinition() } @@ -128,3 +128,22 @@ export async function addTableToSqlite(table: Table) { } await db.put(definition) } + +export async function removeTableFromSqlite(table: Table) { + const db = context.getAppDB() + try { + const definition = await db.get(SQLITE_DESIGN_DOC_ID) + if (definition.sql?.tables?.[table._id!]) { + delete definition.sql.tables[table._id!] + await db.put(definition) + // make sure SQS is cleaned up, tables removed + await db.sqlCleanup() + } + } catch (err: any) { + if (err?.status === 404) { + return + } else { + throw err + } + } +} diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index c723f5f8d6..cd4d656a28 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -148,6 +148,8 @@ export interface Database { sql: string, parameters?: SqlQueryBinding ): Promise + sqlPurge(docIds: string[] | string): Promise + sqlCleanup(): Promise allDocs( params: DatabaseQueryOpts ): Promise>