diff --git a/src/cli/dct.ts b/src/cli/dct.ts index db101f7..27529ea 100644 --- a/src/cli/dct.ts +++ b/src/cli/dct.ts @@ -37,8 +37,8 @@ function main(argv: string[]) { program.version(apiVersion); program - .command('connect [server]') - .description('Connect to a Laboratory service or print connection info.') + .command('connect [service]') + .description('Connect to a Laboratory [service] or print connection info.') .action(connect); program diff --git a/src/laboratory/TODO.md b/src/laboratory/TODO.md index b1871be..48e88e8 100644 --- a/src/laboratory/TODO.md +++ b/src/laboratory/TODO.md @@ -10,6 +10,7 @@ * Server Error: listen EADDRINUSE: address already in use :::3000 * Enforce workflow for status changes (e.g. disallow complete to created) * Authentication + * x Replace entityBaseReviver with io-ts decoder. * x REVIEW use of TypeError instead of IllegalOperationError or EntityNotFoundError * x REVIEW: net.Server vs http.Server * Are README.md dependencies correct? Does the user have to install anything else? @@ -59,7 +60,7 @@ * deploy command * Bash completion api * = usage configuration in yargs - * = Consider using luxon in reviver - probably can't since io-ts uses Date + * x Consider using luxon in reviver - probably can't since io-ts uses Date * x Set version * x examples in usage() * x Spike commander as replacement for yargs @@ -183,10 +184,11 @@ * check sqlite error behavior on string value (varchar(256)) too long. * dateColumn decorator does not seem the apply to createdAt and updatedAt * Investigate schema verification in jsonColumn decorator. - * Consider removing luxon + * x Consider removing luxon - may be using for custom io-ts codec. * Duplicate code * toPOJO() * assertDeepEqual() * Accidentally PUT benchmark to candidates seems error-prone. Also PUT suite/candidate to candidate/suite. +* Investigate BSON. https://www.npmjs.com/package/bson * diff --git a/src/laboratory/client/client.ts b/src/laboratory/client/client.ts index 361b87c..961a829 100644 --- a/src/laboratory/client/client.ts +++ b/src/laboratory/client/client.ts @@ -28,32 +28,10 @@ import { validate, } from '../logic'; -import { entityBaseReviver } from '../server'; - -// tslint:disable-next-line:no-any -function jsonParser(data: any): any { - try { - return JSON.parse(data, entityBaseReviver); - } catch (e) { - // Not all payloads are JSON. Some POSTs and PUTS return "OK" - return data; - } -} - const config: AxiosRequestConfig = { // TODO: put credentials here. }; -const configForGet: AxiosRequestConfig = { - ...config, - transformResponse: [jsonParser], -}; - -const configForPatchPostPut: AxiosRequestConfig = { - ...config, - transformResponse: [jsonParser], -}; - export class LaboratoryClient implements ILaboratory { endpoint: string; @@ -61,8 +39,6 @@ export class LaboratoryClient implements ILaboratory { this.endpoint = endpoint; } - // TODO: error handling pattern/strategy - ///////////////////////////////////////////////////////////////////////////// // // Benchmarks @@ -70,7 +46,7 @@ export class LaboratoryClient implements ILaboratory { ///////////////////////////////////////////////////////////////////////////// async allBenchmarks(): Promise { const url = new URL('benchmarks', this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const benchmarks = validate(BenchmarkArrayType, response.data); return benchmarks; } @@ -78,7 +54,7 @@ export class LaboratoryClient implements ILaboratory { async oneBenchmark(rawName: string): Promise { const name = normalizeName(rawName); const url = new URL(`benchmarks/${name}`, this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const benchmark = validate(BenchmarkType, response.data); return benchmark; } @@ -93,7 +69,7 @@ export class LaboratoryClient implements ILaboratory { throw new IllegalOperationError(message); } const url = new URL(`benchmarks/${name}`, this.endpoint); - await axios.put(url.toString(), benchmark, configForPatchPostPut); + await axios.put(url.toString(), benchmark, config); } ///////////////////////////////////////////////////////////////////////////// @@ -103,7 +79,7 @@ export class LaboratoryClient implements ILaboratory { ///////////////////////////////////////////////////////////////////////////// async allCandidates(): Promise { const url = new URL('candidates', this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const candidates = validate(CandidateArrayType, response.data); return candidates; } @@ -111,7 +87,7 @@ export class LaboratoryClient implements ILaboratory { async oneCandidate(rawName: string): Promise { const name = normalizeName(rawName); const url = new URL(`candidates/${name}`, this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const candidate = validate(CandidateType, response.data); return candidate; } @@ -126,7 +102,7 @@ export class LaboratoryClient implements ILaboratory { throw new IllegalOperationError(message); } const url = new URL(`candidates/${name}`, this.endpoint); - await axios.put(url.toString(), candidate, configForPatchPostPut); + await axios.put(url.toString(), candidate, config); } ///////////////////////////////////////////////////////////////////////////// @@ -136,7 +112,7 @@ export class LaboratoryClient implements ILaboratory { ///////////////////////////////////////////////////////////////////////////// async allSuites(): Promise { const url = new URL('suites', this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const suites = validate(SuiteArrayType, response.data); return suites; } @@ -144,7 +120,7 @@ export class LaboratoryClient implements ILaboratory { async oneSuite(rawName: string): Promise { const name = normalizeName(rawName); const url = new URL(`suites/${name}`, this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const suite = validate(SuiteType, response.data); return suite; } @@ -156,7 +132,7 @@ export class LaboratoryClient implements ILaboratory { throw new IllegalOperationError(message); } const url = new URL(`suites/${name}`, this.endpoint); - await axios.put(url.toString(), suite, configForPatchPostPut); + await axios.put(url.toString(), suite, config); } ///////////////////////////////////////////////////////////////////////////// @@ -166,7 +142,7 @@ export class LaboratoryClient implements ILaboratory { ///////////////////////////////////////////////////////////////////////////// async allRuns(): Promise { const url = new URL('runs', this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const runs = validate(RunArrayType, response.data); return runs; } @@ -174,18 +150,14 @@ export class LaboratoryClient implements ILaboratory { async oneRun(rawName: string): Promise { const name = normalizeRunName(rawName); const url = new URL(`runs/${name}`, this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const run = validate(RunType, response.data); return run; } async createRunRequest(spec: IRunRequest): Promise { const url = new URL('runs', this.endpoint); - const response = await axios.post( - url.toString(), - spec, - configForPatchPostPut - ); + const response = await axios.post(url.toString(), spec, config); const run = validate(RunType, response.data); return run; } @@ -194,21 +166,21 @@ export class LaboratoryClient implements ILaboratory { const name = normalizeRunName(rawName); const url = new URL(`runs/${name}`, this.endpoint); const body: IUpdateRunStatus = { status }; - await axios.patch(url.toString(), body, configForPatchPostPut); + await axios.patch(url.toString(), body, config); } async reportRunResults(rawName: string, measures: Measures): Promise { const name = normalizeRunName(rawName); const url = new URL(`runs/${name}/results`, this.endpoint); const body: IReportRunResults = { measures }; - await axios.patch(url.toString(), body, configForPatchPostPut); + await axios.patch(url.toString(), body, config); } async allRunResults(benchmark: string, suite: string): Promise { const b = normalizeName(benchmark); const s = normalizeName(suite); const url = new URL(`runs/${b}/${s}`, this.endpoint); - const response = await axios.get(url.toString(), configForGet); + const response = await axios.get(url.toString(), config); const results = validate(ResultArrayType, response.data); return results; } diff --git a/src/laboratory/logic/interfaces.ts b/src/laboratory/logic/interfaces.ts index e97af4e..12a4d11 100644 --- a/src/laboratory/logic/interfaces.ts +++ b/src/laboratory/logic/interfaces.ts @@ -1,16 +1,19 @@ +import { either } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; +import { DateTime } from 'luxon'; export const apiVersion = '0.0.1'; // tslint:disable-next-line:variable-name -const DateType = new t.Type( - 'Date2', - (input: unknown): input is Date => input instanceof Date, - // `t.success` and `t.failure` are helpers used to build `Either` instances - (input, context) => - input instanceof Date ? t.success(input) : t.failure(input, context), - // `A` and `O` are the same, so `encode` is just the identity function - t.identity +const DateType = new t.Type( + 'Date', + (u): u is Date => u instanceof Date, + (u, c) => + either.chain(t.string.validate(u, c), s => { + const d = DateTime.fromISO(s); + return d.isValid ? t.success(d.toJSDate()) : t.failure(u, c); + }), + a => a.toISOString() ); /////////////////////////////////////////////////////////////////////////////// diff --git a/src/laboratory/server/app.ts b/src/laboratory/server/app.ts index 7324ab3..1e76622 100644 --- a/src/laboratory/server/app.ts +++ b/src/laboratory/server/app.ts @@ -27,8 +27,8 @@ export async function createApp(lab: ILaboratory): Promise { app.use( // tslint:disable-next-line: deprecation bodyParser.json({ + // TODO: REVIEW: magic number 100kb limit: '100kb', - reviver: entityBaseReviver, }) ); @@ -61,12 +61,3 @@ export async function createApp(lab: ILaboratory): Promise { return app; } - -// tslint:disable-next-line:no-any -export function entityBaseReviver(key: string, value: any) { - if (key === 'updatedAt' || key === 'createdAt') { - return new Date(value); - } else { - return value; - } -} diff --git a/test/unit/laboratory/client/client.test.ts b/test/unit/laboratory/client/client.test.ts index f2266ad..00b5b68 100644 --- a/test/unit/laboratory/client/client.test.ts +++ b/test/unit/laboratory/client/client.test.ts @@ -7,10 +7,14 @@ import { LaboratoryClient } from '../../../../src/laboratory/client'; import { benchmark1, candidate1, run1, suite1, runRequest1 } from '../data'; import { - RunStatus, - IUpdateRunStatus, + BenchmarkType, + CandidateType, IReportRunResults, + IUpdateRunStatus, Measures, + RunStatus, + SuiteType, + validate, } from '../../../../src'; chai.use(chaiAsPromised); @@ -54,7 +58,8 @@ describe('laboratory/client', () => { const client = new LaboratoryClient(endpoint); await client.upsertBenchmark(benchmark1); - assert.deepEqual(request!, benchmark1); + const observed = validate(BenchmarkType, request!); + assert.deepEqual(observed, benchmark1); }); it('upsertBenchmark() - name mismatch', async () => { @@ -107,7 +112,8 @@ describe('laboratory/client', () => { const client = new LaboratoryClient(endpoint); await client.upsertCandidate(candidate1); - assert.deepEqual(request!, candidate1); + const observed = validate(CandidateType, request!); + assert.deepEqual(observed, candidate1); }); it('upsertCandidate() - name mismatch', async () => { @@ -160,7 +166,8 @@ describe('laboratory/client', () => { const client = new LaboratoryClient(endpoint); await client.upsertSuite(suite1); - assert.deepEqual(request!, suite1); + const observed = validate(SuiteType, request!); + assert.deepEqual(observed, suite1); }); it('upsertSuite() - name mismatch', async () => { diff --git a/test/unit/laboratory/data.ts b/test/unit/laboratory/data.ts index 203b20d..39a9095 100644 --- a/test/unit/laboratory/data.ts +++ b/test/unit/laboratory/data.ts @@ -19,6 +19,11 @@ import { export const serviceURL = 'http://localhost:3000'; // TODO: plumb real url. export const blobBase = 'http://blobs'; +export const timestamps = { + createdAt: new Date('2020-03-19T21:37:31.452Z'), + updatedAt: new Date('2020-03-20T22:37:31.452Z'), +}; + export const pipelines: IPipeline[] = [ { mode: 'mode1', @@ -39,6 +44,7 @@ export const benchmark1: IBenchmark = { author: 'author1', version: apiVersion, pipelines, + ...timestamps, }; export const benchmark2: IBenchmark = { @@ -46,12 +52,14 @@ export const benchmark2: IBenchmark = { author: 'author2', version: apiVersion, pipelines, + ...timestamps, }; export const benchmark3: IBenchmark = { name: 'benchmark3', author: 'author3', version: apiVersion, + ...timestamps, pipelines, }; @@ -62,6 +70,7 @@ export const candidate1: ICandidate = { benchmark: 'benchmark1', mode: 'mode1', image: 'candidate1-image', + ...timestamps, }; export const candidate2: ICandidate = { @@ -71,6 +80,7 @@ export const candidate2: ICandidate = { benchmark: 'benchmark1', mode: 'mode1', image: 'candidate2-image', + ...timestamps, }; export const candidate3: ICandidate = { @@ -80,6 +90,7 @@ export const candidate3: ICandidate = { benchmark: 'benchmark1', mode: 'mode1', image: 'candidate3-image', + ...timestamps, }; export const suite1: ISuite = { @@ -88,6 +99,7 @@ export const suite1: ISuite = { version: apiVersion, benchmark: 'benchmark1', mode: 'mode1', + ...timestamps, }; export const suite2: ISuite = { @@ -96,6 +108,7 @@ export const suite2: ISuite = { version: apiVersion, benchmark: 'benchmark1', mode: 'mode1', + ...timestamps, }; export const suite3: ISuite = { @@ -104,6 +117,7 @@ export const suite3: ISuite = { version: apiVersion, benchmark: 'benchmark1', mode: 'mode1', + ...timestamps, }; export const runRequest1: IRunRequest = { @@ -121,4 +135,5 @@ export const run1: IRun = { suite: suite1, blob: new URL(runid, blobBase).toString(), status: RunStatus.CREATED, + ...timestamps, }; diff --git a/test/unit/laboratory/logic/benchmarks.test.ts b/test/unit/laboratory/logic/benchmarks.test.ts index 0820ca4..453f6cb 100644 --- a/test/unit/laboratory/logic/benchmarks.test.ts +++ b/test/unit/laboratory/logic/benchmarks.test.ts @@ -74,8 +74,6 @@ describe('laboratory/benchmarks', () => { }); it('upsertBenchmark()', async () => { - console.log('benchmark'); - await lab.upsertBenchmark(benchmark1); const results1 = await lab.allBenchmarks(); assertDeepEqual(results1, [benchmark1]); diff --git a/test/unit/laboratory/server/server.test.ts b/test/unit/laboratory/server/server.test.ts index 6526c27..54d9d92 100644 --- a/test/unit/laboratory/server/server.test.ts +++ b/test/unit/laboratory/server/server.test.ts @@ -23,6 +23,7 @@ import { createApp } from '../../../../src/laboratory/server'; interface XMLHttpRequest {} import { + BenchmarkType, IBenchmark, ICandidate, IReportRunResults, @@ -33,6 +34,10 @@ import { IUpdateRunStatus, Measures, RunStatus, + validate, + CandidateType, + SuiteType, + RunType, } from '../../../../src'; import { benchmark1, candidate1, run1, suite1 } from '../data'; @@ -69,20 +74,20 @@ describe('laboratory/server', () => { it('oneBenchmark()', async () => { const lab = new MockLaboratory(); - const expected = 'benchmark1'; - let observed: string | undefined; + let observedName: string | undefined; lab.oneBenchmark = async (name: string): Promise => { - observed = name; + observedName = name; return benchmark1; }; chai .request(await createApp(lab)) - .get(`/benchmarks/${expected}`) + .get(`/benchmarks/${benchmark1.name}`) .end((err, res) => { assert.equal(res.status, 200); - assertDeepEqual(res.body, benchmark1); - assert.equal(observed, expected); + const observed = validate(BenchmarkType, res.body); + assert.deepEqual(observed, benchmark1); + assert.equal(observedName, benchmark1.name); }); }); @@ -138,20 +143,20 @@ describe('laboratory/server', () => { it('oneCandidate()', async () => { const lab = new MockLaboratory(); - const expected = 'candidate1'; - let observed: string | undefined; + let observedName: string | undefined; lab.oneCandidate = async (name: string): Promise => { - observed = name; + observedName = name; return candidate1; }; chai .request(await createApp(lab)) - .get(`/candidates/${expected}`) + .get(`/candidates/${candidate1.name}`) .end((err, res) => { assert.equal(res.status, 200); - assertDeepEqual(res.body, candidate1); - assert.equal(observed, expected); + const observed = validate(CandidateType, res.body); + assert.deepEqual(observed, candidate1); + assert.equal(observedName, candidate1.name); }); }); @@ -207,20 +212,20 @@ describe('laboratory/server', () => { it('oneSuite()', async () => { const lab = new MockLaboratory(); - const expected = 'suite1'; - let observed: string | undefined; + let observedName: string | undefined; lab.oneSuite = async (name: string): Promise => { - observed = name; + observedName = name; return suite1; }; chai .request(await createApp(lab)) - .get(`/suites/${expected}`) + .get(`/suites/${suite1.name}`) .end((err, res) => { assert.equal(res.status, 200); - assertDeepEqual(res.body, suite1); - assert.equal(observed, expected); + const observed = validate(SuiteType, res.body); + assert.deepEqual(observed, suite1); + assert.equal(observedName, suite1.name); }); }); @@ -273,20 +278,20 @@ describe('laboratory/server', () => { it('oneRun()', async () => { const lab = new MockLaboratory(); - const expected = 'run1'; - let observed: string | undefined; + let observedName: string | undefined; lab.oneRun = async (name: string): Promise => { - observed = name; + observedName = name; return run1; }; chai .request(await createApp(lab)) - .get(`/runs/${expected}`) + .get(`/runs/${run1.name}`) .end((err, res) => { assert.equal(res.status, 200); - assertDeepEqual(res.body, run1); - assert.equal(observed, expected); + const observed = validate(RunType, res.body); + assert.deepEqual(observed, run1); + assert.equal(observedName, run1.name); }); }); @@ -298,10 +303,10 @@ describe('laboratory/server', () => { suite: run1.suite.name, }; - let observed: IRun; + let observedRequest: IRunRequest; lab.createRunRequest = async (spec: IRunRequest): Promise => { - observed = run1; - return observed; + observedRequest = spec; + return run1; }; chai @@ -310,8 +315,9 @@ describe('laboratory/server', () => { .send(runRequest) .end((err, res) => { assert.equal(res.status, 200); + assert.deepEqual(observedRequest, runRequest); + const observed = validate(RunType, res.body); assert.deepEqual(observed, run1); - assertDeepEqual(res.body, run1); }); });