import { azure, azureResourceGroup, type AzureRuntimeClient, azureSubscriptionId, } from '../azure' import {azureDatabaseSchema} from '../cloud-spi/databaseSchema' import {NotFoundError, RuntimeError, ValidationError} from '../cloud-spi/errors' import type { CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema, SqlConnectionInput, SqlDatabase, SqlQueryResult, SqlTable, } from '../cloud-spi/types' import type {SqlDataClient, SqlDataConnection} from './MssqlDataClient' import {PostgresDataClient} from './PostgresDataClient' import {postgresTlsMode} from './sqlTransport' import { filterBySearch, isValidServerName, numberValue, rawStringValue, recordValue, stringValue, tagList, withApiVersion, } from './sqlAdapterUtils' const API_VERSION = '2025-08-01' const RESOURCE_ID_PREFIX = 'postgres-flexible-server:' const LIST_DATABASES_QUERY = ` SELECT datname AS name, CASE WHEN datallowconn THEN 'ONLINE' ELSE 'OFFLINE' END AS state, NULL::text AS "createdAt", datname IN ('postgres', 'template0', 'template1') AS "isSystem" FROM pg_database WHERE datallowconn ORDER BY "isSystem" DESC, datname` const LIST_TABLES_QUERY = ` SELECT namespace.nspname AS "schemaName", relation.relname AS name, CASE WHEN relation.relkind IN ('v', 'm') THEN 'view' ELSE 'table' END AS "objectType", CASE WHEN relation.relkind IN ('r', 'p') AND relation.reltuples >= 0 THEN relation.reltuples::bigint ELSE NULL END AS "rowCount" FROM pg_class AS relation INNER JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace WHERE namespace.nspname NOT IN ('pg_catalog', 'information_schema') AND namespace.nspname NOT LIKE 'pg_toast%' AND relation.relkind IN ('r', 'p', 'v', 'm') ORDER BY namespace.nspname, relation.relname` interface AzurePostgresListResponse { value?: AzurePostgresRecord[] } interface AzurePostgresRecord { id?: string name?: string type?: string location?: string tags?: Record sku?: Record properties?: Record } export class AzurePostgresAdapter implements CloudServiceAdapter { readonly cloud = 'azure' as const readonly service = 'database' as const constructor( private readonly client: AzureRuntimeClient = azure, private readonly dataClient: SqlDataClient = new PostgresDataClient(), ) {} schema(): ServiceSchema { return azureDatabaseSchema() } async list(query: ResourceQuery = {}): Promise { const body = await this.postgresJson(postgresServersPath(), {method: 'GET'}, true) return filterBySearch((body?.value ?? []).map(toPostgresResource), query.search) } async get(id: string): Promise { const serverName = postgresServerName(id) const body = await this.postgresJson(postgresServerPath(serverName), {method: 'GET'}, true) return body ? toPostgresResource(body) : null } async create(input: CreateResourceInput): Promise { const serverName = stringValue(input.values.serverName) const location = stringValue(input.values.location) || 'eastus' const administratorLogin = stringValue(input.values.administratorLogin) const administratorLoginPassword = stringValue(input.values.administratorLoginPassword) if (!serverName) throw new ValidationError('serverName is required') if (!isValidServerName(serverName)) throw new ValidationError('Use a valid PostgreSQL flexible server name.') if (!administratorLogin) throw new ValidationError('administratorLogin is required') if (!administratorLoginPassword) throw new ValidationError('administratorLoginPassword is required') const body = await this.postgresJson(postgresServerPath(serverName), { method: 'PUT', body: JSON.stringify({ location, sku: {name: 'Standard_B1ms', tier: 'Burstable'}, properties: { administratorLogin, administratorLoginPassword, version: '17', storage: {storageSizeGB: 32}, }, }), }) if (!body) throw new RuntimeError('PostgreSQL flexible server creation returned an empty response') return toPostgresResource(body) } async delete(id: string): Promise { await this.postgresFetch(postgresServerPath(postgresServerName(id)), {method: 'DELETE'}) } async listSqlDatabases(serverId: string, connection: SqlConnectionInput): Promise { const result = await this.querySql(serverId, {...connection, database: 'postgres'}, LIST_DATABASES_QUERY) return (result.resultSets[0]?.rows ?? []).flatMap((row) => { const name = stringValue(row.name) if (!name) return [] return [{ name, state: stringValue(row.state) || 'UNKNOWN', createdAt: stringValue(row.createdAt) || null, isSystem: row.isSystem === true, }] }) } async listSqlTables(serverId: string, connection: SqlConnectionInput): Promise { const result = await this.querySql(serverId, connection, LIST_TABLES_QUERY) return (result.resultSets[0]?.rows ?? []).flatMap((row) => { const schema = stringValue(row.schemaName) const name = stringValue(row.name) if (!schema || !name) return [] return [{ schema, name, type: row.objectType === 'view' ? 'view' as const : 'table' as const, rowCount: integerValue(row.rowCount), }] }) } async querySql(serverId: string, connection: SqlConnectionInput, query: string): Promise { if (!query.trim()) throw new ValidationError('SQL query is required') return this.dataClient.query(await this.dataConnection(serverId, connection), query) } private postgresFetch(path: string, init: RequestInit, emptyOnNotFound = false): Promise { return this.client.fetch(withApiVersion(path, API_VERSION), { ...init, headers: { 'accept': 'application/json', 'content-type': 'application/json', ...(init.headers ?? {}), }, }, {emptyOnNotFound}) } private async postgresJson(path: string, init: RequestInit, emptyOnNotFound = false): Promise { const response = await this.postgresFetch(path, init, emptyOnNotFound) if (!response || response.status === 204) return null return await response.json() as T } private async dataConnection(serverId: string, input: SqlConnectionInput): Promise { const resource = await this.get(serverId) if (!resource) throw new NotFoundError('PostgreSQL flexible server not found') const endpoint = recordValue(resource.metadata.endpoint) const server = stringValue(endpoint?.address) const port = numberValue(endpoint?.port) const username = rawStringValue(input.username) const password = rawStringValue(input.password) const database = stringValue(input.database) || 'postgres' const hasLocalPort = endpoint?.port !== undefined && endpoint.port !== null // The endpoint comes from the resource the runtime returned, so a missing // address is a runtime problem rather than something the caller sent. if (!server || port === null) throw new RuntimeError('PostgreSQL server endpoint is not available') if (!username) throw new ValidationError('PostgreSQL username is required') if (!password) throw new ValidationError('PostgreSQL password is required') return { server, port, database, username, password, tlsMode: postgresTlsMode(server, hasLocalPort), } } } export function isPostgresServerResourceId(id: string): boolean { return id.startsWith(RESOURCE_ID_PREFIX) } function postgresServersPath(): string { return `/subscriptions/${encodeURIComponent(azureSubscriptionId())}/resourceGroups/${encodeURIComponent(azureResourceGroup())}/providers/Microsoft.DBforPostgreSQL/flexibleServers` } function postgresServerPath(serverName: string): string { return `${postgresServersPath()}/${encodeURIComponent(serverName)}` } function postgresServerName(id: string): string { const name = isPostgresServerResourceId(id) ? id.slice(RESOURCE_ID_PREFIX.length) : id if (!name) throw new ValidationError('PostgreSQL flexible server id is required') return name } function toPostgresResource(server: AzurePostgresRecord): CloudResource { const name = stringValue(server.name) const properties = server.properties ?? {} const sku = recordValue(server.sku) const network = recordValue(properties.network) const storage = recordValue(properties.storage) return { id: `${RESOURCE_ID_PREFIX}${name}`, name, cloud: 'azure', service: 'database', type: 'postgres-flexible-server', region: stringValue(server.location) || null, createdAt: null, status: stringValue(properties.state) || stringValue(properties.provisioningState) || null, engine: 'postgresql', version: stringValue(properties.version) || null, instanceClass: stringValue(sku?.name) || null, metadata: { provider: 'azure', databaseService: 'postgresql', resourceKind: 'flexible-server', armId: server.id, serverName: name, administratorLogin: properties.administratorLogin, fullyQualifiedDomainName: properties.fullyQualifiedDomainName, publicNetworkAccess: network?.publicNetworkAccess, storageSizeGB: storage?.storageSizeGB, endpoint: { address: properties.fullyQualifiedDomainName, port: numberValue(properties.localPort), }, tags: tagList(server.tags), }, } } function integerValue(value: unknown): number | null { if (typeof value === 'number' && Number.isSafeInteger(value)) return value if (typeof value !== 'string' || !/^-?\d+$/.test(value)) return null const parsed = Number(value) return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null }