Skip to content

Commit 25816d2

Browse files
Refactor: Schema Definition in TypeScript with Zod (#181)
* Create health router * Dictionary Routes moved into router Note: Changes route `/diff/` to `/dictionaries/diff` * Comments and minor changes * Refactor clients for external services into `/external` directory * Enforce TS strict and fix type issues * Major version bump, add immer and zod * Remove explicit paths declaration and rules redundant with strict * Copyright year update * Dictionary Schema defined with Zod * Move dictionary model to `/db` dir and base off new types * Move diff code to a service * Clean types, validate request inputs * Update types and db code reference * Move express handler wrappers to /routers dir * Clean up types, use immer for immutable changes * Handle ZodErrors thrown during requests * Use new types * Put `/diff` first to ensure it does not get blocked by `/:id` requests * Small type fixes * Update tests and fixtures to work with latest type validations * Provide some basic schema examples * Remove unused meta-schema * Move /diff to its own router * Organize all imports * Diff request path back to original * Organize Imports * Mocha Testing Config separate from npm scripts * Explicitly set `noImplicitAny` to enforce array.map type inference on unions * Move Reference Types to their own file * Refactor references utilities to use proper reference types * Move reference test fixtures and fix linebreak normalization tests * Proper formatting for beta version Co-authored-by: Anders Richardsson <[email protected]> * Comment clarification and typo correction Co-authored-by: Anders Richardsson <[email protected]> --------- Co-authored-by: Anders Richardsson <[email protected]>
1 parent 00a95a3 commit 25816d2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+9721
-9430
lines changed

.mocharc.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extension": ["ts"],
3+
"require": "ts-node/register",
4+
"spec": "test/**/*.ts",
5+
"timeout": 35000
6+
}

package-lock.json

