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.
package/src/schema.js ADDED
@@ -0,0 +1,247 @@
1
+ // @ts-check
2
+ import { type } from "arktype";
3
+ import { SchemaError, ValidationError } from "./errors.js";
4
+ import { encodeData, stable, validName } from "./encoding.js";
5
+ import { indexMethod } from "./index-methods.js";
6
+
7
+ /** @param {any} definition @returns {any} */
8
+ function normalizeType(definition) {
9
+ if (definition === "date") {
10
+ return "Date";
11
+ }
12
+ if (typeof definition === "string" && definition.endsWith("?")) {
13
+ const base = definition.slice(0, -1);
14
+ if (base === "date") {
15
+ return { optional: true, value: "Date" };
16
+ }
17
+ return { optional: true, value: base };
18
+ }
19
+ return definition;
20
+ }
21
+
22
+ /** @param {any} definition */
23
+ function serializableType(definition) {
24
+ if (typeof definition === "string") {
25
+ return definition;
26
+ }
27
+ if (definition && typeof definition.expression === "string") {
28
+ return definition.expression;
29
+ }
30
+ if (Array.isArray(definition) || (definition && typeof definition === "object")) {
31
+ return stable(definition);
32
+ }
33
+ throw new SchemaError("Column type must be an ArkType definition");
34
+ }
35
+
36
+ /** @param {import("./types.js").IndexDefinition} index @param {Record<string, import("./types.js").ColumnDefinition>} columns @returns {import("./types.js").Index} */
37
+ function normalizeIndex(index, columns) {
38
+ if (!index || typeof index !== "object") {
39
+ throw new SchemaError("Index must be an object");
40
+ }
41
+ const name = validName(index.name, "index name");
42
+ const method = index.method ?? "btree";
43
+ indexMethod(method);
44
+ if (!Array.isArray(index.columns) || !index.columns.length) {
45
+ throw new SchemaError(`Index ${name} must declare columns`);
46
+ }
47
+ const fields = index.columns.map((entry) => {
48
+ let field;
49
+ /** @type {import("./types.js").Direction} */
50
+ let direction = "asc";
51
+ if (typeof entry === "string") {
52
+ field = entry;
53
+ } else {
54
+ field = entry?.field;
55
+ direction = entry?.direction ?? "asc";
56
+ }
57
+ if (typeof field !== "string" || !field || !columns[field.split(".")[0]]) {
58
+ throw new SchemaError(`Index ${name} references unknown column ${field}`);
59
+ }
60
+ if (!["asc", "desc"].includes(direction)) {
61
+ throw new SchemaError(`Index ${name} has an invalid direction`);
62
+ }
63
+ return { name: field, direction };
64
+ });
65
+ if (new Set(fields.map((field) => field.name)).size !== fields.length) {
66
+ throw new SchemaError(`Index ${name} repeats a column`);
67
+ }
68
+ return { name, method, fields, unique: index.unique === true };
69
+ }
70
+
71
+ /** @param {{ version: number }} config @param {import("./types.js").TableDefinition[]} definitions @returns {import("./types.js").CompiledSchema} */
72
+ export function compileSchema(config, definitions) {
73
+ if (!config || !Number.isSafeInteger(config.version) || config.version < 1) {
74
+ throw new SchemaError("Schema version must be an integer of at least 1");
75
+ }
76
+ if (!Array.isArray(definitions) || !definitions.length) {
77
+ throw new SchemaError("Schema needs at least one table");
78
+ }
79
+ /** @type {Map<string, import("./types.js").Table>} */
80
+ const tables = new Map();
81
+ /** @type {Record<string, import("./types.js").StoredTable>} */
82
+ const storedTables = {};
83
+ for (const definition of definitions) {
84
+ if (!definition || typeof definition !== "object") {
85
+ throw new SchemaError("Table definition must be an object");
86
+ }
87
+ const name = validName(definition.table, "table name");
88
+ if (tables.has(name)) {
89
+ throw new SchemaError(`Duplicate table ${name}`);
90
+ }
91
+ const columns = definition.columns;
92
+ if (!columns || typeof columns !== "object" || Array.isArray(columns)) {
93
+ throw new SchemaError(`Table ${name} needs columns`);
94
+ }
95
+ if (
96
+ !columns.id ||
97
+ columns.id.primary !== true ||
98
+ Object.entries(columns).filter(([, col]) => col?.primary).length !== 1
99
+ ) {
100
+ throw new SchemaError(`Table ${name} must have exactly one primary column named id`);
101
+ }
102
+ /** @type {Record<string, any>} */
103
+ const shape = { "+": "reject" };
104
+ /** @type {Record<string, import("./types.js").StoredColumn>} */
105
+ const storedColumns = {};
106
+ for (const [columnName, column] of Object.entries(columns)) {
107
+ validName(columnName, "column name");
108
+ if (!column || typeof column !== "object" || !Object.hasOwn(column, "type")) {
109
+ throw new SchemaError(`Column ${name}.${columnName} needs a type`);
110
+ }
111
+ const normalized = normalizeType(column.type);
112
+ const optional = normalized?.optional === true;
113
+ let value = normalized;
114
+ let shapeKey = columnName;
115
+ if (optional) {
116
+ value = normalized.value;
117
+ shapeKey = `${columnName}?`;
118
+ }
119
+ shape[shapeKey] = value;
120
+ /** @type {import("./types.js").StoredColumn["default"]} */
121
+ let storedDefault = null;
122
+ if (Object.hasOwn(column, "default")) {
123
+ if (typeof column.default === "function") {
124
+ storedDefault = { kind: "function" };
125
+ } else {
126
+ storedDefault = { kind: "value", value: encodeData(column.default).toString("base64") };
127
+ }
128
+ }
129
+ storedColumns[columnName] = {
130
+ type: serializableType(column.type),
131
+ primary: column.primary === true,
132
+ optional,
133
+ default: storedDefault,
134
+ };
135
+ }
136
+ if (storedColumns.id.optional || !/^(number|string)(\.|$)/.test(storedColumns.id.type)) {
137
+ throw new SchemaError(`Table ${name} id must be a required string or number`);
138
+ }
139
+ let validator;
140
+ try {
141
+ validator = type(shape).onDeepUndeclaredKey("reject");
142
+ } catch (error) {
143
+ throw new SchemaError(`Invalid ArkType schema for ${name}: ${error.message}`);
144
+ }
145
+ const indexes = (definition.indexes ?? []).map((index) => normalizeIndex(index, columns));
146
+ if (new Set(indexes.map((index) => index.name)).size !== indexes.length) {
147
+ throw new SchemaError(`Table ${name} repeats an index name`);
148
+ }
149
+ tables.set(name, { name, columns, indexes, validator });
150
+ storedTables[name] = { columns: storedColumns, indexes };
151
+ }
152
+ return {
153
+ version: config.version,
154
+ tables,
155
+ stored: { version: config.version, tables: storedTables },
156
+ };
157
+ }
158
+
159
+ /** @param {object} value @returns {value is import("./types.js").Document} */
160
+ function hasValidId(value) {
161
+ if (!("id" in value)) {
162
+ return false;
163
+ }
164
+ return (
165
+ typeof value.id === "string" || (typeof value.id === "number" && Number.isFinite(value.id))
166
+ );
167
+ }
168
+
169
+ /** @param {import("./types.js").Table} table @param {unknown} document @param {boolean} [applyDefaults] @returns {import("./types.js").Document} */
170
+ export function validateDocument(table, document, applyDefaults = true) {
171
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
172
+ throw new ValidationError("Document must be an object");
173
+ }
174
+ /** @type {Record<string, unknown>} */
175
+ const candidate = { ...document };
176
+ if (applyDefaults) {
177
+ for (const [name, column] of Object.entries(table.columns)) {
178
+ if (candidate[name] === undefined && Object.hasOwn(column, "default")) {
179
+ if (typeof column.default === "function") {
180
+ candidate[name] = column.default();
181
+ } else {
182
+ candidate[name] = structuredClone(column.default);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ const result = table.validator(candidate);
188
+ if (result instanceof type.errors) {
189
+ throw new ValidationError(
190
+ `Document failed ${table.name} schema validation: ${result.summary}`,
191
+ result.flatByPath,
192
+ );
193
+ }
194
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
195
+ throw new ValidationError("Document schema must produce an object");
196
+ }
197
+ if (!hasValidId(result)) {
198
+ throw new ValidationError("id must be a string or finite number");
199
+ }
200
+ encodeData(result);
201
+ return result;
202
+ }
203
+
204
+ /** @param {import("./types.js").StoredSchema} previous @param {import("./types.js").StoredSchema} next @returns {import("./types.js").SchemaDiff} */
205
+ export function schemaDiff(previous, next) {
206
+ if (next.version < previous.version) {
207
+ throw new SchemaError(
208
+ `Schema version ${next.version} is older than stored version ${previous.version}`,
209
+ );
210
+ }
211
+ if (next.version === previous.version) {
212
+ if (stable(previous.tables) !== stable(next.tables)) {
213
+ throw new SchemaError("Schema definition changed without increasing its version");
214
+ }
215
+ return { changed: false, additions: new Map() };
216
+ }
217
+ const additions = new Map();
218
+ for (const [name, oldTable] of Object.entries(previous.tables)) {
219
+ const newTable = next.tables[name];
220
+ if (!newTable) {
221
+ throw new SchemaError(`Removing table ${name} is not a safe migration`);
222
+ }
223
+ for (const [field, oldColumn] of Object.entries(oldTable.columns)) {
224
+ if (!newTable.columns[field] || stable(oldColumn) !== stable(newTable.columns[field])) {
225
+ throw new SchemaError(`Changing or removing ${name}.${field} is not a safe migration`);
226
+ }
227
+ }
228
+ for (const oldIndex of oldTable.indexes) {
229
+ const newIndex = newTable.indexes.find((index) => index.name === oldIndex.name);
230
+ if (!newIndex || stable(oldIndex) !== stable(newIndex)) {
231
+ throw new SchemaError(
232
+ `Changing or removing index ${name}.${oldIndex.name} is not a safe migration`,
233
+ );
234
+ }
235
+ }
236
+ }
237
+ for (const [name, newTable] of Object.entries(next.tables)) {
238
+ const oldTable = previous.tables[name];
239
+ additions.set(name, {
240
+ columns: Object.keys(newTable.columns).filter((field) => !oldTable?.columns[field]),
241
+ indexes: newTable.indexes.filter(
242
+ (index) => !oldTable?.indexes.some((old) => old.name === index.name),
243
+ ),
244
+ });
245
+ }
246
+ return { changed: true, additions };
247
+ }
package/src/storage.js ADDED
@@ -0,0 +1,334 @@
1
+ // @ts-check
2
+ import { DbBuilder, IsolationLevel, ObjectStore } from "@slatedb/uniffi";
3
+ import { randomUUID } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { SchemaError, CursorError } from "./errors.js";
7
+ import { compileSchema, schemaDiff, validateDocument } from "./schema.js";
8
+ import { addIndexes } from "./indexes.js";
9
+ import {
10
+ decodeData,
11
+ docKey,
12
+ docPrefix,
13
+ encodeData,
14
+ indexPrefix,
15
+ range,
16
+ stable,
17
+ } from "./encoding.js";
18
+ import { Collection } from "./collection.js";
19
+
20
+ const META_KEY = Buffer.from("M\0schema");
21
+ const CURSOR_TTL = 5 * 60 * 1000;
22
+
23
+ /** @param {import("./types.js").Reader} reader @param {Uint8Array} prefix @param {Uint8Array} [after] */
24
+ export async function* scan(reader, prefix, after) {
25
+ const iterator = await reader.scan(range(prefix, after));
26
+ try {
27
+ while (true) {
28
+ const row = await iterator.next();
29
+ if (!row) {
30
+ return;
31
+ }
32
+ yield { key: Buffer.from(row.key), value: Buffer.from(row.value) };
33
+ }
34
+ } finally {
35
+ iterator.dispose();
36
+ }
37
+ }
38
+
39
+ export class Database {
40
+ /** @param {import("./types.js").DatabaseConfig} config */
41
+ constructor(config) {
42
+ if (
43
+ !config ||
44
+ typeof config.store !== "string" ||
45
+ typeof config.path !== "string" ||
46
+ !config.path
47
+ ) {
48
+ throw new SchemaError("SoroDB needs { store: URL, path: nonempty string }");
49
+ }
50
+ this.config = config;
51
+ if (
52
+ config.cursorTtlMs !== undefined &&
53
+ (!Number.isSafeInteger(config.cursorTtlMs) || config.cursorTtlMs < 1)
54
+ ) {
55
+ throw new SchemaError("cursorTtlMs must be a positive integer");
56
+ }
57
+ this.cursorTtlMs = config.cursorTtlMs ?? CURSOR_TTL;
58
+ this.definition = null;
59
+ this.native = null;
60
+ this.store = null;
61
+ this.opening = null;
62
+ this.closed = false;
63
+ /** @type {Map<string, import("./types.js").Session>} */
64
+ this.sessions = new Map();
65
+ }
66
+
67
+ /** @param {{ version: number }} config @param {import("./types.js").TableDefinition[]} definitions */
68
+ schema(config, definitions) {
69
+ if (this.opening || this.native || this.closed) {
70
+ throw new SchemaError("Register the schema before opening the database");
71
+ }
72
+ this.definition = compileSchema(config, definitions);
73
+ return this;
74
+ }
75
+
76
+ async _ready() {
77
+ if (this.closed) {
78
+ throw new SchemaError("Database is closed");
79
+ }
80
+ if (!this.definition) {
81
+ throw new SchemaError("Register a schema before using collections");
82
+ }
83
+ if (!this.opening) {
84
+ this.opening = this._open();
85
+ }
86
+ return this.opening;
87
+ }
88
+
89
+ async _open() {
90
+ const definition = this.definition;
91
+ if (!definition) {
92
+ throw new SchemaError("Register a schema before opening the database");
93
+ }
94
+ // SlateDB 0.16 accepts the filesystem location in DbBuilder's path, not
95
+ // in ObjectStore.resolve's URL. Keep the friendlier SoroDB config shape.
96
+ const local = this.config.store.startsWith("file:");
97
+ let storeUrl = this.config.store;
98
+ let databasePath = this.config.path;
99
+ if (local) {
100
+ storeUrl = "file:///";
101
+ databasePath = join(fileURLToPath(this.config.store), this.config.path);
102
+ }
103
+ this.store = ObjectStore.resolve(storeUrl);
104
+ const builder = new DbBuilder(databasePath, this.store);
105
+ try {
106
+ this.native = await builder.build();
107
+ } catch (error) {
108
+ this.store.dispose();
109
+ this.store = null;
110
+ throw error;
111
+ } finally {
112
+ builder.dispose();
113
+ }
114
+ try {
115
+ const bytes = await this.native.get(META_KEY);
116
+ if (!bytes) {
117
+ await this.native.put(META_KEY, encodeData(definition.stored));
118
+ } else {
119
+ const previous = decodeData(bytes);
120
+ const diff = schemaDiff(previous, definition.stored);
121
+ if (diff.changed) {
122
+ await this._migrate(diff);
123
+ }
124
+ }
125
+ } catch (error) {
126
+ await this.native.shutdown().catch(() => {});
127
+ this.native.dispose();
128
+ this.store.dispose();
129
+ this.native = null;
130
+ this.store = null;
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ /** @param {import("./types.js").SchemaDiff} diff */
136
+ async _migrate(diff) {
137
+ const definition = this.definition;
138
+ const native = this.native;
139
+ if (!definition || !native) {
140
+ throw new SchemaError("Database is not open");
141
+ }
142
+ for (const [name, changes] of diff.additions) {
143
+ if (!changes.columns.length && !changes.indexes.length) {
144
+ continue;
145
+ }
146
+ const table = definition.tables.get(name);
147
+ if (!table) {
148
+ throw new SchemaError(`Collection ${name} is absent from the schema`);
149
+ }
150
+ // A prior attempt may have stopped after writing some entries. Rebuild only
151
+ // indexes newly introduced by this version before exposing the schema.
152
+ for (const index of changes.indexes) {
153
+ for await (const row of scan(native, indexPrefix(name, index.name))) {
154
+ await native.delete(row.key);
155
+ }
156
+ }
157
+ for await (const row of scan(native, docPrefix(name))) {
158
+ const before = decodeData(row.value);
159
+ const after = validateDocument(table, before);
160
+ const changed = stable(before) !== stable(after);
161
+ const tx = await native.begin(IsolationLevel.SerializableSnapshot);
162
+ try {
163
+ if (changed) {
164
+ await tx.put(docKey(name, after.id), encodeData(after));
165
+ }
166
+ if (changes.indexes.length) {
167
+ await addIndexes(tx, table, after, changes.indexes);
168
+ }
169
+ await tx.commit();
170
+ } catch (error) {
171
+ await tx.rollback().catch(() => {});
172
+ throw error;
173
+ } finally {
174
+ tx.dispose();
175
+ }
176
+ }
177
+ }
178
+ await native.put(META_KEY, encodeData(definition.stored));
179
+ }
180
+
181
+ /** @param {string} name */
182
+ async collection(name) {
183
+ await this._ready();
184
+ if (!this.definition) {
185
+ throw new SchemaError("Register a schema before using collections");
186
+ }
187
+ const table = this.definition.tables.get(name);
188
+ if (!table) {
189
+ throw new SchemaError(`Collection ${name} is absent from the schema`);
190
+ }
191
+ return new Collection(this, table);
192
+ }
193
+
194
+ /** @template T @param {(tx: import("@slatedb/uniffi").DbTransaction) => Promise<T>} fn @returns {Promise<T>} */
195
+ async _atomic(fn) {
196
+ await this._ready();
197
+ if (!this.native) {
198
+ throw new SchemaError("Database is not open");
199
+ }
200
+ const tx = await this.native.begin(IsolationLevel.SerializableSnapshot);
201
+ try {
202
+ const result = await fn(tx);
203
+ await tx.commit();
204
+ return result;
205
+ } catch (error) {
206
+ await tx.rollback().catch(() => {});
207
+ throw error;
208
+ } finally {
209
+ tx.dispose();
210
+ }
211
+ }
212
+
213
+ /** @template T @param {(tx: { collection: (name: string) => Promise<Collection> }) => Promise<T>} callback @returns {Promise<T>} */
214
+ async transaction(callback) {
215
+ if (typeof callback !== "function") {
216
+ throw new SchemaError("transaction requires a callback");
217
+ }
218
+ const definition = this.definition;
219
+ if (!definition) {
220
+ throw new SchemaError("Register a schema before using collections");
221
+ }
222
+ return this._atomic(async (native) => {
223
+ /** @type {{ collection: (name: string) => Promise<Collection> }} */
224
+ const tx = {
225
+ collection: async (name) => {
226
+ const table = definition.tables.get(name);
227
+ if (!table) {
228
+ throw new SchemaError(`Collection ${name} is absent from the schema`);
229
+ }
230
+ return new Collection(this, table, native);
231
+ },
232
+ };
233
+ return callback(tx);
234
+ });
235
+ }
236
+
237
+ _session() {
238
+ const now = Date.now();
239
+ for (const [id, session] of this.sessions) {
240
+ if (!session.active && session.lastUsed + this.cursorTtlMs < now) {
241
+ this._releaseSession(id);
242
+ }
243
+ }
244
+ }
245
+
246
+ /** @param {string} fingerprint */
247
+ async _newSession(fingerprint) {
248
+ await this._ready();
249
+ if (!this.native) {
250
+ throw new SchemaError("Database is not open");
251
+ }
252
+ this._session();
253
+ const id = randomUUID();
254
+ const snapshot = await this.native.snapshot();
255
+ const session = { snapshot, fingerprint, lastUsed: Date.now(), active: 0 };
256
+ this.sessions.set(id, session);
257
+ this._armSession(id, session);
258
+ return { id, session };
259
+ }
260
+
261
+ /** @param {string} id @param {import("./types.js").Session} session */
262
+ _armSession(id, session) {
263
+ clearTimeout(session.timer);
264
+ if (session.active) {
265
+ return;
266
+ }
267
+ session.timer = setTimeout(() => this._releaseSession(id), this.cursorTtlMs);
268
+ session.timer.unref?.();
269
+ }
270
+
271
+ /** @param {import("./types.js").Session} session */
272
+ _holdSession(session) {
273
+ session.active++;
274
+ clearTimeout(session.timer);
275
+ }
276
+
277
+ /** @param {string} id */
278
+ _idleSession(id) {
279
+ const session = this.sessions.get(id);
280
+ if (!session) {
281
+ return;
282
+ }
283
+ session.active--;
284
+ session.lastUsed = Date.now();
285
+ this._armSession(id, session);
286
+ }
287
+
288
+ /** @param {string} id @param {string} fingerprint */
289
+ _getSession(id, fingerprint) {
290
+ this._session();
291
+ const session = this.sessions.get(id);
292
+ if (!session || session.fingerprint !== fingerprint) {
293
+ throw new CursorError("Cursor is expired or belongs to a different query");
294
+ }
295
+ session.lastUsed = Date.now();
296
+ this._armSession(id, session);
297
+ return session;
298
+ }
299
+
300
+ /** @param {string} id */
301
+ _releaseSession(id) {
302
+ const session = this.sessions.get(id);
303
+ if (session) {
304
+ clearTimeout(session.timer);
305
+ session.snapshot.dispose();
306
+ this.sessions.delete(id);
307
+ }
308
+ }
309
+
310
+ async close() {
311
+ if (this.closed) {
312
+ return;
313
+ }
314
+ this.closed = true;
315
+ if (this.opening) {
316
+ await this.opening.catch(() => {});
317
+ }
318
+ for (const id of this.sessions.keys()) {
319
+ this._releaseSession(id);
320
+ }
321
+ if (this.native) {
322
+ try {
323
+ await this.native.shutdown();
324
+ } finally {
325
+ this.native.dispose();
326
+ this.native = null;
327
+ }
328
+ }
329
+ if (this.store) {
330
+ this.store.dispose();
331
+ this.store = null;
332
+ }
333
+ }
334
+ }
package/src/types.js ADDED
@@ -0,0 +1,29 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {"asc" | "desc"} Direction */
4
+ /** @typedef {Record<string, unknown> & { id: string | number }} Document */
5
+ /** @typedef {{ store: string, path: string, cursorTtlMs?: number }} DatabaseConfig */
6
+ /** @typedef {string | import("arktype").Type<unknown> | readonly ArkTypeValue[] | { [key: string]: ArkTypeValue }} ArkTypeDefinition */
7
+ /** @typedef {ArkTypeDefinition | null | boolean | number | bigint | Date | Uint8Array} ArkTypeValue */
8
+ /** @typedef {null | boolean | number | string | bigint | Date | Uint8Array | readonly DataValue[] | { [key: string]: DataValue }} DataValue */
9
+ /** @typedef {{ type: ArkTypeDefinition, primary?: boolean, default?: DataValue | (() => DataValue) }} ColumnDefinition */
10
+ /** @typedef {{ name: string, method?: "btree", columns: (string | { field: string, direction?: Direction })[], unique?: boolean }} IndexDefinition */
11
+ /** @typedef {{ table: string, columns: Record<string, ColumnDefinition>, indexes?: IndexDefinition[] }} TableDefinition */
12
+ /** @typedef {{ name: string, direction: Direction }} IndexField */
13
+ /** @typedef {{ name: string, method: "btree", fields: IndexField[], unique: boolean }} Index */
14
+ /** @typedef {{ name: string, columns: Record<string, ColumnDefinition>, indexes: Index[], validator: (document: unknown) => unknown }} Table */
15
+ /** @typedef {{ type: string, primary: boolean, optional: boolean, default: null | { kind: string, value?: string } }} StoredColumn */
16
+ /** @typedef {{ columns: Record<string, StoredColumn>, indexes: Index[] }} StoredTable */
17
+ /** @typedef {{ version: number, tables: Record<string, StoredTable> }} StoredSchema */
18
+ /** @typedef {{ version: number, tables: Map<string, Table>, stored: StoredSchema }} CompiledSchema */
19
+ /** @typedef {{ changed: boolean, additions: Map<string, { columns: string[], indexes: Index[] }> }} SchemaDiff */
20
+ /** @typedef {import("@slatedb/uniffi").Db | import("@slatedb/uniffi").DbSnapshot | import("@slatedb/uniffi").DbTransaction} Reader */
21
+ /** @typedef {{ snapshot: import("@slatedb/uniffi").DbSnapshot, fingerprint: string, lastUsed: number, active: number, timer?: NodeJS.Timeout }} Session */
22
+ /** @typedef {{ op: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "contains" | "startsWith" | "exists", path: string, value: unknown }} Predicate */
23
+ /** @typedef {{ op: "and", conditions: WhereNode[] } | { op: "or", conditions: WhereNode[] } | { op: "not", condition: WhereNode } | Predicate} WhereNode */
24
+ /** @typedef {{ eq: (path: string, value: unknown) => WhereNode, ne: (path: string, value: unknown) => WhereNode, gt: (path: string, value: unknown) => WhereNode, gte: (path: string, value: unknown) => WhereNode, lt: (path: string, value: unknown) => WhereNode, lte: (path: string, value: unknown) => WhereNode, in: (path: string, value: unknown[]) => WhereNode, contains: (path: string, value: unknown) => WhereNode, startsWith: (path: string, value: string) => WhereNode, exists: (path: string, value?: boolean) => WhereNode, and: (...conditions: WhereNode[]) => WhereNode, or: (...conditions: WhereNode[]) => WhereNode, not: (condition: WhereNode) => WhereNode }} WhereBuilder */
25
+ /** @typedef {{ asc: (path: string) => OrderBuilder, desc: (path: string) => OrderBuilder }} OrderBuilder */
26
+ /** @typedef {{ where?: (builder: WhereBuilder) => WhereNode, orderBy?: (builder: OrderBuilder) => unknown, select?: string[], limit?: number, cursor?: string }} QueryOptions */
27
+ /** @typedef {{ id: string, last: Buffer, fingerprint: string }} Cursor */
28
+
29
+ export {};
@@ -0,0 +1,20 @@
1
+ import { Query } from "./query.js";
2
+ export declare class Collection {
3
+ db: import("./storage.js").Database;
4
+ table: import("./types.js").Table;
5
+ transaction: import("@slatedb/uniffi").DbTransaction | null;
6
+ /** @param {import("./storage.js").Database} db @param {import("./types.js").Table} table @param {import("@slatedb/uniffi").DbTransaction | null} [transaction] */
7
+ constructor(db: import("./storage.js").Database, table: import("./types.js").Table, transaction?: import("@slatedb/uniffi").DbTransaction | null);
8
+ /** @template T @param {(tx: import("@slatedb/uniffi").DbTransaction) => Promise<T>} callback @returns {Promise<T>} */
9
+ _write<T>(callback: (tx: import("@slatedb/uniffi").DbTransaction) => Promise<T>): Promise<T>;
10
+ /** @param {import("./types.js").Document} document */
11
+ create(document: import("./types.js").Document): Promise<import("./types.js").Document>;
12
+ /** @param {string | number} id */
13
+ get(id: string | number): Promise<any>;
14
+ /** @param {string | number} id @param {import("./types.js").Document} document */
15
+ replace(id: string | number, document: import("./types.js").Document): Promise<import("./types.js").Document>;
16
+ /** @param {string | number} id */
17
+ delete(id: string | number): Promise<boolean>;
18
+ /** @param {import("./types.js").QueryOptions} [options] */
19
+ filter(options?: import("./types.js").QueryOptions): Query;
20
+ }
@@ -0,0 +1,32 @@
1
+ /** @param {Uint8Array} bytes */
2
+ export declare function part(bytes: Uint8Array): Buffer<ArrayBuffer>;
3
+ /** @param {number} value */
4
+ declare function sortableNumber(value: number): Buffer<ArrayBuffer>;
5
+ export { sortableNumber };
6
+ /** @param {unknown} name @param {string} [label] */
7
+ export declare function validName(name: unknown, label?: string): string;
8
+ /** @param {unknown} id */
9
+ export declare function idBytes(id: unknown): Buffer<ArrayBuffer>;
10
+ /** @param {string} table */
11
+ export declare function docPrefix(table: string): Buffer<ArrayBuffer>;
12
+ /** @param {string} table @param {string | number} id */
13
+ export declare function docKey(table: string, id: string | number): Buffer<ArrayBuffer>;
14
+ /** @param {string} table @param {string} name */
15
+ export declare function indexPrefix(table: string, name: string): Buffer<ArrayBuffer>;
16
+ /** @param {unknown} object @param {string} path @returns {unknown} */
17
+ export declare function getPath(object: unknown, path: string): unknown;
18
+ /** @param {Uint8Array} prefix */
19
+ export declare function prefixEnd(prefix: Uint8Array): Buffer<ArrayBuffer> | undefined;
20
+ /** @param {Uint8Array} prefix @param {Uint8Array} [after] */
21
+ export declare function range(prefix: Uint8Array, after?: Uint8Array): {
22
+ start: Uint8Array<ArrayBufferLike>;
23
+ start_inclusive: boolean;
24
+ end: Buffer<ArrayBuffer> | undefined;
25
+ end_inclusive: boolean;
26
+ };
27
+ /** @param {unknown} value */
28
+ export declare function encodeData(value: unknown): Buffer<ArrayBuffer>;
29
+ /** @param {Uint8Array} bytes @returns {any} */
30
+ export declare function decodeData(bytes: Uint8Array): any;
31
+ /** @param {unknown} value */
32
+ export declare function stable(value: unknown): string;
@@ -0,0 +1,26 @@
1
+ export declare class SoroError extends Error {
2
+ code: string;
3
+ details: unknown;
4
+ /** @param {string} message @param {string} code @param {unknown} [details] */
5
+ constructor(message: string, code: string, details?: unknown);
6
+ }
7
+ export declare class ValidationError extends SoroError {
8
+ /** @param {string} message @param {unknown} [details] */
9
+ constructor(message: string, details?: unknown);
10
+ }
11
+ export declare class SchemaError extends SoroError {
12
+ /** @param {string} message @param {unknown} [details] */
13
+ constructor(message: string, details?: unknown);
14
+ }
15
+ export declare class ConflictError extends SoroError {
16
+ /** @param {string} message @param {unknown} [details] */
17
+ constructor(message: string, details?: unknown);
18
+ }
19
+ export declare class QueryError extends SoroError {
20
+ /** @param {string} message @param {unknown} [details] */
21
+ constructor(message: string, details?: unknown);
22
+ }
23
+ export declare class CursorError extends SoroError {
24
+ /** @param {string} message @param {unknown} [details] */
25
+ constructor(message: string, details?: unknown);
26
+ }