wok-server 0.2.2 → 0.3.1

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.
Files changed (37) hide show
  1. package/dist/config/index.js +17 -3
  2. package/dist/log/file.js +4 -11
  3. package/dist/mvc/config.js +23 -16
  4. package/dist/mvc/handler/json.js +27 -0
  5. package/dist/mvc/index.js +6 -295
  6. package/dist/mvc/render/file.js +3 -85
  7. package/dist/mvc/server.js +263 -0
  8. package/dist/mvc/static/header.js +67 -0
  9. package/dist/mvc/static/index.js +6 -0
  10. package/dist/mvc/static/mime-type.js +84 -0
  11. package/dist/mvc/static/server-cache-config.js +66 -0
  12. package/dist/mvc/static/server-cache.js +133 -0
  13. package/dist/mvc/static/static-handler.js +355 -0
  14. package/dist/mysql/manager/ops/update.js +15 -1
  15. package/dist/validation/index.js +12 -1
  16. package/dist/validation/validator/array.js +7 -11
  17. package/dist/validation/validator/min.js +2 -2
  18. package/documentation/zh-cn/config.md +31 -0
  19. package/documentation/zh-cn/mvc.md +48 -0
  20. package/documentation/zh-cn/mysql.md +72 -0
  21. package/documentation/zh-cn/validate.md +21 -15
  22. package/package.json +1 -1
  23. package/types/config/index.d.ts +10 -0
  24. package/types/mvc/config.d.ts +13 -1
  25. package/types/mvc/handler/json.d.ts +14 -0
  26. package/types/mvc/index.d.ts +3 -36
  27. package/types/mvc/server.d.ts +85 -0
  28. package/types/mvc/static/header.d.ts +27 -0
  29. package/types/mvc/static/index.d.ts +3 -0
  30. package/types/mvc/static/mime-type.d.ts +2 -0
  31. package/types/mvc/static/server-cache-config.d.ts +30 -0
  32. package/types/mvc/static/server-cache.d.ts +76 -0
  33. package/types/mvc/static/static-handler.d.ts +72 -0
  34. package/types/mysql/manager/ops/update.d.ts +7 -1
  35. package/types/validation/validator/array.d.ts +2 -2
  36. package/types/validation/validator/min.d.ts +2 -2
  37. package/types/validation/validator/plain-obj.d.ts +1 -1
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StaticHandler = void 0;
4
+ const fs_1 = require("fs");
5
+ const promises_1 = require("fs/promises");
6
+ const path_1 = require("path");
7
+ const zlib_1 = require("zlib");
8
+ const render_1 = require("../render");
9
+ const header_1 = require("./header");
10
+ const mime_type_1 = require("./mime-type");
11
+ const server_cache_1 = require("./server-cache");
12
+ const server_cache_config_1 = require("./server-cache-config");
13
+ class StaticHandler {
14
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream';
15
+ maxFileSize;
16
+ cache;
17
+ rules;
18
+ /**
19
+ * 静态处理器。规则说明:
20
+ * 请求路径仅支持前缀匹配,不支持通配符,比如 /a/demo.html 可以匹配 /a 路径,响应配置的文件目录下的 demo.html 文件。
21
+ * 路径配置是有优先级的,如果访问 /a/b/music.mp3 则会匹配到 /a/b 的配置,而不是 /a ,因为 /a/b 的配置更详细,优先级也更高,
22
+ * 并且如果从 /a/b 配置的目录下没有找到文件,也不会再尝试 /a 的配置。
23
+ *
24
+ * 静态文件同时也支持主页自动映射,比如访问 /a/b/c ,会匹配到 /a/b 的配置,然后在配置的文件目录下寻找文件 c ,
25
+ * 如果找不到则尝试寻找目录 c 下的 index.html 文件。
26
+ * @param rules 规则设置
27
+ */
28
+ constructor(rules) {
29
+ const config = (0, server_cache_config_1.getConfig)();
30
+ this.maxFileSize = (0, server_cache_config_1.parseSize)(config.maxFileSize);
31
+ if (config.enable) {
32
+ this.cache = new server_cache_1.ServerCache({ maxSize: (0, server_cache_config_1.parseSize)(config.maxSize), maxAge: config.maxAge });
33
+ }
34
+ this.rules = [];
35
+ // 规则解析,路径判定,设置的目录必须存在或可以被创建
36
+ // 重复记录表 ,作用是为了路径去重判定,可以提示哪些路径是重复的
37
+ const duplicateMap = new Map();
38
+ for (const entry of Object.entries(rules)) {
39
+ const [path, setting] = entry;
40
+ const dir = (0, path_1.isAbsolute)(setting.dir) ? setting.dir : (0, path_1.resolve)(process.cwd(), setting.dir);
41
+ if (!(0, fs_1.existsSync)(dir)) {
42
+ throw new Error(`Static file configuration error,path ${dir} does not exist,config dir:${setting.dir}`);
43
+ }
44
+ const statRes = (0, fs_1.statSync)(dir);
45
+ if (!statRes.isDirectory()) {
46
+ throw new Error(`Static file configuration error,path ${dir} is not a directory,config dir:${setting.dir}`);
47
+ }
48
+ let finalPath = path.startsWith('/') ? path : '/' + path;
49
+ // 保持以 / 结尾,为了匹配方便
50
+ if (!finalPath.endsWith('/')) {
51
+ finalPath += '/';
52
+ }
53
+ if (duplicateMap.has(finalPath)) {
54
+ throw new Error(`Static path duplicated: ${duplicateMap.get(finalPath)} and ${path}`);
55
+ }
56
+ duplicateMap.set(finalPath, path);
57
+ this.rules.push({ path: finalPath, dir, cacheAge: setting.cacheAge || 0 });
58
+ }
59
+ // 优先级排序
60
+ this.rules.sort((o1, o2) => {
61
+ let priority1 = o1.path === '/' ? -1 : o1.path.split('/').length;
62
+ let priority2 = o2.path === '/' ? -1 : o2.path.split('/').length;
63
+ // 如果 o1 优先级高,就应该排前面,返回小于0的值,反之亦然\
64
+ // 前面的优先级值是值越大优先级越高,反过来减
65
+ return priority2 - priority1;
66
+ });
67
+ }
68
+ /**
69
+ * 处理静态文件 get 请求
70
+ * @param request
71
+ * @param response
72
+ * @param path
73
+ * @returns 是否能够处理,如果因为找不到文件或其它原因无法处理则返回 false ,由后续流程继续处理
74
+ */
75
+ async handleGet(request, response, path) {
76
+ // 解析消息头
77
+ const headersInfo = (0, header_1.parseHeaders)(request.headers);
78
+ // 构建文件信息
79
+ let file = await this.buildRespFile(path, headersInfo);
80
+ if (!file) {
81
+ return false;
82
+ }
83
+ // content-type
84
+ response.setHeader('Content-Type', file.mimeType);
85
+ // client cache
86
+ if (file.maxAge === 0) {
87
+ response.setHeader('Cache-Control', 'no-store');
88
+ }
89
+ else if (file.maxAge) {
90
+ response.setHeader('Cache-Control', `max-age=${file.maxAge}`);
91
+ }
92
+ response.setHeader('Last-Modified', file.mtime.toUTCString());
93
+ // 开始响应,先判定 if-modified-since
94
+ if (headersInfo.ifModifiedSince) {
95
+ if (headersInfo.ifModifiedSince >= file.mtime) {
96
+ response.statusCode = 304;
97
+ response.end();
98
+ return true;
99
+ }
100
+ }
101
+ if (headersInfo.range) {
102
+ const { start } = headersInfo.range;
103
+ if (isNaN(start) || start < 0) {
104
+ (0, render_1.renderError)(response, `Range not satisfiable,start is ${start}.`, 416);
105
+ return true;
106
+ }
107
+ const maxEnd = file.size - 1;
108
+ let end = headersInfo.range.end ? headersInfo.range.end : maxEnd;
109
+ // 校验 end
110
+ if (end <= start) {
111
+ (0, render_1.renderError)(response, `Range not satisfiable,end must be greater than start.`, 416);
112
+ return true;
113
+ }
114
+ if (end > maxEnd) {
115
+ (0, render_1.renderError)(response, `Range not satisfiable,end must not be greater than ${maxEnd}.`, 416);
116
+ return true;
117
+ }
118
+ // range 是前后都包含的,详细可以参考规范:
119
+ // https://www.rfc-editor.org/rfc/rfc9110#field.range
120
+ // 但是 buffer.subarray 中包含前不包含后,createReadStream 是前后都包含的,需要注意
121
+ // 还需要注意 gzip 编码
122
+ response.setHeader('Content-Range', `bytes ${start}-${end}/${file.size}`);
123
+ response.statusCode = 206;
124
+ if (file.bufferOrPath instanceof Buffer) {
125
+ let buffer = file.bufferOrPath.subarray(start, end + 1);
126
+ if (headersInfo.gzip) {
127
+ buffer = await new Promise((resolve, reject) => {
128
+ (0, zlib_1.gzip)(buffer, (err, res) => {
129
+ if (err) {
130
+ reject(err);
131
+ return;
132
+ }
133
+ resolve(res);
134
+ });
135
+ });
136
+ response.setHeader('Content-Encoding', 'gzip');
137
+ }
138
+ await this.endRespWithBuffer(response, buffer);
139
+ }
140
+ else {
141
+ const filePath = file.bufferOrPath;
142
+ // 文件处理
143
+ await new Promise((resolve, reject) => {
144
+ response.setHeader('Content-Encoding', 'gzip');
145
+ response.once('finish', resolve).once('error', reject);
146
+ if (headersInfo.gzip) {
147
+ (0, fs_1.createReadStream)(filePath, { start, end }).pipe((0, zlib_1.createGzip)()).pipe(response);
148
+ }
149
+ else {
150
+ (0, fs_1.createReadStream)(filePath, { start, end }).pipe(response);
151
+ }
152
+ });
153
+ }
154
+ return true;
155
+ }
156
+ // gzip
157
+ if (headersInfo.gzip) {
158
+ response.setHeader('Content-Encoding', 'gzip');
159
+ if (file.bufferOrPath instanceof Buffer) {
160
+ // buffer 是缓存的文件,gzip 编码缓存的文件即是已经压缩后的文件
161
+ await this.endRespWithBuffer(response, file.bufferOrPath);
162
+ }
163
+ else {
164
+ (0, fs_1.createReadStream)(file.bufferOrPath).pipe((0, zlib_1.createGzip)()).pipe(response);
165
+ }
166
+ return true;
167
+ }
168
+ // 普通的整个文件请求处理
169
+ if (file.bufferOrPath instanceof Buffer) {
170
+ await this.endRespWithBuffer(response, file.bufferOrPath);
171
+ }
172
+ else {
173
+ const filePath = file.bufferOrPath;
174
+ await new Promise((resolve, reject) => {
175
+ response.once('finish', resolve).once('error', reject);
176
+ (0, fs_1.createReadStream)(filePath).pipe(response);
177
+ });
178
+ }
179
+ return true;
180
+ }
181
+ /**
182
+ * 处理 head 请求,不响应正文内容,如果文件可以被找到,仅仅响应以下的消息头:
183
+ * Content-Length
184
+ * Content-Type
185
+ * Last-Modified
186
+ * Cache-Control
187
+ *
188
+ * @param path 请求路径
189
+ * @param response
190
+ * @returns 是否成功处理
191
+ */
192
+ async handleHead(path, response) {
193
+ // 构建文件信息
194
+ let file = await this.buildRespFile(path, { gzip: false });
195
+ if (!file) {
196
+ return false;
197
+ }
198
+ response.setHeader('Content-Length', file.size);
199
+ response.setHeader('Content-Type', file.mimeType);
200
+ response.setHeader('Last-Modified', file.mtime.toUTCString());
201
+ if (file.maxAge === 0) {
202
+ response.setHeader('Cache-Control', 'no-store');
203
+ }
204
+ else if (file.maxAge) {
205
+ response.setHeader('Cache-Control', `max-age=${file.maxAge}`);
206
+ }
207
+ response.end();
208
+ return true;
209
+ }
210
+ async endRespWithBuffer(response, buffer) {
211
+ await new Promise((resolve, reject) => {
212
+ response.write(buffer, err => {
213
+ if (err) {
214
+ reject(err);
215
+ }
216
+ else {
217
+ resolve();
218
+ }
219
+ });
220
+ });
221
+ await new Promise((resolve, reject) => {
222
+ response.end(resolve);
223
+ });
224
+ }
225
+ /**
226
+ * 构建响应文件
227
+ *
228
+ * @param path
229
+ * @param headers
230
+ * @returns
231
+ */
232
+ async buildRespFile(path, headers) {
233
+ if (!this.cache) {
234
+ const file = await this.findFile(path);
235
+ if (!file) {
236
+ return null;
237
+ }
238
+ let { mtime } = file.stats;
239
+ mtime.setMilliseconds(0);
240
+ return {
241
+ maxAge: file.maxAge,
242
+ mtime: mtime,
243
+ size: file.stats.size,
244
+ bufferOrPath: file.filePath,
245
+ mimeType: (0, mime_type_1.decideContentType)(file.filePath) || this.DEFAULT_CONTENT_TYPE
246
+ };
247
+ }
248
+ const cacheingGzip = headers.gzip && !headers.range;
249
+ const key = cacheingGzip ? `gzip-${path}` : path;
250
+ let file;
251
+ const cachedVal = await this.cache.computeIfAbsent(key, async () => {
252
+ file = (await this.findFile(path)) || undefined;
253
+ if (!file) {
254
+ return null;
255
+ }
256
+ // 文件太大不能被缓存
257
+ if (file.stats.size > this.maxFileSize) {
258
+ return null;
259
+ }
260
+ let buffer = await (0, promises_1.readFile)(file.filePath);
261
+ // gzip 存储压缩后的文件
262
+ if (cacheingGzip) {
263
+ buffer = await new Promise((resolve, reject) => {
264
+ (0, zlib_1.gzip)(buffer, (err, res) => {
265
+ if (err) {
266
+ reject(err);
267
+ }
268
+ else {
269
+ resolve(res);
270
+ }
271
+ });
272
+ });
273
+ }
274
+ let { mtime } = file.stats;
275
+ mtime.setMilliseconds(0);
276
+ return {
277
+ buffer,
278
+ mtime: mtime,
279
+ cacheAge: file.maxAge,
280
+ mimeType: (0, mime_type_1.decideContentType)(file.filePath) || this.DEFAULT_CONTENT_TYPE
281
+ };
282
+ });
283
+ if (cachedVal) {
284
+ return {
285
+ maxAge: cachedVal.cacheAge,
286
+ mtime: cachedVal.mtime,
287
+ size: cachedVal.buffer.length,
288
+ bufferOrPath: cachedVal.buffer,
289
+ mimeType: cachedVal.mimeType
290
+ };
291
+ }
292
+ // 判定文件大小
293
+ if (file) {
294
+ let { mtime } = file.stats;
295
+ mtime.setMilliseconds(0);
296
+ return {
297
+ maxAge: file.maxAge,
298
+ mtime: mtime,
299
+ size: file.stats.size,
300
+ bufferOrPath: file.filePath,
301
+ mimeType: (0, mime_type_1.decideContentType)(file.filePath) || this.DEFAULT_CONTENT_TYPE
302
+ };
303
+ }
304
+ return null;
305
+ }
306
+ /**
307
+ * 根据路径查找文件
308
+ * @param path 访问路径
309
+ * @returns 返回被查询到的文件信息,如果找不到则返回 null
310
+ */
311
+ async findFile(path) {
312
+ let matchedRule;
313
+ for (const rule of this.rules) {
314
+ if (rule.path === '/') {
315
+ matchedRule = rule;
316
+ break;
317
+ }
318
+ if (path.startsWith(rule.path)) {
319
+ matchedRule = rule;
320
+ break;
321
+ }
322
+ }
323
+ // 匹配失败
324
+ if (!matchedRule) {
325
+ return null;
326
+ }
327
+ // 构建地址
328
+ let finalPath = matchedRule.path === '/' ? path : path.substring(matchedRule.path.length);
329
+ if (finalPath.startsWith('/')) {
330
+ finalPath = finalPath.substring(1);
331
+ }
332
+ let filePath = (0, path_1.resolve)(matchedRule.dir, finalPath);
333
+ if (!(0, fs_1.existsSync)(filePath)) {
334
+ return null;
335
+ }
336
+ const fileStat = await (0, promises_1.stat)(filePath);
337
+ // 如果是目录,尝试查找 index.html
338
+ if (fileStat.isDirectory()) {
339
+ const indexPath = (0, path_1.resolve)(filePath, 'index.html');
340
+ if (!(0, fs_1.existsSync)(indexPath)) {
341
+ return null;
342
+ }
343
+ const indexStat = await (0, promises_1.stat)(indexPath);
344
+ if (!indexStat.isFile()) {
345
+ return null;
346
+ }
347
+ return { filePath: indexPath, stats: indexStat };
348
+ }
349
+ if (!fileStat.isFile()) {
350
+ return null;
351
+ }
352
+ return { filePath, stats: fileStat, maxAge: matchedRule.cacheAge };
353
+ }
354
+ }
355
+ exports.StaticHandler = StaticHandler;
@@ -65,6 +65,16 @@ function updatorToSql(table, updater) {
65
65
  continue;
66
66
  }
