Start initial typescript conversion, and add validation debugging

Add webpack config and dependencies to have typescript built, and also
convert src/lib/validation.coffee to typescript.

In this conversion I also added a lot of debugging which should help the
upcoming local mode development.

Change-type: minor
Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
Cameron Diver 2018-04-27 14:27:42 +01:00
parent 78107b1199
commit cfddbf65e4
No known key found for this signature in database
GPG Key ID: 69264F9C923F55C1
6 changed files with 465 additions and 97 deletions

View File

@ -38,7 +38,7 @@ COPY package.json /usr/src/app/
RUN JOBS=MAX npm install --no-optional --unsafe-perm
COPY webpack.config.js fix-jsonstream.js hardcode-migrations.js /usr/src/app/
COPY webpack.config.js fix-jsonstream.js hardcode-migrations.js tsconfig.json /usr/src/app/
COPY src /usr/src/app/src
COPY test /usr/src/app/test

View File

@ -12,9 +12,10 @@
"build": "webpack",
"lint": "resin-lint src/ test/",
"versionist": "versionist",
"test": "npm run lint && mocha -r coffee-script/register test/**/*"
"test": "npm run lint && mocha -r ts-node/register -r coffee-script/register test/**/*"
},
"dependencies": {
"@types/lodash": "^4.14.108",
"mkfifo": "^0.1.5",
"sqlite3": "^3.1.0"
},
@ -47,6 +48,7 @@
"mixpanel": "0.0.20",
"mkdirp": "^0.5.1",
"mocha": "^5.0.5",
"mochainon": "^2.0.0",
"network-checker": "~0.0.5",
"node-loader": "^0.6.0",
"null-loader": "^0.1.1",
@ -61,7 +63,10 @@
"semver": "^5.3.0",
"semver-regex": "^1.0.0",
"shell-quote": "^1.6.1",
"ts-loader": "^3.5.0",
"ts-node": "^6.0.1",
"typed-error": "~0.1.0",
"typescript": "^2.8.3",
"uglifyjs-webpack-plugin": "^1.0.1",
"versionist": "^2.8.0",
"webpack": "^3.0.0"

View File

@ -1,94 +0,0 @@
_ = require 'lodash'
exports.checkInt = checkInt = (s, options = {}) ->
# Make sure `s` exists and is not an empty string.
if !s?
return
i = parseInt(s, 10)
if isNaN(i)
return
if options.positive && i <= 0
return
return i
exports.checkString = (s) ->
# Make sure `s` exists and is not an empty string, or 'null' or 'undefined'.
# This might happen if the parsing of config.json on the host using jq is wrong (it is buggy in some versions).
if !s? or !_.isString(s) or s in [ 'null', 'undefined', '' ]
return
return s
exports.checkTruthy = (v) ->
switch v
when '1', 'true', true, 'on', 1 then true
when '0', 'false', false, 'off', 0 then false
else return
exports.isValidShortText = isValidShortText = (t) ->
_.isString(t) and t.length <= 255
exports.isValidEnv = isValidEnv = (obj) ->
_.isObject(obj) and _.every obj, (val, key) ->
isValidShortText(key) and /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key) and _.isString(val)
exports.isValidLabelsObject = isValidLabelsObject = (obj) ->
_.isObject(obj) and _.every obj, (val, key) ->
isValidShortText(key) and /^[a-zA-Z][a-zA-Z0-9\.\-]*$/.test(key) and _.isString(val)
undefinedOrValidEnv = (val) ->
if val? and !isValidEnv(val)
return false
return true
exports.isValidDependentAppsObject = (apps) ->
return false if !_.isObject(apps)
return _.every apps, (val, appId) ->
val = _.defaults(_.clone(val), { config: undefined, environment: undefined, commit: undefined, image: undefined })
return false if !isValidShortText(appId) or !checkInt(appId)?
return _.conformsTo(val, {
name: isValidShortText
image: (i) -> !val.commit? or isValidShortText(i)
commit: (c) -> !c? or isValidShortText(c)
config: undefinedOrValidEnv
environment: undefinedOrValidEnv
})
isValidService = (service, serviceId) ->
return false if !isValidShortText(serviceId) or !checkInt(serviceId)
return _.conformsTo(service, {
serviceName: isValidShortText
image: isValidShortText
environment: isValidEnv
imageId: (i) -> checkInt(i)?
labels: isValidLabelsObject
})
exports.isValidAppsObject = (obj) ->
return false if !_.isObject(obj)
return _.every obj, (val, appId) ->
return false if !isValidShortText(appId) or !checkInt(appId)?
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
name: isValidShortText
releaseId: (r) -> !r? or checkInt(r)?
services: (s) -> _.isObject(s) and _.every(s, isValidService)
})
exports.isValidDependentDevicesObject = (devices) ->
return false if !_.isObject(devices)
return _.every devices, (val, uuid) ->
return false if !isValidShortText(uuid)
return _.conformsTo(val, {
name: isValidShortText
apps: (a) ->
return (
_.isObject(a) and
!_.isEmpty(a) and
_.every a, (app) ->
app = _.defaults(_.clone(app), { config: undefined, environment: undefined })
_.conformsTo(app, { config: undefinedOrValidEnv, environment: undefinedOrValidEnv })
)
})
exports.validStringOrUndefined = (s) ->
_.isUndefined(s) or (_.isString(s) and !_.isEmpty(s))
exports.validObjectOrUndefined = (o) ->
_.isUndefined(o) or _.isObject(o)

