sorodb 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ # Contributing to SoroDB
2
+
3
+ ## Set up a checkout
4
+
5
+ Use Node.js 20 or later, Bun for the committed `bun.lock`, and npm for the package preview command.
6
+
7
+ ```sh
8
+ bun install --frozen-lockfile
9
+ node examples/basic.js
10
+ bun run test
11
+ bun run typecheck
12
+ bun run build:types
13
+ ```
14
+
15
+ The example uses an in-memory store. File-backed tests create and remove temporary directories.
16
+
17
+ ## Make a change
18
+
19
+ The package is ESM JavaScript with JSDoc types checked by TypeScript. `npm run build:types` generates publishable declarations in `types/`; npm also runs it automatically before packing or publishing. Public imports come from `src/index.js`; add public APIs there deliberately. Use `src/types.js` for shared JSDoc definitions. See the [architecture map](docs/explanation/architecture.md) for module responsibilities.
20
+
21
+ Add a behavioral test in `test/sorodb.test.js` for changes to validation, persistence, indexes, query results, transactions, or cursor behavior. Update the [API reference](docs/reference/api.md) and relevant guide when public behavior changes. Keep runnable examples aligned with the documented API.
22
+
23
+ Before opening a pull request, run:
24
+
25
+ ```sh
26
+ bun run test
27
+ bun run typecheck
28
+ bun run lint
29
+ bun run fmt --check
30
+ bun run pack:dry
31
+ ```
32
+
33
+ The package preview should include `src`, `types`, `LICENSE`, `README.md`, `CONTRIBUTING.md`, `docs`, and `examples`. Summarize the behavior changed, the checks you ran, and any compatibility or migration impact in the pull request.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alloys Mila
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # SoroDB
2
+
3
+ SoroDB is an embedded document database for Node.js, built on SlateDB. Define a schema, store JavaScript documents, and query them with indexes, transactions, and snapshot-backed pagination.
4
+
5
+ SoroDB is an ECMAScript module and requires Node.js 20 or later.
6
+ TypeScript declarations are generated from the JSDoc annotations and included in the npm package.
7
+
8
+ ## Quick start
9
+
10
+ From this checkout, run `bun install --frozen-lockfile` first. Once SoroDB is published to npm, an application can install it with `npm install sorodb`. This example uses an in-memory store; its data disappears when the process ends.
11
+
12
+ ```js
13
+ import SoroDB from "sorodb";
14
+
15
+ const db = SoroDB({ store: "memory:///", path: "quick-start" });
16
+ db.schema({ version: 1 }, [
17
+ {
18
+ table: "users",
19
+ columns: {
20
+ id: { type: "number", primary: true },
21
+ name: { type: "string" },
22
+ },
23
+ },
24
+ ]);
25
+
26
+ try {
27
+ const users = await db.collection("users");
28
+ await users.create({ id: 1, name: "Ada" });
29
+ const page = await users.filter({ where: (w) => w.eq("name", "Ada") }).page();
30
+ console.log(page.items); // [{ id: 1, name: "Ada" }]
31
+ } finally {
32
+ await db.close();
33
+ }
34
+ ```
35
+
36
+ Register the schema before the first collection or transaction call. Always close the database to release the underlying store and any active query snapshots.
37
+
38
+ ## Store data in Amazon S3
39
+
40
+ Create a bucket and give the application read and write access to it. Set `AWS_DEFAULT_REGION` to the bucket's region and provide credentials through the environment, such as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (plus `AWS_SESSION_TOKEN` for temporary credentials). The [S3 object-store configuration](https://docs.rs/object_store/latest/object_store/aws/struct.AmazonS3Builder.html#method.from_env) lists supported environment variables.
41
+
42
+ Replace `your-bucket-name` with your bucket. The `store` URL selects the bucket; `path` places this database under the `sorodb-demo` prefix in that bucket.
43
+
44
+ ```js
45
+ import SoroDB from "sorodb";
46
+
47
+ const db = SoroDB({ store: "s3://your-bucket-name", path: "sorodb-demo" });
48
+ db.schema({ version: 1 }, [
49
+ {
50
+ table: "users",
51
+ columns: {
52
+ id: { type: "number", primary: true },
53
+ name: { type: "string" },
54
+ },
55
+ },
56
+ ]);
57
+
58
+ try {
59
+ const users = await db.collection("users");
60
+ if (!(await users.get(1))) await users.create({ id: 1, name: "Ada" });
61
+ console.log(await users.get(1)); // { id: 1, name: "Ada" }
62
+ } finally {
63
+ await db.close();
64
+ }
65
+ ```
66
+
67
+ Run the program again with the same bucket, path, and schema to read the stored document. See the [SlateDB object-store guide](https://slatedb.io/docs/get-started/quickstart/) for supported store URLs.
68
+
69
+ ## Store data in Cloudflare R2
70
+
71
+ This Node.js example uses R2's S3-compatible API. Create an R2 bucket and an API token with **Object Read & Write** access to that bucket. Set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` to the token's R2 credentials, and set `AWS_DEFAULT_REGION=auto`. See [Cloudflare's R2 S3 guide](https://developers.cloudflare.com/r2/get-started/s3/) for the bucket, token, and account ID setup.
72
+
73
+ Replace `your-account-id` and `your-bucket-name` below. The `store` URL selects the R2 endpoint and bucket; `path` places the database under the `sorodb-demo` prefix in the bucket.
74
+
75
+ ```js
76
+ import SoroDB from "sorodb";
77
+
78
+ const db = SoroDB({
79
+ store: "https://your-account-id.r2.cloudflarestorage.com/your-bucket-name",
80
+ path: "sorodb-demo",
81
+ });
82
+ db.schema({ version: 1 }, [
83
+ {
84
+ table: "users",
85
+ columns: {
86
+ id: { type: "number", primary: true },
87
+ name: { type: "string" },
88
+ },
89
+ },
90
+ ]);
91
+
92
+ try {
93
+ const users = await db.collection("users");
94
+ if (!(await users.get(1))) await users.create({ id: 1, name: "Ada" });
95
+ console.log(await users.get(1)); // { id: 1, name: "Ada" }
96
+ } finally {
97
+ await db.close();
98
+ }
99
+ ```
100
+
101
+ Run the program again with the same endpoint, bucket, path, and schema to read the stored document. For a bucket in a specific R2 jurisdiction, use its [jurisdiction-specific endpoint](https://developers.cloudflare.com/r2/api/tokens/).
102
+
103
+ ## Documentation
104
+
105
+ - [Getting started](docs/getting-started.md): build a file-backed database and reopen it.
106
+ - [API reference](docs/reference/api.md): database, collection, query, and error APIs.
107
+ - [Schema and query reference](docs/reference/schema-and-query.md): types, indexes, filters, and ordering.
108
+ - Guides: [schema upgrades](docs/guides/schema-upgrades.md), [indexes and sorting](docs/guides/indexes-and-sorting.md), [transactions](docs/guides/transactions.md), and [pagination](docs/guides/pagination.md).
109
+ - [Architecture](docs/explanation/architecture.md): how documents, indexes, and snapshots fit together.
110
+ - [Contributing](CONTRIBUTING.md): setup and checks for repository changes.
111
+
112
+ ## Run this repository
113
+
114
+ ```sh
115
+ bun install --frozen-lockfile
116
+ node examples/basic.js
117
+ bun run test
118
+ bun run typecheck
119
+ bun run build:types
120
+ ```
121
+
122
+ The [basic example](examples/basic.js) also demonstrates a descending index and async iteration.
123
+
124
+ ## License
125
+
126
+ SoroDB is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,34 @@
1
+ # Architecture
2
+
3
+ This page maps the implementation for contributors. The [API reference](../reference/api.md) describes the supported public behavior.
4
+
5
+ ## Module map
6
+
7
+ | Module | Responsibility |
8
+ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
9
+ | [`src/index.js`](../../src/index.js) | Public factory and error exports. |
10
+ | [`src/storage.js`](../../src/storage.js) | SlateDB lifecycle, transactions, schema upgrades, and cursor sessions. |
11
+ | [`src/schema.js`](../../src/schema.js) | ArkType compilation, document validation, and safe schema comparisons. |
12
+ | [`src/collection.js`](../../src/collection.js) | CRUD operations and index maintenance around writes. |
13
+ | [`src/query.js`](../../src/query.js) | Filter expressions, query planning, projection, iteration, and pages. |
14
+ | [`src/index-methods.js`](../../src/index-methods.js), [`src/indexes.js`](../../src/indexes.js) | B-tree key encoding, uniqueness checks, and index entry changes. |
15
+ | [`src/encoding.js`](../../src/encoding.js) | CBOR values, document keys, key ranges, and stable value encoding. |
16
+ | [`src/errors.js`](../../src/errors.js), [`src/types.js`](../../src/types.js) | Error classes and JSDoc type definitions. |
17
+
18
+ ## Storage and writes
19
+
20
+ `SoroDB(config)` constructs a `Database` without opening SlateDB. `schema()` compiles and stores the requested definition in memory. The first collection or transaction operation opens the native store and compares the persisted schema with that definition. The database uses distinct key prefixes for schema metadata, documents, and index entries.
21
+
22
+ Document values are CBOR-encoded. An index entry's key contains the indexed values, and its value points to the document key. CRUD writes update both in one serializable snapshot transaction. Unique indexes check for an existing pointer before writing. A failed write rolls back the transaction, so its document and index entries are not partially committed.
23
+
24
+ ## Schema upgrades
25
+
26
+ `schemaDiff` compares the stored and requested definitions. For a higher version, it permits additions and rejects changes or removals to existing definitions. `_migrate` validates old documents with the new table definition, applies defaults, rebuilds newly added indexes, and writes the new schema metadata after backfill succeeds. The index rebuild path is designed to allow retrying an interrupted or failed addition. The repository's `test/sorodb.test.js` exercises successful backfill and a failed unique-index addition.
27
+
28
+ ## Reads and cursors
29
+
30
+ `Query` compiles filter and order options when `filter()` is called. Its planner uses a direct document lookup for suitable `id` equality, an index scan for matching sort order or eligible equality, and otherwise a document scan. Every candidate is checked against the full filter expression before projection.
31
+
32
+ Async iteration reads from a snapshot, or from the active transaction when used there. `page()` retains a snapshot in a database-local cursor session so later pages see the same data even if writes occur. The cursor token carries a session ID, last key, and query fingerprint. Sessions expire after inactivity and are released on completion, explicit `Query.close()`, or `Database.close()`.
33
+
34
+ When changing public behavior, update the matching [reference page](../reference/api.md), task guide, and behavioral test together.
@@ -0,0 +1,68 @@
1
+ # Getting started
2
+
3
+ This tutorial creates a file-backed SoroDB database, writes documents, queries them, and reopens the data. You need Node.js 20 or later. Run `bun install --frozen-lockfile` in this checkout and save `app.mjs` at the repository root. Once SoroDB is published to npm, you can instead install it in another ESM project with `npm install sorodb`.
4
+
5
+ ## 1. Create a store and schema
6
+
7
+ Save the following as `app.mjs`:
8
+
9
+ ```js
10
+ import { mkdir } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+ import { pathToFileURL } from "node:url";
13
+ import SoroDB from "sorodb";
14
+
15
+ const directory = resolve("./sorodb-data");
16
+ await mkdir(directory, { recursive: true });
17
+ const config = { store: pathToFileURL(directory).href, path: "app" };
18
+
19
+ function openDatabase() {
20
+ return SoroDB(config).schema({ version: 1 }, [
21
+ {
22
+ table: "users",
23
+ columns: {
24
+ id: { type: "number", primary: true },
25
+ name: { type: "string" },
26
+ joinedAt: { type: "date", default: () => new Date() },
27
+ },
28
+ indexes: [{ name: "joined_at", columns: [{ field: "joinedAt", direction: "desc" }] }],
29
+ },
30
+ ]);
31
+ }
32
+
33
+ const db = openDatabase();
34
+ try {
35
+ const users = await db.collection("users");
36
+ await users.create({ id: 1, name: "Ada" });
37
+ await users.create({ id: 2, name: "Grace" });
38
+
39
+ for await (const user of users.filter({ orderBy: (order) => order.desc("joinedAt") })) {
40
+ console.log(user.name, user.joinedAt);
41
+ }
42
+ } finally {
43
+ await db.close();
44
+ }
45
+ ```
46
+
47
+ Run `node app.mjs`. The `store` URL identifies the filesystem directory; `path` names the SlateDB database within it. The first operation that needs storage opens the database. `close()` releases it.
48
+
49
+ ## 2. Reopen and read
50
+
51
+ Replace the write block inside `try` with this block, then run `node app.mjs` again:
52
+
53
+ ```js
54
+ const users = await db.collection("users");
55
+ console.log(await users.get(1));
56
+ console.log((await users.filter({ where: (w) => w.eq("name", "Grace") }).page()).items);
57
+ ```
58
+
59
+ The first result is Ada's document, including a `Date` in `joinedAt`. The second result contains Grace's document. Keep the same schema version and definition when reopening existing data.
60
+
61
+ ## Next steps
62
+
63
+ - [Schema and query reference](reference/schema-and-query.md) lists the supported definitions and operators.
64
+ - [Indexes and sorting](guides/indexes-and-sorting.md) explains the index required by `orderBy`.
65
+ - [Schema upgrades](guides/schema-upgrades.md) shows how to evolve a persistent schema.
66
+ - [API reference](reference/api.md) covers CRUD, transactions, and query results.
67
+
68
+ For a disposable database, use `{ store: "memory:///", path: "example" }` instead of a file URL. See [`examples/basic.js`](../examples/basic.js) for a complete in-memory example.
@@ -0,0 +1,70 @@
1
+ # Add indexes for queries and sorting
2
+
3
+ Declare indexes in the table schema. They are maintained when documents are created, replaced, or deleted. The current index method is `btree`.
4
+
5
+ ## Sort by an indexed field
6
+
7
+ ```js
8
+ import SoroDB, { ConflictError } from "sorodb";
9
+
10
+ const db = SoroDB({ store: "memory:///", path: "index-guide" });
11
+ db.schema({ version: 1 }, [
12
+ {
13
+ table: "users",
14
+ columns: {
15
+ id: { type: "number", primary: true },
16
+ name: { type: "string" },
17
+ email: { type: "string" },
18
+ joinedAt: { type: "date" },
19
+ },
20
+ indexes: [
21
+ { name: "recent_users", columns: [{ field: "joinedAt", direction: "desc" }] },
22
+ { name: "email_unique", columns: ["email"], unique: true },
23
+ ],
24
+ },
25
+ ]);
26
+
27
+ try {
28
+ const users = await db.collection("users");
29
+ await users.create({
30
+ id: 1,
31
+ name: "Ada",
32
+ email: "ada@example.com",
33
+ joinedAt: new Date("2026-01-01"),
34
+ });
35
+ await users.create({
36
+ id: 2,
37
+ name: "Grace",
38
+ email: "grace@example.com",
39
+ joinedAt: new Date("2026-02-01"),
40
+ });
41
+ const newest = await users.filter({ orderBy: (order) => order.desc("joinedAt") }).page();
42
+ console.log(newest.items.map((user) => user.name)); // ["Grace", "Ada"]
43
+
44
+ try {
45
+ await users.create({
46
+ id: 3,
47
+ name: "Another Ada",
48
+ email: "ada@example.com",
49
+ joinedAt: new Date(),
50
+ });
51
+ } catch (error) {
52
+ if (!(error instanceof ConflictError)) throw error;
53
+ console.log(error.code); // CONFLICT
54
+ }
55
+ } finally {
56
+ await db.close();
57
+ }
58
+ ```
59
+
60
+ The requested order must match the leading fields and directions of one declared index. For example, an index on `(team asc, joinedAt desc)` supports `order.asc("team")` and `order.asc("team").desc("joinedAt")`. It does not support sorting only by `joinedAt`. Ascending `id` is the one order that needs no declared index. An unsupported order raises `QueryError` when `filter()` builds the query.
61
+
62
+ ## Enforce uniqueness
63
+
64
+ The `email_unique` index in the example raises `ConflictError` for a duplicate value. Creating or replacing a document with another document's indexed value has the same result. A document missing an indexed field is omitted from that unique index; `null` is indexed and therefore subject to the constraint.
65
+
66
+ ## Equality lookups
67
+
68
+ An equality filter on `id` uses the document key. Other equality filters may use an index if its first field is the filtered path, the field is ascending, and the value is indexable. All returned candidates are still checked against the entire filter expression. Other predicates may require a scan, so choose indexes for the filters and orderings that matter to your workload.
69
+
70
+ For index syntax and supported values, see the [schema and query reference](../reference/schema-and-query.md#index-definitions).
@@ -0,0 +1,47 @@
1
+ # Read stable pages
2
+
3
+ Call `page()` on a filtered collection. It returns `items` and a `nextCursor`, which is `null` after the final page. The default page size is 100; set a positive `limit` for another size.
4
+
5
+ ```js
6
+ import SoroDB from "sorodb";
7
+
8
+ const db = SoroDB({ store: "memory:///", path: "pagination-guide" });
9
+ db.schema({ version: 1 }, [
10
+ {
11
+ table: "users",
12
+ columns: {
13
+ id: { type: "number", primary: true },
14
+ team: { type: "string" },
15
+ },
16
+ },
17
+ ]);
18
+
19
+ try {
20
+ const users = await db.collection("users");
21
+ for (let id = 1; id <= 3; id++) await users.create({ id, team: "platform" });
22
+
23
+ const options = {
24
+ where: (w) => w.eq("team", "platform"),
25
+ orderBy: (order) => order.asc("id"),
26
+ limit: 2,
27
+ };
28
+ let cursor;
29
+ do {
30
+ const page = await users.filter({ ...options, cursor }).page();
31
+ for (const user of page.items) console.log(user);
32
+ cursor = page.nextCursor ?? undefined;
33
+ } while (cursor);
34
+
35
+ const abandoned = users.filter({ limit: 1 });
36
+ const first = await abandoned.page();
37
+ if (first.nextCursor) abandoned.close();
38
+ } finally {
39
+ await db.close();
40
+ }
41
+ ```
42
+
43
+ Pages in one cursor chain read from the same snapshot, so intervening writes do not change the remaining pages. To continue, use the same table, filter, order, and projection. The page size can change. A cursor belongs to the same open database object and expires after inactivity; `cursorTtlMs` sets that interval and defaults to five minutes. A mismatched, invalid, expired, or closed cursor raises `CursorError`.
44
+
45
+ If you abandon a page chain, keep a reference to its query and call `query.close()` to release its snapshot promptly, as the example does with `abandoned`.
46
+
47
+ `page()` cannot run inside a transaction. Async iteration is useful when you want to consume results in one pass, including within a transaction. A cursor is an opaque token; store or transmit it unchanged, and do not expect it to work after the database closes.
@@ -0,0 +1,67 @@
1
+ # Upgrade a persistent schema
2
+
3
+ SoroDB stores the schema version and definition with the database. Register the same version and definition to reopen it unchanged. Increase the version when changing the definition; changing a stored definition without increasing the version raises `SchemaError`.
4
+
5
+ An upgrade can add tables, columns, or indexes. Removing or changing an existing table, column, or index is rejected as unsafe. SoroDB validates existing documents when it backfills newly added columns and builds newly added indexes.
6
+
7
+ ## Add a column with a default and an index
8
+
9
+ This complete example creates version 1 in a temporary directory, then reopens it with an added `createdAt` column and index:
10
+
11
+ ```js
12
+ import { mkdtemp, rm } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+ import SoroDB from "sorodb";
17
+
18
+ const directory = await mkdtemp(join(tmpdir(), "sorodb-upgrade-"));
19
+ const config = { store: pathToFileURL(directory).href, path: "data" };
20
+ try {
21
+ const first = SoroDB(config).schema({ version: 1 }, [
22
+ {
23
+ table: "items",
24
+ columns: {
25
+ id: { type: "number", primary: true },
26
+ label: { type: "string" },
27
+ },
28
+ },
29
+ ]);
30
+ try {
31
+ await (await first.collection("items")).create({ id: 1, label: "one" });
32
+ } finally {
33
+ await first.close();
34
+ }
35
+
36
+ const second = SoroDB(config).schema({ version: 2 }, [
37
+ {
38
+ table: "items",
39
+ columns: {
40
+ id: { type: "number", primary: true },
41
+ label: { type: "string" },
42
+ createdAt: { type: "date", default: () => new Date("2026-01-01") },
43
+ },
44
+ indexes: [{ name: "created_at", columns: ["createdAt"] }],
45
+ },
46
+ ]);
47
+ try {
48
+ const items = await second.collection("items"); // opens and upgrades
49
+ console.log(await items.get(1));
50
+ } finally {
51
+ await second.close();
52
+ }
53
+ } finally {
54
+ await rm(directory, { recursive: true, force: true });
55
+ }
56
+ ```
57
+
58
+ Supply a default for a new required field if existing documents need a value. An optional field can be added without a default. If backfill validation fails or a new unique index finds duplicates, opening fails; correct the stored data under the previous schema, close it, and retry the upgrade.
59
+
60
+ ## Before an upgrade
61
+
62
+ 1. Keep the full existing table and index definitions in the new schema.
63
+ 2. Increase `version` and add only the new definitions.
64
+ 3. Check that defaults produce valid values for old documents and that unique index values have no duplicates.
65
+ 4. Back up the database directory before upgrading production data, then open with the new schema and verify representative reads and queries.
66
+
67
+ The repository's `test/sorodb.test.js` covers a successful upgrade and a failed unique-index backfill followed by a retry.
@@ -0,0 +1,38 @@
1
+ # Write across collections in a transaction
2
+
3
+ Use `db.transaction()` when related operations must commit together. The callback receives a transaction object; obtain collections through `tx.collection()` so their reads and writes participate in the transaction.
4
+
5
+ ```js
6
+ import SoroDB from "sorodb";
7
+
8
+ const db = SoroDB({ store: "memory:///", path: "transaction-guide" });
9
+ db.schema({ version: 1 }, [
10
+ { table: "users", columns: { id: { type: "number", primary: true }, name: { type: "string" } } },
11
+ { table: "notes", columns: { id: { type: "string", primary: true }, text: { type: "string" } } },
12
+ ]);
13
+
14
+ try {
15
+ await db.transaction(async (tx) => {
16
+ const users = await tx.collection("users");
17
+ const notes = await tx.collection("notes");
18
+
19
+ await users.create({ id: 1, name: "Ada" });
20
+ await notes.create({ id: "welcome", text: "Hello, Ada" });
21
+ });
22
+
23
+ await db.transaction(async (tx) => {
24
+ const users = await tx.collection("users");
25
+ for await (const user of users.filter({ where: (w) => w.eq("name", "Ada") })) {
26
+ console.log(user);
27
+ }
28
+ });
29
+ } finally {
30
+ await db.close();
31
+ }
32
+ ```
33
+
34
+ The transaction commits when the callback resolves. If an operation or the callback throws, the transaction rolls back. Await each operation before the callback returns.
35
+
36
+ Collection `get`, `create`, `replace`, and `delete` work inside a transaction. Queries can use async iteration, as shown above.
37
+
38
+ `query.page()` and cursor-based iteration are unavailable on transaction-bound collections. For paginated reads, run `page()` outside the transaction; see [pagination](pagination.md).
@@ -0,0 +1,90 @@
1
+ # API reference
2
+
3
+ This page describes the API available from `sorodb`. Import the default or named `SoroDB` factory. The package also exports the error classes listed below.
4
+
5
+ ```js
6
+ import SoroDB, { ConflictError, CursorError } from "sorodb";
7
+ ```
8
+
9
+ All database and collection operations that access storage are asynchronous. Call `db.close()` when finished.
10
+
11
+ ## Database
12
+
13
+ ### `SoroDB(config)`
14
+
15
+ Creates a database object. `config` requires a `store` URL string and a nonempty `path` string. Tested store forms are `memory:///` and a filesystem URL such as one produced by `pathToFileURL(directory).href`. For a file URL, SoroDB joins the URL's directory with `path`. Optional `cursorTtlMs` is a positive integer; its default is five minutes.
16
+
17
+ ### `db.schema({ version }, definitions)`
18
+
19
+ Registers the schema and returns `db` for chaining. `version` is an integer of at least 1. `definitions` is a nonempty array of table definitions. Register it before opening the database. The database opens when a collection or transaction first needs storage. See [schema and query reference](schema-and-query.md) and [schema upgrades](../guides/schema-upgrades.md).
20
+
21
+ ### `await db.collection(name)`
22
+
23
+ Returns a collection registered in the schema. Unknown collection names raise `SchemaError`.
24
+
25
+ ### `await db.transaction(async (tx) => { ... })`
26
+
27
+ Runs the callback in a transaction. Obtain transaction-bound collections with `await tx.collection(name)`. The transaction commits after the callback resolves and rolls back if it throws. Use `await` for operations inside the callback. See [transactions](../guides/transactions.md).
28
+
29
+ ### `await db.close()`
30
+
31
+ Closes the database and releases active cursor snapshots. Calling it again is safe. A closed database cannot be reopened; construct a new SoroDB object to reopen the same store.
32
+
33
+ ## Collection
34
+
35
+ Get a collection with `await db.collection(name)` or, inside a transaction, `await tx.collection(name)`.
36
+
37
+ | Method | Result | Behavior |
38
+ | ---------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- |
39
+ | `await collection.create(document)` | Validated document, including applied defaults | Requires a unique `id`; duplicate IDs or unique index values raise `ConflictError`. |
40
+ | `await collection.get(id)` | Document or `null` | Reads by string or number ID. |
41
+ | `await collection.replace(id, document)` | Validated replacement | Requires an existing document and the same `id`; replaces the whole document. |
42
+ | `await collection.delete(id)` | `true` or `false` | Returns `false` when no document had that ID. |
43
+ | `collection.filter(options?)` | Query object | Builds a query without reading storage yet. |
44
+
45
+ Writes validate against the table schema. Extra fields are rejected. Values must be CBOR-compatible plain data: null, booleans, finite numbers, strings, BigInts, Dates, byte arrays, arrays, and plain objects. Cycles, sparse arrays, invalid Dates, and unsupported object types raise `ValidationError`. See [schema and query reference](schema-and-query.md).
46
+
47
+ ## Query
48
+
49
+ `collection.filter(options)` accepts the following optional fields:
50
+
51
+ | Option | Meaning |
52
+ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
53
+ | `where: (w) => expression` | Filter expression built with the operators in the [query reference](schema-and-query.md#filters). |
54
+ | `orderBy: (order) => order.asc(path)` or `.desc(path)` | Sort by one or more fields. Chaining calls adds fields. Requires a matching index except for ascending `id`. |
55
+ | `select: [path, ...]` | Project fields; `id` is always present. Paths may be dotted. |
56
+ | `limit: positiveInteger` | Maximum items from an iterator, or page size for `page()`. |
57
+ | `cursor: string` | Continue a previous page or iteration from a matching query snapshot. |
58
+
59
+ ### `await query.page()`
60
+
61
+ Returns `{ items, nextCursor }`, where `nextCursor` is a string if another page exists and `null` otherwise. The default page size is 100. Reuse the same filter, order, and projection with the returned cursor. The cursor is tied to an in-memory snapshot on the same open database object, so it cannot survive `db.close()` or expiration. `page()` is unavailable inside a transaction. See [pagination](../guides/pagination.md).
62
+
63
+ ### `for await (const document of query)`
64
+
65
+ Iterates matching documents. A `limit` applies if provided; otherwise iteration continues through the matching results. Async iteration works with transaction-bound collections. Complete or break out of the loop to release its snapshot.
66
+
67
+ ### `query.close()`
68
+
69
+ Releases a snapshot retained by a `page()` query when abandoning its next cursor. The cursor then cannot be resumed.
70
+
71
+ ## Errors
72
+
73
+ All exported errors extend `SoroError` and have a `code` string. Some include `details`.
74
+
75
+ | Export | Code | Typical cause |
76
+ | ----------------- | ------------------ | ------------------------------------------------------------------------------------ |
77
+ | `ValidationError` | `VALIDATION_ERROR` | Document violates its schema or contains unsupported values. |
78
+ | `SchemaError` | `SCHEMA_ERROR` | Invalid schema, incompatible version, unknown collection, or invalid database state. |
79
+ | `ConflictError` | `CONFLICT` | Duplicate ID, unique index collision, or missing replacement target. |
80
+ | `QueryError` | `QUERY_ERROR` | Invalid filter or sort, or unsupported query operation. |
81
+ | `CursorError` | `CURSOR_ERROR` | Invalid, expired, or mismatched cursor. |
82
+
83
+ ```js
84
+ try {
85
+ await users.create({ id: 1, name: "Ada" });
86
+ } catch (error) {
87
+ if (error instanceof ConflictError) console.error(error.code, error.message);
88
+ else throw error;
89
+ }
90
+ ```