67
67
  const val = updater[column];
68
+ // undefined 表示不参与更新,作用是方便编写一些特殊的逻辑,比如特定情况下不更新
69
+ if (val === undefined) {
70
+ continue;
71
+ }
72
+ // 兼容将值设置成 null 的情况,和 ['setNull’] 等同
73
+ if (val === null) {
74
+ updateFragList.push(' ?? = NULL ');
75
+ values.push(column);
76
+ continue;
77
+ }
68
78
  if (Array.isArray(val)) {
69
79
  // set null
70
80
  if (val[0] === 'setNull') {
@@ -77,7 +87,11 @@ function updatorToSql(table, updater) {
77
87
  values.push(column, column, val[1]);
78
88
  continue;
79
89
  }
80
- continue;
90
+ if (val[0] === 'set') {
91
+ updateFragList.push(' ?? = ? ');
92
+ values.push(column, (0, utils_2.processColumnValue)(val[1]));
93
+ continue;
94
+ }
81
95
  }
82
96
  updateFragList.push(' ?? = ? ');
83
97
  values.push(column, (0, utils_2.processColumnValue)(val));
@@ -18,7 +18,18 @@ function validate(obj, opts) {
18
18
  if (result.propPath) {
19
19
  propPath.push(...result.propPath);
20
20
  }
21
- throw new exception_1.ValidationException(result.message, result.validator, propPath.join('.'), val);
21
+ const fullPath = propPath
22
+ .map((prop, idx) => {
23
+ if (idx === 0) {
24
+ return prop;
25
+ }
26
+ if (prop.startsWith('[') && prop.endsWith(']')) {
27
+ return prop;
28
+ }
29
+ return '.' + prop;
30
+ })
31
+ .join('');
32
+ throw new exception_1.ValidationException(result.message, result.validator, fullPath, val);
22
33
  }
23
34
  }
