Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init routes #2346

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion augen/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
test/checkers
test/checkers*/*
test/checkers-raw/*.ts
test/types/*.js
public/docs/*
public/server-api.json
Expand Down
7 changes: 5 additions & 2 deletions augen/src/augen.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ export function typeCheckers(fileRoutes, fromPath) {
}
const importLines = [`import { createValidate } from 'typia'`]
const createLines = []
const dedupedTypeIds = new Set()
for (const file in typeIdsByFile) {
if (file.endsWith('.js')) continue
const typeIds = Array.from(typeIdsByFile[file]).filter(t => t !== 'any')
const typeIds = Array.from(typeIdsByFile[file]).filter(t => t !== 'any' && !dedupedTypeIds.has(t))
if (!typeIds.length) continue
importLines.push(`import { ${typeIds.join(', ')} } from '${fromPath}/${file}'`)
const filename = file.split('.').slice(0, -1).join('.')
importLines.push(`import type { ${typeIds.join(', ')} } from '${fromPath}/${filename}.js'`)
for (const typeId of typeIds) {
createLines.push(`export const valid${typeId} = createValidate<${typeId}>()`)
}
for (const t of typeIds) dedupedTypeIds.add(t)
}
const content = importLines.join('\n') + '\n\n' + createLines.join('\n')
return content
Expand Down
6 changes: 3 additions & 3 deletions augen/src/tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function getApp({ api }, getHandlerInitArg = null) {
***************/

