Skip to content

Commit 301a1e4

Browse files
Merge pull request #166 from JarvusInnovations/develop
Release: v1.1.0
2 parents 846b305 + e278740 commit 301a1e4

33 files changed

Lines changed: 3540 additions & 188 deletions

docs/api.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ import type {
3333
OpenRepoOptions, OpenSheetOptions, OpenSheetsOptions,
3434
TransactionOptions, TransactionResult, TransactionHandler, Author,
3535
SheetConfig, SheetFieldConfig, SortRule, UpsertResult,
36-
SheetConstructorOptions, IndexKeyFn, DefineIndexOptions, QueryFilter,
36+
SheetConstructorOptions, IndexKeyFn, DefineIndexOptions, QueryFilter, QueryOptions,
37+
DiffStatus, DiffOptions, DiffChange,
38+
AttachmentBlobHandle, AttachmentEntry,
3739
OpenStoreOptions, Store, StoreTx, StoreTransactFn, ValidatorMap, InferRecord,
38-
PushDaemonOptions, PushDaemonStatus, BackoffConfig,
40+
PushDaemonOptions, PushDaemonStatus, PushFailureReason, BackoffConfig,
3941
JSONSchema, StandardSchemaV1, ValidationIssue, StandardSchemaResult,
4042
RecordLike, PathTemplateBlob, PathTemplateTree, PathTemplateQueryResult,
4143
} from 'gitsheets';
@@ -89,9 +91,9 @@ const store = await openStore(repo, {
8991

9092
| Method | Returns |
9193
| --- | --- |
92-
| `sheet.query(filter?)` | `AsyncGenerator<T>` — iterator |
93-
| `sheet.queryFirst(filter?)` | `Promise<T \| undefined>` |
94-
| `sheet.queryAll(filter?)` | `Promise<T[]>` |
94+
| `sheet.query(filter?, opts?)` | `AsyncGenerator<T>` — iterator; `opts.signal` for AbortSignal cancellation |
95+
| `sheet.queryFirst(filter?, opts?)` | `Promise<T \| undefined>` — honors `opts.signal` |
96+
| `sheet.queryAll(filter?, opts?)` | `Promise<T[]>` — honors `opts.signal` |
9597
| `sheet.pathForRecord(record)` | `Promise<string>` — rendered path, no write |
9698
| `sheet.normalizeRecord(record)` | `Promise<T>` — canonical form, no write |
9799

@@ -115,6 +117,15 @@ All write methods route through a transaction — permissive mode auto-opens one
115117
| `sheet.getAttachments(record)` | Map of name → BlobObject |
116118
| `sheet.setAttachment(record, name, blob)` | Add or replace |
117119
| `sheet.setAttachments(record, map)` | Bulk variant |
120+
| `sheet.deleteAttachment(record, name)` | Remove a single attachment; throws `NotFoundError` if missing |
121+
| `sheet.deleteAttachments(record)` | Remove all attachments; idempotent no-op when no attachment dir |
122+
| `sheet.attachments(record)` | `AsyncGenerator<AttachmentEntry>` yielding `{name, mimeType, blob}` with `.read()` / `.stream()` |
123+
124+
### Diff
125+
126+
| Method | Returns |
127+
| --- | --- |
128+
| `sheet.diffFrom(srcCommitHash?, opts?)` | `AsyncGenerator<DiffChange<T>>` — per-record changes since `srcCommitHash` (defaults to the empty tree). `opts.blobs` / `opts.records` / `opts.patches` attach hologit blob handles, parsed src/dst records, and RFC 6902 JSON Patches respectively. Throws `RefError` if `srcCommitHash` doesn't resolve. |
118129

119130
### Indexing
120131

@@ -173,15 +184,17 @@ Returned from `repo.startPushDaemon(opts)`. EventEmitter with:
173184
| Event | Payload |
174185
| --- | --- |
175186
| `push` | `{ commit, durationMs }` |
176-
| `retry` | `{ commit, attempt, nextDelayMs }` |
177-
| `error` | `{ commit, err, attempt }` |
187+
| `retry` | `{ commit, attempt, nextDelayMs, reason }` |
188+
| `error` | `{ commit, err, attempt, reason }``reason: 'non-fast-forward' \| 'unknown'`. NFF is terminal (no retry). |
178189
| `stopped` | (none) |
179190

180191
| Method | Purpose |
181192
| --- | --- |
182-
| `daemon.status()` | Snapshot: running, lastPushAt, lastError, pendingCommits, currentBackoffMs, currentAttempt |
193+
| `daemon.status()` | Snapshot: running, lastPushAt, lastError (with `reason`), pendingCommits, currentBackoffMs, currentAttempt |
183194
| `daemon.stop({ timeoutMs })` | Drain in-flight retries (graceful) |
184195

196+
On `startPushDaemon` the daemon also runs a one-shot startup-backlog check (fetch + `rev-list --count <remote>/<branch>..<branch>`) and queues any commits ahead of the remote; this is how a restarted daemon catches up.
197+
185198
[`specs/behaviors/push-sync.md`](https://github.com/JarvusInnovations/gitsheets/blob/develop/specs/behaviors/push-sync.md) · [production recipe](recipes/production-push-daemon.md)
186199

187200
## Errors
@@ -234,4 +247,4 @@ Read [`specs/api/conventions.md`](https://github.com/JarvusInnovations/gitsheets
234247

235248
## Stability
236249

237-
Everything documented in [`specs/`](https://github.com/JarvusInnovations/gitsheets/tree/develop/specs) (and re-exported from `gitsheets`) is stable from v1.0 forward. Internal modules under `dist/` that aren't re-exported are implementation details and may change in minor releases.
250+
Everything documented in [`specs/`](https://github.com/JarvusInnovations/gitsheets/tree/develop/specs) (and re-exported from `gitsheets`) is stable from v1.0 onward. Additions in minor versions are additive (new methods, new options). Internal modules under `dist/` that aren't re-exported are implementation details and may change in minor releases.

docs/cli.md

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ operation on a `Repository` (or `Sheet`/`Transaction`).
1212
| --- | --- | --- |
1313
| `--git-dir <path>` | discovered from cwd | `GIT_DIR` |
1414
| `--root <path>` | `/` | `GITSHEETS_ROOT` |
15+
| `--prefix <path>` | none | `GITSHEETS_PREFIX` |
1516
| `--ref <ref>` | `HEAD` | `GITSHEETS_REF` |
1617
| `--commit-to <ref>` | derived from `--ref` ||
1718
| `--message <msg>` | auto-generated ||
1819
| `--author-name <name>`, `--author-email <email>` | git config ||
1920
| `--trailer <Key>=<value>` (repeatable) | none ||
2021

22+
`--prefix` scopes records to a sub-tree under each sheet's configured root —
23+
useful for multi-tenant deployments where one git repo holds many tenants
24+
under `<root>/<tenant>/...`. With `--prefix tenant-a`, a sheet whose config
25+
declares `root = 'users'` reads/writes records at `users/tenant-a/<path>.toml`.
26+
The sheet's `.gitsheets/<name>.toml` config file is unaffected — only the
27+
record data tree is scoped.
28+
2129
## Commands
2230

2331
### `gitsheets upsert <sheet> [input]`
@@ -28,14 +36,38 @@ Insert or update one or more records.
2836
gitsheets upsert users users.json
2937
gitsheets upsert users - < records.jsonl
3038
gitsheets upsert users '{"slug":"jane","email":"jane@x.org"}'
39+
gitsheets upsert users users.csv --format=csv
40+
gitsheets upsert users users.toml --format=toml
3141
```
3242

3343
`input` is inline JSON (single record `{...}` or array `[{...}, ...]`),
3444
a file path, or `-` for stdin.
3545

36-
(In v1.0 `upsert` accepts no extra flags beyond the global tx flags.
37-
A `--patch` flag for RFC 7396 merge semantics is deferred to [#149](https://github.com/JarvusInnovations/gitsheets/issues/149);
38-
use the library `Sheet.patch(query, partial)` for now.)
46+
Flags:
47+
48+
- `--format <json|toml|csv>` — input format. Default: inferred from extension (`*.toml` → TOML, `*.csv` → CSV), otherwise JSON.
49+
- `--encoding <enc>` — encoding for file/stdin input. Default `utf8`. Accepts any Node `BufferEncoding`.
50+
- `--delete-missing`**DESTRUCTIVE**: full-replace mode. After upserting every input record, deletes any record in the sheet whose path isn't in the input set, in the same transaction. If validation fails on any input record, the whole transaction rolls back and the tree is unchanged.
51+
- `--attachment <name>=<source>` (repeatable) — attach a file alongside the (single) upserted record in the same transaction. `<source>` is a file path (relative to the input file's directory, or cwd when input is stdin) or `-` for stdin (only one `-` per command). Requires a single-record input set.
52+
53+
```bash
54+
gitsheets upsert users '{"slug":"jane"}' \
55+
--attachment avatar.jpg=./assets/jane.jpg \
56+
--attachment bio.md=-
57+
```
58+
59+
- `--patch` — treat each input record as an [RFC 7396 JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7396). Fields present in the sheet's path template are split out as the query (which existing record to find); the remaining fields are merged into that record. `null` deletes a field. Cannot be combined with `--delete-missing` or `--attachment`.
60+
61+
```bash
62+
# Update jane's email; delete her bio; add a team field.
63+
gitsheets upsert users '{"slug":"jane","email":"new@x.org","bio":null,"team":"eng"}' --patch
64+
```
65+
66+
For sheets whose template uses expressions (e.g., `${{ slug.toLowerCase() }}`), the CLI does a best-effort identifier scan to find the query fields. If that doesn't yield the right split, use the library API `Sheet.patch(query, partial)` directly.
67+
68+
TOML input supports three layouts: a `[[records]]` array of tables (recommended for multi-record files), a top-level table where every value is itself a table (each value becomes one record), or a single-record document.
69+
70+
CSV input uses the first row as a header. Cell values stay as strings — type coercion belongs in the consumer schema, not in CSV parsing.
3971

4072
Output (per record):
4173

@@ -45,30 +77,55 @@ Output (per record):
4577

4678
### `gitsheets query <sheet>`
4779

48-
Read records. Output is newline-delimited JSON.
80+
Read records. Output is newline-delimited JSON by default.
4981

5082
```bash
5183
gitsheets query users
5284
gitsheets query users --filter email=jane@x.org
5385
gitsheets query users --filter status=active --filter project_id=p1
5486
gitsheets query users --fields slug email --limit 100
87+
gitsheets query users --format=csv --fields slug email
88+
gitsheets query users --format=toml > users.toml
5589
```
5690

5791
Flags:
5892

5993
- `--filter <field>=<value>` (repeatable) — equality filter
60-
- `--fields <name>...` — output column subset
94+
- `--fields <name>...` — output column subset (order preserved)
6195
- `--limit <n>`
96+
- `--format <json|csv|tsv|toml>` — output format. Default `json` (newline-delimited).
97+
- `--headers` / `--no-headers` — emit a header row for CSV/TSV. Default `true`.
98+
99+
TOML output emits a `[[records]]` array of tables, round-trippable through `upsert --format=toml`.
62100

63101
### `gitsheets read <sheet> <path>`
64102

65103
Read a single record by its rendered path.
66104

67105
```bash
68106
gitsheets read users jane
107+
gitsheets read users jane --format=toml
69108
```
70109

71-
Output: pretty-printed JSON.
110+
Flags:
111+
112+
- `--format <json|toml|csv|tsv>` — output format. Default: pretty-printed JSON.
113+
114+
### `gitsheets edit <sheet> <path>`
115+
116+
Open a record in `$EDITOR` for interactive editing. On save:
117+
118+
1. The CLI re-reads the tmpfile, parses it as TOML.
119+
2. If the file is byte-identical to what was written out, no commit is made (idempotent — no empty commits).
120+
3. Otherwise the parsed record is upserted in a transaction; validation, normalization, and path rendering all run.
121+
122+
Editor resolution: `$VISUAL``$EDITOR``vi`. The editor is spawned through a shell, so `EDITOR="code --wait"` and similar wrapped commands work. Exit-code 0 from the editor means "save"; anything else aborts without commit.
123+
124+
```bash
125+
gitsheets edit users jane
126+
```
127+
128+
Validation failures abort with a clear message rather than re-opening the editor — re-edit the input via `gitsheets read users <path> --format=toml` plus `upsert` if you need a second pass.
72129

73130
### `gitsheets normalize <sheet>`
74131

@@ -80,6 +137,55 @@ or after a manual edit that didn't preserve canonical key order.
80137
gitsheets normalize users
81138
```
82139

140+
### `gitsheets init <sheet>`
141+
142+
Scaffold `.gitsheets/<sheet>.toml` with sensible defaults. Useful as a first-step before bulk-importing records.
143+
144+
```bash
145+
gitsheets init users
146+
gitsheets init users --path='${{ slug }}'
147+
gitsheets init users --schema=./schemas/user.schema.json
148+
```
149+
150+
Defaults:
151+
152+
- `root = <sheet>` (the sheet name)
153+
- `path = '${{ id }}'`
154+
155+
Flags:
156+
157+
- `--path <tpl>` — override the path template.
158+
- `--schema <file>` — embed a JSON Schema file under `[gitsheet.schema]`. The schema is validated through the same loader the runtime uses; an invalid schema aborts before the transaction commits.
159+
- `--force` — overwrite an existing `.gitsheets/<sheet>.toml`. Without `--force`, init refuses to clobber.
160+
161+
### `gitsheets infer <sheet>`
162+
163+
Scan every record in the sheet and write a conservative starter `[gitsheet.schema]` block into `.gitsheets/<sheet>.toml`. The committed schema captures observed types per field, plus `minimum`/`maximum` hints for numeric fields and `items.type` for arrays. Fields present in every record are listed under `required`.
164+
165+
```bash
166+
gitsheets infer users
167+
```
168+
169+
Existing `root`, `path`, and any non-schema `fields.<name>.sort` rules are preserved. The result is a starting point — review and tighten (add `pattern`, `format`, etc.) before relying on it.
170+
171+
### `gitsheets migrate-config <sheet>`
172+
173+
Convert a pre-v1.0 `[gitsheet.fields]` config to a v1.0 `[gitsheet.schema]` config in one transaction.
174+
175+
```bash
176+
gitsheets migrate-config orders
177+
```
178+
179+
Migration table (matches [`specs/behaviors/validation.md`](https://github.com/JarvusInnovations/gitsheets/blob/develop/specs/behaviors/validation.md#migration-of-pre-v10-gitsheetfields-config)):
180+
181+
| Pre-v1.0 field | v1.0 placement |
182+
| --- | --- |
183+
| `type: 'number' \| 'string' \| 'boolean'` | `[gitsheet.schema.properties.<name>].type` |
184+
| `enum: [...]` | `[gitsheet.schema.properties.<name>].enum` |
185+
| `default: <value>` | `[gitsheet.schema.properties.<name>].default` |
186+
| `sort: ...` | `[gitsheet.fields.<name>.sort]` (unchanged) |
187+
| `trueValues: [...]`, `falseValues: [...]` | Warning to stderr — move to CSV-ingest helper |
188+
83189
## Exit codes
84190

85191
| Code | Meaning |
@@ -105,13 +211,3 @@ gitsheets: ValidationError: record failed JSON Schema validation
105211
code: validation_failed
106212
issue: email: must match format "email" (json-schema)
107213
```
108-
109-
## Deferred commands
110-
111-
These are documented in [`specs/api/cli.md`](https://github.com/JarvusInnovations/gitsheets/blob/develop/specs/api/cli.md)
112-
but not shipped in the v1.0 substrate; they land in follow-up PRs:
113-
114-
- `gitsheets edit <sheet> <path>` — open in `$EDITOR`
115-
- `gitsheets infer <sheet>` — generate a starter JSON Schema from records
116-
- `gitsheets migrate-config <sheet>` — convert legacy `[gitsheet.fields]`
117-
- `gitsheets init <sheet>` — scaffold a new sheet config (#139)

docs/migration-guide.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ If you've been running a pre-v1.0 internal install, here's what changes and how
2727
| `sheet.delete(query)` | `sheet.delete(query)` (same name) |
2828
| `sheet.query(filter)` | `sheet.query(filter)` async iterator (same name) |
2929
| `sheet.commit(message)` | `repo.transact({ message }, async tx => { await tx.sheet(...).upsert(...) })` |
30-
| `--patch-existing` (CLI, `deepmerge`) | `--patch` (CLI, RFC 7396)deferred to [#149](https://github.com/JarvusInnovations/gitsheets/issues/149); use library `sheet.patch(query, partial)` for now |
30+
| `--patch-existing` (CLI, `deepmerge`) | `--patch` (CLI, RFC 7396 — semantic change: `null` deletes, arrays replace) |
3131
| HTTP `POST /sheets/:name` | Your own HTTP layer + `sheet.upsert(...)` |
3232

3333
## Permissive vs strict mode
@@ -147,11 +147,16 @@ Every gitsheets error extends `GitsheetsError` and carries a stable `code`. See
147147

148148
## CLI changes
149149

150-
- `git sheet upsert` / `query` / `read` / `normalize` — same command shape, rebuilt against the new core (`edit` deferred to [#150](https://github.com/JarvusInnovations/gitsheets/issues/150))
151-
- `--patch-existing``--patch` (RFC 7396 semantics, deferred — use library `Sheet.patch` for now)
150+
- `git sheet upsert` / `query` / `read` / `normalize` — same command shape, rebuilt against the new core
151+
- `git sheet edit <sheet> <path>` — new in v1.1: `$EDITOR`-based round-trip on a record
152+
- `git sheet init <sheet>` — new in v1.1: scaffold a starter `.gitsheets/<sheet>.toml`
153+
- `git sheet infer <sheet>` / `migrate-config <sheet>` — new in v1.1: schema inference and pre-v1.0 fields-config migration
154+
- `--patch-existing``--patch` (RFC 7396 semantics: `null` deletes, arrays replace)
155+
- `--format` (`json|toml|csv` upsert; `json|csv|tsv|toml` query/read), `--encoding`, `--delete-missing`, `--attachment <name>=<source>` — all shipped in v1.1
156+
- `--prefix` (`GITSHEETS_PREFIX`) for multi-tenant sub-tree scoping
152157
- New global flags: `--message`, `--author-name`, `--author-email`, `--trailer Key=Value`, `--ref`, `--commit-to`
153158
- Exit codes are stable from v1.0 onward — see [CLI reference](cli.md#exit-codes)
154-
- `--format` (CSV / TOML), `--encoding`, `--delete-missing`, `--attachments` deferred to v1.x
159+
- `--working` (read/write the working tree state) remains deferred — tracked at [#165](https://github.com/JarvusInnovations/gitsheets/issues/165)
155160

156161
## Going forward
157162

docs/recipes/migrating-config.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
Pre-v1.0 gitsheets used a `[gitsheet.fields]` block for typed field configuration. v1.0 replaces it with a proper `[gitsheet.schema]` JSON Schema block. The mapping is lossless for `type` / `enum` / `default`; `sort` stays where it is (it's a different concept); `trueValues` / `falseValues` move to a CSV-ingest helper.
44

5-
> The `gitsheets migrate-config <sheet>` CLI command that automates this is tracked at [#151](https://github.com/JarvusInnovations/gitsheets/issues/151) — until that lands, follow this guide manually.
5+
The `gitsheets migrate-config <sheet>` CLI command (shipped in v1.1) automates this:
6+
7+
```bash
8+
gitsheets migrate-config users --message='migrate users config to v1.0 schema'
9+
```
10+
11+
This guide walks through what the command does step-by-step, useful when you want to understand the resulting config or apply migrations by hand.
612

713
## Mapping table
814

@@ -233,6 +239,6 @@ gitsheets: ValidationError: record failed JSON Schema validation
233239
## See also
234240

235241
- [Validation](../validation.md) — full pipeline
236-
- [Issue #151](https://github.com/JarvusInnovations/gitsheets/issues/151)`gitsheets migrate-config` CLI (automated migration)
242+
- [`gitsheets migrate-config`](../cli.md#gitsheets-migrate-config-sheet)the CLI command that automates this whole flow
237243
- [`specs/behaviors/validation.md`](https://github.com/JarvusInnovations/gitsheets/blob/develop/specs/behaviors/validation.md)
238244
- [`specs/behaviors/normalization.md`](https://github.com/JarvusInnovations/gitsheets/blob/develop/specs/behaviors/normalization.md)

docs/validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ interface ValidationIssue {
149149
| `enum: [...]` | `[gitsheet.schema.properties.<name>].enum` |
150150
| `default: <value>` | `[gitsheet.schema.properties.<name>].default` |
151151
| `sort: ...` | `[gitsheet.fields.<name>.sort]` (unchanged — different concept) |
152-
| `trueValues` / `falseValues` | Moves to a CSV-ingest helper (deferred) |
152+
| `trueValues` / `falseValues` | Out of scope for the validation layer — `gitsheets migrate-config` emits a warning; CSV-input boolean coercion stays a consumer concern |
153153

154154
See the [migrating-config recipe](recipes/migrating-config.md) for a worked example.
155155

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@
4444
"@iarna/toml": "^2.2.5",
4545
"ajv": "^8.20.0",
4646
"ajv-formats": "^3.0.1",
47+
"csv-parse": "^6.2.1",
48+
"csv-stringify": "^6.7.0",
4749
"hologit": "^0.49.1",
50+
"rfc6902": "^5.2.0",
4851
"sort-keys": "^6.0.0",
4952
"yargs": "^18.0.0"
5053
},

0 commit comments

Comments
 (0)