@halooj/framework 0.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.
@@ -0,0 +1,46 @@
1
+ import { expect } from 'chai';
2
+ import { describe, it } from 'node:test';
3
+ import { projection } from '../api';
4
+
5
+ const input = {
6
+ foo: 1,
7
+ bar: 'string',
8
+ arr: [
9
+ { sub: 1, another: 'key' },
10
+ { sub: 2 },
11
+ ],
12
+ obj: {
13
+ sub: 3,
14
+ },
15
+ };
16
+
17
+ describe('projection', () => {
18
+ it('pick', () => {
19
+ expect(projection(input, { foo: 1, bar: 1 })).to.deep.equal({ foo: 1, bar: 'string' });
20
+ });
21
+ it('array', () => {
22
+ expect(projection(input, { arr: { sub: 1 } })).to.deep.equal({ arr: [{ sub: 1 }, { sub: 2 }] });
23
+ });
24
+ it('non-exist', () => {
25
+ expect(projection(input, { 'non-exist': 1 })).to.deep.equal({});
26
+ });
27
+ it('partial', () => {
28
+ expect(projection(input, { arr: { sub: 1, another: 1 } })).to.deep.equal({ arr: [{ sub: 1, another: 'key' }, { sub: 2 }] });
29
+ });
30
+ it('object', () => {
31
+ expect(projection(input, { obj: { sub: 1 } })).to.deep.equal({ obj: { sub: 3 } });
32
+ });
33
+ });
34
+ describe('safety', () => {
35
+ it('prototype', () => {
36
+ // eslint-disable-next-line
37
+ expect(projection({}, { __proto__: 1 })).to.deep.equal({});
38
+ expect(projection({}, { prototype: 1 })).to.deep.equal({});
39
+ });
40
+ it('circular', () => {
41
+ const circular = {};
42
+ // @ts-ignore
43
+ circular.circular = circular;
44
+ expect(projection(circular, { circular: 1 })).to.deep.equal({ circular });
45
+ });
46
+ });
@@ -0,0 +1,20 @@
1
+ import { expect } from 'chai';
2
+ import { describe, it } from 'node:test';
3
+ import { Types } from '../validator';
4
+
5
+ const k = Symbol.for('schemastery');
6
+
7
+ describe('validator', () => {
8
+ it('NumericArray', () => {
9
+ expect(Types.NumericArray('1,2,3')).to.deep.equal([1, 2, 3]);
10
+ expect(Types.NumericArray([1, 2, 3])).to.deep.equal([1, 2, 3]);
11
+ expect(() => Types.NumericArray('123a')).to.throw();
12
+ });
13
+
14
+ it('CommaSeperatedArray', () => {
15
+ expect(Types.CommaSeperatedArray('1,2,3')).to.deep.equal(['1', '2', '3']);
16
+ expect(Types.CommaSeperatedArray([1, 2, 3])).to.deep.equal(['1', '2', '3']);
17
+ expect(Types.CommaSeperatedArray('123a')).to.deep.equal(['123a']);
18
+ expect(k in Types.CommaSeperatedArray).to.eq(true);
19
+ });
20
+ });
package/tsconfig.json ADDED
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"es2022","lib":["esnext"],"module":"preserve","esModuleInterop":true,"moduleResolution":"bundler","jsx":"react-jsx","sourceMap":false,"composite":true,"strict":false,"strictBindCallApply":true,"resolveJsonModule":true,"experimentalDecorators":true,"incremental":true,"outDir":"/Users/mac/Desktop/HaloTeam/HaloOJ/未命名/.cache/ts-out/framework/framework","rootDir":".","paths":{"vj/*":["../../packages/ui-default/*"]}},"include":["**/*.ts","**/*.tsx"],"exclude":["**/public","**/frontend","**/node_modules","**/bin","**/dist","**/__mocks__"]}
package/validator.ts ADDED
@@ -0,0 +1,204 @@
1
+ import assert from 'assert';
2
+ import saslprep from '@mongodb-js/saslprep';
3
+ import emojiRegex from 'emoji-regex';
4
+ import sanitize from 'sanitize-filename';
5
+ import Schema from 'schemastery';
6
+
7
+ type InputType = string | number | Record<string, any> | any[];
8
+ export type Converter<T> = (value: any) => T;
9
+ export type Validator<Loose extends boolean = true> = (value: Loose extends true ? any : InputType) => boolean;
10
+ export type Type<T> = Schema<T> | readonly [Converter<T>, Validator<false>?, (boolean | 'convert')?];
11
+
12
+ const MaybeArray = <T>(inner: Schema<T>) => Schema.union([Schema.array(inner), inner]);
13
+ type CheckFunction = <IsNumber extends boolean>(v: IsNumber extends true ? number : string) => boolean;
14
+ const ArrayBase = <IsNumber extends boolean>(check: CheckFunction, number: IsNumber, doSplit: boolean = number) => Schema.transform(
15
+ MaybeArray(Schema.union([Number, String])),
16
+ (v) => {
17
+ const input = (doSplit && typeof v === 'string') ? v.split(',') : v;
18
+ const res = number
19
+ ? (typeof input === 'string' ? [+input] : typeof input === 'number' ? [input] : input.map(Number))
20
+ : typeof input === 'string' ? [input] : typeof input === 'number' ? [input.toString()] : input.map(String);
21
+ const locate = number
22
+ ? res.find((i) => !Number.isFinite(+i) || !check(+i as any))
23
+ : res.find((i) => !check(i as any));
24
+ if (locate !== undefined) throw new Error(`Invalid input: ${locate}`);
25
+ return res;
26
+ },
27
+ ) as Schema<any, IsNumber extends true ? number[] : string[]>;
28
+
29
+ export interface Types {
30
+ // String outputs
31
+ Content: Type<string>;
32
+ Key: Type<string>;
33
+ /** @deprecated */
34
+ Name: Type<string>;
35
+ Username: Type<string>;
36
+ Password: Type<string>;
37
+ UidOrName: Type<string>;
38
+ Email: Type<string>;
39
+ Filename: Type<string>;
40
+ DomainId: Type<string>;
41
+ ProblemId: Type<string | number>;
42
+ Role: Type<string>;
43
+ Title: Type<string>;
44
+ Emoji: Type<string>;
45
+ ShortString: Type<string>;
46
+ String: Type<string>;
47
+
48
+ // Number outputs
49
+ Int: Type<number>;
50
+ UnsignedInt: Type<number>;
51
+ PositiveInt: Type<number>;
52
+ Float: Type<number>;
53
+
54
+ // Other
55
+ ObjectId: Type<typeof import('mongodb').ObjectId>;
56
+ Boolean: Type<boolean>;
57
+ Date: Type<string>;
58
+ Time: Type<string>;
59
+ Range: <T extends string | number>(range: Array<T> | Record<string, any>) => Type<T>;
60
+ NumericArray: Type<number[]>;
61
+ CommaSeperatedArray: Type<string[]>;
62
+ Set: Type<Set<any>>;
63
+ Any: Type<any>;
64
+ ArrayOf: <T extends Type<any>>(type: T, isOptional?: boolean) => (T extends Type<infer R> ? Type<R[]> : never);
65
+ AnyOf: <T extends Type<any>>(...type: T[]) => (T extends Type<infer R> ? Type<R> : never);
66
+ }
67
+
68
+ const basicString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
69
+ convert || ((v) => v.toString()),
70
+ (v) => {
71
+ const res = v.toString();
72
+ if (regex && !regex.test(res)) return false;
73
+ if (cb && !cb(res)) return false;
74
+ return !!res.length;
75
+ },
76
+ ] as [(v) => string, (v) => boolean];
77
+ const saslprepString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
78
+ convert || ((v) => saslprep(v.toString().trim())),
79
+ (v) => {
80
+ try {
81
+ const res = saslprep(v.toString().trim());
82
+ if (regex && !regex.test(res)) return false;
83
+ if (cb && !cb(res)) return false;
84
+ return !!res.length;
85
+ } catch (e) {
86
+ return false;
87
+ }
88
+ },
89
+ ] as [(v) => string, (v) => boolean];
90
+
91
+ export const Types = {
92
+ Content: [(v) => v.toString().trim(), (v) => v?.toString()?.trim() && v.toString().trim().length < 65536],
93
+ Key: saslprepString(/^[\w-]{1,255}$/),
94
+ /** @deprecated */
95
+ Name: saslprepString(/^.{1,255}$/),
96
+ Filename: saslprepString(/^[^\\/?#~!|*]{1,255}$/, (i) => sanitize(i) === i),
97
+ UidOrName: saslprepString(/^(?:.{3,31}|[\u4E00-\u9FA5]{2}|-?[0-9]+)$/),
98
+ Username: saslprepString(/^(?:.{3,31}|[\u4E00-\u9FA5]{2})$/),
99
+ Password: basicString(/^.{6,255}$/),
100
+ ProblemId: saslprepString(/^(?:[a-z0-9]{1,10}-)?[a-z0-9]+$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)),
101
+ Email: saslprepString(/^[\w.+-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+$/i),
102
+ DomainId: saslprepString(/^[a-zA-Z]\w{3,31}$/),
103
+ Role: saslprepString(/^[\w\u4E00-\u9FA5]{1,31}$/),
104
+ Title: basicString(/^.{1,64}$/, (i) => !!i.trim()),
105
+ ShortString: basicString(/^.{1,255}$/),
106
+ String: basicString(),
107
+
108
+ Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
109
+ UnsignedInt: [(v) => +v, (v) => /^(?:-0|\+?[0-9]+)$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
110
+ PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && Number.isSafeInteger(+v)],
111
+ Float: [(v) => +v, (v) => Number.isFinite(+v)],
112
+
113
+ ObjectId: [() => { throw new Error('mongodb package not found'); }, () => true],
114
+ Boolean: [(v) => !!(v && !['false', 'off', 'no', '0'].includes(v)), null, true],
115
+ Date: [
116
+ (v) => {
117
+ const d = v.split('-');
118
+ assert(d.length === 3);
119
+ assert(d[0].length === 4);
120
+ return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
121
+ },
122
+ (v) => {
123
+ const d = v.toString().split('-');
124
+ if (d.length !== 3) return false;
125
+ if (d[0].length !== 4) return false;
126
+ const st = `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
127
+ return Number.isFinite(new Date(st).getTime());
128
+ },
129
+ ],
130
+ Time: [
131
+ (v) => {
132
+ const t = v.split(':');
133
+ assert(t.length === 2);
134
+ return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
135
+ },
136
+ (v) => {
137
+ const t = v.toString().split(':');
138
+ if (t.length !== 2) return false;
139
+ const d = new Date(`2020-01-01 ${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`);
140
+ return Number.isFinite(d.getTime());
141
+ },
142
+ ],
143
+ Range: (range) => [
144
+ (v) => {
145
+ if (range instanceof Array) {
146
+ for (const item of range) {
147
+ if (typeof item === 'number' && item === +v) return +v;
148
+ if (item === v) return v;
149
+ }
150
+ }
151
+ return v;
152
+ },
153
+ (v) => {
154
+ if (range instanceof Array) {
155
+ for (const item of range) {
156
+ if (typeof item === 'string' && item === v) return true;
157
+ if (typeof item === 'number' && item === +v) return true;
158
+ }
159
+ } else {
160
+ for (const key in range) {
161
+ if (key === v) return true;
162
+ }
163
+ }
164
+ return false;
165
+ },
166
+ ],
167
+ NumericArray: ArrayBase(Number.isFinite, true),
168
+ CommaSeperatedArray: ArrayBase(() => true, false, true),
169
+ Set: [(v) => {
170
+ if (v instanceof Array) return new Set(v);
171
+ return v ? new Set([v]) : new Set();
172
+ }, null],
173
+ Emoji: [
174
+ (v: string) => v.matchAll(emojiRegex()).next().value[0],
175
+ (v) => emojiRegex().test(v.toString()),
176
+ ],
177
+ Any: [(v) => v, null],
178
+ ArrayOf: (type, isOptional = false) => [
179
+ (v) => {
180
+ const arr = v instanceof Array ? v : [v];
181
+ return arr.map((i) => {
182
+ if (isOptional && [undefined, null, ''].includes(i)) return undefined;
183
+ return type[0](i);
184
+ });
185
+ },
186
+ (v) => {
187
+ if (!type[1]) return true;
188
+ const arr = v instanceof Array ? v : [v];
189
+ return arr.every((i) => {
190
+ if ([undefined, null, ''].includes(i)) return isOptional;
191
+ return type[1](i);
192
+ });
193
+ },
194
+ ] as any,
195
+ AnyOf: (...types) => [
196
+ (v) => types.find((type) => type[1](v))[0](v),
197
+ (v) => types.some((type) => type[1](v)),
198
+ ] as any,
199
+ } satisfies Types;
200
+
201
+ try {
202
+ const { ObjectId } = require('mongodb');
203
+ Types.ObjectId = [((v) => new ObjectId(v)) as any, ObjectId.isValid];
204
+ } catch (e) { }