+8,097-8,078
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+76-74
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,78 @@
11
{
2-
"name": "lectern",
3-
"version": "1.14.0",
4-
"description": "Data Dictionary Management",
5-
"scripts": {
6-
"start": "npm run serve",
7-
"build": "npm run build-ts",
8-
"serve": "node dist/server.js",
9-
"watch-node": "nodemon dist/server.js",
10-
"test": "mocha --timeout 35000 --exit -r ts-node/register test/**/*.ts",
11-
"watch-test": "mocha --watch --timeout 35000 --exit -r ts-node/register test/**/*.ts",
12-
"build-ts": "tsc",
13-
"watch-ts": "tsc -w",
14-
"debug": "npm run build && npm run watch-debug",
15-
"serve-debug": "nodemon --inspect dist/server.js",
16-
"watch-debug": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec node --inspect-brk -r ts-node/register ./src/server.ts",
17-
"gitmoji-config": "gitmoji -g",
18-
"commit": "gitmoji -c"
19-
},
20-
"repository": {
21-
"type": "git",
22-
"url": "git+https://github.com/overture-stack/lectern.git"
23-
},
24-
"author": "andricDu",
25-
"license": "AGPL-3.0",
26-
"bugs": {
27-
"url": "https://github.com/overture-stack/lectern/issues"
28-
},
29-
"homepage": "https://github.com/overture-stack/lectern#readme",
30-
"devDependencies": {
31-
"@types/body-parser": "^1.19.2",
32-
"@types/chai": "^4.1.7",
33-
"@types/errorhandler": "0.0.32",
34-
"@types/express": "^4.17.13",
35-
"@types/jsonwebtoken": "^8.3.2",
36-
"@types/lodash": "^4.14.136",
37-
"@types/memoizee": "^0.4.7",
38-
"@types/mocha": "^5.2.7",
39-
"@types/ms": "^0.7.31",
40-
"@types/swagger-ui-express": "^3.0.1",
41-
"chai": "^4.3.6",
42-
"chai-http": "^4.3.0",
43-
"concurrently": "^5.3.0",
44-
"gitmoji-cli": "^4.8.0",
45-
"husky": "^3.1.0",
46-
"mocha": "^9.2.0",
47-
"nodemon": "^2.0.15",
48-
"prettier": "^2.8.0",
49-
"testcontainers": "^1.1.19",
50-
"ts-node": "^10.0.0",
51-
"typescript": "^5.0.0"
52-
},
53-
"dependencies": {
54-
"ajv": "^8.0.0",
55-
"axios": "^1.0.0",
56-
"body-parser": "^1.19.2",
57-
"dotenv": "^16.0.0",
58-
"errorhandler": "^1.5.1",
59-
"express": "^4.18.2",
60-
"jsonwebtoken": "^8.5.1",
61-
"lodash": "^4.17.21",
62-
"memoizee": "^0.4.15",
63-
"mongoose": "^7.0.0",
64-
"ms": "^2.1.3",
65-
"node-vault": "^0.9.22",
66-
"swagger-ui-express": "^4.3.0",
67-
"winston": "^3.6.0"
68-
},
69-
"prettier": {
70-
"printWidth": 120,
71-
"trailingComma": "all",
72-
"singleQuote": true,
73-
"arrowParens": "always",
74-
"useTabs": true
75-
}
2+
"name": "lectern",
3+
"version": "2.0.0-next.0",
4+
"description": "Data Dictionary Management",
5+
"scripts": {
6+
"start": "npm run serve",
7+
"build": "npm run build-ts",
8+
"serve": "node dist/server.js",
9+
"watch-node": "nodemon dist/server.js",
10+
"test": "mocha ",
11+
"watch-test": "mocha --watch",
12+
"build-ts": "tsc",
13+
"watch-ts": "tsc -w",
14+
"debug": "npm run build && npm run watch-debug",
15+
"serve-debug": "nodemon --inspect dist/server.js",
16+
"watch-debug": "nodemon --watch 'src/**/*.ts' --ignore 'src/**/*.spec.ts' --exec node --inspect -r ts-node/register ./src/server.ts",
17+
"gitmoji-config": "gitmoji -g",
18+
"commit": "gitmoji -c"
19+
},
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/overture-stack/lectern.git"
23+
},
24+
"author": "andricDu",
25+
"license": "AGPL-3.0",
26+
"bugs": {
27+
"url": "https://github.com/overture-stack/lectern/issues"
28+
},
29+
"homepage": "https://github.com/overture-stack/lectern#readme",
30+
"devDependencies": {
31+
"@types/body-parser": "^1.19.2",
32+
"@types/chai": "^4.1.7",
33+
"@types/errorhandler": "0.0.32",
34+
"@types/express": "^4.17.13",
35+
"@types/jsonwebtoken": "^8.3.2",
36+
"@types/lodash": "^4.14.136",
37+
"@types/memoizee": "^0.4.7",
38+
"@types/mocha": "^5.2.7",
39+
"@types/ms": "^0.7.31",
40+
"@types/swagger-ui-express": "^3.0.1",
41+
"chai": "^4.3.6",
42+
"chai-http": "^4.3.0",
43+
"concurrently": "^5.3.0",
44+
"gitmoji-cli": "^4.8.0",
45+
"husky": "^3.1.0",
46+
"mocha": "^9.2.0",
47+
"nodemon": "^2.0.15",
48+
"prettier": "^2.8.0",
49+
"testcontainers": "^1.1.19",
50+
"ts-node": "^10.0.0",
51+
"typescript": "^5.0.0"
52+
},
53+
"dependencies": {
54+
"ajv": "^8.0.0",
55+
"axios": "^1.0.0",
56+
"body-parser": "^1.19.2",
57+
"dotenv": "^16.0.0",
58+
"errorhandler": "^1.5.1",
59+
"express": "^4.18.2",
60+
"immer": "^10.0.2",
61+
"jsonwebtoken": "^8.5.1",
62+
"lodash": "^4.17.21",
63+
"memoizee": "^0.4.15",
64+
"mongoose": "^7.0.0",
65+
"ms": "^2.1.3",
66+
"node-vault": "^0.9.22",
67+
"swagger-ui-express": "^4.3.0",
68+
"winston": "^3.6.0",
69+
"zod": "^3.21.4"
70+
},
71+
"prettier": {
72+
"printWidth": 120,
73+
"trailingComma": "all",
74+
"singleQuote": true,
75+
"arrowParens": "always",
76+
"useTabs": true
77+
}
7678
}

samples/dictionary/simple.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "Simple",
3+
"version": "1.0",
4+
"schemas": [
5+
{
6+
"name": "primatives",
7+
"description": "Includes one field of each primative type without any restrictions. No Frills.",
8+
"fields": [
9+
{ "name": "boolean_field", "valueType": "boolean" },
10+
{ "name": "integer_field", "valueType": "integer" },
11+
{ "name": "number_field", "valueType": "number" },
12+
{ "name": "string_field", "valueType": "string" }
13+
]
14+
}
15+
]
16+
}

samples/schemas/primatives.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "primatives",
3+
"description": "Includes one field of each primative type without any restrictions. No Frills.",
4+
"fields": [
5+
{ "name": "boolean_field", "valueType": "boolean" },
6+
{ "name": "integer_field", "valueType": "integer" },
7+
{ "name": "number_field", "valueType": "number" },
8+
{ "name": "string_field", "valueType": "string" }
9+
]
10+
}

