Implemented server routes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import {database} from "./start";
|
||||
import {Dict, Group} from "../types";
|
||||
import {Filter, ObjectId, WithId} from "mongodb";
|
||||
import {Filter, ObjectId} from "mongodb";
|
||||
|
||||
export interface GroupData {
|
||||
groups: Dict<Group>,
|
||||
@@ -8,7 +8,7 @@ export interface GroupData {
|
||||
base: string[]
|
||||
}
|
||||
|
||||
type InsertMeta<T> = T & {
|
||||
export type InsertMeta<T> = T & {
|
||||
createdOn: Date,
|
||||
modifiedOn: Date,
|
||||
accessedOn: Date
|
||||
@@ -16,8 +16,8 @@ type InsertMeta<T> = T & {
|
||||
|
||||
type GroupFilter = Filter<InsertMeta<GroupData>>
|
||||
|
||||
export async function setGroups(data: GroupData) {
|
||||
const collection = (await database.resolve())?.collection<InsertMeta<GroupData>>('setups')
|
||||
export async function setGroups(data: GroupData): Promise<string|undefined> {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return
|
||||
const result = await collection.insertOne({
|
||||
...data,
|
||||
@@ -29,16 +29,82 @@ export async function setGroups(data: GroupData) {
|
||||
return result.insertedId.toString()
|
||||
}
|
||||
|
||||
function getUuid(uuid: string): GroupFilter {
|
||||
return {
|
||||
_id: new ObjectId(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
export async function setFactories(uuid: string, type: 'ignored'|'base', factories: string[]): Promise<boolean> {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return false
|
||||
collection.updateOne(getUuid(uuid), {$set: {[type]: factories} as never})
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
export async function getGroup(uuid: string) {
|
||||
const collection = (await database.resolve())?.collection<InsertMeta<GroupData>>('setups')
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return
|
||||
console.log(uuid)
|
||||
const data = (await collection.findOne({
|
||||
_id: new ObjectId(uuid)
|
||||
} as GroupFilter)) ?? undefined
|
||||
const data = (await collection.findOne(getUuid(uuid))) ?? undefined
|
||||
if (data) {
|
||||
await collection.updateOne({_id: new ObjectId(uuid)}, { $set: {accessedOn: new Date()}})
|
||||
await collection.updateOne(getUuid(uuid), { $set: {accessedOn: new Date()}})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export async function renameGroup(uuid: string, oldName: string, newName: string): Promise<boolean> {
|
||||
oldName = oldName.replace(/[.$]/g, '')
|
||||
newName = newName.replace(/[.$]/g, '')
|
||||
const data = await getGroup(uuid)
|
||||
if (data?.groups && !(newName in data.groups)) {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
console.log("fere", `groups.${oldName}`, `groups.${newName}`)
|
||||
if (!collection) return false
|
||||
await collection.updateOne(getUuid(uuid), { $set: {
|
||||
[`groups.${oldName}.name`]: newName
|
||||
} as never})
|
||||
await collection.updateOne(getUuid(uuid), { $rename: {
|
||||
[`groups.${oldName}`]: `groups.${newName}`
|
||||
}})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function addGroup(uuid: string, name: string): Promise<boolean> {
|
||||
name = name.replace(/[.$]/g, '')
|
||||
const data = await getGroup(uuid)
|
||||
if (data?.groups && !(name in data.groups)) {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return false
|
||||
await collection.updateOne(getUuid(uuid), {$set: {[`groups.${name}`]: {name, exports: [], malls: []} as never}})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function removeGroup(uuid: string, name: string): Promise<boolean> {
|
||||
name = name.replace(/[.$]/g, '')
|
||||
const data = await getGroup(uuid)
|
||||
if (data?.groups && name in data.groups) {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return false
|
||||
console.log(`groups.${name}`)
|
||||
await collection.updateOne(getUuid(uuid), {$unset: {[`groups.${name}`]: ""}})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function setFactoriesOfGroup(uuid: string, name: string, type: 'exports'|'malls', factories: string[]): Promise<boolean> {
|
||||
name = name.replace(/[.$]/g, '')
|
||||
const data = await getGroup(uuid)
|
||||
if (data?.groups && (name in data.groups)) {
|
||||
const collection = (await database.resolve())?.collection('setups')
|
||||
if (!collection) return false
|
||||
await collection.updateOne(getUuid(uuid), {$set: {[`groups.${name}.${type}`]: factories} as never})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Db, MongoClient} from 'mongodb'
|
||||
import {Collection, Db, MongoClient} from 'mongodb'
|
||||
import getConfig from 'next/config'
|
||||
import {Resolvable} from "../utils/Resolvable";
|
||||
import {GroupData, InsertMeta} from "./groups";
|
||||
|
||||
const { serverRuntimeConfig: {
|
||||
MONGO_URL,
|
||||
@@ -9,12 +10,14 @@ const { serverRuntimeConfig: {
|
||||
MONGO_DB
|
||||
} } = getConfig()
|
||||
|
||||
async function getDatabase(): Promise<Db | undefined> {
|
||||
async function getDatabase() {
|
||||
const url = `mongodb://${MONGO_USER ? `${MONGO_USER}:${MONGO_PASS ?? ''}@` : ''}${MONGO_URL}`;
|
||||
const client = new MongoClient(url);
|
||||
await client.connect();
|
||||
console.log('Connected successfully to server')
|
||||
return client.db(MONGO_DB)
|
||||
return client.db(MONGO_DB) as unknown as (Omit<Db, 'collection'> & {
|
||||
collection : (_: 'setups') => Collection<InsertMeta<GroupData>>
|
||||
})
|
||||
}
|
||||
|
||||
export const database = new Resolvable(getDatabase)
|
||||
|
||||
2
src/next-types.d.ts
vendored
2
src/next-types.d.ts
vendored
@@ -11,12 +11,12 @@ declare module 'next/config' {
|
||||
}
|
||||
|
||||
export interface PublicRuntimeConfig {
|
||||
TENANT_TYPE?: string
|
||||
}
|
||||
|
||||
const getConfig: () => {
|
||||
serverRuntimeConfig: ServerRuntimeConfig,
|
||||
publicRuntimeConfig: PublicRuntimeConfig
|
||||
}
|
||||
//export const serConfig = () =>
|
||||
export default getConfig
|
||||
}
|
||||
|
||||
25
src/types/ApiSchemas.ts
Normal file
25
src/types/ApiSchemas.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface GroupRenameBody {
|
||||
newName: string
|
||||
}
|
||||
export interface GroupSetFactoryArrayBody {
|
||||
type: 'exports' | 'malls'
|
||||
factories: string[]
|
||||
}
|
||||
export interface GroupIdParam {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
export interface SetFactoryArrayBody {
|
||||
type: 'ignored' | 'base'
|
||||
factories: string[]
|
||||
}
|
||||
export interface IdParam {
|
||||
id: string
|
||||
}
|
||||
30
src/types/ApiSchemasFrontend.ts
Normal file
30
src/types/ApiSchemasFrontend.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* tslint:disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface GroupRenameBody {
|
||||
newName: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
export interface GroupSetFactoryArrayBody {
|
||||
type: 'exports' | 'malls'
|
||||
factories: string[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
export interface GroupIdParam {
|
||||
id: string
|
||||
name: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
export interface SetFactoryArrayBody {
|
||||
type: 'ignored' | 'base'
|
||||
factories: string[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
export interface IdParam {
|
||||
id: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
7
src/types/FrontendApi.ts
Normal file
7
src/types/FrontendApi.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {ValidationError} from "jsonschema";
|
||||
|
||||
export interface ErrorMessage {
|
||||
message: string // Human readable error message
|
||||
details?: string | string[] // Stacktrace, details, extra information...
|
||||
validation?: ValidationError[]
|
||||
}
|
||||
36
src/utils/errors.ts
Normal file
36
src/utils/errors.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ErrorMessage } from '../types/FrontendApi'
|
||||
import {NextApiHandler, NextApiRequest, NextApiResponse} from 'next'
|
||||
import { logger } from './logger'
|
||||
import {NextFetchEvent, NextMiddleware, NextRequest} from "next/server";
|
||||
|
||||
export class NetworkError extends Error {
|
||||
public content?: ErrorMessage
|
||||
|
||||
constructor(
|
||||
content?: ErrorMessage | string,
|
||||
public throwingError?: unknown,
|
||||
public statusCode = 500
|
||||
) {
|
||||
super((typeof content === 'string' ? content : content?.message) ?? 'NetworkError')
|
||||
this.name = 'NetworkError'
|
||||
this.content = typeof content === 'string' ? { message: content } : content
|
||||
}
|
||||
}
|
||||
|
||||
export function nextHandler(fn: NextApiHandler): NextApiHandler {
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
await fn(req, res)
|
||||
} catch (err) {
|
||||
if (err instanceof NetworkError) {
|
||||
const ntwErr = err as NetworkError
|
||||
if (ntwErr.throwingError) logger.error(ntwErr.throwingError)
|
||||
res.status(ntwErr.statusCode)
|
||||
res.json(ntwErr.content ?? { message: 'An error occurred!' })
|
||||
} else {
|
||||
logger.error(err)
|
||||
res.status(500).json({ message: 'An error occurred!' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/utils/logger.ts
Normal file
7
src/utils/logger.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const logger = {
|
||||
verbose: console.log,
|
||||
debug: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
}
|
||||
142
src/validation/defaults.ts
Normal file
142
src/validation/defaults.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { JSONSchema } from 'json-schema-to-typescript'
|
||||
|
||||
function cloneJSON<T>(source: T) {
|
||||
return JSON.parse(JSON.stringify(source)) as T
|
||||
}
|
||||
export function isObject(item: unknown): item is Record<string, unknown> {
|
||||
return typeof item === 'object' && item !== null && item.toString() === {}.toString()
|
||||
}
|
||||
|
||||
export function merge<T extends Record<string, unknown>, U extends Record<string, unknown>>(
|
||||
target: T,
|
||||
source: U
|
||||
): T & U {
|
||||
const result = cloneJSON(target) as T & U
|
||||
|
||||
for (const key in source) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const tk = result[key]
|
||||
const sk = source[key]
|
||||
if (isObject(tk) && isObject(sk)) {
|
||||
result[key] = merge(tk, sk)
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
result[key] = source[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getLocalRef(path: string, definitions: JSONSchema['definitions']) {
|
||||
const pathSplit = path.replace(/^#\/definitions\//, '').split('/')
|
||||
|
||||
const find = (p: string[], root: JSONSchema['definitions']): JSONSchema => {
|
||||
const key = p.shift()
|
||||
if (!root || !key || !root[key]) {
|
||||
return {}
|
||||
} else if (!p.length) {
|
||||
return root[key]
|
||||
}
|
||||
return find(p, root[key] as JSONSchema['definitions'])
|
||||
}
|
||||
|
||||
const result = find(pathSplit, definitions)
|
||||
|
||||
if (!isObject(result)) {
|
||||
return result
|
||||
}
|
||||
return cloneJSON(result)
|
||||
}
|
||||
|
||||
function mergeAllOf(
|
||||
allOfList: Exclude<JSONSchema['allOf'], undefined>,
|
||||
definitions: JSONSchema['definitions']
|
||||
) {
|
||||
const length = allOfList.length
|
||||
let index = -1
|
||||
let result = {}
|
||||
|
||||
while (++index < length) {
|
||||
let item = allOfList[index]
|
||||
|
||||
item = typeof item.$ref !== 'undefined' ? getLocalRef(item.$ref, definitions) : item
|
||||
|
||||
result = merge(result, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function defaults(schema: JSONSchema, definitions: JSONSchema['definitions']): unknown {
|
||||
if (typeof schema.default !== 'undefined') {
|
||||
return schema.default
|
||||
} else if (typeof schema.allOf !== 'undefined') {
|
||||
const mergedItem = mergeAllOf(schema.allOf, definitions)
|
||||
return defaults(mergedItem, definitions)
|
||||
} else if (typeof schema.$ref !== 'undefined') {
|
||||
const reference = getLocalRef(schema.$ref, definitions)
|
||||
return defaults(reference, definitions)
|
||||
} else if (schema.type === 'object') {
|
||||
if (!schema.properties) {
|
||||
return {}
|
||||
}
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const key in schema.properties) {
|
||||
if (key in schema.properties) {
|
||||
result[key] = defaults(schema.properties[key], definitions)
|
||||
if (typeof result[key] === 'undefined') {
|
||||
delete result[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
} else if (schema.type === 'array') {
|
||||
if (!schema.items) {
|
||||
return []
|
||||
}
|
||||
|
||||
// minimum item count
|
||||
const ct = schema.minItems || 0
|
||||
// tuple-typed arrays
|
||||
if (schema.items instanceof Array) {
|
||||
const values = schema.items.map(item => defaults(item, definitions))
|
||||
// remove undefined items at the end (unless required by minItems)
|
||||
for (let i = values.length - 1; i >= 0; i--) {
|
||||
if (typeof values[i] !== 'undefined') {
|
||||
break
|
||||
}
|
||||
if (i + 1 > ct) {
|
||||
values.pop()
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
// object-typed arrays
|
||||
const value = defaults(schema.items, definitions)
|
||||
if (typeof value === 'undefined') {
|
||||
return []
|
||||
}
|
||||
const values: unknown[] = []
|
||||
for (let i = 0; i < Math.max(1, ct); i++) {
|
||||
values.push(cloneJSON(value))
|
||||
}
|
||||
return values
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getDefaults(
|
||||
schema: JSONSchema,
|
||||
definitions?: JSONSchema['definitions'] | undefined
|
||||
): unknown {
|
||||
if (typeof definitions === 'undefined') {
|
||||
definitions = schema.definitions || {}
|
||||
} else if (isObject(schema.definitions)) {
|
||||
definitions = merge(definitions, schema.definitions)
|
||||
}
|
||||
|
||||
return defaults(cloneJSON(schema), definitions)
|
||||
}
|
||||
113
src/validation/index.ts
Normal file
113
src/validation/index.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Validator } from 'jsonschema'
|
||||
import { getDefaults, isObject, merge } from './defaults'
|
||||
import { TransformedValidatorResult, ValidatorOptions } from './types'
|
||||
import { transformInstance } from './transform'
|
||||
import { compile, JSONSchema } from 'json-schema-to-typescript'
|
||||
import * as fs from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { NetworkError } from '../utils/errors'
|
||||
import getConfig from "next/config";
|
||||
|
||||
const {publicRuntimeConfig: {TENANT_TYPE}} = getConfig()
|
||||
|
||||
const validatorStorage = new Validator()
|
||||
|
||||
export async function addSchema(schemas: JSONSchema[]) {
|
||||
if (TENANT_TYPE === 'local') {
|
||||
const fileName = join('./src/types/ApiSchemas.ts')
|
||||
const fileNameFrontend = join('./src/types/ApiSchemasFrontend.ts')
|
||||
const style = await fs.readFile('./.prettierrc.json', 'utf8').then(JSON.parse)
|
||||
let data = ''
|
||||
let dataFrontend = ''
|
||||
for (const schema of schemas) {
|
||||
if (!schema.id) throw new SyntaxError('Id of schema has to be defined!')
|
||||
validatorStorage.addSchema(schema)
|
||||
// compile schema to interface
|
||||
let entity = await compile(schema, schema.id, {
|
||||
style,
|
||||
bannerComment: data === '' ? undefined : '\n\n\n'
|
||||
})
|
||||
entity = entity.replace(/^( {2})+\[k: string]: unknown\n/gm, '')
|
||||
dataFrontend += entity
|
||||
// remove ? from defaulted properties
|
||||
const defaults = getDefaults(schema)
|
||||
if (isObject(defaults)) {
|
||||
iterObject(defaults, (key, depth) => {
|
||||
entity = entity.replace(
|
||||
RegExp(` {${2 * depth + 2}}${key}\\?`, 'gm'),
|
||||
' '.repeat(2 * depth + 2) + key
|
||||
)
|
||||
})
|
||||
}
|
||||
// add interface to others
|
||||
data += entity
|
||||
}
|
||||
const current = await fs.readFile(fileName, 'utf8').catch(() => undefined)
|
||||
if (current !== data) {
|
||||
await fs.writeFile(fileName, data)
|
||||
await fs.writeFile(fileNameFrontend, dataFrontend)
|
||||
}
|
||||
} else {
|
||||
for (const schema of schemas) {
|
||||
if (!schema.id) throw new SyntaxError('Id of schema has to be defined!')
|
||||
validatorStorage.addSchema(schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validate<T = Record<string, unknown>>(
|
||||
instance: Record<string, unknown>,
|
||||
schemaName: string,
|
||||
options?: ValidatorOptions
|
||||
): TransformedValidatorResult<T> {
|
||||
const schema = validatorStorage.schemas[schemaName] as JSONSchema | undefined
|
||||
if (!schema) throw new SyntaxError(`Schema '${schemaName}' not implemented!`)
|
||||
const defaulted =
|
||||
options?.applyDefaults !== false ? mergeDefault(getDefaults(schema), instance) : instance
|
||||
const transformed =
|
||||
options?.transform !== false ? transformInstance(defaulted, schema) : defaulted
|
||||
const result = validatorStorage.validate(
|
||||
transformed,
|
||||
schema,
|
||||
options
|
||||
) as TransformedValidatorResult<T>
|
||||
if (result.errors.length) {
|
||||
throw new NetworkError(
|
||||
{
|
||||
message: 'Validation error',
|
||||
validation: result.errors
|
||||
},
|
||||
undefined,
|
||||
400
|
||||
)
|
||||
}
|
||||
result.transformed = transformed as T
|
||||
return result
|
||||
}
|
||||
|
||||
export function applyDefaults<T>(schemaName: string, instance: Partial<T>): T {
|
||||
const schema = validatorStorage.schemas[schemaName] as JSONSchema | undefined
|
||||
if (!schema) throw new SyntaxError(`Schema '${schemaName}' not implemented!`)
|
||||
return mergeDefault(getDefaults(schema), instance) as T
|
||||
}
|
||||
|
||||
function mergeDefault(target: unknown, source: unknown) {
|
||||
return isObject(target) && isObject(source) ? merge(target, source) : source
|
||||
}
|
||||
|
||||
function iterObject(
|
||||
object: Record<string, unknown>,
|
||||
cb: (key: string, depth: number, value: unknown) => void
|
||||
) {
|
||||
function iter(o: Record<string, unknown>, d: number) {
|
||||
Object.keys(o).forEach(k => {
|
||||
const next = o[k]
|
||||
if (isObject(next)) {
|
||||
iter(next as Record<string, unknown>, d + 1)
|
||||
} else {
|
||||
cb(k, d, next)
|
||||
}
|
||||
})
|
||||
}
|
||||
iter(object, 0)
|
||||
}
|
||||
54
src/validation/schemas.ts
Normal file
54
src/validation/schemas.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { addSchema } from './index'
|
||||
import { Resolvable } from '../utils/Resolvable'
|
||||
|
||||
export function addSchemas() {
|
||||
return addSchema([
|
||||
{
|
||||
id: '/GroupRenameBody',
|
||||
type: 'object',
|
||||
required: ['newName'],
|
||||
properties: {
|
||||
newName: { type: 'string', minLength: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '/GroupSetFactoryArrayBody',
|
||||
type: 'object',
|
||||
required: ['type', 'factories'],
|
||||
properties: {
|
||||
type: {type: 'string', enum: ['exports', 'malls']},
|
||||
factories: {type: 'array', items: {type: 'string', minLength: 3}}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'GroupIdParam',
|
||||
type: 'object',
|
||||
required: ['id', 'name'],
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 24, maxLength: 24 },
|
||||
name: { type: 'string', minLength: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '/SetFactoryArrayBody',
|
||||
type: 'object',
|
||||
required: ['type', 'factories'],
|
||||
properties: {
|
||||
type: {type: 'string', enum: ['ignored', 'base']},
|
||||
factories: {type: 'array', items: {type: 'string', minLength: 3}}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'IdParam',
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string', minLength: 24, maxLength: 24 },
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
export const waitForInitSchemas = new Resolvable(() => {
|
||||
return addSchemas()
|
||||
})
|
||||
72
src/validation/transform.ts
Normal file
72
src/validation/transform.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Schema, validate, ValidationError } from 'jsonschema'
|
||||
import deepcopy from 'deepcopy'
|
||||
import { JSONSchema } from 'json-schema-to-typescript'
|
||||
|
||||
const types: Record<
|
||||
string,
|
||||
{
|
||||
test: (_: unknown) => boolean
|
||||
cast: (_: unknown) => unknown
|
||||
redo?: boolean
|
||||
}
|
||||
> = {
|
||||
integer: {
|
||||
test: val => !isNaN(val as number),
|
||||
cast: val => parseInt(`${val}`, 10)
|
||||
},
|
||||
number: {
|
||||
test: val => !isNaN(val as number),
|
||||
cast: val => parseFloat(`${val}`)
|
||||
},
|
||||
boolean: {
|
||||
test: val => val === true || val === false,
|
||||
cast: val => val === 'true' || val === '1'
|
||||
},
|
||||
array: {
|
||||
test: Array.isArray,
|
||||
cast: val => `${val}`.split(',').map(x => x.trim()),
|
||||
redo: true
|
||||
}
|
||||
}
|
||||
|
||||
function set(obj: Record<string, unknown>, is: (string | number)[], value: unknown): void {
|
||||
if (is.length === 1 && value !== undefined) {
|
||||
obj[is[0]] = value
|
||||
} else {
|
||||
set(obj[is[0]] as Record<string, unknown>, is.slice(1), value)
|
||||
}
|
||||
}
|
||||
|
||||
function get(obj: Record<string, unknown>, is: (string | number)[]): unknown {
|
||||
if (is.length === 0) {
|
||||
return obj
|
||||
}
|
||||
return get(obj[is[0]] as Record<string, unknown>, is.slice(1))
|
||||
}
|
||||
|
||||
export function transformInstance(instance: unknown, schema: JSONSchema): unknown {
|
||||
const errors = validate(instance, schema).errors
|
||||
|
||||
const BASE_NAME = 'instance'
|
||||
const carrier = {
|
||||
[BASE_NAME]: deepcopy(instance)
|
||||
}
|
||||
|
||||
const shouldRedo = errors
|
||||
.filter(error => error.name === 'type')
|
||||
.filter(error => typeof error.schema !== 'string' && `${error.schema.type}` in types)
|
||||
.map((error: ValidationError & { schema: Schema }) => {
|
||||
const type = types[`${error.schema.type}`]
|
||||
const path = [BASE_NAME, ...error.path]
|
||||
const val = type.cast(get(carrier, path))
|
||||
|
||||
if (type.test(val)) {
|
||||
set(carrier, path, val)
|
||||
}
|
||||
return type.redo
|
||||
})
|
||||
|
||||
return shouldRedo.some(x => x)
|
||||
? transformInstance(carrier[BASE_NAME], schema)
|
||||
: carrier[BASE_NAME]
|
||||
}
|
||||
10
src/validation/types.ts
Normal file
10
src/validation/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Options, ValidatorResult } from 'jsonschema'
|
||||
|
||||
export interface ValidatorOptions extends Options {
|
||||
applyDefaults?: boolean
|
||||
transform?: boolean
|
||||
}
|
||||
|
||||
export interface TransformedValidatorResult<T> extends ValidatorResult {
|
||||
transformed: T
|
||||
}
|
||||
Reference in New Issue
Block a user