Aller au contenu

Collections

Une collection est un objet TypeScript qui décrit une table de base de données et la façon dont elle doit apparaître dans Rebase CMS. Elle définit :

  • Schéma — Propriétés (colonnes), leurs types et règles de validation
  • Relations — Clés étrangères, tables de jonction et chemins de jointure
  • Sécurité — Politiques de sécurité au niveau des lignes (Row Level Security)
  • Hooks de cycle de vie — Fonctions de rappel (callbacks) pour les opérations de création, mise à jour et suppression
  • Comportement du CMS — Modes d’affichage, édition en ligne, vues d’entités, actions — le tout sous admin

Enveloppez le littéral dans defineCollection. À l’exécution, il s’agit d’une fonction d’identité — elle renvoie l’objet sans le modifier — son coût est donc nul. Son avantage réside dans l’inférence : un paramètre de type const capture les clés de vos properties en tant que types littéraux, et les champs représentant des clés dans le bloc admin sont ensuite vérifiés par rapport à celles-ci. Un nom qui ne fait pas partie de vos propriétés entraîne une erreur de compilation, et pas seulement une suggestion manquante.

import { defineCollection } from "@rebasepro/cms-types";
const products = defineCollection({
name: "Products",
slug: "products",
table: "products",
properties: {
name: { name: "Name", type: "string" },
price: { name: "Price", type: "number" }
},
admin: {
display: { title: "name" }, // completion: "name" | "price"
sort: ["price", "asc"], // completion on the first element
propertiesOrder: ["name", "price"]
}
});
admin: {
display: { title: "nmae" }
// ~~~~~~ Type '"nmae"' is not assignable to type
// 'PropertyPath<…>'. Did you mean '"name"'?
}

Les champs vérifiés sont display, sort, propertiesOrder et listProperties. Trois formes sont acceptées en plus d’une simple clé de propriété :

Forme Exemple Notes
Chemin pointé dans un map "profile.displayName" La racine doit être une vraie propriété ; le chemin sous-jacent n’est pas vérifié.
Colonne de sous-collection enfant "subcollection:orders" propertiesOrder / listProperties uniquement.
Une clé de additionalFields "score" as AdditionalFieldKey Nécessite le cast — voir ci-dessous.

AdditionalFieldDelegate.key est une simple string, le système de types n’a donc aucun moyen de savoir quelles clés supplémentaires une collection déclare. Plutôt que de rouvrir ces champs à n’importe quelle chaîne, le cast rend l’exception explicite :

import type { AdditionalFieldKey } from "@rebasepro/cms-types";
propertiesOrder: ["title", "score" as AdditionalFieldKey]

Importez-le depuis @rebasepro/cms-types dans un projet qui comporte un panneau d’administration — c’est cette version qui vérifie également les types du bloc admin. Un projet BaaS headless, qui ne comporte aucun bloc admin, importe plutôt la même fonction depuis @rebasepro/common.

L’annotation directe du type fonctionne toujours et reste vérifiée :

import type { PostgresCollectionConfig } from "@rebasepro/types";
const products: PostgresCollectionConfig = {
name: "Products",
slug: "products",
table: "products",
properties: {
name: { name: "Name", type: "string" }
}
};

mais une annotation ne fait que valider la forme — elle ne peut pas voir vos noms de propriétés, donc les champs de clés de admin se rabattent sur l’acceptation de n’importe quelle chaîne. Privilégiez defineCollection à moins que vous n’ayez besoin de nommer le type.

Un seul fichier, deux publics. Tout ce qui concerne la base de données et l’API se trouve au niveau racine ; tout ce que le panneau d’administration affiche se trouve dans admin.

const posts = {
// ── The backend reads these ──────────────────────────────
slug: "posts",
table: "posts",
properties: { /* … */ },
relations: [ /* … */ ],
securityRules: [ /* … */ ],
callbacks: { /* … */ },
history: true,
// ── The admin panel reads these ──────────────────────────
admin: {
icon: "FileText",
listProperties: ["title", "status"],
defaultViewMode: "table",
entityViews: ["preview"]
}
};

