@sjcrh/proteinpaint-server 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/genome/hg19.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- var clinvar_1 = require("../dataset/clinvar");
4
3
  exports.default = {
5
4
  species: 'human',
6
5
  genomefile: 'genomes/hg19.gz',
@@ -22,11 +21,6 @@ exports.default = {
22
21
  db: 'utils/meme/motif_databases/HUMAN/HOCOMOCOv11_full_HUMAN_mono_meme_format.meme',
23
22
  annotationfile: 'utils/meme/motif_databases/HUMAN/HOCOMOCOv11_full_annotation_HUMAN_mono.tsv'
24
23
  },
25
- clinvarVCF: {
26
- file: 'hg19/clinvar.hg19.hgvs_short.vep.bcf.gz',
27
- infokey: 'CLNSIG',
28
- categories: clinvar_1.clinsig
29
- },
30
24
  tracks: [
31
25
  {
32
26
  __isgene: true,
package/genome/hg38.js CHANGED
@@ -1,6 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- var clinvar_1 = require("../dataset/clinvar");
4
3
  var cgc_js_1 = require("./cgc.js");
5
4
  exports.default = {
6
5
  species: 'human',
@@ -30,11 +29,6 @@ exports.default = {
30
29
  db: 'utils/meme/motif_databases/HUMAN/HOCOMOCOv11_full_HUMAN_mono_meme_format.meme',
31
30
  annotationfile: 'utils/meme/motif_databases/HUMAN/HOCOMOCOv11_full_annotation_HUMAN_mono.tsv'
32
31
  },
33
- clinvarVCF: {
34
- file: 'hg38/clinvar.hg38.hgvs_short.vep.bcf.gz',
35
- infokey: 'CLNSIG',
36
- categories: clinvar_1.clinsig
37
- },
38
32
  tracks: [
39
33
  {
40
34
  __isgene: true,
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-server",
3
- "version": "2.26.0",
3
+ "version": "2.27.0",
4
4
  "description": "a genomics visualization tool for exploring a cohort's genotype and phenotype data",
5
5
  "main": "server.js",
6
6
  "bin": "start.js",
7
7
  "imports": {
8
- "#shared/*": "./shared/*.js",
8
+ "#shared/*": "./shared/*",
9
+ "#shared/types/*": "./shared/types/*",
9
10
  "#src/*": "./src/*",
10
11
  "#routes/*": "./routes/*"
11
12
  },
@@ -15,17 +16,18 @@
15
16
  "start": "tsc --esModuleInterop genome/*.ts dataset/*.ts && nodemon --enable-source-maps server.js --watch ./server*.js* --watch dataset/*.ts --watch genome/*.ts",
16
17
  "pretest": "tsc --esModuleInterop genome/*.ts dataset/*.ts && ./test/pretest.js",
17
18
  "test": "webpack --env NODE_ENV=test exportsFilename=all-test-context.js && node --enable-source-maps test/serverTests.js",
18
- "pretest:unit": "npm run checkers && tsc genome/hg38.test.ts",
19
- "test:unit": "webpack --env NODE_ENV=test exportsFilename=unit-test-context.js && tsc --esModuleInterop dataset/*.ts && node --enable-source-maps test/serverTests.js",
19
+ "precheckers": "tsc --esModuleInterop genome/*.ts dataset/*.ts",
20
+ "checkers": "webpack --config=./test/webpack.config.js && node test/emitPrepFiles.bundle.js && typia generate --input shared/checkers-raw --output shared/checkers",
21
+ "pretest:type": "npm run checkers",
22
+ "test:type": "webpack --env NODE_ENV=test exportsFilename=type-test-context.js && node --enable-source-maps test/serverTests.js",
23
+ "pretest:unit": "npm run precheckers",
24
+ "test:unit": "webpack --env NODE_ENV=test exportsFilename=unit-test-context.js && node --enable-source-maps test/serverTests.js",
20
25
  "pretest:integration": "tsc --esModuleInterop genome/*.ts dataset/*.ts",
21
26
  "test:integration": "echo 'TODO: server integration tests'",
22
27
  "prepack": "tsc --esModuleInterop genome/*.ts dataset/*.ts && webpack --env NODE_ENV=production",
23
28
  "response": "nodemon modules/test/test.server.js --watch src",
24
29
  "getconf": "../build/getConfigProp.js",
25
- "predoc": "npm run checkers",
26
- "doc": "typedoc --json ../public/docs/server.json",
27
- "precheckers": "npx ts-node ./augen/cli.js typeCheckers $PWD/routes ../../../routes > shared/checkers/raw/index.ts",
28
- "checkers": "typia generate --input shared/checkers/raw --output shared/checkers/transformed --project ./shared/checkers/tsconfig.json"
30
+ "doc": "../augen/build.sh routes shared/types/routes shared/checkers ../public/docs/server"
29
31
  },
30
32
  "author": "",
31
33
  "license": "SEE LICENSE IN ./LICENSE",
@@ -45,7 +47,6 @@
45
47
  "ts-node": "^10.9.1",
46
48
  "ts-patch": "^3.0.2",
47
49
  "typedoc": "^0.24.8",
48
- "typedoc-plugin-replace-text": "^3.1.0",
49
50
  "typescript": "^5.0.3",
50
51
  "typia": "^4.1.14",
51
52
  "webpack": "^5.76.0",
@@ -54,7 +55,8 @@
54
55
  "webpack-notifier": "^1.15.0"
55
56
  },
56
57
  "dependencies": {
57
- "@sjcrh/proteinpaint-rust": "2.26.0",
58
+ "@sjcrh/augen": "2.27.0",
59
+ "@sjcrh/proteinpaint-rust": "2.27.0",
58
60
  "better-sqlite3": "^7.5.3",
59
61
  "body-parser": "^1.15.2",
60
62
  "canvas": "~2.9.3",
@@ -0,0 +1,84 @@
1
+ # Server Routes
2
+
3
+ ## Introduction
4
+
5
+ This directory contains files that specify server route APIs. By following this guidelines,
6
+ the auto-generation of server routes, tests, and API documentation will work as expected.
7
+
8
+ ## Guidelines
9
+
10
+ ### 1. Use Express to do most of the routing logic
11
+
12
+ - decentralize the route handling code into smaller, independent handler functions
13
+ - common request processing logic, like genome, dataset, termdb set-up should be imported
14
+ from a shared helper module that is common to a group of routes, or for more advanced cases,
15
+ moved to a [router-level middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.router)
16
+
17
+ ### 2. Export an `api` from the route file
18
+
19
+ Use the code from other files in this directory as examples
20
+
21
+ TODO: define the `api` type
22
+
23
+ ```ts
24
+ // work-in-progress
25
+ type RouteApi {
26
+ [key as methods]: RouteApiMethod
27
+ }
28
+
29
+ type methods = 'get' | 'post'
30
+
31
+ type initArg = {
32
+ app?: any // Express app instance
33
+ genome: any // `Genome` from shared/types/genome.ts
34
+ }
35
+
36
+ /**
37
+ @param
38
+ */
39
+ type RouteApiMethod = {
40
+ endpoint: string
41
+ init: (initArg) => void
42
+ request: {
43
+ typeId: string
44
+ body?: any // specific to the route
45
+ }
46
+ response: {
47
+ typeId: string
48
+ header?: {
49
+ status: number
50
+ }
51
+ body?: any // specific to the route
52
+ }
53
+ examples: RouteExample[]
54
+ }
55
+
56
+ type RouteExample = {
57
+ request: {
58
+ body?: any
59
+ }
60
+ response?: {
61
+ header: {
62
+ status: number
63
+ }
64
+ body?: any
65
+ }
66
+ }
67
+ ```
68
+ ### 3. Use the appropriate [HTTP response code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
69
+
70
+ This is a best practice especially for error responses. Use `res.status(code)` to set the error code.
71
+ This convention helps with error troubleshooting. Examples:
72
+ - Status `400` 'Bad Request', something is wrong with the http request payload
73
+ - Status `401` 'Unauthorized', the user must authenticate. The `server/src/auth.js` sets this status code
74
+ - Status `403` 'Forbidden', the user is authenticated/signed-in, but is not permitted to access the requested data
75
+ - Status `404` 'Not Found' for genome, dataset, or other data that is not found
76
+ - Status `500` 'Server Error' for errors related to the server process or host machine, such as the GDC API
77
+ not being available. Do not use code=`500` for errors that are related to specific request handler or data processing functions.
78
+
79
+ ### 4. Auto-generate
80
+
81
+ - the server code will detect the routes in `server/src/run.sh`
82
+ - `npm run doc` to see the documented routes in http://localhost:3000/server.html
83
+ - `./augen/readme.sh > public/docs/readme.json` for content in http://localhost:3000/readme.html
84
+
@@ -0,0 +1,140 @@
1
+ import { BurdenRequest, BurdenResponse } from '#shared/types/routes/burden.ts'
2
+ import lines2R from '#src/lines2R.js'
3
+ import path from 'path'
4
+ import serverconfig from '#src/serverconfig.js'
5
+ import { write_file } from '#src/utils.js'
6
+
7
+ export const api = {
8
+ endpoint: 'burden',
9
+ methods: {
10
+ get: {
11
+ init({ genomes }) {
12
+ return async (req: any, res: any): Promise<void> => {
13
+ try {
14
+ const genome = genomes[req.query.genome]
15
+ if (!genome) throw `invalid q.genome=${req.query.genome}`
16
+ const q = req.query as BurdenRequest
17
+ const ds = genome.datasets[q.dslabel]
18
+ if (!ds) throw `invalid q.genome=${req.query.dslabel}`
19
+ if (!ds.cohort.cumburden?.files) throw `missing ds.cohort.cumburden.files`
20
+
21
+ const estimates = await getBurdenEstimates(req, ds)
22
+ const { keys, rows } = formatPayload(estimates)
23
+ res.send({ status: 'ok', keys, rows } as BurdenResponse)
24
+ } catch (e: any) {
25
+ res.send({ status: 'error', error: e.message || e })
26
+ }
27
+ }
28
+ },
29
+ request: {
30
+ typeId: 'BurdenRequest'
31
+ },
32
+ response: {
33
+ typeId: 'BurdenResponse'
34
+ },
35
+ examples: [
36
+ {
37
+ request: {
38
+ body: {
39
+ genome: 'hg38',
40
+ // TODO: !!! use hg38-test and TermdbTest !!!
41
+ dslabel: 'SJLife',
42
+ diaggrp: 5,
43
+ sex: 1,
44
+ white: 1,
45
+ agedx: 1,
46
+ bleo: 0,
47
+ etop: 0,
48
+ cisp: 0,
49
+ carbo: 0,
50
+ steriod: 0,
51
+ vcr: 0,
52
+ hdmtx: 0,
53
+ itmt: 0,
54
+ ced: 0,
55
+ dox: 0,
56
+ heart: 0,
57
+ brain: 0,
58
+ abd: 0,
59
+ pelvis: 0,
60
+ chest: 0
61
+ }
62
+ },
63
+ response: {
64
+ header: { status: 200 }
65
+ }
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ }
71
+
72
+ async function getBurdenEstimates(q, ds) {
73
+ const infile = path.join(serverconfig.cachedir, Math.random().toString() + '.json')
74
+ for (const k in q.query) {
75
+ q.query[k] = Number(q.query[k])
76
+ }
77
+ const data = Object.assign({}, defaults, q.query)
78
+ //console.log(40, data, JSON.stringify(data))
79
+ await write_file(infile, JSON.stringify(data))
80
+ // TODO: use the dataset location
81
+ const { fit, surv, sample } = ds.cohort.cumburden.files
82
+ if (!fit || !surv || !sample) throw `missing one or more of ds.cohort.burden.files.{fit, surv, sample}`
83
+ const args = [
84
+ infile,
85
+ `${serverconfig.tpmasterdir}/${fit}`,
86
+ `${serverconfig.tpmasterdir}/${surv}`,
87
+ `${serverconfig.tpmasterdir}/${sample}`
88
+ ]
89
+ const Routput = await lines2R(path.join(serverconfig.binpath, 'utils/burden.R'), [], args)
90
+ const estimates = JSON.parse(Routput[0])
91
+ return estimates
92
+ }
93
+
94
+ function formatPayload(estimates) {
95
+ const rawKeys = Object.keys(estimates[0])
96
+ const outKeys = [] as string[]
97
+ const keys = [] as string[]
98
+ for (const k of rawKeys) {
99
+ if (k == 'chc') {
100
+ keys.push(k)
101
+ outKeys.push(k)
102
+ } else {
103
+ const age = Number(k.slice(1).split(',')[0])
104
+ if (age <= 60 && age % 2 == 0) {
105
+ keys.push(k)
106
+ outKeys.push(`burden${age}`)
107
+ }
108
+ }
109
+ }
110
+ const rows = [] as number[][]
111
+ // v = an array of objects with age as keys as cumulative burden as value for a given CHC
112
+ for (const v of estimates) {
113
+ rows.push(keys.map(k => v[k]))
114
+ }
115
+ return { keys: outKeys, rows }
116
+ }
117
+
118
+ const defaults = Object.freeze({
119
+ diaggrp: 5,
120
+ sex: 0,
121
+ white: 1,
122
+ agedx: 1,
123
+ // chemotherapy
124
+ steriod: 0,
125
+ bleo: 0,
126
+ vcr: 0, //12, // Vincristine
127
+ etop: 0, //2500, // Etoposide
128
+ itmt: 0, // Intrathecal methothrexate_grp: 0,
129
+ ced: 0, //1.6, // Cyclophosphamide, 0.7692 mean 7692.
130
+ cisp: 0, //300, // Cisplatin
131
+ dox: 0, // Anthracycline, 3 mean 300 ml/m2
132
+ carbo: 0, // Carboplatin
133
+ hdmtx: 0, // High-Dose Methotrexate
134
+ // radiation
135
+ brain: 0, //5.4,
136
+ chest: 0, //2.4,
137
+ heart: 0,
138
+ pelvis: 0,
139
+ abd: 0 //2.4
140
+ })
@@ -0,0 +1,106 @@
1
+ import { GdcMafResponse, File } from '#shared/types/routes/gdcMaf.ts'
2
+ import { fileSize } from '#shared/fileSize.js'
3
+ import path from 'path'
4
+ import got from 'got'
5
+
6
+ const apihost = process.env.PP_GDC_HOST || 'https://api.gdc.cancer.gov'
7
+
8
+ export const api = {
9
+ endpoint: 'gdcMaf',
10
+ methods: {
11
+ get: {
12
+ init({ genomes }) {
13
+ // genomes parameter is not used
14
+ // could be used later to verify hg38/GDC is on this instance and otherwise disable this route..
15
+
16
+ return async (req: any, res: any): Promise<void> => {
17
+ try {
18
+ const files = await listMafFiles(req)
19
+ const payload = { files } as GdcMafResponse
20
+ res.send(payload)
21
+ } catch (e: any) {
22
+ res.send({ status: 'error', error: e.message || e })
23
+ }
24
+ }
25
+ },
26
+ request: {
27
+ typeId: null
28
+ //valid: default to type checker
29
+ },
30
+ response: {
31
+ typeId: 'GdcMafResponse'
32
+ // will combine this with type checker
33
+ //valid: (t) => {}
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ /*
40
+ req.query {
41
+ filter0 // optional gdc GFF cohort filter, invisible and read only
42
+ }
43
+ */
44
+ async function listMafFiles(req: any) {
45
+ const filters = {
46
+ op: 'and',
47
+ content: [
48
+ {
49
+ op: '=',
50
+ content: { field: 'data_format', value: 'MAF' }
51
+ }
52
+ ]
53
+ }
54
+
55
+ if (req.query.filter0) {
56
+ filters.content.push(req.query.filter0)
57
+ }
58
+
59
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json' }
60
+
61
+ const data = {
62
+ filters,
63
+ size: 1000,
64
+ fields: [
65
+ 'id',
66
+ 'file_size',
67
+ 'experimental_strategy',
68
+ 'cases.submitter_id', // used when listing all cases & files
69
+ //'associated_entities.entity_submitter_id', // semi human readable
70
+ //'associated_entities.case_id', // case uuid
71
+ 'cases.samples.sample_type',
72
+ 'analysis.workflow_type' // to drop out those as skip_workflow_type
73
+ ].join(',')
74
+ }
75
+
76
+ const response = await got.post(path.join(apihost, 'files'), { headers, body: JSON.stringify(data) })
77
+
78
+ let re
79
+ try {
80
+ re = JSON.parse(response.body)
81
+ } catch (e) {
82
+ throw 'invalid JSON from ' + api.endpoint
83
+ }
84
+ if (!Array.isArray(re.data?.hits)) throw 're.data.hits[] not array'
85
+
86
+ // flatten api return to table row objects
87
+ // it is possible to set a max size limit to limit the number of files passed to client
88
+ const files = [] as File[]
89
+ for (const h of re.data.hits) {
90
+ const file = {
91
+ id: h.id,
92
+ workflow_type: h.analysis?.workflow_type,
93
+ experimental_strategy: h.experimental_strategy,
94
+ file_size: fileSize(h.file_size)
95
+ } as File
96
+ const c = h.cases?.[0]
97
+ if (c) {
98
+ file.case_submitter_id = c.submitter_id
99
+ if (c.samples) {
100
+ file.sample_types = c.samples.map(i => i.sample_type).join(', ')
101
+ }
102
+ }
103
+ files.push(file)
104
+ }
105
+ return files
106
+ }
@@ -1,11 +1,14 @@
1
1
  import { getResult } from '#src/gene.js'
2
+ import { GeneLookupRequest, GeneLookupResponse } from '#shared/types/routes/genelookup.ts'
2
3
 
3
4
  function init({ genomes }) {
4
5
  return (req: any, res: any): void => {
5
6
  try {
7
+ const q = req.query as GeneLookupRequest
6
8
  const g = genomes[req.query.genome]
7
9
  if (!g) throw 'invalid genome name'
8
- res.send(getResult(g, req.query))
10
+ const result = getResult(g, req.query) as GeneLookupResponse
11
+ res.send(result)
9
12
  } catch (e: any) {
10
13
  res.send({ error: e.message || e })
11
14
  if (e.stack) console.log(e.stack)
@@ -45,14 +48,3 @@ export const api: any = {
45
48
  }
46
49
  }
47
50
  }
48
-
49
- export type GeneLookupRequest = {
50
- input: string
51
- genome: string
52
- deep: boolean
53
- }
54
-
55
- export type GeneLookupResponse = {
56
- error?: string
57
- hits: string[]
58
- }
@@ -1,13 +1,14 @@
1
1
  import { getStat } from '#src/health.ts'
2
+ import { HealthCheckResponse } from '#shared/types/routes/healthcheck.ts'
2
3
 
3
4
  export const api = {
4
5
  endpoint: 'healthcheck',
5
6
  methods: {
6
7
  get: {
7
8
  init({ genomes }) {
8
- return async (req: any, res: any): Promise<void> => {
9
+ return async (req: undefined, res: any): Promise<void> => {
9
10
  try {
10
- const health = await getStat(genomes)
11
+ const health = (await getStat(genomes)) as HealthCheckResponse
11
12
  res.send(health)
12
13
  } catch (e: any) {
13
14
  res.send({ status: 'error', error: e.message || e })
@@ -26,47 +27,3 @@ export const api = {
26
27
  }
27
28
  }
28
29
  }
29
-
30
- /**
31
- * for documentation only, to signify integer: not type-checked statically
32
- */
33
- type int = number
34
-
35
- /**
36
- * Information aboute the server build version and dates,
37
- * including the date when the server was last launched
38
- */
39
- export type VersionInfo = {
40
- pkgver: string
41
- codedate: string
42
- launchdate: string
43
- }
44
-
45
- type BuildByGenome = {
46
- [index: string]: GenomeBuildInfo
47
- }
48
-
49
- export type GenomeBuildInfo = {
50
- genedb: DbInfo
51
- termdbs?: {
52
- [index: string]: DbInfo
53
- }
54
- }
55
-
56
- type DbInfo = {
57
- buildDate: string // "unknown" or a Date-convertible string
58
- tables?: {
59
- [index: string]: int
60
- }
61
- }
62
-
63
- /**
64
- * Server sttus and data related to it's health
65
- */
66
- export type HealthCheckResponse = {
67
- status: 'ok' | 'error'
68
- genomes: BuildByGenome
69
- versionInfo: VersionInfo
70
- w?: number[]
71
- rs?: number
72
- }
@@ -0,0 +1,78 @@
1
+ import { trigger_getViolinPlotData } from '#src/termdb.violin.js'
2
+ // import { getViolinRequest, getViolinResponse } from '#shared/types/routes/termdb.violin'
3
+
4
+ export const api: any = {
5
+ endpoint: 'termdb/violin',
6
+ methods: {
7
+ get: {
8
+ init,
9
+ request: {
10
+ typeId: 'getViolinRequest'
11
+ },
12
+ response: {
13
+ typeId: 'getViolinResponse'
14
+ },
15
+ examples: [
16
+ {
17
+ request: {
18
+ body: {
19
+ genome: 'hg38-test',
20
+ dslabel: 'TermdbTest',
21
+ embedder: 'localhost',
22
+ devicePixelRatio: 2.200000047683716,
23
+ maxThickness: 150,
24
+ screenThickness: 1218,
25
+ filter: {
26
+ type: 'tvslst',
27
+ in: true,
28
+ join: '',
29
+ lst: [
30
+ {
31
+ tag: 'cohortFilter',
32
+ type: 'tvs',
33
+ tvs: { term: { id: 'subcohort', type: 'categorical' }, values: [{ key: 'ABC', label: 'ABC' }] }
34
+ }
35
+ ]
36
+ },
37
+ svgw: 227.27272234672367,
38
+ orientation: 'horizontal',
39
+ datasymbol: 'bean',
40
+ radius: 5,
41
+ strokeWidth: 0.2,
42
+ axisHeight: 60,
43
+ rightMargin: 50,
44
+ unit: 'abs',
45
+ plotThickness: 150,
46
+ termid: 'agedx'
47
+ }
48
+ },
49
+ response: {
50
+ header: { status: 200 }
51
+ }
52
+ }
53
+ ]
54
+ },
55
+ post: {
56
+ alternativeFor: 'get',
57
+ init
58
+ }
59
+ }
60
+ }
61
+
62
+ function init({ genomes }) {
63
+ return async (req: any, res: any): Promise<void> => {
64
+ const q = req.query // as getViolinRequest
65
+ try {
66
+ const g = genomes[req.query.genome]
67
+ const ds = g.datasets[req.query.dslabel]
68
+ if (!g) throw 'invalid genome name'
69
+ const data = await trigger_getViolinPlotData(req.query, null, ds, g) // as getViolinResponse
70
+ res.send(data)
71
+ } catch (e) {
72
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
73
+ // @ts-ignore
74
+ res.send({ error: e?.message || e })
75
+ if (e instanceof Error && e.stack) console.log(e)
76
+ }
77
+ }
78
+ }