encore-infrastructure

Par encoredev · skills

Déclarez des bases de données, Pub/Sub, des tâches cron, du cache, du stockage d'objets et des secrets avec Encore.ts.

npx skills add https://github.com/encoredev/skills --skill encore-infrastructure

Déclaration d'infrastructure Encore

Instructions

Encore.ts utilise une infrastructure déclarative - vous définissez les ressources en code et Encore gère le provisionnement :

  • Localement (encore run) - Encore exécute l'infrastructure dans Docker (Postgres, Redis, etc.)
  • Production - Déployez via Encore Cloud sur votre AWS/GCP, ou auto-hébergez en utilisant la configuration d'infrastructure générée

Règle critique

Toute l'infrastructure doit être déclarée au niveau du package (haut du fichier), jamais à l'intérieur des fonctions.

Bases de données (PostgreSQL)

import { SQLDatabase } from "encore.dev/storage/sqldb";

// CORRECT : Au niveau du package
const db = new SQLDatabase("mydb", {
  migrations: "./migrations",
});

// INCORRECT : À l'intérieur d'une fonction
async function setup() {
  const db = new SQLDatabase("mydb", { migrations: "./migrations" });
}

Migrations

Créez des migrations dans le répertoire migrations/ :

service/
├── encore.service.ts
├── api.ts
├── db.ts
└── migrations/
    ├── 001_create_users.up.sql
    └── 002_add_email_index.up.sql

Nommage des migrations : {number}_{description}.up.sql

Pub/Sub

Topics

import { Topic } from "encore.dev/pubsub";

interface OrderCreatedEvent {
  orderId: string;
  userId: string;
  total: number;
}

// Déclaration au niveau du package
export const orderCreated = new Topic<OrderCreatedEvent>("order-created", {
  deliveryGuarantee: "at-least-once",
});

Publication

await orderCreated.publish({
  orderId: "123",
  userId: "user-456",
  total: 99.99,
});

Souscriptions

import { Subscription } from "encore.dev/pubsub";

const _ = new Subscription(orderCreated, "send-confirmation-email", {
  handler: async (event) => {
    await sendEmail(event.userId, event.orderId);
  },
});

Attributs de message

Utilisez Attribute<T> pour les champs qui doivent être des attributs de message (pour le filtrage/l'ordonnancement) :

import { Topic, Attribute } from "encore.dev/pubsub";

interface CartEvent {
  cartId: Attribute<string>;  // Utilisé pour l'ordonnancement
  userId: string;
  action: "add" | "remove";
  productId: string;
}

// Topic ordonné - les événements avec le même cartId sont livrés dans l'ordre
export const cartEvents = new Topic<CartEvent>("cart-events", {
  deliveryGuarantee: "at-least-once",
  orderingAttribute: "cartId",
});

Références de topic

Passez l'accès au topic à d'autre code tout en maintenant l'analyse statique :

import { Publisher } from "encore.dev/pubsub";

// Créez une référence avec permission de publication
const publisherRef = orderCreated.ref<Publisher>();

// Utilisez la référence
async function notifyOrder(ref: typeof publisherRef, orderId: string) {
  await ref.publish({ orderId, userId: "123", total: 99.99 });
}

Tâches cron

import { CronJob } from "encore.dev/cron";
import { api } from "encore.dev/api";

// L'endpoint à appeler
export const cleanupExpiredSessions = api(
  { expose: false },
  async (): Promise<void> => {
    // Logique de nettoyage
  }
);

// Déclaration cron au niveau du package
const _ = new CronJob("cleanup-sessions", {
  title: "Clean up expired sessions",
  schedule: "0 * * * *",  // Chaque heure
  endpoint: cleanupExpiredSessions,
});

Formats de planification

Format Exemple Description
every "1h", "30m" Intervalle simple (doit diviser 24h uniformément)
schedule "0 9 * * 1" Expression cron (9h chaque lundi)

Stockage d'objets

import { Bucket } from "encore.dev/storage/objects";

// Au niveau du package
export const uploads = new Bucket("user-uploads", {
  versioned: false,  // Mettez à true pour conserver plusieurs versions des objets
});

// Bucket public (fichiers accessibles via URL publique)
export const publicAssets = new Bucket("public-assets", {
  public: true,
  versioned: false,
});

Opérations

// Télécharger
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
  contentType: "image/jpeg",
});

// Télécharger
const data = await uploads.download("path/to/file.jpg");

// Vérifier l'existence
const exists = await uploads.exists("path/to/file.jpg");

// Obtenir les attributs (taille, type de contenu, ETag)
const attrs = await uploads.attrs("path/to/file.jpg");

// Supprimer
await uploads.remove("path/to/file.jpg");

// Lister les objets
for await (const entry of uploads.list({})) {
  console.log(entry.key, entry.size);
}

// URL publique (uniquement pour les buckets publics)
const url = publicAssets.publicUrl("image.jpg");

URLs signées

Générez des URLs temporaires pour télécharger/recevoir sans exposer votre bucket :

// URL de téléchargement signée (expire dans 2 heures)
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });

// URL de téléchargement signée
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });

Références de bucket

Passez l'accès au bucket avec des permissions spécifiques à d'autre code :

import { Uploader, Downloader } from "encore.dev/storage/objects";