24
35
  }
@@ -17,18 +17,14 @@ function array(opts) {
17
17
  // 条目处理
18
18
  for (let i = 0; i < val.length; i++) {
19
19
  const item = val[i];
20
- for (const validation of Object.entries(opts)) {
21
- const [prop, validators] = validation;
22
- const val = item[prop];
23
- for (const validate of validators) {
24
- const result = validate(val);
25
- if (!result.ok) {
26
- const propPath = [`[${i}]`, prop];
27
- if (result.propPath) {
28
- propPath.push(...result.propPath);
29
- }
30
- return { ok: false, validator, message: result.message, propPath };
20
+ for (const validation of opts) {
21
+ const result = validation(item);
22
+ if (!result.ok) {
23
+ const propPath = [`[${i}]`];
24
+ if (result.propPath) {
25
+ propPath.push(...result.propPath);
31
26
  }
27
+ return { ok: false, validator, message: result.message, propPath };
32
28
  }
33
29
  }
34
30
  }
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.min = void 0;
4
4
  const i18n_1 = require("../../i18n");
5
5
  /**
6
- * 最大值校验.
7
- * @param max 最大值
6
+ * 最小值校验.
7
+ * @param min 最小值
8
8
  * @param msg 错误消息
9
9
  */
10
10
  function min(min, msg) {
@@ -9,6 +9,28 @@
9
9
  | :------------- | :--------------------------------------------------------------- |
10
10
  | registerConfig | 注册配置信息,注册会立即匹配环境变量并返回映射了属性后的配置对象 |
11
11
  | getConfig | 获取配置信息 |
12
+ | registerConfig | 根据环境变量生成配置信息,但是不注册 |
13
+
14
+ ## 配置类型和实例对象
15
+
16
+ 要进行配置映射,首先要定义一个配置类型,下面是一个例子:
17
+
18
+ ```ts
19
+ /**
20
+ * 配置定义
21
+ */
22
+ interface CustomConfig {
23
+ appId: string
24
+ appSecret: string
25
+ }
26
+ ```
27
+
28
+ **配置类型的所有属性都不能是可空的,必须有值,这样才可以映射。**
29
+ 当然也可以不定义类型,直接申明一个对象,调用 registerConfig 也可以完成类型的自动推断,见后面 registerConfig 的说明。
30
+
31
+ 配置对象的目前仅支持下列类型:string、number、boolean。
32
+
33
+ ## registerConfig
12
34
 
13
35
  环境变量的映射需要有一个前缀,然后将对象的属性拼接前缀,再进行转换,去匹配环境变量。
14
36
  比如配置对象有个属性是 appId,设置的前缀是 cus ,那么匹配的环境变量就是 CUS_APP_ID。
@@ -60,9 +82,18 @@ const config2 = registerConfig(
60
82
  )
61
83
  ```
62
84
 
85
+ ## getConfig
86
+
63
87
  getConfig 函数仅一个参数,传递前缀,可以获取到配置对象。
64
88
  一般很少需要使用,推荐调用 registerConfig 时将返回的配置对象导出以供其它程序模块使用。
65
89
 
66
90
  ```ts
67
91
  const config2 = getConfig('c2')
68
92
  ```
93
+
94
+ ## generateConfig
95
+
96
+ 根据环境变量来生成配置对象,与 registerConfig 的参数是一致的,和 registerConfig 不同的是
97
+ generateConfig 可以多次重复使用,方便在运行时更改环境变量后再生成配置,而调用 registerConfig
98
+ 一旦注册后不能再被更改。之所以这样设计是为了方便程序的测试,模拟在不同的环境下运行程序,
99
+ 和其它的一些特殊需求。
@@ -275,6 +275,40 @@ export const userCreateHandler = createJsonHandler<Form, Resp>({
275
275
 
276
276
  校验时会自动根据消息头 `accept-language` 切换校验器的语言,对于没有自定义错误信息的校验使用切换后的语言给予默认的提示。
277
277
 
278
+ 从 0.3.0 版本开始,createJsonHandler 支持了通过 cache 选项来设置使用缓存组件将响应结果进行缓存,
279
+ 在可以复用缓存的情况下,避免再次执行整个 handle 方法的流程,从而提升性能。
280
+
281
+ ```ts
282
+ export const userGetHandler = createJsonHandler<Form, User>({
283
+ // 设置缓存,参数和 handle 方法一样的
284
+ // 只能缓存有效的响应信息,如果没有响应正文则不会进行缓存
285
+ async cache(body, exchange) {
286
+ // 使用参数 id 来构建缓存的 key
287
+ const key = `get-user-${body.id}`
288
+ // 返回缓存的 key 和有效时间,有效时间是可选的
289
+ return { key, expiresInSeconds: 60 }
290
+ },
291
+ async handle(body, exchange) {
292
+ // handle 流程省略 ...
293
+ }
294
+ })
295
+ ```
296
+
297
+ 由于缓存是基于缓存组件,那么也可以通过缓存组件来清理缓存。
298
+
299
+ ```ts
300
+ export const userUpdateHandler = createJsonHandler<Form>({
301
+ async handle(body, exchange) {
302
+ // handle 部分流程省略 ...
303
+ // 将用户详情接口的缓存清理掉
304
+ getCache().remove(`get-user-${body.id}`)
305
+ }
306
+ })
307
+ ```
308
+
309
+ 但是不要使用缓存组件来获取缓存内容,因为为了提升性能,缓存的内容是 Bufer,
310
+ 当使用缓存时避免再次执行对象序列化,并不是 handle 方法返回的对象。
311
+
278
312
  ### 二进制(Binary)上传
279
313
 
280
314
  二制进上传也就是将文件以二进制的形式写入请求正文,请求正文仅存储文件内容没有别的信息,
@@ -534,6 +568,20 @@ dir 参数是映射文件目录的地址,可以是绝对路径,也可以是
534
568
  静态文件同时也支持主页自动映射,比如访问 /a/b/c ,会匹配到 /a/b 的配置,然后在配置的文件目录下寻找文件 c ,
535
569
  如果找不到则尝试寻找目录 c 下的 index.html 文件。
536
570
 
571
+ #### 静态文件服务器缓存
572
+
573
+ 从 0.3.0 版本开始,支持服务器端缓存静态文件。要启用静态文件缓存,需要配置以下的环境变量:
574
+
575
+ | 环境变量 | 说明 |
576
+ | :-------------------------------- | :---------------------------------------------------------------- |
577
+ | SERVER_STATIC_CACHE_ENABLE | 是否启用服务器缓存,默认 false |
578
+ | SERVER_STATIC_CACHE_MAX_AGE | 服务器缓存时间,单位秒,默认 600 |
579
+ | SERVER_STATIC_CACHE_MAX_FILE_SIZE | 最大可缓存的文件大小,支持语义化格式,如 10m 和 100k 等,默认 10m |
580
+ | SERVER_STATIC_CACHE_MAX_SIZE | 缓存最大空间,一旦超出将执行清理,同上支持语义化格式,默认 100, |
581
+
582
+ 静态文件的服务器缓存不支持清除,只能等待过期,或者空间满被清理掉。所以,只适合缓存长期不需要改变的文件,
583
+ 目前还没有支持按规则来缓存,后续的版本有可能会考虑。
584
+
537
585
  ### 请求日志
538
586
 
539
587
  通过将环境变量 SERVER_ACCESS_LOG 设置为 true 可以开启请求日志,开启后会在每次响应完成后输出请求和响应信息到日志里,默认是关闭的。
@@ -406,6 +406,78 @@ await mananger.find({
406
406
  json_extract 和 json_length 查询都需要传递元组,第一个参数就是查询的类型,第二个参数是字段名称,
407
407
  json_extract 还有第三个参数是属性路径。属性路径的格式和 js 获取属性的语法是一样的,只是用 $ 来指代字段的值。
408
408
 
409
+ ```ts
410
+ // 批量修改出题人 id 为 x333 的记录
411
+ await mananger.updateMany({
412
+ table: tableQuestion,
413
+ query: c => c.eq(['json_extract', 'question_setter', '$.id'], 'x333'),
414
+ updater: {
415
+ // 更新 question_setter 信息
416
+ question_setter: { id: 'x333', name: '李帅' }
417
+ }
418
+ })
419
+ ```
420
+
421
+ 更新目前并没有支持 json_set 等函数,无法做到只更新 json 字段中的部分信息,只能整个更新。
422
+ 这类的操作使用的较少,后续的版本再考虑要不要支持。
423
+
424
+ ### 特殊修改操作
425
+
426
+ partialUpdate 和 updateMany 方法支持局部修改一些字段,并且支持一些特殊的修改操作,比如自增和置空。
427
+
428
+ ```ts
429
+ await manager.updateMany(tableUser, c => c.between('balance', 23, 24), {
430
+ // 将 balance 增加 2
431
+ balance: ['inc', 2],
432
+ // 将 consume_type 置空
433
+ consume_type:['setNull']
434
+ })
435
+ ```
436
+
437
+ 如果将一个字段的值设置为 null 也可以置空,但是这要求设置 ts 类型支持。
438
+
439
+ ```ts
440
+ interface User {
441
+ id: string
442
+ // 将 role 的类型设置包含 null
443
+ role: string | null
444
+ }
445
+
446
+ await manager.partialUpdate(tableUser, {
447
+ id: '001',
448
+ // 等同于 role:['setNull']
449
+ role: null
450
+ })
451
+ ```
452
+
453
+ 之所以采用使用特殊的数组来这种方式,主要目的是为了方便,不必引入新的类型,代码编写方便。
454
+ **但是,在引入 json 类型后,如果 json 字段是数组就可能会和特殊修改操作有冲突,**
455
+ **比如将 json 字段的值设置为 ['setNull'],和置空操作是一样的,最终会被认为要置空字段**。
456
+
457
+ ```ts
458
+ // record 实体定义
459
+ interface Record {
460
+ id: string
461
+ // extra 字段是一个 json 数组
462
+ extra: string[]
463
+ }
464
+ await manager.partialUpdate(tableRecord, {
465
+ id: '001',
466
+ // 这样操作会被认为要将 extra 置空
467
+ extra: ['setNull']
468
+ })
469
+ ```
470
+
471
+ 组件提供了 set 操作来解决冲突,也是一个特殊的数组,第一个元素是 'set',第二个元素是要设置的值。
472
+
473
+ ```ts
474
+ await manager.partialUpdate(tableRecord, {
475
+ id: '001',
476
+ // 这样就可以将 extra 设置为 ['setNull']
477
+ extra: ['set',['setNull']]
478
+ })
479
+ ```
480
+
409
481
 
410
482
  ### 预编译 sql
411
483
 
@@ -32,8 +32,6 @@ validate(
32
32
  | array | 数组校验,设置元素对象的校验规则来校验每一个元素 |
33
33
  | plainObject | 对象校验,如果属性是对象可进一步校验属性值的属性 |
34
34
 
35
- **注意: array 和 plainObject 在校验层级深的对象时,可能无法拥有准确的类型推断,必须在使用这两个函数时指定泛型,比如 `plainObject<User>({...})`,否则编译会报错。**
36
-
37
35
  下面是对象和数组嵌套的校验示例。
38
36
 
39
37
  ```ts
@@ -66,19 +64,27 @@ validate<User>(
66
64
  })
67
65
  ],
68
66
  tags: [
69
- array({
70
- id: [notBlank()],
71
- name: [notBlank()],
72
- permissinos: [
73
- notNull(),
74
- // 这里无法自动进行推断类型,必须指定类型,否则编译会有错误
75
- // 层级过深就必须指定泛型
76
- plainObject<{ edit?: boolean; read?: boolean }>({
77
- edit: [notNull()],
78
- read: [notNull()]
79
- })
80
- ]
81
- })
67
+ // 标签不得超过5个
68
+ maxLength(5),
69
+ // 标签列表不能为空
70
+ notNull(),
71
+ // 校验标签数组的元素
72
+ array([
73
+ // 元素不能为空
74
+ notNull(),
75
+ // 元素的属性校验
76
+ plainObject({
77
+ id: [notBlank()],
78
+ name: [notBlank()],
79
+ permissinos: [
80
+ notNull(),
81
+ plainObject({
82
+ edit: [notNull()],
83
+ read: [notNull()]
84
+ })
85
+ ]
86
+ })
87
+ ])
82
88
  ]
83
89
  }
84
90
  )