@evumi/berth 0.0.8 → 0.0.10

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @evumi/berth
2
2
 
3
- Berth 项目 Queues(队列)和 Alarms(定时器)的客户端,用法照 Cloudflare Queues 和 Durable Object alarms。也负责处理平台推给应用的请求(队列批次、定时器、Cron HTTP 目标),并校验签名。零运行时依赖,支持 Node ≥ 18、Bun、Deno 和 edge 运行时。
3
+ Berth 项目 Queues(队列)和 Alarms(定时器)的客户端,用法照 Cloudflare Queues 和 Durable Object alarms。也负责处理平台推给应用的请求(队列批次、定时器、Cron HTTP 目标),并校验签名。主入口零运行时依赖,支持 Node ≥ 18、Bun、Deno 和 edge 运行时。文档数据库是独立子路径,驱动按需安装。
4
4
 
5
5
  项目开了 `queues` 或 `alarms` 能力后,Berth 会给容器注入 `BERTH_CONFIG_URL`、`BERTH_CONFIG_TOKEN` 和 `BERTH_SIGNING_SECRET`。环境是请求来源的那个环境:预览环境只会用到自己的队列和定时器。
6
6
 
@@ -134,10 +134,73 @@ await trialEnd.deleteAlarm(); // 没有也不报错
134
134
 
135
135
  请求失败抛 `BerthError`,带 `status`、`code`、`message`(429 时还有 `retryAfterMs`)。`code` 取服务器返回的值:`unauthorized`、`capability_disabled`、`not_found`、`bad_request`、`conflict`、`too_large`、`rate_limited`、`quota_exceeded`、`unavailable`、`queue_full`。客户端自己的 `code`:`timeout`、`network`、`invalid_response`、`missing_env`。错误信息里不含令牌。
136
136
 