// f: a filename under the server/routes dir
export async function testApi(route, f, checkers) {
export function testApi(route, f, checkers) {
const { api } = route

tape('\n', function (test) {
Expand All @@ -52,7 +52,7 @@ export async function testApi(route, f, checkers) {
if (!m.examples) m.examples = [{ request: {}, response: {} }]

for (const x of m.examples) {
tape(`${api.endpoint} ${METHOD}`, async test => {
tape(`${api.endpoint} ${METHOD}`, test => {
if (m.alternativeFor) {
console.log(`${METHOD} method tested previously as '${m.alternativeFor.toUpperCase()}'`)
test.end()
Expand Down Expand Up @@ -87,7 +87,7 @@ export async function testApi(route, f, checkers) {
}
const route = app.routes[api.endpoint]
test.equal(typeof route?.get, 'function', 'should exist as a route')
await route.get(req, res)
route.get(req, res)
test.end()
})
}
Expand Down
5 changes: 0 additions & 5 deletions augen/test/checkers-raw/index.ts

This file was deleted.

4 changes: 2 additions & 2 deletions augen/test/unit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ runTests()

async function runTests() {
const files = readdirSync(join(__dirname, './routes'))
const endpoints = files.filter(f => f.endsWith('.ts') || f.endsWith('.js'))
const endpoints = files.filter(f => f.endsWith('.ts'))
for (const f of endpoints) {
const route = await import(`./routes/${f}`)
await testApi(route, f, checkers)
testApi(route, f, checkers)
}
}
6 changes: 3 additions & 3 deletions client/plots/wsiviewer/WSIViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import TileSource from 'ol/source/Tile'
import { WSIViewerInteractions } from '#plots/wsiviewer/interactions/WSIViewerInteractions.ts'
import Settings from '#plots/wsiviewer/Settings.ts'
import wsiViewerDefaults from '#plots/wsiviewer/defaults.ts'
import { GetWSImagesRequest, GetWSImagesResponse } from '#routes/wsimages.ts'
import { WSImagesRequest, WSImagesResponse } from '#routes/wsimages.ts'
import wsiViewerImageFiles from './wsimagesloaded.ts'
import { WSImage } from '#routes/samplewsimages.ts'
import { table2col } from '#dom/table2col'
Expand Down Expand Up @@ -169,14 +169,14 @@ export default class WSIViewer {
const layers: Array<TileLayer<Zoomify>> = []

for (let i = 0; i < wsimages.length; i++) {
const body: GetWSImagesRequest = {
const body: WSImagesRequest = {
genome: state.genome || state.vocab.genome,
dslabel: state.dslabel || state.vocab.dslabel,
sampleId: state.sample_id,
wsimage: wsimages[i].filename
}

const data: GetWSImagesResponse = await dofetch3('wsimages', { body })
const data: WSImagesResponse = await dofetch3('wsimages', { body })

if (data.status === 'error') {
return []
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"sethooks": "./utils/hooks/init.sh",
"getconf": "node -p 'JSON.stringify(require(\"./server/src/serverconfig.js\"),null,\" \")'",
"clean": "git add -A; git stash --staged",
"doc": "npm run doc --workspace=server"
"doc": "npm run doc --workspace=shared/types"
},
"author": "",
"license": "SEE LICENSE IN ./LICENSE",
Expand Down
8 changes: 4 additions & 4 deletions public/server.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js" integrity="sha512-FHsFVKQ/T1KWJDGSbrUhTJyS1ph3eRrxI228ND0EGaEp6v4a/vGwPWd3Dtd/+9cI7ccofZvl/wulICEurHN1pg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script><!-- pragma: allowlist secret -->
<link rel="stylesheet" href="/docs/server/assets/style.css"/>
<link rel="stylesheet" href="/docs/server/assets/highlight.css"/>
<script src='/docs/server/checkers.js' type='module'></script>
<!-- <script src='/docs/server/checkers.js' type='module'></script> -->
<style>
.sjpp-endpoint-div {
margin: 20px 10px 44px 10px;
Expand Down Expand Up @@ -69,12 +69,12 @@
</div>
</div>
</div>
<script>
<script type='module'>
import * as ppcheckers from './docs/server/checkers.js'

init()



async function init() {
const extracts = await fetch('/docs/server/extracts.json').then(r => r.json()).catch(console.error)
const sidebarLinks = d3.select('.tsd-small-nested-navigation')
Expand Down Expand Up @@ -280,7 +280,7 @@
.on('click', function(selectData) {
const d = selectData.selected
if (d?.url == 'spec') return
fetchData(api, d, method, m, section, window.ppcheckers[`valid${m.response.typeId}`])
fetchData(api, d, method, m, section, ppcheckers[`valid${m.response.typeId}`])
})
}

Expand Down
1 change: 0 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"dedup": "./dedupjs.sh",
"//todo": "refactor or deprecate the scripts below",
"pretest": "tsc && ./test/pretest.js",
"prepare": "ts-patch install",
"pretest:type": "npm run checkers",
"pretest:integration": "tsc",
"test:integration": "echo 'TODO: server integration tests'",
Expand Down
20 changes: 20 additions & 0 deletions server/routes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Server Routes

## Background

The files in `server/routes` code exports route API expectations
and initialization methods in a standard 'shape'. In this way,
tools such as `augen` may easily auto-initialize server route handlers,
tests, and documentation from the exported route API.

At a minimum, each file in `server/routes` imports request and response
type definitions from the similarly named file in `shared/types/src/routes/`,
via the `#types` subpath alias. Since these type defininitions can be used
statically by client code, they are kept in the `shared/types` workspace instead
of server or client workspace.


## Instructions

To create a `server/route` file, follow the comments/instructions in `shared/types/src/routes/_template_.ts`

39 changes: 26 additions & 13 deletions server/routes/_template_.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
export const api: any = {
// using snp types only as an example
import type { SnpRequest, SnpResponse, RouteApi } from '#types'
// imported payload is typed as RoutePayload
import { snpPayload } from '#types'

export const api: RouteApi = {
// route endpoint
// - no need for trailing slash
// - should be a noun (method is based on HTTP GET, POST, etc)
// - should be a noun
// - don't add 'Data' as response is assumed to be data
endpoint: '...',
// - don't prefix with `get`, such as `/getmyroute`, the method is already indicated via HTTP GET
endpoint: '/myroute',
methods: {
get: {
init,
request: {
typeId: 'any'
},
response: {
typeId: 'any'
}
...snpPayload, // this would "spread"/copy-over payload key-values from snpPayload, such as {request: {typeId}, examples: []}
init
},
post: {
alternativeFor: 'get',
...snpPayload, // repeat for post method, since PP routes are mostly read-only and POST can handle bigger payloads
init
}
// !!! DO NOT USE expressjs 'all' method shortcut !!!
// it will initialize 20+ methods includng HEAD which can break expected HTTP response
}
}

// init() is used to generate server route handler,
// augen.setRoutes() will use it to generate the second argument to expressjs method init,
// such as `app.get(api.endpoint, init({app, genomes}))`
function init({ genomes }) {
return async function (req, res) {
// to avoid linter unused var errors
console.log(genomes, req, res)
// use the colon syntax for clarity, the type is seen upfront instead of at the end
const q: SnpRequest = req.query
console.log(genomes[q.genome])
// can also use 'satisfies' keyword instead of colon syntax;
// do not use 'as' keyword, which is less strict than 'satisfies'
res.send({} satisfies SnpResponse)
// or perhaps more commonly,
// const result: SnpResponse = someFunction(q)
// res.send(result)
}
}
31 changes: 11 additions & 20 deletions server/routes/brainImaging.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import fs from 'fs'
import path from 'path'
import serverconfig from '#src/serverconfig.js'
import type { CategoricalTW, GetBrainImagingRequest, FilesByCategory, TermWrapper } from '#types'
import type { CategoricalTW, BrainImagingRequest, BrainImagingResponse, FilesByCategory, RouteApi } from '#types'
import { brainImagingPayload } from '#types'
import { spawn } from 'child_process'
import { getData } from '../src/termdb.matrix.js'

/*
given one or more samples, map the sample(s) to brain template and return the image
*/
export const api: any = {
export const api: RouteApi = {
endpoint: 'brainImaging',
methods: {
get: {
init,
request: {
typeId: 'GetBrainImagingRequest'
},
response: {
typeId: 'GetBrainImagingResponse'
}
...brainImagingPayload,
init
},
post: {
init,
request: {
typeId: 'GetBrainImagingRequest'
},
response: {
typeId: 'GetBrainImagingResponse'
}
...brainImagingPayload,
init
}
}
}

function init({ genomes }) {
return async (req: any, res: any): Promise<void> => {
return async (req, res): Promise<void> => {
try {
const query = req.query as GetBrainImagingRequest
const query: BrainImagingRequest = req.query

const g = genomes[query.genome]
if (!g) throw 'invalid genome name'
Expand All @@ -55,15 +46,15 @@ function init({ genomes }) {
}

const brainImage = await getBrainImage(query, genomes, plane, index)
res.send({ brainImage, plane })
res.send({ brainImage, plane } satisfies BrainImagingResponse)
} catch (e: any) {
console.log(e)
res.status(404).send('Sample brain image not found')
}
}
}

async function getBrainImage(query: GetBrainImagingRequest, genomes: any, plane: string, index: number): Promise<any> {
async function getBrainImage(query: BrainImagingRequest, genomes: any, plane: string, index: number): Promise<any> {
const ds = genomes[query.genome].datasets[query.dslabel]
const q = ds.queries.NIdata
const key = query.refKey
Expand Down
16 changes: 8 additions & 8 deletions server/routes/brainImagingSamples.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import fs from 'fs'
import path from 'path'
import serverconfig from '#src/serverconfig.js'
import type { BrainSample, CategoricalTW, GetBrainImagingSamplesRequest, GetBrainImagingSamplesResponse } from '#types'
import type { BrainSample, BrainImagingSamplesRequest, BrainImagingSamplesResponse, RouteApi } from '#types'
import { spawn } from 'child_process'

/*
given one or more samples, map the sample(s) to brain template and return the image
*/
export const api: any = {
export const api: RouteApi = {
endpoint: 'brainImagingSamples',
methods: {
get: {
init,
request: {
typeId: 'GetBrainImagingSamplesRequest'
typeId: 'BrainImagingSamplesRequest'
},
response: {
typeId: 'GetBrainImagingSamplesResponse'
typeId: 'BrainImagingSamplesResponse'
}
}
}
Expand All @@ -25,23 +25,23 @@ export const api: any = {
function init({ genomes }) {
return async (req: any, res: any): Promise<void> => {
try {
const query = req.query as GetBrainImagingSamplesRequest
const query: BrainImagingSamplesRequest = req.query

const g = genomes[query.genome]
if (!g) throw 'invalid genome name'
const ds = g.datasets[query.dslabel]
if (!ds) throw 'invalid dataset name'

const samples = await getBrainImageSamples(query, genomes)
res.send({ samples })
res.send({ samples } satisfies BrainImagingSamplesResponse)
} catch (e: any) {
console.log(e)
res.status(404).send('Sample brain image not found')
}
}
}

async function getBrainImageSamples(query: GetBrainImagingSamplesRequest, genomes: any): Promise<BrainSample[]> {
async function getBrainImageSamples(query: BrainImagingSamplesRequest, genomes: any): Promise<BrainSample[]> {
const ds = genomes[query.genome].datasets[query.dslabel]
const q = ds.queries.NIdata
const key = query.refKey
Expand Down Expand Up @@ -75,7 +75,7 @@ async function getBrainImageSamples(query: GetBrainImagingSamplesRequest, genome
}

//function called on mds3 init when validate the query, it defines the get method used by the route
export async function validate_query_NIdata(ds, genome) {
export async function validate_query_NIdata(ds) {
const q = ds.queries.NIdata
if (!q || !serverconfig.features?.showBrainImaging) return
for (const key in q) {
Expand Down
Loading
Loading