Cette séparation n’est pas cosmétique. C’est ce qui permet à Rebase d’être un backend autonome :

  • Un projet BaaS ou headless n’écrit jamais de bloc admin. Ses collections — ou aucune collection, puisque le mode BaaS introspecte la base de données — décrivent les données et l’autorisation, rien d’autre. @rebasepro/types ne contient aucun code d’interface utilisateur, ainsi l’arbre de dépendances d’un projet headless reste réservé au serveur.
  • Le backend ne lit jamais l’intérieur du bloc. Il est retiré avant qu’une collection ne soit sérialisée vers le point de terminaison du contrat ou dans un bundle de build, et il est exclu de la version de schéma — ainsi, modifier une icône n’invalide pas tous les SDK générés.

Le bloc admin n’existe que si vous installez les types admin

Section intitulée « Le bloc admin n’existe que si vous installez les types admin »

@rebasepro/types ne déclare aucun champ admin — ni sur une collection, ni sur une propriété. Dans un projet BaaS, en écrire un constitue une erreur de type. @rebasepro/cms-types l’ajoute par fusion de déclarations (declaration merging), ainsi une seule ligne par projet suffit à l’activer :

// config/cms.d.ts
/// <reference types="@rebasepro/cms-types" />

Après cela, les types de base standard intègrent un bloc entièrement typé — une coquille comme icoon est une erreur, et vous bénéficiez de l’autocomplétion :

import { defineCollection } from "@rebasepro/cms-types";
const posts = defineCollection({
slug: "posts",
name: "Posts",
table: "posts",
properties: {
title: { name: "Title", type: "string", admin: { multiline: true } }
},
admin: { icon: "FileText" }
});

Une augmentation s’applique à l’ensemble du programme TypeScript, et config/ et frontend/ sont des programmes distincts — c’est pourquoi la référence a sa place dans le package config. Il n’existe pas de type wrapper AdminCollectionConfig : une fois le champ fusionné, CollectionConfig constitue le type de rédaction principal.

Avant la version 0.11, ces champs se trouvaient au niveau racine. Pour les déplacer :

node scripts/codemod/collections-admin-block.mjs config/collections

L’outil signale tout ce qu’il ne peut pas déplacer en toute sécurité — notamment la présentation dans relations[].overrides, qui requiert overrides: { admin: { … } } manuellement.

import { defineCollection } from "@rebasepro/cms-types";
export const productsCollection = defineCollection({
slug: "products", // URL path and API endpoint
name: "Products", // Display name (plural)
singularName: "Product", // Display name (singular)
table: "products", // PostgreSQL table name
properties: {
name: {
type: "string",
name: "Product Name",
validation: { required: true }
},
price: {
type: "number",
name: "Price",
validation: { required: true, min: 0 }
},
category: {
type: "string",
name: "Category",
enum: [
{ id: "electronics", label: "Electronics", color: "blue" },
{ id: "clothing", label: "Clothing", color: "pink" },
{ id: "books", label: "Books", color: "orange" }
]
},
description: {
type: "string",
name: "Description",
admin: { multiline: true }
},
active: {
type: "boolean",
name: "Active",
defaultValue: true
},
createdAt: {
type: "date",
name: "Created At",
autoValue: "on_create",
admin: { readOnly: true }
}
},
admin: {
icon: "Package" // Lucide icon name
}
});
Propriété Type Description
slug string Requis. Identifiant compatible URL. Utilisé dans l’URL de l’interface d’administration et le chemin de l’API REST (/api/data/{slug}).
name string Requis. Nom d’affichage (au pluriel). Affiché dans la navigation et les en-têtes de page.
singularName string Nom d’affichage pour une entité unique. Utilisé dans « Nouveau produit », « Modifier le produit », etc.
description string Une phrase décrivant le contenu de cette collection, affichée au-dessus de la liste. Markdown.
table string Nom de la table PostgreSQL. Vaut par défaut toSnakeCase(slug) — définissez-le uniquement pour dissocier l’URL de la table, par exemple une table blog_posts existante exposée sur /posts.
admin.icon string Un nom d’icône Lucide, par exemple "FileText", "ShoppingCart". Un élément rendu fonctionne également, mais le nom survit à la sérialisation, c’est donc ce que l’éditeur de schéma réécrit.
Propriété Type Description
properties Properties Requis. Correspondance clé de propriété → définition de propriété. Chaque clé devient une colonne de base de données.
relations Relation[] Relations SQL — clés étrangères, tables de jonction. Voir Relations.
securityRules SecurityRule[] Politiques de sécurité au niveau des lignes (Row Level Security). Voir Règles de sécurité.
indexes CollectionIndex[] Index Postgres nécessaires à cette table. Voir Index.
search SearchConfig Recherche plein texte classée sur les champs que vous nommez, y compris le contenu JSONB et les tableaux. Postgres uniquement. Voir Recherche.
auth boolean | AuthCollectionConfig Marquer la collection comme collection d’authentification (gestion des utilisateurs, réinitialisation de mot de passe, etc.)
schema string Schéma Postgres dans lequel réside la table — "public", "rebase", "auth". Vaut "public" par défaut.
disableDefaultPolicies boolean Supprimer les politiques de base injectées par le générateur — un SELECT admin/serveur, et sur une collection auth une lecture par soi-même plus un contrôle d’écriture réservé aux admins — et prendre l’entière responsabilité du RLS de cette collection. false par défaut. Voir Règles de sécurité.
softDelete boolean | { field?: string } Transforme un delete en horodatage et masque les lignes horodatées de chaque lecture. true utilise deletedAt ; la forme objet permet de renommer le champ. La collection doit déclarer elle-même cette propriété date. Postgres uniquement — voir Suppression réversible (Soft delete)
strictWrites boolean Rejette avec une erreur 400 toute écriture qui nomme un champ que cette collection ne déclare pas. true par défaut. Définissez-le à false uniquement lorsque la colonne existe réellement et n’est pas déclarée — renseignée par un déclencheur, ou introspectée plutôt que spécifiée.