137
+
138
+ ## 文档数据库(SQLite / libSQL)
139
+
140
+ `@evumi/berth/documents` 提供一个刻意收小的 Mongo 风格接口,不用定义业务表或 schema。底层是一张 JSON 文档表;不是 MongoDB 协议、Mongoose 或完整 MongoDB 兼容层。
141
+
142
+ Bun 本地持久化(Worker 默认路径是 `/data/worker.sqlite`):
143
+
144
+ ```ts
145
+ import { openBunDocumentStore } from '@evumi/berth/documents/bun';
146
+
147
+ const db = await openBunDocumentStore(Bun.env.BERTH_SQLITE_PATH ?? '/data/worker.sqlite');
148
+ const notes = db.collection<{ title: string; views: number; author?: { name: string } }>('notes');
149
+ const { insertedId } = await notes.insertOne({ title: 'hello', views: 0 });
150
+ await notes.updateOne({ _id: insertedId }, { $inc: { views: 1 } });
151
+ const page = await notes.find({ views: { $gte: 1 } }).sort({ views: -1 }).limit(20).skip(0).toArray();
152
+ await notes.createIndex({ views: -1 });
153
+ await db.close(); // 通常在应用关闭时调用
154
+ ```
155
+
156
+ 远程 libSQL 复用 Berth 已有的绑定变量:
157
+
158
+ ```sh
159
+ pnpm add @libsql/client
160
+ ```
161
+
162
+ ```ts
163
+ import { openLibSQLDocumentStore } from '@evumi/berth/documents/libsql';
164
+ const db = await openLibSQLDocumentStore({
165
+ url: Bun.env.LIBSQL_URL!,
166
+ authToken: Bun.env.LIBSQL_AUTH_TOKEN,
167
+ });
168
+ ```
169
+
170
+ 也能注入已有的官方 libSQL 客户端或自定义 adapter:
171
+
172
+ ```ts
173
+ import { createClient } from '@libsql/client';
174
+ import { createDocumentStore } from '@evumi/berth/documents';
175
+ const client = createClient({ url: 'file:./app.sqlite' });
176
+ const db = createDocumentStore(client);
177
+ await db.ready(); // 幂等初始化;各 CRUD 也会等待初始化
178
+ // 低层工厂不接管 client 的生命周期或改变它的 PRAGMA。
179
+ ```
180
+
181
+ Bun 和本地 `file:` 便利工厂使用 WAL、`synchronous=FULL`、`busy_timeout=5000`。远程工厂服从服务器的持久化设置;它不承诺复制、节点或磁盘丢失后的数据安全。`busy_timeout` 是 SQLite 锁等待,不会重放已发送的写操作。所有工厂均无透明重试;数据库错误可能表示写入已执行但响应丢失,不能据此直接重发 `$inc`。
182
+
183
+ API 和边界:
184
+
185
+ - `insertOne(document)` 返回 `{ insertedId }`;可传 1–256 字符的字符串 `_id`,否则生成 UUID。`findOne(filter?)` 返回文档或 `null`;`find(filter?)` 返回不可变 cursor。`updateOne(filter, update)` 返回 `{ matchedCount }`,不声称内容一定改变;`deleteOne(filter)` 返回 `{ deletedCount }`;`countDocuments(filter?)` 返回匹配总数。
186
+ - 查询支持标量等值、`$eq/$ne/$gt/$gte/$lt/$lte/$in/$exists`、`$and/$or`。点路径仅遍历对象,例如 `author.name`;每段为 ASCII 字母或下划线开头,后接字母、数字、下划线。没有数组元素匹配、正则、任意 JS、聚合、join、upsert、unique index 或跨调用事务。
187
+ - `null` 只匹配显式 JSON null;missing 独立,`$exists:false` 只匹配 missing。`$ne` 包括 missing。布尔与数值严格区分;范围查询只比较同类的 number 或 string。`$in:[]` 不匹配任何文档。JSON 数值是 JS number;高精度业务值用字符串保存。
188
+ - 排序使用 SQLite JSON 提取值的排序:missing/null 同组,然后 number/boolean(false=0、true=1),然后 text、array/object 的 JSON 文本。字符串使用 SQLite BINARY 顺序;这个跨类型排序不是 MongoDB BSON 顺序。末尾追加 `_id ASC` 保证稳定结果;未指定排序时按 `_id ASC`。`findOne/updateOne/deleteOne` 选匹配中的最小 `_id`。
189
+ - 默认读取 100 条,最大 1000;`limit(0)` 是空结果。skip 最大 1,000,000,较大 offset 和无索引过滤会扫描;计数不受读取条数上限影响。过滤最多 64 条件、10 层逻辑嵌套、每个 `$in` 最多 100 项。
190
+ - `$set` 支持 JSON 值,`$unset` 的值为 `true` 或 `1`,`$inc` 支持有限数。missing 的自增从 0 开始;已有 null、布尔、字符串、对象、数组会让整次更新失败。父路径如果存在但不是对象,也失败;不允许重叠更新路径或修改 `_id`。每次更新是一条原子 SQL,最多 32 个字段,不会先读取到 JS 再写回。
191
+ - 单文档最多 1 MiB(UTF-8 JSON),最多 32 层;仅接受普通 JSON。undefined、NaN/Infinity、Date、BigInt、循环、getter、稀疏数组和自定义原型均拒绝。数组可以存储但没有数组查询语义。
192
+ - `createIndex({ field: 1|-1 })` 创建最多 4 字段的普通表达式索引,返回名称。物理索引以 collection 为首列,覆盖同库全部 collection;相同规格共享索引,新增索引会影响所有 collection 的写入成本。查询中的 collection 和值始终绑定参数;字段路径经过校验,表达式与索引完全一致。
193
+ - collection 是命名空间,不是权限隔离。每个项目/环境应使用自己的数据库文件或凭据;持有同一数据库客户端的业务代码可以访问该库所有 collection。内部 `_berth_documents` 表不应手动改写。
194
+
195
+ 错误为 `DocumentError`,`code` 是 `invalid_input`、`database_error` 或 `invalid_data`。数据库错误不包含 SQL 参数或文档内容。`database_error` 不说明远端是否已提交;需要业务幂等设计才能安全重试。
196
+
197
+ 真实数据库测试和三条执行路径的复现基准见 [bench/README.md](bench/README.md)。
198
+
137
199
  ## 开发
138
200
 
