Skip to content

Commit

Permalink
feat: config validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ade Yahya Prasetyo committed Mar 25, 2021
1 parent 4ed157e commit 23df0e5
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 48 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
"@graphql-mesh/graphql": "^0.13.13",
"@graphql-mesh/openapi": "^0.10.6",
"@types/react": "^17.0.2",
"@warungpintar/ninshu": "^0.0.2",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"commander": "^7.1.0",
"cookie": "^0.4.1",
"date-fns": "^2.19.0",
Expand Down
80 changes: 48 additions & 32 deletions src/cli/runServer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as esbuild from 'esbuild';
import path from 'path';
import chalk from 'chalk';
import { pipe } from 'fp-ts/function';
import { map, mapLeft } from 'fp-ts/Either';
import { bimap } from 'fp-ts/Either';

import { run } from '../express';
import { getConfig } from '../config';
Expand All @@ -11,32 +12,29 @@ import { getPatResolvers } from '../utils';
import { PATH_RESOLVER_DIR } from '../constant';

const runServer = (config: string, port: number) => {
console.clear();
const filePath = path.join(process.cwd(), config ?? '.warlock.yaml');
pipe(
getConfig(filePath),
mapLeft(logger.error),
map((c) => {
const pathResolvers: string[] = getPatResolvers(c);

console.time(`building ${pathResolvers.length} resolvers`);
pathResolvers.forEach((resolverPath) => {
esbuild.buildSync({
entryPoints: [path.join(process.cwd(), resolverPath)],
bundle: true,
minify: true,
platform: 'node',
format: 'cjs',
outfile: path.join(PATH_RESOLVER_DIR, resolverPath),
bimap(
(e) => {
// watch config
fileWatcher(() => {
runServer(config, port);
})(filePath);
e.forEach((errItem) => {
logger.error(
chalk.underline.red(errItem?.problem) +
' | ' +
chalk.yellow(errItem?.reason),
);
});
});
logger.info('building resolvers');
console.timeEnd(`building ${pathResolvers.length} resolvers`);
},
(c) => {
const pathResolvers: string[] = getPatResolvers(c);

const server = run({ port, config: c });

// watch resolvers
pathResolvers.forEach((resolverPath) => {
fileWatcher(() => {
console.time(`building ${pathResolvers.length} resolvers`);
pathResolvers.forEach((resolverPath) => {
esbuild.buildSync({
entryPoints: [path.join(process.cwd(), resolverPath)],
bundle: true,
Expand All @@ -45,18 +43,36 @@ const runServer = (config: string, port: number) => {
format: 'cjs',
outfile: path.join(PATH_RESOLVER_DIR, resolverPath),
});
logger.info('restaring server');
})(path.join(process.cwd(), resolverPath));
});
});
logger.info('building resolvers');
console.timeEnd(`building ${pathResolvers.length} resolvers`);

// watch config
fileWatcher(() => {
server.close(() => {
runServer(config, port);
logger.info('restaring server');
const server = run({ port, config: c });

// watch resolvers
pathResolvers.forEach((resolverPath) => {
fileWatcher(() => {
esbuild.buildSync({
entryPoints: [path.join(process.cwd(), resolverPath)],
bundle: true,
minify: true,
platform: 'node',
format: 'cjs',
outfile: path.join(PATH_RESOLVER_DIR, resolverPath),
});
logger.info('restaring server');
})(path.join(process.cwd(), resolverPath));
});
})(filePath);
}),

// watch config
fileWatcher(() => {
server.close(() => {
runServer(config, port);
logger.info('restaring server');
});
})(filePath);
},
),
);
};

Expand Down
138 changes: 122 additions & 16 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import * as yaml from 'js-yaml';
// import path from 'path';
import * as fs from 'fs';
import * as E from 'fp-ts/lib/Either';
import * as IOE from 'fp-ts/lib/IOEither';
import * as F from 'fp-ts/lib/function';
import * as T from './types';
/**
* TODO : make it utils for this project
*/
import {
validate,
validateString,
validateRequired,
validateFqdn,
} from '@warungpintar/ninshu';

const VERB_LIST = ['get', 'post', 'delete', 'patch', 'put'];

const validateUrl = validate((url: string) => {
try {
// eslint-disable-next-line no-new
new URL(url);
return true;
} catch {
return false;
}
});

const validateEmptyQs = validate((url: URL) => {
return !url.search;
});

type ConfigError = {
problem: string;
reason: string;
};

const readFileSync = (file: string): IOE.IOEither<Error, string> =>
IOE.tryCatch(() => fs.readFileSync(file, 'utf8'), E.toError);

Expand All @@ -16,19 +40,101 @@ const emptyConfig: T.Config = {
graphql: undefined,
};

const returnValidObjectOrEmptyObject = (val: any): (() => T.Config) => () =>
typeof val === 'object' ? val : emptyConfig;

const yamlLoad = (
file: string,
opts?: yaml.LoadOptions,
): IOE.IOEither<Error, T.Config> =>
IOE.tryCatch(() => {
const loaded = yaml.load(file, opts);
switch (typeof loaded) {
case 'object':
return loaded ?? emptyConfig;
default:
return emptyConfig;
}
}, E.toError);
): IOE.IOEither<ConfigError[], T.Config> =>
IOE.tryCatch(
F.pipe(yaml.load(file, opts), returnValidObjectOrEmptyObject),
(reason) => [
{
problem: file,
reason: String(reason),
},
],
);

const validateConfig = (
config: T.Config,
): E.Either<ConfigError[], T.Config> => {
const { rest } = config;
const sources = rest?.sources ?? [];

const errors = sources.reduce((prev, next) => {
const origin = next.origin ?? '';
const transforms = next.transforms ?? {};
const _origin = origin.replace(/^(http|https):\/\//, '');
const concatPrev = (e: ConfigError) => {
prev = [...prev, e];
};

// validate origin
F.flow(
validateRequired("origin can't be nil"),
E.chain(validateString('origin should be string')),
E.chain(validateFqdn('invalid origin')),
E.mapLeft((e) => {
concatPrev({ problem: origin ?? 'undefined origin', reason: e });
}),
)(_origin);

// validate path
Object.keys(transforms).forEach((tpath) => {
F.flow(
validateRequired("path can't be nil"),
E.chain(validateString('path should be string')),
E.map((val) => origin.concat(val)),
E.chain(validateUrl('broken origin')),
E.map((val) => new URL(val)),
E.chain(validateEmptyQs("path can't contain query string")),
E.mapLeft((e) => {
concatPrev({ problem: tpath, reason: e });
}),
)(tpath);
});

Object.values(transforms).forEach((tval) => {
const verbs = Object.keys(tval ?? {});
const verbsHandler = Object.values(tval ?? {});

// validate verb
verbs.forEach((verb) => {
if (!VERB_LIST.includes(verb.toLocaleLowerCase())) {
concatPrev({
problem: verb,
reason: 'only accept '.concat(VERB_LIST.join(', ')),
});
}
});

// validate handler
verbsHandler.forEach((handlers: T.WarlockConfig.Field[]) => {
handlers.forEach((handler) => {
const parts = handler.field?.split('.') ?? [];
// @TODO: validate resolver

if (parts[0] !== 'root') {
concatPrev({
problem: handler.field ?? 'undefined field',
reason: 'field should always starts with root',
});
}
});
});
});

return prev;
}, [] as ConfigError[]);

if (errors.length > 0) {
return E.left(errors);
}

return E.right(config);
};

/**
* this function return WarlockConfig
Expand All @@ -44,11 +150,11 @@ export const getConfig = (file: string) => {
const p = F.pipe(
file,
readFileSync,
IOE.bimap(E.toError, yamlLoad),
IOE.bimap(() => [{ problem: file, reason: "can't read file" }], yamlLoad),
IOE.flatten,
);

return p();
return E.chain(validateConfig)(p());
};

export const getConfigWithDefault = (file: string) => {
Expand Down
27 changes: 27 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,13 @@
dependencies:
regenerator-runtime "^0.13.4"

"@babel/runtime@^7.12.13":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
dependencies:
regenerator-runtime "^0.13.4"

"@babel/template@^7.10.4", "@babel/template@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
Expand Down Expand Up @@ -3032,6 +3039,16 @@
eslint-config-prettier "^6.7.0"
eslint-config-xo-typescript "^0.23.0"

"@warungpintar/ninshu@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@warungpintar/ninshu/-/ninshu-0.0.2.tgz#a4ec5fb8baa8616faaabe97b60c6bd2cb81b5e96"
integrity sha512-fr7EVCGglby5wUUKp2bRmi5WBwcLO0WjSpz+/qTtozHMyrDNi7kNpscoPAnFix0zh2P2Vtl/tSpydAQRCNo2OA==
dependencies:
"@babel/runtime" "^7.12.13"
expressive-ts "^0.0.2"
fp-ts "^2.9.5"
io-ts "^2.2.14"

"@warungpintar/prettier-config@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@warungpintar/prettier-config/-/prettier-config-0.1.2.tgz#8e14d0d9aee272084ec29b2af615abe883dafb13"
Expand Down Expand Up @@ -5223,6 +5240,11 @@ [email protected], express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"

expressive-ts@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/expressive-ts/-/expressive-ts-0.0.2.tgz#3ce75474879e610c71cfdfb88d106e8164f26cf3"
integrity sha512-FypiruhZHavVe7x7uLGRv34MwKdBTSMik57pDyHlLw44y3ivxnLfyqmcKo6sAclagU5LBllpdeE06B1HOSHYVw==

ext@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
Expand Down Expand Up @@ -6233,6 +6255,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"

io-ts@^2.2.14:
version "2.2.16"
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.16.tgz#597dffa03db1913fc318c9c6df6931cb4ed808b2"
integrity sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q==

ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
Expand Down

0 comments on commit 23df0e5

Please sign in to comment.