Implemented server routes
This commit is contained in:
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.cache
|
||||||
|
public
|
||||||
|
static
|
||||||
|
coverage
|
||||||
18
.prettierrc.json
Normal file
18
.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.css",
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import {createContext, FC, useCallback, useContext, useMemo, useState} from "react";
|
import {createContext, FC, useCallback, useContext, useEffect, useMemo, useState} from "react";
|
||||||
import {Group} from "../../src/types";
|
import {Group} from "../../src/types";
|
||||||
import {useLocalStorage} from "../../src/hooks/useLocalStorage";
|
|
||||||
import {ReactNodeLike} from "prop-types";
|
import {ReactNodeLike} from "prop-types";
|
||||||
import pako from "pako";
|
import pako from "pako";
|
||||||
import {Dict} from "../../src/types";
|
import {Dict} from "../../src/types";
|
||||||
|
import {fixedEncodeURIComponent} from "../../src/utils";
|
||||||
|
import {GroupRenameBody, GroupSetFactoryArrayBody, SetFactoryArrayBody} from "../../src/types/ApiSchemasFrontend";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNodeLike
|
children: ReactNodeLike
|
||||||
@@ -68,9 +69,20 @@ interface StoredFile {
|
|||||||
excludedSuggestions: string[]
|
excludedSuggestions: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupProvider: FC<Props> = ({children, initial}) => {
|
export const postFetchJson = async (url: string, body: Dict<unknown>) => {
|
||||||
const [excludedSuggestions, setExcludedSuggestions] = useState<string[]>(initial.ignored)
|
const res = await fetch(url, {
|
||||||
const [basicValues, setBasicValues] = useState<string[]>(initial.base)
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GroupProvider: FC<Props> = ({children, id, initial}) => {
|
||||||
|
const [excludedSuggestions, _setExcludedSuggestions] = useState<string[]>(initial.ignored)
|
||||||
|
const [basicValues, _setBasicValues] = useState<string[]>(initial.base)
|
||||||
const [groups, setGroups] = useState<Dict<Group>>(initial.groups)
|
const [groups, setGroups] = useState<Dict<Group>>(initial.groups)
|
||||||
|
|
||||||
const doNotSuggest = useMemo<Set<string>>(() => {
|
const doNotSuggest = useMemo<Set<string>>(() => {
|
||||||
@@ -81,35 +93,85 @@ export const GroupProvider: FC<Props> = ({children, initial}) => {
|
|||||||
return new Set([...Object.values(groups).flatMap(group => [...group.exports])])
|
return new Set([...Object.values(groups).flatMap(group => [...group.exports])])
|
||||||
}, [groups])
|
}, [groups])
|
||||||
|
|
||||||
|
const setExcludedSuggestions = useCallback<typeof _setExcludedSuggestions>((val) => {
|
||||||
|
_setExcludedSuggestions(val)
|
||||||
|
postFetchJson(`/api/${fixedEncodeURIComponent(id)}/factories`, {
|
||||||
|
type: 'ignored',
|
||||||
|
factories: val
|
||||||
|
} as SetFactoryArrayBody)
|
||||||
|
.catch(console.error)
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
const setBasicValues = useCallback<typeof _setBasicValues>((val) => {
|
||||||
|
_setBasicValues(val)
|
||||||
|
postFetchJson(`/api/${fixedEncodeURIComponent(id)}/factories`, {
|
||||||
|
type: 'base',
|
||||||
|
factories: val
|
||||||
|
} as SetFactoryArrayBody)
|
||||||
|
.catch(console.error)
|
||||||
|
}, [id])
|
||||||
|
|
||||||
const addGroup = useCallback((name: string, exports: string[] = [], malls: string[] = []) => {
|
const addGroup = useCallback((name: string, exports: string[] = [], malls: string[] = []) => {
|
||||||
|
name = name.replace(/[.$]/g, '')
|
||||||
if (name in groups) return false
|
if (name in groups) return false
|
||||||
setGroups(groups => {
|
setGroups(groups => {
|
||||||
groups[name] = { name, exports, malls }
|
groups[name] = { name, exports, malls }
|
||||||
return groups
|
return {...groups}
|
||||||
})
|
})
|
||||||
|
;(async () => {
|
||||||
|
await postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/add`, {})
|
||||||
|
if (exports.length) {
|
||||||
|
await postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, {
|
||||||
|
type: 'exports',
|
||||||
|
factories: exports
|
||||||
|
} as GroupSetFactoryArrayBody)
|
||||||
|
}
|
||||||
|
if (malls.length) {
|
||||||
|
await postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, {
|
||||||
|
type: 'malls',
|
||||||
|
factories: exports
|
||||||
|
} as GroupSetFactoryArrayBody)
|
||||||
|
}
|
||||||
|
})().catch(console.error)
|
||||||
return true
|
return true
|
||||||
}, [groups, setGroups])
|
}, [groups, id])
|
||||||
const removeGroup = useCallback((name: string) => {
|
const removeGroup = useCallback((name: string) => {
|
||||||
|
name = name.replace(/[.$]/g, '')
|
||||||
setGroups(groups => {
|
setGroups(groups => {
|
||||||
delete groups[name]
|
delete groups[name]
|
||||||
return groups
|
console.log(groups[name])
|
||||||
|
return {...groups}
|
||||||
})
|
})
|
||||||
}, [setGroups])
|
postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/remove`, {})
|
||||||
|
.catch(console.error)
|
||||||
|
}, [id])
|
||||||
const renameGroup = useCallback((name: string, newName: string) => {
|
const renameGroup = useCallback((name: string, newName: string) => {
|
||||||
if (name === newName) return
|
name = name.replace(/[.$]/g, '')
|
||||||
|
newName = newName.replace(/[.$]/g, '')
|
||||||
|
if (newName in groups) return
|
||||||
setGroups(groups => {
|
setGroups(groups => {
|
||||||
groups[newName] = {...groups[name], name: newName}
|
groups[newName] = {...groups[name], name: newName}
|
||||||
delete groups[name]
|
delete groups[name]
|
||||||
return groups
|
return {...groups}
|
||||||
})
|
})
|
||||||
}, [setGroups])
|
postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/rename`, {
|
||||||
|
newName
|
||||||
|
} as GroupRenameBody)
|
||||||
|
.catch(console.error)
|
||||||
|
}, [groups, id])
|
||||||
|
|
||||||
const setFactories = useCallback((name: string, factories: string[], type: 'inputs'|'intermediates'|'exports'|'malls') => {
|
const setFactories = useCallback((name: string, factories: string[], type: 'exports'|'malls') => {
|
||||||
|
name = name.replace(/[.$]/g, '')
|
||||||
setGroups(groups => {
|
setGroups(groups => {
|
||||||
groups[name] = {...groups[name], [type]: factories}
|
groups[name] = {...groups[name], [type]: factories}
|
||||||
return groups
|
return {...groups}
|
||||||
})
|
})
|
||||||
}, [setGroups])
|
postFetchJson(`/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, {
|
||||||
|
type,
|
||||||
|
factories
|
||||||
|
} as GroupSetFactoryArrayBody)
|
||||||
|
.catch(console.error)
|
||||||
|
}, [id])
|
||||||
const getInputType = useCallback((uid: string) => {
|
const getInputType = useCallback((uid: string) => {
|
||||||
if (basicValues.includes(uid)) return 'base'
|
if (basicValues.includes(uid)) return 'base'
|
||||||
else if (exportedFactories.has(uid)) return 'produced'
|
else if (exportedFactories.has(uid)) return 'produced'
|
||||||
@@ -133,7 +195,7 @@ export const GroupProvider: FC<Props> = ({children, initial}) => {
|
|||||||
setGroups(value.groups)
|
setGroups(value.groups)
|
||||||
setBasicValues(value.basicValues)
|
setBasicValues(value.basicValues)
|
||||||
setExcludedSuggestions(value.excludedSuggestions)
|
setExcludedSuggestions(value.excludedSuggestions)
|
||||||
}, [setBasicValues, setExcludedSuggestions, setGroups])
|
}, [])
|
||||||
|
|
||||||
const value: GroupContextType = useMemo(() => ({
|
const value: GroupContextType = useMemo(() => ({
|
||||||
doNotSuggest,
|
doNotSuggest,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const FactorySelectBase: FC<Props> = ({id, factories, onSetFactories}) => {
|
|||||||
|
|
||||||
return <Select
|
return <Select
|
||||||
id={id}
|
id={id}
|
||||||
|
instanceId={id}
|
||||||
value={state}
|
value={state}
|
||||||
components={{
|
components={{
|
||||||
MultiValueLabel: ({data, innerProps}) => (
|
MultiValueLabel: ({data, innerProps}) => (
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ const nextConfig = {
|
|||||||
MONGO_DB: envVar.get('MONGO_DB').required().asString(),
|
MONGO_DB: envVar.get('MONGO_DB').required().asString(),
|
||||||
MONGO_USER: envVar.get('MONGO_USER').required().asString(),
|
MONGO_USER: envVar.get('MONGO_USER').required().asString(),
|
||||||
MONGO_PASS: envVar.get('MONGO_PASS').required().asString()
|
MONGO_PASS: envVar.get('MONGO_PASS').required().asString()
|
||||||
|
},
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
TENANT_TYPE: envVar.get('TENANT_TYPE').asString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"env-var": "^7.1.1",
|
"env-var": "^7.1.1",
|
||||||
|
"jsonschema": "^1.4.1",
|
||||||
"mongodb": "^4.8.1",
|
"mongodb": "^4.8.1",
|
||||||
"next": "12.2.4",
|
"next": "12.2.4",
|
||||||
"next-superjson-plugin": "^0.3.0",
|
"next-superjson-plugin": "^0.3.0",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"@types/seedrandom": "^3.0.2",
|
"@types/seedrandom": "^3.0.2",
|
||||||
"eslint": "8.21.0",
|
"eslint": "8.21.0",
|
||||||
"eslint-config-next": "12.2.4",
|
"eslint-config-next": "12.2.4",
|
||||||
|
"json-schema-to-typescript": "^11.0.2",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
pages/api/[id]/factories.ts
Normal file
17
pages/api/[id]/factories.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {nextHandler} from "../../../src/utils/errors";
|
||||||
|
import {validate} from "../../../src/validation";
|
||||||
|
import {IdParam, SetFactoryArrayBody} from "../../../src/types/ApiSchemas";
|
||||||
|
import {setFactories} from "../../../src/database/groups";
|
||||||
|
import {waitForInitSchemas} from "../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
const { transformed: params } = validate<IdParam>(req.query, '/IdParam')
|
||||||
|
const { transformed: body } = validate<SetFactoryArrayBody>(req.body, '/SetFactoryArrayBody')
|
||||||
|
|
||||||
|
const success = await setFactories(params.id, body.type, body.factories)
|
||||||
|
res.json({ success })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
16
pages/api/[id]/group/[name]/add.ts
Normal file
16
pages/api/[id]/group/[name]/add.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {nextHandler} from "../../../../../src/utils/errors";
|
||||||
|
import {validate} from "../../../../../src/validation";
|
||||||
|
import {GroupIdParam} from "../../../../../src/types/ApiSchemas";
|
||||||
|
import {addGroup} from "../../../../../src/database/groups";
|
||||||
|
import {waitForInitSchemas} from "../../../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
const { transformed: params } = validate<GroupIdParam>(req.query, '/GroupIdParam')
|
||||||
|
|
||||||
|
const success = await addGroup(params.id, params.name)
|
||||||
|
res.json({ success })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
17
pages/api/[id]/group/[name]/factories.ts
Normal file
17
pages/api/[id]/group/[name]/factories.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {nextHandler} from "../../../../../src/utils/errors";
|
||||||
|
import {validate} from "../../../../../src/validation";
|
||||||
|
import {GroupIdParam, GroupSetFactoryArrayBody} from "../../../../../src/types/ApiSchemas";
|
||||||
|
import {setFactoriesOfGroup} from "../../../../../src/database/groups";
|
||||||
|
import {waitForInitSchemas} from "../../../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
const { transformed: params } = validate<GroupIdParam>(req.query, '/GroupIdParam')
|
||||||
|
const { transformed: body } = validate<GroupSetFactoryArrayBody>(req.body, '/GroupSetFactoryArrayBody')
|
||||||
|
|
||||||
|
const success = await setFactoriesOfGroup(params.id, params.name, body.type, body.factories)
|
||||||
|
res.json({ success })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
16
pages/api/[id]/group/[name]/remove.ts
Normal file
16
pages/api/[id]/group/[name]/remove.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {nextHandler} from "../../../../../src/utils/errors";
|
||||||
|
import {validate} from "../../../../../src/validation";
|
||||||
|
import {GroupIdParam} from "../../../../../src/types/ApiSchemas";
|
||||||
|
import {removeGroup} from "../../../../../src/database/groups";
|
||||||
|
import {waitForInitSchemas} from "../../../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
const { transformed: params } = validate<GroupIdParam>(req.query, '/GroupIdParam')
|
||||||
|
|
||||||
|
const success = await removeGroup(params.id, params.name)
|
||||||
|
res.json({ success })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
17
pages/api/[id]/group/[name]/rename.ts
Normal file
17
pages/api/[id]/group/[name]/rename.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {nextHandler} from "../../../../../src/utils/errors";
|
||||||
|
import {validate} from "../../../../../src/validation";
|
||||||
|
import {GroupIdParam, GroupRenameBody} from "../../../../../src/types/ApiSchemas";
|
||||||
|
import {renameGroup} from "../../../../../src/database/groups";
|
||||||
|
import {waitForInitSchemas} from "../../../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
const { transformed: params } = validate<GroupIdParam>(req.query, '/GroupIdParam')
|
||||||
|
const { transformed: body } = validate<GroupRenameBody>(req.body, '/GroupRenameBody')
|
||||||
|
|
||||||
|
const success = await renameGroup(params.id, params.name, body.newName)
|
||||||
|
res.json({ success })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
15
pages/api/dev/schemas.ts
Normal file
15
pages/api/dev/schemas.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {GroupData, setGroups} from "../../../src/database/groups";
|
||||||
|
import {NetworkError, nextHandler} from "../../../src/utils/errors";
|
||||||
|
import getConfig from "next/config";
|
||||||
|
import {addSchemas, waitForInitSchemas} from "../../../src/validation/schemas";
|
||||||
|
|
||||||
|
const {publicRuntimeConfig: {TENANT_TYPE}} = getConfig()
|
||||||
|
|
||||||
|
const handler = nextHandler(async (req, res) => {
|
||||||
|
if (req.method !== 'GET') throw new NetworkError('Invalid method')
|
||||||
|
if (TENANT_TYPE !== 'local') throw new NetworkError('Not allowed', undefined, 400)
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
|
res.json({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default handler
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import {NextApiHandler} from "next";
|
|
||||||
import {GroupData, setGroups} from "../../src/database/groups";
|
import {GroupData, setGroups} from "../../src/database/groups";
|
||||||
|
import {nextHandler} from "../../src/utils/errors";
|
||||||
|
import {waitForInitSchemas} from "../../src/validation/schemas";
|
||||||
|
|
||||||
const handler: NextApiHandler = async (req, res) => {
|
const handler = nextHandler(async (req, res) => {
|
||||||
if (req.method !== 'POST') throw new Error('Invalid method')
|
if (req.method !== 'POST') throw new Error('Invalid method')
|
||||||
|
await waitForInitSchemas.resolve()
|
||||||
const data = req.body as GroupData
|
const data = req.body as GroupData
|
||||||
|
|
||||||
const uuid = await setGroups(data)
|
const uuid = await setGroups(data)
|
||||||
res.json({ uuid })
|
res.json({ uuid })
|
||||||
}
|
})
|
||||||
|
|
||||||
export default handler
|
export default handler
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {database} from "./start";
|
import {database} from "./start";
|
||||||
import {Dict, Group} from "../types";
|
import {Dict, Group} from "../types";
|
||||||
import {Filter, ObjectId, WithId} from "mongodb";
|
import {Filter, ObjectId} from "mongodb";
|
||||||
|
|
||||||
export interface GroupData {
|
export interface GroupData {
|
||||||
groups: Dict<Group>,
|
groups: Dict<Group>,
|
||||||
@@ -8,7 +8,7 @@ export interface GroupData {
|
|||||||
base: string[]
|
base: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type InsertMeta<T> = T & {
|
export type InsertMeta<T> = T & {
|
||||||
createdOn: Date,
|
createdOn: Date,
|
||||||
modifiedOn: Date,
|
modifiedOn: Date,
|
||||||
accessedOn: Date
|
accessedOn: Date
|
||||||
@@ -16,8 +16,8 @@ type InsertMeta<T> = T & {
|
|||||||
|
|
||||||
type GroupFilter = Filter<InsertMeta<GroupData>>
|
type GroupFilter = Filter<InsertMeta<GroupData>>
|
||||||
|
|
||||||
export async function setGroups(data: GroupData) {
|
export async function setGroups(data: GroupData): Promise<string|undefined> {
|
||||||
const collection = (await database.resolve())?.collection<InsertMeta<GroupData>>('setups')
|
const collection = (await database.resolve())?.collection('setups')
|
||||||
if (!collection) return
|
if (!collection) return
|
||||||
const result = await collection.insertOne({
|
const result = await collection.insertOne({
|
||||||
...data,
|
...data,
|
||||||
@@ -29,16 +29,82 @@ export async function setGroups(data: GroupData) {
|
|||||||
return result.insertedId.toString()
|
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) {
|
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
|
if (!collection) return
|
||||||
console.log(uuid)
|
const data = (await collection.findOne(getUuid(uuid))) ?? undefined
|
||||||
const data = (await collection.findOne({
|
|
||||||
_id: new ObjectId(uuid)
|
|
||||||
} as GroupFilter)) ?? undefined
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await collection.updateOne({_id: new ObjectId(uuid)}, { $set: {accessedOn: new Date()}})
|
await collection.updateOne(getUuid(uuid), { $set: {accessedOn: new Date()}})
|
||||||
}
|
}
|
||||||
return data
|
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 getConfig from 'next/config'
|
||||||
import {Resolvable} from "../utils/Resolvable";
|
import {Resolvable} from "../utils/Resolvable";
|
||||||
|
import {GroupData, InsertMeta} from "./groups";
|
||||||
|
|
||||||
const { serverRuntimeConfig: {
|
const { serverRuntimeConfig: {
|
||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
@@ -9,12 +10,14 @@ const { serverRuntimeConfig: {
|
|||||||
MONGO_DB
|
MONGO_DB
|
||||||
} } = getConfig()
|
} } = getConfig()
|
||||||
|
|
||||||
async function getDatabase(): Promise<Db | undefined> {
|
async function getDatabase() {
|
||||||
const url = `mongodb://${MONGO_USER ? `${MONGO_USER}:${MONGO_PASS ?? ''}@` : ''}${MONGO_URL}`;
|
const url = `mongodb://${MONGO_USER ? `${MONGO_USER}:${MONGO_PASS ?? ''}@` : ''}${MONGO_URL}`;
|
||||||
const client = new MongoClient(url);
|
const client = new MongoClient(url);
|
||||||
await client.connect();
|
await client.connect();
|
||||||
console.log('Connected successfully to server')
|
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)
|
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 {
|
export interface PublicRuntimeConfig {
|
||||||
|
TENANT_TYPE?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfig: () => {
|
const getConfig: () => {
|
||||||
serverRuntimeConfig: ServerRuntimeConfig,
|
serverRuntimeConfig: ServerRuntimeConfig,
|
||||||
publicRuntimeConfig: PublicRuntimeConfig
|
publicRuntimeConfig: PublicRuntimeConfig
|
||||||
}
|
}
|
||||||
//export const serConfig = () =>
|
|
||||||
export default getConfig
|
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