Tous les éléments suivants vont dans admin.

Propriété Type Par défaut Description
defaultViewMode "list" | "table" | "cards" | "kanban" "list" Mode d’affichage par défaut
enabledViews ViewMode[] Les quatre intégrés plus chaque vue personnalisée déclarée (kanban seulement lorsqu’une propriété enum existe) Modes d’affichage disponibles
kanban KanbanConfig — Configuration Kanban (propriété de colonne). À toujours associer avec orderProperty — voir Modes d’affichage
orderProperty string — Clé de la propriété de type string contenant la clé d’ordre de glisser-déposer. Requis pour un tableau Kanban fonctionnel
openEntityMode "side_panel" | "full_screen" | "split" | "dialog" Selon la vue : "split" pour la liste, "full_screen" pour le tableau et les cartes, "side_panel" pour le kanban et les vues personnalisées Mode d’ouverture des entités pour modification
sideDialogWidth number | string — Largeur du panneau latéral (side dialog)
inlineEditing boolean true Activer l’édition en ligne dans la vue feuille de calcul
defaultSize "xs" | "s" | "m" | "l" | "xl" "m" Densité par défaut des vues liste, tableau et cartes (la hauteur de ligne du tableau)
pagination boolean | number true (50) Activer la pagination et/ou définir la taille de la page. false charge toutes les lignes ; en développement, la console avertit une fois lorsqu’il y en a plus de 10 000
listProperties string[] — Propriétés à afficher dans la vue en liste
propertiesOrder string[] — Ordre des colonnes dans la vue tableau
selectionEnabled boolean true Activer la sélection des lignes
hideFromNavigation boolean false Masquer dans la navigation latérale
defaultSelectedView string | function — Vue ou sous-collection ouverte par défaut

Dans admin, à l’exception de history, qui est une fonctionnalité backend et reste au niveau racine.

Propriété Type Par défaut Description
formAutoSave boolean false Sauvegarde automatique deux secondes après une modification, une fois le formulaire valide. Un nouveau formulaire n’est pas enregistré tant que rien n’y est saisi.
localChangesBackup "manual_apply" | "auto_apply" | false "manual_apply" Sauvegarde locale des modifications non enregistrées
hideIdFromForm boolean false Masquer l’ID de l’entité dans le formulaire
hideIdFromCollection boolean false Masquer la colonne ID dans le tableau
includeJsonView boolean true Proposer les valeurs brutes dans l’inspecteur d’enregistrement
history boolean false Suivre les modifications dans l’historique de l’entité
alwaysApplyDefaultValues boolean false Lorsqu’un enregistrement existant s’ouvre dans le formulaire, remplit avec le defaultValue déclaré de chaque propriété toute valeur que l’enregistrement laisse vide (null ou absente), de sorte que le prochain enregistrement l’écrive
previewProperties string[] — Propriétés à afficher dans les aperçus de référence
display EntityDisplay — Ce qui remplit chaque rôle d’affichage — voir Affichage de l’entité

Au niveau racine, car le backend les lit :

