npm install @k3000/store结构化存储工具
``
`
*1、低版本升级到1.3.0请执行upgrade和submit更新存储文件。
*2、1.5.0版本请注意page的用法。
*3、1.6.1修改String中文存储的BUG建议更新,更新请注意第1、2条和page的用法。
*4、1.7.0修复中文路径问题。
*5、1.8.0添加存储目录的备份与迁移的描述(无功能调整)。
*6、1.9.0让Trae builder优化代码添加了注释。
*7、反馈与讨论:19815488@qq.com
`
初始化和更新升级
`
import upgrade from 'dmb3'
const {appendSet, submit} = upgrade('test', {version: 1, password: '', newPwd: ''})
appendSet(/ /)
submit()
`
文件结构为:假设存储名为:test
`
test ┳ 1.2 ┳ buffer ┳
┃ ┃ ┇ // buffer格式或者文本格式的存储目录
┃ ┃ ┗
┃ ┣ data // 数据存储文件
┃ ┣ index // 数据索引文件
┃ ┗ index.mjs // 数据访问文件,可以通过该文件访问指定版本
┣ ...
┣ 3 ┳ ...
┃ ┗ index.mjs // 数据访问文件,可以通过该文件访问指定版本
┣ 4 ...
┗ index.mjs // 默认入口文件,指向最新版本
Id
数据类型
类型:Id, Uint, Int, Bool, Time, Float, String, Buffer, Text
| 类型 | 说明 | 默认长度(字节) | 默认值 | 备注 |
| :--- | :--- | :--- | :--- | :--- |
| | 自增主键 | 4 | 自动自增 | 默认为索引 |
Uint
| | 无符号整数 | 4 | 0 | 可选 1/2/3/4 字节 |
Int
| | 有符号整数 | 4 | 0 | 可选 1/2/3/4 字节 |
Float
| | 双精度浮点 | 8 | 0 | - |
Bool
| | 布尔值 | 1 | false | 实际存储为 Uint(1) |
Time
| | 时间 | 6 | 0 | 可设 Time.now / Time.update |
String
| | 定长字符串 | 12 | '' | 超出长度会被截断 |
Bigint
| | 64位整数 | 8 | 0n | - |
Buffer
| | 二进制数据 | 1 | - | 存储在外置文件,主文件存标志位 |
Text
| | 长文本 | 1 | - | 存储在外置文件,主文件存标志位 |
`
`
import upgrade, {
Bigint,
bigintSerialize,
BigUint,
Buffer,
Float,
Id,
Int,
String,
Text,
Time,
Uint
} from 'dmb3'
const {appendSet, submit} = upgrade('test')
appendSet('user', {
uid: String,
pwd: String('存MD5指').length(32),
loginTime: Time().value(Time.update)
}, '用户表')
submit().then(() => console.log('完成'))
`
缺省写法与完整写法
`
// Time.value(Time.update) 和 Time().value(Time.update) 两种写法一样。
/* 例如:
appendSet('test', {
id: Id,
})
等价
appendSet('test', {
id: Id(),
})
*/
// Id 用法
Id('备注') // 会补全默认值
Id()
.index(false) // 是否索引,此类型默认为 true
.value('2') // 默认值,此类型缺省为 1
.step(2) // 缺省自增值,此类型缺省为 1
.remark('备注') // 可缺省
// Uint 用法
Uint(4)
.index(false) // 是否索引,此类型默认为 false
.value(2) // 默认值,此类型没有缺省值
.step(2) // 缺省自增值,此类型没有缺省值
.remark('备注') // 可缺省
// Int 用法
Int()
.index(false) // 是否索引,此类型默认为 false
.value(2) // 默认值,此类型没有缺省值
.step(2) // 缺省自增值,此类型没有缺省值
.length(4) // 长度,此类型默认为 4
.remark('备注') // 可缺省
// Bool 用法
Bool()
.index(false) // 是否索引,此类型默认为 false
.value(false) // 默认值,此类型缺省为 false
.remark('备注') // 可缺省
// Time 用法
Time()
.index(false) // 是否索引,此类型默认为 false
.value(Time.update) // 默认值,此类型没有缺省值,可取值:Time.update、Time.now
.remark('备注') // 可缺省
// Float 用法
Float()
.index(false) // 是否索引,此类型默认为 false
.value(0.1) // 默认值,此类型没有缺省值
.step(0.1) // 缺省自增值,此类型没有缺省值
.remark('备注') // 可缺省
// String 用法
String()
.index(false) // 是否索引,此类型默认为 false
.value('123456') // 默认值,此类型没有缺省值
.length(8) // 长度,此类型默认为 12
.remark('备注') // 可缺省
// Buffer 用法
Buffer().remark('备注') // 可缺省
// Text 用法
Text().remark('备注') // 可缺省
`
存储结构更新写法
`
// 新增集合
appendSet('user', {
uid: String,
pwd: String('存MD5指').length(32)
}, '用户表')
// 更新集合备注
updateSetRemark('log', '日志表')
// 更新列
updateCol('user', {
loginTime: Time().value(Time.update).remark('登录时间')
})
// 更新索引
updateIndex('test', {
uid: true,
pwd: false,
})
// 更名操作
renameSet('test', 'test2')
renameCol('user', {
uid: 'usn',
test: 'test2',
})
// 删除操作
deleteSet('test2')
deleteCol('user', 'test2')
const {submit} = upgrade('test', {
version: 7,
password: '123123',
newPwd: '111111' // 新密码
})
submit()
`
打开使用已有存储文件
使用
`
import storage from './test/index.mjs' // 使用最新版本
import storage from './test/7/index.mjs'// 使用指定版本
import storage, {remark, getStruct} from './test/7/index.mjs' // 获取存储结构和集合备注
`
查:
`
storage.admin.find(admin => admin.usn.includes('test')).role
if (storage.admin.indexByUid('admin')[0].pwd === '123456');
// 带索引的时间
storage.admin.indexByLoginTime(-Infinity, '2022/01/01')
// 简单分页查询
storage.admin.page(admin => predicate(admin), index, size)
// 结果缓存的分页查询,缓存10秒
const params = {key: 'name', index: 1, size: 10}
storage.admin.page(admin => predicate(admin), 'index', 'size', params)
// 联合查询 eachFlat类似LEFT JOIN,filterFlat类似INNER JOIN
storage.user.eachFlat(storage.userRole, (a, b) => a.userId === b.userId)
.filterFlat(storage.role, 'roleId')
`
增:
`
storage.admin.push(
{
usn: 'admin',
pwd: '123456'
},
{
usn: 'test',
pwd: '123456'
}
)
`
改:
`
storage.admin.find(admin => admin.usn === 'admin').role = 'root'
`
删:
`
storage.admin.remove(...storage.admin.filter(admin => admin.usn.startsWith('test')))
test.mjs
更多使用方法参考
`
保存
没3秒会自动保存,也可以手工操作
`
import {close} from './test/index.mjs'
close() // 关闭并保存
upgrade
备份与迁移
当使用升级的时候会按照版本号备份历史版本。
Storage
也能直接将整个存储目录保存,复制妥存。
同样道理,迁移就是将存储目录拷贝到相应项目目录,使用相应版本的库运行即可。
---
概述
- 本工具提供一种轻量的、可版本化的本地结构化存储方案,面向 Node.js 项目;以“集合/列”定义模式,生成访问层代码,数据落盘到二进制文件并配合加密索引文件。
- 适合嵌入式、桌面端或需要无需外部数据库的应用,支持索引查询、范围查询、分页与联合查询,且提供平滑的结构升级与密码迁移能力。
核心概念
- :底层存储运行时,负责数据读写、缓存、加解密、结构与记录的持久化以及并发打开检查。
Entity
- :单条记录对象,字段访问器封装了二进制读写与索引维护;支持 Buffer/Text 外置文件字段。
Entities
- :集合的代理,提供 push/pop/unshift/splice/remove/page 等数组语义与 indexBy 查询。
Id/Uint/Int/BigUint/Bigint/Time/Float/String/Buffer/Text
- 集合/列定义:通过类型 DSL()声明列类型、索引、默认值、长度、步进。
index
目录结构详解
- :二进制索引文件,包含两段加密 JSON:结构(struct)与记录(record)。头部额外包含一个 4 字节整数作为“打开计数”,用于并发打开检测。
data
- :二进制数据文件,按集合总长度定长布局存储;每条记录按列 position/length 定位读取。
buffer/
- :外置文件目录,存放 Buffer/Text 字段实际内容;字段本体存储一个字节作为标志位,值为 1 表示存在外置内容。
- :访问层代码,按结构自动生成集合实体类与集合代理;顶层 index.mjs 始终重导出最新版本。
AES-192-CBC
加密与密码
- 结构与记录的加解密使用 ,密钥由 md5(password)+salt 派生,IV 来自 MD5 的中间字节;密码来源于 Storage(import.meta.url) 的查询参数或 upgrade 传参。
upgrade('name', { newPwd, password })
- 密码迁移:通过 在提交时重新加密结构与记录;若密码不匹配或存储文件损坏会抛出错误。
upgrade(name, options)
版本与升级流程
- 会:
v
- 计算最新版本 并将其目录完整复制到新版本目录 version;
Storage
- 打开旧/新 (支持密码切换),准备生成路径(index.mjs/type.ts);
submit()
- 返回结构变更 API; 会写入加密结构与记录、关闭句柄、生成访问代码,并在集合结构有更新时把旧版数据迁移到新版集合。
updateIndex('set', { col: true })
索引与查询
- 为列开启索引可使用 或在 DSL 中 String().index(true) 等;索引是按字段二进制值排序的记录位置列表。
set2
- 字段写入使用两种模式:索引列通过 维护有序索引(二分定位插入),普通列使用 set 直接写入。
indexBy
- 查询方法:
- 值查询: 返回与二进制值相等的所有记录;
indexBy
- 范围查询: 或针对 Time 用 indexByTime({ after, before });
filter/find/some/findIndex
- 普通数组查询: 等对集合进行内存遍历。
page(predicate, index, size, params)
分页与缓存
- 支持两种模式:
page(fn, 1, 10)
- 简单分页: 返回第 1 页、大小 10 的结果(按记录顺序);
page(fn, 'index', 'size', params)
- 缓存分页: 将 params 作为缓存键,命中时不重复计算;内部每轮(3 秒)标记过期,下一轮未访问的缓存将被移除。
Buffer
大字段(Buffer/Text)
- /Text 字段内容存储在 buffer/ 目录,以记录起始位置+列偏移作为文件名;字段本体只存放 1 字节标记位。
readFileSync
- 删除或替换记录时,会自动清理关联的外置文件;读取时通过 返回二进制或文本内容。
Storage(import.meta.url)
性能与注意事项
- 自动保存与清理:每 3 秒写回需要保存的值、刷新记录并清理过期缓存;
- 缓存上限:Value/Entity/Cache 的 Map 最多各 99999 条;超过上限的写入会直接落盘而不进入缓存;
- 并发打开:打开计数变化会抛错提示“已在其他地方打开”,防止同目录并发写入;
- 路径解析: 在 file URL 情况下使用 process.cwd() 作为目录基础,请在正确工作目录下运行或传显式路径;
Time
- 时间编码: 字段以定长十六进制 Buffer 表示,使用 d2b/b2d 辅助范围比较;最大时间常量为 MaxTime。
`
API 参考(结构变更)
`
const gen = upgrade(name, { version, password, newPwd, store })
gen.appendSet(name, set, remark) // 新增集合
gen.updateCol(name, set) // 更新集合列
gen.renameSet(name, newName) // 集合更名
gen.renameCol(name, { old: new }) // 列更名
gen.deleteSet(name) // 删除集合
gen.deleteCol(name, colName) // 删除列
gen.updateSetRemark(name, remark) // 更新集合备注
gen.updateIndex(name, { col: true }) // 索引开关
gen.submit() // 提交生成与数据迁移
`
最小示例
`
import upgrade, { Id, String, Time, Uint } from '@k3000/store/generator.mjs'
const { appendSet, submit } = upgrade('myStore')
appendSet('user', {
id: Id,
uid: String('账号').index(true),
pwd: String('密码').length(64),
createdAt: Time('创建时间').index(true).value(Time.now),
status: Uint(1).remark('状态位')
}, '用户表')
await submit()
// 使用
import storage from './myStore/index.mjs'
storage.user.push({ uid: 'alice', pwd: '*', status: 1 })
const recent = storage.user.indexByCreatedAt({ after: new Date('2024-01-01') })
Error: ENOENT: no such file or directory, open '.../data'
常见问题(FAQ)
- :当前工作目录与版本目录不匹配或目录未初始化。请先执行 upgrade 生成版本,并在该目录下运行。
密码不匹配或者存储文件不正确
- :传入密码与存储目录加密信息不一致,或索引文件损坏。请确认密码或重新初始化。
索引查询无结果
- :检查是否为该列开启索引(定义或 updateIndex),以及值/范围是否与列类型与长度匹配。
page 返回空
- :检查 size 是否为正数,predicate 是否正确,或缓存参数键名是否与 'index'/'size' 一致。
upgrade` 会将旧版本目录完整复制为备份;迁移仅需复制整个存储目录到目标项目,并确保使用兼容版本的库与正确密码即可。
备份与迁移补充
- 每次
缺点与不足
这个项目实现了一个轻量级的、无依赖的、基于文件的结构化存储系统(类似简易数据库),虽然设计精巧且自给自足,但在生产环境中使用存在显著的风险,“不建议用作主数据库”。
以下是存在的主要缺点与不足:
1. 严重的安全与稳定性隐患 (Critical)
非异步 I/O 阻塞事件循环: architect.mjs 中大量使用了 readSync, writeSync, openSync 等同步文件操作。Node.js 是单线程的,这意味着每次读写数据库都会完全卡死整个应用程序的事件循环,直到文件操作完成。在高并发或大文件读写时,会导致服务器无响应。
缺乏 Crash Safety (崩溃安全): 没有发现 Write-Ahead Log (WAL) 或事务日志机制。依赖内存缓存 (needSave 标志) 和每 3 秒的定时器 (Storage) 落盘。
数据丢失风险: 如果进程在 3 秒间隔内崩溃或断电,这期间的所有数据修改将永久丢失。
文件损坏风险: 如果在写入数据的瞬间崩溃,文件可能只写了一半,导致数据库彻底损坏且无法恢复。
并发控制脆弱: 仅依赖文件头部的“打开计数器”来防止并发,并建议“防止同目录并发写入”。这无法有效处理多进程环境,容易导致竞态条件(Race Condition)和数据错乱。
2. 性能与扩展性瓶颈
内存占用高: 索引(Indexes)似乎是全量加载到内存中的 (#index Set)。对于大数据集,这会消耗大量 RAM。
写入性能随数据量下降: 索引维护使用了 splice 插入 (set2 方法中),这是一个 O(N) 操作。随着数据量增加,插入新数据并维护排序索引的速度会越来越慢。
缓存限制: 硬编码了 MaxSize = 99999 的缓存上限,超过后直接落盘。这种策略比较生硬。
3. 功能局限性
查询能力有限: 虽然支持 filter, find, indexBy...,但本质上很多操作是在遍历内存或文件。不支持复杂的 SQL 语义(如复杂的 JOIN, GROUP BY, 子查询等)。flat 系列方法仅提供了非常基础的关联能力。
缺乏生态工具: 作为一个自定义的二进制格式,没有现成的 GUI 客户端、备份工具、数据迁移工具(除了自带的 upgrade)、可视化监控等。调试数据非常困难,只能写代码查。
4. 安全性细节
密码学实践不标准:
使用 md5(key) 作为后续密钥派生的基础。MD5 如今被认为是不够安全的(抗碰撞性弱),虽然后续用了 scrypt,但起手式用 MD5 会降低整体熵值。
IV 来源于 MD5 的中间字节,这种非随机生成的 IV 在某些加密场景下可能是不安全的。
测试代码中暗示用户密码可能只存了 MD5 (String('存MD5指')),这是过时的做法,现代应用应使用 bcrypt/argon2。
总结建议