Migrating from pre-v1.0 ¶
If you’ve been running a pre-v1.0 internal install, here’s what changes and how to update.
What’s removed entirely ¶
- The
GitSheetsclass (backend/lib/GitSheets.js) — early-prototype API. Replaced byRepository+Sheet+Transaction. - The HTTP server (
backend/server.js) — Koa-based REST surface. The library no longer ships an HTTP layer; build your own with Fastify / Koa / Express / Hono and call the JS API. - The Vue frontend (
src/*Vue,tests/e2eCypress) — demo UI. If you need a UI, build it as a separate consumer. - CSV-ingest commands (
backend/commands/singer-target.js) — Singer target. Not in the v1.0 surface. deepmergefor--patch-existing— replaced by RFC 7396 JSON Merge Patch (Sheet.patch(query, partial)).
What’s reshaped ¶
gitsheetsis now a single npm package at the repo root (wasbackend/).- TypeScript-first, ESM-only.
"type": "module". Node ≥ 20 or Bun ≥ 1. [gitsheet.fields]config moves to[gitsheet.schema](JSON Schema). Thesortrules stay where they are. See the migrating-config recipe.- Errors are typed classes. No more string-matching on
err.message. Seeapi/errors.mdfor the hierarchy + stable codes.
API translation table ¶
| Pre-v1.0 | v1.0 |
|---|---|
new GitSheets({ gitDir }) |
await openRepo({ gitDir }) |
gitsheets.getSheet(name) |
await repo.openSheet(name) |
sheet.upsert(record) |
sheet.upsert(record) (same name, now goes through a transaction) |
sheet.delete(query) |
sheet.delete(query) (same name) |
sheet.query(filter) |
sheet.query(filter) async iterator (same name) |
sheet.commit(message) |
repo.transact({ message }, async tx => { await tx.sheet(...).upsert(...) }) |
--patch-existing (CLI, deepmerge) |
--patch (CLI, RFC 7396 — semantic change: null deletes, arrays replace) |
HTTP POST /sheets/:name |
Your own HTTP layer + sheet.upsert(...) |
Permissive vs strict mode ¶
Pre-v1.0, every upsert mutated the working tree and required an explicit commit() to make changes durable. v1.0 inverts this:
- Permissive default — standalone
sheet.upsert(record)auto-opens a transaction with an auto-generated commit message. Simple scripts just work. - Strict mode — call
repo.requireExplicitTransactions()to require every write to go throughrepo.transact. Useful when you want every commit to carry intentional metadata (author, message, trailers).
// Permissive (default)
await sheet.upsert({ slug: 'jane', email: 'jane@x.org' });
// → commits with message "users upsert"
// Strict
repo.requireExplicitTransactions();
await sheet.upsert({ slug: 'jane', email: 'jane@x.org' });
// ✗ throws TransactionError('transaction_required')
await repo.transact({ message: 'add jane' }, async (tx) => {
await tx.sheet('users').upsert({ slug: 'jane', email: 'jane@x.org' });
});
// ✓
For production / request-bound flows, prefer strict mode + explicit repo.transact — see the request-bound transactions recipe.
A worked translation ¶
Pre-v1.0 ¶
const { GitSheets } = require('gitsheets');
const gs = new GitSheets({ gitDir: '/data/.git' });
await gs.init();
const users = await gs.getSheet('users');
await users.upsert({ slug: 'jane', email: 'jane@x.org' });
await users.upsert({ slug: 'bob', email: 'bob@x.org' });
await users.commit('add users');
const found = await users.queryFirst({ slug: 'jane' });
v1.0 ¶
import { openRepo } from 'gitsheets';
const repo = await openRepo({ gitDir: '/data/.git' });
const users = await repo.openSheet('users');
await repo.transact(
{ message: 'add users' },
async (tx) => {
const s = tx.sheet('users');
await s.upsert({ slug: 'jane', email: 'jane@x.org' });
await s.upsert({ slug: 'bob', email: 'bob@x.org' });
},
);
const found = await users.queryFirst({ slug: 'jane' });
Differences:
- ESM
importinstead of CommonJSrequire openRepo(factory) instead ofnew GitSheets()+await gs.init()openSheetinstead ofgetSheet(factory consistency)- Bundle multiple upserts into one transaction with
repo.transact— same shape, atomic, one commit queryFirstis unchanged
Validation reshape ¶
If you had [gitsheet.fields] configs, see the migrating-config recipe for the field-by-field migration. The short story:
type/enum/defaultmove to[gitsheet.schema.properties.<name>]sortstays at[gitsheet.fields.<name>.sort](different concept — canonical normalization)trueValues/falseValuesmove to your CSV ingest code (no longer a sheet-config concern)
Error handling ¶
Pre-v1.0 you might have done:
try {
await users.upsert(record);
} catch (err) {
if (err.message.startsWith('invalid tree ref')) { /* ... */ }
}
v1.0:
import { GitsheetsError, ValidationError, RefError } from 'gitsheets';
try {
await users.upsert(record);
} catch (err) {
if (err instanceof ValidationError) {
// err.issues — structured ValidationIssue[]
} else if (err instanceof RefError) {
// err.code === 'ref_not_found' | 'not_an_ancestor'
} else if (err instanceof GitsheetsError) {
// err.code — stable string
// err.status — HTTP status hint
} else {
throw err;
}
}
Every gitsheets error extends GitsheetsError and carries a stable code. See specs/api/errors.md for the full hierarchy + code table.
CLI changes ¶
git sheet upsert/query/read/normalize— same command shape, rebuilt against the new coregit sheet edit <sheet> <path>— new in v1.1:$EDITOR-based round-trip on a recordgit sheet init <sheet>— new in v1.1: scaffold a starter.gitsheets/<sheet>.tomlgit sheet infer <sheet>/migrate-config <sheet>— new in v1.1: schema inference and pre-v1.0 fields-config migration--patch-existing→--patch(RFC 7396 semantics:nulldeletes, arrays replace)--format(json|toml|csvupsert;json|csv|tsv|tomlquery/read),--encoding,--delete-missing,--attachment <name>=<source>— all shipped in v1.1--prefix(GITSHEETS_PREFIX) for multi-tenant sub-tree scoping- New global flags:
--message,--author-name,--author-email,--trailer Key=Value,--ref,--commit-to - Exit codes are stable from v1.0 onward — see CLI reference
--working(read/write the working tree state) remains deferred — tracked at #165
Going forward ¶
Once migrated, the recipes are the fastest path to common patterns:
If you hit something the migration guide doesn’t cover, open an issue — likely a missing migration note, possibly an actual gap.