Propriété Type Description
callbacks CollectionCallbacks Hooks de cycle de vie (beforeSave, afterSave, beforeDelete, etc.)
childCollections () => CollectionConfig[] Les collections imbriquées sous une entité de celle-ci. Rempli lors de la normalisation à partir de ce avec quoi le pilote les exprime — des subcollections Firestore, une relation hasMany Postgres — un pilote personnalisé est donc la seule raison de le définir manuellement
dataSource string Quelle source de données enregistrée soutient cette collection (par défaut : celle sans nom)
engine string Le moteur sous-jacent — "postgres", "firestore", "mongodb". Résolu à partir de dataSource ; à définir uniquement pour surcharger
databaseId string Base de données ou schéma au sein du moteur
metadata Record<string, unknown> Tout ce dont votre propre code a besoin d’associer à une collection. Rebase ne le lit pas ; il survit sans modification à la sérialisation
ownerId string Formulaire d’administration uniquement — non appliqué par l’API ou la base de données. L’identifiant utilisateur que l’éditeur de collection attribue à une collection qu’il crée, et affiche à côté de son nom. Rien dans le traitement de la requête ne le consulte

subcollections et path ne concernent que les configurations de bases de données orientées documents — FirebaseCollectionConfig et, pour path, MongoDBCollectionConfig :

Propriété Type Description
subcollections () => CollectionConfig[] Firestore uniquement. Collections imbriquées sous chaque document. Une collection Postgres exprime la même chose avec une relation hasMany, qui remplit childCollections
path string Firestore et MongoDB uniquement. Le chemin ou le nom de collection au niveau du moteur, lorsqu’il diffère du slug

Et dans admin, car seul le panneau les affiche :

Propriété Type Description
admin.entityActions EntityAction[] Actions personnalisées sur les entités (archiver, publier, etc.)
admin.Actions React.ComponentType Composant d’actions de barre d’outils personnalisé
admin.entityViews EntityCustomView[] Onglets personnalisés dans la vue détaillée de l’entité
admin.additionalFields AdditionalFieldDelegate[] Colonnes calculées/virtuelles
admin.exportable boolean | ExportConfig Activer l’exportation de données
admin.components CollectionComponentOverrideMap Surcharges de composants d’interface utilisateur au niveau de la collection

L’écriture de l’un de ces six éléments au niveau racine génère une erreur au démarrage, accompagnée d’un message indiquant la clé et son nouvel emplacement.

Chaque surface qui affiche un enregistrement présente un sous-ensemble de six rôles : title, subtitle, image, status, date et tags. Une ligne de liste combine image + titre + sous-titre + statut + date, une carte est identique avec l’image au-dessus, un sélecteur de référence affiche titre + sous-titre, et un en-tête de page se limite au titre seul.

Chaque rôle est dérivé de vos propriétés, et chacun peut également être spécifié — sous la forme d’un chemin de propriété ou d’une fonction :

const exercises = defineCollection({
name: "Exercises",
slug: "exercises",
table: "exercises",
properties: {
name: { name: "Name", type: "string" },
cover: { name: "Cover", type: "string", storage: { storagePath: "covers/" } },
city: { name: "City", type: "string" }
},
admin: {
display: {
title: "name", // a property path
image: "cover",
subtitle: ({ entity }) => `in ${entity.values.city}` // computed
}
}
});

Tout ce que vous omettez conserve sa valeur dérivée ; spécifier un rôle n’implique donc pas de spécifier les six.

Un résolveur peut être async, ce qui permet à un rôle de lire une information que l’enregistrement ne contient pas directement — un document dans une sous-collection, une valeur issue d’une API :

admin: {
display: {
// The exercise's name lives one document down, per locale.
title: async ({ entity, context }) => {
const locale = await context.data.exercise_locales.get(`${entity.id}/de-DE`);
return locale?.exercise_title;
}
}
}

Pendant que la promesse est en cours, la surface affiche la valeur dérivée et la remplace par la valeur résolue dès qu’elle est disponible — un titre n’affiche jamais d’indicateur de chargement. Les résultats sont mis en cache par enregistrement et par rôle, et les requêtes simultanées pour une même paire partagent un seul appel, de sorte qu’une liste de cinquante lignes ne résout chaque ligne qu’une seule fois plutôt qu’à chaque rendu.

