GraphQL
PetraDB fonctionne avec tout serveur GraphQL qui s’exécute sur Node.js. Ce guide utilise GraphQL Yoga — un serveur léger et conforme aux spécifications — mais la même approche s’applique à Apollo Server, Mercurius ou tout autre framework. PetraDB s’exécute en processus, donc les resolvers appellent le moteur directement sans aller-retour réseau.
Installation
Section intitulée « Installation »npm install @petradb/engine graphql-yoga graphqlConfiguration
Section intitulée « Configuration »import { Session } from '@petradb/engine';import { createYoga, createSchema } from 'graphql-yoga';import { createServer } from 'http';
const db = new Session();
await db.execute(` CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT )`);Schéma et resolvers
Section intitulée « Schéma et resolvers »Définissez un schéma GraphQL correspondant à vos tables PetraDB :
const yoga = createYoga({ schema: createSchema({ typeDefs: ` type User { id: Int! name: String! email: String }
type Query { users: [User!]! user(id: Int!): User }
type Mutation { createUser(name: String!, email: String): User! updateUser(id: Int!, name: String, email: String): User deleteUser(id: Int!): Boolean! } `, resolvers: { Query: { users: async () => { const [{ rows }] = await db.execute('SELECT * FROM users'); return rows; }, user: async (_, { id }) => { const [{ rows }] = await db.prepare('SELECT * FROM users WHERE id = $1') .execute([id]); return rows[0] || null; }, }, Mutation: { createUser: async (_, { name, email }) => { const [{ rows }] = await db.prepare( 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *' ).execute([name, email ?? null]); return rows[0]; }, updateUser: async (_, { id, name, email }) => { const sets = []; const params = []; let i = 1; if (name !== undefined) { sets.push(`name = $${i++}`); params.push(name); } if (email !== undefined) { sets.push(`email = $${i++}`); params.push(email ?? null); } if (sets.length === 0) return null; params.push(id); const [{ rows }] = await db.prepare( `UPDATE users SET ${sets.join(', ')} WHERE id = $${i} RETURNING *` ).execute(params); return rows[0] || null; }, deleteUser: async (_, { id }) => { const [result] = await db.prepare('DELETE FROM users WHERE id = $1').execute([id]); return result.rowCount > 0; }, }, }, }),});
const server = createServer(yoga);server.listen(4000, () => console.log('GraphQL API running on http://localhost:4000/graphql'));Requêtes paramétrées
Section intitulée « Requêtes paramétrées »Utilisez toujours db.prepare() avec des paramètres positionnels ($1, $2, …) pour les valeurs fournies par l’utilisateur. Cela prévient l’injection SQL et gère la conversion de types automatiquement.
const [{ rows }] = await db.prepare( 'SELECT * FROM users WHERE name = $1 AND email = $2').execute([name, email]);Resolvers imbriqués
Section intitulée « Resolvers imbriqués »Pour les données liées, ajoutez des resolvers de champs qui exécutent des requêtes supplémentaires :
await db.execute(` CREATE TABLE posts ( id SERIAL PRIMARY KEY, author_id INTEGER NOT NULL REFERENCES users(id), title TEXT NOT NULL, body TEXT )`);
const schema = createSchema({ typeDefs: ` type User { id: Int! name: String! posts: [Post!]! }
type Post { id: Int! title: String! body: String author: User! }
type Query { users: [User!]! posts: [Post!]! } `, resolvers: { Query: { users: async () => { const [{ rows }] = await db.execute('SELECT * FROM users'); return rows; }, posts: async () => { const [{ rows }] = await db.execute('SELECT * FROM posts'); return rows; }, }, User: { posts: async (user) => { const [{ rows }] = await db.prepare( 'SELECT * FROM posts WHERE author_id = $1' ).execute([user.id]); return rows; }, }, Post: { author: async (post) => { const [{ rows }] = await db.prepare( 'SELECT * FROM users WHERE id = $1' ).execute([post.author_id]); return rows[0]; }, }, },});Pagination
Section intitulée « Pagination »La pagination par décalage correspond directement au SQL LIMIT et OFFSET :
const resolvers = { Query: { users: async (_, { limit = 10, offset = 0 }) => { const [{ rows }] = await db.prepare( 'SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2' ).execute([limit, offset]); return rows; }, },};Pour la pagination par curseur, utilisez l’ID de ligne comme curseur :
const resolvers = { Query: { users: async (_, { first = 10, after }) => { let sql = 'SELECT * FROM users'; const params = []; if (after) { sql += ' WHERE id > $1'; params.push(after); } sql += ` ORDER BY id LIMIT $${params.length + 1}`; params.push(first + 1); // récupérer un élément supplémentaire pour vérifier hasNextPage
const [{ rows }] = await db.prepare(sql).execute(params); const hasNextPage = rows.length > first; const edges = rows.slice(0, first);
return { edges: edges.map((node) => ({ node, cursor: node.id })), pageInfo: { hasNextPage, endCursor: edges.length ? edges[edges.length - 1].id : null, }, }; }, },};Transactions
Section intitulée « Transactions »Encapsulez les mutations multi-étapes dans une transaction avec BEGIN / COMMIT / ROLLBACK :
const resolvers = { Mutation: { transferCredits: async (_, { fromId, toId, amount }) => { await db.execute('BEGIN'); try { await db.prepare( 'UPDATE accounts SET balance = balance - $1 WHERE id = $2' ).execute([amount, fromId]); await db.prepare( 'UPDATE accounts SET balance = balance + $1 WHERE id = $2' ).execute([amount, toId]); await db.execute('COMMIT'); return true; } catch (e) { await db.execute('ROLLBACK'); throw e; } }, },};Agrégations
Section intitulée « Agrégations »Utilisez les agrégats SQL et retournez des champs calculés :
const resolvers = { Query: { userStats: async () => { const [{ rows }] = await db.execute(` SELECT COUNT(*) AS total, COUNT(email) AS with_email, MIN(id) AS first_id, MAX(id) AS last_id FROM users `); return rows[0]; }, },};Mapping de types
Section intitulée « Mapping de types »PetraDB retourne des types JavaScript natifs, donc les scalaires GraphQL fonctionnent sans coercition manuelle :
| Type PetraDB | Type JS | Scalaire GraphQL |
|---|---|---|
SERIAL / INTEGER | number | Int |
BIGINT | number | Int |
DOUBLE / REAL | number | Float |
NUMERIC | number | Float |
TEXT / VARCHAR | string | String |
BOOLEAN | boolean | Boolean |
DATE / TIMESTAMP | Date | String (ou scalaire personnalisé) |
JSON | object | scalaire personnalisé ou String |
NULL | null | champ nullable |