432
src/lib/validation.ts Normal file
View File

@ -0,0 +1,432 @@
import * as _ from 'lodash';
import { inspect } from 'util';
export interface CheckIntOptions {
positive?: boolean;
}
export interface EnvVarObject {
[name: string]: string;
}
export interface LabelObject {
[name: string]: string;
}
const ENV_VAR_KEY_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const LABEL_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9\.\-]*$/;
/**
* checkInt
*
* Check an input string as a number, optionally specifying a requirement
* to be positive
*/
export function checkInt(s: string | null, options: CheckIntOptions = {}): number | void {
if (s == null) {
return;
}
const i = parseInt(s, 10);
if (isNaN(i)) {
return;
}
if (options.positive && i <= 0) {
return;
}
return i;
}
/**
* checkString
*
* Check that a string exists, and is not an empty string, 'null', or 'undefined'
*/
export function checkString(s: string): string | void {
if (s == null || !_.isString(s) || _.includes([ 'null', 'undefined', '' ], s)) {
return;
}
return s;
}
/**
* checkTruthy
*
* Given a value which can be a string, boolean or number, return a boolean
* which represents if the input was truthy
*/
export function checkTruthy(v: string | boolean | number): boolean | void {
switch(v) {
case '1':
case 'true':
case true:
case 'on':
case 1:
return true;
case '0':
case 'false':
case false:
case 'off':
case 0:
return false;
default:
return;
}
}
/*
* isValidShortText
*
* Check that the input string is definitely a string,
* and has a length which is less than 255
*/
export function isValidShortText(t: string): boolean {
return _.isString(t) && t.length <= 255;
}
/**
* isValidEnv
*
* Given a env var object, check types and values for the keys
* and values
*/
export function isValidEnv(obj: EnvVarObject): boolean {
if (!_.isObject(obj)) {
console.log('debug: Non-object passed to validation.isValidEnv');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, key) => {
if (!isValidShortText(key)) {
console.log('debug: Non-valid short text env var key passed to validation.isValidEnv');
console.log(`\tKey: ${inspect(key)}`)
return false;
}
if (!ENV_VAR_KEY_REGEX.test(key)) {
console.log('debug: Invalid env var key passed to validation.isValidEnv');
console.log(`\tKey: ${inspect(key)}`)
return false;
}
if (!_.isString(val)) {
console.log('debug: Non-string value passed to validation.isValidEnv');
console.log(`\tval: ${inspect(key)}`);
return false;
}
return true;
});
}
/**
* isValidLabelsObject
*
* Given a labels object, test the types and values for validity
*/
export function isValidLabelsObject(obj: LabelObject): boolean {
if (!_.isObject(obj)) {
console.log('debug: Non-object passed to validation.isValidLabelsObject');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, key) => {
if (!isValidShortText(key)) {
console.log('debug: Non-valid short text label key passed to validation.isValidLabelsObject');
console.log(`\tkey: ${inspect(key)}`);
return false;
}
if (!LABEL_NAME_REGEX.test(key)) {
console.log('debug: Invalid label name passed to validation.isValidLabelsObject');
console.log(`\tkey: ${inspect(key)}`);
return false;
}
if (!_.isString(val)) {
console.log('debug: Non-string value passed to validation.isValidLabelsObject');
console.log(`\tval: ${inspect(val)}`);
return false;
}
return true;
});
}
function undefinedOrValidEnv(val: EnvVarObject): boolean {
return val == null || isValidEnv(val);
}
/**
* isValidDependentAppsObject
*
* Given a dependent apps object from a state endpoint, validate it
*
* TODO: Type the input
*/
export function isValidDependentAppsObject(apps: any): boolean {
if (!_.isObject(apps)) {
console.log('debug: non-object passed to validation.isValidDependentAppsObject');
console.log(`\tapps: ${inspect(apps)}`);
return false;
}
return _.every(apps, (val, appId) => {
val = _.defaults(_.clone(val), {
config: undefined,
environment: undefined,
commit: undefined,
image: undefined,
});
if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidDependentAppsObject');
console.log(`\tappId: ${inspect(appId)}`);
return false;
}
return _.conformsTo(val, {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid name passed to validation.isValidDependentAppsObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
image: (i: any) => {
if (val.commit != null && !isValidShortText(i)) {
console.log('debug: non valid image passed to validation.isValidDependentAppsObject');
console.log(`\timage: ${inspect(i)}`);
return false;
}
return true;
},
commit: (c: any) => {
if (c != null && !isValidShortText(c)) {
console.log('debug: invalid commit passed to validation.isValidDependentAppsObject');
console.log(`\tcommit: ${inspect(c)}`);
return false;
}
return true;
},
config: (c: any) => {
if (!undefinedOrValidEnv(c)) {
console.log('debug; Invalid config passed to validation.isValidDependentAppsObject');
console.log(`\tconfig: ${inspect(c)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!undefinedOrValidEnv(e)) {
console.log('debug; Invalid environment passed to validation.isValidDependentAppsObject');
console.log(`\tenvironment: ${inspect(e)}`);
return false;
}
return true;
}
});
});
}
function isValidService(service: any, serviceId: string): boolean {
if (!isValidShortText(serviceId) || !checkInt(serviceId)) {
console.log('debug: Invalid service id passed to validation.isValidService');
console.log(`\tserviceId: ${inspect(serviceId)}`);
return false;
}
return _.conformsTo(service, {
serviceName: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidService');
console.log(`\tserviceName: ${inspect(n)}`);
return false;
}
return true;
},
image: (i: any) => {
if (!isValidShortText(i)) {
console.log('debug: Invalid image passed to validation.isValidService');
console.log(`\timage: ${inspect(i)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!isValidEnv(e)) {
console.log('debug: Invalid env passed to validation.isValidService');
console.log(`\tenvironment: ${inspect(e)}`);
return false;
}
return true;
},
imageId: (i: any) => {
if (checkInt(i) == null) {
console.log('debug: Invalid image id passed to validation.isValidService');
console.log(`\timageId: ${inspect(i)}`);
return false;
}
return true;
},
labels: (l: any) => {
if (!isValidLabelsObject(l)) {
console.log('debug: Invalid labels object passed to validation.isValidService');
console.log(`\tlabels: ${inspect(l)}`);
return false;
}
return true;
}
});
}
/**
* isValidAppsObject
*
* Given an apps object from the state endpoint, validate the fields and
* return whether it's valid.
*
* TODO: Type the input correctly
*/
export function isValidAppsObject(obj: any): boolean {
if (!_.isObject(obj)) {
console.log('debug: Invalid object passed to validation.isValidAppsObject');
console.log(`\tobj: ${inspect(obj)}`);
return false;
}
return _.every(obj, (val, appId) => {
if (!isValidShortText(appId) || !checkInt(appId)) {
console.log('debug: Invalid appId passed to validation.isValidAppsObject');
console.log(`\tappId: ${inspect(appId)}`);
return false;
}
return _.conformsTo(_.defaults(_.clone(val), { releaseId: undefined }), {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid service name passed to validation.isValidAppsObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
releaseId: (r: any) => {
if (r != null && checkInt(r) == null) {
console.log('debug: Invalid releaseId passed to validation.isValidAppsObject');
console.log(`\treleaseId: ${inspect(r)}`);
return false;
}
return true;
},
services: (s: any) => {
if (!_.isObject(s)) {
console.log('debug: Non-object service passed to validation.isValidAppsObject');
console.log(`\tservices: ${inspect(s)}`);
return false;
}
return _.every(s, (svc, svcId) => {
if (!isValidService(svc, svcId)) {
console.log('debug: Invalid service object passed to validation.isValidAppsObject');
console.log(`\tsvc: ${inspect(svc)}`);
return false;
}
return true;
});
}
});
});
}
/**
* isValidDependentDevicesObject
*
* Validate a dependent devices object from the state endpoint.
*/
export function isValidDependentDevicesObject(devices: any): boolean {
if (!_.isObject(devices)) {
console.log('debug: Non-object passed to validation.isValidDependentDevicesObject');
console.log(`\tdevices: ${inspect(devices)}`);
return false;
}
return _.every(devices, (val, uuid) => {
if (!isValidShortText(uuid)) {
console.log('debug: Invalid uuid passed to validation.isValidDependentDevicesObject');
console.log(`\tuuid: ${inspect(uuid)}`);
return false;
}
return _.conformsTo(val, {
name: (n: any) => {
if (!isValidShortText(n)) {
console.log('debug: Invalid device name passed to validation.isValidDependentDevicesObject');
console.log(`\tname: ${inspect(n)}`);
return false;
}
return true;
},
apps: (a: any) => {
if (!_.isObject(a)) {
console.log('debug: Invalid apps object passed to validation.isValidDependentDevicesObject');
console.log(`\tapps: ${inspect(a)}`);
return false;
}
if (_.isEmpty(a)) {
console.log('debug: Empty object passed to validation.isValidDependentDevicesObject');
return false;
}
return _.every(a, (app) => {
app = _.defaults(_.clone(app), { config: undefined, environment: undefined });
return _.conformsTo(app, {
config: (c: any) => {
if (!undefinedOrValidEnv(c)) {
console.log('debug: Invalid config passed to validation.isValidDependentDevicesObject');
console.log(`\tconfig: ${inspect(c)}`);
return false;
}
return true;
},
environment: (e: any) => {
if (!undefinedOrValidEnv(e)) {
console.log('debug: Invalid environment passed to validation.isValidDependentDevicesObject');
console.log(`\tconfig: ${inspect(e)}`);
return false;
}
return true;
}
});
});
}
});
});
}
/**
* validStringOrUndefined
*
* Ensure a string is either undefined, or a non-empty string
*/
export function validStringOrUndefined(s: string | undefined): boolean {
return _.isUndefined(s) || (_.isString(s) && !_.isEmpty(s));
}
/**
* validStringOrUndefined
*
* Ensure an object is either undefined or an actual object
*/
export function validObjectOrUndefined(o: object | undefined): boolean {
return _.isUndefined(o) || _.isObject(o);
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"strictNullChecks": true,
"outDir": "./build"
},
"include": [
"src/**/*.ts",
"typings/**/*.d.ts"
]
}

View File

@ -77,7 +77,7 @@ module.exports = function (env) {
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: [".js", ".json", ".coffee"]
extensions: [".js", ".ts", ".json", ".coffee"]
},
target: 'node',
node: {
@ -96,6 +96,14 @@ module.exports = function (env) {
{
test: /\.coffee$/,
use: require.resolve('coffee-loader')
},
{
test: /\.ts$/,
use: [
{
loader: 'ts-loader',
}
]
}
]
},