Renvoyez undefined lorsqu’un enregistrement n’a rien pour ce rôle ; la solution de repli (fallback) propre à la surface est mieux informée de ce qui doit y figurer à la place (un titre de section utilise le nom singulier de la collection, un lien utilise l’id). Un résolveur qui lève une exception est traité comme undefined et consigné dans les logs une seule fois — un titre qui ne peut pas être récupéré ne doit pas faire planter la ligne qui l’affiche.

Privilégiez un chemin chaque fois que la valeur se trouve sur l’enregistrement : un chemin conserve le rendu propre à la propriété, ainsi un statut énuméré reste un badge coloré et une date reste formatée, ce qu’un résolveur renvoyant une simple chaîne ne peut pas reproduire.

Lorsque display.title n’est pas défini, la propriété utilisée comme titre d’affichage de l’entité (aperçus, en-têtes) est résolue automatiquement :

  1. Si propertiesOrder est explicitement défini, la première propriété non-ID qui est de type relation ou string est choisie comme titre.
  2. Si aucun propertiesOrder n’est défini, le framework parcourt les propriétés dans l’ordre et sélectionne la première propriété de type string.

Lorsque propertiesOrder est explicitement défini, les propriétés de relation ne sont pas automatiquement filtrées des colonnes d’aperçu par défaut (alors qu’elles sont exclues des valeurs par défaut non ordonnées pour éviter des opérations de jointure lentes).

Quel que soit le contenu de la propriété de titre, le panneau affiche une chaîne de caractères. Une date est formatée, un tableau est joint, et pour une relation — qui arrive sous la forme { id, data: { values } } plutôt que sous forme de texte — le panneau recherche le premier champ parmi name, title, label ou displayName sur la ligne liée, en se rabattant sur son id à défaut. Ainsi, un titre peut désigner une propriété de type relation tout en s’affichant sous la forme d’un nom plutôt que d’un uuid.

Il ne s’agit pas d’un helper exporté : c’est le comportement standard de chaque surface affichant un enregistrement. Rien à appeler, et rien à importer.

Pour les collections dynamiques qui changent en fonction de l’utilisateur ou de données externes, utilisez une fonction builder :

const collectionsBuilder: CollectionConfigsBuilder = ({ user, authController }) => {
const collections = [productsCollection];
// `extra` is whatever your auth provider put there, so name its shape here.
const extra = authController.extra as { role?: string };
if (extra.role === "admin") {
collections.push(adminSettingsCollection);
}
return collections;
};

Vous pouvez définir des filtres par défaut ou forcés. Tous trois relèvent de la présentation — ce avec quoi le panneau s’ouvre — ils résident donc dans admin :

import { defineCollection } from "@rebasepro/cms-types";
const invoices = defineCollection({
slug: "invoices",
name: "Invoices",
table: "invoices",
properties: {
active: { name: "Active", type: "boolean" },
tenantId: { name: "Tenant", type: "string" },
createdAt: { name: "Created", type: "date" }
},
admin: {
// Default filter — users can change it
defaultFilter: { active: ["==", true] },
// Fixed filter — cannot be changed; filters the user applies
// on other fields combine with it
fixedFilter: { tenantId: ["==", currentTenantId] },
// Default sort
sort: ["createdAt", "desc"]
}
});

L’utilisateur peut tout de même filtrer une collection dotée d’un fixedFilter : ce qu’il applique sur d’autres champs s’y combine (ET), et les champs fixes restent verrouillés.

Un fixedFilter restreint ce que le panneau demande ; ce n’est pas une barrière de sécurité. Ce qu’un appelant est autorisé à lire est régi par une règle de sécurité, que la base de données applique à chaque appelant, qu’il s’agisse du panneau ou non.

Rien dans Rebase ne plafonne le nombre de collections, mais deux coûts augmentent avec lui :

  • Le premier téléchargement de l’admin. Le panneau a besoin de sa navigation dès que quelqu’un se connecte ; la configuration de chaque collection est donc incluse dans ce que le navigateur récupère pour l’écran de connexion — environ 2,4 Ko minifiés chacune, si bien que 200 collections ajoutent environ 480 Ko, soit environ 80 Ko en gzip. Ce dont l’ouverture d’une collection a besoin (le tableau, les vues d’enregistrement, le formulaire) ne se charge qu’à ce moment-là.
  • rebase db push. Atlas planifie un push en comparant le schéma entier, et cela croît plus vite que le nombre de tables : à 400 tables ou plus, un push prend des dizaines de secondes — voir combien de temps prend un push.