@cicctencent/json-orm
v0.1.10
Published
TypeORM-compatible JSON file storage engine — datasource facade, repository shim, query builder and in-memory SQL evaluator, with injectable storage root (framework-agnostic).
Readme
@cicctencent/json-orm
TypeORM 兼容的 JSON 文件存储引擎(框架无关、业务解耦的公共包):以 JSON 文件承载
data/<表名>/<主键>.json 的落盘存储,提供 TypeORM DataSource / Repository 子集、
QueryBuilder 与内存 SQL 求值器,作为无数据库场景下的透明持久化回退层。
本包为独立公共包,不依赖
@midwayjs/*、@cicctencent/*或任何应用内部模块; 承载其运行所需的目录解析、非实体表注册等应用决策,由接入方以配置注入。
安装
作为 pnpm workspace 包接入(推荐):
// app/work-space/package.json
{
"dependencies": {
"@cicctencent/json-orm": "workspace:*"
}
}pnpm install运行时依赖(peerDependencies,宿主需已安装):typeorm@^0.3.31、reflect-metadata@^0.2.2。
配置注入(使用前必做)
引擎不内置任何存储根目录或非实体表注册策略;首次访问前必须调用
configureJsonStorage() 注入:
import {
configureJsonStorage,
getJsonDataSource,
createJsonRepository,
sqlTableFor,
FsStorageBackend,
type JsonStorageBackend,
} from '@cicctencent/json-orm';
// 存储根目录(如 <data>/db)与非实体表注册(仅经原始 SQL 访问、无 @Entity 定义的表)
configureJsonStorage({
dbDir: '/path/to/data/db',
nonEntityTables: {
some_raw_table: { primaryColumn: 'id' },
},
});dbDir未注入而触发集合落盘时,引擎抛出明确错误并指引先调用configureJsonStorage({ dbDir }),不会静默回退到隐式目录。nonEntityTables可选;注入后isNonEntityTable/getNonEntityPrimaryColumn/getNonEntityUniqueColumns/listNonEntityTables按注入数据返回,原始 SQL 的幂等 upsert 按注入主键/唯一列判重。- 配置支持分次部分注入、后写覆盖;测试可调用
resetJsonStorageForTest()隔离。
存储后端(可注入、异步)
引擎全部持久化 I/O 经可注入的 JsonStorageBackend(异步接口)完成,不直接依赖 fs:
readFile / writeFileAtomic(内部 tmp+rename 原子写)/ readDir / deleteFile /
mkdir / stat(跨进程缓存失效检测)/ withLock(跨进程临界区互斥)/ nextId
(后端级原子自增)/ ensureSeqAtLeast(显式大主键抬升序列器)。
默认后端为 FsStorageBackend(fs/promises 实现,等价于改造前的本地 JSON 落盘)。
注入方式:
// 方式 A:仅注入 dbDir(自动构造 FsStorageBackend,向后兼容)
configureJsonStorage({ dbDir: '/path/to/data/db' });
// 方式 B:注入自定义后端(如对象存储 COS/S3),优先于 dbDir
const cosBackend: JsonStorageBackend = createCosBackend(/* ... */);
configureJsonStorage({ backend: cosBackend });所有数据访问方法(find / save / update / delete / upsert / query、
原生 SQL 的 SELECT/INSERT/UPDATE/DELETE 求值、集合读写、序列分配)全程异步,
数据路径无阻塞式同步 I/O。调用方对接 DataSource / Repository / EntityManager
公共 API 时无需改动(这些方法本就返回 Promise)。
多进程安全
- 序列器唯一:
nextId()的分配在后端withLock临界区内完成读-改-写,跨进程严格 递增且唯一;显式写入大于当前序列器的主键时经ensureSeqAtLeast抬升,避免后续撞车。 - 跨进程缓存失效:每个进程在内存保留整表快照以支撑 SQL 扫描性能;读取前经
backend.stat(表目录)检测变更(etag优先,两侧均无 etag 时以mtimeMs增大 为准),若他进程已变更则重载。节流由reloadTtlMs(configureJsonStorage({ reloadTtlMs }),默认0即每次访问校验)调控;>0时 在 TTL 窗口内可复用缓存(权衡:可能短暂读到他进程写入前的快照)。 - 写路径强制校验:
write/delete/nextId不受reloadTtlMs节流,写入前 必定先做一次失效校验,保证写入后记录的水位与内存快照一致,不掩蔽他进程写入。 - 单行写仍用 tmp+rename 文件级原子(临时文件名含 pid 与随机量,并发写同一行不产生
交错内容,也不抛
ENOENT),不引入整表写锁。 - 锁归属校验:
withLock以令牌标识持有者,释放前校验令牌;锁被判定为孤儿并由 他人取得后,原持有者不会误删他人锁(避免两进程同时进入临界区)。
公开 API 概览
| 类别 | 导出 |
| --- | --- |
| 配置 | configureJsonStorage / resetJsonStorageForTest / JsonStorageOptions / NonEntityTableInfo / JsonStorageBackend / JsonStorageFileStat / FsStorageBackend |
| 数据源 | JsonDataSource / JsonEntityManager / getJsonDataSource / resetJsonDataSourceForTest / sqlTableFor |
| 仓库 | JsonRepository / createJsonRepository / FindOptionsLoose |
| 查询/SQL | JsonQueryBuilder / executeRawQuery / SqlTable / GetSqlTable / toParams |
| 集合 | JsonCollection / getCollection / resetCollectionsForTest / JsonRow |
| 实体元数据 | resolveEntityMeta / resolveEntityMetaByTable / JsonEntityMeta / JsonEntityColumn 及非实体表四函数 |
| SQL 词法/表达式 | tokenizeSql / TokenCursor / matchesWhere / looseEqual / compareValues / likeMatch / parseExpr / evalExpr / truthy / hasAggregate / AGG_FUNCS / Expr / EvalContext / AliasInfo / jsonPathGet / jsonUnquote / toDate / mysqlDatetime |
完整类型与行为见本仓库 openspec/specs/json-orm-storage-engine/spec.md。
支持的 TypeORM 能力矩阵
下表列出本引擎对 TypeORM 公共 API 的覆盖情况。✓ 实现 = 行为与 TypeORM 一致;
~ 垫片 = 运行时可达但语义有差异(见下节);× 不支持 = 暂未实现,调用会抛错或静默无操作。
Repository(实体仓库)
| 能力 | 状态 | 说明 |
| --- | --- | --- |
| find / findBy / findOne / findOneBy / findAndCount / findAndCountBy | ✓ | 对象条件 + 排序/分页/select 子集 |
| findOneOrFail / findOneByOrFail | ✓ | 未命中抛 EntityNotFoundError |
| count / countBy | ✓ | 含对象条件 |
| save / insert | ✓ | 自增主键、UUID 主键、UPSERT、ODKU 语义 |
| update / delete / increment / decrement | ✓ | 空条件 fail-loud(禁止全表误伤) |
| deleteAll / updateAll | ✓ | 显式全表操作(带守卫) |
| upsert | ✓ | conflictPaths 默认主键,可指定唯一列 |
| softDelete / restore / softRemove / recover | ✓ | 依赖 @DeleteDateColumn |
| exists / exist / existsBy | ✓ | |
| sum / average / minimum / maximum | ✓ | 支持 where 过滤与列名 |
| preload / merge / hasId / getId / create | ✓ | |
| clear | ✓ | 清空整表 |
| query / sql / createQueryBuilder | ✓ / ~ | 原生 SQL + 查询构造器 |
| find 的 relations 选项 | × | 关系/嵌套实体未实现(见已知限制) |
EntityManager(JsonEntityManager)
全部读写方法(save / find / count / increment / upsert / softDelete / query …
共约 40 个)逐一对齐 Repository,并额外提供 updateAll / deleteAll。manager
字段、createEntityManager() 亦可获取。
DataSource(JsonDataSource)
| 能力 | 状态 |
| --- | --- |
| initialize / connect / destroy / close / setOptions | ✓ |
| getRepository / getTreeRepository / getCustomRepository / getMetadata / hasMetadata | ✓ |
| synchronize / dropDatabase / runMigrations / undoLastMigration / showMigrations | ~(空操作,见下节) |
| createQueryRunner | ~(hasTable 恒 true,schema 方法为 no-op) |
| transaction / createEntityManager / query / sql | ~ / ✓ |
类型说明:
JsonRepository/JsonDataSource/JsonQueryBuilder是 TypeORM 公共接口的兼容垫片, 运行时行为对齐,但未implements对应接口(Repository<Entity>/DataSource/SelectQueryBuilder)。消费方如需const ds: DataSource = getJsonDataSource()通过类型检查, 暂需as unknown as DataSource断言;彻底消除需实现真正的 TypeORMDriver(见已知限制)。
QueryBuilder(JsonQueryBuilder)
| 能力 | 状态 |
| --- | --- |
| select(含 * / 别名 / DISTINCT)、from / addFrom | ✓ |
| where(字符串 / 对象 / whereInIds / Brackets 嵌套) | ✓ |
| leftJoin / innerJoin / leftJoinAndSelect | ~(仅笛卡尔积占位,无外键嵌套填充) |
| groupBy / having / addGroupBy / addHaving | ✓(聚合 + HAVING) |
| orderBy(ASC/DESC、NULLS FIRST/LAST、对象式、空参清除)/ addOrderBy | ✓ |
| limit / offset(MySQL LIMIT offset, count)/ skip / take | ✓ |
| update().set().where().orderBy().limit() / delete().where().orderBy().limit() | ✓ |
| orUpdate(ODKU)/ into(entity, columns) | ✓ |
| 取值:getMany / getOne / getManyAndCount / getCount / getExists / getRawMany / getRawOne / getRawAndEntities / getQuery | ✓ |
| withDeleted(软删过滤开关) | ✓ |
| subQuery / whereExists | × |
FindOperator(对象条件运算符)
Equal · Not · In / Any · IsNull · LessThan / LessThanOrEqual · MoreThan /
MoreThanOrEqual · Between · Like / ILike · And · Or ·
ArrayContains / ArrayContainedBy / ArrayOverlap · JsonContains · Raw ✓
数组运算符作用于 JSON 数组列(PostgreSQL 风格);
Raw接收(dbColumn, aliases?) => string表达式,由引擎求值器解释。
原生 SQL 函数(内存求值器)
标量:COALESCE / NULLIF / IF / IFNULL / NOW / CURRENT_TIMESTAMP /
UNIX_TIMESTAMP / FROM_UNIXTIME / DATE_FORMAT / TIMESTAMPDIFF / DATEDIFF /
ABS / ROUND / FLOOR / CEIL / CONCAT / CONCAT_WS / LEAST / GREATEST /
SIGN / MOD / POWER / SQRT / LPAD / RPAD / LOCATE / INSTR / REVERSE /
REPLACE / STR_TO_DATE / YEAR / MONTH / DAY / HOUR / MINUTE / SECOND /
DATE / CURDATE / CURTIME / DATE_ADD / DATE_SUB / TIMESTAMPADD ·
JSON:JSON_EXTRACT / JSON_UNQUOTE / JSON_CONTAINS / JSON_OVERLAPS / JSON_SET /
JSON_OBJECT / JSON_ARRAY · 聚合:COUNT / SUM / AVG / MIN / MAX / GROUP_CONCAT。
完整函数注册表见 json-sql-expr.ts 的 FUNCS / AGG_FUNCS。
写操作返回:
UPDATE/DELETE返回{ affected, raw };原生 SQL 的UPDATE/DELETE返回 OkPacket 形态,其中affectedRows为匹配行数、changedRows为实际变更行数 (与 MySQLinfo的 Rows matched / Changed 对应)。据此判断「是否真的改到了值」时 应使用changedRows。
与 MySQL 的语义差异 & 已知限制
本引擎是「单进程 JSON 文件 + 内存 SQL 求值」对 TypeORM 的兼容回退层,而非真实数据库。 下列差异需接入方知悉,避免在语义假设上踩坑。
写入语义
- 事务是 no-op:
DataSource.transaction()/EntityManager.transaction()/QueryRunner的事务方法直接透传回调执行,不提供跨表/跨语句原子性与回滚。 JSON 模式无法保证「失败整体回滚」;若业务强依赖事务一致性,应改用真实数据库。 - DDL 是空操作:
synchronize()/dropDatabase()/runMigrations()等为 no-op;createQueryRunner()仅用于 schema 探测,hasTable恒返回true。因此if (!(await hasTable(t))) await createTable(t)这类条件建表代码不会执行建表 ——JSON 模式本就无需建表,但调用方需知悉。 - 多进程一致性:序列器(
nextId)与写锁经后端withLock保证跨进程唯一与互斥, 但transaction之外的「读-改-写」临界区仍非强一致;高并发写入建议依赖主键/唯一约束冲突检测。
关系与实体能力(× 暂不支持)
| 能力 | 说明 |
| --- | --- |
| 关系(@ManyToOne / @OneToMany / @OneToOne / @ManyToMany / @JoinColumn / @JoinTable) | 装饰器未解析;find({ relations })、leftJoinAndSelect 仅做笛卡尔积占位,不按外键填充嵌套实体 |
| EntitySchema | 元数据仅从装饰器(getMetadataArgsStorage)读取,以 EntitySchema 定义的实体查不到 |
| 订阅器 / 生命周期钩子(@BeforeInsert 等 11 个 hook、EntitySubscriberInterface) | 写入不经过任何钩子 |
| @Tree / TreeRepository | 未实现 |
| BaseEntity 静态方法 | 未提供(基于 Repository 的便捷封装缺失) |
SQL 方言缺口
- 子查询 / CTE:
FROM (SELECT …)、IN (SELECT …)、EXISTS (SELECT …)、WITH暂未实现。 - 集合运算:
UNION/UNION ALL未实现。 - 窗口函数:
OVER (PARTITION BY …)未实现(聚合仅支持GROUP BY形态)。 - JOIN 类型:
RIGHT/FULL JOIN未实现;LEFT/INNER仅笛卡尔积占位(见上)。 - 标识符:原生 SQL 中双引号标识符(
"col")在部分路径未支持,建议统一使用反引号或无引号。 - 上节「原生 SQL 函数」列出的函数为已支持集合,超出部分会抛「未知函数」错误。
类型兼容性(发布须知)
JsonRepository / JsonDataSource / JsonQueryBuilder 是 TypeORM 公共接口的兼容垫片,
运行时行为对齐,但未 implements 对应接口。因此:
const ds: DataSource = getJsonDataSource(); // ❌ 类型检查不过
const ds = getJsonDataSource() as unknown as DataSource; // ✓ 暂需断言要彻底消除该断层(让消费方获得完整 TypeORM 类型体验),需实现真正的 TypeORM Driver
(JsonDriver + JsonQueryRunner + RdbmsSchemaBuilder no-op),使 Repository / EntityManager /
QueryBuilder / 关系 / 钩子 / 迁移由 TypeORM 自身提供——这是本引擎实现「完整支持 TypeORM」的前置项。
示例
examples/ 目录提供了可直接运行的完整示例(需先 pnpm build 产出 dist,再用
npx tsx examples/<file>.ts 运行),覆盖最常见接入场景:
| 示例 | 场景 |
| --- | --- |
| 01-basic-entity.ts | 定义实体 + Repository 的 save/find/findOneBy/findAndCount/update/delete |
| 02-query-builder.ts | QueryBuilder:字符串/对象条件、LEFT JOIN、GROUP BY 聚合、分页计数 |
| 03-raw-sql.ts | query:SELECT/INSERT/UPDATE/DELETE、ODKU upsert、TRUNCATE;位置/命名参数 |
| 04-non-entity-table.ts | 非实体表(仅原始 SQL 访问)+ nonEntityTables 注册 |
| 05-custom-backend.ts | 实现 JsonStorageBackend 注入自定义(内存)后端 |
| 06-find-operators.ts | Repository where 的 FindOperator:In/Like/Not/IsNull/MoreThan/Between |
| 07-sql-functions.ts | 原生 SQL 函数 / 聚合(GROUP BY+HAVING)/ And·Or 组合 / JSON 包含查询 |
关键约定(两套命名空间、合并写语义、多进程安全等)见 examples/README.md。
构建与测试
pnpm build # 产出 dist/esm(NodeNext)与 dist/cjs,并经 fixup-cjs 修复 .d.ts
pnpm typecheck # 仅类型检查(ESM 配置)
pnpm test # vitest 运行 src/__tests__ 下引擎测试(含 157 用例)接入方注意:本包以
workspace:*形式被应用消费,应用生产构建/本地开发前 须先产出本包dist(即先执行pnpm --filter @cicctencent/json-orm build)。 应用侧bin/dev.sh的共享包构建段已注释,沿用「本地预构建 dist」的既有约定。
许可证
MIT
