npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@odoro-cli/cloud-connect

v0.1.0

Published

Connexion PostgreSQL a une base Odoro, cote serveur. Pool, requetes liees, erreurs distinguees.

Downloads

105

Readme

@odoro-cli/cloud-connect

Le paquet que votre application installe pour parler a sa base Odoro.

npm install @odoro-cli/cloud-connect
import { connecter, sql } from '@odoro-cli/cloud-connect'

const db = connecter({ connectionString: process.env.ODORO_DATABASE_URL! })

const { rows } = await db.query<{ id: string; nom: string }>(
  sql`select id, nom from clients where pays = ${pays}`,
)

C'est tout ce dont une application a besoin en regime normal : une chaine de connexion, et des requetes. Le reste de ce document explique d'ou vient cette chaine, et pourquoi elle ne se redemande pas.


Serveur uniquement. Ce n'est pas une recommandation.

Ce paquet ouvre une connexion PostgreSQL directe. Une chaine de connexion envoyee au navigateur est lisible par quiconque ouvre les outils de developpement, et elle ouvre la base entiere — toutes les tables, en lecture et en ecriture, sans passer par la moindre de vos regles.

Le paquet refuse donc de fonctionner dans un navigateur, de deux facons :

  • votre empaqueteur, s'il vise le navigateur, resout la condition d'export browser vers un module qui leve a l'import : la construction echoue ;
  • a l'execution, connecter() leve si window et document sont tous les deux presents.

Et le paquet n'accepte aucun jeton de plateforme : il n'a aucun parametre qui pourrait en transporter un. Voir la section suivante pour ce que cela protege.

Exposez vos propres routes a votre client, et gardez la base derriere.


Le chemin complet, de zero a la premiere requete

1. Obtenir un jeton de plateforme

[!WARNING] Un jeton de plateforme porte des portees sur toute votre organisation : databases:create, databases:destroy, billing:write. Qui le tient peut detruire votre production et changer votre plan payant.

Il sert a gerer des bases — depuis votre poste, ou depuis un pipeline de deploiement. Il n'a rien a faire dans l'application qui sert vos clients, et ce paquet-ci ne le demande jamais. Ne le posez ni dans un navigateur, ni dans une image de conteneur, ni dans un depot.

Depuis le tableau de bord Odoro, ou par l'API :

curl -X POST https://api.odoro.dev/v1/tokens \
  -H "authorization: Bearer $ODORO_SESSION" \
  -H "content-type: application/json" \
  -d '{
    "projectId": "prj_…",
    "label": "deploiement production",
    "scopes": ["databases:read", "databases:write", "credentials:rotate"]
  }'

Les portees demandees sont intersectees avec les votres : un jeton ne peut pas porter plus que celui qui le demande.

2. Creer une base — ou en prendre une existante

Avec @odoro-cli/cloud-sdk, le client de gestion (un autre paquet que celui-ci) :

import { createClient } from '@odoro-cli/cloud-sdk'

const api = createClient({ baseUrl: 'https://api.odoro.dev', token: process.env.ODORO_TOKEN! })

// Le provisionnement est asynchrone : `createAndWait` interroge jusqu'au bout.
const operation = await api.databases.createAndWait({
  idempotencyKey: crypto.randomUUID(),
  environmentId: 'env_…',
  name: 'production',
  region: 'eu-central-1',
})

const databaseId = operation.subject

Ou, pour une base qui existe deja :

const { databases } = await api.databases.list({ environmentId: 'env_…' })

3. Obtenir la chaine de connexion — une fois

const { credentialId, connectionString, issuedAt } = await api.credentials.rotate({
  databaseId,
})

[!IMPORTANT] Cette route est une rotation, pas une lecture.

Elle emet un identifiant neuf et invalide le precedent. La chaine est rendue une seule fois : aucune autre route ne la rend, et celle-ci ne la rend pas deux fois. Qui la perd la fait tourner — et fait du meme coup tomber toutes les instances qui tenaient l'ancienne.

Elle est par ailleurs plafonnee a cinq appels par minute.

4. Ranger la chaine la ou votre application la lira

Dans un coffre — Secrets Manager, Vault, les secrets de votre plateforme — ou, a defaut, dans une variable d'environnement :

ODORO_DATABASE_URL=postgres://…

C'est le point de bascule entre les deux mondes. Les etapes 1 a 3 se font une fois, depuis un poste ou un pipeline, avec un jeton de plateforme. L'etape 5 se fait a chaque demarrage, dans votre application, sans aucun jeton.

5. Se connecter et interroger

import { connecter, sql, estConnectError } from '@odoro-cli/cloud-connect'

