init
This commit is contained in:
commit
7c7077a44a
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
package-lock.json
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"printWidth": 79
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "axp-ts",
|
||||||
|
"version": "1.9.5",
|
||||||
|
"description": "TypeScript helper library",
|
||||||
|
"author": "AntoXa PRO <info@antoxa.pro>",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"tsconfig.ts"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.21.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
}
|
||||||
|
}
|
67
src/data-result.ts
Normal file
67
src/data-result.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import type { TPagination } from './pagination'
|
||||||
|
import type { TNotificationItem } from './notification'
|
||||||
|
|
||||||
|
import { Pagination } from './pagination'
|
||||||
|
|
||||||
|
export type TInfoDataResult = {
|
||||||
|
type: 'object' | 'array' | 'undefined'
|
||||||
|
length: number
|
||||||
|
pagination?: TPagination
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDataResult<T> {
|
||||||
|
status: number
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
info: TInfoDataResult
|
||||||
|
errors: TNotificationItem[]
|
||||||
|
setData(data: T, pagination?: TPagination): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataResultEntity<T> implements IDataResult<T> {
|
||||||
|
status: number = 200
|
||||||
|
message: string = 'Ok'
|
||||||
|
data: T
|
||||||
|
info: TInfoDataResult = { type: 'undefined', length: 0 }
|
||||||
|
errors: TNotificationItem[] = []
|
||||||
|
|
||||||
|
constructor(data: any = {}) {
|
||||||
|
this.data = data
|
||||||
|
this.setData()
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(data: T = null, pagination?: TPagination): void {
|
||||||
|
if (data !== null) {
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если данные есть.
|
||||||
|
if (this.data) {
|
||||||
|
// Если данные это массив.
|
||||||
|
if (Array.isArray(this.data)) {
|
||||||
|
this.info.type = 'array'
|
||||||
|
this.info.length = [...this.data].length
|
||||||
|
|
||||||
|
if (pagination) {
|
||||||
|
this.info.pagination = pagination
|
||||||
|
} else {
|
||||||
|
const pagination = new Pagination(
|
||||||
|
{ page: 1, limit: this.info.length, total: this.info.length },
|
||||||
|
this.info.length
|
||||||
|
)
|
||||||
|
this.info.pagination = pagination.toObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если общее кол-во меньше чем размер массива.
|
||||||
|
// Это обычно значение по умолчанию "0" при инициализации объекта пагинации.
|
||||||
|
if (this.info.pagination.total < this.info.length) {
|
||||||
|
this.info.pagination.total = this.info.length
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Данные это объект.
|
||||||
|
this.info.type = 'object'
|
||||||
|
this.info.length = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
src/entities/gender.ts
Normal file
28
src/entities/gender.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Перечисление гендеров.
|
||||||
|
*/
|
||||||
|
export enum GenderEnum {
|
||||||
|
man = 'man',
|
||||||
|
woman = 'woman'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Тип гендера.
|
||||||
|
*/
|
||||||
|
export type TGender = keyof typeof GenderEnum
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Список значений типов гендера.
|
||||||
|
*/
|
||||||
|
export const GENDERS = Object.keys(GenderEnum)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка гендера.
|
||||||
|
*/
|
||||||
|
export const isGender = (val?: any | null) => {
|
||||||
|
try {
|
||||||
|
return GENDERS.includes(String(val))
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
1
src/entities/index.ts
Normal file
1
src/entities/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './gender'
|
139
src/form/ctrl.ts
Normal file
139
src/form/ctrl.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import type { z } from 'zod'
|
||||||
|
|
||||||
|
export type TFormSchemaCtrlArgs = {
|
||||||
|
key?: string
|
||||||
|
type?: string
|
||||||
|
label?: string
|
||||||
|
component?: string
|
||||||
|
|
||||||
|
hidden?: boolean
|
||||||
|
description?: string
|
||||||
|
readonly?: boolean
|
||||||
|
multiple?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
cssClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormSchemaCtrl {
|
||||||
|
key: string
|
||||||
|
type: string
|
||||||
|
label: string
|
||||||
|
component: string
|
||||||
|
|
||||||
|
hidden?: boolean
|
||||||
|
description?: string
|
||||||
|
readonly?: boolean
|
||||||
|
multiple?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
cssClass?: string
|
||||||
|
|
||||||
|
static toString(ctrl: TFormSchemaCtrlArgs, description?: string) {
|
||||||
|
let result = ''
|
||||||
|
try {
|
||||||
|
if (description) {
|
||||||
|
const descObj = JSON.parse(description)
|
||||||
|
ctrl = Object.assign(descObj, ctrl)
|
||||||
|
}
|
||||||
|
result = JSON.stringify(ctrl)
|
||||||
|
} catch (ex) {}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(args: TFormSchemaCtrlArgs, shape: z.ZodTypeAny) {
|
||||||
|
let desc: any = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shape.description) {
|
||||||
|
desc = JSON.parse(shape.description)
|
||||||
|
} else {
|
||||||
|
desc = JSON.parse(shape?._def?.innerType?._def?.description || '{}')
|
||||||
|
}
|
||||||
|
} catch (ex: any) {
|
||||||
|
desc = { label: shape.description || 'Unknown' }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = desc.key || args.key || 'unknown'
|
||||||
|
this.label = desc.label || args.label || 'Label'
|
||||||
|
|
||||||
|
// Тип.
|
||||||
|
this.type = desc.type || args.type
|
||||||
|
|
||||||
|
if (!this.type && shape) {
|
||||||
|
if (
|
||||||
|
shape?._def?.innerType?._def?.innerType?._def?.schema?._def?.typeName
|
||||||
|
) {
|
||||||
|
this.type =
|
||||||
|
shape?._def?.innerType?._def?.innerType?._def?.schema?._def?.typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape._def?.innerType?._def?.innerType?._def?.typeName) {
|
||||||
|
this.type = shape?._def?.innerType?._def?.innerType._def.typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape._def.innerType?._def?.schema?._def?.typeName) {
|
||||||
|
this.type = shape._def.innerType?._def?.schema?._def?.typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape._def.innerType?._def?.typeName) {
|
||||||
|
this.type = shape._def.innerType._def.typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape._def.schema?._def?.typeName) {
|
||||||
|
this.type = shape._def.schema._def.typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.type) {
|
||||||
|
this.type = shape._def.typeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переименовываем тип.
|
||||||
|
switch (this.type) {
|
||||||
|
case 'ZodString':
|
||||||
|
this.type = 'string'
|
||||||
|
break
|
||||||
|
case 'ZodNumber':
|
||||||
|
this.type = 'number'
|
||||||
|
break
|
||||||
|
case 'ZodBoolean':
|
||||||
|
this.type = 'boolean'
|
||||||
|
break
|
||||||
|
case 'ZodArray':
|
||||||
|
this.type = 'array'
|
||||||
|
break
|
||||||
|
case 'ZodDate':
|
||||||
|
this.type = 'date'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Компонент.
|
||||||
|
this.component = desc.component || args.component
|
||||||
|
if (!this.component) {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'string':
|
||||||
|
this.component = 'ui-field-text'
|
||||||
|
break
|
||||||
|
case 'number':
|
||||||
|
this.component = 'ui-field-number'
|
||||||
|
break
|
||||||
|
case 'boolean':
|
||||||
|
this.component = 'ui-field-checkbox'
|
||||||
|
break
|
||||||
|
case 'date':
|
||||||
|
this.component = 'ui-field-date'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.component = 'ui-field'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не обязательные.
|
||||||
|
this.hidden = desc.hidden || args.hidden
|
||||||
|
this.description = desc.description || args.description
|
||||||
|
this.readonly = desc.readonly || args.readonly
|
||||||
|
this.multiple = desc.multiple || args.multiple
|
||||||
|
this.disabled = desc.disabled || args.disabled
|
||||||
|
this.cssClass = desc.cssClass || args.cssClass
|
||||||
|
}
|
||||||
|
}
|
191
src/form/fields.ts
Normal file
191
src/form/fields.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import type { ZodTypeAny } from 'zod'
|
||||||
|
import type { TFormSchemaCtrlArgs } from './ctrl'
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { GenderEnum } from '../entities'
|
||||||
|
import {
|
||||||
|
getPhoneNumberValue,
|
||||||
|
regexFIO,
|
||||||
|
validPhoneNumber,
|
||||||
|
regexSearch,
|
||||||
|
regexId
|
||||||
|
} from './helpers'
|
||||||
|
import { FormSchemaCtrl } from './ctrl'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create field schema.
|
||||||
|
*/
|
||||||
|
export const fieldSchema = <T extends ZodTypeAny>(
|
||||||
|
base: T,
|
||||||
|
args?: TFormSchemaCtrlArgs
|
||||||
|
) => base.describe(FormSchemaCtrl.toString(args, base.description))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base fields schema.
|
||||||
|
*/
|
||||||
|
export const bFieldsSchema = {
|
||||||
|
number: z
|
||||||
|
.preprocess(value => Number(value), z.number())
|
||||||
|
.describe(
|
||||||
|
FormSchemaCtrl.toString({
|
||||||
|
label: 'Номер',
|
||||||
|
type: 'number',
|
||||||
|
component: 'ui-field-number'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
string: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(2)
|
||||||
|
.max(64)
|
||||||
|
.describe(
|
||||||
|
FormSchemaCtrl.toString({
|
||||||
|
label: 'Строка',
|
||||||
|
type: 'string',
|
||||||
|
component: 'ui-field-text'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
date: z
|
||||||
|
.preprocess(value => new Date(String(value)), z.date())
|
||||||
|
.describe(
|
||||||
|
FormSchemaCtrl.toString({
|
||||||
|
label: 'Дата',
|
||||||
|
type: 'date',
|
||||||
|
component: 'ui-field-date'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
boolean: z
|
||||||
|
.preprocess(value => String(value) === 'true', z.boolean())
|
||||||
|
.describe(
|
||||||
|
FormSchemaCtrl.toString({
|
||||||
|
label: 'Логическое значение',
|
||||||
|
type: 'boolean',
|
||||||
|
component: 'ui-field-checkbox'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common fields schema.
|
||||||
|
*/
|
||||||
|
export const cFieldsSchema = z.object({
|
||||||
|
...bFieldsSchema,
|
||||||
|
_id: fieldSchema(bFieldsSchema.string.regex(regexId, 'Не валидный ID'), {
|
||||||
|
label: 'ID'
|
||||||
|
}),
|
||||||
|
dateCreate: fieldSchema(bFieldsSchema.date, {
|
||||||
|
label: 'Дата создания'
|
||||||
|
}),
|
||||||
|
dateUpdate: fieldSchema(bFieldsSchema.date, {
|
||||||
|
label: 'Дата изменения'
|
||||||
|
}),
|
||||||
|
q: fieldSchema(
|
||||||
|
z.preprocess(
|
||||||
|
val => String(val).replace(regexSearch, ''),
|
||||||
|
bFieldsSchema.string
|
||||||
|
),
|
||||||
|
{
|
||||||
|
label: 'Поиск',
|
||||||
|
component: 'ui-field-search'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name: fieldSchema(bFieldsSchema.string, {
|
||||||
|
label: 'Название'
|
||||||
|
}),
|
||||||
|
title: fieldSchema(bFieldsSchema.string, {
|
||||||
|
label: 'Заголовок'
|
||||||
|
}),
|
||||||
|
comment: fieldSchema(z.string().trim().min(2).max(1000), {
|
||||||
|
label: 'Комментарий',
|
||||||
|
component: 'ui-field-text-area'
|
||||||
|
}),
|
||||||
|
description: fieldSchema(z.string().trim().min(2).max(1000), {
|
||||||
|
label: 'Описание',
|
||||||
|
component: 'ui-field-text-area'
|
||||||
|
}),
|
||||||
|
text: fieldSchema(z.string().trim().min(2).max(3000), {
|
||||||
|
label: 'Текст',
|
||||||
|
component: 'ui-field-text-area'
|
||||||
|
}),
|
||||||
|
login: fieldSchema(bFieldsSchema.string, {
|
||||||
|
label: 'Логин'
|
||||||
|
}),
|
||||||
|
email: fieldSchema(bFieldsSchema.string.email(), {
|
||||||
|
label: 'Email'
|
||||||
|
}),
|
||||||
|
password: fieldSchema(bFieldsSchema.string.min(6), {
|
||||||
|
label: 'Пароль',
|
||||||
|
component: 'ui-field-password'
|
||||||
|
}),
|
||||||
|
price: fieldSchema(
|
||||||
|
z.preprocess(val => Number(val), z.number().nonnegative()),
|
||||||
|
{ label: 'Стоимость' }
|
||||||
|
),
|
||||||
|
alias: fieldSchema(
|
||||||
|
bFieldsSchema.string
|
||||||
|
.toLowerCase()
|
||||||
|
.regex(/^[a-z-]+$/, 'Только латиница и тире "-"'),
|
||||||
|
{ label: 'Псевдоним' }
|
||||||
|
),
|
||||||
|
published: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Опубликован(а)'
|
||||||
|
}),
|
||||||
|
active: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Активный(ная)'
|
||||||
|
}),
|
||||||
|
enabled: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Включен(а)'
|
||||||
|
}),
|
||||||
|
disabled: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Отключен(а)'
|
||||||
|
}),
|
||||||
|
open: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Открыт(а)'
|
||||||
|
}),
|
||||||
|
close: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Закрыто'
|
||||||
|
}),
|
||||||
|
closed: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Закрыт(а)'
|
||||||
|
}),
|
||||||
|
online: fieldSchema(bFieldsSchema.boolean, {
|
||||||
|
label: 'Онлайн'
|
||||||
|
}),
|
||||||
|
firstName: fieldSchema(
|
||||||
|
bFieldsSchema.string.regex(regexFIO, 'Только кириллица'),
|
||||||
|
{ label: 'Имя' }
|
||||||
|
),
|
||||||
|
middleName: fieldSchema(
|
||||||
|
bFieldsSchema.string.regex(regexFIO, 'Только кириллица'),
|
||||||
|
{ label: 'Отчество' }
|
||||||
|
),
|
||||||
|
lastName: fieldSchema(
|
||||||
|
bFieldsSchema.string.regex(regexFIO, 'Только кириллица'),
|
||||||
|
{ label: 'Фамилия' }
|
||||||
|
),
|
||||||
|
birthday: fieldSchema(bFieldsSchema.date, {
|
||||||
|
label: 'Дата рождения'
|
||||||
|
}),
|
||||||
|
phone: fieldSchema(
|
||||||
|
z
|
||||||
|
.preprocess(val => getPhoneNumberValue(val) || 0, bFieldsSchema.number)
|
||||||
|
.refine(val => validPhoneNumber(val), {
|
||||||
|
message: 'Не вервый формат номера телефона'
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
label: 'Телефон',
|
||||||
|
component: 'ui-field-phone'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
gender: fieldSchema(z.enum([GenderEnum.man, GenderEnum.woman]), {
|
||||||
|
label: 'Пол',
|
||||||
|
component: 'ui-field-select-gender'
|
||||||
|
}),
|
||||||
|
year: fieldSchema(bFieldsSchema.number, {
|
||||||
|
label: 'Год'
|
||||||
|
}),
|
||||||
|
days: fieldSchema(bFieldsSchema.number.array(), {
|
||||||
|
label: 'Дни недели',
|
||||||
|
component: 'ui-picker-days'
|
||||||
|
})
|
||||||
|
})
|
112
src/form/helpers.ts
Normal file
112
src/form/helpers.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Проверка ФИО.
|
||||||
|
*/
|
||||||
|
export const regexFIO = /^[А-яЁё]+$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значения номера.
|
||||||
|
*/
|
||||||
|
export const regexPhone = /^[9]\d{9}$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регулярка для поиска.
|
||||||
|
*/
|
||||||
|
export const regexSearch = /[\'\"\+\(\)]/g
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Регулярка ID.
|
||||||
|
*/
|
||||||
|
export const regexId = /^[a-f\d]{24}$/i
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает значение номера телефона.
|
||||||
|
*/
|
||||||
|
export const getPhoneNumberValue = (phone?: any): number | undefined => {
|
||||||
|
if (phone) {
|
||||||
|
if (typeof phone === 'number') {
|
||||||
|
phone = phone.toString()
|
||||||
|
}
|
||||||
|
if (typeof phone === 'string') {
|
||||||
|
try {
|
||||||
|
phone = phone.replace(/\D/g, '').replace(/^[78]/, '').substring(0, 10)
|
||||||
|
return Number(phone) || undefined
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидация мобильного номера телефона.
|
||||||
|
*/
|
||||||
|
export const validPhoneNumber = (value?: number | string) => {
|
||||||
|
if (!value) return false
|
||||||
|
const str: string = value.toString()
|
||||||
|
if (str.length !== 10) return false
|
||||||
|
if (str.match(regexPhone) === null) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формат номера телефона.
|
||||||
|
*/
|
||||||
|
export const getPhoneNumberFormat = (
|
||||||
|
phone?: number | string,
|
||||||
|
prefix: string = '+7 '
|
||||||
|
): string => {
|
||||||
|
let result = prefix
|
||||||
|
const strValue = getPhoneNumberValue(phone)?.toString().substring(0, 10)
|
||||||
|
if (strValue) {
|
||||||
|
for (let i = 0; i < strValue.length; i++) {
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
result += '('
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
result += ') '
|
||||||
|
break
|
||||||
|
case 6:
|
||||||
|
result += '-'
|
||||||
|
break
|
||||||
|
case 8:
|
||||||
|
result += '-'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result += strValue[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Функция для проеобрадования загоавных букв в верхний регистр.
|
||||||
|
*/
|
||||||
|
export const capitalize = (str: string = '') => {
|
||||||
|
return str[0] ? str[0].toUpperCase() + str.substring(1) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка ID.
|
||||||
|
*/
|
||||||
|
export const isId = (val: any) =>
|
||||||
|
typeof val === 'string' && val.match(/^[a-f\d]{24}$/i)
|
||||||
|
|
||||||
|
type TDate = Date | number | string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразование даты в общий формат (YYYY-MM-DD).
|
||||||
|
*/
|
||||||
|
export const getDateCommonFormat = (val?: TDate | null): string => {
|
||||||
|
try {
|
||||||
|
if (val) {
|
||||||
|
const date = new Date(val)
|
||||||
|
const day = date.toLocaleDateString('ru-RU', { day: '2-digit' })
|
||||||
|
const month = date.toLocaleDateString('ru-RU', { month: '2-digit' })
|
||||||
|
const year = date.toLocaleDateString('ru-RU', { year: 'numeric' })
|
||||||
|
const format = `${year}-${month}-${day}`
|
||||||
|
return format
|
||||||
|
}
|
||||||
|
} catch (ex) {}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
4
src/form/index.ts
Normal file
4
src/form/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './helpers'
|
||||||
|
export * from './ctrl'
|
||||||
|
export * from './fields'
|
||||||
|
export * from './model'
|
100
src/form/model.ts
Normal file
100
src/form/model.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import type { z } from 'zod'
|
||||||
|
import type { TNotificationItem } from '../notification'
|
||||||
|
|
||||||
|
import { FormSchemaCtrl } from './ctrl'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интерфейс базовой формы для сущностей в БД.
|
||||||
|
*/
|
||||||
|
export interface IFormModel<T> {
|
||||||
|
_id: string
|
||||||
|
|
||||||
|
dateCreate?: Date
|
||||||
|
dateUpdate?: Date
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
obj: T
|
||||||
|
ctrls: FormSchemaCtrl[]
|
||||||
|
|
||||||
|
_errors: {[PropKey in keyof T]?: string}
|
||||||
|
errors: TNotificationItem[]
|
||||||
|
|
||||||
|
isValid(): boolean
|
||||||
|
setValidError(code: string, text: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовая модель для валидирования форм.
|
||||||
|
*/
|
||||||
|
export class BaseFormModel<T extends Object = {}> implements IFormModel<T> {
|
||||||
|
_id: string
|
||||||
|
|
||||||
|
dateCreate?: Date
|
||||||
|
dateUpdate?: Date
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
obj: T
|
||||||
|
schema: z.ZodObject<z.ZodRawShape>
|
||||||
|
ctrls: FormSchemaCtrl[] = []
|
||||||
|
|
||||||
|
_errors: {[PropKey in keyof T]?: string} = {}
|
||||||
|
|
||||||
|
constructor(obj: any = {}, schema: z.ZodObject<z.ZodRawShape>) {
|
||||||
|
this._id = obj._id || 'create'
|
||||||
|
delete obj._id
|
||||||
|
|
||||||
|
if (obj.dateCreate) {
|
||||||
|
try {
|
||||||
|
this.dateCreate = new Date(obj.dateCreate)
|
||||||
|
delete obj.dateCreate
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.dateUpdate) {
|
||||||
|
try {
|
||||||
|
this.dateUpdate = new Date(obj.dateUpdate)
|
||||||
|
delete obj.dateUpdate
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.obj = obj
|
||||||
|
this.schema = schema
|
||||||
|
|
||||||
|
// Создаём контролы.
|
||||||
|
for (const key in this.schema.shape) {
|
||||||
|
this.ctrls.push(new FormSchemaCtrl({ key }, this.schema.shape[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заголовок.
|
||||||
|
if (this.schema.description) this.title = this.schema.description
|
||||||
|
}
|
||||||
|
|
||||||
|
get errors() {
|
||||||
|
let items: TNotificationItem[] = []
|
||||||
|
for (const code in this._errors) {
|
||||||
|
const text = this._errors[code]
|
||||||
|
items.push({ code, text })
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
this._errors = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.obj = this.schema.parse(this.obj) as T
|
||||||
|
return true
|
||||||
|
} catch (ex) {
|
||||||
|
const error = ex as z.ZodError
|
||||||
|
for (const issues of error.issues) {
|
||||||
|
this.setValidError(issues.path.toString(), issues.message)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidError(code: string, text: string) {
|
||||||
|
this._errors[code] = code + ' - ' + text
|
||||||
|
}
|
||||||
|
}
|
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './entities'
|
||||||
|
export * from './form'
|
||||||
|
export * from './notification'
|
||||||
|
export * from './pagination'
|
||||||
|
export * from './data-result'
|
14
src/notification.ts
Normal file
14
src/notification.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export type TNotificationItem = {
|
||||||
|
code: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationItem implements TNotificationItem {
|
||||||
|
code: string
|
||||||
|
text: string
|
||||||
|
|
||||||
|
constructor(args: {code: string, text: string }) {
|
||||||
|
this.code = args.code
|
||||||
|
this.text = args.text
|
||||||
|
}
|
||||||
|
}
|
151
src/pagination.ts
Normal file
151
src/pagination.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { cFieldsSchema, fieldSchema } from './form'
|
||||||
|
|
||||||
|
export const paginationSchema = z.object({
|
||||||
|
page: fieldSchema(cFieldsSchema.shape.number, {
|
||||||
|
label: 'Номер страницы'
|
||||||
|
}),
|
||||||
|
limit: fieldSchema(cFieldsSchema.shape.number, {
|
||||||
|
label: 'Лимит на странице'
|
||||||
|
}),
|
||||||
|
total: fieldSchema(cFieldsSchema.shape.number, {
|
||||||
|
label: 'Общее кол-во'
|
||||||
|
}),
|
||||||
|
skip: fieldSchema(cFieldsSchema.shape.number, {
|
||||||
|
label: 'Пропустить'
|
||||||
|
}),
|
||||||
|
pages: fieldSchema(cFieldsSchema.shape.number, {
|
||||||
|
label: 'Кол-во всех страниц'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TPagination = z.infer<typeof paginationSchema>
|
||||||
|
|
||||||
|
// Константы.
|
||||||
|
const DEFAULTS = { page: 1, limit: 10, maxLimit: 100 }
|
||||||
|
|
||||||
|
export type TPaginationParseArg = number | string | undefined
|
||||||
|
|
||||||
|
export type TPaginationArguments = {
|
||||||
|
page?: TPaginationParseArg
|
||||||
|
limit?: TPaginationParseArg
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPagination extends TPagination {
|
||||||
|
/**
|
||||||
|
* Максимальный лимит элементов.
|
||||||
|
*/
|
||||||
|
get maxLimit(): number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсинг аргументов.
|
||||||
|
* @param arg - Значение аргумента для парсинга.
|
||||||
|
* @param defaultReturnValue - Возвращаемое значение по умолчанию.
|
||||||
|
* @returns Возвращает абсолютное значение числа аргумента.
|
||||||
|
*/
|
||||||
|
parseArg(
|
||||||
|
arg: number | string | undefined,
|
||||||
|
defaultReturnValue: number
|
||||||
|
): number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Присваивает значения для основных свойств класса и считает кол-во
|
||||||
|
* пропускаемых элементов в зависимости от полученных аргументов.
|
||||||
|
* @param args - Аргументы пагинации.
|
||||||
|
* @returns Возвращает текущий экземпляр класса.
|
||||||
|
*/
|
||||||
|
set(args: TPaginationArguments): this
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает простой объект пагинации.
|
||||||
|
* @returns Объект пагинации.
|
||||||
|
*/
|
||||||
|
toObject(): TPagination
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pagination implements IPagination {
|
||||||
|
/**
|
||||||
|
* Максимальный лимит элементов.
|
||||||
|
*/
|
||||||
|
private _maxLimit: number
|
||||||
|
|
||||||
|
page: number = DEFAULTS.page
|
||||||
|
limit: number = DEFAULTS.limit
|
||||||
|
|
||||||
|
skip: number = 0
|
||||||
|
total: number = 0
|
||||||
|
pages: number = 0
|
||||||
|
|
||||||
|
static parseArg(
|
||||||
|
arg: TPaginationParseArg,
|
||||||
|
defaultReturnValue: number
|
||||||
|
): number {
|
||||||
|
return Math.abs(
|
||||||
|
typeof arg === 'string'
|
||||||
|
? Number.parseInt(arg) || defaultReturnValue
|
||||||
|
: arg || defaultReturnValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(args: TPaginationArguments = {}, maxLimit?: number) {
|
||||||
|
this._maxLimit = this.parseArg(maxLimit, DEFAULTS.maxLimit)
|
||||||
|
this.set(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxLimit() {
|
||||||
|
return this._maxLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArg(arg: TPaginationParseArg, defaultReturnValue: number): number {
|
||||||
|
return Pagination.parseArg(arg, defaultReturnValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(args: TPaginationArguments = {}): this {
|
||||||
|
let isCalcSkip: boolean = false
|
||||||
|
|
||||||
|
// Страница.
|
||||||
|
if (args.page && args.page !== this.page) {
|
||||||
|
// Инициализипуем страницу.
|
||||||
|
this.page = this.parseArg(args.page, DEFAULTS.page)
|
||||||
|
isCalcSkip = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Лимит.
|
||||||
|
if (args.limit && args.limit !== this.limit) {
|
||||||
|
this.limit = this.parseArg(args.limit, this.limit)
|
||||||
|
isCalcSkip = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка лимита.
|
||||||
|
if (this.limit > this.maxLimit) this.limit = this.maxLimit
|
||||||
|
|
||||||
|
// Общее кол-во.
|
||||||
|
if (args.total && args.total !== this.total) {
|
||||||
|
this.total = Math.abs(args.total)
|
||||||
|
this.pages = Math.ceil(this.total / this.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерасчёт пропускаемых элементов.
|
||||||
|
if (isCalcSkip) {
|
||||||
|
let skip = 0
|
||||||
|
try {
|
||||||
|
skip = (this.page - 1) * this.limit
|
||||||
|
} catch (ex: any) {}
|
||||||
|
this.skip = skip > 0 ? skip : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
toObject(): TPagination {
|
||||||
|
return {
|
||||||
|
page: this.page,
|
||||||
|
limit: this.limit,
|
||||||
|
total: this.total,
|
||||||
|
|
||||||
|
skip: this.skip,
|
||||||
|
pages: this.pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["es2017"],
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user