/** * 文档集合(技术方案 19):比 kv 更适合"列表 + 条件查询 + 排序分页"的业务数据。 * 平台托管(platform)走 Data API `/data/v1/db/*`;edgeone 用 Pages Blob 每文档一个对象;sqlite 落本地文件(技术方案 33);memory 为本地降级。 * 只能在服务端使用。 */ /** 文档:应用自定义字段 + 平台补充的 _id/_createdAt/_updatedAt(毫秒时间戳) */ export type Doc = T & { _id: string; _createdAt: number; _updatedAt: number; }; export type FilterOp = { $gt?: V; $gte?: V; $lt?: V; $lte?: V; $ne?: V; $in?: V[]; $nin?: V[]; /** 字符串包含(不区分大小写);数组字段则表示"包含某元素" */ $contains?: V extends Array ? E : V; $exists?: boolean; }; /** 过滤:{字段: 值} 等值;{字段: {$gt: 1}} 操作符;$and/$or/$not 组合;字段支持 a.b 点路径 */ export type Filter> = ({ [K in keyof T]?: T[K] | FilterOp; } & { [key: string]: unknown; }) | { $and?: Filter[]; $or?: Filter[]; $not?: Filter; }; export type Sort = Record; export interface FindOptions> { filter?: Filter; sort?: Sort; skip?: number; /** 单页上限 200,默认 50 */ limit?: number; } export interface FindResult { docs: Doc[]; /** 满足 filter 的总数 */ total: number; /** 还有下一页时为下一次的 skip,否则 null */ nextSkip: number | null; } export interface UpdateInput { set?: Partial & Record; unset?: string[]; /** 数值字段增减:{ views: 1 } */ inc?: Record; /** 不存在时创建(默认 false) */ upsert?: boolean; } /** 聚合的分组键:字段原值(字符串/数字/布尔)、时间桶名,或"没有分组/字段缺失"的 null */ export type AggregateKey = string | number | boolean | null; /** 聚合指标(都返回数字):计数、求和、均值、最小、最大、去重计数 */ export type AggregateMetric = { $count: true; } | { $sum: string; } | { $avg: string; } | { $min: string; } | { $max: string; } | { $countDistinct: string; }; /** 时间分桶:把毫秒时间戳(或可解析的日期字符串)字段按小时/天/周/月归组 */ export interface AggregateGroup { field: string; unit?: 'hour' | 'day' | 'week' | 'month'; /** 时区偏移分钟,**默认 480(北京时间)**;按 UTC 分天传 0 */ tzOffsetMinutes?: number; } export interface AggregateOptions, M extends Record = Record> { /** 先筛(与 find 同一套过滤语法) */ filter?: Filter; /** 分组字段(支持 a.b 点路径)或时间分桶;不传 = 整个集合一行 */ groupBy?: string | AggregateGroup; metrics: M; /** 按指标名或 'key' 排序;默认 { key: 1 } */ sort?: Record; /** 返回的分组数,默认 100、上限 1000 */ limit?: number; } export type AggregateRow = Record> = { key: AggregateKey; } & Record; export interface Collection> { insert(doc: Partial & Record): Promise>; insertMany(docs: Array & Record>): Promise; get(id: string): Promise | null>; find(options?: FindOptions): Promise>; /** 取第一条匹配(等价 find({filter, limit:1}).docs[0]) */ findOne(filter?: Filter, options?: Omit, 'filter' | 'limit'>): Promise | null>; count(filter?: Filter): Promise; /** * 聚合统计(技术方案 35):分组在**服务端**完成,只拿回几行——看板/报表别再 `find` 全量回来自己 reduce。 * ```ts * const byDay = await orders.aggregate({ * filter: { status: 'paid' }, * groupBy: { field: '_createdAt', unit: 'day' }, // 默认按北京时间分天 * metrics: { n: { $count: true }, total: { $sum: 'amount' } }, * }) // → [{ key: '2026-01-01', n: 12, total: 3400 }, …] * ``` */ aggregate>(options: AggregateOptions): Promise[]>; update(id: string, input: UpdateInput): Promise | null>; /** * 条件更新(乐观锁,技术方案 34 §2):当前文档满足 ifMatch 才更新,**不满足返回 null**(不是抛错)。 * 把"先判断再写"的判断交给服务端,避免并发下超卖/重复处理: * ```ts * const ok = await seats.updateIf(id, { inc: { left: -1 } }, { left: { $gt: 0 }, status: 'open' }) * if (!ok) return { error: '名额已满' } * ``` * 并发写太密集(重试 5 次仍失败)时抛 AppSdkError('CONFLICT')。 */ updateIf(id: string, input: UpdateInput, ifMatch: Filter): Promise | null>; /** * 按 filter 找,找不到才插入 doc(技术方案 34 §2)。替代"`findOne` 没有就 `insert`"——后者并发下会插出两条。 * 平台 / edgeone 驱动会先取一把同名锁再查再写。 */ getOrCreate(filter: Filter, doc: Partial & Record): Promise<{ doc: Doc; created: boolean; }>; replace(id: string, doc: Partial & Record): Promise>; delete(id: string): Promise; deleteMany(filter?: Filter): Promise; /** 清空集合 */ drop(): Promise; } export interface DbClient { collection>(name: string): Collection; collections(): Promise>; } export declare function matchesFilter(doc: unknown, filter: unknown): boolean; export declare function applySort(docs: T[], sort?: Sort): T[]; /** 时间有序 id(与服务端同格式) */ export declare function newDocId(): string; export declare function withMeta(doc: Record, id: string, createdAt: number, updatedAt: number): Doc; /** 在一组内存文档上执行 find(memory / edgeone 驱动共用) */ export declare function queryDocs(all: Doc[], options?: FindOptions): FindResult; /** 在一组内存文档上执行 aggregate(memory / sqlite / edgeone 驱动共用,语义与服务端一致) */ export declare function aggregateDocs>(all: Doc[], options: AggregateOptions): AggregateRow[]; /** 对已有文档应用 set/unset/inc */ export declare function applyUpdate(current: Doc, input: UpdateInput): Doc; /** * getOrCreate 的公共实现:先拿一把 `db:{集合}:{filter 哈希}` 的 kv 锁,让"同一条件"的并发请求串行,锁内查不到才插入。 * 单进程驱动(memory / sqlite)同样要锁——await 之间照样会交错,两个请求都查不到就会插出两条。 */ export declare function getOrCreateWith(c: Collection, name: string, filter: Filter, doc: Partial & Record): Promise<{ doc: Doc; created: boolean; }>; export declare function getDb(): DbClient; /** 便捷单例:`import { db } from '@chatu-ai/app-sdk'` */ export declare const db: DbClient;