@anjianshi/utils 1.2.7 → 1.3.0
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/env-node/env-reader.d.ts +12 -0
- package/env-node/env-reader.js +31 -0
- package/env-node/{logging.d.ts → logging/handlers.d.ts} +1 -6
- package/env-node/{logging.js → logging/handlers.js} +1 -12
- package/env-node/logging/index.d.ts +11 -0
- package/env-node/logging/index.js +14 -0
- package/env-node/typeorm/adapt-logging.d.ts +11 -0
- package/env-node/typeorm/adapt-logging.js +42 -0
- package/env-node/typeorm/index.d.ts +31 -0
- package/env-node/typeorm/index.js +39 -0
- package/lang/index.d.ts +1 -0
- package/lang/index.js +1 -0
- package/lang/may-success.d.ts +40 -0
- package/lang/may-success.js +27 -0
- package/lang/types.d.ts +12 -43
- package/lang/types.js +0 -13
- package/md5.d.ts +30 -0
- package/md5.js +309 -0
- package/package.json +3 -1
- package/src/env-node/env-reader.ts +33 -0
- package/src/env-node/{logging.ts → logging/handlers.ts} +1 -21
- package/src/env-node/logging/index.ts +16 -0
- package/src/env-node/typeorm/adapt-logging.ts +54 -0
- package/src/env-node/typeorm/index.ts +62 -0
- package/src/lang/index.ts +1 -0
- package/src/lang/may-success.ts +57 -0
- package/src/lang/types.ts +14 -59
- package/src/md5.ts +319 -0
- package/src/url.ts +48 -55
- package/src/validators/array.ts +62 -0
- package/src/validators/base.ts +49 -0
- package/src/validators/boolean.ts +24 -0
- package/src/validators/factories.ts +47 -0
- package/src/validators/index.ts +9 -0
- package/src/validators/number.ts +43 -0
- package/src/validators/object.ts +70 -0
- package/src/validators/string.ts +55 -0
- package/url.d.ts +31 -15
- package/url.js +22 -28
- package/validators/array.d.ts +20 -0
- package/validators/array.js +44 -0
- package/validators/base.d.ts +26 -0
- package/validators/base.js +28 -0
- package/validators/boolean.d.ts +4 -0
- package/validators/boolean.js +28 -0
- package/validators/factories.d.ts +18 -0
- package/validators/factories.js +41 -0
- package/validators/index.d.ts +7 -0
- package/validators/index.js +7 -0
- package/validators/number.d.ts +15 -0
- package/validators/number.js +32 -0
- package/validators/object.d.ts +20 -0
- package/validators/object.js +52 -0
- package/validators/string.d.ts +17 -0
- package/validators/string.js +35 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { success, failed, type MaySuccess } from '../lang/index.js'
|
|
2
|
+
|
|
3
|
+
export interface BaseOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 是否允许 null 值
|
|
6
|
+
* @default false
|
|
7
|
+
*/
|
|
8
|
+
null: boolean
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 字段是否必须有值(不能是 undefined)
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
required: boolean
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 默认值,字段无值(或值为 undefined)时生效,值为 null 不会生效。
|
|
18
|
+
* 指定后 required 选项将失去作用。
|
|
19
|
+
*/
|
|
20
|
+
defaults: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Validator<ExtraOptions = unknown> {
|
|
24
|
+
readonly options: BaseOptions & ExtraOptions
|
|
25
|
+
|
|
26
|
+
constructor(options: Partial<BaseOptions> & ExtraOptions) {
|
|
27
|
+
this.options = {
|
|
28
|
+
null: false,
|
|
29
|
+
required: true,
|
|
30
|
+
defaults: undefined,
|
|
31
|
+
...options,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 各子类继承此方法补充验证逻辑
|
|
37
|
+
*/
|
|
38
|
+
validate(field: string, value: unknown): MaySuccess<unknown> {
|
|
39
|
+
if (typeof value === 'undefined') {
|
|
40
|
+
if (typeof this.options.defaults !== 'undefined') {
|
|
41
|
+
value = this.options.defaults
|
|
42
|
+
} else if (this.options.required) {
|
|
43
|
+
return failed(`${field} is required`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value === null && !this.options.null) return failed(`${field} cannot be null`)
|
|
47
|
+
return success(value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js'
|
|
2
|
+
import { Validator } from './base.js'
|
|
3
|
+
|
|
4
|
+
export class BooleanValidator extends Validator {
|
|
5
|
+
validate(field: string, value: unknown) {
|
|
6
|
+
const superResult = super.validate(field, value)
|
|
7
|
+
if (!superResult.success) return superResult
|
|
8
|
+
|
|
9
|
+
value = superResult.data
|
|
10
|
+
if (value === null || value === undefined) return superResult
|
|
11
|
+
|
|
12
|
+
if (typeof value === 'string') {
|
|
13
|
+
const str = value.trim().toLowerCase()
|
|
14
|
+
if (['1', 'true', 'on', 'yes'].includes(str)) value = true
|
|
15
|
+
else if (['0', 'false', 'off', 'no'].includes(str)) value = false
|
|
16
|
+
} else if (typeof value === 'number') {
|
|
17
|
+
if (value === 1) value = true
|
|
18
|
+
else if (value === 0) value = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value !== 'boolean') return failed(`${field} must be true or false`)
|
|
22
|
+
return success(value)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创建 validator 的快捷方式
|
|
3
|
+
*/
|
|
4
|
+
import { ArrayValidator, type ArrayOptions } from './array.js'
|
|
5
|
+
import { Validator, type BaseOptions } from './base.js'
|
|
6
|
+
import { BooleanValidator } from './boolean.js'
|
|
7
|
+
import { NumberValidator } from './number.js'
|
|
8
|
+
import { ObjectValidator, type RecordOptions } from './object.js'
|
|
9
|
+
import { StringValidator } from './string.js'
|
|
10
|
+
|
|
11
|
+
/** 仅进行基本检查(如检查空值),不检查具体格式 */
|
|
12
|
+
export function any(options?: Partial<BaseOptions>) {
|
|
13
|
+
return new Validator(options ?? {})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function string(...args: ConstructorParameters<typeof StringValidator>) {
|
|
17
|
+
return new StringValidator(...args)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function number(...args: ConstructorParameters<typeof NumberValidator>) {
|
|
21
|
+
return new NumberValidator(...args)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function boolean(options: Partial<BaseOptions> = {}) {
|
|
25
|
+
return new BooleanValidator(options)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function array(options: Validator | (ArrayOptions & Partial<BaseOptions>)) {
|
|
29
|
+
if (options instanceof Validator) options = { item: options }
|
|
30
|
+
return new ArrayValidator(options)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function tuple(validators: Validator[], baseOptions?: Partial<BaseOptions>) {
|
|
34
|
+
return new ArrayValidator({
|
|
35
|
+
...(baseOptions ?? {}),
|
|
36
|
+
tuple: validators,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function struct(validators: Record<string, Validator>, options: Partial<BaseOptions> = {}) {
|
|
41
|
+
return new ObjectValidator({ ...options, struct: validators })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function record(options: Validator | (RecordOptions & Partial<BaseOptions>)) {
|
|
45
|
+
if (options instanceof Validator) options = { record: options }
|
|
46
|
+
return new ObjectValidator(options)
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js'
|
|
2
|
+
import { type BaseOptions, Validator } from './base.js'
|
|
3
|
+
|
|
4
|
+
export interface NumberOptions {
|
|
5
|
+
/** 数值最小值 */
|
|
6
|
+
min?: number
|
|
7
|
+
/** 数值最大值 */
|
|
8
|
+
max?: number
|
|
9
|
+
/** 是否允许小数 @default false */
|
|
10
|
+
float: boolean
|
|
11
|
+
/** 指定可选值 */
|
|
12
|
+
enum?: number[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class NumberValidator extends Validator<NumberOptions> {
|
|
16
|
+
constructor(options: Partial<BaseOptions & NumberOptions> = {}) {
|
|
17
|
+
super({
|
|
18
|
+
float: false,
|
|
19
|
+
...options,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
validate(field: string, value: unknown) {
|
|
24
|
+
const superResult = super.validate(field, value)
|
|
25
|
+
if (!superResult.success) return superResult
|
|
26
|
+
|
|
27
|
+
value = superResult.data
|
|
28
|
+
if (value === null || value === undefined) return superResult
|
|
29
|
+
const opt = this.options
|
|
30
|
+
|
|
31
|
+
if (typeof value === 'string') value = parseFloat(value)
|
|
32
|
+
|
|
33
|
+
if (typeof value !== 'number' || !isFinite(value))
|
|
34
|
+
return failed(`${field} must be a valid number`)
|
|
35
|
+
if (opt.enum !== undefined && !opt.enum.includes(value))
|
|
36
|
+
return failed(`${field} can only be one of ${opt.enum.join(', ')}.`)
|
|
37
|
+
if (!opt.float && value % 1 !== 0) return failed(`${field} must be a integer`)
|
|
38
|
+
if (typeof opt.min === 'number' && value < opt.min) return failed(`${field} must >= ${opt.min}`)
|
|
39
|
+
if (typeof opt.max === 'number' && value > opt.max) return failed(`${field} must <= ${opt.max}`)
|
|
40
|
+
|
|
41
|
+
return success(value)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import isPlainObject from 'lodash/isPlainObject.js'
|
|
2
|
+
import { success, failed } from '../lang/index.js'
|
|
3
|
+
import { Validator } from './base.js'
|
|
4
|
+
|
|
5
|
+
/** 验证有明确键值对结构的对象 */
|
|
6
|
+
export type StructOptions = {
|
|
7
|
+
/** 定义对象结构,及各个值的验证规则 */
|
|
8
|
+
struct: Record<string, Validator>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 验证有任意多个 key,但值的类型固定的对象
|
|
13
|
+
*/
|
|
14
|
+
export type RecordOptions = {
|
|
15
|
+
/** 验证单个值 */
|
|
16
|
+
record: Validator
|
|
17
|
+
/** 对象至少要有几项 */
|
|
18
|
+
min?: number
|
|
19
|
+
/** 对象最多有几项 */
|
|
20
|
+
max?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ObjectValidator extends Validator<StructOptions | RecordOptions> {
|
|
24
|
+
validate(field: string, value: unknown) {
|
|
25
|
+
const superResult = super.validate(field, value)
|
|
26
|
+
if (!superResult.success) return superResult
|
|
27
|
+
|
|
28
|
+
value = superResult.data
|
|
29
|
+
if (value === null || value === undefined) return superResult
|
|
30
|
+
const opt = this.options
|
|
31
|
+
|
|
32
|
+
if (!isPlainObject(value)) return failed(`${field} should be a plain object`)
|
|
33
|
+
|
|
34
|
+
const formatted: Record<string, unknown> = {}
|
|
35
|
+
if ('struct' in opt) {
|
|
36
|
+
for (const [key, itemValidator] of Object.entries(opt.struct)) {
|
|
37
|
+
const itemResult = itemValidator.validate(
|
|
38
|
+
`${field}["${key}"]`,
|
|
39
|
+
(value as Record<string, unknown>)[key]
|
|
40
|
+
)
|
|
41
|
+
if (itemResult.success) {
|
|
42
|
+
if (itemResult.data !== undefined) formatted[key] = itemResult.data
|
|
43
|
+
} else {
|
|
44
|
+
return itemResult
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
for (const [key, itemValue] of Object.entries(value as Record<string, unknown>)) {
|
|
49
|
+
// record 场景下,值为 undefined 的项目视为不存在,不保留在验证结果里,
|
|
50
|
+
// 不然一些因为不想赋值而填充了 undefined 值的项目可能意外触发验证失败,或意外得到了默认值。
|
|
51
|
+
// (因此 validator 的 required 选项和 defaults 选项也没有意义了)
|
|
52
|
+
if (itemValue === undefined) continue
|
|
53
|
+
|
|
54
|
+
const itemResult = opt.record.validate(`${field}["${key}"]`, itemValue)
|
|
55
|
+
if (itemResult.success) {
|
|
56
|
+
if (itemResult.data !== undefined) formatted[key] = itemResult.data
|
|
57
|
+
} else {
|
|
58
|
+
return itemResult
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const length = Object.keys(formatted).length
|
|
63
|
+
if (typeof opt.min === 'number' && length < opt.min)
|
|
64
|
+
return failed(`size of ${field} should >= ${opt.min}`)
|
|
65
|
+
if (typeof opt.max === 'number' && length > opt.max)
|
|
66
|
+
return failed(`size of ${field} should <= ${opt.max}`)
|
|
67
|
+
}
|
|
68
|
+
return success(formatted)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js'
|
|
2
|
+
import { type BaseOptions, Validator } from './base.js'
|
|
3
|
+
|
|
4
|
+
export interface StringOptions {
|
|
5
|
+
/** 字符串最小长度。defaults='' 时默认为 0,否则默认为 1 */
|
|
6
|
+
min: number
|
|
7
|
+
/** 字符串最大长度。 */
|
|
8
|
+
max?: number
|
|
9
|
+
/** 字符串需匹配此正则 */
|
|
10
|
+
pattern?: RegExp
|
|
11
|
+
/** 指定一个数组或 TypeScript enum,字段值必须在此 enum 之中 */
|
|
12
|
+
enum?: string[] | Record<string, string>
|
|
13
|
+
/** 验证之前,是否先清除两侧空白字符(默认开启) */
|
|
14
|
+
trim: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class StringValidator extends Validator<StringOptions> {
|
|
18
|
+
constructor(options: Partial<BaseOptions & StringOptions> = {}) {
|
|
19
|
+
super({
|
|
20
|
+
min: options.defaults === '' ? 0 : 1,
|
|
21
|
+
trim: true,
|
|
22
|
+
...options,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validate(field: string, value: unknown) {
|
|
27
|
+
const opt = this.options
|
|
28
|
+
|
|
29
|
+
const superResult = super.validate(field, value)
|
|
30
|
+
if (!superResult.success) return superResult
|
|
31
|
+
|
|
32
|
+
value = superResult.data
|
|
33
|
+
if (value === null || value === undefined) return superResult
|
|
34
|
+
if (typeof value !== 'string') return failed(`${field} must be a string`)
|
|
35
|
+
|
|
36
|
+
const formatted = opt.trim ? value.trim() : value
|
|
37
|
+
|
|
38
|
+
if (typeof opt.min === 'number' && formatted.length < opt.min)
|
|
39
|
+
return failed(`${field}'s length must >= ${opt.min}`)
|
|
40
|
+
|
|
41
|
+
if (typeof opt.max === 'number' && formatted.length > opt.max)
|
|
42
|
+
return failed(`${field}'s length must <= ${opt.max}`)
|
|
43
|
+
|
|
44
|
+
if (opt.pattern && !opt.pattern.exec(formatted))
|
|
45
|
+
return failed(`${field} does not match the pattern.`)
|
|
46
|
+
|
|
47
|
+
if (opt.enum !== undefined) {
|
|
48
|
+
const validValues = Array.isArray(opt.enum) ? opt.enum : Object.values(opt.enum)
|
|
49
|
+
if (!validValues.includes(formatted))
|
|
50
|
+
return failed(`${field} can only be one of ${validValues.join(', ')}.`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return success(formatted)
|
|
54
|
+
}
|
|
55
|
+
}
|
package/url.d.ts
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 URL 中解析出 query 对象
|
|
3
|
+
* 注意:不带 ? 号的纯 query 内容需手动加上 ? 再传入。
|
|
4
|
+
*
|
|
5
|
+
* [array]
|
|
6
|
+
* 是否把重复出现的 key 保存为数组(默认不开启)
|
|
7
|
+
* a=1&a=2 => { a: [1,2] }
|
|
8
|
+
*
|
|
9
|
+
* [loose]
|
|
10
|
+
* 是否开启“宽松模式”(默认不开启)
|
|
11
|
+
* 1. hash 里的内容也会被解析,以兼容拼接错误的 URL(把 query 拼到了 hash 后面)。
|
|
12
|
+
* 2. 出现多个 ? 符号时,会把 ? 也当做 & 分隔符(index.html?a=1&b=2?c=3)
|
|
13
|
+
*
|
|
14
|
+
* [decode]
|
|
15
|
+
* 是否对 query 值进行 decode(默认开启)
|
|
16
|
+
*/
|
|
1
17
|
declare function parseQuery(url: string, options?: {
|
|
2
18
|
array?: false;
|
|
3
|
-
|
|
19
|
+
loose?: boolean;
|
|
20
|
+
decode?: boolean;
|
|
4
21
|
}): Record<string, string>;
|
|
5
22
|
declare function parseQuery(url: string, options: {
|
|
6
23
|
array: true;
|
|
7
|
-
|
|
24
|
+
loose?: boolean;
|
|
25
|
+
decode?: boolean;
|
|
8
26
|
}): Record<string, string | string[]>;
|
|
9
27
|
export { parseQuery };
|
|
10
|
-
/**
|
|
11
|
-
* 取 query 中指定参数的值
|
|
12
|
-
* - 参数存在,返回参数值(可能是空字符串);不存在返回 null
|
|
13
|
-
* - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
|
|
14
|
-
* - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
|
|
15
|
-
*/
|
|
16
|
-
export declare function getQueryParam(name: string, url: string): string | null;
|
|
17
28
|
/**
|
|
18
29
|
* 把对象合并成 query string。
|
|
19
|
-
*
|
|
30
|
+
* - 支持字符串、数值、布尔值、数组。
|
|
31
|
+
* - 布尔值会替换成 0 和 1。
|
|
32
|
+
* - 数组会多次赋值:{ a: [1,2,3] } => 'a=1&a=2&a=3',不支持嵌套数组
|
|
33
|
+
* - encode 为 true 时会对 value 执行 encodeURIComponent(默认为 true)
|
|
20
34
|
*/
|
|
21
35
|
type StringifyVal = string | number | boolean;
|
|
22
|
-
|
|
36
|
+
type StringifyQuery = {
|
|
23
37
|
[key: string]: StringifyVal | StringifyVal[] | undefined;
|
|
24
|
-
}
|
|
38
|
+
};
|
|
39
|
+
export declare function stringifyQuery(obj: StringifyQuery, encode?: boolean): string;
|
|
25
40
|
/**
|
|
26
41
|
* 拆分 URL 的各个部分
|
|
27
42
|
*
|
|
28
43
|
* bare 为 true,则 search 不带 '?',hash 不带 '#'
|
|
29
44
|
* 否则和 location.search / hash 一样
|
|
45
|
+
* (默认为 true)
|
|
30
46
|
*/
|
|
31
47
|
export declare function splitUrl(url: string, bare?: boolean): {
|
|
32
48
|
base: string;
|
|
@@ -34,12 +50,12 @@ export declare function splitUrl(url: string, bare?: boolean): {
|
|
|
34
50
|
hash: string;
|
|
35
51
|
};
|
|
36
52
|
/**
|
|
37
|
-
*
|
|
53
|
+
* 把 query 和 hash 内容合并到 url 上
|
|
38
54
|
*
|
|
39
55
|
* query object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
|
|
40
|
-
* hash string
|
|
56
|
+
* hash string 带不带开头的 '#' 皆可。会代替 url 已有的 hash。
|
|
41
57
|
*/
|
|
42
|
-
export declare function combineUrl(origUrl: string, query?:
|
|
58
|
+
export declare function combineUrl(origUrl: string, query?: StringifyQuery, hash?: string): string;
|
|
43
59
|
/**
|
|
44
60
|
* decodeURIComponent() 对于不规范编码的字符串可能会报错(例如字符串里出现了“%”)
|
|
45
61
|
* 用此函数代替可避免此问题
|
package/url.js
CHANGED
|
@@ -9,23 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import isPlainObject from 'lodash/isPlainObject.js';
|
|
11
11
|
function parseQuery(url, options) {
|
|
12
|
-
|
|
12
|
+
const { array = false, loose = false, decode = true } = options ?? {};
|
|
13
|
+
// 正常状态下,将仅剩 a=1&b=1(即不会再有 ? 和 #);loose 模型下,可能为 a=1&b=2#c=3?d=4
|
|
14
|
+
const queryString = (loose ? /(\?|#)(.+)/ : /(\?)(.+?)(#|$)/).exec(url)?.[2] ?? '';
|
|
15
|
+
if (!queryString)
|
|
13
16
|
return {};
|
|
14
|
-
const { array = false, strict = false } = options ?? {};
|
|
15
|
-
const queryString = strict
|
|
16
|
-
? (/^[^?#]*\?(.+?)(\?|#|$)/.exec(url) ?? ['', ''])[1] // 排除 hash、重复的 ? 符号
|
|
17
|
-
: url;
|
|
18
17
|
const query = {};
|
|
19
18
|
const reg = /([^#?&]*)=([^#?&]*)/g;
|
|
20
19
|
let re = reg.exec(queryString);
|
|
21
20
|
while (re) {
|
|
22
|
-
const [name,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
else
|
|
28
|
-
query[realName] = [value];
|
|
21
|
+
const [name, rawValue] = [re[1], re[2]];
|
|
22
|
+
const value = decode ? safeDecode(rawValue) : rawValue;
|
|
23
|
+
if (array && query[name] !== undefined) {
|
|
24
|
+
const prev = query[name];
|
|
25
|
+
query[name] = Array.isArray(prev) ? [...prev, value] : [prev, value];
|
|
29
26
|
}
|
|
30
27
|
else {
|
|
31
28
|
query[name] = value;
|
|
@@ -35,28 +32,24 @@ function parseQuery(url, options) {
|
|
|
35
32
|
return query;
|
|
36
33
|
}
|
|
37
34
|
export { parseQuery };
|
|
38
|
-
|
|
39
|
-
* 取 query 中指定参数的值
|
|
40
|
-
* - 参数存在,返回参数值(可能是空字符串);不存在返回 null
|
|
41
|
-
* - 解析 query 固定基于 parseQuery() 的 { array: false, strict: false } 规则
|
|
42
|
-
* - 和 parseQuery() 一样,url 可以根据需要传 location.href/search/hash
|
|
43
|
-
*/
|
|
44
|
-
export function getQueryParam(name, url) {
|
|
45
|
-
const query = parseQuery(url);
|
|
46
|
-
return typeof query[name] === 'string' ? query[name] : null;
|
|
47
|
-
}
|
|
48
|
-
export function stringifyQuery(obj) {
|
|
35
|
+
export function stringifyQuery(obj, encode = true) {
|
|
49
36
|
if (!isPlainObject(obj))
|
|
50
37
|
return '';
|
|
51
38
|
return (Object.entries(obj)
|
|
52
39
|
// 过滤值为 undefined 的项目,使其完全不出现在最终的 query 中
|
|
53
40
|
.filter((entry) => entry[1] !== undefined)
|
|
54
|
-
.map(([name, value]) => stringifyQueryItem(name, value))
|
|
41
|
+
.map(([name, value]) => stringifyQueryItem(name, value, encode))
|
|
55
42
|
.join('&'));
|
|
56
43
|
}
|
|
57
|
-
function stringifyQueryItem(name, value) {
|
|
44
|
+
function stringifyQueryItem(name, value, encode) {
|
|
58
45
|
if (Array.isArray(value))
|
|
59
|
-
return value.map(subValue => stringifyQueryItem(
|
|
46
|
+
return value.map(subValue => stringifyQueryItem(name, subValue, encode)).join('&');
|
|
47
|
+
if (typeof value === 'boolean')
|
|
48
|
+
value = value ? '1' : '0';
|
|
49
|
+
if (typeof value === 'number')
|
|
50
|
+
value = value.toString();
|
|
51
|
+
if (encode)
|
|
52
|
+
value = encodeURIComponent(value);
|
|
60
53
|
return `${name}=${value}`;
|
|
61
54
|
}
|
|
62
55
|
/**
|
|
@@ -64,6 +57,7 @@ function stringifyQueryItem(name, value) {
|
|
|
64
57
|
*
|
|
65
58
|
* bare 为 true,则 search 不带 '?',hash 不带 '#'
|
|
66
59
|
* 否则和 location.search / hash 一样
|
|
60
|
+
* (默认为 true)
|
|
67
61
|
*/
|
|
68
62
|
export function splitUrl(url, bare = true) {
|
|
69
63
|
let hashIndex = url.indexOf('#');
|
|
@@ -81,10 +75,10 @@ export function splitUrl(url, bare = true) {
|
|
|
81
75
|
};
|
|
82
76
|
}
|
|
83
77
|
/**
|
|
84
|
-
*
|
|
78
|
+
* 把 query 和 hash 内容合并到 url 上
|
|
85
79
|
*
|
|
86
80
|
* query object 与现有 search 合并,替换同名项(值为数组的,用新数组代替老的,不会合并数组)
|
|
87
|
-
* hash string
|
|
81
|
+
* hash string 带不带开头的 '#' 皆可。会代替 url 已有的 hash。
|
|
88
82
|
*/
|
|
89
83
|
export function combineUrl(origUrl, query = {}, hash = '') {
|
|
90
84
|
if (hash.startsWith('#'))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Validator } from './base.js';
|
|
2
|
+
/** 验证元素数量任意、元素类型相同的数组 */
|
|
3
|
+
export type ArrayOptions = {
|
|
4
|
+
/** 验证数组各元素 */
|
|
5
|
+
item: Validator;
|
|
6
|
+
/** 数组最小长度 */
|
|
7
|
+
min?: number;
|
|
8
|
+
/** 数组最大长度 */
|
|
9
|
+
max?: number;
|
|
10
|
+
/** 是否对数组元素进行去重 @defaults false */
|
|
11
|
+
unique?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/** 验证元素数量固定、类型可以不同的数组 */
|
|
14
|
+
export type TupleOptions = {
|
|
15
|
+
/** 验证数组各元素(validator 与元素一一对应) */
|
|
16
|
+
tuple: Validator[];
|
|
17
|
+
};
|
|
18
|
+
export declare class ArrayValidator extends Validator<ArrayOptions | TupleOptions> {
|
|
19
|
+
validate(field: string, value: unknown): import("../lang/may-success.js").Failed<void> | import("../lang/may-success.js").Success<unknown>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js';
|
|
2
|
+
import { Validator } from './base.js';
|
|
3
|
+
export class ArrayValidator extends Validator {
|
|
4
|
+
validate(field, value) {
|
|
5
|
+
const superResult = super.validate(field, value);
|
|
6
|
+
if (!superResult.success)
|
|
7
|
+
return superResult;
|
|
8
|
+
value = superResult.data;
|
|
9
|
+
if (value === null || value === undefined)
|
|
10
|
+
return superResult;
|
|
11
|
+
const opt = this.options;
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return failed(`${field} should be an array`);
|
|
14
|
+
let formatted = [];
|
|
15
|
+
if ('item' in opt) {
|
|
16
|
+
if (typeof opt.min === 'number' && value.length < opt.min)
|
|
17
|
+
return failed(`array ${field}'s length should >= ${opt.min}`);
|
|
18
|
+
if (typeof opt.max === 'number' && value.length > opt.max)
|
|
19
|
+
return failed(`array ${field}'s length should <= ${opt.max}`);
|
|
20
|
+
for (let i = 0; i < value.length; i++) {
|
|
21
|
+
const itemResult = opt.item.validate(`${field}[${i}]`, value[i]);
|
|
22
|
+
if (itemResult.success)
|
|
23
|
+
formatted.push(itemResult.data);
|
|
24
|
+
else
|
|
25
|
+
return itemResult;
|
|
26
|
+
}
|
|
27
|
+
if (opt.unique === true)
|
|
28
|
+
formatted = [...new Set(formatted)];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (value.length > opt.tuple.length)
|
|
32
|
+
return failed(`${field} should be a tuple with ${opt.tuple.length} items`);
|
|
33
|
+
// 这种情况不能遍历 value,因为它的长度可能小于 opt.tuple
|
|
34
|
+
for (let i = 0; i < opt.tuple.length; i++) {
|
|
35
|
+
const itemResult = opt.tuple[i].validate(`${field}[${i}]`, value[i]);
|
|
36
|
+
if (itemResult.success)
|
|
37
|
+
formatted.push(itemResult.data);
|
|
38
|
+
else
|
|
39
|
+
return itemResult;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return success(formatted);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type MaySuccess } from '../lang/index.js';
|
|
2
|
+
export interface BaseOptions {
|
|
3
|
+
/**
|
|
4
|
+
* 是否允许 null 值
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
null: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 字段是否必须有值(不能是 undefined)
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
required: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* 默认值,字段无值(或值为 undefined)时生效,值为 null 不会生效。
|
|
15
|
+
* 指定后 required 选项将失去作用。
|
|
16
|
+
*/
|
|
17
|
+
defaults: unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare class Validator<ExtraOptions = unknown> {
|
|
20
|
+
readonly options: BaseOptions & ExtraOptions;
|
|
21
|
+
constructor(options: Partial<BaseOptions> & ExtraOptions);
|
|
22
|
+
/**
|
|
23
|
+
* 各子类继承此方法补充验证逻辑
|
|
24
|
+
*/
|
|
25
|
+
validate(field: string, value: unknown): MaySuccess<unknown>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js';
|
|
2
|
+
export class Validator {
|
|
3
|
+
options;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.options = {
|
|
6
|
+
null: false,
|
|
7
|
+
required: true,
|
|
8
|
+
defaults: undefined,
|
|
9
|
+
...options,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 各子类继承此方法补充验证逻辑
|
|
14
|
+
*/
|
|
15
|
+
validate(field, value) {
|
|
16
|
+
if (typeof value === 'undefined') {
|
|
17
|
+
if (typeof this.options.defaults !== 'undefined') {
|
|
18
|
+
value = this.options.defaults;
|
|
19
|
+
}
|
|
20
|
+
else if (this.options.required) {
|
|
21
|
+
return failed(`${field} is required`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (value === null && !this.options.null)
|
|
25
|
+
return failed(`${field} cannot be null`);
|
|
26
|
+
return success(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { success, failed } from '../lang/index.js';
|
|
2
|
+
import { Validator } from './base.js';
|
|
3
|
+
export class BooleanValidator extends Validator {
|
|
4
|
+
validate(field, value) {
|
|
5
|
+
const superResult = super.validate(field, value);
|
|
6
|
+
if (!superResult.success)
|
|
7
|
+
return superResult;
|
|
8
|
+
value = superResult.data;
|
|
9
|
+
if (value === null || value === undefined)
|
|
10
|
+
return superResult;
|
|
11
|
+
if (typeof value === 'string') {
|
|
12
|
+
const str = value.trim().toLowerCase();
|
|
13
|
+
if (['1', 'true', 'on', 'yes'].includes(str))
|
|
14
|
+
value = true;
|
|
15
|
+
else if (['0', 'false', 'off', 'no'].includes(str))
|
|
16
|
+
value = false;
|
|
17
|
+
}
|
|
18
|
+
else if (typeof value === 'number') {
|
|
19
|
+
if (value === 1)
|
|
20
|
+
value = true;
|
|
21
|
+
else if (value === 0)
|
|
22
|
+
value = false;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value !== 'boolean')
|
|
25
|
+
return failed(`${field} must be true or false`);
|
|
26
|
+
return success(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创建 validator 的快捷方式
|
|
3
|
+
*/
|
|
4
|
+
import { ArrayValidator, type ArrayOptions } from './array.js';
|
|
5
|
+
import { Validator, type BaseOptions } from './base.js';
|
|
6
|
+
import { BooleanValidator } from './boolean.js';
|
|
7
|
+
import { NumberValidator } from './number.js';
|
|
8
|
+
import { ObjectValidator, type RecordOptions } from './object.js';
|
|
9
|
+
import { StringValidator } from './string.js';
|
|
10
|
+
/** 仅进行基本检查(如检查空值),不检查具体格式 */
|
|
11
|
+
export declare function any(options?: Partial<BaseOptions>): Validator<unknown>;
|
|
12
|
+
export declare function string(...args: ConstructorParameters<typeof StringValidator>): StringValidator;
|
|
13
|
+
export declare function number(...args: ConstructorParameters<typeof NumberValidator>): NumberValidator;
|
|
14
|
+
export declare function boolean(options?: Partial<BaseOptions>): BooleanValidator;
|
|
15
|
+
export declare function array(options: Validator | (ArrayOptions & Partial<BaseOptions>)): ArrayValidator;
|
|
16
|
+
export declare function tuple(validators: Validator[], baseOptions?: Partial<BaseOptions>): ArrayValidator;
|
|
17
|
+
export declare function struct(validators: Record<string, Validator>, options?: Partial<BaseOptions>): ObjectValidator;
|
|
18
|
+
export declare function record(options: Validator | (RecordOptions & Partial<BaseOptions>)): ObjectValidator;
|