src/app-health.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020 The Ontario Institute for Cancer Research. All rights reserved
2+
* Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
33
*
44
* This program and the accompanying materials are made available under the terms of
55
* the GNU Affero General Public License v3.0. You should have received a copy of the

src/app.ts

+21-53
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020 The Ontario Institute for Cancer Research. All rights reserved
2+
* Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
33
*
44
* This program and the accompanying materials are made available under the terms of
55
* the GNU Affero General Public License v3.0. You should have received a copy of the
@@ -17,35 +17,19 @@
1717
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1818
*/
1919

20-
import express, { RequestHandler, Express } from 'express';
2120
import bodyParser from 'body-parser';
22-
import { AppConfig } from './config/appConfig';
23-
import * as dictionaryController from './controllers/dictionaryController';
24-
import { errorHandler } from './utils/errors';
21+
import express, { Express } from 'express';
2522
import * as swaggerUi from 'swagger-ui-express';
26-
import * as swagger from './config/swagger.json';
27-
import ego from './services/egoTokenService';
28-
import logger from './config/logger';
29-
import { dbHealth, Status } from './app-health';
3023

31-
/**
32-
* Decorator to handle errors from async express route handlers
33-
*/
34-
const wrapAsync = (fn: RequestHandler<any, any, any, any>): RequestHandler => {
35-
return (req, res, next) => {
36-
const routePromise: any = fn(req, res, next);
37-
if (routePromise.catch) {
38-
routePromise.catch(next);
39-
}
40-
};
41-
};
24+
import { AppConfig } from './config/appConfig';
25+
import logger from './config/logger';
26+
import * as swagger from './config/swagger.json';
27+
import dictionaryRouter from './routers/dictionaryRouter';
28+
import diffRouter from './routers/diffRouter';
29+
import healthRouter from './routers/healthRouter';
30+
import { errorHandler } from './utils/errors';
4231

4332
const App = (config: AppConfig): Express => {
44-
/**
45-
* Auth Decorator
46-
*/
47-
const egoDecorator = process.env.AUTH_ENABLED === 'true' ? ego() : wrapAsync;
48-
4933
// Create Express server with mongoConfig
5034
const app = express();
5135
const serverPort = config.serverPort();
@@ -60,43 +44,27 @@ const App = (config: AppConfig): Express => {
6044
}),
6145
);
6246

63-
swagger['info']['version'] = process.env.npm_package_version;
64-
app.use(openApiPath, swaggerUi.serve, swaggerUi.setup(swagger));
65-
66-
logger.info(`OpenAPI setup... done: http://localhost:${serverPort}${openApiPath}`);
67-
68-
app.get('/health', (req, res) => {
69-
if (dbHealth.status == Status.OK) {
70-
const resBody = {
71-
appStatus: 'Up',
72-
dbStatus: dbHealth.status,
73-
};
74-
return res.status(200).send(resBody);
75-
} else {
76-
const resBody = {
77-
appStatus: 'Error/Unknown',
78-
dbStatus: dbHealth.status,
79-
};
80-
return res.status(500).send(resBody);
81-
}
82-
});
83-
47+
// Root Handler:
8448
app.get('/', (_, res) => {
8549
const details = {
86-
app: 'Lectern',
8750
version: process.env.npm_package_version,
8851
commit: process.env.COMMIT_SHA,
8952
};
9053
res.send(details);
9154
});
55+
app.use('/health', healthRouter);
56+
app.use('/dictionaries', dictionaryRouter);
57+
app.use('/diff', diffRouter);
9258

93-
app.get('/dictionaries', wrapAsync(dictionaryController.listDictionaries));
94-
app.post('/dictionaries', egoDecorator(dictionaryController.createDictionary));
95-
app.get('/dictionaries/:dictId', wrapAsync(dictionaryController.getDictionary));
96-
app.post('/dictionaries/:dictId/schemas', egoDecorator(dictionaryController.addSchema));
97-
app.put('/dictionaries/:dictId/schemas', egoDecorator(dictionaryController.updateSchema));
98-
app.get('/diff/', wrapAsync(dictionaryController.diffDictionaries));
59+
/**
60+
* Swagger Setup
61+
*/
62+
swagger.info.version = process.env.npm_package_version || '';
63+
64+
app.use(openApiPath, swaggerUi.serve, swaggerUi.setup(swagger));
65+
logger.info(`Access swagger docs: http://localhost:${serverPort}${openApiPath}`);
9966

67+
// Error handler must be added last to capture all thrown errors.
10068
app.use(errorHandler);
10169

10270
return app;

0 commit comments

Comments
 (0)