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 {useLocalStorage} from "../../src/hooks/useLocalStorage";
|
||||
import {ReactNodeLike} from "prop-types";
|
||||
import pako from "pako";
|
||||
import {Dict} from "../../src/types";
|
||||
import {fixedEncodeURIComponent} from "../../src/utils";
|
||||
import {GroupRenameBody, GroupSetFactoryArrayBody, SetFactoryArrayBody} from "../../src/types/ApiSchemasFrontend";
|
||||
|
||||
interface Props {
|
||||
children: ReactNodeLike
|
||||
@@ -68,9 +69,20 @@ interface StoredFile {
|
||||
excludedSuggestions: string[]
|
||||
}
|
||||
|
||||
export const GroupProvider: FC<Props> = ({children, initial}) => {
|
||||
const [excludedSuggestions, setExcludedSuggestions] = useState<string[]>(initial.ignored)
|
||||
const [basicValues, setBasicValues] = useState<string[]>(initial.base)
|
||||
export const postFetchJson = async (url: string, body: Dict<unknown>) => {
|
||||
const res = await fetch(url, {
|
||||
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 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])])
|
||||
}, [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[] = []) => {
|
||||
name = name.replace(/[.$]/g, '')
|
||||
if (name in groups) return false
|
||||
setGroups(groups => {
|
||||
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
|
||||
}, [groups, setGroups])
|
||||
}, [groups, id])
|
||||
const removeGroup = useCallback((name: string) => {
|
||||
name = name.replace(/[.$]/g, '')
|
||||
setGroups(groups => {
|
||||
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) => {
|
||||
if (name === newName) return
|
||||
name = name.replace(/[.$]/g, '')
|
||||
newName = newName.replace(/[.$]/g, '')
|
||||
if (newName in groups) return
|
||||
setGroups(groups => {
|
||||
groups[newName] = {...groups[name], name: newName}
|
||||
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 => {
|
||||
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) => {
|
||||
if (basicValues.includes(uid)) return 'base'
|
||||
else if (exportedFactories.has(uid)) return 'produced'
|
||||
@@ -133,7 +195,7 @@ export const GroupProvider: FC<Props> = ({children, initial}) => {
|
||||
setGroups(value.groups)
|
||||
setBasicValues(value.basicValues)
|
||||
setExcludedSuggestions(value.excludedSuggestions)
|
||||
}, [setBasicValues, setExcludedSuggestions, setGroups])
|
||||
}, [])
|
||||
|
||||
const value: GroupContextType = useMemo(() => ({
|
||||
doNotSuggest,
|
||||
|
||||
@@ -25,6 +25,7 @@ const FactorySelectBase: FC<Props> = ({id, factories, onSetFactories}) => {
|
||||
|
||||
return <Select
|
||||
id={id}
|
||||
instanceId={id}
|
||||
value={state}
|
||||
components={{
|
||||
MultiValueLabel: ({data, innerProps}) => (
|
||||
|
||||
@@ -10,6 +10,9 @@ const nextConfig = {
|
||||
MONGO_DB: envVar.get('MONGO_DB').required().asString(),
|
||||
MONGO_USER: envVar.get('MONGO_USER').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",
|
||||
"deepcopy": "^2.1.0",
|
||||
"env-var": "^7.1.1",
|
||||
"jsonschema": "^1.4.1",
|
||||
"mongodb": "^4.8.1",
|
||||
"next": "12.2.4",
|
||||
"next-superjson-plugin": "^0.3.0",
|
||||
@@ -31,6 +32,7 @@
|
||||
"@types/seedrandom": "^3.0.2",
|
||||
"eslint": "8.21.0",
|
||||
"eslint-config-next": "12.2.4",
|
||||
"json-schema-to-typescript": "^11.0.2",
|
||||
"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 {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')
|
||||
await waitForInitSchemas.resolve()
|
||||
const data = req.body as GroupData
|
||||
|
||||
const uuid = await setGroups(data)
|
||||
res.json({ uuid })
|
||||
}
|
||||
})
|
||||
|
||||
export default handler
|
||||
|
||||
@@ -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