import { context, SQLITE_DESIGN_DOC_ID } from "@budibase/backend-core" import { FieldType, RelationshipFieldMetadata, SQLiteDefinition, SQLiteTable, SQLiteTables, SQLiteType, Table, } from "@budibase/types" import { cloneDeep } from "lodash" import tablesSdk from "../" import { CONSTANT_INTERNAL_ROW_COLS, generateJunctionTableID, } from "../../../../db/utils" type PreSaveSQLiteDefinition = Omit const BASIC_SQLITE_DOC: PreSaveSQLiteDefinition = { _id: SQLITE_DESIGN_DOC_ID, language: "sqlite", sql: { tables: {}, options: { table_name: "tableId", }, }, } const FieldTypeMap: Record = { [FieldType.BOOLEAN]: SQLiteType.NUMERIC, [FieldType.DATETIME]: SQLiteType.TEXT, [FieldType.FORMULA]: SQLiteType.TEXT, [FieldType.LONGFORM]: SQLiteType.TEXT, [FieldType.NUMBER]: SQLiteType.REAL, [FieldType.STRING]: SQLiteType.TEXT, [FieldType.AUTO]: SQLiteType.REAL, [FieldType.OPTIONS]: SQLiteType.TEXT, [FieldType.JSON]: SQLiteType.BLOB, [FieldType.INTERNAL]: SQLiteType.BLOB, [FieldType.BARCODEQR]: SQLiteType.BLOB, [FieldType.ATTACHMENTS]: SQLiteType.BLOB, [FieldType.ATTACHMENT_SINGLE]: SQLiteType.BLOB, [FieldType.ARRAY]: SQLiteType.BLOB, [FieldType.LINK]: SQLiteType.BLOB, [FieldType.BIGINT]: SQLiteType.TEXT, // TODO: consider the difference between multi-user and single user types (subtyping) [FieldType.BB_REFERENCE]: SQLiteType.TEXT, [FieldType.BB_REFERENCE_SINGLE]: SQLiteType.TEXT, } function buildRelationshipDefinitions( table: Table, relationshipColumn: RelationshipFieldMetadata ): { tableId: string definition: SQLiteTable } { const tableId = table._id!, relatedTableId = relationshipColumn.tableId return { tableId: generateJunctionTableID(tableId, relatedTableId), definition: { ["doc1.rowId"]: SQLiteType.TEXT, ["doc1.tableId"]: SQLiteType.TEXT, ["doc1.fieldName"]: SQLiteType.TEXT, ["doc2.rowId"]: SQLiteType.TEXT, ["doc2.tableId"]: SQLiteType.TEXT, ["doc2.fieldName"]: SQLiteType.TEXT, tableId: SQLiteType.TEXT, }, } } // this can generate relationship tables as part of the mapping function mapTable(table: Table): SQLiteTables { const tables: SQLiteTables = {} const fields: Record = {} for (let [key, column] of Object.entries(table.schema)) { // relationships should be handled differently if (column.type === FieldType.LINK) { const { tableId, definition } = buildRelationshipDefinitions( table, column ) tables[tableId] = { fields: definition } } if (!FieldTypeMap[column.type]) { throw new Error(`Unable to map type "${column.type}" to SQLite type`) } fields[key] = FieldTypeMap[column.type] } // there are some extra columns to map - add these in const constantMap: Record = {} CONSTANT_INTERNAL_ROW_COLS.forEach(col => { constantMap[col] = SQLiteType.TEXT }) const thisTable: SQLiteTable = { ...constantMap, ...fields, } tables[table._id!] = { fields: thisTable } return tables } // nothing exists, need to iterate though existing tables async function buildBaseDefinition(): Promise { const tables = await tablesSdk.getAllInternalTables() const definition = cloneDeep(BASIC_SQLITE_DOC) for (let table of tables) { definition.sql.tables = { ...definition.sql.tables, ...mapTable(table), } } return definition } export async function syncDefinition(): Promise { const db = context.getAppDB() const definition = await buildBaseDefinition() await db.put(definition) } export async function addTable(table: Table) { const db = context.getAppDB() let definition: PreSaveSQLiteDefinition | SQLiteDefinition try { definition = await db.get(SQLITE_DESIGN_DOC_ID) } catch (err) { definition = await buildBaseDefinition() } definition.sql.tables = { ...definition.sql.tables, ...mapTable(table), } await db.put(definition) } export async function removeTable(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.sqlDiskCleanup() } } catch (err: any) { if (err?.status === 404) { return } else { throw err } } }