@loomcore/common 0.0.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.
- package/dist/errors/custom.error.js +6 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/validation.error.js +26 -0
- package/dist/models/address.interface.js +1 -0
- package/dist/models/api-response.interface.js +1 -0
- package/dist/models/auditable.model.js +8 -0
- package/dist/models/entity.model.js +6 -0
- package/dist/models/error.interface.js +1 -0
- package/dist/models/geo-json.interface.js +1 -0
- package/dist/models/index.js +15 -0
- package/dist/models/login-response.model.js +9 -0
- package/dist/models/model-spec.interface.js +1 -0
- package/dist/models/organization.model.js +24 -0
- package/dist/models/paged-result.interface.js +1 -0
- package/dist/models/password-reset-token.interface.js +15 -0
- package/dist/models/query-options.model.js +14 -0
- package/dist/models/token-response.model.js +8 -0
- package/dist/models/user-context.model.js +12 -0
- package/dist/models/user.model.js +35 -0
- package/dist/types/index.js +1 -0
- package/dist/types/sort-direction.type.js +1 -0
- package/dist/utils/entity.utils.js +102 -0
- package/dist/utils/index.js +1 -0
- package/dist/validation/formats/date-time.js +7 -0
- package/dist/validation/formats/date.js +14 -0
- package/dist/validation/formats/email.js +4 -0
- package/dist/validation/formats/objectid.js +3 -0
- package/dist/validation/formats/time.js +20 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/typebox-extensions.js +65 -0
- package/dist/validation/typebox-setup.js +161 -0
- package/package.json +44 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CustomError } from './custom.error.js';
|
|
2
|
+
export class ValidationError extends CustomError {
|
|
3
|
+
statusCode = 400;
|
|
4
|
+
validationError;
|
|
5
|
+
constructor(validationError) {
|
|
6
|
+
if (Array.isArray(validationError)) {
|
|
7
|
+
super(validationError[0]?.message || 'Validation Error');
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
super('Validation Error');
|
|
11
|
+
}
|
|
12
|
+
this.validationError = validationError;
|
|
13
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
14
|
+
}
|
|
15
|
+
serializeErrors() {
|
|
16
|
+
if (Array.isArray(this.validationError)) {
|
|
17
|
+
return this.validationError.map((error) => {
|
|
18
|
+
return {
|
|
19
|
+
message: error.message,
|
|
20
|
+
field: error.path.slice(1)
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return [{ message: 'Validation Error' }];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { TypeboxIsoDate } from '../validation/index.js';
|
|
3
|
+
export const AuditableSchema = Type.Object({
|
|
4
|
+
_created: TypeboxIsoDate(),
|
|
5
|
+
_createdBy: Type.String(),
|
|
6
|
+
_updated: TypeboxIsoDate(),
|
|
7
|
+
_updatedBy: Type.String(),
|
|
8
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './user.model.js';
|
|
2
|
+
export * from './organization.model.js';
|
|
3
|
+
export * from './user-context.model.js';
|
|
4
|
+
export * from './entity.model.js';
|
|
5
|
+
export * from './login-response.model.js';
|
|
6
|
+
export * from './token-response.model.js';
|
|
7
|
+
export * from './auditable.model.js';
|
|
8
|
+
export * from './query-options.model.js';
|
|
9
|
+
export * from './password-reset-token.interface.js';
|
|
10
|
+
export * from './model-spec.interface.js';
|
|
11
|
+
export * from './paged-result.interface.js';
|
|
12
|
+
export * from './geo-json.interface.js';
|
|
13
|
+
export * from './address.interface.js';
|
|
14
|
+
export * from './error.interface.js';
|
|
15
|
+
export * from './api-response.interface.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TokenResponseSchema } from './token-response.model.js';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { UserContextSchema } from './user-context.model.js';
|
|
4
|
+
import { entityUtils } from '../utils/entity.utils.js';
|
|
5
|
+
export const LoginResponseSchema = Type.Object({
|
|
6
|
+
tokens: TokenResponseSchema,
|
|
7
|
+
userContext: UserContextSchema
|
|
8
|
+
});
|
|
9
|
+
export const LoginResponseSpec = entityUtils.getModelSpec(LoginResponseSchema);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { entityUtils } from '../utils/index.js';
|
|
3
|
+
export const OrganizationSchema = Type.Object({
|
|
4
|
+
name: Type.String({
|
|
5
|
+
title: 'Name'
|
|
6
|
+
}),
|
|
7
|
+
code: Type.String({
|
|
8
|
+
title: 'Code'
|
|
9
|
+
}),
|
|
10
|
+
description: Type.Optional(Type.String({
|
|
11
|
+
title: 'Description'
|
|
12
|
+
})),
|
|
13
|
+
status: Type.Number({
|
|
14
|
+
title: 'Status'
|
|
15
|
+
}),
|
|
16
|
+
isMetaOrg: Type.Boolean({
|
|
17
|
+
title: 'Is Meta Organization',
|
|
18
|
+
default: false
|
|
19
|
+
}),
|
|
20
|
+
authToken: Type.Optional(Type.String({
|
|
21
|
+
title: 'Authentication Token'
|
|
22
|
+
}))
|
|
23
|
+
});
|
|
24
|
+
export const OrganizationSpec = entityUtils.getModelSpec(OrganizationSchema, { isAuditable: true });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { entityUtils } from '../utils/index.js';
|
|
3
|
+
export const PasswordResetTokenSchema = Type.Object({
|
|
4
|
+
email: Type.String({
|
|
5
|
+
title: 'Email',
|
|
6
|
+
format: 'email'
|
|
7
|
+
}),
|
|
8
|
+
token: Type.String({
|
|
9
|
+
title: 'Token'
|
|
10
|
+
}),
|
|
11
|
+
expiresOn: Type.Number({
|
|
12
|
+
title: 'Expires On'
|
|
13
|
+
})
|
|
14
|
+
});
|
|
15
|
+
export const PasswordResetTokenSpec = entityUtils.getModelSpec(PasswordResetTokenSchema, { isAuditable: true });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class QueryOptions {
|
|
2
|
+
orderBy;
|
|
3
|
+
sortDirection;
|
|
4
|
+
page;
|
|
5
|
+
pageSize;
|
|
6
|
+
filters;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.orderBy = options.orderBy;
|
|
9
|
+
this.sortDirection = options.sortDirection ?? 'asc';
|
|
10
|
+
this.page = options.page ?? 1;
|
|
11
|
+
this.pageSize = options.pageSize ?? 10;
|
|
12
|
+
this.filters = options.filters;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { entityUtils } from '../utils/entity.utils.js';
|
|
3
|
+
export const TokenResponseSchema = Type.Object({
|
|
4
|
+
accessToken: Type.String(),
|
|
5
|
+
refreshToken: Type.String(),
|
|
6
|
+
expiresOn: Type.Number()
|
|
7
|
+
});
|
|
8
|
+
export const TokenResponseSpec = entityUtils.getModelSpec(TokenResponseSchema);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { PublicUserSchema } from './user.model.js';
|
|
3
|
+
import { entityUtils } from '../utils/entity.utils.js';
|
|
4
|
+
export const EmptyUserContext = {
|
|
5
|
+
user: {},
|
|
6
|
+
_orgId: undefined
|
|
7
|
+
};
|
|
8
|
+
export const UserContextSchema = Type.Object({
|
|
9
|
+
user: PublicUserSchema,
|
|
10
|
+
_orgId: Type.Optional(Type.String())
|
|
11
|
+
});
|
|
12
|
+
export const UserContextSpec = entityUtils.getModelSpec(UserContextSchema);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TypeboxIsoDate } from '../validation/typebox-extensions.js';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { entityUtils } from '../utils/entity.utils.js';
|
|
4
|
+
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
5
|
+
export const UserPasswordSchema = Type.Object({
|
|
6
|
+
password: Type.String({
|
|
7
|
+
title: 'Password',
|
|
8
|
+
minLength: 6,
|
|
9
|
+
maxLength: 30
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
export const passwordValidator = TypeCompiler.Compile(UserPasswordSchema);
|
|
13
|
+
export const UserSchema = Type.Object({
|
|
14
|
+
email: Type.String({
|
|
15
|
+
title: 'Email',
|
|
16
|
+
format: 'email'
|
|
17
|
+
}),
|
|
18
|
+
firstName: Type.Optional(Type.String({
|
|
19
|
+
title: 'First Name'
|
|
20
|
+
})),
|
|
21
|
+
lastName: Type.Optional(Type.String({
|
|
22
|
+
title: 'Last Name'
|
|
23
|
+
})),
|
|
24
|
+
displayName: Type.Optional(Type.String({
|
|
25
|
+
title: 'Display Name'
|
|
26
|
+
})),
|
|
27
|
+
password: UserPasswordSchema.properties.password,
|
|
28
|
+
roles: Type.Optional(Type.Array(Type.String({
|
|
29
|
+
title: 'Roles',
|
|
30
|
+
}))),
|
|
31
|
+
_lastLoggedIn: Type.Optional(TypeboxIsoDate({ title: 'Last Login Date' })),
|
|
32
|
+
_lastPasswordChange: Type.Optional(TypeboxIsoDate({ title: 'Last Password Change Date' })),
|
|
33
|
+
});
|
|
34
|
+
export const UserSpec = entityUtils.getModelSpec(UserSchema, { isAuditable: true });
|
|
35
|
+
export const PublicUserSchema = Type.Omit(UserSpec.fullSchema, ['password']);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sort-direction.type.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ValidationError } from '../errors/index.js';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
4
|
+
import { EntitySchema } from '../models/entity.model.js';
|
|
5
|
+
import { AuditableSchema } from '../models/auditable.model.js';
|
|
6
|
+
import { Value } from '@sinclair/typebox/value';
|
|
7
|
+
function getValidator(schema) {
|
|
8
|
+
const validator = TypeCompiler.Compile(schema);
|
|
9
|
+
return validator;
|
|
10
|
+
}
|
|
11
|
+
function getModelSpec(schema, options = {}) {
|
|
12
|
+
const partialSchema = Type.Partial(schema);
|
|
13
|
+
const schemasToIntersect = [];
|
|
14
|
+
schemasToIntersect.push(schema);
|
|
15
|
+
schemasToIntersect.push(EntitySchema);
|
|
16
|
+
if (options.isAuditable) {
|
|
17
|
+
schemasToIntersect.push(AuditableSchema);
|
|
18
|
+
}
|
|
19
|
+
const fullSchema = Type.Intersect(schemasToIntersect);
|
|
20
|
+
const validator = getValidator(schema);
|
|
21
|
+
const partialValidator = getValidator(partialSchema);
|
|
22
|
+
const fullValidator = getValidator(fullSchema);
|
|
23
|
+
const encode = (entity, overrideSchema) => {
|
|
24
|
+
if (entity === null || entity === undefined) {
|
|
25
|
+
return entity;
|
|
26
|
+
}
|
|
27
|
+
const schemaToUse = overrideSchema || fullSchema;
|
|
28
|
+
return Value.Parse(['Encode', 'Clean'], schemaToUse, entity);
|
|
29
|
+
};
|
|
30
|
+
const decode = (entity) => {
|
|
31
|
+
if (entity === null || entity === undefined) {
|
|
32
|
+
return entity;
|
|
33
|
+
}
|
|
34
|
+
return Value.Parse(['Clean', 'Default', 'Convert', 'Decode'], fullSchema, entity);
|
|
35
|
+
};
|
|
36
|
+
const clean = (entity) => {
|
|
37
|
+
if (!entity)
|
|
38
|
+
return entity;
|
|
39
|
+
return Value.Clean(fullSchema, entity);
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
schema,
|
|
43
|
+
partialSchema,
|
|
44
|
+
fullSchema,
|
|
45
|
+
validator,
|
|
46
|
+
partialValidator,
|
|
47
|
+
fullValidator,
|
|
48
|
+
isAuditable: !!options.isAuditable,
|
|
49
|
+
isMultiTenant: !!options.isMultiTenant,
|
|
50
|
+
encode,
|
|
51
|
+
decode,
|
|
52
|
+
clean
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function validate(validator, data) {
|
|
56
|
+
const valid = validator.Check(data);
|
|
57
|
+
if (!valid) {
|
|
58
|
+
const errors = [...validator.Errors(data)];
|
|
59
|
+
return errors;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function handleValidationResult(validationErrors, methodName) {
|
|
64
|
+
if (validationErrors) {
|
|
65
|
+
throw new ValidationError(validationErrors);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isValidObjectId(id) {
|
|
69
|
+
let result = false;
|
|
70
|
+
if (typeof id === 'string' || id instanceof String) {
|
|
71
|
+
result = id.match(/^[0-9a-fA-F]{24}$/) ? true : false;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(`entityUtils.isValidObjectId called with something other than a string. id = ${id}`);
|
|
75
|
+
console.log(`typeof id = ${typeof id}`);
|
|
76
|
+
console.log('id = ', id);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function isAuditable(entity) {
|
|
81
|
+
return entity !== null &&
|
|
82
|
+
typeof entity === 'object' &&
|
|
83
|
+
(entity.hasOwnProperty('_created') ||
|
|
84
|
+
entity.hasOwnProperty('_createdBy') ||
|
|
85
|
+
entity.hasOwnProperty('_updated') ||
|
|
86
|
+
entity.hasOwnProperty('_updatedBy'));
|
|
87
|
+
}
|
|
88
|
+
function isDecimalMultipleOf(value, multipleOf, precision = 2) {
|
|
89
|
+
const scale = Math.pow(10, precision);
|
|
90
|
+
const scaledValue = Math.round(value * scale);
|
|
91
|
+
const scaledMultipleOf = Math.round(multipleOf * scale);
|
|
92
|
+
return scaledValue % scaledMultipleOf === 0;
|
|
93
|
+
}
|
|
94
|
+
export const entityUtils = {
|
|
95
|
+
handleValidationResult,
|
|
96
|
+
isValidObjectId,
|
|
97
|
+
isAuditable,
|
|
98
|
+
getValidator,
|
|
99
|
+
getModelSpec,
|
|
100
|
+
validate,
|
|
101
|
+
isDecimalMultipleOf
|
|
102
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './entity.utils.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IsDate } from './date.js';
|
|
2
|
+
import { IsTime } from './time.js';
|
|
3
|
+
const DATE_TIME_SEPARATOR = /t|\s/i;
|
|
4
|
+
export function IsDateTime(value, strictTimeZone) {
|
|
5
|
+
const dateTime = value.split(DATE_TIME_SEPARATOR);
|
|
6
|
+
return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone);
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
2
|
+
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
3
|
+
function IsLeapYear(year) {
|
|
4
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
5
|
+
}
|
|
6
|
+
export function IsDate(value) {
|
|
7
|
+
const matches = DATE.exec(value);
|
|
8
|
+
if (!matches)
|
|
9
|
+
return false;
|
|
10
|
+
const year = +matches[1];
|
|
11
|
+
const month = +matches[2];
|
|
12
|
+
const day = +matches[3];
|
|
13
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
2
|
+
export function IsTime(value, strictTimeZone) {
|
|
3
|
+
const matches = TIME.exec(value);
|
|
4
|
+
if (!matches)
|
|
5
|
+
return false;
|
|
6
|
+
const hr = +matches[1];
|
|
7
|
+
const min = +matches[2];
|
|
8
|
+
const sec = +matches[3];
|
|
9
|
+
const tz = matches[4];
|
|
10
|
+
const tzSign = matches[5] === '-' ? -1 : 1;
|
|
11
|
+
const tzH = +(matches[6] || 0);
|
|
12
|
+
const tzM = +(matches[7] || 0);
|
|
13
|
+
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz))
|
|
14
|
+
return false;
|
|
15
|
+
if (hr <= 23 && min <= 59 && sec < 60)
|
|
16
|
+
return true;
|
|
17
|
+
const utcMin = min - tzM * tzSign;
|
|
18
|
+
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0);
|
|
19
|
+
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61;
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Type, Kind, ValueGuard } from '@sinclair/typebox';
|
|
2
|
+
import { TypeRegistry } from '@sinclair/typebox';
|
|
3
|
+
import { Decimal as _Decimal } from 'decimal.js';
|
|
4
|
+
import { Value } from '@sinclair/typebox/value';
|
|
5
|
+
export function TypeboxIsoDate(options = {}) {
|
|
6
|
+
return Type.Transform(Type.String({ format: 'date-time', ...options }))
|
|
7
|
+
.Decode(value => new Date(value))
|
|
8
|
+
.Encode(value => value.toISOString());
|
|
9
|
+
}
|
|
10
|
+
export function TypeboxDate(options = {}) {
|
|
11
|
+
return Type.Transform(Type.String({ format: 'date', ...options }))
|
|
12
|
+
.Decode(value => {
|
|
13
|
+
const date = new Date(value);
|
|
14
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
15
|
+
return date;
|
|
16
|
+
})
|
|
17
|
+
.Encode(value => {
|
|
18
|
+
return value.toISOString().split('T')[0];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function TypeboxObjectId(options = {}) {
|
|
22
|
+
return Type.String({
|
|
23
|
+
format: 'objectid',
|
|
24
|
+
...options
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function TypeboxDecimal(options = {}) {
|
|
28
|
+
return { ...options, [Kind]: 'Decimal', type: 'number' };
|
|
29
|
+
}
|
|
30
|
+
TypeRegistry.Set('Decimal', (schema, value) => {
|
|
31
|
+
return ((ValueGuard.IsNumber(value)) &&
|
|
32
|
+
(ValueGuard.IsNumber(schema.multipleOf) ? new _Decimal(value).mod(new _Decimal(schema.multipleOf)).equals(0) : true) &&
|
|
33
|
+
(ValueGuard.IsNumber(schema.exclusiveMaximum) ? value < schema.exclusiveMaximum : true) &&
|
|
34
|
+
(ValueGuard.IsNumber(schema.exclusiveMinimum) ? value > schema.exclusiveMinimum : true) &&
|
|
35
|
+
(ValueGuard.IsNumber(schema.maximum) ? value <= schema.maximum : true) &&
|
|
36
|
+
(ValueGuard.IsNumber(schema.minimum) ? value >= schema.minimum : true));
|
|
37
|
+
});
|
|
38
|
+
export function TypeboxMoney(options = {}) {
|
|
39
|
+
return TypeboxDecimal({
|
|
40
|
+
multipleOf: 0.01,
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function TypeboxPercentage(options = {}) {
|
|
45
|
+
return TypeboxDecimal({
|
|
46
|
+
minimum: 0,
|
|
47
|
+
maximum: 100,
|
|
48
|
+
...options
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function testMoneyType() {
|
|
52
|
+
const schema = Type.Object({
|
|
53
|
+
price: TypeboxMoney({ minimum: 0 })
|
|
54
|
+
});
|
|
55
|
+
const valid = Value.Check(schema, { price: 39.99 });
|
|
56
|
+
console.log('39.99 is valid:', valid);
|
|
57
|
+
const valid2 = Value.Check(schema, { price: 100.00 });
|
|
58
|
+
console.log('100.00 is valid:', valid2);
|
|
59
|
+
const invalid = Value.Check(schema, { price: 39.999 });
|
|
60
|
+
console.log('39.999 is valid:', invalid);
|
|
61
|
+
if (!invalid) {
|
|
62
|
+
const errors = [...Value.Errors(schema, { price: 39.999 })];
|
|
63
|
+
console.log('Validation errors:', errors);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import { ValueErrorType, SetErrorFunction } from '@sinclair/typebox/errors';
|
|
3
|
+
import { Kind } from '@sinclair/typebox';
|
|
4
|
+
import { FormatRegistry } from '@sinclair/typebox';
|
|
5
|
+
import { IsDateTime } from './formats/date-time.js';
|
|
6
|
+
import { IsDate } from './formats/date.js';
|
|
7
|
+
import './typebox-extensions.js';
|
|
8
|
+
import { IsEmail } from './formats/email.js';
|
|
9
|
+
import { IsObjectId } from './formats/objectid.js';
|
|
10
|
+
export const isValidObjectId = (value) => {
|
|
11
|
+
try {
|
|
12
|
+
return ObjectId.isValid(value) &&
|
|
13
|
+
new ObjectId(value).toString() === value;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export const initializeTypeBox = () => {
|
|
20
|
+
SetErrorFunction(customSetErrorFunction);
|
|
21
|
+
FormatRegistry.Set('objectid', value => IsObjectId(value));
|
|
22
|
+
FormatRegistry.Set('date-time', value => IsDateTime(value));
|
|
23
|
+
FormatRegistry.Set('date', value => IsDate(value));
|
|
24
|
+
FormatRegistry.Set('email', value => IsEmail(value));
|
|
25
|
+
};
|
|
26
|
+
const customSetErrorFunction = (error) => {
|
|
27
|
+
const formattedPath = error.path === '' ? 'value' : error.path;
|
|
28
|
+
const fieldName = error.schema.title || formattedPath;
|
|
29
|
+
switch (error.errorType) {
|
|
30
|
+
case ValueErrorType.ArrayContains:
|
|
31
|
+
return `${fieldName} must be an error to contain at least one matching value`;
|
|
32
|
+
case ValueErrorType.ArrayMaxContains:
|
|
33
|
+
return `${fieldName} must contain no more than ${error.schema.maxContains} matching values`;
|
|
34
|
+
case ValueErrorType.ArrayMinContains:
|
|
35
|
+
return `${fieldName} must contain at least ${error.schema.minContains} matching values`;
|
|
36
|
+
case ValueErrorType.ArrayMaxItems:
|
|
37
|
+
return `${fieldName} length to be less or equal to ${error.schema.maxItems}`;
|
|
38
|
+
case ValueErrorType.ArrayMinItems:
|
|
39
|
+
return `${fieldName} length to be greater or equal to ${error.schema.minItems}`;
|
|
40
|
+
case ValueErrorType.ArrayUniqueItems:
|
|
41
|
+
return `${fieldName} elements must be unique`;
|
|
42
|
+
case ValueErrorType.Array:
|
|
43
|
+
return `${fieldName} must be an array`;
|
|
44
|
+
case ValueErrorType.AsyncIterator:
|
|
45
|
+
return `${fieldName} must be an AsyncIterator`;
|
|
46
|
+
case ValueErrorType.BigIntExclusiveMaximum:
|
|
47
|
+
return `${fieldName} value must be less than ${error.schema.exclusiveMaximum}`;
|
|
48
|
+
case ValueErrorType.BigIntExclusiveMinimum:
|
|
49
|
+
return `${fieldName} value must be greater than ${error.schema.exclusiveMinimum}`;
|
|
50
|
+
case ValueErrorType.BigIntMaximum:
|
|
51
|
+
return `${fieldName} value must be less or equal to ${error.schema.maximum}`;
|
|
52
|
+
case ValueErrorType.BigIntMinimum:
|
|
53
|
+
return `${fieldName} value must be greater or equal to ${error.schema.minimum}`;
|
|
54
|
+
case ValueErrorType.BigIntMultipleOf:
|
|
55
|
+
return `${fieldName} value must be a multiple of ${error.schema.multipleOf}`;
|
|
56
|
+
case ValueErrorType.BigInt:
|
|
57
|
+
return `${fieldName} must be bigint`;
|
|
58
|
+
case ValueErrorType.Boolean:
|
|
59
|
+
return `${fieldName} must be boolean`;
|
|
60
|
+
case ValueErrorType.DateExclusiveMinimumTimestamp:
|
|
61
|
+
return `${fieldName} Date must be greater than ${error.schema.exclusiveMinimumTimestamp}`;
|
|
62
|
+
case ValueErrorType.DateExclusiveMaximumTimestamp:
|
|
63
|
+
return `${fieldName} Date must be less than ${error.schema.exclusiveMaximumTimestamp}`;
|
|
64
|
+
case ValueErrorType.DateMinimumTimestamp:
|
|
65
|
+
return `${fieldName} Date timestamp must be greater or equal to ${error.schema.minimumTimestamp}`;
|
|
66
|
+
case ValueErrorType.DateMaximumTimestamp:
|
|
67
|
+
return `${fieldName} Date timestamp must be less or equal to ${error.schema.maximumTimestamp}`;
|
|
68
|
+
case ValueErrorType.DateMultipleOfTimestamp:
|
|
69
|
+
return `${fieldName} Date timestamp must be a multiple of ${error.schema.multipleOfTimestamp}`;
|
|
70
|
+
case ValueErrorType.Date:
|
|
71
|
+
return `${fieldName} must be a Date`;
|
|
72
|
+
case ValueErrorType.Function:
|
|
73
|
+
return `${fieldName} must be a function`;
|
|
74
|
+
case ValueErrorType.IntegerExclusiveMaximum:
|
|
75
|
+
return `${fieldName} must be less than ${error.schema.exclusiveMaximum}`;
|
|
76
|
+
case ValueErrorType.IntegerExclusiveMinimum:
|
|
77
|
+
return `${fieldName} must be greater than ${error.schema.exclusiveMinimum}`;
|
|
78
|
+
case ValueErrorType.IntegerMaximum:
|
|
79
|
+
return `${fieldName} must be less than or equal to ${error.schema.maximum}`;
|
|
80
|
+
case ValueErrorType.IntegerMinimum:
|
|
81
|
+
return `${fieldName} must be greater than or equal to ${error.schema.minimum}`;
|
|
82
|
+
case ValueErrorType.IntegerMultipleOf:
|
|
83
|
+
return `${fieldName} must be a multiple of ${error.schema.multipleOf}`;
|
|
84
|
+
case ValueErrorType.Integer:
|
|
85
|
+
return `${fieldName} must be an integer`;
|
|
86
|
+
case ValueErrorType.IntersectUnevaluatedProperties:
|
|
87
|
+
return `${fieldName} property must exist`;
|
|
88
|
+
case ValueErrorType.Intersect:
|
|
89
|
+
return `${fieldName} all operands must match`;
|
|
90
|
+
case ValueErrorType.Iterator:
|
|
91
|
+
return `${fieldName} must be an Iterator`;
|
|
92
|
+
case ValueErrorType.Literal:
|
|
93
|
+
return `${fieldName} must be ${typeof error.schema.const === 'string' ? `'${error.schema.const}'` : error.schema.const}`;
|
|
94
|
+
case ValueErrorType.Never:
|
|
95
|
+
return `${fieldName} is never`;
|
|
96
|
+
case ValueErrorType.Not:
|
|
97
|
+
return `${fieldName} must not be ${JSON.stringify(error.schema)}`;
|
|
98
|
+
case ValueErrorType.Null:
|
|
99
|
+
return `${fieldName} must be null`;
|
|
100
|
+
case ValueErrorType.NumberExclusiveMaximum:
|
|
101
|
+
return `${fieldName} must be less than ${error.schema.exclusiveMaximum}`;
|
|
102
|
+
case ValueErrorType.NumberExclusiveMinimum:
|
|
103
|
+
return `${fieldName} must be greater than ${error.schema.exclusiveMinimum}`;
|
|
104
|
+
case ValueErrorType.NumberMaximum:
|
|
105
|
+
return `${fieldName} must be less or equal to ${error.schema.maximum}`;
|
|
106
|
+
case ValueErrorType.NumberMinimum:
|
|
107
|
+
return `${fieldName} must be greater than or equal to ${error.schema.minimum}`;
|
|
108
|
+
case ValueErrorType.NumberMultipleOf:
|
|
109
|
+
return `${fieldName} must be a multiple of ${error.schema.multipleOf}`;
|
|
110
|
+
case ValueErrorType.Number:
|
|
111
|
+
return `${fieldName} must be a number`;
|
|
112
|
+
case ValueErrorType.Object:
|
|
113
|
+
return `${fieldName} must be an object`;
|
|
114
|
+
case ValueErrorType.ObjectAdditionalProperties:
|
|
115
|
+
return `${fieldName} has unexpected property`;
|
|
116
|
+
case ValueErrorType.ObjectMaxProperties:
|
|
117
|
+
return `${fieldName} should have no more than ${error.schema.maxProperties} properties`;
|
|
118
|
+
case ValueErrorType.ObjectMinProperties:
|
|
119
|
+
return `${fieldName} should have at least ${error.schema.minProperties} properties`;
|
|
120
|
+
case ValueErrorType.ObjectRequiredProperty:
|
|
121
|
+
return `${fieldName} must have required property`;
|
|
122
|
+
case ValueErrorType.Promise:
|
|
123
|
+
return `${fieldName} must be a Promise`;
|
|
124
|
+
case ValueErrorType.RegExp:
|
|
125
|
+
return `${fieldName} must match regular expression`;
|
|
126
|
+
case ValueErrorType.StringFormatUnknown:
|
|
127
|
+
return `${fieldName} uses unknown format '${error.schema.format}'`;
|
|
128
|
+
case ValueErrorType.StringFormat:
|
|
129
|
+
return `${fieldName} must match match '${error.schema.format}' format`;
|
|
130
|
+
case ValueErrorType.StringMaxLength:
|
|
131
|
+
return `${fieldName} length must be less or equal to ${error.schema.maxLength}`;
|
|
132
|
+
case ValueErrorType.StringMinLength:
|
|
133
|
+
return `${fieldName} length must be greater or equal to ${error.schema.minLength}`;
|
|
134
|
+
case ValueErrorType.StringPattern:
|
|
135
|
+
return `${fieldName} string must match '${error.schema.pattern}'`;
|
|
136
|
+
case ValueErrorType.String:
|
|
137
|
+
return `${fieldName} must be a string`;
|
|
138
|
+
case ValueErrorType.Symbol:
|
|
139
|
+
return `${fieldName} must be a symbol`;
|
|
140
|
+
case ValueErrorType.TupleLength:
|
|
141
|
+
return `${fieldName} must have ${error.schema.maxItems || 0} elements`;
|
|
142
|
+
case ValueErrorType.Tuple:
|
|
143
|
+
return `${fieldName} must be a tuple`;
|
|
144
|
+
case ValueErrorType.Uint8ArrayMaxByteLength:
|
|
145
|
+
return `${fieldName} byte length less or equal to ${error.schema.maxByteLength}`;
|
|
146
|
+
case ValueErrorType.Uint8ArrayMinByteLength:
|
|
147
|
+
return `${fieldName} byte length greater or equal to ${error.schema.minByteLength}`;
|
|
148
|
+
case ValueErrorType.Uint8Array:
|
|
149
|
+
return `${fieldName} must be Uint8Array`;
|
|
150
|
+
case ValueErrorType.Undefined:
|
|
151
|
+
return `${fieldName} must be undefined`;
|
|
152
|
+
case ValueErrorType.Union:
|
|
153
|
+
return `${fieldName} must match one of the union variants`;
|
|
154
|
+
case ValueErrorType.Void:
|
|
155
|
+
return `${fieldName} must be void`;
|
|
156
|
+
case ValueErrorType.Kind:
|
|
157
|
+
return `Expected kind '${error.schema[Kind]}'`;
|
|
158
|
+
default:
|
|
159
|
+
return 'Unknown error type';
|
|
160
|
+
}
|
|
161
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loomcore/common",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Loom Core Models- common models, interfaces, types, and utils for Loom Core. All things common to both api and client apps.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"clean": "rm -rf dist",
|
|
8
|
+
"tsc": "tsc --project tsconfig.prod.json",
|
|
9
|
+
"build": "npm-run-all -s clean tsc",
|
|
10
|
+
"add": "git add .",
|
|
11
|
+
"commit": "git commit -m \"Updates\"",
|
|
12
|
+
"patch": "npm version patch",
|
|
13
|
+
"push": "git push",
|
|
14
|
+
"gar-login": "npx --yes google-artifactregistry-auth",
|
|
15
|
+
"publishMe": "npm publish",
|
|
16
|
+
"pub": "npm-run-all -s add commit patch build push publishMe",
|
|
17
|
+
"auth-and-pub": "npm-run-all -s add commit patch build push gar-login publishMe"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "Tim Hardy",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"types": "dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/**/*"
|
|
27
|
+
],
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./dist/index.js",
|
|
30
|
+
"./types": "./dist/types/index.js"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"decimal.js": "^10.5.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@sinclair/typebox": "^0.34.33",
|
|
37
|
+
"@types/express": "^5.0.1",
|
|
38
|
+
"express": "^5.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"npm-run-all": "^4.1.5",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|