crabatool 1.0.371 → 1.0.380
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/index.js +3 -1
- package/lib/jsoncrud.js +254 -0
- package/lib/server.js +9 -0
- package/lib/utils.js +22 -6
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -4,10 +4,12 @@ var start = require('./tool/start.js');
|
|
|
4
4
|
exports = module.exports;
|
|
5
5
|
exports.utils = require('./lib/utils.js');
|
|
6
6
|
exports.server = require('./lib/server.js');
|
|
7
|
-
exports.stringBuilder = require('./lib/stringBuilder.js');
|
|
8
7
|
exports.stringUtils = require('./lib/stringUtils.js');
|
|
9
8
|
exports.pager = require('./lib/pager.js');
|
|
9
|
+
exports.cuid = require('./lib/cuid.js');
|
|
10
|
+
exports.JSONCRUD = require('./lib/jsoncrud.js');
|
|
10
11
|
exports.DbHelper = require('./lib/db/dbHelper.js');
|
|
12
|
+
exports.StringBuilder = require('./lib/stringBuilder.js');
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
exports.run = function(options) {
|
package/lib/jsoncrud.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const fs = require('fs').promises
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const cuid = require('./cuid.js');
|
|
4
|
+
|
|
5
|
+
class JSONCRUD {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} config - 配置项
|
|
8
|
+
* @param {string} [config.storagePath] - 文件存储路径
|
|
9
|
+
* @param {boolean} [config.useMemory] - 是否使用内存存储
|
|
10
|
+
*/
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = config
|
|
13
|
+
this.entities = {} // 存储所有实体 { entityName: { data, relations } }
|
|
14
|
+
this.initialized = false
|
|
15
|
+
|
|
16
|
+
if (config.storagePath && !config.useMemory) {
|
|
17
|
+
this.storageFile = path.resolve(config.storagePath)
|
|
18
|
+
this.initFileStorage()
|
|
19
|
+
} else {
|
|
20
|
+
this.useMemory = true
|
|
21
|
+
this.initialized = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 初始化实体结构
|
|
26
|
+
defineEntity(entityName, relations = {}) {
|
|
27
|
+
if (!this.entities[entityName]) {
|
|
28
|
+
this.entities[entityName] = {
|
|
29
|
+
data: [],
|
|
30
|
+
relations: relations // 定义关联关系 { targetEntity: foreignKey }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 保存到文件
|
|
36
|
+
async saveToFile() {
|
|
37
|
+
if (!this.useMemory) {
|
|
38
|
+
// 构建包含所有实体的存储对象
|
|
39
|
+
const dataToSave = Object.entries(this.entities).reduce((acc, [entityName, entityData]) => {
|
|
40
|
+
acc[entityName] = entityData.data
|
|
41
|
+
return acc
|
|
42
|
+
}, {})
|
|
43
|
+
|
|
44
|
+
// 创建存储目录(如果不存在)
|
|
45
|
+
await fs.mkdir(path.dirname(this.storageFile), { recursive: true })
|
|
46
|
+
|
|
47
|
+
// 原子化写入(避免写入过程中崩溃导致数据丢失)
|
|
48
|
+
const tmpFile = `${this.storageFile}.tmp`
|
|
49
|
+
await fs.writeFile(tmpFile, JSON.stringify(dataToSave, null, 2))
|
|
50
|
+
await fs.rename(tmpFile, this.storageFile)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 核心CRUD方法
|
|
55
|
+
async create(entityName, item) {
|
|
56
|
+
await this.ready()
|
|
57
|
+
this.verifyEntityExists(entityName)
|
|
58
|
+
|
|
59
|
+
const newItem = {
|
|
60
|
+
...item,
|
|
61
|
+
id: cuid.newCuidString(),
|
|
62
|
+
createdAt: new Date().toISOString()
|
|
63
|
+
}
|
|
64
|
+
this.entities[entityName].data.push(newItem)
|
|
65
|
+
await this.persist()
|
|
66
|
+
return newItem
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async find(entityName, {
|
|
70
|
+
filterFunc,
|
|
71
|
+
page = 1,
|
|
72
|
+
pageSize = 10,
|
|
73
|
+
sortBy = 'createdAt',
|
|
74
|
+
sortOrder = 'desc'
|
|
75
|
+
} = {}) {
|
|
76
|
+
await this.ready()
|
|
77
|
+
this.verifyEntityExists(entityName)
|
|
78
|
+
|
|
79
|
+
var allData = [...this.entities[entityName].data];
|
|
80
|
+
if (typeof filterFunc == 'function') {
|
|
81
|
+
allData = allData.filter(filterFunc);
|
|
82
|
+
}
|
|
83
|
+
const sorted = allData.sort((a, b) => {
|
|
84
|
+
sortOrder === 'asc' ?
|
|
85
|
+
a[sortBy]?.localeCompare(b[sortBy]) :
|
|
86
|
+
b[sortBy]?.localeCompare(a[sortBy])
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const start = (page - 1) * pageSize
|
|
90
|
+
const end = start + pageSize
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
data: sorted.slice(start, end),
|
|
94
|
+
pageData: {
|
|
95
|
+
count: allData.length,
|
|
96
|
+
page,
|
|
97
|
+
pageSize,
|
|
98
|
+
totalPages: Math.ceil(allData.length / pageSize)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async findRelated(entityName, targetEntity, relationKey, targetId) {
|
|
104
|
+
await this.ready()
|
|
105
|
+
this.verifyEntityExists(entityName)
|
|
106
|
+
this.verifyEntityExists(targetEntity)
|
|
107
|
+
|
|
108
|
+
const relationConfig = this.entities[entityName].relations[targetEntity]
|
|
109
|
+
if (!relationConfig) {
|
|
110
|
+
throw new Error(`Relation between ${entityName} and ${targetEntity} not defined`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this.entities[entityName].data.filter(
|
|
114
|
+
item => item[relationConfig.foreignKey] === targetId
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async update(entityName, id, updates) {
|
|
119
|
+
await this.ready()
|
|
120
|
+
this.verifyEntityExists(entityName)
|
|
121
|
+
|
|
122
|
+
const index = this.entities[entityName].data.findIndex(item => item.id === id)
|
|
123
|
+
if (index === -1) return null
|
|
124
|
+
|
|
125
|
+
this.entities[entityName].data[index] = {
|
|
126
|
+
...this.entities[entityName].data[index],
|
|
127
|
+
...updates,
|
|
128
|
+
updatedAt: new Date().toISOString()
|
|
129
|
+
}
|
|
130
|
+
await this.persist()
|
|
131
|
+
return this.entities[entityName].data[index]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async delete(entityName, id) {
|
|
135
|
+
await this.ready()
|
|
136
|
+
this.verifyEntityExists(entityName)
|
|
137
|
+
|
|
138
|
+
const initialLength = this.entities[entityName].data.length
|
|
139
|
+
this.entities[entityName].data = this.entities[entityName].data.filter(
|
|
140
|
+
item => item.id !== id
|
|
141
|
+
)
|
|
142
|
+
const success = this.entities[entityName].data.length < initialLength
|
|
143
|
+
if (success) await this.persist()
|
|
144
|
+
return success
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 辅助方法
|
|
148
|
+
verifyEntityExists(entityName, create) {
|
|
149
|
+
if (!this.entities[entityName]) {
|
|
150
|
+
this.defineEntity(entityName);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 等待初始化完成
|
|
155
|
+
async ready() {
|
|
156
|
+
while (!this.initialized) {
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 初始化文件存储
|
|
162
|
+
async initFileStorage() {
|
|
163
|
+
try {
|
|
164
|
+
// 1. 读取存储文件内容
|
|
165
|
+
const fileContent = await fs.readFile(this.storageFile, 'utf-8')
|
|
166
|
+
const storedData = JSON.parse(fileContent || '{}')
|
|
167
|
+
|
|
168
|
+
// 2. 自动创建存储中的实体(如果尚未定义)
|
|
169
|
+
Object.keys(storedData).forEach(entityName => {
|
|
170
|
+
if (!this.entities[entityName]) {
|
|
171
|
+
this.defineEntity(entityName, {}) // 默认空关联关系
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// 3. 合并存储数据到已定义实体
|
|
176
|
+
Object.entries(storedData).forEach(([entityName, items]) => {
|
|
177
|
+
if (this.entities[entityName]) {
|
|
178
|
+
// 保留现有关联关系配置
|
|
179
|
+
this.entities[entityName].data = items.map(item => ({
|
|
180
|
+
...item,
|
|
181
|
+
// 确保存在时间戳字段
|
|
182
|
+
createdAt: item.createdAt || new Date().toISOString(),
|
|
183
|
+
updatedAt: item.updatedAt || null
|
|
184
|
+
}))
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.code === 'ENOENT') {
|
|
190
|
+
// 4. 文件不存在时初始化空文件
|
|
191
|
+
await this.saveToFile()
|
|
192
|
+
} else if (err instanceof SyntaxError) {
|
|
193
|
+
// 5. 处理文件内容损坏的情况
|
|
194
|
+
console.error('Storage file corruption detected, resetting...')
|
|
195
|
+
await this.saveToFile()
|
|
196
|
+
} else {
|
|
197
|
+
throw err
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
// 6. 确保初始化完成标记
|
|
201
|
+
this.initialized = true
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 通用保存方法
|
|
206
|
+
async persist() {
|
|
207
|
+
if (this.useMemory) return
|
|
208
|
+
await this.saveToFile()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
}
|
|
212
|
+
exports = module.exports = JSONCRUD;
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
// 使用示例
|
|
216
|
+
async function main() {
|
|
217
|
+
const db = new JSONCRUD({ storagePath: './multi-data.json' })
|
|
218
|
+
|
|
219
|
+
// 1. 定义实体和关联关系
|
|
220
|
+
db.defineEntity('users', {
|
|
221
|
+
posts: { foreignKey: 'authorId' }
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
db.defineEntity('posts', {
|
|
225
|
+
author: { foreignKey: 'id' } // 反向关联
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// 2. 创建数据
|
|
229
|
+
const user = await db.create('users', {
|
|
230
|
+
name: 'John',
|
|
231
|
+
email: 'john@example.com'
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
await db.create('posts', {
|
|
235
|
+
title: 'First Post',
|
|
236
|
+
content: 'Hello World',
|
|
237
|
+
authorId: user.id
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// 3. 分页查询
|
|
241
|
+
const postPage = await db.find('posts', {
|
|
242
|
+
page: 1,
|
|
243
|
+
pageSize: 5,
|
|
244
|
+
sortBy: 'createdAt'
|
|
245
|
+
})
|
|
246
|
+
console.log('Paginated posts:', postPage)
|
|
247
|
+
|
|
248
|
+
// 4. 关联查询
|
|
249
|
+
const userPosts = await db.findRelated('posts', 'users', 'author', user.id)
|
|
250
|
+
console.log('User posts:', userPosts)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
main()
|
|
254
|
+
*/
|
package/lib/server.js
CHANGED
|
@@ -157,6 +157,15 @@ function createServer(app, options) {
|
|
|
157
157
|
var port = server.address().port;
|
|
158
158
|
var url = 'http://127.0.0.1:' + port;
|
|
159
159
|
|
|
160
|
+
// 告诉业务已经启动服务了
|
|
161
|
+
var globaljs = path.join(webPath, '../lib/global.js');
|
|
162
|
+
if (fs.existsSync(globaljs)) {
|
|
163
|
+
var global = require(globaljs);
|
|
164
|
+
if (global.main) {
|
|
165
|
+
global.main(server);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
160
169
|
if (options && options.onRun) {
|
|
161
170
|
options.onRun(port);
|
|
162
171
|
}
|
package/lib/utils.js
CHANGED
|
@@ -136,7 +136,7 @@ class Utils {
|
|
|
136
136
|
text = text.replace('_', ''); // 第一个字符不能是下划线
|
|
137
137
|
return text.replace(/[^a-zA-Z0-9_]/g, ''); // 过滤非字母、数字、下划线的其它字符
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
format() {
|
|
141
141
|
// 字符串参数化
|
|
142
142
|
var args = new Array(arguments.length);
|
|
@@ -346,6 +346,14 @@ class Utils {
|
|
|
346
346
|
next(); // 继续处理
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
isEmpty(v) {
|
|
350
|
+
return v === '' || this._isUndefinedOrNull(v);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
isUndefinedOrNull(v) {
|
|
354
|
+
return v === null || v === undefined;
|
|
355
|
+
}
|
|
356
|
+
|
|
349
357
|
// 删除文件夹,清空文件夹,删除目录,清空目录,清空整个目录,包括子目录和文件
|
|
350
358
|
cleardirsSync(dirname) {
|
|
351
359
|
if (!fs.existsSync(dirname)) {
|
|
@@ -543,11 +551,19 @@ class Utils {
|
|
|
543
551
|
|
|
544
552
|
var stat = fs.statSync(filePath);
|
|
545
553
|
if (stat.isDirectory()) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
554
|
+
if (options.level === 0) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (options.folder) {
|
|
559
|
+
fileList.push(filePath);
|
|
560
|
+
} else {
|
|
561
|
+
var options2 = {};
|
|
562
|
+
Object.assign(options2, options);
|
|
563
|
+
options2.source = filePath;
|
|
564
|
+
utils.getFiles(options2, fileList);
|
|
565
|
+
}
|
|
566
|
+
} else if (!options.folder) {
|
|
551
567
|
var ename = path.extname(filePath);
|
|
552
568
|
if (!ename || (exts && !exts.includes(ename))) {
|
|
553
569
|
return;
|