Modifica dello schema live
L’editor dello schema nel pannello di amministrazione riscrive il codice sorgente delle tue collection. Questo funziona sulla tua macchina e da nessun’altra parte: i file di un server distribuito vengono ricostruiti dal repository a ogni deploy, quindi una modifica effettuata lì verrebbe scartata a quello successivo.
La modifica dello schema live è la risposta a questo problema. Essa esegue il commit della modifica nel tuo repository, quindi applica il DDL — così la modifica sopravvive al deploy successivo, poiché quest’ultimo viene generato a partire da essa.
GET /api/admin/schema/status whether this backend can do it, and whether you mayPOST /api/admin/schema/plan what would happen, without doing itPOST /api/admin/schema/apply commit, then applyStatus e plan richiedono lo scope schema:read, e apply richiede schema:write, come qualsiasi altra interfaccia di /api/admin che indica uno scope. Un amministratore li possiede entrambi. L’applicazione richiede inoltre un elemento in più oltre allo scope — consulta Chi può applicare.
Pianifica prima di applicare
Sezione intitolata “Pianifica prima di applicare”/plan non ha effetti collaterali. Invia tramite POST la modifica, e ti
indicherà cosa comporta. Una modifica a una collection esistente è una patch — ciò che è
cambiato, come operazioni su percorsi di chiavi — mentre una nuova collection è
l’intero collection:
{ "collectionId": "posts", "patch": [ { "op": "set", "path": ["properties", "subtitle"], "value": { "name": "Subtitle", "type": "string" } }, { "op": "remove", "path": ["admin", "group"] }] }Solo le chiavi che una patch nomina vengono scritte nel file della collection.
Tutto il resto resta com’è — import, commenti, formattazione, l’onClick di
un’azione sull’entità, una proprietà condivisa da un altro modulo, un enum
importato altrove. Una patch che si estende dentro qualcosa definito nel
codice (status: statusProperty, enum: LOCALE_ENUM, uno ...spread) viene
rifiutata con l’espressione su cui si è imbattuta, in modo che la modifica
venga fatta dove risiede quel codice. Un intero collection inviato per una
collection già esistente viene trasformato nella patch di ciò che differisce
da essa, e una chiave il cui valore è codice non viene mai rimossa in questo
modo. Il pannello di amministrazione invia patch. Sulla 0.23 /plan e
/apply accettano solo l’intero collection come dovrebbe risultare alla
fine.
$ADMIN_TOKEN è un token di accesso — l’accessToken restituito da un accesso — per un account che possiede schema:read: un amministratore, o un ruolo che lo dichiara. Nulla sulla macchina lo imposta per te.
curl -X POST https://your-app/api/admin/schema/plan \ -H "authorization: Bearer $ADMIN_TOKEN" \ -H "content-type: application/json" \ -d '{"collectionId":"posts","collection":{}}'{ "applicable": true, "verdict": "safe", "changes": [ { "kind": "add-property", "verdict": "safe", "collection": "posts", "property": "subtitle", "detail": "New optional property subtitle …" } ], "statements": ["ALTER TABLE \"public\".\"posts\" ADD COLUMN IF NOT EXISTS \"subtitle\" TEXT;"], "files": ["config/collections/posts.ts", "backend/src/schema.generated.ts"]}files è ogni file che l’apply include nel commit: il sorgente della collezione, quando il backend può scriverlo, poi lo schema generato.
Non si tratta di una comodità superflua. Due dei tre verdetti sono rifiuti, e uno di essi è un rifiuto che altrimenti scopriresti solo premendo il pulsante su un database attivo.
I tre verdetti
Sezione intitolata “I tre verdetti”| Verdetto | Significato |
|---|---|
safe |
Il percorso di ensure all’avvio è in grado di esprimerlo e il risultato corrisponde alla tua configurazione. Applicato. |
diverges |
Si applica in parte, lasciando un database che non corrisponde alla tua configurazione — e nulla lo segnala. Rifiutato. |
needs-migration |
Il percorso di ensure non è affatto in grado di esprimerlo. Rifiutato. |
diverges è quello che vale la pena comprendere a fondo, perché queste modifiche sembrano essere andate a buon fine:
- Una proprietà obbligatoria aggiunta a una tabella che contiene già righe arriva come nullable.
NOT NULLviene verificato a fronte di ogni riga già presente, e le righe scritte prima che la proprietà esistesse non hanno alcun valore per essa. Su una tabella vuota non c’è nulla da verificare, quindi il vincolo viene applicato ed è consideratosafe. - Rendere obbligatoria una proprietà esistente ha la stessa logica:
SET NOT NULLscansiona la tabella, quindi èsafesu una tabella vuota edivergessu una popolata finché non esegui un backfill.
Due modifiche che in passato risultavano diverges ora sono safe, poiché il percorso di ensure le esegue:
- L’aggiunta di un valore a un enum esistente va a buon fine, tramite
ALTER TYPE … ADD VALUE IF NOT EXISTS. In precedenza veniva ignorata insieme all’intero tipo, e la prima riga che utilizzava il nuovo valore veniva rifiutata da un tipo che non lo conosceva. - Rendere facoltativa una proprietà obbligatoria rimuove il vincolo
NOT NULL. In precedenza veniva lasciato invariato, quindi le scritture che omettevano la proprietà continuavano a fallire.
Vincoli richiesti ma non applicati
Sezione intitolata “Vincoli richiesti ma non applicati”Una modifica può essere applicabile e lasciare comunque non applicato qualcosa richiesto dalla tua configurazione — come nel caso di una proprietà obbligatoria su una tabella popolata. Questo non costituisce un rifiuto, pertanto non compare in changes; appare invece in withheldConstraints, con l’ostacolo e ciò che lo risolverebbe:
{ "withheldConstraints": [ { "target": "public.posts.author", "kind": "not-null", "reason": "\"author\" is required, but \"public.posts\" already holds rows …", "remedy": "Backfill the column, then apply this again." } ]}Il percorso di ensure all’avvio segnala la stessa cosa come avviso. Prima dell’introduzione di questa funzionalità, un vincolo trattenuto veniva omesso in silenzio.
needs-migration copre tutto ciò che il percorso di ensure non può fare:
eliminare una collection o una proprietà, modificare il tipo di una colonna
(un toggle su un intero, una stringa che diventa un enum, il tipo di elemento
di un array, la larghezza di un varchar), rinominare una colonna, cambiare una
chiave primaria, rimuovere un valore di enum, rendere univoca una colonna
esistente, e modificare una relazione — il suo kind, il suo target, il suo
localKey, il suo onDelete. Viene rifiutata anche una hasMany o hasOne
la cui colonna di collegamento non è creata da nulla. Ogni rifiuto specifica
la modifica e cosa fare in alternativa.
Il verdetto viene letto dallo schema che ciascun lato produce — lo stesso
piano a partire dal quale vengono generati schema.generated.ts e db push
— quindi una modifica che cambia il database non può essere segnalata come
nessuna modifica. Due modifiche che sembrano cambiamenti e non vengono
rifiutate (; la 0.23 segnala entrambe come richiedenti una migrazione):
- Rinominare la chiave di una proprietà mantenendo la sua colonna
(
columnNameimpostato sul vecchio nome della colonna) non sposta alcun dato. Èsafe; i client dell’API leggono il nuovo nome. - Impostare, modificare o eliminare un default vincola solo le scritture
future. È
safe, e viene applicato conALTER COLUMN … SET DEFAULT/DROP DEFAULT.
Modificare solo la sorgente
Sezione intitolata “Modificare solo la sorgente”Una modifica rifiutata può comunque essere scritta nella sorgente della tua
collection e committata, lasciando il database com’è — rimuovere una
proprietà che non servi più è il caso tipico. Invia /apply con "sourceOnly": true. Non viene eseguito nulla; il
messaggio di commit specifica cosa mantiene il database, ad esempio
chore(schema): remove sku from products (source only — column products.sku kept),
e ogni modifica nel piano porta una frase sourceOnly che indica cosa lascia
indietro — compreso il caso in cui una colonna lasciata indietro è NOT NULL
senza default, il che fa fallire ogni inserimento successivo finché non viene
eliminata o resa nullable. Una modifica senza una frase di questo tipo
(spostare una chiave primaria, una relazione la cui colonna di collegamento
non è creata da nulla) non può essere scritta nella sola sorgente.
L’eliminazione di una collection dal pannello di amministrazione segue lo
stesso percorso: /apply con "remove": true e "sourceOnly": true elimina
il file della collection e la sua voce in index.ts e committa entrambi; la
tabella e le sue righe restano. Viene rifiutata mentre un’altra collection
importa il file (una relazione verso di essa), nominando l’importatore —
eliminarlo interromperebbe il caricamento di ogni collection.
Sulla 0.23 /apply non accetta né sourceOnly né remove, e “Edit source
only” nel pannello scrive il file senza eseguire un commit.
Cosa viene committato
Sezione intitolata “Cosa viene committato”Non solo il file della collection. Da esso viene generato lo schema Drizzle, e uno schema non aggiornato compromette il deploy successivo, quindi entrambi finiscono nello stesso commit:
config/collections/<name>.ts— la collection stessabackend/src/schema.generated.ts— lo schema Drizzle
Questi percorsi sono relativi al tuo progetto, non al tuo repository. Quando i due coincidono — un progetto rebase init, che rappresenta il caso comune — non c’è nulla di cui preoccuparsi. Quando il progetto si trova in una sottodirectory di un repository più grande, i percorsi includono il relativo prefisso, individuato risalendo dalla directory delle collection fino al rebase.json più vicino. Un progetto senza rebase.json mantiene i percorsi semplici.
Nel commit non finisce alcun SQL. rebase db push e rebase db generate scrivono il proprio a partire dalle collection a ogni esecuzione, in .rebase/sql/, che è in gitignore.
Sulla 0.23 il commit porta anche drizzle/schema.sql, drizzle/policies.sql e
drizzle/search.sql, scritti nella root del progetto.
Il messaggio di commit descrive la modifica piuttosto che limitarsi ad annunciarne una, ed è attribuito alla persona che l’ha effettuata, con il nome visualizzato e l’email
dell’account (un utente senza nessuno dei due viene registrato come <uid>@users.noreply.rebase.pro).
Da 0.25 Su 0.24 i commit
dell’autenticazione integrata riportano l’uid come nome.
Una modifica dello schema con un autore e un diff nella cronologia del tuo progetto è qualcosa che né Firebase né Supabase offrono: le modifiche alle loro tabelle sono invisibili al tuo repository.
Chi può applicare
Sezione intitolata “Chi può applicare”Possedere schema:read è sufficiente per eseguire plan. La pianificazione non ha effetti collaterali, e un job CI che verifichi se una modifica proposta per una collection sia applicabile ne rappresenta un ottimo caso d’uso.
L’applicazione è un privilegio distinto, poiché applicare scrive un commit e un commit reca con sé un autore:
| Chiamante | Plan | Apply |
|---|---|---|
Una persona autenticata che possiede schema:write |
sì | sì |
Una chiave API che possiede schema:read / schema:write |
sì | no |
| La service key del server | sì | no |
Una credenziale non è un autore. api-key:7c3f… nel tuo ambiente di CI non è una persona fisica, e consentirle di scrivere nel repository produrrebbe esattamente quella cronologia non attribuibile che questa funzionalità è stata concepita per rimpiazzare.
Sulla 0.23 la linea di confine è il ruolo admin: un admin pianifica e
applica, e qualsiasi chiave API può pianificare.
Se ciò che desideri è una modifica automatizzata dello schema — ad esempio una pipeline di migrazione — attivala esplicitamente:
initializeRebaseBackend({ // …the rest of your config liveSchema: { allowMachineApply: true }})oppure REBASE_LIVE_SCHEMA_ALLOW_MACHINE_APPLY=true. Il commit viene quindi attribuito alla credenziale per nome — Rebase API key (7c3f) — in modo tale che, leggendo il git log a distanza di un mese, sia ancora possibile distinguere le modifiche apportate da una persona.
GET /api/admin/schema/status segnala ciò che tu sei autorizzato a fare, non soltanto ciò che il server supporta, consentendo così a un pannello di disabilitare il controllo spiegandone il motivo, invece di rifiutare l’operazione dopo che hai già deciso di procedere:
{ "enabled": true, "canPlan": true, "canApply": false, "applyRefusedCode": "SCHEMA_EDIT_REQUIRES_A_PERSON", "applyRefusedBecause": "This request is authenticated with an API key …"}Se il tuo progetto gestisce migrazioni con controllo di versione
Sezione intitolata “Se il tuo progetto gestisce migrazioni con controllo di versione”L’applicazione in questo contesto non scrive una migrazione, e non può farlo: una migrazione adotta il formato di Atlas con un file di integrità, generato da un binario esterno a fronte di un database usa e getta, e un server in esecuzione non dispone di nessuno dei due.
Ciò che invece committa è la collection, ed è a partire da essa che rebase db generate scrive la migrazione. Di conseguenza, la migrazione è a portata di un solo comando:
rebase db generateSia il plan sia il risultato lo segnalano esplicitamente quando il progetto prevede migrazioni, poiché in caso contrario il problema passerebbe inosservato: il database conterrebbe la modifica e il repository la descriverebbe, ma l’ambiente successivo configurato rieseguendo le migrazioni non la conterrebbe, senza alcun avviso.
Un progetto configurato tramite boot-ensure — il runtime gestito e qualsiasi self-host che mantenga REBASE_MIGRATE_ON_BOOT sul valore predefinito — non necessita di alcuna migrazione. Le sue collection costituiscono lo schema, e l’avvio successivo si occupa della riconciliazione.
Prima il commit, poi l’applicazione
Sezione intitolata “Prima il commit, poi l’applicazione”L’ordine è importante e non è arbitrario.
Se il DDL venisse eseguito per primo e il commit fallisse, il database avrebbe una colonna non descritta dal repository. Il percorso di ensure non elimina mai nulla, quindi il deploy successivo non la rimuoverebbe né la segnalerebbe — una colonna invisibile, assente dalle tue collection, finché qualcuno non andasse a cercarla.
Eseguire prima il commit fallisce nel modo opposto: il repository descrive qualcosa che il database non possiede ancora. Questo è lo stato ordinario di ogni progetto nell’intervallo tra una modifica e un deploy, e la fase di boot lo riconcilia all’avvio successivo.
Pertanto, un’applicazione fallita non è un errore. La risposta lo indica chiaramente:
{ "applied": false, "applyError": "connection refused", "committed": { "sha": "1a2b3c4de", "branch": "main" }, "summary": "Committed 1a2b3c4de on main, but the database was not changed. The change will be applied on the next boot."}Dove funziona
Sezione intitolata “Dove funziona”La modifica live dello schema riscrive il sorgente delle tue collection con lo stesso editor
usato dai salvataggi del pannello che toccano solo il sorgente (richiede che ts-morph sia
installato). Da dove arriva quel sorgente decide dove funziona:
- Su disco (
collectionsDir, l’impostazione predefinita): funziona dove funziona l’editor che tocca solo il sorgente. Quell’editor è disattivato conNODE_ENV=productiona meno che il server non impostischemaEditor: true. - In un repository che indichi (
liveSchema.repository, oppureREBASE_LIVE_SCHEMA_REPOSITORYper un bundle): il sorgente viene letto da GitHub e la modifica viene committata di nuovo lì, quindi funziona ovunque, produzione compresa. È l’opt-in per un bundle compilato.
MongoDB
Sezione intitolata “MongoDB”Tutto quanto descritto sopra riguarda Postgres, dove una modifica dello schema si traduce in istruzioni DDL. Su MongoDB non ci sono tabelle da alterare: l’aggiunta di una proprietà non aggiunge nulla, la sua rimozione non rimuove nulla, e un documento scritto ieri rimane valido anche domani.
Di conseguenza, ogni modifica è applicabile, nulla viene mai rifiutato e il piano non contiene istruzioni — il commit è la modifica stessa. Il pannello mostra “Commit” anziché “Commit and apply”, e non asserisce che sia stato eseguito alcunché sul database.
L’unico aspetto che merita un’attenta lettura è la rimozione. Su Postgres la rimozione di una proprietà viene rifiutata perché comporterebbe l’eliminazione di una colonna. Su MongoDB il campo rimane in ogni documento che lo contiene; la tua API smette semplicemente di restituirlo. La modifica lo esplicita chiaramente, evitando di lasciare presumere il comportamento tipico dei database relazionali.
| Distribuzione | Funziona |
|---|---|
rebase dev sulla tua macchina |
sì |
| Self-host con il progetto e il suo working tree git montati | sì, quando NODE_ENV non è production, oppure con schemaEditor: true |
| Un bundle compilato: un self-host che serve una build, oppure l’immagine pubblicata | solo se fa opt-in con REBASE_LIVE_SCHEMA_REPOSITORY — vedi Da un bundle compilato |
Qualsiasi server con liveSchema.repository configurato |
sì, NODE_ENV=production compreso |
Un bundle è un output compilato, quindi non contiene il sorgente delle collection, e il
runtime che lo serve disattiva l’editor che tocca solo il sorgente. Se non indica un
repository, /api/admin/schema/status risponde enabled: false con il codice
SCHEMA_EDITOR_DISABLED: modifica le collection in sviluppo, fai il commit e distribuisci.
Da un bundle compilato
Sezione intitolata “Da un bundle compilato”Un bundle non ha codice proprio da configurare, quindi fa opt-in con variabili d’ambiente. Nulla cambia per un bundle che non le imposta.
REBASE_LIVE_SCHEMA_REPOSITORY=acme/storefront # turns it onREBASE_LIVE_SCHEMA_GITHUB_TOKEN=github_pat_… # or the three _GITHUB_APP_* variables# REBASE_LIVE_SCHEMA_BRANCH=main # default# REBASE_LIVE_SCHEMA_COLLECTIONS_PATH=config/collections # defaultL’elenco completo è in Configurazione.
Un repository indicato senza una credenziale utilizzabile (nessuna, entrambi i tipi, metà di una
GitHub App), o un valore che non è owner/name, si rifiuta di avviarsi, anziché servire
un editor che fallisce quando qualcuno preme Applica.
Cosa resta uguale quando è attivo:
- È una superficie
/api/admin: nessuna risposta senzaschema:read, e/applyrichiedeschema:writee una persona (vedi Chi può applicare). - L’unica destinazione di scrittura è il repository che hai indicato. L’editor che tocca solo il sorgente, che scrive file accanto al processo, resta disattivato.
- Ogni modifica viene pianificata, mostrata, committata e poi applicata, esattamente come sulla tua macchina.
Una cosa è diversa. Il deployment in esecuzione continua a servire le collection con cui è stato compilato finché la tua pipeline non compila e distribuisce il commit. Il database ha la modifica subito; il pannello di amministrazione e l’API vedono la nuova definizione della collection dopo quel deploy.
Quindi un deployment fa una sola modifica per deploy. Dopo che un commit è stato scritto, la
successiva applicazione viene rifiutata con SCHEMA_CHANGE_FAILED, indicando il commit, finché un processo
che esegue quel commit non sostituisce questo. Una seconda modifica pianificata sulle
collection che questo deployment esegue ancora farebbe il commit di file di schema generati
che escludono la prima. La pianificazione resta disponibile nel frattempo.
Fare il commit su un repository remoto
Sezione intitolata “Fare il commit su un repository remoto”liveSchema.repository fa sì che il server faccia il commit di ogni modifica su un
repository GitHub tramite le Git Data API, invece di eseguire git commit in un
working tree. Non dipende dal fatto che l’editor che tocca solo il sorgente sia attivo: con un
repository configurato, la modifica live funziona con NODE_ENV=production e con
schemaEditor: false, perché nulla su questa macchina viene scritto. Un bundle
lo imposta tramite le variabili sopra.
initializeRebaseBackend({ // …the rest of your config liveSchema: { repository: { kind: "github", owner: "acme", repo: "storefront", branch: "main", // Where the collection source lives in that repository. // Defaults to "config/collections". collectionsPath: "config/collections", auth: { kind: "token", token: process.env.GITHUB_TOKEN! } } }})La directory delle collection viene quindi letta dal repository, riscritta con lo stesso editor utilizzato in locale e ciò che è cambiato viene committato nuovamente tramite le Git Data API: un blob per file, un tree, un commit e l’aggiornamento di un ref. Nulla viene clonato e nulla viene lasciato su disco. Viene letta l’intera directory, non solo il file modificato, quindi una relazione può avere come destinazione un’altra collection (il suo file viene importato) e l’eliminazione di una collection funziona come in locale: rifiutata finché un’altra collection la importa, e rimossa da index.ts nello stesso commit.
auth accetta un token o un’installazione di una GitHub App:
auth: { kind: "app", appId: "123456", privateKey: process.env.GITHUB_APP_PRIVATE_KEY!, installationId: "987654"}Utilizza il token per un singolo progetto che esegue commit su un repository già di tua proprietà — configurare un’App solo per permettere al tuo server di committare richiede troppa complessità burocratica per una credenziale su una sola riga. Utilizza invece l’App per un control plane che gestisce un’unica chiave condivisa tra più progetti, esattamente come fa Rebase Cloud: una sola App, un’installazione per ciascun progetto e nessun segreto specifico per singolo cliente da ruotare.
Il token richiede il permesso contents: read and write su quel repository, e nient’altro.
Senza liveSchema.repository, il commit è un semplice git commit nel working tree che contiene collectionsDir: nulla da autenticare, nessun token, nessuna rete.
Due aspetti rendono sicura l’esecuzione a fronte di un repository su cui sta lavorando qualcun altro:
- Esegue lo stage dei soli file che ha generato. Un commit dello schema che inglobasse lavori a metà sarebbe impossibile da revisionare; pertanto, l’operazione viene categoricamente rifiutata se nell’albero risulta già modificato uno dei propri file.
- Il percorso remoto non esegue mai un aggiornamento forzato di un ref. Se qualcosa è stato inviato mentre il commit veniva generato, l’aggiornamento viene rifiutato: perdere silenziosamente il commit di qualcuno è peggio di un fallimento dell’operazione.
Limitazioni
Sezione intitolata “Limitazioni”- Solo modifiche additive. Tutto il resto viene rifiutato con una motivazione, poiché il percorso di ensure è l’unico elemento a modificare uno schema e può eseguire solo aggiunte.
- Non viene scritto alcun file di migrazione. Un progetto configurato tramite boot-ensure non ne ha bisogno; un progetto configurato con migrazioni dovrebbe eseguire
rebase db generate, che ne genera uno tramite Atlas con l’hash di integrità richiesto da Atlas. - Solo Postgres. La funzionalità viene rilevata sul driver, e gli altri motori rispondono con
SCHEMA_EDITING_UNSUPPORTED.
Correlati
Sezione intitolata “Correlati”- Generazione dello schema — le stesse modifiche da riga di comando
- Definizione delle collection — ciò che l’editor sta riscrivendo
- Studio — il pannello accessibile dietro queste route