Azure PostgreSQL pour TypeScript (node-postgres)
Connectez-vous à Azure Database for PostgreSQL Flexible Server en utilisant le package pg (node-postgres) avec support pour l'authentification par mot de passe et Microsoft Entra ID (sans mot de passe).
Installation
npm install pg @azure/identity
npm install -D @types/pg
Variables d'environnement
# Requis
AZURE_POSTGRESQL_HOST=<server>.postgres.database.azure.com
AZURE_POSTGRESQL_DATABASE=<database>
AZURE_POSTGRESQL_PORT=5432
# Pour l'authentification par mot de passe
AZURE_POSTGRESQL_USER=<username>
AZURE_POSTGRESQL_PASSWORD=<password>
# Pour l'authentification Entra ID
AZURE_POSTGRESQL_USER=<entra-user>@<server> # ex. user@contoso.com
AZURE_POSTGRESQL_CLIENTID=<managed-identity-client-id> # Pour l'identité managée assignée par l'utilisateur
AZURE_TOKEN_CREDENTIALS=prod # Requis uniquement si DefaultAzureCredential est utilisé en production
Authentification
Option 1 : Authentification par mot de passe
import { Client, Pool } from "pg";
const client = new Client({
host: process.env.AZURE_POSTGRESQL_HOST,
database: process.env.AZURE_POSTGRESQL_DATABASE,
user: process.env.AZURE_POSTGRESQL_USER,
password: process.env.AZURE_POSTGRESQL_PASSWORD,
port: Number(process.env.AZURE_POSTGRESQL_PORT) || 5432,
ssl: { rejectUnauthorized: true } // Requis pour Azure
});
await client.connect();
Option 2 : Microsoft Entra ID (Sans mot de passe) - Recommandé
import { Client, Pool } from "pg";
import { DefaultAzureCredential, ManagedIdentityCredential } from "@azure/identity";
// Dev local : DefaultAzureCredential. Production : définissez AZURE_TOKEN_CREDENTIALS=prod ou AZURE_TOKEN_CREDENTIALS=<specific_credential>
const credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
// Ou utilisez une credential spécifique directement en production :
// Voir https://learn.microsoft.com/javascript/api/overview/azure/identity-readme?view=azure-node-latest#credential-classes
// const credential = new ManagedIdentityCredential();
// Pour l'identité managée assignée par l'utilisateur
// const credential = new DefaultAzureCredential({
// managedIdentityClientId: process.env.AZURE_POSTGRESQL_CLIENTID
// });
// Acquérir un token d'accès pour Azure PostgreSQL
const tokenResponse = await credential.getToken(
"https://ossrdbms-aad.database.windows.net/.default"
);
const client = new Client({
host: process.env.AZURE_POSTGRESQL_HOST,
database: process.env.AZURE_POSTGRESQL_DATABASE,
user: process.env.AZURE_POSTGRESQL_USER, // Utilisateur Entra ID
password: tokenResponse.token, // Token comme mot de passe
port: Number(process.env.AZURE_POSTGRESQL_PORT) || 5432,
ssl: { rejectUnauthorized: true }
});
await client.connect();
Flux de travail principaux
1. Connexion client unique
import { Client } from "pg";
const client = new Client({
host: process.env.AZURE_POSTGRESQL_HOST,
database: process.env.AZURE_POSTGRESQL_DATABASE,
user: process.env.AZURE_POSTGRESQL_USER,
password: process.env.AZURE_POSTGRESQL_PASSWORD,
port: 5432,
ssl: { rejectUnauthorized: true }
});
try {
await client.connect();
const result = await client.query("SELECT NOW() as current_time");
console.log(result.rows[0].current_time);
} finally {
await client.end(); // Toujours fermer la connexion
}
2. Pool de connexions (Recommandé pour la production)
import { Pool } from "pg";
const pool = new Pool({
host: process.env.AZURE_POSTGRESQL_HOST,
database: process.env.AZURE_POSTGRESQL_DATABASE,
user: process.env.AZURE_POSTGRESQL_USER,
password: process.env.AZURE_POSTGRESQL_PASSWORD,
port: 5432,
ssl: { rejectUnauthorized: true },
// Configuration du pool
max: 20, // Connexions maximales dans le pool
idleTimeoutMillis: 30000, // Fermer les connexions inactives après 30s
connectionTimeoutMillis: 10000 // Délai d'expiration pour les nouvelles connexions
});
// Requête utilisant le pool (acquiert et libère automatiquement la connexion)
const result = await pool.query("SELECT * FROM users WHERE id = $1", [userId]);
// Extraction explicite pour plusieurs requêtes
const client = await pool.connect();
try {
const res1 = await client.query("SELECT * FROM users");
const res2 = await client.query("SELECT * FROM orders");
} finally {
client.release(); // Retourner la connexion au pool
}
// Nettoyage à l'arrêt
await pool.end();
3. Requêtes paramétrées (Prévenir l'injection SQL)
// TOUJOURS utiliser des requêtes paramétrées - ne JAMAIS concaténer l'entrée utilisateur
const userId = 123;
const email = "user@example.com";
// Paramètre unique
const result = await pool.query(
"SELECT * FROM users WHERE id = $1",
[userId]
);
// Plusieurs paramètres
const result = await pool.query(
"INSERT INTO users (email, name, created_at) VALUES ($1, $2, NOW()) RETURNING *",
[email, "John Doe"]
);
// Paramètre tableau
const ids = [1, 2, 3, 4, 5];
const result = await pool.query(
"SELECT * FROM users WHERE id = ANY($1::int[])",
[ids]
);
4. Transactions
const client = await pool.connect();
try {
await client.query("BEGIN");
const userResult = await client.query(
"INSERT INTO users (email) VALUES ($1) RETURNING id",
["user@example.com"]
);
const userId = userResult.rows[0].id;
await client.query(
"INSERT INTO orders (user_id, total) VALUES ($1, $2)",
[userId, 99.99]
);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
5. Fonction d'aide pour les transactions
async function withTransaction<T>(
pool: Pool,
fn: (client: PoolClient) => Promise<T>
): Promise<T> {
const client = await pool.connect();
try {
await client.query("BEGIN");
const result = await fn(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
// Utilisation
const order = await withTransaction(pool, async (client) => {
const user = await client.query(
"INSERT INTO users (email) VALUES ($1) RETURNING *",
["user@example.com"]
);
const order = await client.query(
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING *",
[user.rows[0].id, 99.99]
);
return order.rows[0];
});
6. Requêtes typées avec TypeScript
import { Pool, QueryResult } from "pg";
interface User {
id: number;
email: string;
name: string;
created_at: Date;
}
// Typer le résultat de la requête
const result: QueryResult<User> = await pool.query<User>(
"SELECT * FROM users WHERE id = $1",
[userId]
);
const user: User | undefined = result.rows[0];
// Insertion sûre pour le type
async function createUser(
pool: Pool,
email: string,
name: string
): Promise<User> {
const result = await pool.query<User>(
"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
[email, name]
);
return result.rows[0];
}
Pool avec actualisation du token Entra ID
Pour les applications longue durée, les tokens expirent et doivent être actualisés :
import { Pool, PoolConfig } from "pg";
import { DefaultAzureCredential, AccessToken } from "@azure/identity";
class AzurePostgresPool {
private pool: Pool | null = null;
private credential: DefaultAzureCredential;
private tokenExpiry: Date | null = null;
private config: Omit<PoolConfig, "password">;
constructor(config: Omit<PoolConfig, "password">) {
this.credential = new DefaultAzureCredential({requiredEnvVars: ["AZURE_TOKEN_CREDENTIALS"]});
this.config = config;
}
private async getToken(): Promise<string> {
const tokenResponse = await this.credential.getToken(
"https://ossrdbms-aad.database.windows.net/.default"
);
this.tokenExpiry = new Date(tokenResponse.expiresOnTimestamp);
return tokenResponse.token;
}
private isTokenExpired(): boolean {
if (!this.tokenExpiry) return true;
// Actualiser 5 minutes avant l'expiration
return new Date() >= new Date(this.tokenExpiry.getTime() - 5 * 60 * 1000);
}
async getPool(): Promise<Pool> {
if (this.pool && !this.isTokenExpired()) {
return this.pool;
}
// Fermer le pool existant si le token a expiré
if (this.pool) {
await this.pool.end();
}
const token = await this.getToken();
this.pool = new Pool({
...this.config,
password: token
});
return this.pool;
}
async query<T>(text: string, params?: any[]): Promise<QueryResult<T>> {
const pool = await this.getPool();
return pool.query<T>(text, params);
}
async end(): Promise<void> {
if (this.pool) {
await this.pool.end();
this.pool = null;
}
}
}
// Utilisation
const azurePool = new AzurePostgresPool({
host: process.env.AZURE_POSTGRESQL_HOST!,
database: process.env.AZURE_POSTGRESQL_DATABASE!,
user: process.env.AZURE_POSTGRESQL_USER!,
port: 5432,
ssl: { rejectUnauthorized: true },
max: 20
});
const result = await azurePool.query("SELECT NOW()");
Gestion des erreurs
import { DatabaseError } from "pg";
try {
await pool.query("INSERT INTO users (email) VALUES ($1)", [email]);
} catch (error) {
if (error instanceof DatabaseError) {
switch (error.code) {
case "23505": // unique_violation
console.error("Entrée dupliquée :", error.detail);
break;
case "23503": // foreign_key_violation
console.error("Contrainte de clé étrangère échouée :", error.detail);
break;
case "42P01": // undefined_table
console.error("Table n'existe pas :", error.message);
break;
case "28P01": // invalid_password
console.error("Authentification échouée");
break;
case "57P03": // cannot_connect_now (server starting)
console.error("Serveur indisponible, réessayez plus tard");
break;
default:
console.error(`Erreur PostgreSQL ${error.code} : ${error.message}`);
}
}
throw error;
}
Format de chaîne de connexion
// Alternative : Utiliser une chaîne de connexion
const pool = new Pool({
connectionString: `postgres://${user}:${password}@${host}:${port}/${database}?sslmode=require`
});
// Avec SSL requis (Azure)
const connectionString =
`postgres://user:password@server.postgres.database.azure.com:5432/mydb?sslmode=require`;
Événements du pool
const pool = new Pool({ /* config */ });
pool.on("connect", (client) => {
console.log("Nouveau client connecté au pool");
});
pool.on("acquire", (client) => {
console.log("Client extrait du pool");
});
pool.on("release", (err, client) => {
console.log("Client retourné au pool");
});
pool.on("remove", (client) => {
console.log("Client supprimé du pool");
});
pool.on("error", (err, client) => {
console.error("Erreur inattendue du pool :", err);
});
Configuration spécifique à Azure
| Paramètre | Valeur | Description |
|---|---|---|
ssl.rejectUnauthorized |
true |
Toujours utiliser SSL pour Azure |
| Port par défaut | 5432 |
Port PostgreSQL standard |
| Port PgBouncer | 6432 |
Utiliser quand PgBouncer est activé |
| Étendue du token | https://ossrdbms-aad.database.windows.net/.default |
Étendue du token Entra ID |
| Durée de vie du token | ~1 heure | Actualiser avant l'expiration |
Directives de dimensionnement du pool
| Charge de travail | max |
idleTimeoutMillis |
|---|---|---|
| Légère (dev/test) | 5-10 | 30000 |
| Moyenne (production) | 20-30 | 30000 |
| Élevée (concurrence élevée) | 50-100 | 10000 |
Remarque : Azure PostgreSQL a des limites de connexion selon la SKU. Vérifiez les connexions maximales de votre tier.
Bonnes pratiques
- Toujours utiliser les pools de connexions pour les applications de production
- Utiliser des requêtes paramétrées - Ne JAMAIS concaténer l'entrée utilisateur
- Toujours fermer les connexions - Utiliser
try/finallyou les pools de connexions - Activer SSL - Requis pour Azure (
ssl: { rejectUnauthorized: true }) - Gérer l'actualisation des tokens - Les tokens Entra ID expirent après ~1 heure
- Définir les délais d'expiration de connexion - Éviter les blocages sur les problèmes réseau
- Utiliser les transactions - Pour les opérations multi-instructions
- Surveiller les métriques du pool - Suivre
pool.totalCount,pool.idleCount,pool.waitingCount - Arrêt élégant - Appeler
pool.end()à la fermeture de l'application - Utiliser les génériques TypeScript - Typer les résultats de vos requêtes pour la sécurité
Types clés
import {
Client,
Pool,
PoolClient,
PoolConfig,
QueryResult,
QueryResultRow,
DatabaseError,
QueryConfig
} from "pg";
Liens de référence
| Ressource | URL |
|---|---|
| Documentation node-postgres | https://node-postgres.com |
| Package npm | https://www.npmjs.com/package/pg |
| Référentiel GitHub | https://github.com/brianc/node-postgres |
| Documentation Azure PostgreSQL | https://learn.microsoft.com/azure/postgresql/flexible-server/ |
| Connexion sans mot de passe | https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-connect-with-managed-identity |