UNPKG

@flowfuse/flowfuse

Version:

An open source low-code development platform

332 lines (329 loc) • 15.9 kB
const { generatePassword } = require('../../../../lib/userTeam') const libPg = require('./lib/pg.js') module.exports = { init: async function (app, options) { this._app = app this._options = options || {} if (!options.database) { throw new Error('Postgres LocalFS driver requires database options to be provided') } this._adminClient = libPg.newClient(options.database || {}) this._adminClient.on('error', (err) => { this._app.log.error(`Postgres LocalFS driver error: ${err.toString()}`) }) try { await this._adminClient.connect() } catch (err) { app.log.error(`Failed to connect to Postgres: ${err.toString()}`) } app.log.info('Postgres LocalFS driver initialized') }, shutdown: async function () { try { this._app.log.info('Shutting down Postgres LocalFS driver') await this._adminClient.end() } catch (err) { this._app.log.debug(`Error shutting down Postgres LocalFS driver: ${err.toString()}`) } }, getDatabases: async function (team) { const tables = await this._app.db.models.Table.byTeamId(team.id) if (tables && tables.length > 0) { return tables } else { return [] } }, getDatabase: async function (team, databaseId) { const table = await this._app.db.models.Table.byId(team.id, databaseId) if (table) { return table } else { throw new Error(`Database ${databaseId} for team ${team.hashid} does not exist`) } }, createDatabase: async function (team, name) { const existing = await this._app.db.models.Table.byTeamId(team.id) // Will need removing when we support multiple databases per team if (existing && existing.length > 0) { throw new Error('Database already exists') } const res = await this._adminClient.query('SELECT datname FROM pg_database WHERE datistemplate = false AND datname = $1', [team.hashid]) if (res.rows.length > 0) { throw new Error('Database already exists') } else { const escapedDatabaseName = libPg.pg.escapeIdentifier(team.hashid) await this._adminClient.query(`CREATE DATABASE ${escapedDatabaseName}`) await this._adminClient.query(`REVOKE connect ON DATABASE ${escapedDatabaseName} FROM PUBLIC;`) await this._adminClient.query(`ALTER DATABASE ${escapedDatabaseName} SET statement_timeout='30s';`) const teamClient = libPg.newClient({ ...this._options.database, database: team.hashid }) const password = generatePassword() try { await teamClient.connect() // Escape identifiers for role and database names const escapedRoleName = libPg.pg.escapeIdentifier(`${team.hashid}-role`) const escapedUserName = libPg.pg.escapeIdentifier(team.hashid) // Escape the password literal for direct inclusion in DDL const escapedPassword = libPg.pg.escapeLiteral(password) // Create everything needed for the team await teamClient.query(`CREATE ROLE ${escapedRoleName} WITH LOGIN`) await teamClient.query(`GRANT CONNECT ON DATABASE ${escapedDatabaseName} TO ${escapedRoleName}`) await teamClient.query(`GRANT ALL PRIVILEGES ON DATABASE ${escapedDatabaseName} TO ${escapedRoleName}`) await teamClient.query(`GRANT CREATE ON SCHEMA public TO ${escapedRoleName}`) await teamClient.query(`GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${escapedRoleName}`) await teamClient.query(`GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${escapedRoleName}`) await teamClient.query(`CREATE USER ${escapedUserName} WITH PASSWORD ${escapedPassword}`) await teamClient.query(`GRANT ${escapedRoleName} TO ${escapedUserName}`) } finally { await teamClient.end() } this._app.log.info(`Database created for team ${team.hashid}`) const credentials = { host: this._options.database.host, port: this._options.database.port, ssl: this._options.database.ssl, database: team.hashid, user: team.hashid, password } const table = await this._app.db.models.Table.create({ name, TeamId: team.id, credentials }) return table } }, destroyDatabase: async function (team, databaseId) { const db = await this._app.db.models.Table.byId(team.id, databaseId) if (db) { const res = await this._adminClient.query('SELECT datname FROM pg_database WHERE datistemplate = false AND datname = $1', [team.hashid]) if (res.rows.length === 1) { try { await this._adminClient.query(`DROP DATABASE IF EXISTS ${libPg.pg.escapeIdentifier(team.hashid)}`) await this._adminClient.query(`DROP USER IF EXISTS ${libPg.pg.escapeIdentifier(team.hashid)}`) await this._adminClient.query(`DROP ROLE IF EXISTS ${libPg.pg.escapeIdentifier(team.hashid + '-role')}`) await db.destroy() } catch (err) { // console.log(err) } } else { throw new Error(`Database ${team.hashid} does not exist`) } } else { throw new Error(`Database ${team.hashid} does not exist`) } }, getTables: async function (team, database, paginationOptions) { // SELECT * FROM pg_catalog.pg_tables; const databaseExists = await this._app.db.models.Table.byId(team.id, database) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${database} for team ${team.hashid} does not exist`) } try { const teamClient = libPg.newClient({ ...this._options.database, database: team.hashid }) try { await teamClient.connect() const res = await teamClient.query('SELECT "tablename" FROM "pg_catalog"."pg_tables" WHERE "schemaname" != \'pg_catalog\' AND "schemaname" != \'information_schema\'') if (res.rows && res.rows.length > 0) { const tables = res.rows.map(row => { return { name: row.tablename, schema: row.schemaname } }) return { count: tables.length, tables, meta: {} } } else { return { count: 0, tables: [], meta: {} } } } finally { teamClient.end() } } catch (err) { console.error('Error retrieving tables:', err) throw new Error(`Failed to retrieve tables for team ${team.hashid}: ${err.message}`) } }, getTable: async function (team, database, table) { // SELECT column_name, data_type, is_nullable, column_default // FROM information_schema.columns // WHERE table_name = 'your_table_name'; const databaseExists = await this._app.db.models.Table.byId(team.id, database) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${database} for team ${team.hashid} does not exist`) } try { const teamClient = libPg.newClient({ ...this._options.database, database: team.hashid }) try { await teamClient.connect() const res = await teamClient.query('SELECT column_name, udt_name, is_nullable, column_default, character_maximum_length, is_generated FROM information_schema.columns WHERE table_name = $1', [table]) if (res.rows && res.rows.length > 0) { return res.rows.map(row => { const col = { name: row.column_name, type: row.udt_name, nullable: row.is_nullable === 'YES', default: row.column_default, generated: row.is_generated === 'ALWAYS' } if (row.character_maximum_length) { col.maxLength = row.character_maximum_length } return col }) } else { return null } } finally { teamClient.end() } } catch (err) { console.error('Error retrieving table:', err) throw new Error(`Failed to retrieve table ${table} for team ${team.hashid}: ${err.message}`) } }, getTableData: async function (team, database, table, pagination) { const rows = Math.min(parseInt(pagination.limit) || 10, 10) const databaseExists = await this._app.db.models.Table.byId(team.id, database) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${database} for team ${team.hashid} does not exist`) } try { const teamClient = libPg.newClient({ ...this._options.database, database: team.hashid }) try { await teamClient.connect() const escapedTable = libPg.pg.escapeIdentifier(table) const query = `SELECT * FROM ${escapedTable} LIMIT $1` const res = await teamClient.query(query, [rows || 10]) if (res.rows && res.rows.length > 0) { return { count: res.rows.length, rows: res.rows, meta: {} } } else { return { count: 0, rows: [], meta: {} } } } finally { teamClient.end() } } catch (err) { console.error('Error retrieving table:', err) throw new Error(`Failed to retrieve table ${table} for team ${team.hashid}: ${err.message}`) } }, /** * Query data in tables * IMPORTANT: this method is primarily intended for internal usage (like getting AI schema hints) * @param {Object} team - The team object * @param {String} databaseId - The database ID * @param {String} queryText - The SQL query to execute * @param {Array} params - The parameters for the query * @returns {String} - The result(s) of the query * @throws {Error} - If the database does not exist */ query: async function (team, databaseId, queryText, params = undefined) { const databaseExists = await this._app.db.models.Table.byId(team.id, databaseId) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${databaseId} for team ${team.hashid} does not exist`) } const teamClient = libPg.newClient({ ...this._options.backend, database: team.hashid }) try { await teamClient.connect() const result = await teamClient.query(queryText, params) return result } catch (error) { console.error('Error running query:', error) throw error } finally { await teamClient.end() } }, createTable: async function (team, databaseId, tableName, columns) { const databaseExists = await this._app.db.models.Table.byId(team.id, databaseId) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${databaseId} for team ${team.hashid} does not exist`) } try { const options = databaseExists.credentials const teamClient = libPg.newClient(options) try { await teamClient.connect() let query = `CREATE TABLE IF NOT EXISTS ${libPg.pg.escapeIdentifier(tableName)} (\n` for (const [i, col] of columns.entries()) { if (col.name.length === 0 || col.type.length === 0) { continue } let column = `${libPg.pg.escapeIdentifier(col.name)} ` if (['bigint', 'bigserial', 'boolean', 'date', 'timestamptz', 'real', 'double precision', 'text'].includes(col.type)) { column += `${col.type} ` } else { throw new Error('Unsupported column type') } column += `${col.nullable ? '' : 'NOT NULL'} ` if (col.default) { if (typeof col.default === 'string' && col.type === 'text') { column += `DEFAULT ${libPg.pg.escapeLiteral(col.default)}` } else if (col.type === 'bigint') { column += `DEFAULT ${parseInt(col.default)}` } else if (['real', 'double precision'].includes(col.type)) { column += `DEFAULT ${parseFloat(column.default)}` } else if (col.type === 'boolean') { column += `DEFAULT ${column.default === 'true'}` } else if (col.type === 'timestamptz') { column += 'DEFAULT NOW()' } } if (i + 1 !== columns.length) { query += column + ',\n' } else { query += column + '\n' } } if (query.endsWith(' ,\n')) { query = query.replace(/ ,\n$/, '\n') } query += ')' await teamClient.query(query) } finally { teamClient.end() } } catch (err) { console.error('Error retrieving table:', err) throw new Error(`Failed to create table ${tableName} for team ${team.hashid}: ${err.message}`) } }, dropTable: async function (team, databaseId, tableName) { const databaseExists = await this._app.db.models.Table.byId(team.id, databaseId) if (!databaseExists || databaseExists.TeamId !== team.id) { throw new Error(`Database ${databaseId} for team ${team.hashid} does not exist`) } try { const options = databaseExists.credentials const teamClient = libPg.newClient(options) try { await teamClient.connect() await teamClient.query(`DROP TABLE ${libPg.pg.escapeIdentifier(tableName)}`) } finally { teamClient.end() } } catch (err) { console.error('Error retrieving table:', err) throw new Error(`Failed to create table ${tableName} for team ${team.hashid}: ${err.message}`) } }, createColumn: async function (team, database, table, column) {}, removeColumn: async function (team, database, table, column) {} }