139
201
  ```sh
140
202
  pnpm install
141
203
  pnpm typecheck
142
204
  pnpm test # 先构建,再跑 vitest(含 sdk/testdata 的共享签名向量)
205
+ pnpm test:bun # Bun持久化烟测 + 4个Bun写进程、2个读进程共享SQLite的并发检查
143
206
  ```
@@ -0,0 +1,3 @@
1
+ import { type OwnedDocumentStore } from '../documents.js';
2
+ /** Local durable SQLite. Native Bun code is loaded only from this optional entry. */
3
+ export declare function openBunDocumentStore(filename: string): Promise<OwnedDocumentStore>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.openBunDocumentStore = openBunDocumentStore;
37
+ const documents_js_1 = require("../documents.js");
38
+ /** Local durable SQLite. Native Bun code is loaded only from this optional entry. */
39
+ async function openBunDocumentStore(filename) {
40
+ const moduleName = 'bun:sqlite';
41
+ const { Database } = await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s)));
42
+ const db = new Database(filename);
43
+ try {
44
+ db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000;');
45
+ const client = { async execute({ sql, args }) { return { rows: db.query(sql).all(...args) }; } };
46
+ const store = (0, documents_js_1.createDocumentStore)(client);
47
+ await store.ready();
48
+ return { ...store, close: () => db.close() };
49
+ }
50
+ catch (error) {
51
+ db.close();
52
+ throw error;
53
+ }
54
+ }
@@ -0,0 +1,14 @@
1
+ export type JSONScalar = string | number | boolean | null;
2
+ export type JSONValue = JSONScalar | JSONValue[] | {
3
+ [key: string]: JSONValue;
4
+ };
5
+ export declare const MAX_DOCUMENT_BYTES: number;
6
+ export declare class DocumentError extends Error {
7
+ readonly code: 'invalid_input' | 'database_error' | 'invalid_data';
8
+ constructor(code: 'invalid_input' | 'database_error' | 'invalid_data', message: string);
9
+ }
10
+ export declare function invalid(message: string): never;
11
+ export declare function entries(value: unknown, label?: string): [string, unknown][];
12
+ export declare function validateJSON(value: unknown, ancestors?: Set<object>, depth?: number): asserts value is JSONValue;
13
+ export declare function jsonText(value: unknown): string;
14
+ export declare function scalar(value: unknown): asserts value is JSONScalar;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DocumentError = exports.MAX_DOCUMENT_BYTES = void 0;
4
+ exports.invalid = invalid;
5
+ exports.entries = entries;
6
+ exports.validateJSON = validateJSON;
7
+ exports.jsonText = jsonText;
8
+ exports.scalar = scalar;
9
+ exports.MAX_DOCUMENT_BYTES = 1024 * 1024;
10
+ class DocumentError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = 'DocumentError';
16
+ }
17
+ }
18
+ exports.DocumentError = DocumentError;
19
+ function invalid(message) {
20
+ throw new DocumentError('invalid_input', message);
21
+ }
22
+ // Read descriptors rather than properties: validation must not execute getters/toJSON.
23
+ function entries(value, label = 'object') {
24
+ if (value === null || typeof value !== 'object' || Array.isArray(value) ||
25
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
26
+ invalid(`${label} must be a plain object`);
27
+ return Reflect.ownKeys(value).map(key => {
28
+ const desc = Object.getOwnPropertyDescriptor(value, key);
29
+ if (typeof key !== 'string' || !desc.enumerable || !('value' in desc))
30
+ invalid(`${label} must contain enumerable data properties only`);
31
+ return [key, desc.value];
32
+ });
33
+ }
34
+ function validateJSON(value, ancestors = new Set(), depth = 0) {
35
+ if (depth > 32)
36
+ invalid('JSON nesting exceeds 32 levels');
37
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
38
+ return;
39
+ if (typeof value === 'number') {
40
+ if (!Number.isFinite(value))
41
+ invalid('JSON numbers must be finite');
42
+ return;
43
+ }
44
+ if (typeof value !== 'object')
45
+ invalid('Value is not JSON');
46
+ if (ancestors.has(value))
47
+ invalid('JSON must not contain cycles');
48
+ ancestors.add(value);
49
+ if (Array.isArray(value)) {
50
+ if (Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1)
51
+ invalid('JSON arrays must not contain holes or extra properties');
52
+ for (let i = 0; i < value.length; i++) {
53
+ const desc = Object.getOwnPropertyDescriptor(value, String(i));
54
+ if (!desc || !('value' in desc) || !desc.enumerable)
55
+ invalid('JSON arrays must contain data values');
56
+ validateJSON(desc.value, ancestors, depth + 1);
57
+ }
58
+ }
59
+ else {
60
+ for (const [, child] of entries(value, 'JSON object'))
61
+ validateJSON(child, ancestors, depth + 1);
62
+ }
63
+ ancestors.delete(value);
64
+ }
65
+ function jsonText(value) {
66
+ validateJSON(value);
67
+ const text = JSON.stringify(value);
68
+ if (new TextEncoder().encode(text).byteLength > exports.MAX_DOCUMENT_BYTES)
69
+ invalid('Document exceeds 1 MiB');
70
+ return text;
71
+ }
72
+ function scalar(value) {
73
+ if (value !== null && typeof value !== 'string' && typeof value !== 'boolean' && typeof value !== 'number')
74
+ invalid('Query operands must be JSON scalars');
75
+ if (typeof value === 'number' && !Number.isFinite(value))
76
+ invalid('Query numbers must be finite');
77
+ if (typeof value === 'string' && value.length > exports.MAX_DOCUMENT_BYTES)
78
+ invalid('Query string is too large');
79
+ }
@@ -0,0 +1,7 @@
1
+ import { type OwnedDocumentStore } from '../documents.js';
2
+ export interface LibSQLDocumentConfig {
3
+ url: string;
4
+ authToken?: string;
5
+ }
6
+ /** Install @libsql/client separately. Remote durability is a server setting. */
7
+ export declare function openLibSQLDocumentStore(config: LibSQLDocumentConfig): Promise<OwnedDocumentStore>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.openLibSQLDocumentStore = openLibSQLDocumentStore;
37
+ const documents_js_1 = require("../documents.js");
38
+ /** Install @libsql/client separately. Remote durability is a server setting. */
39
+ async function openLibSQLDocumentStore(config) {
40
+ const { createClient } = await Promise.resolve().then(() => __importStar(require('@libsql/client')));
41
+ const client = createClient(config);
42
+ try {
43
+ if (config.url.startsWith('file:')) {
44
+ await client.execute('PRAGMA journal_mode=WAL');
45
+ await client.execute('PRAGMA synchronous=FULL');
46
+ await client.execute('PRAGMA busy_timeout=5000');
47
+ }
48
+ const store = (0, documents_js_1.createDocumentStore)(client);
49
+ await store.ready();
50
+ return { ...store, close: () => client.close() };
51
+ }
52
+ catch (error) {
53
+ client.close();
54
+ throw error;
55
+ }
56
+ }
@@ -0,0 +1,17 @@
1
+ export type SQLValue = string | number | null;
2
+ export type Filter = {
3
+ [field: string]: unknown;
4
+ };
5
+ export type Sort = {
6
+ [field: string]: 1 | -1;
7
+ };
8
+ export type Fragment = {
9
+ sql: string;
10
+ args: SQLValue[];
11
+ };
12
+ export declare function path(field: string): string;
13
+ export declare function valueSQL(field: string): string;
14
+ export declare function typeSQL(field: string): string;
15
+ export declare function compileFilter(filter?: Filter): Fragment;
16
+ export declare function sortFields(sort: Sort): [string, 1 | -1][];
17
+ export declare function compileSort(sort: Sort): string;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.path = path;
4
+ exports.valueSQL = valueSQL;
5
+ exports.typeSQL = typeSQL;
6
+ exports.compileFilter = compileFilter;
7
+ exports.sortFields = sortFields;
8
+ exports.compileSort = compileSort;
9
+ const json_js_1 = require("./json.js");
10
+ function path(field) {
11
+ if (field.length > 256 || field.split('.').length > 16 || !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(field))
12
+ (0, json_js_1.invalid)('Invalid object field path');
13
+ return '$.' + field.split('.').map(p => `"${p}"`).join('.');
14
+ }
15
+ // Paths are validated identifiers, never caller-supplied SQL. Keeping their literal
16
+ // representation identical lets SQLite use expression indexes for these predicates.
17
+ function valueSQL(field) {
18
+ return field === '_id' ? 'id' : `json_extract(data, '${path(field)}')`;
19
+ }
20
+ function typeSQL(field) {
21
+ return field === '_id' ? "'text'" : `json_type(data, '${path(field)}')`;
22
+ }
23
+ function comparison(field, operator, value) {
24
+ (0, json_js_1.scalar)(value);
25
+ const type = typeSQL(field), expr = valueSQL(field);
26
+ if (operator !== '=' && (value === null || typeof value === 'boolean'))
27
+ (0, json_js_1.invalid)('Range comparisons require a number or string');
28
+ if (value === null)
29
+ return { sql: `${type} = 'null'`, args: [] };
30
+ if (typeof value === 'boolean')
31
+ return { sql: `${type} = '${value ? 'true' : 'false'}'`, args: [] };
32
+ const guard = typeof value === 'number' ? `${type} IN ('integer', 'real')` : `${type} = 'text'`;
33
+ return { sql: `(${guard} AND ${expr} ${operator} ?)`, args: [value] };
34
+ }
35
+ function compileFilter(filter = {}) {
36
+ let conditions = 0;
37
+ const walk = (input, depth) => {
38
+ if (depth > 10)
39
+ (0, json_js_1.invalid)('Filter nesting exceeds 10 levels');
40
+ const parts = [], args = [];
41
+ const append = (f) => { parts.push(`(${f.sql})`); args.push(...f.args); };
42
+ for (const [field, condition] of (0, json_js_1.entries)(input, 'filter')) {
43
+ if (++conditions > 64)
44
+ (0, json_js_1.invalid)('Filter exceeds 64 conditions');
45
+ if (field === '$and' || field === '$or') {
46
+ if (!Array.isArray(condition) || !condition.length || condition.length > 64)
47
+ (0, json_js_1.invalid)('Logical filters require 1 to 64 child filters');
48
+ const children = condition.map(child => walk(child, depth + 1));
49
+ append({ sql: children.map(c => `(${c.sql})`).join(field === '$and' ? ' AND ' : ' OR '), args: children.flatMap(c => c.args) });
50
+ continue;
51
+ }
52
+ path(field);
53
+ if (condition === null || typeof condition !== 'object') {
54
+ append(comparison(field, '=', condition));
55
+ continue;
56
+ }
57
+ const operators = (0, json_js_1.entries)(condition, 'field operators');
58
+ if (!operators.length)
59
+ (0, json_js_1.invalid)('Field operators must not be empty');
60
+ for (const [operator, operand] of operators) {
61
+ if (++conditions > 64)
62
+ (0, json_js_1.invalid)('Filter exceeds 64 conditions');
63
+ if (operator === '$exists') {
64
+ if (typeof operand !== 'boolean')
65
+ (0, json_js_1.invalid)('$exists requires a boolean');
66
+ append({ sql: `${typeSQL(field)} IS ${operand ? 'NOT ' : ''}NULL`, args: [] });
67
+ }
68
+ else if (operator === '$in') {
69
+ if (!Array.isArray(operand) || operand.length > 100)
70
+ (0, json_js_1.invalid)('$in requires at most 100 scalar values');
71
+ const choices = operand.map(v => comparison(field, '=', v));
72
+ append({ sql: choices.length ? choices.map(c => `(${c.sql})`).join(' OR ') : '0', args: choices.flatMap(c => c.args) });
73
+ }
74
+ else if (operator === '$ne') {
75
+ const equal = comparison(field, '=', operand);
76
+ append({ sql: `NOT coalesce((${equal.sql}), 0)`, args: equal.args });
77
+ }
78
+ else {
79
+ const sqlOperator = { $eq: '=', $gt: '>', $gte: '>=', $lt: '<', $lte: '<=' }[operator];
80
+ if (!sqlOperator)
81
+ (0, json_js_1.invalid)('Unsupported query operator');
82
+ append(comparison(field, sqlOperator, operand));
83
+ }
84
+ }
85
+ }
86
+ return { sql: parts.length ? parts.join(' AND ') : '1', args };
87
+ };
88
+ return walk(filter, 0);
89
+ }
90
+ function sortFields(sort) {
91
+ const fields = (0, json_js_1.entries)(sort, 'sort');
92
+ if (!fields.length || fields.length > 4)
93
+ (0, json_js_1.invalid)('Sort/index requires 1 to 4 fields');
94
+ return fields.map(([field, order]) => {
95
+ path(field);
96
+ if (order !== 1 && order !== -1)
97
+ (0, json_js_1.invalid)('Sort direction must be 1 or -1');
98
+ return [field, order];
99
+ });
100
+ }
101
+ function compileSort(sort) {
102
+ const fields = sortFields(sort);
103
+ if (!fields.some(([field]) => field === '_id'))
104
+ fields.push(['_id', 1]);
105
+ return fields.map(([field, order]) => `${valueSQL(field)} ${order === 1 ? 'ASC' : 'DESC'}`).join(', ');
106
+ }
@@ -0,0 +1,66 @@
1
+ import { DocumentError, MAX_DOCUMENT_BYTES, type JSONValue } from './documents/json.js';
2
+ import { type Filter, type Sort, type SQLValue, type Fragment } from './documents/query.js';
3
+ export { DocumentError, MAX_DOCUMENT_BYTES };
4
+ export type { JSONValue, Filter, Sort, SQLValue };
5
+ export type Document<T extends object = Record<string, JSONValue>> = T & {
6
+ _id: string;
7
+ };
8
+ export type Update = {
9
+ $set?: Record<string, JSONValue>;
10
+ $unset?: Record<string, true | 1>;
11
+ $inc?: Record<string, number>;
12
+ };
13
+ export interface DocumentClient {
14
+ execute(statement: {
15
+ sql: string;
16
+ args: SQLValue[];
17
+ }): PromiseLike<{
18
+ rows: readonly Record<string, unknown>[];
19
+ }>;
20
+ }
21
+ export interface DocumentStore {
22
+ ready(): Promise<void>;
23
+ collection<T extends object = Record<string, JSONValue>>(name: string): DocumentCollection<T>;
24
+ }
25
+ export interface OwnedDocumentStore extends DocumentStore {
26
+ close(): void | Promise<void>;
27
+ }
28
+ type Execute = (sql: string, args?: SQLValue[]) => Promise<readonly Record<string, unknown>[]>;
29
+ /** A collection is a query namespace, not an authorization boundary. Give each
30
+ * project/environment its own database credential or local file. No retry occurs. */
31
+ export declare function createDocumentStore(client: DocumentClient): DocumentStore;
32
+ export declare class DocumentCursor<T extends object> {
33
+ private readonly collection;
34
+ private readonly execute;
35
+ private readonly filter;
36
+ private readonly order;
37
+ private readonly take;
38
+ private readonly offset;
39
+ constructor(collection: string, execute: Execute, filter: Fragment, order?: string, take?: number, offset?: number);
40
+ sort(sort: Sort): DocumentCursor<T>;
41
+ limit(n: number): DocumentCursor<T>;
42
+ skip(n: number): DocumentCursor<T>;
43
+ toArray(): Promise<Document<T>[]>;
44
+ }
45
+ export declare class DocumentCollection<T extends object> {
46
+ private readonly name;
47
+ private readonly execute;
48
+ constructor(name: string, execute: Execute);
49
+ insertOne(document: Omit<T, '_id'> & {
50
+ _id?: string;
51
+ }): Promise<{
52
+ insertedId: string;
53
+ }>;
54
+ find(filter?: Filter): DocumentCursor<T>;
55
+ findOne(filter?: Filter): Promise<Document<T> | null>;
56
+ countDocuments(filter?: Filter): Promise<number>;
57
+ deleteOne(filter: Filter): Promise<{
58
+ deletedCount: number;
59
+ }>;
60
+ updateOne(filter: Filter, update: Update): Promise<{
61
+ matchedCount: number;
62
+ }>;
63
+ /** Indexes cover all collections, with collection as the leading key.
64
+ * Identical specs share one index. Queries still bind the collection. */
65
+ createIndex(spec: Sort): Promise<string>;
66
+ }