const db = connecter({
  connectionString: process.env.ODORO_DATABASE_URL!,
  mode: 'service',
  applicationName: 'ma-boutique-api',
})

try {
  const { rows } = await db.query<{ id: string; nom: string }>(
    sql`select id, nom from clients where pays = ${pays} limit ${20}`,
  )
  console.log(rows)
} catch (cause) {
  if (estConnectError(cause, 'INJOIGNABLE')) {
    // Reessayable : la base se reveille peut-etre.
  }
}

process.on('SIGTERM', () => void db.fermer())

Pourquoi votre application ne doit pas appeler credentials.rotate

C'est la seule erreur de conception que ce paquet existe pour vous empecher de commettre. Aller chercher la chaine a chaque demarrage semble elegant — plus de secret a gerer — et coute trois choses :

  1. Chaque demarrage couperait les autres. La rotation invalide l'identifiant precedent. Vos six instances deja en service perdent leur connexion au moment ou la septieme demarre.
  2. Vous vous bloqueriez vous-meme. Cinq appels par minute : un deploiement roulant de six instances echoue sur sa propre limite de cadence.
  3. Vous transporteriez un jeton de plateforme dans votre application. Un jeton qui porte databases:destroy et billing:write sur toute votre organisation, la ou vous n'aviez besoin que d'une base.

La chaine de connexion se fournit donc a l'application ; elle ne se recupere pas. C'est pourquoi connecter() ne prend qu'une chaine, et pourquoi ce paquet ne connait aucune URL du plan de controle.

Quand vous faites tourner un identifiant pour de bon

Une fuite, un depart, une rotation periodique. L'ordre qui n'interrompt rien :

  1. api.credentials.rotate({ databaseId }) depuis votre pipeline ;
  2. ecrire la nouvelle chaine dans votre coffre ;
  3. faire adopter la nouvelle chaine a vos instances.

Pour la troisieme etape, un redemarrage marche et coute une interruption. remplacerChaine ne coute rien :

// Le pool neuf sert avant que l'ancien ne soit vide : les requetes en cours
// finissent, les suivantes partent sur le nouvel identifiant.
await db.remplacerChaine(nouvelleChaine)

Si la nouvelle chaine est invalide, rien ne change et l'ancienne connexion continue de servir.

Une erreur AUTHENTIFICATION ne doit pas vous faire appeler rotate. C'est le reflexe naturel, et il transforme une instance en panne en parc entier en panne. Relisez d'abord la chaine courante dans votre coffre : quelqu'un d'autre a probablement deja fait tourner l'identifiant.


Les valeurs ne rentrent jamais dans le texte

import { sql, identifiant, joindre } from '@odoro-cli/cloud-connect'

const nom = "Robert'); drop table clients; --"

await db.query(sql`select * from clients where nom = ${nom}`)
// texte   → 'select * from clients where nom = $1'
// valeurs → ["Robert'); drop table clients; --"]

Chaque ${…} devient un emplacement lie. pg savait deja lier des valeurs ; ce que le gabarit ajoute, c'est de rendre la forme sure plus courte a ecrire que la concatenation — parce que c'est la facilite comparee des deux ecritures qui produit les injections, pas l'ignorance.

Les fragments se composent, et les emplacements sont renumerotes :

const conditions = [sql`pays = ${pays}`]
if (actifsSeulement) conditions.push(sql`actif = ${true}`)

await db.query(sql`select * from clients where ${joindre(conditions, ' and ')}`)

Un nom de table ou de colonne ne peut pas etre lie — PostgreSQL ne l'accepte pas en parametre. C'est le seul echappement, et il ne prend jamais une valeur :

await db.query(sql`select * from clients order by ${identifiant(colonne)} desc`)

La forme native reste disponible quand vous la preferez :

await db.query('select * from clients where pays = $1', [pays])

Les transactions tiennent sur une seule connexion

await db.transaction(async (tx) => {
  const { rows } = await tx.query<{ id: string }>(
    sql`insert into commandes (client_id) values (${clientId}) returning id`,
  )
  await tx.query(sql`insert into lignes (commande_id, article) values (${rows[0]!.id}, ${article})`)
})

Une exception annule tout et relance. N'envoyez jamais begin et commit par query : avec un pool, les deux instructions tombent sur des connexions differentes, la transaction reste ouverte sur l'une et le commit echoue sur l'autre — sans que rien ne le signale avant que les verrous ne s'accumulent.


Distinguer les pannes, parce que les gestes sont opposes

import { estConnectError } from '@odoro-cli/cloud-connect'

