From 3e19cb69fc4bcf16f8930bca7fd9256e0bd2e2f0 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 01:36:13 +0100 Subject: [PATCH 1/3] docs: pass the database docs to the docs standard --- README.md | 28 ++--- docs/1.query_types/1.index.md | 50 ++++---- docs/1.query_types/2.table.md | 49 ++++---- docs/1.query_types/3.selection.md | 50 +++++--- docs/1.query_types/4.single_selection.md | 67 ++++++----- docs/1.query_types/5.stream.md | 126 +++++++++++++-------- docs/1.query_types/6.feed.md | 49 ++++---- docs/1.query_types/7.query.md | 47 ++++---- docs/1.query_types/8.datum.md | 57 ++++++---- docs/1.query_types/9.valueproxy.md | 84 +++++++------- docs/2.operations/1.index.md | 36 +++--- docs/2.operations/2.database_management.md | 54 +++++---- docs/2.operations/3.table_operations.md | 48 ++++---- docs/2.operations/4.crud.md | 84 ++++++++------ docs/2.operations/5.indexes.md | 60 +++++----- docs/2.operations/6.filtering.md | 54 +++++---- docs/2.operations/7.lookup.md | 65 ++++++----- docs/2.operations/8.atomic_mutations.md | 36 +++--- docs/3.results/1.index.md | 36 +++--- docs/3.results/2.write_results.md | 54 +++++++-- docs/3.results/3.index_changes.md | 34 +++--- docs/3.results/4.table_changes.md | 55 ++++++--- docs/3.results/5.database_changes.md | 38 ++++--- docs/3.results/6.value_changes.md | 38 ++++--- docs/index.md | 120 ++++++++++---------- 25 files changed, 833 insertions(+), 586 deletions(-) diff --git a/README.md b/README.md index 0b34b39..bfb92d2 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,28 @@ -# @antelopejs/interface-database +# `@antelopejs/interface-database`
-npm -License -Discord -Docs +npm +License +Discord +Docs
-AQL (Antelope Query Language), the database-agnostic query builder of AntelopeJS: write a chainable, typed query once and run it on any database module that implements the interface. +AQL (Antelope Query Language) is the database-agnostic query builder of AntelopeJS. You write a chainable, typed query once, and any database module that implements the interface runs it. ## What it offers -- `Schema` declares tables, fields and indexes; each schema can hold several isolated instances. +- `Schema` declares tables, fields and indexes. Each schema can hold several isolated instances. - `Table`, `Selection` and `SingleSelection` insert, read, update and delete documents. -- `Stream` filters, maps, joins, groups and aggregates; `ValueProxy` builds the expressions inside them. +- `Stream` filters, maps, joins, groups and aggregates. `ValueProxy` builds the expressions inside them. - `changes()` streams inserts, updates and deletions as they happen. -- Queries are lazy: nothing runs until you `await` them, call `.run()` or iterate them. +- Queries are lazy. Nothing runs until you `await` a query, call `.run()` or iterate it. ## Implementations -| Module | Notes | -| ------------------------------------------------------------------ | ------------------------------------------------------------ | -| [`@antelopejs/mongodb`](https://github.com/AntelopeJS/mongodb) | MongoDB; also implements `@antelopejs/interface-mongodb` | -| [`@antelopejs/rethinkdb`](https://github.com/AntelopeJS/rethinkdb) | RethinkDB; also implements `@antelopejs/interface-rethinkdb` | +| Module | Notes | +| ------------------------------------------------------------------ | ------------------------------------------------------------- | +| [`@antelopejs/mongodb`](https://github.com/AntelopeJS/mongodb) | MongoDB. Also implements `@antelopejs/interface-mongodb`. | +| [`@antelopejs/rethinkdb`](https://github.com/AntelopeJS/rethinkdb) | RethinkDB. Also implements `@antelopejs/interface-rethinkdb`. | ## Install @@ -69,7 +69,7 @@ export async function listLongNovels() { } ``` -Callbacks such as the `filter` above receive a `ValueProxy`, not a document: only its methods travel to the database. +Callbacks such as the `filter` above receive a `ValueProxy`, not a document. The callback runs once, when the query is built, and the query records only the `ValueProxy` calls it makes. ## Documentation diff --git a/docs/1.query_types/1.index.md b/docs/1.query_types/1.index.md index c19cbad..4faa45c 100644 --- a/docs/1.query_types/1.index.md +++ b/docs/1.query_types/1.index.md @@ -1,6 +1,10 @@ -# Query Types +--- +title: Query types +description: The AQL query classes, what each one represents, how they inherit from each other, and the execution methods they share. +mode: reference +--- -AQL provides several query classes that form a hierarchy of capabilities. Each class encapsulates a different aspect of database interactions, from full table access down to individual value manipulation. +AQL has several query classes that form a hierarchy of capabilities. Each class covers one level of a database interaction, from a whole table down to one value inside a query. ## Choose by the result you need @@ -13,13 +17,13 @@ AQL provides several query classes that form a hierarchy of capabilities. Each c | One query result | `Datum` | Read a field or transform the value. | | A value inside a query callback | `ValueProxy` | Build an expression with `eq`, `add`, or other query operators. | -`ValueProxy` describes database expressions; it is not the fetched JavaScript value. Use its operators inside query callbacks, then execute the outer query to obtain data. +`ValueProxy` describes a database expression. It is not the fetched JavaScript value. Use its operators inside query callbacks, then execute the outer query to get data. -![Compose query stages without database I/O, then execute through the runtime provider. The staged-query entry point only builds stages.](../assets/query-execution.svg){width="480"} +![Compose query stages without database I/O, then execute through the runtime provider. The staged-query entry point only builds stages.](../assets/query-execution.svg "illustration"){width="480"} -## Query Type Hierarchy +## Query type hierarchy -All query types inherit from `StagedObject`, which records operations as a pipeline of stages. The hierarchy is: +Every query type inherits from `StagedObject`, which records operations as a pipeline of stages. The hierarchy is: ``` Query @@ -32,14 +36,14 @@ Query ValueProxy (used within query callbacks) ``` -## Common Features +## Common features -All query types that extend `Query` share these capabilities: +Every query type that extends `Query` has these methods: -- **`run()`** - Execute the query and return a `Promise` with the result. -- **`then()`** - `PromiseLike` support, so queries can be awaited directly. -- **`cursor()`** - Return an `AsyncGenerator` for streaming results one at a time. -- **`[Symbol.asyncIterator]()`** - Enable `for await...of` loops over query results. +- `run()` executes the query and returns a `Promise` with the result. +- `then()` makes the query `PromiseLike`, so you can await it directly. +- `cursor()` returns an `AsyncGenerator` that yields results one at a time. +- `[Symbol.asyncIterator]()` enables `for await...of` loops over the query results. ```typescript fragment import { Schema } from "@antelopejs/interface-database"; @@ -56,7 +60,7 @@ for await (const user of schema.instance().table("users")) { } ``` -## Recognize a Query Type +## Recognize a query type `IsQuery`, `IsDatum`, `IsStream` and `IsValueProxy` tell what kind of staged object a value is: @@ -68,15 +72,15 @@ function describe(query: unknown) { } ``` -Each provider binding of the interface has its own copy of these classes, so a query built by a module bound to another database is not an `instanceof` this module's `Stream`. The predicates recognize it whichever copy built it. Keep `instanceof` for checking that a query was built for this module's own binding. +Each provider binding of the interface has its own copy of these classes. A query that a module bound to another database built is therefore not an `instanceof` this module's `Stream`. The predicates recognize a query whichever copy built it. Use `instanceof` only to check that a query was built for this module's own binding. -## Available Query Types +## Available query types -- [**Table**](./2.table.md) - Represents a database table with insert, get, getAll, and between operations -- [**Selection**](./3.selection.md) - A set of documents supporting update, replace, and delete -- [**SingleSelection**](./4.single_selection.md) - A single document with update, replace, delete, and change feed support -- [**Stream**](./5.stream.md) - A sequence of results with map, filter, join, group, aggregate, and sort operations -- [**Change Feeds**](./6.feed.md) - Real-time change notifications via the `changes()` method -- [**Query**](./7.query.md) - The base class providing execution and async iteration -- [**Datum**](./8.datum.md) - A single value with field access, transformation, and lookup capabilities -- [**ValueProxy**](./9.valueproxy.md) - Proxy for in-query value operations (arithmetic, string, date, array, object) +- [Table](./2.table.md) is a database table with `insert`, `get`, `getAll`, `between`, and `atomicMutation`. +- [Selection](./3.selection.md) is a set of documents that supports `update`, `replace`, and `delete`. +- [SingleSelection](./4.single_selection.md) is one document with `update`, `replace`, `delete`, and change feed support. +- [Stream](./5.stream.md) is a sequence of results with `map`, `filter`, `join`, `group`, aggregation, and sort operations. +- [Change feeds](./6.feed.md) are real-time change notifications from the `changes()` method. +- [Query](./7.query.md) is the base class that provides execution and async iteration. +- [Datum](./8.datum.md) is a single value with field access, transformation, and lookup. +- [ValueProxy](./9.valueproxy.md) is a proxy for in-query operations on numbers, strings, dates, arrays, and objects. diff --git a/docs/1.query_types/2.table.md b/docs/1.query_types/2.table.md index 0a1bc60..fb4a528 100644 --- a/docs/1.query_types/2.table.md +++ b/docs/1.query_types/2.table.md @@ -1,25 +1,26 @@ -# Table +--- +title: Table +description: The Table query type and its methods for inserting documents, reading them by primary key, secondary index, or range, and applying atomic mutations. +mode: reference +--- -The `Table` class represents a database table and provides methods for inserting documents and retrieving them by primary key, secondary index, or range. It extends `Selection`, inheriting all multi-document operations and stream capabilities. +The `Table` class represents a database table. It inserts documents and retrieves them by primary key, secondary index, or range. It extends `Selection`, so it also has every multi-document operation and stream method. -## Class Definition +## Class definition ```typescript fragment class Table extends Selection { atomicMutation( - id: string, + key: string, request: AtomicMutation, ): Query; insert( - obj: DeepPartial | DeepPartial[], + obj: (DeepPartial & object) | (DeepPartial & object)[], options?: InsertOptions, ): Query; - get(key: string): SingleSelection; - getAll( - keys: string | number | boolean | (string | number | boolean)[], - index?: string, - ): Selection; - between( + get(key: ValueProxyOrValue): SingleSelection; + getAll(keys: IndexKeys, index?: string): Selection; + between( index: TK, low: T[TK] & IndexBound, high: T[TK] & IndexBound, @@ -27,9 +28,9 @@ class Table extends Selection { } ``` -## Access a Table +## Access a table -Tables are accessed through a `SchemaInstance`. Define a schema, create an instance, then call `table()`: +You reach a table through a `SchemaInstance`. Define a schema, create an instance, then call `table()`: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -45,7 +46,7 @@ await schema.createInstance(); const users = schema.instance().table("users"); ``` -## Insert Documents +## Insert documents The `insert` method adds one or more documents to the table and returns the generated IDs. @@ -78,13 +79,13 @@ const upsertIds = await users.insert( | ---------- | ----------------------- | ------------------------------------------------- | | `conflict` | `"update" \| "replace"` | Behavior when a document with the same ID exists. | -## Retrieve Documents +## Retrieve documents ### `get(key)` -Retrieve a single document by its primary key. Returns a `SingleSelection`. +Retrieves a single document by its primary key, given as a string or a `ValueProxy` of a string. Returns a `SingleSelection`. -Awaiting this selection is declared as `T`, not `T | null`. The declaration does not prove a row exists; providers can return `null` for missing keys, so check before reading fields. +Awaiting this selection is declared as `T`, not `T | null`. The declaration does not prove that a row exists. A provider can return `null` for a missing key, so check the result before you read its fields. ```typescript fragment const user = await users.get("user-123"); @@ -96,7 +97,7 @@ if (user) { ### `getAll(keys, index?)` -Retrieve multiple documents by key values. When `index` is provided, the lookup uses the specified secondary index instead of the primary key. +Retrieves multiple documents by key values. `keys` is an `IndexKeys`: one key, a list of keys, or a list of compound keys that each hold a compound index's values in its field order. A key is a string, a number, or a boolean. When you pass `index`, the lookup uses that secondary index instead of the primary key. ```typescript fragment // By primary key @@ -108,7 +109,7 @@ const admins = await users.getAll("admin", "role"); ### `between(index, low, high)` -Retrieve documents where the specified index value falls within a range. The lower bound is inclusive, and the upper bound is exclusive. A bound is an `IndexBound`: a string, number, boolean, `Date` or `null`, or a list of them for a compound index. +Retrieves the documents whose value for the index falls within a range. The lower bound is inclusive, and the upper bound is exclusive. A bound is an `IndexBound`: a string, number, boolean, `Date` or `null`, or a list of them for a compound index. ```typescript fragment const recent = await users.between( @@ -118,11 +119,11 @@ const recent = await users.between( ); ``` -## Inherited Methods +## Inherited methods -For conditional single-record writes, see [Atomic Mutations](../2.operations/8.atomic_mutations.md). `atomicMutation()` is a table method, not a selection method; it returns an explicit outcome rather than a modified count. +For conditional single-record writes, see [Atomic mutations](../2.operations/8.atomic_mutations.md). `atomicMutation()` is a table method, not a selection method. It returns an explicit outcome, `"applied"`, `"not-applied"`, or `"unknown"`, rather than a modified count. -Since `Table` extends `Selection`, which extends `Stream`, it inherits: +`Table` extends `Selection`, which extends `Stream`, so it inherits: -- **Selection methods**: `update()`, `replace()`, `delete()` -- see [Selection](./3.selection.md) -- **Stream methods**: `map()`, `filter()`, `orderBy()`, `count()`, `join()`, and more -- see [Stream](./5.stream.md) +- The selection methods `update()`, `replace()`, and `delete()`. See [Selection](./3.selection.md). +- The stream methods `map()`, `filter()`, `orderBy()`, `count()`, `join()`, and the others listed on [Stream](./5.stream.md). diff --git a/docs/1.query_types/3.selection.md b/docs/1.query_types/3.selection.md index f6e98fb..5a63fcb 100644 --- a/docs/1.query_types/3.selection.md +++ b/docs/1.query_types/3.selection.md @@ -1,26 +1,40 @@ -# Selection +--- +title: Selection +description: The Selection query type and its bulk update, replace, and delete methods, including how the update overloads resolve their return type. +mode: reference +--- -The `Selection` class represents a set of documents and provides methods for bulk mutation operations. It extends `Stream`, inheriting all transformation, filtering, and aggregation capabilities. +The `Selection` class represents a set of documents and provides bulk mutation methods. It extends `Stream`, so it also has every transformation, filtering, and aggregation method. -## Class Definition +## Class definition ```typescript fragment class Selection extends Stream { - update(document: DeepPartial): Query; - update(document: (val: ValueProxy) => U): Query; - replace(document: DeepPartial): Query; + update( + document: (val: ValueProxy) => U, + ): ExtractType extends DeepPartial ? Query : never; + update(document: DeepPartial & object): Query; + replace(document: DeepPartial & object): Query; delete(): Query; } ``` -## Create a Selection +The updater overload comes first and the document overload last: -Selections are produced by table methods such as `getAll()` and `between()`, or by chaining stream operations like `filter()`: +- A function argument resolves to the updater overload. It returns `Query` when the type the updater builds fits `DeepPartial`, and `never` otherwise. +- An object argument resolves to the document overload, which returns `Query`. +- An argument typed `any` resolves to the updater overload, and the conditional gives `never`. The result has no `.run()` and cannot be awaited for a count. Give the argument a real type. +- A type-level read such as `ReturnType` takes the last overload and reads `Query`. + +## Create a selection + +Table methods such as `getAll()` and `between()` produce selections. So does a chained stream operation such as `filter()`: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Define the myapp schema first"); const users = schema.instance().table("users"); // Selection from getAll @@ -34,9 +48,9 @@ const recentUsers = users.between( ); ``` -## Update Documents +## Update documents -The `update` method modifies matching documents with the provided partial data. Fields not included in the update object remain unchanged. It returns the number of modified documents. +The `update` method changes the matching documents with the given partial data. Fields that the update object leaves out stay unchanged. It returns the number of modified documents. ```typescript fragment // Update with a partial object @@ -52,9 +66,9 @@ await users.getAll("active", "status").update((user) => ({ })); ``` -## Replace Documents +## Replace documents -The `replace` method substitutes entire documents with the provided data. Unlike `update`, all fields not present in the replacement are removed. +The `replace` method replaces each whole document with the given data. Unlike `update`, it removes every field that the replacement leaves out. ```typescript fragment const count = await users.getAll("user-123").replace({ @@ -67,9 +81,9 @@ const count = await users.getAll("user-123").replace({ console.log(`Replaced ${count} documents`); ``` -## Delete Documents +## Delete documents -The `delete` method removes all documents in the selection and returns the number of deleted documents. +The `delete` method removes every document in the selection and returns the number of deleted documents. ```typescript fragment const count = await users.getAll("inactive", "status").delete(); @@ -77,9 +91,9 @@ const count = await users.getAll("inactive", "status").delete(); console.log(`Deleted ${count} inactive users`); ``` -## Inherited Methods +## Inherited methods -Since `Selection` extends `Stream`, it inherits all stream operations: +`Selection` extends `Stream`, so it inherits every stream operation: - Transformation: `map()`, `pluck()`, `without()` - Filtering: `filter()` @@ -89,4 +103,4 @@ Since `Selection` extends `Stream`, it inherits all stream operations: - Grouping: `group()` - Deduplication: `distinct()` -See [Stream](./5.stream.md) for complete details on these methods. +See [Stream](./5.stream.md) for the details of these methods. diff --git a/docs/1.query_types/4.single_selection.md b/docs/1.query_types/4.single_selection.md index 3a3a3c5..5a38eaa 100644 --- a/docs/1.query_types/4.single_selection.md +++ b/docs/1.query_types/4.single_selection.md @@ -1,38 +1,47 @@ -# SingleSelection +--- +title: SingleSelection +description: The SingleSelection query type for one document, with its read, update, replace, delete, and change feed methods. +mode: reference +--- -The `SingleSelection` class represents a reference to a single document in a table. It extends `Datum` and provides methods for updating, replacing, deleting, and monitoring changes to an individual document. +The `SingleSelection` class represents a reference to one document in a table. It extends `Datum` and adds methods that update, replace, delete, and watch that document. -## Class Definition +## Class definition ```typescript fragment class SingleSelection extends Datum { - update(document: DeepPartial): Query; - update(document: (val: ValueProxy) => U): Query; - replace(document: DeepPartial): Query; + update( + document: (val: ValueProxy) => U, + ): ExtractType extends DeepPartial ? Query : never; + update(document: DeepPartial & object): Query; + replace(document: DeepPartial & object): Query; delete(): Query; changes(): Query[]>; } ``` -## Create a SingleSelection +The `update` overloads resolve as they do on [Selection](./3.selection.md). An argument typed `any` resolves to `never`, with no `.run()`. A type-level read such as `ReturnType["update"]>` reads `Query`. -SingleSelection objects are produced by calling `get()` on a table: +## Create a `SingleSelection` + +Calling `get()` on a table produces a `SingleSelection`: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Define the myapp schema first"); const users = schema.instance().table("users"); // Get a document by primary key const user = users.get("user-123"); ``` -## Read a Document +## Read a document -Since `SingleSelection` extends `Datum`, you can execute it directly to retrieve the document value: +`SingleSelection` extends `Datum`, so you can execute it directly to retrieve the document. -The declared result is `T`, even though a provider can return `null` for a missing key. Check for absence before accessing the document; the generic type is not an existence guarantee. +The declared result is `T`, even though a provider can return `null` for a missing key. Check for absence before you access the document. The generic type does not guarantee that the document exists. ```typescript fragment const user = await users.get("user-123"); @@ -52,9 +61,9 @@ const display = await users.get("user-123").do((u) => ({ })); ``` -## Update a Document +## Update a document -The `update` method modifies fields in the document. Unmentioned fields remain unchanged. Returns the number of modified documents. +The `update` method changes fields in the document. Fields it does not mention stay unchanged. It returns the number of modified documents. ```typescript fragment // Update with a partial object @@ -70,9 +79,9 @@ await users.get("user-123").update((user) => ({ })); ``` -## Replace a Document +## Replace a document -The `replace` method substitutes the entire document with new data. +The `replace` method replaces the whole document with new data. ```typescript fragment const count = await users.get("user-123").replace({ @@ -83,9 +92,9 @@ const count = await users.get("user-123").replace({ }); ``` -## Delete a Document +## Delete a document -The `delete` method removes the document from the table. +The `delete` method removes the document from the table and returns the number of deleted documents. ```typescript fragment const count = await users.get("user-123").delete(); @@ -95,9 +104,9 @@ if (count === 1) { } ``` -## Monitor Changes +## Monitor changes -The `changes` method returns change events for the document. Each event contains a `changeType` and the `oldValue`/`newValue` of the document. +The `changes` method returns change events for the document. Each event holds a `changeType`, plus the `oldValue` and `newValue` of the document. ```typescript fragment for await (const change of users.get("user-123").changes()) { @@ -107,15 +116,15 @@ for await (const change of users.get("user-123").changes()) { } ``` -## Inherited Methods +## Inherited methods -Since `SingleSelection` extends `Datum`, it inherits: +`SingleSelection` extends `Datum`, so it inherits: -- **`key()`** - Access a field by name -- **`default()`** - Provide a fallback value for null -- **`do()`** - Apply a transformation function -- **`lookup()`** - Perform a foreign key lookup -- **`pluck()`** - Select specific fields -- **`cast()`** - Change the TypeScript type +- `key()` reads a field by name. +- `default()` gives a fallback value for `null`. +- `do()` applies a transformation function. +- `lookup()` performs a foreign key lookup. +- `pluck()` selects specific fields. +- `cast()` changes the TypeScript type. -See [Datum](./8.datum.md) for complete details. +See [Datum](./8.datum.md) for the details. diff --git a/docs/1.query_types/5.stream.md b/docs/1.query_types/5.stream.md index 3919d5b..9090112 100644 --- a/docs/1.query_types/5.stream.md +++ b/docs/1.query_types/5.stream.md @@ -1,8 +1,12 @@ -# Stream +--- +title: Stream +description: The Stream query type and its methods for transforming, filtering, joining, grouping, ordering, aggregating, and watching a sequence of documents. +mode: reference +--- -The `Stream` class represents a sequence of documents that can be transformed, filtered, joined, grouped, and aggregated. It extends `Query` and serves as the foundation for collection-based operations in AQL. +The `Stream` class represents a sequence of documents that you can transform, filter, join, group, and aggregate. It extends `Query` and is the base of every collection operation in AQL. -## Class Definition +## Class definition ```typescript fragment class Stream extends Query { @@ -10,27 +14,54 @@ class Stream extends Query { cast(): Stream; // Field access - key(key: K, def?: U): Stream< - U extends undefined ? T[K] : Exclude | U - >; - default(val: Value): Stream | U>; + key( + key: ValueProxyOrValue, + def?: U & QueryValue, + ): Stream | U>; + default( + val: Value, + ): Stream | U>; // Transformation map(mapper: (val: ValueProxy) => U): Stream>; filter(predicate: (val: ValueProxy) => ValueProxyOrValue): this; - pluck(...fields: string[]): Stream>; - without(...fields: string[]): Stream>; + pluck(...fields: K[]): Stream>; + without(...fields: K[]): Stream>; // Combining union(other: Stream): Stream; // Joining - join(right: Stream, predicate, mapper): Stream>; - joinInner(right: Stream, predicate, mapper): Stream>; - lookup(right: Selection, localKey: TK, otherKey: keyof U): Stream<...>; + join( + right: Stream, + predicate: ( + left: ValueProxy, + right: ValueProxy, + ) => ValueProxyOrValue, + mapper: (left: ValueProxy, right: ValueProxy) => V, + ): Stream>; + joinInner( + right: Stream, + predicate: ( + left: ValueProxy, + right: ValueProxy, + ) => ValueProxyOrValue, + mapper: (left: ValueProxy, right: ValueProxy) => V, + ): Stream>; + lookup( + right: Stream, + localKey: TK, + otherKey: keyof U & string, + ): Stream & Record>; // Grouping - group(index: K, mapper: (stream: Stream, group: ValueProxy) => U): Stream>; + group( + index: K, + mapper: ( + stream: Stream, + group: ValueProxy, + ) => U, + ): Stream>; // Ordering and pagination orderBy(index: string, direction?: "asc" | "desc"): this; @@ -38,24 +69,25 @@ class Stream extends Query { nth(n: Value): Datum; // Aggregation - count(field?: keyof T): Datum; - sum(field?: keyof T): Datum; - avg(field?: keyof T): Datum; - min(field?: keyof T): Datum; - max(field?: keyof T): Datum; + count(field?: keyof T & string): Datum; + sum(field?: keyof T & string): Datum; + avg(field?: keyof T & string): Datum; + min(field?: keyof T & string): Datum; + max(field?: keyof T & string): Datum; distinct(): Datum; - distinct(field: TK): Stream; + distinct(field: undefined): Datum; + distinct(field: TK): Stream; // Change feeds changes(): Query[]>; } ``` -## Field Access +## Field access -Access a specific field from all documents in the stream with `key()`, or provide a default value for null fields with `default()`. +`key()` reads one field from every document in the stream. `default()` gives a fallback value for `null` values. -`key(field, fallback)` takes a field name, or a `ValueProxy` yielding one. It also accepts a fallback. When it is not `undefined`, the result type excludes `null` and `undefined` from the field type and includes the fallback's type; without it, the declared field type is unchanged. +`key(field, fallback)` takes a field name, or a `ValueProxy` that yields one, and an optional fallback. When the fallback is not `undefined`, the result type drops `null` and `undefined` from the field type and adds the fallback's type. Without a fallback, the declared field type is unchanged. ```typescript fragment const users = schema.instance().table("users"); @@ -67,11 +99,11 @@ const emails = await users.key("email"); const phones = await users.key("phone").default("N/A"); ``` -## Transform Data +## Transform data ### `map()` -Transform each document using a mapping function. The callback receives a `ValueProxy` for constructing expressions. +Transforms each document with a mapping function. The callback receives a `ValueProxy` to build expressions with. ```typescript fragment const profiles = await users.map((user) => ({ @@ -83,7 +115,7 @@ const profiles = await users.map((user) => ({ ### `filter()` -Select documents matching a predicate. The callback returns a boolean expression. +Keeps the documents that match a predicate. The callback returns a boolean expression. ```typescript fragment const active = await users.filter((user) => @@ -93,7 +125,7 @@ const active = await users.filter((user) => ### `pluck()` and `without()` -Select or exclude specific fields from all documents. +`pluck()` keeps only the named fields of every document, and `without()` removes them. Both accept only field names of `T`. ```typescript fragment // Keep only specific fields @@ -103,11 +135,11 @@ const slim = await users.pluck("_id", "name", "email"); const safe = await users.without("password", "ssn"); ``` -## Combine Streams +## Combine streams ### `union()` -Concatenate two streams without deduplication. +Concatenates two streams without deduplication. ```typescript fragment const allPremium = await users @@ -115,11 +147,11 @@ const allPremium = await users .union(users.filter((u) => u.key("plan").eq("enterprise"))); ``` -## Join Data +## Join data -### `join()` - Left Join +### `join()` for a left join -Perform a left join between two streams. Every document from the left stream appears in the result, with `null` for the right side when no match exists. +Performs a left join between two streams. Every document from the left stream appears in the result. When no right document matches, the right side is `null`. ```typescript fragment const usersWithOrders = await users.join( @@ -132,9 +164,9 @@ const usersWithOrders = await users.join( ); ``` -### `joinInner()` - Inner Join +### `joinInner()` for an inner join -Perform an inner join. Only documents with matches on both sides appear in the result. +Performs an inner join. Only documents with a match on both sides appear in the result. ```typescript fragment const matched = await users.joinInner( @@ -149,7 +181,7 @@ const matched = await users.joinInner( ### `lookup()` -Replace a foreign key field with the referenced document from another table. See [Lookup](../2.operations/7.lookup.md) for detailed usage. +Replaces a foreign key field, or a list of foreign keys, with the referenced documents from another stream. See [Lookup](../2.operations/7.lookup.md) for the details. ```typescript fragment const postsWithAuthors = await schema @@ -158,9 +190,9 @@ const postsWithAuthors = await schema .lookup(schema.instance().table("users"), "authorId", "_id"); ``` -## Group Data +## Group data -The `group` method partitions the stream by an index value, then applies a mapping function to each group. The mapper receives the group's sub-stream and the group key. +The `group` method splits the stream by an index value, then applies a mapping function to each group. The mapper receives the stream of the group's documents and the group key. Each mapper result is one element of the new stream. ```typescript fragment const statsByRole = await users.group("role", (stream, role) => ({ @@ -170,11 +202,11 @@ const statsByRole = await users.group("role", (stream, role) => ({ })); ``` -## Order and Paginate +## Order and paginate ### `orderBy()` -Sort the stream by an index field. +Sorts the stream by an index field, in ascending or descending order. ```typescript fragment const sorted = await users.orderBy("name", "asc"); @@ -183,7 +215,7 @@ const newest = await users.orderBy("createdAt", "desc"); ### `slice()` -Extract a subsection of the stream for pagination. +Takes a subsection of the stream, from an offset and for a number of documents, for pagination. ```typescript fragment const page1 = await users.orderBy("createdAt").slice(0, 10); @@ -192,15 +224,15 @@ const page2 = await users.orderBy("createdAt").slice(10, 10); ### `nth()` -Retrieve a single document by its position in the stream. Returns a `Datum`. +Retrieves one document by its position in the stream. Returns a `Datum`. ```typescript fragment const first = await users.orderBy("createdAt").nth(0); ``` -## Aggregate Data +## Aggregate data -Stream provides several aggregation methods that return a `Datum`: +These aggregation methods return a `Datum`. `count(field)` counts the distinct values of the field, and `count()` counts the documents. ```typescript fragment const totalUsers = await users.count(); @@ -213,7 +245,7 @@ const minOrder = await schema.instance().table("orders").min("amount"); ### `distinct()` -Retrieve deduplicated documents or distinct values of a specific field. +Without a field, `distinct()` returns a `Datum` of the deduplicated documents. With a field, it returns a `Stream` of that field's distinct values. ```typescript fragment // All distinct documents @@ -223,9 +255,9 @@ const unique = await users.distinct(); const roles = await users.distinct("role"); ``` -## Change Feeds +## Change feeds -The `changes` method subscribes to real-time change notifications on the stream. +The `changes` method turns the stream into a feed of real-time change notifications. ```typescript fragment for await (const change of users.changes()) { @@ -233,6 +265,6 @@ for await (const change of users.changes()) { } ``` -An open-ended feed may never produce a completed array. Consume events incrementally, and stop the loop when the consumer no longer needs them. +An open-ended feed may never produce a completed array. Consume the events one at a time, and stop the loop when the consumer no longer needs them. -See [Change Feeds](./6.feed.md) for more details. +See [Change feeds](./6.feed.md) for the details. diff --git a/docs/1.query_types/6.feed.md b/docs/1.query_types/6.feed.md index 84da48d..bc79dca 100644 --- a/docs/1.query_types/6.feed.md +++ b/docs/1.query_types/6.feed.md @@ -1,8 +1,12 @@ -# Change Feeds +--- +title: Change feeds +description: How changes() turns a stream or a single selection into a feed of change events, the Changes event shape, and how to consume a feed. +mode: reference +--- -Change feeds provide real-time notifications when documents are inserted, updated, or deleted. Both `Stream` and `SingleSelection` expose a `changes()` method that returns change events. +A change feed sends a notification when a document is inserted, updated, or deleted. Both `Stream` and `SingleSelection` expose a `changes()` method that returns change events. -## The Changes Interface +## The `Changes` interface ```typescript interface Changes { @@ -12,20 +16,21 @@ interface Changes { } ``` -| Property | Description | -| ------------ | ------------------------------------------------------------------ | -| `changeType` | The type of change: `"added"`, `"removed"`, or `"modified"`. | -| `oldValue` | The document value before the change. Undefined for new documents. | -| `newValue` | The document value after the change. Undefined for deletions. | +| Property | Description | +| ------------ | -------------------------------------------------------------------- | +| `changeType` | The type of change: `"added"`, `"removed"`, or `"modified"`. | +| `oldValue` | The document value before the change. `undefined` for new documents. | +| `newValue` | The document value after the change. `undefined` for deletions. | -## Create a Change Feed +## Create a change feed -Call `changes()` on a stream or single selection to build a feed query. Consume it with async iteration to receive events as they arrive: +Calling `changes()` on a stream or a single selection builds a feed query. Async iteration over the feed receives the events as they arrive: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Define the myapp schema first"); const users = schema.instance().table("users"); // Monitor all changes to a table @@ -35,9 +40,9 @@ const allChanges = users.changes(); const userChanges = users.get("user-123").changes(); ``` -## Process Change Events +## Process change events -Each change event includes a `changeType` field that indicates the nature of the change: +Each change event has a `changeType` field that tells what kind of change happened: ```typescript fragment for await (const change of users.changes()) { @@ -52,9 +57,9 @@ for await (const change of users.changes()) { } ``` -## Use with Async Iteration +## Use with async iteration -`changes()` returns `Query[]>`, but `Query`'s cursor and async iterator yield each array element: one `Changes` event, not a batch. A direct `await` uses `run()` and asks the provider for a completed result; an open-ended feed may never complete, so use iteration for continuous monitoring. +`changes()` returns `Query[]>`, but the cursor and the async iterator of `Query` yield each array element. Each iteration gives one `Changes` event, not a batch. A direct `await` calls `run()` and asks the provider for a completed result. An open-ended feed may never complete, so use iteration to watch a feed continuously. ```ts docs-check import { Schema } from "@antelopejs/interface-database"; @@ -77,13 +82,13 @@ export async function watchUsers(onChange: (change: Changes) => boolean) { } ``` -Breaking out of the loop closes its cursor. When using `cursor()` manually, call `return()` when finished. +Breaking out of the loop closes its cursor. If you call `cursor()` yourself, call `return()` on the cursor when you are done. -## Use Cases +## Use cases -Change feeds are well suited for: +Change feeds fit these uses: -- **Real-time UI updates** - Push changes to connected clients as they happen. -- **Event-driven architectures** - React to data changes with downstream processing. -- **Change observation** - Observe writes while a feed is active. Durable auditing requires provider-specific delivery and recovery guarantees; this interface alone does not promise a complete audit log. -- **Cache invalidation** - Clear or update caches when the underlying data changes. +- Real-time UI updates, which push changes to connected clients as they happen. +- Event-driven architectures, which react to data changes with downstream processing. +- Change observation, which sees writes while a feed is active. Durable auditing needs delivery and recovery guarantees from the provider. This interface alone does not promise a complete audit log. +- Cache invalidation, which clears or updates caches when the underlying data changes. diff --git a/docs/1.query_types/7.query.md b/docs/1.query_types/7.query.md index 7a73d00..a753e6f 100644 --- a/docs/1.query_types/7.query.md +++ b/docs/1.query_types/7.query.md @@ -1,51 +1,60 @@ -# Query +--- +title: Query +description: The Query base class and the ways to execute a query, with run(), await, a cursor, or for await...of, plus lazy composition. +mode: reference +--- -The `Query` class is the base class for all query types in the AQL system. It implements `PromiseLike`, so queries can be awaited directly, and provides cursor-based async iteration for streaming results. +The `Query` class is the base class of every AQL query type. It implements `PromiseLike`, so you can await a query directly. It also provides cursor-based async iteration that yields results one at a time. -## Class Definition +## Class definition ```typescript fragment class Query extends StagedObject implements PromiseLike { run(): Promise; - then( - onfulfilled?: (value: T) => TResult1 | PromiseLike, - onrejected?: (reason: any) => TResult2 | PromiseLike, + then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | null, ): PromiseLike; - cursor(): AsyncGenerator ? T1 : T, void, unknown>; - [Symbol.asyncIterator](): AsyncGenerator<...>; + cursor(): AsyncGenerator ? U : T, void, unknown>; + [Symbol.asyncIterator](): AsyncGenerator< + T extends Array ? U : T, + void, + unknown + >; } ``` -## Execute a Query +## Execute a query ### `run()` -Execute the query pipeline and return the result as a promise. +Executes the query pipeline and returns the result as a promise. ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Define the myapp schema first"); const users = await schema.instance().table("users").run(); ``` ### Direct `await` -Since `Query` implements `PromiseLike`, you can await it directly without calling `run()`: +`Query` implements `PromiseLike`, so you can await it directly without calling `run()`: ```typescript fragment const users = await schema.instance().table("users"); ``` -Both approaches are equivalent. Use whichever reads better in context. +Both forms run the same stages: `then()` calls `run()`. -## Async Iteration +## Async iteration -Array-valued queries yield one element at a time, including one `Changes` event per iteration of a change feed. [Atomic mutations](../2.operations/8.atomic_mutations.md) are an exception: they reject cursor execution and must be awaited directly or executed with `run()`. +A query whose result is an array yields one element at a time. A change feed yields one `Changes` event per iteration. [Atomic mutations](../2.operations/8.atomic_mutations.md) are the exception. Their `cursor()` throws a `TypeError`, so await them directly or execute them with `run()`. ### `cursor()` -Return an `AsyncGenerator` that yields results one at a time. This is useful for processing large result sets without loading everything into memory. +Returns an `AsyncGenerator` that yields results one at a time. Each `next()` call reads one result from the provider, so you can process a large result set without holding all of it at once. ```typescript fragment const cursor = schema.instance().table("users").cursor(); @@ -59,7 +68,7 @@ while (!result.done) { ### `for await...of` -The `[Symbol.asyncIterator]()` method enables the `for await...of` syntax: +The `[Symbol.asyncIterator]()` method returns the query's cursor, which enables the `for await...of` syntax: ```typescript fragment for await (const user of schema.instance().table("users")) { @@ -67,9 +76,9 @@ for await (const user of schema.instance().table("users")) { } ``` -## Query Composition +## Query composition -Queries use lazy evaluation. Operations are recorded as pipeline stages and only execute when `run()`, `then()`, or iteration begins. This means you can build queries incrementally: +Queries are lazy. Each operation records a pipeline stage, and the stages execute only when `run()`, `then()`, or iteration starts. You can therefore build queries step by step: ```typescript fragment const usersTable = schema.instance().table("users"); diff --git a/docs/1.query_types/8.datum.md b/docs/1.query_types/8.datum.md index d3c29c5..203929c 100644 --- a/docs/1.query_types/8.datum.md +++ b/docs/1.query_types/8.datum.md @@ -1,30 +1,46 @@ -# Datum +--- +title: Datum +description: The Datum query type for a single value, with its field access, default, transformation, lookup, pluck, and cast methods. +mode: reference +--- -The `Datum` class represents a single value in the query system. It extends `Query` and provides methods for accessing fields, applying transformations, and performing foreign key lookups on individual values. +The `Datum` class represents a single value in a query. It extends `Query` and adds methods that read fields, apply transformations, and perform foreign key lookups on that value. -## Class Definition +## Class definition ```typescript fragment class Datum extends Query { cast(): Datum; - key, U = undefined>(key: K, def?: U): Datum< + key & string, U = undefined>( + key: K, + def?: U & QueryValue, + ): Datum< U extends undefined ? NonNullable[K] : NonNullable[K]> | U >; - default(val: Value): Datum | U>; + default( + val: Value, + ): Datum | U>; do(mapper: (val: ValueProxy) => U): Datum>; - lookup(other: Selection, localKey: keyof T, otherKey: keyof U): Datum<...>; - pluck(...fields: string[]): Datum>; + lookup( + other: Stream, + localKey: TK, + otherKey: keyof U & string, + ): Datum & Record>; + pluck & string>( + ...fields: K[] + ): Datum, K> | Extract>; } ``` -## Field Access +## Field access -The `key` method retrieves a field from a document datum. You can chain `key` calls for nested field access. A non-undefined fallback removes `null` and `undefined` from the declared field result and adds the fallback's type. Omitting the fallback preserves the declared field type. +The `key` method reads a field from a document datum. Chain `key` calls to read a nested field. A fallback that is not `undefined` removes `null` and `undefined` from the declared field type and adds the fallback's type. Without a fallback, the declared field type is unchanged. ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error("Define the myapp schema first"); const users = schema.instance().table("users"); // Access a field @@ -37,9 +53,9 @@ const city = await users.get("user-123").key("address").key("city"); const phone = await users.get("user-123").key("phone", "N/A"); ``` -## Default Values +## Default values -The `default` method provides a fallback value when the datum is null or undefined. +The `default` method gives a fallback value for a datum that is `null` or `undefined`. ```typescript fragment const user = await users.get("maybe-missing").default({ @@ -51,7 +67,7 @@ const user = await users.get("maybe-missing").default({ ## Transform with `do()` -The `do` method applies a transformation function to the datum. The callback receives a `ValueProxy` that provides operators for building expressions. +The `do` method applies a transformation function to the datum. The callback receives a `ValueProxy`, whose operators build expressions. ```typescript fragment const formatted = await users.get("user-123").do((user) => ({ @@ -61,9 +77,9 @@ const formatted = await users.get("user-123").do((user) => ({ })); ``` -## Foreign Key Lookup +## Foreign key lookup -The `lookup` method replaces a foreign key field with the referenced document from another table. +The `lookup` method replaces a foreign key field with the referenced document from another stream, such as a table. A field that holds a list of keys becomes a list of documents. ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -79,7 +95,8 @@ interface User { name: string; } -const blog = Schema.get<{ posts: Post; users: User }>("blog")!; +const blog = Schema.get<{ posts: Post; users: User }>("blog"); +if (!blog) throw new Error("Define the blog schema first"); const post = await blog .instance() .table("posts") @@ -90,17 +107,17 @@ const post = await blog console.log(post.authorId.name); ``` -## Field Selection +## Field selection -The `pluck` method selects specific fields from a document datum, discarding the rest. +The `pluck` method keeps the named fields of a document datum and discards the rest. ```typescript fragment const profile = await users.get("user-123").pluck("name", "email", "avatar"); ``` -## Type Casting +## Type casting -The `cast` method changes the TypeScript type of the datum without performing any runtime conversion. Use this when you know the actual type differs from what the compiler infers. +The `cast` method changes the TypeScript type of the datum and performs no runtime conversion. Use it when the actual type differs from what the compiler infers. Nothing checks that the new type matches what the query returns. ```typescript fragment const raw = users.get("user-123").cast<{ name: string; role: string }>(); diff --git a/docs/1.query_types/9.valueproxy.md b/docs/1.query_types/9.valueproxy.md index 43e1f15..86be34f 100644 --- a/docs/1.query_types/9.valueproxy.md +++ b/docs/1.query_types/9.valueproxy.md @@ -1,14 +1,18 @@ -# ValueProxy +--- +title: ValueProxy +description: The ValueProxy class that query callbacks receive, and its operators on any value, numbers, dates, strings, arrays, and objects. +mode: reference +--- -The `ValueProxy` class acts as a proxy to a database value. Operations on a ValueProxy are not executed immediately but are recorded as query stages. ValueProxy objects appear inside callback functions for `map`, `filter`, `do`, `update`, `join`, and other query methods. +The `ValueProxy` class is a proxy to a value in the database. An operation on a `ValueProxy` does not run right away. The proxy records it as a query stage. Callbacks of `map`, `filter`, `do`, `update`, `join`, and other query methods receive `ValueProxy` objects. -## Class Definition +## Class definition -ValueProxy provides type-specific methods based on the underlying value type. The available operations are organized by category. +Which `ValueProxy` methods compile depends on the type of the underlying value. This page groups the operations by that type. -## Create a ValueProxy +## Create a `ValueProxy` -ValueProxy instances are created automatically when you use callback-based query methods. You can also create one from a constant value: +Query methods that take a callback create the `ValueProxy` instances they pass to it. `ValueProxy.constant()` creates one from a constant value: ```typescript import { ValueProxy } from "@antelopejs/interface-database"; @@ -21,19 +25,19 @@ A constant, and any operand a stage records (`eq`, `ne`, `default`, `gt`, `inclu `eq`, `ne`, `gt`, `ge`, `lt`, `le` and `includes` check the operand against the type of the proxy, so a record type declared with `interface` is accepted there. `ValueProxy.constant`, `default`, `sub`, `merge` and the default of `key` infer their type from the argument and need a `QueryValue`: an `interface` has no implicit index signature and does not compile, so declare the record with `type`. -## General Operations +## General operations -These methods are available on all ValueProxy types. +These methods are available on every `ValueProxy`. ### `default(value)` -Return the given value if the proxy is null. +Returns the given value if the proxy is `null`. ```typescript fragment const phone = user.key("phone").default("N/A"); ``` -### `eq(value)` / `ne(value)` +### `eq(value)` and `ne(value)` Equality and inequality comparison. @@ -42,9 +46,9 @@ const isActive = user.key("status").eq("active"); const isNotAdmin = user.key("role").ne("admin"); ``` -### `and(value)` / `or(value)` / `not()` +### `and(value)`, `or(value)`, and `not()` -Logical operators for combining boolean expressions. The operand of `and` and `or` compiles when it is a `boolean`, `null`, `undefined`, `any`, or a `ValueProxy` of `boolean`, `boolean | null` or `boolean | undefined`. A `string`, a `number`, a `ValueProxy`, a `ValueProxy`, a generic `ValueProxy` and a union such as `ValueProxy | undefined` are compile-time errors. The database evaluates operands by truthiness, so the type is narrower than what the runtime accepts. +Logical operators that combine boolean expressions. The operand of `and` and `or` compiles when it is a `boolean`, `null`, `undefined`, `any`, or a `ValueProxy` of `boolean`, `boolean | null` or `boolean | undefined`. A `string`, a `number`, a `ValueProxy`, a `ValueProxy`, a generic `ValueProxy` and a union such as `ValueProxy | undefined` are compile-time errors. The database evaluates operands by truthiness, so the type accepts less than the runtime does. ```typescript fragment const canAccess = user.key("active").and(user.key("verified")); @@ -57,11 +61,11 @@ const isInactive = user.key("active").not(); ### `cast()` -Change the TypeScript type without runtime conversion. +Changes the TypeScript type without a runtime conversion. Nothing checks that the new type matches the value. -## Number Operations +## Number operations -Available when the proxy wraps a numeric value. +These methods compile when the proxy wraps a number. ### Arithmetic @@ -93,9 +97,9 @@ const unshifted = flags.brshift(2); const signPreserved = flags.brshiftPreserveSign(2); ``` -## Comparison Operations +## Comparison operations -Available for `number`, `string`, and `Date` proxy values. +These methods compile for `number`, `string`, and `Date` proxy values. ```typescript fragment const isAdult = age.gt(17); @@ -111,9 +115,9 @@ const isBefore = date.lt(deadline); | `lt(v)` | Less than | | `le(v)` | Less than or equal | -## Date Operations +## Date operations -Available when the proxy wraps a `Date` value. All date component methods accept an optional `timezone` parameter. +These methods compile when the proxy wraps a `Date` value. Every component method except `epoch()` accepts an optional `timezone` parameter. `add` takes a number of seconds. ### Arithmetic @@ -137,7 +141,7 @@ const tod = created.timeofday(); // seconds since midnight const epoch = created.epoch(); // seconds since UNIX epoch ``` -### Range Check +### Range check ```typescript fragment const inRange = created.during( @@ -146,11 +150,11 @@ const inRange = created.during( ); ``` -`during` also takes bounds as milliseconds since the UNIX epoch, as `new Date(n)` reads them. +The lower bound of `during` is inclusive, and the upper bound is exclusive. `during` also takes plain `Date` bounds, or bounds as milliseconds since the UNIX epoch, as `new Date(n)` reads them. -## String Operations +## String operations -Available when the proxy wraps a `string` value. `add` concatenates two strings, as `concat` does, and two lists on a list. A `null` operand answers `null` for any receiver of `add`. +These methods compile when the proxy wraps a `string` value. `add` concatenates two strings, as `concat` does, and two lists on a list. A `null` operand answers `null` for any receiver of `add`. ```typescript fragment const upper = name.upcase(); @@ -161,18 +165,18 @@ const len = name.strlen(); const matches = email.match("^.*@gmail\\.com$"); ``` -| Method | Description | -| ------------------ | ------------------------------- | -| `upcase()` | Convert to uppercase | -| `downcase()` | Convert to lowercase | -| `split(sep, max?)` | Split by separator | -| `concat(other)` | Concatenate with another string | -| `strlen()` | Number of Unicode codepoints | -| `match(regex)` | Test against a regex pattern | +| Method | Description | +| ------------------- | ------------------------------- | +| `upcase()` | Convert to uppercase | +| `downcase()` | Convert to lowercase | +| `split(sep?, max?)` | Split by separator | +| `concat(other)` | Concatenate with another string | +| `strlen()` | Number of Unicode codepoints | +| `match(regex)` | Test against a regex pattern | -## Array Operations +## Array operations -Available when the proxy wraps an array value. +These methods compile when the proxy wraps an array value. ```typescript fragment const first = tags.index(0); @@ -192,9 +196,9 @@ const highest = scores.max(); | --------------- | ----------------------------------------------------------------------------- | | `index(n)` | Element at position `n` | | `includes(val)` | Check if the array contains a value | -| `slice(s, e?)` | Extract a sub-array | +| `slice(s, e?)` | Extract a sub-array from `s` (inclusive) to `e` (exclusive) | | `map(fn)` | Transform each element | -| `filter(fn)` | Keep elements matching a predicate; a number keeps the element unless it is 0 | +| `filter(fn)` | Keep elements matching a predicate. A number keeps the element unless it is 0 | | `isempty()` | Check if the array is empty | | `count()` | Number of elements | | `sum()` | Sum of numeric elements | @@ -202,11 +206,11 @@ const highest = scores.max(); | `min()` | Minimum of numeric, string or date elements | | `max()` | Maximum of numeric, string or date elements | -## Object Operations +## Object operations -Available when the proxy wraps a record/object value. +These methods compile when the proxy wraps an object value. -`key(field, fallback)` preserves the declared field type when the fallback is omitted or `undefined`. Otherwise, the proxy's value type is `Exclude | FallbackType`. The fallback may itself be a `ValueProxy`, so its value type participates in the result. +`key(field, fallback)` keeps the declared field type when the fallback is omitted or `undefined`. Otherwise, the proxy's value type is `Exclude | FallbackType`. The fallback can itself be a `ValueProxy`, and then its value type is part of the result. ```typescript fragment const email = user.key("email"); @@ -225,9 +229,9 @@ const hasRequired = user.hasfields("email", "name"); | `values()` | Array of field values | | `hasfields(...fs)` | Check if all specified fields exist | -## Compose Expressions +## Compose expressions -ValueProxy methods are chainable, enabling complex expressions within query callbacks: +You can chain `ValueProxy` methods to build larger expressions inside query callbacks: ```typescript fragment const result = await users.filter((user) => diff --git a/docs/2.operations/1.index.md b/docs/2.operations/1.index.md index 7c5bab5..b250ffe 100644 --- a/docs/2.operations/1.index.md +++ b/docs/2.operations/1.index.md @@ -1,35 +1,39 @@ -# Operations +--- +title: Operations +description: The AQL operation guides, grouped by what they act on, from schemas and instances to documents, indexes, and queries. +mode: reference +--- -This section covers practical operations you can perform with the AQL interface. These guides walk through schema management, table access, document CRUD, index usage, filtering, and foreign key lookups. +The pages in this section show the operations of the AQL interface: schema management, table access, document CRUD, index use, filtering, and foreign key lookups. -## Schema and Instance Management +## Schema and instance management AQL organizes data through schemas and instances: -- [2. Schema Management](./2.database_management.md) - Define schemas, create and destroy instances +- [2. Schema management](./2.database_management.md). Define schemas, then create and destroy instances. -## Table Access +## Table access Tables live inside schema instances: -- [3. Table Operations](./3.table_operations.md) - Access tables and work with their contents +- [3. Table operations](./3.table_operations.md). Access tables and work with their contents. -## Document Operations +## Document operations -Documents are the fundamental unit of data in AQL: +Documents are the unit of data in AQL: -- [4. CRUD Operations](./4.crud.md) - Insert, read, update, replace, and delete documents -- [8. Atomic Mutations](./8.atomic_mutations.md) - Conditional single-record updates and deletes, with explicit outcomes +- [4. CRUD operations](./4.crud.md). Insert, read, update, replace, and delete documents. +- [8. Atomic single-record mutations](./8.atomic_mutations.md). Conditional single-record updates and deletes, with explicit outcomes. -## Index Operations +## Index operations -Indexes accelerate queries by enabling efficient lookups: +Indexes let the database find documents without a full scan: -- [5. Index Management](./5.indexes.md) - Define secondary indexes in schema definitions +- [5. Index management](./5.indexes.md). Define secondary indexes in schema definitions. -## Query Operations +## Query operations Transform and filter data with the query builder: -- [6. Filtering and Querying](./6.filtering.md) - Filter, compare, and transform documents -- [7. Lookup](./7.lookup.md) - Foreign key joins and data population +- [6. Filtering and querying](./6.filtering.md). Filter, compare, and transform documents. +- [7. Lookup](./7.lookup.md). Foreign key joins that replace a key with the document it references. diff --git a/docs/2.operations/2.database_management.md b/docs/2.operations/2.database_management.md index 3d40e69..8f8bacd 100644 --- a/docs/2.operations/2.database_management.md +++ b/docs/2.operations/2.database_management.md @@ -1,10 +1,14 @@ -# Schema Management +--- +title: Schema management +description: Define an AQL schema, then create, access, list, and destroy its instances, one per tenant if you need isolation. +mode: how-to +--- -AQL organizes data through schemas and instances. A `Schema` defines the structure of your database (tables, fields, and indexes). Each schema can have multiple instances, and the underlying module implementation determines how instances map to physical storage. +AQL organizes data through schemas and instances. A `Schema` defines the structure of your database: tables, fields, and indexes. Each schema can have many instances, and the database module that implements the interface decides how instances map to physical storage. -## Define a Schema +## Define a schema -Create a schema by providing an ID and a definition of tables with their fields and indexes: +Create a schema from an ID and a definition of its tables, each with its fields and indexes: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -26,9 +30,11 @@ const schema = new Schema("myapp", { }); ``` +The ID names the stored data. Changing it leaves the data stored under the previous ID out of reach. + ### `SchemaDefinition` -A schema definition is a record mapping table names to their `TableDefinition`: +A schema definition is a record that maps each table name to its `TableDefinition`: ```typescript import type { FieldType } from "@antelopejs/interface-database/schema"; @@ -46,9 +52,9 @@ interface IndexDefinition { } ``` -## Create an Instance +## Create an instance -After defining a schema, create an instance to start working with data. The optional `id` parameter distinguishes multiple instances of the same schema. +After you define a schema, create an instance to start working with data. The optional `id` parameter tells instances of the same schema apart, and `createInstance()` resolves to the ID of the created instance. ```typescript fragment // Create the default instance @@ -58,9 +64,9 @@ await schema.createInstance(); await schema.createInstance("tenant-abc"); ``` -## Access an Instance +## Access an instance -Retrieve a `SchemaInstance` to access tables: +Get a `SchemaInstance` to access its tables: ```typescript fragment // Default instance @@ -70,9 +76,7 @@ const db = schema.instance(); const tenantDb = schema.instance("tenant-abc"); ``` -To operate across every instance of the schema, pass the `CROSS_INSTANCE` sentinel: - -This deliberately removes the instance boundary for supported queries, including updates and deletes. Keep it out of tenant-controlled request paths. Selecting an instance is not a substitute for authorizing the caller. +To operate across every instance of the schema, pass the `CROSS_INSTANCE` sentinel. It removes the instance boundary for the queries that support it, updates and deletes included. Keep it out of request paths that a tenant controls. Selecting an instance does not authorize the caller. ```typescript fragment import { CROSS_INSTANCE } from "@antelopejs/interface-database"; @@ -80,7 +84,7 @@ import { CROSS_INSTANCE } from "@antelopejs/interface-database"; const everything = schema.instance(CROSS_INSTANCE).table("users"); ``` -## Destroy an Instance +## Destroy an instance Remove an instance and its data: @@ -88,20 +92,20 @@ Remove an instance and its data: await schema.destroyInstance("tenant-abc"); ``` -## List Instances +## List instances -List the IDs of named instances of a schema: +List the IDs of the named instances of a schema: ```typescript fragment const ids = await schema.listInstances(); // ["tenant-abc", "tenant-xyz"] ``` -The default (unnamed) instance is not included. +The list leaves out the default (unnamed) instance. -## Retrieve a Schema +## Retrieve a schema -Use the static `Schema.get()` method to look up a previously defined schema by its ID: +Look up a schema defined elsewhere by its ID with the static `Schema.get()` method. It returns `undefined` when no schema has that ID: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -113,9 +117,9 @@ if (schema) { } ``` -## Work with Tables +## Work with tables -Put the table-to-document mapping on `Schema`, then access tables by name. The generic parameter of `table()` is a key of that mapping, not a document type: +Put the table-to-document mapping on `Schema`, then access tables by name. The type parameter of `table()` is a key of that mapping, not a document type: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -134,17 +138,19 @@ interface Post { authorId: string; } -const typedSchema = Schema.get<{ users: User; posts: Post }>("myapp")!; +const typedSchema = Schema.get<{ users: User; posts: Post }>("myapp"); +if (!typedSchema) throw new Error('Schema "myapp" is not defined'); + const db = typedSchema.instance(); const users = db.table("users"); // Table const posts = db.table("posts"); // Table ``` -Use the same mapping when declaring a schema: `new Schema<{ users: User; posts: Post }>("myapp", definition)`. This type argument describes the schema; it does not validate stored data. +Use the same mapping when you declare a schema: `new Schema<{ users: User; posts: Post }>("myapp", definition)`. The type argument describes the schema. It does not validate stored data, and `Schema.get()` does not check it against the type arguments of the constructor call that registered the ID. -## Multi-Tenant Pattern +## Multi-tenant pattern -Schemas with multiple instances support multi-tenant architectures. Each tenant gets an isolated instance of the same schema: +A schema with many instances suits a multi-tenant application. Each tenant gets an isolated instance of the same schema: ```typescript import { Schema } from "@antelopejs/interface-database"; diff --git a/docs/2.operations/3.table_operations.md b/docs/2.operations/3.table_operations.md index ae94629..f4aeb09 100644 --- a/docs/2.operations/3.table_operations.md +++ b/docs/2.operations/3.table_operations.md @@ -1,22 +1,28 @@ -# Table Operations +--- +title: Table operations +description: Get a table from a schema instance, type it through the schema mapping, and read, chain, and insert documents on it. +mode: how-to +--- -Tables are the primary containers for documents in AQL. They are accessed through a `SchemaInstance` and provide methods for inserting, retrieving, and querying documents. +A table holds documents in AQL. You get one from a `SchemaInstance`, and it has methods to insert, retrieve, and query documents. -## Access a Table +## Access a table -Retrieve a table reference from a schema instance: +Get a table reference from a schema instance: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const db = schema.instance(); const users = db.table("users"); ``` -## Type-Safe Tables +## Type-safe tables -Provide the table-to-document mapping to `Schema`, not to `table()`: +Give the table-to-document mapping to `Schema`, not to `table()`: ```ts docs-check import { Schema } from "@antelopejs/interface-database"; @@ -46,7 +52,7 @@ const schema = new Schema<{ users: User }>("myapp", { }); const users = schema.instance().table("users"); -// TypeScript now validates field names and types +// TypeScript now checks field names and types export async function readUser() { const user = await users.get("user-123"); // Declared as User const emails = await users.key("email"); // string[] @@ -54,20 +60,20 @@ export async function readUser() { } ``` -`get()` declares `SingleSelection`, so awaiting it is typed as `User`, not `User | null`. This declaration does not guarantee that a row exists: a provider may return `null` for a missing key. Check the result before accessing fields. `Schema`'s generic mapping describes documents but does not validate database contents. +`get()` returns `SingleSelection`, so awaiting it is typed as `User`, not `User | null`. The type does not guarantee that a row exists: a provider may return `null` for a missing key. Check the result before you read its fields. The mapping on `Schema` describes documents. It does not validate what the database holds. -## Basic Operations +## Basic operations -### Retrieve All Documents +### Retrieve all documents -A table reference doubles as a stream of all its documents: +A table reference is also a stream of all its documents: ```typescript fragment const allUsers = await users; console.log(`Total users: ${allUsers.length}`); ``` -### Retrieve by Primary Key +### Retrieve by primary key ```typescript fragment const user = await users.get("user-123"); @@ -77,13 +83,17 @@ if (user) { } ``` -### Retrieve by Secondary Index +### Retrieve by secondary index ```typescript fragment const admins = await users.getAll("admin", "role"); ``` -### Retrieve by Range +Without an index name, `getAll()` matches the primary key. + +### Retrieve by range + +`between()` takes an index name, a lowest value, and a highest value. The highest value is excluded. ```typescript fragment const recent = await users.between( @@ -93,9 +103,9 @@ const recent = await users.between( ); ``` -## Chain Operations +## Chain operations -Since `Table` extends `Selection` and `Stream`, you can chain any stream operation directly: +`Table` extends `Selection`, which extends `Stream`, so you can chain any stream operation on a table: ```typescript fragment // Filter and sort @@ -114,7 +124,7 @@ const avgAge = await users.avg("age"); const names = await users.map((u) => u.key("name")); ``` -## Insert Documents +## Insert documents ```typescript fragment // Single document @@ -137,4 +147,4 @@ await users.insert( ); ``` -For complete CRUD documentation, see [CRUD Operations](./4.crud.md). +For every create, read, update, and delete operation, see [CRUD operations](./4.crud.md). diff --git a/docs/2.operations/4.crud.md b/docs/2.operations/4.crud.md index b94eb4b..25c37e8 100644 --- a/docs/2.operations/4.crud.md +++ b/docs/2.operations/4.crud.md @@ -1,17 +1,25 @@ -# CRUD Operations +--- +title: CRUD operations +description: Insert, read, update, replace, and delete AQL documents, including upserts, duplicate keys, and batch inserts. +mode: how-to +--- -CRUD operations (Create, Read, Update, Delete) form the foundation of document management in AQL. All examples assume a schema is already defined and an instance is available. +CRUD stands for create, read, update, and delete. The examples on this page assume a schema is already defined and use its default instance: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const users = schema.instance().table("users"); ``` -## Create Documents +## Create documents + +### Insert a single document -### Insert a Single Document +`insert()` resolves to the IDs of the inserted documents. ```typescript fragment const ids = await users.insert({ @@ -24,7 +32,7 @@ const ids = await users.insert({ console.log("Inserted ID:", ids[0]); ``` -### Insert Multiple Documents +### Insert multiple documents ```typescript fragment const ids = await users.insert([ @@ -35,9 +43,9 @@ const ids = await users.insert([ console.log("Inserted IDs:", ids); ``` -### Duplicate Keys +### Duplicate keys -Without a `conflict` option, inserting a row whose `_id`, or whose key in a [unique index](./5.indexes.md#unique-indexes), is already taken in the instance throws a `DuplicateKeyError`. Updates and replacements that would duplicate a unique key throw it too. When a batch insert hits a duplicate, whether its other rows are stored depends on the database. +Without a `conflict` option, an insert throws a `DuplicateKeyError` when the instance already holds the row's `_id` or its key in a [unique index](./5.indexes.md#unique-indexes). An update or a replacement that would duplicate a unique key throws it too. Nothing of the refused write is stored for the row that hit the duplicate. When a batch insert hits a duplicate, the database decides whether its other rows are stored. ```typescript fragment import { IsDuplicateKeyError } from "@antelopejs/interface-database"; @@ -50,9 +58,9 @@ try { } ``` -### Upsert (Insert or Update) +### Upsert (insert or update) -Use the `conflict` option to handle documents that already exist: +To handle documents that already exist, set the `conflict` option to `"update"` or `"replace"`: ```typescript fragment // Update existing fields, keep unmentioned fields @@ -68,9 +76,9 @@ await users.insert( ); ``` -## Read Documents +## Read documents -### Get by Primary Key +### Get by primary key ```typescript fragment const user = await users.get("user-123"); @@ -82,14 +90,16 @@ if (user) { } ``` -### Get by Secondary Index +### Get by secondary index ```typescript fragment const admins = await users.getAll("admin", "role"); console.log(`Found ${admins.length} admins`); ``` -### Get by Range +### Get by range + +The upper bound of `between()` is excluded. ```typescript fragment const recent = await users.between( @@ -99,7 +109,7 @@ const recent = await users.between( ); ``` -### Filter Documents +### Filter documents ```typescript fragment // Filter with a predicate function @@ -108,7 +118,7 @@ const active = await users.filter((user) => ); ``` -### Select Specific Fields +### Select specific fields ```typescript fragment // Keep only listed fields @@ -118,16 +128,18 @@ const profiles = await users.pluck("_id", "name", "avatar"); const safe = await users.without("password", "ssn"); ``` -### Order and Paginate +### Order and paginate + +`slice()` takes an offset and a count. ```typescript fragment const page1 = await users.orderBy("createdAt", "desc").slice(0, 10); const page2 = await users.orderBy("createdAt", "desc").slice(10, 10); ``` -### Stream Results +### Stream results -For large result sets, use async iteration to process documents one at a time: +For a large result set, iterate with `for await` to process one document at a time: ```typescript fragment for await (const user of users) { @@ -135,11 +147,13 @@ for await (const user of users) { } ``` -## Update Documents +## Update documents -### Update a Single Document +`update()` takes either a partial document or a function that builds one. It returns a `Query` that resolves to the number of modified documents. -The `update` method modifies specified fields while preserving all other fields. +### Update a single document + +Pass a partial document. `update()` changes the fields it names and keeps every other field. ```typescript fragment const count = await users.get("user-123").update({ @@ -152,9 +166,11 @@ if (count === 1) { } ``` -### Update with Computed Values +Give the partial document a real type. A value typed `any` matches the function overload of `update()`, and that call is typed `never`, so it has no `.run()`. + +### Update with computed values -Pass a function to compute new values based on current ones: +Pass a function to compute new values from the current ones: ```typescript fragment await users.get("user-123").update((user) => ({ @@ -163,7 +179,9 @@ await users.get("user-123").update((user) => ({ })); ``` -### Update Multiple Documents +The call is typed `Query` when the object the function returns fits the document type, and `never` otherwise. + +### Update multiple documents ```typescript fragment const count = await users @@ -173,9 +191,9 @@ const count = await users console.log(`Activated ${count} users`); ``` -## Replace Documents +## Replace documents -The `replace` method substitutes the entire document. Fields not included in the replacement are removed. +`replace()` swaps in the whole document. It removes the fields that the replacement leaves out, and it resolves to the number of modified documents. ```typescript fragment const count = await users.get("user-123").replace({ @@ -187,9 +205,11 @@ const count = await users.get("user-123").replace({ }); ``` -## Delete Documents +## Delete documents + +`delete()` resolves to the number of deleted documents. -### Delete a Single Document +### Delete a single document ```typescript fragment const count = await users.get("user-123").delete(); @@ -199,7 +219,7 @@ if (count === 1) { } ``` -### Delete Multiple Documents +### Delete multiple documents ```typescript fragment const count = await users @@ -209,9 +229,9 @@ const count = await users console.log(`Deleted ${count} inactive users`); ``` -## Batch Operations +## Batch operations -For large-scale inserts, batch documents to manage memory and throughput: +For a large insert, split the documents into batches to bound memory use and the size of each write: ```typescript fragment const documents = Array.from({ length: 10000 }, (_, i) => ({ diff --git a/docs/2.operations/5.indexes.md b/docs/2.operations/5.indexes.md index 52a7c78..402adf9 100644 --- a/docs/2.operations/5.indexes.md +++ b/docs/2.operations/5.indexes.md @@ -1,10 +1,14 @@ -# Index Management +--- +title: Index management +description: Declare simple, compound, multi, cross-instance, and unique indexes in an AQL schema, and query through them. +mode: how-to +--- -Indexes improve query performance by enabling the database to locate documents quickly based on indexed fields. In AQL, indexes are defined as part of the schema definition. +An index lets the database find documents by the indexed fields without scanning the whole table. In AQL, you declare indexes in the schema definition. -## Define Indexes in a Schema +## Define indexes in a schema -Indexes are declared in the `indexes` property of each table definition: +Declare indexes in the `indexes` property of each table definition: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -32,13 +36,13 @@ interface IndexDefinition { } ``` -When `fields` is omitted, the index name is used as the field name. For compound indexes, provide the `fields` array explicitly. +When you omit `fields`, the index name is the field name. For a compound index, list its fields in the `fields` array. -## Use Indexes for Queries +## Use indexes for queries -### `getAll()` with an Index +### `getAll()` with an index -Retrieve documents by index value: +Get the documents whose index value matches one or more keys: ```typescript fragment const db = schema.instance(); @@ -51,26 +55,26 @@ const admins = await users.getAll("admin", "role"); const targets = await users.getAll(["admin", "moderator"], "role"); ``` -### `between()` with an Index +### `between()` with an index -Retrieve documents where the index value falls within a range: +Get the documents whose index value falls in a range. The upper bound is excluded: ```typescript fragment const results = await users.between("age", 18, 65); ``` -### `orderBy()` with an Index +### `orderBy()` with an index -Sort results using an indexed field for better performance: +Sort results on an indexed field for better performance: ```typescript fragment const sorted = await users.orderBy("email", "asc"); const newest = await users.orderBy("createdAt", "desc"); ``` -## Compound Indexes +## Compound indexes -Compound indexes span multiple fields. Define them with the `fields` array: +A compound index spans several fields. Define it with the `fields` array: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -89,11 +93,11 @@ const schema = new Schema("blog", { }); ``` -Compound indexes support prefix-based lookups. You can query by the first field alone or by both fields together. +A compound index supports prefix lookups. You can query by the first field alone or by both fields together. -## Multi Indexes +## Multi indexes -Set `multi: true` for fields that contain arrays. The database creates an index entry for each element in the array, enabling lookups by any single element: +Set `multi: true` for a field that holds an array. The database creates an index entry for each element of the array, so a lookup by any single element finds the document: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -108,9 +112,9 @@ const schema = new Schema("content", { }); ``` -## Cross-Instance Indexes +## Cross-instance indexes -Indexes are optimized for queries scoped to one instance (`schema.instance(id)`). Queries run with `schema.instance(CROSS_INSTANCE)` can still use them, but may be slow. Set `crossInstance: true` on the indexes that cross-instance queries rely on, and the driver also maintains an index usable by those queries: +An index is optimized for queries scoped to one instance (`schema.instance(id)`). A query run with `schema.instance(CROSS_INSTANCE)` can still use it, but may be slow. Set `crossInstance: true` on the indexes that cross-instance queries rely on, and the driver also maintains an index usable by those queries: ```typescript import { CROSS_INSTANCE, Schema } from "@antelopejs/interface-database"; @@ -131,9 +135,9 @@ await schema .getAll("unpaid", "status"); ``` -The flag defaults to `false`. It never changes query results, only performance. Each flagged index costs extra storage and write overhead, so only set it where cross-instance queries use the index. +The flag defaults to `false`. It never changes query results, only performance. Each flagged index costs extra storage and write overhead, so set it only where cross-instance queries use the index. -## Unique Indexes +## Unique indexes Set `unique: true` to refuse a second row with the same key in one instance. The write that would store it throws a `DuplicateKeyError`, which names the index and its fields. The primary key `_id` is always unique and throws the same error, with `index` set to `"_id"`: @@ -165,13 +169,13 @@ try { } ``` -The key is unique within each instance: two instances may hold the same email. `crossInstance` does not change that. Give every row a value for the fields of a unique index, since databases differ on whether rows missing them conflict. +The key is unique within each instance, so two instances may hold the same email. `crossInstance` does not change that. Give every row a value for the fields of a unique index, because databases differ on whether rows that lack them conflict. -Recognize the error with `IsDuplicateKeyError` rather than `instanceof`: the database module may be bound to another copy of the interface. +Recognize the error with `IsDuplicateKeyError` rather than `instanceof`. The database module may be bound to another copy of the interface. -## Index Design Guidelines +## Index design guidelines -- **Index fields used in `getAll()` and `between()` calls** for best performance. -- **Compound indexes** are useful when you frequently filter or sort by multiple fields together. -- **Avoid over-indexing.** Each index adds storage overhead and slows down writes. -- **Filter early** in your query chain to reduce the number of documents processed. +- Index the fields that your `getAll()` and `between()` calls use. +- Add a compound index when you often filter or sort by the same fields together. +- Avoid over-indexing. Each index adds storage and slows down writes. +- Filter early in the query chain to reduce the number of documents processed. diff --git a/docs/2.operations/6.filtering.md b/docs/2.operations/6.filtering.md index ba51a12..d8be90d 100644 --- a/docs/2.operations/6.filtering.md +++ b/docs/2.operations/6.filtering.md @@ -1,32 +1,38 @@ -# Filtering and Querying +--- +title: Filtering and querying +description: Select, compare, and transform AQL documents with filter predicates and ValueProxy operators, then sort and paginate the result. +mode: how-to +--- -This guide covers techniques for selecting, comparing, and transforming documents using AQL's filter and ValueProxy capabilities. +This guide shows how to select, compare, and transform documents with `filter()` and the `ValueProxy` operators. The examples use this table: ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const users = schema.instance().table("users"); ``` -## Basic Filtering +## Basic filtering -Use the `filter` method with a predicate function. The callback receives a `ValueProxy` representing each document. +Call `filter()` with a predicate function. The predicate receives a `ValueProxy` that stands for each document. ```typescript fragment const active = await users.filter((user) => user.key("status").eq("active")); ``` -## Comparison Operators +## Comparison operators -### Equality and Inequality +### Equality and inequality ```typescript fragment const admins = await users.filter((u) => u.key("role").eq("admin")); const nonAdmins = await users.filter((u) => u.key("role").ne("admin")); ``` -### Numeric Comparisons +### Numeric comparisons ```typescript fragment const adults = await users.filter((u) => u.key("age").ge(18)); @@ -35,7 +41,7 @@ const young = await users.filter((u) => u.key("age").lt(18)); const discounted = await users.filter((u) => u.key("age").le(25)); ``` -## Logical Operators +## Logical operators Combine conditions with `and()`, `or()`, and `not()`: @@ -62,7 +68,9 @@ const target = await users.filter((u) => ); ``` -## String Operations +## String operations + +`match()` takes a regular expression as a string. ```typescript fragment // Case-insensitive match @@ -79,9 +87,9 @@ const fullNameMatch = await users.filter((u) => ); ``` -## Date Filtering +## Date filtering -### Date Comparisons +### Date comparisons ```typescript fragment const newUsers = await users.filter((u) => @@ -96,7 +104,7 @@ const q1Users = await users.filter((u) => ); ``` -### Date Components +### Date components ```typescript fragment const thisYear = await users.filter((u) => u.key("createdAt").year().eq(2024)); @@ -106,7 +114,7 @@ const morningLogins = await users.filter((u) => ); ``` -## Array Operations +## Array operations ```typescript fragment // Check array contents @@ -128,9 +136,9 @@ const untagged = await schema .filter((post) => post.key("tags").isempty()); ``` -## Default Values in Filters +## Default values in filters -Use `default()` to handle potentially null fields: +To compare a field that may be null, give it a fallback with `default()`: ```typescript fragment const activeUsers = await users.filter((u) => @@ -142,7 +150,9 @@ const frequentVisitors = await users.filter((u) => ); ``` -## Ordering and Pagination +## Ordering and pagination + +`orderBy()` takes a field and an optional direction, `"asc"` or `"desc"`. `slice()` takes an offset and a count. ```typescript fragment // Sort ascending @@ -156,12 +166,12 @@ const page1 = await users.orderBy("createdAt").slice(0, 10); const page2 = await users.orderBy("createdAt").slice(10, 10); ``` -## Performance Tips +## Performance tips -1. **Use `getAll()` and `between()` with indexes** instead of `filter()` when possible. Index-based lookups are significantly faster than full scans. -2. **Filter early** in the query chain to reduce the dataset before applying transformations or aggregations. -3. **Limit result sets** with `slice()` to avoid transferring unnecessary data. -4. **Avoid complex computations** inside filter predicates when simpler alternatives exist. +1. Use `getAll()` and `between()` with an index instead of `filter()` where you can. An index lookup avoids the full table scan that a `filter()` runs. +2. Filter early in the query chain, so later transformations and aggregations see fewer documents. +3. Limit result sets with `slice()` to avoid transferring data you do not use. +4. Keep filter predicates simple when a simpler predicate gives the same result. ```typescript fragment // Slower: full table scan with filter diff --git a/docs/2.operations/7.lookup.md b/docs/2.operations/7.lookup.md index 0a70c60..95b515f 100644 --- a/docs/2.operations/7.lookup.md +++ b/docs/2.operations/7.lookup.md @@ -1,24 +1,30 @@ -# Lookup +--- +title: Lookup +description: Replace a foreign key field of AQL documents with the document, or the array of documents, it references in another table. +mode: how-to +--- -The `lookup` method performs a foreign key join, replacing a field in your documents with the referenced data from another table. It is available on both `Stream` and `Datum`. +`lookup()` runs a foreign key join. It replaces a field of your documents with the documents that field references in another table. Both `Stream` and `Datum` have it. ## Parameters -| Parameter | Type | Description | -| ---------- | -------------- | --------------------------------------------------------- | -| `other` | `Selection` | The table containing the referenced documents | -| `localKey` | `keyof T` | The field in the current document holding the foreign key | -| `otherKey` | `keyof U` | The field in the other table to match against | +| Parameter | Type | Description | +| ---------- | ------------------ | --------------------------------------------------------- | +| `other` | `Stream` | The table containing the referenced documents | +| `localKey` | `keyof T & string` | The field in the current document holding the foreign key | +| `otherKey` | `keyof U & string` | The field in the other table to match against | + +`Stream.lookup()` names its first parameter `right`. `Datum.lookup()` names it `other`. ## Behavior -The `lookup` method replaces the local key field with the populated data: +`lookup()` replaces the local key field with the referenced data: -- If the local key contains a **single value**, the field becomes a **single document** from the other table. -- If the local key contains an **array of values**, the field becomes an **array of documents** from the other table. -- TypeScript automatically tracks the type transformation. +- If the local key holds a **single value**, the field becomes a **single document** from the other table. +- If the local key holds an **array of values**, the field becomes an **array of documents** from the other table. +- The result type follows the same rule: `Omit & Record`, where `TK` is the local key. -## One-to-One Relationship +## One-to-one relationship Replace a single foreign key with the referenced document: @@ -37,7 +43,9 @@ interface User { email: string; } -const schema = Schema.get<{ posts: Post; users: User }>("blog")!; +const schema = Schema.get<{ posts: Post; users: User }>("blog"); +if (!schema) throw new Error('Schema "blog" is not defined'); + const db = schema.instance(); const postsWithAuthors = await db @@ -50,7 +58,7 @@ for (const post of postsWithAuthors) { } ``` -## One-to-Many Relationship +## One-to-many relationship Replace an array of foreign keys with an array of documents: @@ -70,9 +78,10 @@ interface Product { price: number; } -const shop = Schema.get<{ orders: Order; products: Product }>( - "shop", -)!.instance(); +const shopSchema = Schema.get<{ orders: Order; products: Product }>("shop"); +if (!shopSchema) throw new Error('Schema "shop" is not defined'); + +const shop = shopSchema.instance(); const ordersWithProducts = await shop .table("orders") .lookup(shop.table("products"), "productIds", "_id"); @@ -85,13 +94,15 @@ for (const order of ordersWithProducts) { } ``` -## Combine with Other Operations +## Combine with other operations -Lookup works seamlessly with filters, sorting, and other stream operations: +`lookup()` chains with filters, sorting, and the other stream operations: ```typescript fragment // The shop schema also declares customers and orders.createdAt. -const db = Schema.get("shop")!.instance(); +const shopSchema = Schema.get("shop"); +if (!shopSchema) throw new Error('Schema "shop" is not defined'); +const db = shopSchema.instance(); // Filter before lookup const recentWithCustomers = await db @@ -100,25 +111,27 @@ const recentWithCustomers = await db .lookup(db.table("customers"), "customerId", "_id"); // Lookup on a single document -const blog = Schema.get<{ posts: Post; users: User }>("blog")!.instance(); +const blogSchema = Schema.get<{ posts: Post; users: User }>("blog"); +if (!blogSchema) throw new Error('Schema "blog" is not defined'); +const blog = blogSchema.instance(); const post = await blog .table("posts") .get("post-1") .lookup(blog.table("users"), "authorId", "_id"); ``` -## Lookup vs Join +## Lookup vs join ### Use `lookup` when: -- You have a straightforward foreign key relationship. +- You have a plain foreign key relationship. - You want the referenced data to replace the foreign key field. -- You are working with one-to-one or one-to-many relationships. +- You work with one-to-one or one-to-many relationships. ### Use `join` or `joinInner` when: -- You need custom join predicates beyond simple equality. -- You want to keep both original and joined data as separate fields. +- You need a join predicate other than equality of two fields. +- You want to keep the original and the joined data as separate fields. - You need to transform the result into a custom structure. ### Comparison diff --git a/docs/2.operations/8.atomic_mutations.md b/docs/2.operations/8.atomic_mutations.md index 56743b5..1a44b9b 100644 --- a/docs/2.operations/8.atomic_mutations.md +++ b/docs/2.operations/8.atomic_mutations.md @@ -1,12 +1,16 @@ -# Atomic single-record mutations +--- +title: Atomic single-record mutations +description: The contract of Table.atomicMutation, which checks one record's revision or one field value and patches or deletes that record in one database mutation. +mode: reference +--- -`Table.atomicMutation(id, request)` checks one immutable, instance-scoped record identity and one condition inside the database mutation. It returns `Query`. It does not provide multi-record transactions, a lock service, receipts, or external-effect coordination. +`Table.atomicMutation(id, request)` checks one immutable, instance-scoped record identity and one condition inside the database mutation. It returns `Query`. It does not provide multi-record transactions, a lock service, receipts, or coordination of external effects. -Generic `filter(...).update(...)` and `filter(...).delete()` do **not** promise this guarantee. Use a direct `schema.instance(instanceId).table(tableId)` with one literal string identity. The default instance is supported; `CROSS_INSTANCE`, filtered selections, expression keys, cursors, and upserts are not. +Generic `filter(...).update(...)` and `filter(...).delete()` do not give this guarantee. Call it on a direct `schema.instance(instanceId).table(tableId)` with one literal string identity. The default instance is supported. `CROSS_INSTANCE`, filtered selections, expression keys, cursors, and upserts are not. ## Compare a revision -Public types are exported from `@antelopejs/interface-database` and `@antelopejs/interface-database/atomic`. An update checks the current revision and installs the patch and next revision together. A delete checks the revision inside the deletion. +`@antelopejs/interface-database` and `@antelopejs/interface-database/atomic` export the public types. An update checks the current revision and installs the patch and the next revision together. A delete checks the revision inside the deletion. ```ts fragment const outcome = await table.atomicMutation(recordId, { @@ -24,19 +28,19 @@ const deletion = await table.atomicMutation(recordId, { }); ``` -Revision tokens are nonempty strings. Each update must install a changed token that the caller guarantees has never been used for that identity. All writers of protected state must advance the revision; otherwise a successful comparison does not prove that the observed state is current. Initialize new records with fresh revisions and use a new identity for each incarnation, including recreation after deletion. +Revision tokens are nonempty strings. Each update must install a changed token that the caller guarantees was never used for that identity. Every writer of protected state must advance the revision. If one does not, a successful comparison does not prove that the observed state is current. Initialize new records with fresh revisions, and use a new identity for each incarnation, including recreation after deletion. -`expectedRevision: { kind: "missing" }` matches an **absent revision field on an existing record**. It does not match a stored `null`, an empty string, or a missing record. An update can use this expectation to bootstrap legacy data atomically; a delete can use it when the domain permits deleting an unversioned record. A backend that cannot distinguish absence from null must reject this expectation rather than weaken it. In PostgreSQL, declared SQL columns collapse this distinction; an overflow JSON field can preserve it. Review mapper serialization before choosing revision storage. +`expectedRevision: { kind: "missing" }` matches an absent revision field on an existing record. It does not match a stored `null`, an empty string, or a missing record. An update can use this expectation to add revisions to existing unversioned records atomically. A delete can use it when the domain permits deleting an unversioned record. A backend that cannot tell absence from null must reject this expectation rather than weaken it. In PostgreSQL, declared SQL columns lose this distinction, and an overflow JSON field can keep it. Review how your mapper serializes the record before you choose where to store the revision. ## Patch semantics -The patch replaces each supplied top-level field. Nested objects and arrays replace their entire previous values; they do not merge recursively. Unspecified fields remain unchanged, and `null` stores null rather than removing the field. There is no unset operation. +The patch replaces each top-level field it supplies. Nested objects and arrays replace their entire previous values. They do not merge recursively. Unspecified fields remain unchanged, and `null` stores null rather than removing the field. There is no unset operation. -Patches contain only constant strings, booleans, finite numbers, null, valid Dates, dense arrays, and plain objects. Undefined, functions, query proxies, cycles, and operator/path field names are rejected. The patch cannot contain `id`, `_id`, the revision field, or adapter-owned scope fields. Adapters must preserve their normal Date storage representation without interpreting objects that resemble query stages as executable expressions. +A patch holds only constant strings, booleans, finite numbers, null, valid Dates, dense arrays, and plain objects. `ValidateAtomicMutation` rejects undefined, functions, query proxies, cycles, and field names that contain `.` or start with `$`. The patch cannot contain `id`, `_id`, the revision field, or scope fields that the adapter owns. Adapters must keep their normal Date storage representation, and must not run objects that resemble query stages as expressions. ## Delete by one observed scalar -`deleteIfEqual` provides a narrower condition for domains where one field equality is sufficient, such as deletion of an expired record whose observed timestamp remains unchanged. +`deleteIfEqual` is a narrower condition for domains where equality of one field is enough. An example is deleting an expired record whose observed timestamp has not changed. ```ts fragment const outcome = await table.atomicMutation(recordId, { @@ -46,9 +50,9 @@ const outcome = await table.atomicMutation(recordId, { }); ``` -`expectedValue` accepts a string, finite number, boolean, or valid Date. Equality is exact and type-sensitive, with Dates compared by their stored instant; scalar equality must not match an array containing that scalar. Missing fields never match. Null, objects, arrays, identity fields, and scope fields are not supported. +`expectedValue` accepts a string, a finite number, a boolean, or a valid Date. Equality is exact and type-sensitive, and Dates compare by their stored instant. A scalar must not match an array that contains that scalar. Missing fields never match. Null, objects, arrays, identity fields, and scope fields are not supported. -This variant does not check or advance a revision and does not prevent ABA: a value can change and later return to its observed value. The domain must prove that equality alone authorizes deletion. It is not a substitute for revision checks on state transitions. +This variant does not check or advance a revision, and it does not prevent ABA: a value can change and later return to its observed value. The domain must prove that equality alone authorizes the deletion. It does not replace revision checks on state transitions. ## Handle all outcomes @@ -58,14 +62,14 @@ This variant does not check or advance a revision and does not prevent ABA: a va | `not-applied` | The adapter received definitive acknowledgement that the target was missing or its scope or condition did not match. | | `unknown` | The adapter cannot determine whether the mutation applied, including acknowledgement loss. | -Definitive validation and unsupported-operation errors throw. Neither an exception nor `unknown` means mismatch. The interface rejects invalid adapter results, including `undefined`, zero, and generic modified counts, with `AtomicMutationUnsupportedError`; adapters must reject unsupported stages explicitly. There is no fallback to generic filtered writes and no separate capability probe. Use an adapter whose implementation has been verified before relying on this API. +Definitive validation errors and unsupported-operation errors throw. Neither an exception nor `unknown` means a mismatch. The interface rejects invalid adapter results, including `undefined`, zero, and generic modified counts, with `AtomicMutationUnsupportedError`. Adapters must reject unsupported stages explicitly. There is no fallback to generic filtered writes and no separate capability probe. Use an adapter whose implementation passed verification before you rely on this API. -The interface and adapter must not automatically retry an uncertain mutation. Do not repeat external effects or infer failure from a later mismatch. Domain-specific recovery can reconcile durable evidence and receipts, but a fresh read alone cannot prove whether an earlier operation or external effect occurred. Any deliberate replay requires a domain proof and does not turn `not-applied` into evidence about the original attempt. +The interface and the adapter must not retry an uncertain mutation on their own. Do not repeat external effects, and do not infer failure from a later mismatch. Domain-specific recovery can reconcile durable evidence and receipts, but a fresh read alone cannot prove whether an earlier operation or external effect occurred. Any deliberate replay requires a domain proof, and it does not turn `not-applied` into evidence about the original attempt. ## Adapter contract -The canonical terminal stage is `{ stage: "atomicMutation", options: undefined, args: [id, request] }`, immediately after `schema`, `instance`, and `table`. `AtomicMutation` is the union of `AtomicUpdate`, `AtomicDelete`, and `AtomicDeleteIfEqual`. +The canonical terminal stage is `{ stage: "atomicMutation", options: undefined, args: [id, request] }`, right after `schema`, `instance`, and `table`. `AtomicMutation` is the union of `AtomicUpdate`, `AtomicDelete`, and `AtomicDeleteIfEqual`. -Adapters can reuse `ValidateAtomicMutation(id, request, protectedFields)` and `ValidateAtomicMutationTable(prefixStages)`. They must additionally validate the terminal envelope and schema-specific restrictions. Scope, immutable identity, and the revision or equality condition must participate in the target-local native mutation, not only an earlier selection or identity subquery. Backend tests must prove contention, scope isolation, missing/null distinction, top-level replacement, and acknowledgement-loss behavior. +Adapters can reuse `ValidateAtomicMutation(id, request, protectedFields)` and `ValidateAtomicMutationTable(prefixStages)`. They must also validate the terminal envelope and the restrictions of their schema. Scope, immutable identity, and the revision or equality condition must take part in the native mutation on the target record, not only in an earlier selection or identity subquery. Backend tests must prove behavior under contention, scope isolation, the distinction between a missing field and null, top-level replacement, and behavior on acknowledgement loss. -This API remains unpublished during adapter and consumer verification. Package metadata alone does not establish capability; do not infer support from the unchanged development package version. +The metadata of an adapter's package does not show whether the adapter implements this contract. Check its implementation and its backend tests. diff --git a/docs/3.results/1.index.md b/docs/3.results/1.index.md index 4c6d650..044d552 100644 --- a/docs/3.results/1.index.md +++ b/docs/3.results/1.index.md @@ -1,22 +1,26 @@ -# Results +--- +title: Results +description: The values AQL queries resolve to, and the schema, table, and index definitions and change events they rest on. +mode: reference +--- -AQL operations return structured result values that communicate what happened during execution. This section covers the result types produced by write operations and change feeds. +An AQL query resolves to a value that says what the operation did. The pages in this section cover the results of write operations, the change events of change feeds, and the schema, table, and index definitions. -## Result Types +## Result types -- [2. Insert Results](./2.write_results.md) - Return values from insert operations -- [3. Index Definitions](./3.index_changes.md) - Index configuration in schema definitions -- [4. Table Definitions](./4.table_changes.md) - Table structure and field types -- [5. Schema and Instance Management](./5.database_changes.md) - Schema lifecycle and instance management -- [6. Change Events](./6.value_changes.md) - Structure of change feed notifications +- [2. Write results](./2.write_results.md). Return values of insert, update, replace, and delete. +- [3. Index definitions](./3.index_changes.md). Index configuration in schema definitions. +- [4. Table definitions](./4.table_changes.md). Table structure and field types. +- [5. Schema and instance management](./5.database_changes.md). Schema lifecycle and instance management. +- [6. Change events](./6.value_changes.md). Structure of change feed notifications. -## Common Patterns +## Common patterns -All AQL operations follow a consistent pattern: +Every AQL operation follows the same pattern: 1. Build a query by chaining operations. -2. Execute the query with `run()` or `await` to get the result. -3. Process the structured result. +2. Run the query with `run()` or `await` to get the result. +3. Process the result. ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -28,14 +32,16 @@ interface User { status: string; } -const schema = Schema.get<{ users: User }>("myapp")!; +const schema = Schema.get<{ users: User }>("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const users = schema.instance().table("users"); -// Insert returns an array of generated IDs +// Insert returns the IDs of the inserted documents const ids = await users.insert({ name: "Alice", email: "alice@example.com" }); console.log("New ID:", ids[0]); -// Update/delete return the number of affected documents +// Update returns the number of modified documents const count = await users.get("user-123").update({ status: "active" }); console.log(`Modified ${count} documents`); diff --git a/docs/3.results/2.write_results.md b/docs/3.results/2.write_results.md index d08ede7..1ee45ca 100644 --- a/docs/3.results/2.write_results.md +++ b/docs/3.results/2.write_results.md @@ -1,8 +1,12 @@ -# Insert Results +--- +title: Write results +description: What insert, update, replace, and delete resolve to in AQL, from the inserted IDs to the count of modified documents. +mode: reference +--- -The `insert` method on `Table` returns a `Query` that resolves to an array of the generated or provided primary keys for the inserted documents. +`Table.insert()` returns a `Query` that resolves to the primary keys of the inserted documents, generated or provided. -## Return Value +## Return value ```typescript fragment // Insert returns string[] (the IDs of inserted documents) @@ -14,7 +18,7 @@ const ids: string[] = await users.insert({ ## Examples -### Single Document Insert +### Single document insert ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -25,7 +29,9 @@ interface User { email: string; } -const schema = Schema.get<{ users: User }>("myapp")!; +const schema = Schema.get<{ users: User }>("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const users = schema.instance().table("users"); const ids = await users.insert({ @@ -36,7 +42,7 @@ const ids = await users.insert({ console.log("Inserted document ID:", ids[0]); ``` -### Multiple Document Insert +### Multiple document insert ```typescript fragment const ids = await users.insert([ @@ -49,9 +55,9 @@ console.log(`Inserted ${ids.length} documents`); console.log("IDs:", ids); ``` -### Insert with Conflict Handling +### Insert with conflict handling -Use the `conflict` option to control behavior when a document with the same primary key already exists: +The `conflict` option, `"update"` or `"replace"`, sets what happens when a document with the same primary key already exists: ```typescript fragment // Update matching fields on conflict @@ -67,16 +73,16 @@ const ids2 = await users.insert( ); ``` -## Update and Delete Results +## Update and delete results -The `update`, `replace`, and `delete` methods return a `Query` that resolves to the count of affected documents: +`update()`, `replace()`, and `delete()` on a `Selection` or a `SingleSelection` return a `Query`. For `update()` and `replace()`, the number is the count of modified documents. For `delete()`, it is the count of deleted documents. ```typescript fragment // Update returns the number of modified documents const updated = await users.get("user-123").update({ status: "active" }); console.log(`Modified: ${updated}`); -// Replace returns the number of replaced documents +// Replace returns the number of modified documents const replaced = await users.get("user-123").replace({ _id: "user-123", name: "New", @@ -90,3 +96,29 @@ const deleted = await users .delete(); console.log(`Deleted: ${deleted}`); ``` + +`update()` has two overloads. A function argument returns `Query` when the object it builds fits the document type, and `never` otherwise. A partial document returns `Query`. A value typed `any` matches the function overload, so that call is typed `never`. + +Read through its type, `update()` resolves to the plain document overload. `ReturnType` is `Query`, so the result of `run()` is a `number`: + +```typescript +import { Schema } from "@antelopejs/interface-database"; + +interface User { + _id: string; + name: string; + status: string; +} + +const schema = new Schema<{ users: User }>("myapp", { + users: { fields: { name: "string", status: "string" }, indexes: {} }, +}); +const users = schema.instance().table("users"); + +type Modified = Awaited["run"]>>; + +const modified: Modified = await users + .get("user-123") + .update({ status: "active" }); +console.log(`Modified: ${modified}`); +``` diff --git a/docs/3.results/3.index_changes.md b/docs/3.results/3.index_changes.md index b60a8c4..aa640d6 100644 --- a/docs/3.results/3.index_changes.md +++ b/docs/3.results/3.index_changes.md @@ -1,6 +1,10 @@ -# Index Definitions +--- +title: Index definitions +description: The IndexDefinition fields of an AQL schema, for simple, compound, multi, cross-instance, and unique indexes. +mode: reference +--- -In AQL, indexes are defined as part of the schema definition rather than created at runtime. The schema declaration specifies which fields to index and what type of index to use. +In AQL, the schema definition declares indexes. You do not create them at runtime. The declaration names the fields to index and the kind of index. ## `IndexDefinition` @@ -13,9 +17,9 @@ interface IndexDefinition { } ``` -## Define Indexes +## Define indexes -Indexes are declared in the `indexes` property of each table definition: +Each table definition declares its indexes in its `indexes` property: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -32,9 +36,9 @@ const schema = new Schema("myapp", { }); ``` -### Simple Indexes +### Simple indexes -When `fields` is omitted, the index name is used as the field name: +Without `fields`, the index name is the field name: ```typescript fragment indexes: { @@ -42,9 +46,9 @@ indexes: { } ``` -### Compound Indexes +### Compound indexes -Provide the `fields` array for indexes spanning multiple fields: +`fields` lists the fields of an index that spans several fields: ```typescript fragment indexes: { @@ -52,9 +56,9 @@ indexes: { } ``` -### Multi Indexes +### Multi indexes -Set `multi: true` for array fields where each element should be individually indexed: +`multi: true` indexes each element of an array field on its own: ```typescript fragment indexes: { @@ -62,9 +66,9 @@ indexes: { } ``` -### Cross-Instance Indexes +### Cross-instance indexes -Set `crossInstance: true` when queries run with `CROSS_INSTANCE` use the index. The driver then also maintains an index usable across all instances. Without it, the index is optimized for queries scoped to one instance and cross-instance queries using it may be slow. Results are never affected: +`crossInstance: true` marks an index that queries run with `CROSS_INSTANCE` use. The driver then also maintains an index usable across all instances. Without it, the index is optimized for queries scoped to one instance, and cross-instance queries that use it may be slow. The flag never changes results: ```typescript fragment indexes: { @@ -72,9 +76,9 @@ indexes: { } ``` -## Use Indexes in Queries +## Use indexes in queries -Once defined, indexes are available for `getAll()` and `between()` operations: +`getAll()` and `between()` query through a defined index: ```typescript fragment const db = schema.instance(); @@ -91,4 +95,4 @@ const recent = await users.between( ); ``` -For more details, see [Index Management](../2.operations/5.indexes.md). +For unique indexes and more examples, see [Index management](../2.operations/5.indexes.md). diff --git a/docs/3.results/4.table_changes.md b/docs/3.results/4.table_changes.md index c11f13d..3cc5ef1 100644 --- a/docs/3.results/4.table_changes.md +++ b/docs/3.results/4.table_changes.md @@ -1,6 +1,10 @@ -# Table Definitions +--- +title: Table definitions +description: The TableDefinition and FieldType shapes of an AQL schema, with string field tokens or io-ts codecs, and typed access to tables. +mode: reference +--- -In AQL, tables are defined as part of a schema. Each table definition specifies the fields and indexes for that table. +In AQL, a schema defines its tables. Each table definition lists the fields and indexes of that table. ## `TableDefinition` @@ -18,19 +22,34 @@ interface TableDefinition { ### `FieldType` -Field types describe the structure of each field: +A field type describes the structure of one field: ```typescript -import type { StringFieldType } from "@antelopejs/interface-database/schema"; - -type FieldType = - | string // Primitive type name (e.g., "string", "number") +type StringFieldType = + | string // Primitive type name, such as "string" or "number" | Array // Array of a given type - | { [subfield: string]: StringFieldType } // Nested object - | import("io-ts").Mixed; // io-ts codec at the field root + | { [subfield: string]: StringFieldType }; // Nested object + +interface IoTsValidation { + readonly _tag: "Left" | "Right"; +} + +type IoTsCodec = { + readonly _A: A; + readonly _O: O; + readonly _I: I; + readonly name: string; + is(u: A | I): boolean; + encode(a: A): O; + decode(i: I): IoTsValidation; +}; + +type FieldType = StringFieldType | IoTsCodec; // io-ts codec at the field root ``` -A field can be described either with the string-token vocabulary (primitives, arrays, nested records) or with a single [io-ts](https://github.com/gcanti/io-ts) codec at the root of the field. A codec is any io-ts `Type`: the interface only relies on its `name`, `is`, `encode` and `decode`, and `decode` must return an `Either`. Codecs are not mixed inside string-tree records — use io-ts combinators (`t.type`, `t.array`, …) to nest within a codec. +`@antelopejs/interface-database/schema` exports `StringFieldType` and `FieldType`. `IoTsCodec` and `IoTsValidation` are not exported. They are shown here as the shape a codec must have. + +A field uses either the string-token vocabulary (primitives, arrays, nested records) or a single [io-ts](https://github.com/gcanti/io-ts) codec at the root of the field. Any io-ts `Type` fits. The type requires its `_A`, `_O`, `_I`, `name`, `is`, `encode`, and `decode` members, and `decode` must return a value tagged `Left` or `Right`, as an io-ts `Either` is. A string-tree record cannot hold a codec. To nest inside a codec, use io-ts combinators such as `t.type` and `t.array`. ```typescript import * as t from "io-ts"; @@ -45,9 +64,9 @@ const Order = { }; ``` -## Define Tables in a Schema +## Define tables in a schema -Tables are declared in the `SchemaDefinition` passed to the `Schema` constructor: +The `SchemaDefinition` passed to the `Schema` constructor declares the tables: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -85,9 +104,9 @@ const schema = new Schema("myapp", { }); ``` -## Access Tables +## Access tables -After creating a schema instance, access tables by name: +After you create a schema instance, access its tables by name: ```typescript fragment await schema.createInstance(); @@ -96,7 +115,7 @@ const users = schema.instance().table("users"); const posts = schema.instance().table("posts"); ``` -For type-safe access, provide the table mapping when declaring or retrieving the schema. `table()` infers the document type from the table name: +For typed access, give the table mapping when you declare or retrieve the schema. `table()` infers the document type from the table name: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -108,8 +127,10 @@ interface User { age: number; } -const typedSchema = Schema.get<{ users: User }>("myapp")!; +const typedSchema = Schema.get<{ users: User }>("myapp"); +if (!typedSchema) throw new Error('Schema "myapp" is not defined'); + const users = typedSchema.instance().table("users"); // Table ``` -For more details on table operations, see [Table Operations](../2.operations/3.table_operations.md). +For more on table operations, see [Table operations](../2.operations/3.table_operations.md). diff --git a/docs/3.results/5.database_changes.md b/docs/3.results/5.database_changes.md index 8e8f05d..929ceea 100644 --- a/docs/3.results/5.database_changes.md +++ b/docs/3.results/5.database_changes.md @@ -1,15 +1,21 @@ -# Schema and Instance Management +--- +title: Schema and instance management +description: The Schema and SchemaInstance classes, SchemaDefinition, InstanceId, and CROSS_INSTANCE, which set the structure and lifecycle of an AQL database. +mode: reference +--- -This page covers the `Schema`, `SchemaInstance`, and related types that control database structure and lifecycle. +This page covers `Schema`, `SchemaInstance`, and the types that set the structure and lifecycle of a database. ## Schema -The `Schema` class defines the structure of a database and manages its instances. +The `Schema` class defines the structure of a database and manages its instances. `T` maps table names to document types, and `Definition` is the type of the definition. ```typescript fragment -class Schema extends StagedObject { - static get(id: string): Schema | undefined; - constructor(id: string, definition: SchemaDefinition); +class Schema extends StagedObject { + static get( + id: string, + ): Schema | undefined; + constructor(id: string, definition: Definition); instance(id?: InstanceId): SchemaInstance; createInstance(id?: string): Query; destroyInstance(id?: string): Query; @@ -17,6 +23,8 @@ class Schema extends StagedObject { } ``` +`createInstance()` resolves to the ID of the created instance. `listInstances()` resolves to the IDs of the named instances, without the default (unnamed) instance. `Schema.get()` returns `undefined` when no schema has the ID, and it does not check its type arguments against the ones the schema was declared with. + ### `SchemaDefinition` ```typescript @@ -34,11 +42,11 @@ const CROSS_INSTANCE: unique symbol; type InstanceId = string | typeof CROSS_INSTANCE; ``` -`schema.instance(CROSS_INSTANCE)` returns a query stage that operates across every instance of the schema. Adapters skip the per-instance filter on read/update/delete and reject inserts. +`schema.instance(CROSS_INSTANCE)` returns a query stage that operates across every instance of the schema. Adapters skip the per-instance filter on reads, updates, and deletes, and reject inserts. -Secondary indexes are optimized for queries scoped to one instance. Set `crossInstance: true` in the `IndexDefinition` of the indexes that cross-instance queries rely on (see [Index Management](../2.operations/5.indexes.md)). +Secondary indexes are optimized for queries scoped to one instance. Set `crossInstance: true` in the `IndexDefinition` of the indexes that cross-instance queries rely on. See [Index management](../2.operations/5.indexes.md). -## Create and Manage Instances +## Create and manage instances ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -68,9 +76,9 @@ await schema.destroyInstance("tenant-1"); const ids = await schema.listInstances(); ``` -## Retrieve a Schema +## Retrieve a schema -Use `Schema.get()` to look up a previously defined schema anywhere in your application: +`Schema.get()` looks up a schema defined anywhere in your application by its ID: ```typescript import { Schema } from "@antelopejs/interface-database"; @@ -83,13 +91,13 @@ if (schema) { } ``` -## SchemaInstance +## `SchemaInstance` -The `SchemaInstance` class represents a single instance of a schema. Its primary method is `table()`, which returns a `Table` for the specified table name. +The `SchemaInstance` class is one instance of a schema. Its own method is `table()`, which returns the `Table` with the given name. ```typescript fragment class SchemaInstance extends StagedObject { - table(id: TK): Table; + table(id: TK): Table; } ``` @@ -99,4 +107,4 @@ const users = db.table("users"); const posts = db.table("posts"); ``` -For practical usage patterns, see [Schema Management](../2.operations/2.database_management.md). +For usage patterns, see [Schema management](../2.operations/2.database_management.md). diff --git a/docs/3.results/6.value_changes.md b/docs/3.results/6.value_changes.md index 284e595..716378a 100644 --- a/docs/3.results/6.value_changes.md +++ b/docs/3.results/6.value_changes.md @@ -1,8 +1,12 @@ -# Change Events +--- +title: Change events +description: The Changes event that an AQL change feed yields, with its change type and the document before and after the change. +mode: reference +--- -The `Changes` interface represents a change event emitted by change feeds. It includes the type of change and the document values before and after the change. +A change feed yields `Changes` events. Each event holds the type of change and the document values before and after the change. `changes()` on a `Stream` or a `SingleSelection` returns a `Query[]>`, and `for await` iterates its events one at a time. -## Interface Definition +## Interface definition ```typescript interface Changes { @@ -20,20 +24,22 @@ interface Changes { | `oldValue` | `T \| undefined` | The document before the change. Undefined for inserts. | | `newValue` | `T \| undefined` | The document after the change. Undefined for deletes. | -## Determine the Change Type +## Determine the change type -The `changeType` field indicates what happened: +The `changeType` field says what happened: -- **`"added"`** - A new document was inserted. `newValue` contains the document; `oldValue` is undefined. -- **`"modified"`** - An existing document was updated. Both `oldValue` and `newValue` are present. -- **`"removed"`** - A document was deleted. `oldValue` contains the deleted document; `newValue` is undefined. +- `"added"`. A new document was inserted. `newValue` holds the document, and `oldValue` is undefined. +- `"modified"`. An existing document was updated. Both `oldValue` and `newValue` are present. +- `"removed"`. A document was deleted. `oldValue` holds the deleted document, and `newValue` is undefined. -## Process Change Events +## Process change events ```typescript import { Schema } from "@antelopejs/interface-database"; -const schema = Schema.get("myapp")!; +const schema = Schema.get("myapp"); +if (!schema) throw new Error('Schema "myapp" is not defined'); + const users = schema.instance().table("users"); for await (const change of users.changes()) { @@ -51,7 +57,7 @@ for await (const change of users.changes()) { } ``` -## Monitor a Single Document +## Monitor a single document ```typescript fragment for await (const change of users.get("user-123").changes()) { @@ -63,9 +69,9 @@ for await (const change of users.get("user-123").changes()) { } ``` -## Use Cases +## Use cases -- **Real-time notifications** - Push updates to connected clients when data changes. -- **Change observation** - Inspect before/after values while subscribed. A durable audit log also needs verified delivery, retention, and recovery guarantees from the provider. -- **Cache invalidation** - Clear or refresh caches when underlying data is modified. -- **Event-driven workflows** - Trigger downstream processing based on data changes. +- Real-time notifications. Push updates to connected clients when data changes. +- Change observation. Inspect the values before and after a change while subscribed. A durable audit log also needs verified delivery, retention, and recovery guarantees from the provider. +- Cache invalidation. Clear or refresh caches when the underlying data changes. +- Event-driven workflows. Start downstream processing when data changes. diff --git a/docs/index.md b/docs/index.md index d96112e..05860e2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,25 @@ -# Query data with AQL +--- +title: Query data with AQL +description: Define a schema, run a first AQL query from an AntelopeJS module, and find the reference page for each query type, operation, and result. +mode: how-to +--- ## Overview -The Database interface provides AQL (Antelope Query Language), a typed query API for AntelopeJS database providers. Build a query from table access, filters, and transformations, then execute it through the provider bound to your module. +The Database interface provides AQL (Antelope Query Language), a typed query API for AntelopeJS database providers. You build a query from table access, filters, and transformations, then execute it through the provider bound to your module. ## Run your first query -Start in an AntelopeJS module with a configured database provider. Installing the interface supplies query builders and contracts, not a database server or credentials. +Start in an AntelopeJS module with a configured database provider. The interface package supplies query builders and contracts. It does not supply a database server or credentials. -1. [Define a schema and create an instance](./2.operations/2.database_management.md). The schema describes tables; the instance selects where their data lives. +1. [Define a schema and create an instance](./2.operations/2.database_management.md). The schema describes tables, and the instance selects where their data lives. 2. [Open a table](./2.operations/3.table_operations.md) and compose a read with [filtering](./2.operations/6.filtering.md). Building the query does not execute it. -3. [Execute with `await` or `run()`](./1.query_types/7.query.md), or consume a cursor for incremental results. -4. [Inspect write results](./3.results/2.write_results.md) when adding mutations. Inserts return IDs; updates and deletes return counts. +3. [Execute the query with `await` or `run()`](./1.query_types/7.query.md), or consume a cursor for incremental results. +4. [Inspect write results](./3.results/2.write_results.md) when you add mutations. Inserts return IDs. Updates and deletes return counts. ### Insert and read one record -Run this function from your module after its database provider is configured. It creates a disposable schema instance and writes one record; do not point this exercise at production data. +Run this function from your module after its database provider is configured. It creates a disposable schema instance and writes one record. Do not point it at production data. ```ts docs-check import { Schema } from "@antelopejs/interface-database"; @@ -43,7 +47,7 @@ export async function tryNotes() { } ``` -`notes` is a query builder; `saved` is a fetched record. Calling the function again inserts another record. When you no longer need this disposable data, explicitly run `await notesSchema.destroyInstance("tutorial")`; it deletes the instance's data. +`notes` is a query builder, and `saved` is a fetched record. Each call of the function inserts another record. To delete the instance and its data when you no longer need them, run `await notesSchema.destroyInstance("tutorial")`. ## Find the right reference @@ -55,58 +59,58 @@ export async function tryNotes() { | How do I conditionally change one record? | [Atomic mutations](./2.operations/8.atomic_mutations.md) | | What did the provider return? | [Results](./3.results/1.index.md) | -## Import Paths - -Import `Schema`, `Query`, `Table`, `Datum`, `Stream`, and `ValueProxy` from `@antelopejs/interface-database` in an AntelopeJS module. This entry point registers schemas with the bound provider and installs query execution (`run()`, `then()`, `cursor()`, and async iteration). - -Supporting types have dedicated paths: import `Changes`, `DeepPartial`, `ExtractType`, `OperandValue`, `QueryValue`, `Value`, `WriteDocument`, and `WriteValue` from `@antelopejs/interface-database/common`, and `FieldType`, `TableDefinition`, and `IndexDefinition` from `@antelopejs/interface-database/schema`. Atomic mutation types are available from the package root and `/atomic`. - -### Advanced: build queries without executing them - -`@antelopejs/interface-database/staged-query` is the build-only entry point for tools that construct and inspect query stages without the AntelopeJS runtime. Its schema does not register with a provider, and its queries expose `build()` but no execution methods when loaded on their own. Do not `await` a build-only query expecting database results. The runtime entry point augments the same staged classes; keep build-only consumers separate from runtime imports when that isolation matters. - -## Documentation Structure - -- [**1. Query Types**](./1.query_types/1.index.md) - Detailed reference for each query type: - - - [Table](./1.query_types/2.table.md) - Table-level operations (insert, get, getAll, between) - - [Selection](./1.query_types/3.selection.md) - Multi-document operations (update, replace, delete) - - [SingleSelection](./1.query_types/4.single_selection.md) - Single-document operations - - [Stream](./1.query_types/5.stream.md) - Transformation, filtering, aggregation, joins, and grouping - - [Change Feeds](./1.query_types/6.feed.md) - Real-time change notifications - - [Query](./1.query_types/7.query.md) - Base query execution and async iteration - - [Datum](./1.query_types/8.datum.md) - Single-value manipulation - - [ValueProxy](./1.query_types/9.valueproxy.md) - In-query value operations (arithmetic, string, date, array, object) - -- [**2. Operations**](./2.operations/1.index.md) - Practical guides: - - - [Schema Management](./2.operations/2.database_management.md) - Create and manage schema instances - - [Table Operations](./2.operations/3.table_operations.md) - Access and work with tables - - [CRUD Operations](./2.operations/4.crud.md) - Create, read, update, and delete documents - - [Index Management](./2.operations/5.indexes.md) - Define and use secondary indexes - - [Filtering and Querying](./2.operations/6.filtering.md) - Filter, compare, and transform data - - [Lookup](./2.operations/7.lookup.md) - Foreign key joins and data population - - [Atomic Mutations](./2.operations/8.atomic_mutations.md) - Conditional single-record mutations and outcomes - -- [**3. Results**](./3.results/1.index.md) - Result types and handling: - - [Insert Results](./3.results/2.write_results.md) - Results from insert operations - - [Index Definitions](./3.results/3.index_changes.md) - Index configuration in schema definitions - - [Table Definitions](./3.results/4.table_changes.md) - Table structure and field types - - [Schema and Instance Management](./3.results/5.database_changes.md) - Schema lifecycle and instance management - - [Change Events](./3.results/6.value_changes.md) - Change feed event structure +## Import paths + +Import `Schema`, `Query`, `Table`, `Datum`, `Stream`, and `ValueProxy` from `@antelopejs/interface-database` in an AntelopeJS module. This entry point registers schemas with the bound provider and installs query execution: `run()`, `then()`, `cursor()`, and async iteration. + +Supporting types have their own paths: + +- `@antelopejs/interface-database/common` exports `Changes`, `DeepPartial`, `ExtractType`, `OperandValue`, `QueryValue`, `Value`, `WriteDocument`, and `WriteValue`. +- `@antelopejs/interface-database/schema` exports `FieldType`, `TableDefinition`, and `IndexDefinition`. +- The package root and `@antelopejs/interface-database/atomic` both export the atomic mutation types. + +### Build queries without executing them + +`@antelopejs/interface-database/staged-query` is the build-only entry point for tools that construct and inspect query stages without the AntelopeJS runtime. Its schema does not register with a provider. Loaded on their own, its queries expose `build()` and no execution methods, so awaiting a build-only query does not return database results. The runtime entry point adds execution to the same staged classes. If a build-only consumer must stay isolated from execution, keep it apart from code that imports the runtime entry point. + +## Documentation structure + +- [Query types](./1.query_types/1.index.md) is the reference for each query type: + - [Table](./1.query_types/2.table.md) covers table-level operations: `insert`, `get`, `getAll`, `between`, and `atomicMutation`. + - [Selection](./1.query_types/3.selection.md) covers multi-document operations: `update`, `replace`, and `delete`. + - [SingleSelection](./1.query_types/4.single_selection.md) covers single-document operations. + - [Stream](./1.query_types/5.stream.md) covers transformation, filtering, aggregation, joins, and grouping. + - [Change feeds](./1.query_types/6.feed.md) covers real-time change notifications. + - [Query](./1.query_types/7.query.md) covers base query execution and async iteration. + - [Datum](./1.query_types/8.datum.md) covers single-value manipulation. + - [ValueProxy](./1.query_types/9.valueproxy.md) covers in-query value operations on numbers, strings, dates, arrays, and objects. +- [Operations](./2.operations/1.index.md) holds the practical guides: + - [Schema management](./2.operations/2.database_management.md) creates and manages schema instances. + - [Table operations](./2.operations/3.table_operations.md) opens tables and works with them. + - [CRUD operations](./2.operations/4.crud.md) creates, reads, updates, and deletes documents. + - [Index management](./2.operations/5.indexes.md) defines and uses secondary indexes. + - [Filtering and querying](./2.operations/6.filtering.md) filters, compares, and transforms data. + - [Lookup](./2.operations/7.lookup.md) joins on foreign keys and populates data. + - [Atomic mutations](./2.operations/8.atomic_mutations.md) covers conditional single-record mutations and their outcomes. +- [Results](./3.results/1.index.md) describes result types and how to handle them: + - [Insert results](./3.results/2.write_results.md) describes what insert operations return. + - [Index definitions](./3.results/3.index_changes.md) describes index configuration in schema definitions. + - [Table definitions](./3.results/4.table_changes.md) describes table structure and field types. + - [Schema and instance management](./3.results/5.database_changes.md) describes the schema lifecycle and instance management. + - [Change events](./3.results/6.value_changes.md) describes the structure of a change feed event. ## Features -- Database-agnostic query interface -- Consistent API across different database implementations -- Type-safe operations using TypeScript generics -- Schema-based database structure definition -- Multi-instance support per schema -- Secondary index support -- Change feeds for real-time updates -- Async iteration with cursor support -- Chainable query builder with lazy evaluation +- A query interface that does not depend on one database. +- One API across database implementations. +- Typed operations through TypeScript generics. +- Database structure defined by a schema. +- Several instances per schema. +- Secondary indexes. +- Change feeds for real-time updates. +- Async iteration with cursors. +- A chainable query builder with lazy evaluation. -## Database Interoperability +## Database interoperability -AQL separates query construction from the database engine. A provider translates the shared operations into its backend's behavior. Before moving an application between providers, check that provider's supported operations and exercise the queries, indexes, and change feeds your application uses; a shared TypeScript contract is not a guarantee of identical backend capabilities or performance. +AQL separates query construction from the database engine. A provider translates the shared operations into its backend's behavior. A shared TypeScript contract does not guarantee identical backend capabilities or performance. Before you move an application to another provider, check which operations that provider supports, and test the queries, indexes, and change feeds your application uses. From b9ef608677b8fb24c80c1515a214c07d20775822 Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 01:36:14 +0100 Subject: [PATCH 2/3] docs: turn on the docs standard --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 982e937..7141c5b 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,9 @@ "test": "src/antelope.test.ts", "skills": [ "./skills" - ] + ], + "docs": { + "standard": true + } } } From dd2b9965c0ce590ba2372545d6086b1e56cbc82c Mon Sep 17 00:00:00 2001 From: Antony Rizzitelli Date: Tue, 3 Nov 2026 01:50:11 +0100 Subject: [PATCH 3/3] docs: list the root Schema class and fix the W5 notes --- docs/1.query_types/2.table.md | 2 +- docs/1.query_types/3.selection.md | 5 ++++- docs/1.query_types/4.single_selection.md | 2 +- docs/1.query_types/8.datum.md | 2 +- docs/2.operations/3.table_operations.md | 2 +- docs/2.operations/6.filtering.md | 2 +- docs/3.results/5.database_changes.md | 22 ++++++++++++++++------ 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/1.query_types/2.table.md b/docs/1.query_types/2.table.md index fb4a528..7299b80 100644 --- a/docs/1.query_types/2.table.md +++ b/docs/1.query_types/2.table.md @@ -85,7 +85,7 @@ const upsertIds = await users.insert( Retrieves a single document by its primary key, given as a string or a `ValueProxy` of a string. Returns a `SingleSelection`. -Awaiting this selection is declared as `T`, not `T | null`. The declaration does not prove that a row exists. A provider can return `null` for a missing key, so check the result before you read its fields. +Awaiting this selection is declared as `T`, not `T | null`. The declaration does not prove that a row exists. A provider can return `null` or `undefined` for a missing key, so check the result before you read its fields. ```typescript fragment const user = await users.get("user-123"); diff --git a/docs/1.query_types/3.selection.md b/docs/1.query_types/3.selection.md index 5a63fcb..0eca855 100644 --- a/docs/1.query_types/3.selection.md +++ b/docs/1.query_types/3.selection.md @@ -93,7 +93,7 @@ console.log(`Deleted ${count} inactive users`); ## Inherited methods -`Selection` extends `Stream`, so it inherits every stream operation: +`Selection` extends `Stream`, so it inherits every stream operation. These are grouped by purpose: - Transformation: `map()`, `pluck()`, `without()` - Filtering: `filter()` @@ -102,5 +102,8 @@ console.log(`Deleted ${count} inactive users`); - Ordering: `orderBy()`, `slice()`, `nth()` - Grouping: `group()` - Deduplication: `distinct()` +- Fields and fallbacks: `key()`, `default()` +- Combining and typing: `union()`, `cast()` +- Change feeds: `changes()` See [Stream](./5.stream.md) for the details of these methods. diff --git a/docs/1.query_types/4.single_selection.md b/docs/1.query_types/4.single_selection.md index 5a38eaa..85791b1 100644 --- a/docs/1.query_types/4.single_selection.md +++ b/docs/1.query_types/4.single_selection.md @@ -41,7 +41,7 @@ const user = users.get("user-123"); `SingleSelection` extends `Datum`, so you can execute it directly to retrieve the document. -The declared result is `T`, even though a provider can return `null` for a missing key. Check for absence before you access the document. The generic type does not guarantee that the document exists. +The declared result is `T`, even though a provider can return `null` or `undefined` for a missing key. Check for absence before you access the document. The generic type does not guarantee that the document exists. ```typescript fragment const user = await users.get("user-123"); diff --git a/docs/1.query_types/8.datum.md b/docs/1.query_types/8.datum.md index 203929c..5640183 100644 --- a/docs/1.query_types/8.datum.md +++ b/docs/1.query_types/8.datum.md @@ -55,7 +55,7 @@ const phone = await users.get("user-123").key("phone", "N/A"); ## Default values -The `default` method gives a fallback value for a datum that is `null` or `undefined`. +The `default` method gives a fallback value for a datum that is `null`. ```typescript fragment const user = await users.get("maybe-missing").default({ diff --git a/docs/2.operations/3.table_operations.md b/docs/2.operations/3.table_operations.md index f4aeb09..37abd68 100644 --- a/docs/2.operations/3.table_operations.md +++ b/docs/2.operations/3.table_operations.md @@ -60,7 +60,7 @@ export async function readUser() { } ``` -`get()` returns `SingleSelection`, so awaiting it is typed as `User`, not `User | null`. The type does not guarantee that a row exists: a provider may return `null` for a missing key. Check the result before you read its fields. The mapping on `Schema` describes documents. It does not validate what the database holds. +`get()` returns `SingleSelection`, so awaiting it is typed as `User`, not `User | null`. The type does not guarantee that a row exists: a provider may return `null` or `undefined` for a missing key. Check the result before you read its fields. The mapping on `Schema` describes documents. It does not validate what the database holds. ## Basic operations diff --git a/docs/2.operations/6.filtering.md b/docs/2.operations/6.filtering.md index d8be90d..88265e0 100644 --- a/docs/2.operations/6.filtering.md +++ b/docs/2.operations/6.filtering.md @@ -152,7 +152,7 @@ const frequentVisitors = await users.filter((u) => ## Ordering and pagination -`orderBy()` takes a field and an optional direction, `"asc"` or `"desc"`. `slice()` takes an offset and a count. +`orderBy()` takes the name of the index to sort by and an optional direction, `"asc"` or `"desc"`. `slice()` takes an offset and a count. ```typescript fragment // Sort ascending diff --git a/docs/3.results/5.database_changes.md b/docs/3.results/5.database_changes.md index 929ceea..c840941 100644 --- a/docs/3.results/5.database_changes.md +++ b/docs/3.results/5.database_changes.md @@ -8,14 +8,22 @@ This page covers `Schema`, `SchemaInstance`, and the types that set the structur ## Schema -The `Schema` class defines the structure of a database and manages its instances. `T` maps table names to document types, and `Definition` is the type of the definition. +The `Schema` class that `@antelopejs/interface-database` exports defines the structure of a database and manages its instances. `T` maps table names to document types. ```typescript fragment -class Schema extends StagedObject { +class Schema extends StagedSchema { + static list(): ReadonlyMap; + constructor(id: string, definition: SchemaDefinition); +} + +// Inherited from StagedSchema +class StagedSchema< + T = any, + Definition = SchemaDefinition, +> extends StagedObject { static get( id: string, - ): Schema | undefined; - constructor(id: string, definition: Definition); + ): StagedSchema | undefined; instance(id?: InstanceId): SchemaInstance; createInstance(id?: string): Query; destroyInstance(id?: string): Query; @@ -23,7 +31,9 @@ class Schema extends StagedObject { } ``` -`createInstance()` resolves to the ID of the created instance. `listInstances()` resolves to the IDs of the named instances, without the default (unnamed) instance. `Schema.get()` returns `undefined` when no schema has the ID, and it does not check its type arguments against the ones the schema was declared with. +The constructor registers the schema, so `Schema.list()` returns every schema this process defined, by ID. A schema defined again under the same ID replaces the earlier one. `createInstance()` resolves to the ID of the created instance. `listInstances()` resolves to the IDs of the named instances, without the default (unnamed) instance. `Schema.get()` returns `undefined` when no schema has the ID, and it does not check its type arguments against the ones the schema was declared with. + +`StagedSchema` is the `Schema` class of the `@antelopejs/interface-database/staged-query` entry. It takes the definition type as a second type parameter. The root `Schema` fixes it to `SchemaDefinition`. ### `SchemaDefinition` @@ -42,7 +52,7 @@ const CROSS_INSTANCE: unique symbol; type InstanceId = string | typeof CROSS_INSTANCE; ``` -`schema.instance(CROSS_INSTANCE)` returns a query stage that operates across every instance of the schema. Adapters skip the per-instance filter on reads, updates, and deletes, and reject inserts. +`schema.instance(CROSS_INSTANCE)` returns a `SchemaInstance` that operates across every instance of the schema. Adapters skip the per-instance filter on reads, updates, and deletes, and reject inserts. Secondary indexes are optimized for queries scoped to one instance. Set `crossInstance: true` in the `IndexDefinition` of the indexes that cross-instance queries rely on. See [Index management](../2.operations/5.indexes.md).