// Créez une référence avec permission de téléchargement uniquement
const uploaderRef = uploads.ref<Uploader>();

// Créez une référence avec permission de réception uniquement
const downloaderRef = uploads.ref<Downloader>();

// Types de permissions : Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter

Mise en cache (Redis)

Clusters de cache

import { CacheCluster } from "encore.dev/storage/cache";

// Au niveau du package
const cluster = new CacheCluster("my-cache", {
  evictionPolicy: "allkeys-lru",
});

Référencez un cluster défini dans un autre service :

const cluster = CacheCluster.named("my-cache");

Politiques d'éviction : "allkeys-lru" (défaut), "noeviction", "allkeys-lfu", "allkeys-random", "volatile-lru", "volatile-lfu", "volatile-ttl", "volatile-random".

Types d'espace de clés

Chaque espace de clés a un type de clé (utilisé pour générer la clé Redis) et un type de valeur.

import {
  StringKeyspace,
  IntKeyspace,
  FloatKeyspace,
  StructKeyspace,
  StringListKeyspace,
  NumberListKeyspace,
  StringSetKeyspace,
  NumberSetKeyspace,
  expireIn,
} from "encore.dev/storage/cache";

// Valeurs string
const tokens = new StringKeyspace<{ tokenId: string }>(cluster, {
  keyPattern: "token/:tokenId",
  defaultExpiry: expireIn(3600 * 1000), // 1 heure en ms
});

await tokens.set({ tokenId: "abc" }, "value");
const val = await tokens.get({ tokenId: "abc" }); // undefined en cas de miss
await tokens.delete({ tokenId: "abc" });

// Valeurs entières (supporte l'incrémentation/décrémentation)
const counters = new IntKeyspace<{ userId: string }>(cluster, {
  keyPattern: "requests/:userId",
  defaultExpiry: expireIn(10 * 1000),
});

const count = await counters.increment({ userId: "user123" }, 1);
await counters.decrement({ userId: "user123" }, 1);

// Valeurs float
const scores = new FloatKeyspace<{ oddsId: string }>(cluster, {
  keyPattern: "odds/:oddsId",
});

// Données structurées (stockées en JSON)
interface UserProfile {
  name: string;
  email: string;
}

const profiles = new StructKeyspace<{ userId: string }, UserProfile>(cluster, {
  keyPattern: "profile/:userId",
  defaultExpiry: expireIn(3600 * 1000),
});

await profiles.set({ userId: "123" }, { name: "Alice", email: "alice@example.com" });

// Listes
const recentItems = new StringListKeyspace<{ userId: string }>(cluster, {
  keyPattern: "recent/:userId",
});

await recentItems.pushRight({ userId: "user123" }, "item1", "item2");
const items = await recentItems.getRange({ userId: "user123" }, 0, -1);

// Ensembles
const tags = new StringSetKeyspace<{ articleId: string }>(cluster, {
  keyPattern: "tags/:articleId",
});

await tags.add({ articleId: "post1" }, "typescript", "encore", "backend");
const hasTag = await tags.contains({ articleId: "post1" }, "typescript");

Motifs de clé avec plusieurs champs

interface ResourceKey {
  userId: string;
  resourcePath: string;
}

const resourceRequests = new IntKeyspace<ResourceKey>(cluster, {
  keyPattern: "requests/:userId/:resourcePath",
  defaultExpiry: expireIn(10 * 1000),
});

Options d'expiration

import {
  expireIn,          // millisecondes
  expireInSeconds,
  expireInMinutes,
  expireInHours,
  expireDailyAt,     // heure UTC spécifique chaque jour
  neverExpire,
  keepTTL,           // conserver le TTL existant lors de la mise à jour
} from "encore.dev/storage/cache";

Options d'écriture

// Remplacer l'expiration par défaut
await keyspace.set(key, value, { expiry: expireInMinutes(30) });

// Conserver le TTL existant
await keyspace.set(key, value, { expiry: keepTTL });

// Définir uniquement si la clé n'existe pas (lève CacheKeyExists sinon)
await keyspace.setIfNotExists(key, value);

// Définir uniquement si la clé existe déjà (lève CacheMiss sinon)
await keyspace.replace(key, value);

Gestion des erreurs

import { CacheMiss, CacheKeyExists } from "encore.dev/storage/cache";

// get() retourne undefined en cas de miss (ne lève pas d'erreur)
const value = await keyspace.get(key);

// replace() lève CacheMiss si la clé n'existe pas
// setIfNotExists() lève CacheKeyExists si la clé existe déjà

Secrets

import { secret } from "encore.dev/config";

// Au niveau du package
const stripeKey = secret("StripeSecretKey");

// Utilisation (appelez comme une fonction)
const key = stripeKey();

Définissez les secrets via CLI :

encore secret set --type prod StripeSecretKey

Directives

  • Les déclarations d'infrastructure DOIVENT être au niveau du package
  • Utilisez des noms explicites pour les ressources
  • Gardez les migrations séquentielles et numérotées
  • Les gestionnaires de souscriptions doivent être idempotents (livraison au-moins-une-fois)
  • Les secrets sont accédés en appelant le secret comme une fonction
  • Les endpoints cron doivent être expose: false (internes uniquement)

Skills similaires