sorodb 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ # Schema and query reference
2
+
3
+ Register a schema with `db.schema({ version }, definitions)` before using a collection. The version must be an integer of at least 1, and `definitions` must contain at least one table.
4
+
5
+ ## Table definitions
6
+
7
+ ```js
8
+ db.schema({ version: 1 }, [
9
+ {
10
+ table: "users",
11
+ columns: {
12
+ id: { type: "number", primary: true },
13
+ name: { type: "string" },
14
+ nickname: { type: "string?" },
15
+ joinedAt: { type: "date", default: () => new Date() },
16
+ },
17
+ indexes: [{ name: "joined_at", columns: [{ field: "joinedAt", direction: "desc" }] }],
18
+ },
19
+ ]);
20
+ ```
21
+
22
+ Each table needs a unique name and a `columns` object. Table, column, and index names must start with a letter and contain only letters, digits, or underscores. Exactly one column must be marked `primary: true`, and it must be named `id`. Its type must be a required string or number type; stored IDs must be strings or finite numbers.
23
+
24
+ Each column has a `type` containing an [ArkType definition](https://arktype.io/docs/intro). Common strings include `"string"`, `"number"`, `"string[]"`, `"bigint"`, and `"unknown"`. SoroDB also accepts `"date"` for a `Date`, `"string?"` for an optional string, and expressions such as `"string | null"` for nullable values. You can pass an ArkType type object where a string definition is insufficient.
25
+
26
+ `default` can be a value or a zero-argument function. SoroDB applies it when a field is `undefined` on create or replace, then validates the result. Value defaults are cloned for each document; function defaults are called for each document. Fields without optional types or defaults must be supplied. Undeclared fields are rejected.
27
+
28
+ Documents are stored as CBOR. Supported data includes null, booleans, finite numbers, strings, BigInts, valid Dates, `Uint8Array` values, dense arrays, and plain objects containing these values. Sparse arrays, cycles, invalid Dates, and unsupported class instances cannot be stored. `Date`, BigInt, and byte array values round-trip through storage.
29
+
30
+ For persistent data, [schema upgrades](../guides/schema-upgrades.md) explains version changes and backfilling.
31
+
32
+ ## Index definitions
33
+
34
+ An index has a unique `name`, a nonempty `columns` array, optional `unique: true`, and optional `method: "btree"`. `btree` is the only current method and is the default. Each column is a field name or `{ field, direction }`; direction defaults to `"asc"` and can be `"desc"`. Dotted paths can point into nested document data, provided the top-level column is declared.
35
+
36
+ ```js
37
+ indexes: [
38
+ { name: "email", columns: ["email"], unique: true },
39
+ {
40
+ name: "recent_by_team",
41
+ columns: ["team", { field: "joinedAt", direction: "desc" }],
42
+ },
43
+ ];
44
+ ```
45
+
46
+ B-tree fields support missing values, null, booleans, finite numbers, BigInts, strings, valid Dates, and byte arrays. Indexing an array or object value fails validation on write. A unique index skips a document when an indexed field is missing; `null` is an indexed value and must be unique when `unique: true`.
47
+
48
+ ## Filters
49
+
50
+ Pass `where` to `collection.filter`. It receives a builder and must return one expression. Paths may be dotted, such as `"profile.city"`.
51
+
52
+ ```js
53
+ const query = users.filter({
54
+ where: (w) => w.and(w.gte("age", 18), w.startsWith("name", "A")),
55
+ });
56
+ ```
57
+
58
+ | Builder | Matches |
59
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
60
+ | `eq(path, value)`, `ne(path, value)` | Equal or unequal values. |
61
+ | `gt`, `gte`, `lt`, `lte` | Ordered comparisons between compatible numbers, strings, BigInts, Dates, or byte arrays. |
62
+ | `in(path, values)` | A field equal to any value in an array. |
63
+ | `contains(path, value)` | A substring in a string, or an equal element in an array. |
64
+ | `startsWith(path, prefix)` | A string with the given prefix. |
65
+ | `exists(path, present?)` | Field is defined; pass `false` to match missing fields. `null` counts as present. |
66
+ | `and(...expressions)`, `or(...expressions)`, `not(expression)` | Combine filter expressions. |
67
+
68
+ Filter results are checked against complete documents, even when an index is used to find candidates. Equality on `id` can use the primary key directly. An equality filter may use a B-tree index when its first field matches the filter path and is ascending; other filters can scan documents. See [indexes and sorting](../guides/indexes-and-sorting.md).
69
+
70
+ ## Ordering, projection, and limits
71
+
72
+ This example assumes `users` has a `team` column and the `recent_by_team` index shown above.
73
+
74
+ ```js
75
+ const page = await users
76
+ .filter({
77
+ where: (w) => w.eq("team", "platform"),
78
+ orderBy: (order) => order.asc("team").desc("joinedAt"),
79
+ select: ["name", "joinedAt"],
80
+ limit: 20,
81
+ })
82
+ .page();
83
+ ```
84
+
85
+ `orderBy` requires an index whose leading fields and directions match the requested order. Ascending `id` works without a declared index. Fields in one order must be distinct. Without `orderBy`, do not rely on a particular result order.
86
+
87
+ `select` accepts field paths and always retains `id`. Missing selected fields are omitted. `limit` must be a positive integer. It caps async iteration and sets the size of a page; `page()` defaults to 100. See [pagination](../guides/pagination.md) for cursors and snapshot behavior.
@@ -0,0 +1,26 @@
1
+ // @ts-check
2
+ import SoroDB from "../src/index.js";
3
+
4
+ const db = SoroDB({ store: "memory:///", path: "example" });
5
+ db.schema({ version: 1 }, [
6
+ {
7
+ table: "users",
8
+ columns: {
9
+ id: { type: "number", primary: true },
10
+ name: { type: "string" },
11
+ joinedAt: { type: "date", default: () => new Date() },
12
+ },
13
+ indexes: [{ name: "joined_at", columns: [{ field: "joinedAt", direction: "desc" }] }],
14
+ },
15
+ ]);
16
+
17
+ try {
18
+ const users = await db.collection("users");
19
+ await users.create({ id: 1, name: "Ada" });
20
+ await users.create({ id: 2, name: "Grace" });
21
+ for await (const user of users.filter({ orderBy: (order) => order.desc("joinedAt") })) {
22
+ console.log(user);
23
+ }
24
+ } finally {
25
+ await db.close();
26
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "sorodb",
3
+ "version": "0.0.0",
4
+ "description": "Embedded document database built on SlateDB",
5
+ "license": "MIT",
6
+ "files": [
7
+ "src",
8
+ "types",
9
+ "LICENSE",
10
+ "README.md",
11
+ "CONTRIBUTING.md",
12
+ "docs",
13
+ "examples"
14
+ ],
15
+ "type": "module",
16
+ "types": "./types/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./types/index.d.ts",
20
+ "import": "./src/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "typecheck": "tsc --noEmit",
26
+ "build:types": "tsc -p tsconfig.build.json && node scripts/finalize-types.js",
27
+ "prepack": "npm run build:types",
28
+ "pack:dry": "npm pack --dry-run",
29
+ "fmt": "oxfmt",
30
+ "lint": "oxlint"
31
+ },
32
+ "dependencies": {
33
+ "@slatedb/uniffi": "^0.16.0",
34
+ "@types/node": "^26.6.2",
35
+ "arktype": "^2.1.20",
36
+ "cbor2": "^2.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "oxlint": "^1.85.0",
40
+ "typescript": "^7.0.2"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ }
45
+ }
@@ -0,0 +1,90 @@
1
+ // @ts-check
2
+ import { ConflictError, SchemaError, ValidationError } from "./errors.js";
3
+ import { decodeData, docKey, encodeData } from "./encoding.js";
4
+ import { validateDocument } from "./schema.js";
5
+ import { addIndexes, removeIndexes } from "./indexes.js";
6
+ import { Query } from "./query.js";
7
+
8
+ export class Collection {
9
+ /** @param {import("./storage.js").Database} db @param {import("./types.js").Table} table @param {import("@slatedb/uniffi").DbTransaction | null} [transaction] */
10
+ constructor(db, table, transaction = null) {
11
+ this.db = db;
12
+ this.table = table;
13
+ this.transaction = transaction;
14
+ }
15
+
16
+ /** @template T @param {(tx: import("@slatedb/uniffi").DbTransaction) => Promise<T>} callback @returns {Promise<T>} */
17
+ async _write(callback) {
18
+ if (this.transaction) {
19
+ return callback(this.transaction);
20
+ }
21
+ return this.db._atomic(callback);
22
+ }
23
+
24
+ /** @param {import("./types.js").Document} document */
25
+ async create(document) {
26
+ const value = validateDocument(this.table, document);
27
+ const key = docKey(this.table.name, value.id);
28
+ return this._write(async (tx) => {
29
+ if (await tx.get(key)) {
30
+ throw new ConflictError(`${this.table.name} id already exists`);
31
+ }
32
+ await tx.put(key, encodeData(value));
33
+ await addIndexes(tx, this.table, value);
34
+ return value;
35
+ });
36
+ }
37
+
38
+ /** @param {string | number} id */
39
+ async get(id) {
40
+ await this.db._ready();
41
+ const reader = this.transaction ?? this.db.native;
42
+ if (!reader) {
43
+ throw new SchemaError("Database is not open");
44
+ }
45
+ const bytes = await reader.get(docKey(this.table.name, id));
46
+ if (bytes) {
47
+ return decodeData(bytes);
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** @param {string | number} id @param {import("./types.js").Document} document */
53
+ async replace(id, document) {
54
+ if (!document || document.id !== id) {
55
+ throw new ValidationError("Replacement document must retain the same id");
56
+ }
57
+ const value = validateDocument(this.table, document);
58
+ const key = docKey(this.table.name, id);
59
+ return this._write(async (tx) => {
60
+ const bytes = await tx.get(key);
61
+ if (!bytes) {
62
+ throw new ConflictError(`${this.table.name} id does not exist`);
63
+ }
64
+ const before = decodeData(bytes);
65
+ await removeIndexes(tx, this.table, before);
66
+ await tx.put(key, encodeData(value));
67
+ await addIndexes(tx, this.table, value);
68
+ return value;
69
+ });
70
+ }
71
+
72
+ /** @param {string | number} id */
73
+ async delete(id) {
74
+ const key = docKey(this.table.name, id);
75
+ return this._write(async (tx) => {
76
+ const bytes = await tx.get(key);
77
+ if (!bytes) {
78
+ return false;
79
+ }
80
+ await removeIndexes(tx, this.table, decodeData(bytes));
81
+ await tx.delete(key);
82
+ return true;
83
+ });
84
+ }
85
+
86
+ /** @param {import("./types.js").QueryOptions} [options] */
87
+ filter(options = {}) {
88
+ return new Query(this, options);
89
+ }
90
+ }
@@ -0,0 +1,200 @@
1
+ // @ts-check
2
+ import { decode, encode } from "cbor2";
3
+ import { ValidationError } from "./errors.js";
4
+
5
+ /** @param {string} value */
6
+ const utf8 = (value) => Buffer.from(value, "utf8");
7
+ /** @param {Uint8Array} bytes */
8
+ export function part(bytes) {
9
+ const out = Buffer.allocUnsafe(bytes.length * 2 + 2);
10
+ let length = 0;
11
+ for (const byte of bytes) {
12
+ out[length++] = byte;
13
+ if (byte === 0) {
14
+ out[length++] = 255;
15
+ }
16
+ }
17
+ out[length++] = 0;
18
+ out[length++] = 0;
19
+ return out.subarray(0, length);
20
+ }
21
+
22
+ /** @param {number} value */
23
+ function sortableNumber(value) {
24
+ if (!Number.isFinite(value)) {
25
+ throw new ValidationError("Indexed numbers must be finite");
26
+ }
27
+ const bytes = Buffer.alloc(8);
28
+ let normalized = value;
29
+ if (Object.is(value, -0)) {
30
+ normalized = 0;
31
+ }
32
+ bytes.writeDoubleBE(normalized);
33
+ if (bytes[0] & 128) {
34
+ for (let i = 0; i < 8; i++) {
35
+ bytes[i] ^= 255;
36
+ }
37
+ } else {
38
+ bytes[0] ^= 128;
39
+ }
40
+ return bytes;
41
+ }
42
+
43
+ export { sortableNumber };
44
+
45
+ /** @param {unknown} name @param {string} [label] */
46
+ export function validName(name, label = "name") {
47
+ if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
48
+ throw new ValidationError(
49
+ `${label} must start with a letter and contain only letters, digits, or underscores`,
50
+ );
51
+ }
52
+ return name;
53
+ }
54
+
55
+ /** @param {unknown} id */
56
+ export function idBytes(id) {
57
+ if (typeof id === "string") {
58
+ return Buffer.concat([Buffer.from([115]), part(utf8(id))]);
59
+ }
60
+ if (typeof id === "number" && Number.isFinite(id)) {
61
+ return Buffer.concat([Buffer.from([110]), sortableNumber(id)]);
62
+ }
63
+ throw new ValidationError("id must be a string or finite number");
64
+ }
65
+
66
+ /** @param {string} table */
67
+ export function docPrefix(table) {
68
+ return utf8(`D\0${table}\0`);
69
+ }
70
+ /** @param {string} table @param {string | number} id */
71
+ export function docKey(table, id) {
72
+ return Buffer.concat([docPrefix(table), idBytes(id)]);
73
+ }
74
+ /** @param {string} table @param {string} name */
75
+ export function indexPrefix(table, name) {
76
+ return utf8(`I\0${table}\0${name}\0`);
77
+ }
78
+
79
+ /** @param {unknown} object @param {string} path @returns {unknown} */
80
+ export function getPath(object, path) {
81
+ let value = object;
82
+ for (const key of path.split(".")) {
83
+ if (value == null) {
84
+ return undefined;
85
+ }
86
+ value = /** @type {Record<string, unknown>} */ (Object(value))[key];
87
+ }
88
+ return value;
89
+ }
90
+
91
+ /** @param {Uint8Array} prefix */
92
+ export function prefixEnd(prefix) {
93
+ const end = Buffer.from(prefix);
94
+ for (let i = end.length - 1; i >= 0; i--) {
95
+ if (end[i] !== 255) {
96
+ end[i]++;
97
+ return end.subarray(0, i + 1);
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+
103
+ /** @param {Uint8Array} prefix @param {Uint8Array} [after] */
104
+ export function range(prefix, after) {
105
+ return {
106
+ start: after || prefix,
107
+ start_inclusive: !after,
108
+ end: prefixEnd(prefix),
109
+ end_inclusive: false,
110
+ };
111
+ }
112
+
113
+ const cborOptions = { collapseBigInts: false, dateTag: 0 };
114
+
115
+ /** @param {unknown} value @param {boolean} [allowUndefined] @param {boolean} [normalizeNumbers] */
116
+ function checked(value, allowUndefined = false, normalizeNumbers = false) {
117
+ const seen = new Set();
118
+ /** @param {any} item @returns {any} */
119
+ function visit(item) {
120
+ if (item === undefined && allowUndefined) {
121
+ return item;
122
+ }
123
+ if (
124
+ item === null ||
125
+ typeof item === "string" ||
126
+ typeof item === "boolean" ||
127
+ typeof item === "bigint"
128
+ ) {
129
+ return item;
130
+ }
131
+ if (typeof item === "number" && Number.isFinite(item)) {
132
+ if (normalizeNumbers && Object.is(item, -0)) {
133
+ return 0;
134
+ }
135
+ return item;
136
+ }
137
+ if (item instanceof Date) {
138
+ if (Number.isNaN(item.getTime())) {
139
+ throw new ValidationError("Invalid Date cannot be stored");
140
+ }
141
+ return item;
142
+ }
143
+ if (item instanceof Uint8Array) {
144
+ if (item.constructor === Uint8Array) {
145
+ return item;
146
+ }
147
+ return new Uint8Array(item.buffer, item.byteOffset, item.byteLength);
148
+ }
149
+ if (typeof item !== "object" || item === null) {
150
+ throw new ValidationError("Documents must contain CBOR-compatible values");
151
+ }
152
+ if (seen.has(item)) {
153
+ throw new ValidationError("Cyclic documents cannot be stored");
154
+ }
155
+ seen.add(item);
156
+ let result;
157
+ if (Array.isArray(item)) {
158
+ result = [];
159
+ for (let i = 0; i < item.length; i++) {
160
+ if (!Object.hasOwn(item, i)) {
161
+ throw new ValidationError("Sparse arrays cannot be stored");
162
+ }
163
+ result.push(visit(item[i]));
164
+ }
165
+ } else {
166
+ if (
167
+ Object.getPrototypeOf(item) !== Object.prototype &&
168
+ Object.getPrototypeOf(item) !== null
169
+ ) {
170
+ throw new ValidationError("Documents must contain plain objects");
171
+ }
172
+ result = /** @type {Record<string, any>} */ (Object.create(null));
173
+ for (const [key, child] of Object.entries(item)) {
174
+ result[key] = visit(child);
175
+ }
176
+ }
177
+ seen.delete(item);
178
+ return result;
179
+ }
180
+ return visit(value);
181
+ }
182
+
183
+ /** @param {unknown} value */
184
+ export function encodeData(value) {
185
+ return Buffer.from(encode(checked(value), cborOptions));
186
+ }
187
+
188
+ /** @param {Uint8Array} bytes @returns {any} */
189
+ export function decodeData(bytes) {
190
+ return decode(new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength), {
191
+ collapseBigInts: false,
192
+ });
193
+ }
194
+
195
+ /** @param {unknown} value */
196
+ export function stable(value) {
197
+ return Buffer.from(encode(checked(value, true, true), { ...cborOptions, cde: true })).toString(
198
+ "hex",
199
+ );
200
+ }
package/src/errors.js ADDED
@@ -0,0 +1,43 @@
1
+ // @ts-check
2
+ export class SoroError extends Error {
3
+ /** @param {string} message @param {string} code @param {unknown} [details] */
4
+ constructor(message, code, details) {
5
+ super(message);
6
+ this.name = this.constructor.name;
7
+ this.code = code;
8
+ if (details !== undefined) {
9
+ this.details = details;
10
+ }
11
+ }
12
+ }
13
+
14
+ export class ValidationError extends SoroError {
15
+ /** @param {string} message @param {unknown} [details] */
16
+ constructor(message, details) {
17
+ super(message, "VALIDATION_ERROR", details);
18
+ }
19
+ }
20
+ export class SchemaError extends SoroError {
21
+ /** @param {string} message @param {unknown} [details] */
22
+ constructor(message, details) {
23
+ super(message, "SCHEMA_ERROR", details);
24
+ }
25
+ }
26
+ export class ConflictError extends SoroError {
27
+ /** @param {string} message @param {unknown} [details] */
28
+ constructor(message, details) {
29
+ super(message, "CONFLICT", details);
30
+ }
31
+ }
32
+ export class QueryError extends SoroError {
33
+ /** @param {string} message @param {unknown} [details] */
34
+ constructor(message, details) {
35
+ super(message, "QUERY_ERROR", details);
36
+ }
37
+ }
38
+ export class CursorError extends SoroError {
39
+ /** @param {string} message @param {unknown} [details] */
40
+ constructor(message, details) {
41
+ super(message, "CURSOR_ERROR", details);
42
+ }
43
+ }
@@ -0,0 +1,131 @@
1
+ // @ts-check
2
+ import { SchemaError, ValidationError } from "./errors.js";
3
+ import { docKey, getPath, indexPrefix, part, sortableNumber } from "./encoding.js";
4
+
5
+ /** @param {bigint} value */
6
+ function sortableBigInt(value) {
7
+ if (value === 0n) {
8
+ return Buffer.from([1]);
9
+ }
10
+ const negative = value < 0n;
11
+ let magnitude = value;
12
+ if (negative) {
13
+ magnitude = -value;
14
+ }
15
+ const hex = magnitude.toString(16);
16
+ let paddedHex = hex;
17
+ if (hex.length % 2) {
18
+ paddedHex = `0${hex}`;
19
+ }
20
+ const digits = Buffer.from(paddedHex, "hex");
21
+ const length = Buffer.alloc(8);
22
+ length.writeBigUInt64BE(BigInt(digits.length));
23
+ if (negative) {
24
+ for (let i = 0; i < length.length; i++) {
25
+ length[i] ^= 255;
26
+ }
27
+ for (let i = 0; i < digits.length; i++) {
28
+ digits[i] ^= 255;
29
+ }
30
+ }
31
+ let sign = 2;
32
+ if (negative) {
33
+ sign = 0;
34
+ }
35
+ return Buffer.concat([Buffer.from([sign]), length, digits]);
36
+ }
37
+
38
+ /** @param {unknown} value */
39
+ function supportsBtreeValue(value) {
40
+ return (
41
+ value === undefined ||
42
+ value === null ||
43
+ typeof value === "boolean" ||
44
+ (typeof value === "number" && Number.isFinite(value)) ||
45
+ typeof value === "bigint" ||
46
+ typeof value === "string" ||
47
+ (value instanceof Date && !Number.isNaN(value.getTime())) ||
48
+ value instanceof Uint8Array
49
+ );
50
+ }
51
+
52
+ /** @param {unknown} value @param {import("./types.js").Direction} [direction] */
53
+ function btreeScalar(value, direction = "asc") {
54
+ let bytes;
55
+ if (value === undefined) {
56
+ bytes = Buffer.from([0]);
57
+ } else if (value === null) {
58
+ bytes = Buffer.from([16]);
59
+ } else if (value === false) {
60
+ bytes = Buffer.from([32]);
61
+ } else if (value === true) {
62
+ bytes = Buffer.from([33]);
63
+ } else if (typeof value === "number") {
64
+ bytes = Buffer.concat([Buffer.from([48]), sortableNumber(value)]);
65
+ } else if (typeof value === "bigint") {
66
+ bytes = Buffer.concat([Buffer.from([56]), sortableBigInt(value)]);
67
+ } else if (value instanceof Date && !Number.isNaN(value.getTime())) {
68
+ bytes = Buffer.concat([Buffer.from([64]), sortableNumber(value.getTime())]);
69
+ } else if (typeof value === "string") {
70
+ bytes = Buffer.concat([Buffer.from([80]), part(Buffer.from(value, "utf8"))]);
71
+ } else if (value instanceof Uint8Array) {
72
+ bytes = Buffer.concat([Buffer.from([96]), part(value)]);
73
+ } else {
74
+ throw new ValidationError("btree indexes support scalars, Dates, BigInts, and byte arrays");
75
+ }
76
+ if (direction === "desc") {
77
+ return Buffer.from(bytes.map((byte) => byte ^ 255));
78
+ }
79
+ return bytes;
80
+ }
81
+
82
+ const btree = {
83
+ supportsValue: supportsBtreeValue,
84
+ encodeValue: btreeScalar,
85
+ /** @param {import("./types.js").Index} index @param {import("./types.js").IndexField[]} order */
86
+ supportsOrder: (index, order) =>
87
+ order.every(
88
+ (field, i) =>
89
+ index.fields[i]?.name === field.name && index.fields[i]?.direction === field.direction,
90
+ ),
91
+ /** @param {string} table @param {import("./types.js").Index} index @param {string} path @param {unknown} value */
92
+ equalityPrefix(table, index, path, value) {
93
+ if (
94
+ index.fields[0]?.name !== path ||
95
+ index.fields[0].direction !== "asc" ||
96
+ !supportsBtreeValue(value)
97
+ ) {
98
+ return null;
99
+ }
100
+ return Buffer.concat([indexPrefix(table, index.name), btreeScalar(value)]);
101
+ },
102
+ };
103
+
104
+ /** @type {Record<string, typeof btree>} */
105
+ const methods = { btree };
106
+
107
+ /** @param {string} name */
108
+ export function indexMethod(name) {
109
+ if (!Object.hasOwn(methods, name)) {
110
+ throw new SchemaError(`Unsupported index method ${name}`);
111
+ }
112
+ return methods[name];
113
+ }
114
+
115
+ /** @param {string} table @param {import("./types.js").Index} index @param {import("./types.js").Document} doc */
116
+ export function indexKey(table, index, doc) {
117
+ const method = indexMethod(index.method);
118
+ const values = [];
119
+ for (const field of index.fields) {
120
+ const value = getPath(doc, field.name);
121
+ if (value === undefined && index.unique) {
122
+ return null;
123
+ }
124
+ values.push(method.encodeValue(value, field.direction));
125
+ }
126
+ const prefix = Buffer.concat([indexPrefix(table, index.name), ...values]);
127
+ if (index.unique) {
128
+ return prefix;
129
+ }
130
+ return Buffer.concat([prefix, part(docKey(table, doc.id))]);
131
+ }
package/src/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // @ts-check
2
+ import { Database } from "./storage.js";
3
+
4
+ /** @param {import("./types.js").DatabaseConfig} config */
5
+ export default function SoroDB(config) {
6
+ return new Database(config);
7
+ }
8
+ export { SoroDB };
9
+ export {
10
+ SoroError,
11
+ ValidationError,
12
+ SchemaError,
13
+ ConflictError,
14
+ QueryError,
15
+ CursorError,
16
+ } from "./errors.js";
package/src/indexes.js ADDED
@@ -0,0 +1,32 @@
1
+ // @ts-check
2
+ import { ConflictError } from "./errors.js";
3
+ import { docKey } from "./encoding.js";
4
+ import { indexKey } from "./index-methods.js";
5
+
6
+ /** @param {import("@slatedb/uniffi").DbTransaction} tx @param {import("./types.js").Table} table @param {import("./types.js").Document} document @param {import("./types.js").Index[]} [indexes] */
7
+ export async function removeIndexes(tx, table, document, indexes = table.indexes) {
8
+ for (const index of indexes) {
9
+ const key = indexKey(table.name, index, document);
10
+ if (key) {
11
+ await tx.delete(key);
12
+ }
13
+ }
14
+ }
15
+
16
+ /** @param {import("@slatedb/uniffi").DbTransaction} tx @param {import("./types.js").Table} table @param {import("./types.js").Document} document @param {import("./types.js").Index[]} [indexes] */
17
+ export async function addIndexes(tx, table, document, indexes = table.indexes) {
18
+ const keyOfDocument = docKey(table.name, document.id);
19
+ for (const index of indexes) {
20
+ const key = indexKey(table.name, index, document);
21
+ if (!key) {
22
+ continue;
23
+ }
24
+ if (index.unique) {
25
+ const existing = await tx.get(key);
26
+ if (existing && !Buffer.from(existing).equals(keyOfDocument)) {
27
+ throw new ConflictError(`Unique index ${table.name}.${index.name} already has this value`);
28
+ }
29
+ }
30
+ await tx.put(key, keyOfDocument);
31
+ }
32
+ }