catch (cause) {
  if (estConnectError(cause)) {
    console.error(cause.genre, cause.details.cible) // jamais le mot de passe
    if (cause.reessayable) return reessayer()
  }
}

| Genre | Ce qui s'est passe | Ce qu'il faut faire | | --- | --- | --- | | CHAINE_INVALIDE | La chaine ne peut pas designer une base | Corriger la configuration. Reessayer ne changera rien | | AUTHENTIFICATION | La base a refuse l'identifiant | Relire la chaine courante dans le coffre. Ne pas faire tourner | | INJOIGNABLE | Le serveur n'a pas repondu | Reessayer : une base endormie met quelques secondes a se reveiller | | DELAI | Une limite de temps a ete atteinte | Regarder la requete et ses index avant de rallonger le delai | | REQUETE | Le serveur a compris et refuse | Corriger la requete : syntaxe, contrainte, droit | | FERMEE | La connexion est fermee | Ouvrir une nouvelle connexion ; un pool ferme ne se rouvre pas |

Une chaine invalide echoue des connecter(), au demarrage, plutot qu'a la premiere requete d'un client. Aucun message d'erreur ne contient votre mot de passe : une erreur finit dans un journal, un agregateur, parfois un ticket.


Fonctions serverless et services longs

const db = connecter({ connectionString, mode: 'ephemere' })

| | service (defaut) | ephemere | | --- | --- | --- | | Connexions gardees | 10 | 1 | | Rendues apres | 30 s | 1 s | | Laisse le processus se terminer | non | oui |

Le mode ephemere est plus lent par requete, et c'est un prix qu'on paie sciemment. Ces politiques se multiplient par le nombre d'instances : dix connexions gardees par une fonction qui monte a cent instances simultanees, ce sont mille connexions demandees a une base qui en accepte cent — et la panne frappe au moment precis ou le trafic monte.

allowExitOnIdle compte autant : sans lui, une connexion inactive garde la boucle d'evenements de Node ouverte et votre fonction ne rend jamais la main. Une fonction ephemere ne peut presque jamais appeler fermer().

Le mode est explicite et non devine : renifler AWS_LAMBDA_FUNCTION_NAME ou VERCEL produirait une liste fausse le mois suivant, et une detection ratee choisirait silencieusement la politique qui fait tomber la base.


Reglages

| Option | Defaut | | | --- | --- | --- | | connectionString | — | La chaine rendue par la rotation | | mode | 'service' | 'service' ou 'ephemere' | | max | selon le mode | Plafond de connexions. Toujours 1 en mode ephemere | | connectTimeoutMs | 10000 | Attente pour ouvrir. Large a dessein : une base endormie met quelques secondes a se reveiller | | statementTimeoutMs | 30000 | Attente pour executer | | applicationName | 'odoro-connect' | Visible dans pg_stat_activity. Vaut la peine d'etre pose : devant une connexion qui tient un verrou, c'est ce qui dit quel service redemarrer |

Les deux delais partent avec la poignee de main PostgreSQL, pas par un SET : avec un pool, deux requetes successives ne tombent pas forcement sur la meme connexion, et un SET envoye sur l'une ne protegerait pas l'autre.

Un idle_in_transaction_session_timeout de trente secondes est pose et n'est pas reglable : un BEGIN dont le COMMIT n'arrive jamais garderait ses verrous jusqu'a ce que la production s'arrete.


Ce que ce paquet ne fait pas

| | Pourquoi | | --- | --- | | Constructeur de requetes | Il faudrait decrire votre schema, donc suivre vos migrations, donc devenir un ORM. Autre metier, autre paquet | | Migrations | Un outil de migration doit tenir un journal versionne, gerer les reprises et les reculs. Le glisser ici ferait un paquet qui fait deux choses a moitie | | ORM | Voir les deux lignes ci-dessus, additionnees | | Gestion des bases | Creer, detruire, facturer : c'est @odoro-cli/cloud-sdk, et cela demande un jeton de plateforme que ce paquet refuse de connaitre | | Recuperation d'identifiants | La route est une rotation. Voir plus haut | | Pilote PostgreSQL | pg est la reference, et le plan de controle Odoro l'emploie deja |


Les deux paquets Odoro, cote a cote

| | @odoro-cli/cloud-sdk | @odoro-cli/cloud-connect | | --- | --- | --- | | A quoi il sert | Gerer des bases | Employer une base | | A qui il parle | Au plan de controle, en HTTP | A PostgreSQL, en direct | | Ce qu'il exige | Un jeton de plateforme | Une chaine de connexion | | Portee d'un secret perdu | Toute l'organisation | Cette base | | Ou il vit | Poste, pipeline de deploiement | Votre application |