@toa.io/extensions.exposition 0.2.1-dev.3
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/package.json +36 -0
- package/src/.manifest/index.js +7 -0
- package/src/.manifest/normalize.js +58 -0
- package/src/.manifest/schema.yaml +69 -0
- package/src/.manifest/validate.js +17 -0
- package/src/constants.js +3 -0
- package/src/deployment.js +23 -0
- package/src/exposition.js +68 -0
- package/src/factory.js +76 -0
- package/src/index.js +9 -0
- package/src/manifest.js +12 -0
- package/src/query/criteria.js +55 -0
- package/src/query/enum.js +35 -0
- package/src/query/index.js +17 -0
- package/src/query/query.js +60 -0
- package/src/query/range.js +28 -0
- package/src/query/sort.js +19 -0
- package/src/remote.js +88 -0
- package/src/server.js +83 -0
- package/src/tenant.js +28 -0
- package/src/translate/etag.js +14 -0
- package/src/translate/index.js +7 -0
- package/src/translate/request.js +68 -0
- package/src/translate/response.js +62 -0
- package/src/tree.js +107 -0
- package/test/manifest.normalize.fixtures.js +37 -0
- package/test/manifest.normalize.test.js +37 -0
- package/test/manifest.validate.test.js +25 -0
- package/test/query.range.test.js +18 -0
- package/test/tree.fixtures.js +21 -0
- package/test/tree.test.js +44 -0
- package/types/annotations.d.ts +10 -0
- package/types/declarations.d.ts +31 -0
- package/types/exposition.d.ts +13 -0
- package/types/http.d.ts +13 -0
- package/types/query.d.ts +16 -0
- package/types/remote.d.ts +19 -0
- package/types/server.d.ts +13 -0
- package/types/tree.d.ts +33 -0
package/src/server.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const express = require('express')
|
|
4
|
+
const cors = require('cors')
|
|
5
|
+
|
|
6
|
+
const { Connector } = require('@toa.io/core')
|
|
7
|
+
const { console } = require('@toa.io/console')
|
|
8
|
+
|
|
9
|
+
const { PORT } = require('./constants')
|
|
10
|
+
|
|
11
|
+
// noinspection JSClosureCompilerSyntax
|
|
12
|
+
/**
|
|
13
|
+
* @implements {toa.extensions.exposition.Server}
|
|
14
|
+
*/
|
|
15
|
+
class Server extends Connector {
|
|
16
|
+
/** @type {import('express').Application} */
|
|
17
|
+
#app
|
|
18
|
+
/** @type {import('http').Server} */
|
|
19
|
+
#server
|
|
20
|
+
|
|
21
|
+
constructor () {
|
|
22
|
+
super()
|
|
23
|
+
|
|
24
|
+
// TODO: remove express
|
|
25
|
+
this.#app = express()
|
|
26
|
+
this.#app.disable('x-powered-by')
|
|
27
|
+
this.#app.enable('case sensitive routing')
|
|
28
|
+
this.#app.enable('strict routing')
|
|
29
|
+
this.#app.disable('etag')
|
|
30
|
+
this.#app.use(express.json())
|
|
31
|
+
this.#app.use(cors({ allowedHeaders: ['content-type'] }))
|
|
32
|
+
|
|
33
|
+
this.#app.use((req, res, next) => {
|
|
34
|
+
req.safe = req.method in SAFE
|
|
35
|
+
|
|
36
|
+
if (req.method in METHODS) next()
|
|
37
|
+
else res.status(501).end()
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
route (route, callback) {
|
|
42
|
+
this.#app.use(route, callback)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async connection () {
|
|
46
|
+
console.info(`Starting HTTP server at :${PORT} ...`)
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const error = () => reject(new Error(`Error starting HTTP server at :${PORT}`))
|
|
50
|
+
// noinspection JSCheckFunctionSignatures
|
|
51
|
+
this.#server = this.#app.listen(PORT, () => {
|
|
52
|
+
console.info(`HTTP server at :${PORT} started`)
|
|
53
|
+
|
|
54
|
+
this.#server.off('error', error)
|
|
55
|
+
resolve()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
this.#server.on('error', error)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async disconnection () {
|
|
63
|
+
console.info(`Stopping HTTP server at :${PORT} ...`)
|
|
64
|
+
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const error = () => reject(new Error(`Error stopping HTTP server at :${PORT}`))
|
|
67
|
+
|
|
68
|
+
this.#server.close(() => {
|
|
69
|
+
console.info(`HTTP server at :${PORT} stopped`)
|
|
70
|
+
|
|
71
|
+
this.#server.off('error', error)
|
|
72
|
+
resolve()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
this.#server.on('error', error)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const METHODS = { HEAD: 1, GET: 1, POST: 1, PUT: 1, PATCH: 1 }
|
|
81
|
+
const SAFE = { HEAD: 1, GET: 1 }
|
|
82
|
+
|
|
83
|
+
exports.Server = Server
|
package/src/tenant.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Connector } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
class Tenant extends Connector {
|
|
6
|
+
#binding
|
|
7
|
+
#declaration
|
|
8
|
+
|
|
9
|
+
constructor (binding, { namespace, name }, resources) {
|
|
10
|
+
super()
|
|
11
|
+
|
|
12
|
+
this.#binding = binding
|
|
13
|
+
this.#declaration = { namespace, name, resources }
|
|
14
|
+
|
|
15
|
+
this.depends(binding)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async connection () {
|
|
19
|
+
await this.#binding.receive('ping', () => this.#expose())
|
|
20
|
+
await this.#expose()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async #expose () {
|
|
24
|
+
await this.#binding.send('expose', this.#declaration)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exports.Tenant = Tenant
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const get = (header) => {
|
|
4
|
+
const match = header.match(rx)
|
|
5
|
+
return match === null ? null : match[1]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const set = (value) => '"' + value + '"'
|
|
9
|
+
|
|
10
|
+
const rx = /^"([^"]+)"$/
|
|
11
|
+
|
|
12
|
+
exports.get = get
|
|
13
|
+
exports.set = set
|
|
14
|
+
exports.rx = rx
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { exceptions: { RequestSyntaxException, RequestConflictException } } = require('@toa.io/core')
|
|
4
|
+
const { empty } = require('@toa.io/generic')
|
|
5
|
+
|
|
6
|
+
const etag = require('./etag')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @hot
|
|
10
|
+
*
|
|
11
|
+
* @param {import('express').Request} req
|
|
12
|
+
* @param {{[key: string]: string}} params
|
|
13
|
+
* @returns {toa.core.Request}
|
|
14
|
+
*/
|
|
15
|
+
const request = (req, params) => {
|
|
16
|
+
const request = {}
|
|
17
|
+
|
|
18
|
+
if (!empty(req.body)) request.input = req.body
|
|
19
|
+
|
|
20
|
+
if (!empty(req.query)) {
|
|
21
|
+
request.query = req.query
|
|
22
|
+
|
|
23
|
+
if (request.query.projection !== undefined) request.query.projection = request.query.projection.split(',')
|
|
24
|
+
if (request.query.sort !== undefined) request.query.sort = request.query.sort.split(',')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!empty(params)) {
|
|
28
|
+
if (req.method === 'POST') {
|
|
29
|
+
if (request.input === undefined) request.input = {}
|
|
30
|
+
|
|
31
|
+
for (const [key, value] of Object.entries(params)) {
|
|
32
|
+
if (request.input[key] === undefined) request.input[key] = value
|
|
33
|
+
else throw new RequestConflictException(`Input property '${key}' conflicts with path parameter`)
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
const criteria = []
|
|
37
|
+
|
|
38
|
+
if (request.query === undefined) request.query = {}
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(params)) {
|
|
41
|
+
if (key === 'id') request.query.id = value
|
|
42
|
+
else criteria.push(key + '==' + value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (criteria.length > 0) {
|
|
46
|
+
const value = criteria.join(';')
|
|
47
|
+
|
|
48
|
+
if (request.query.criteria === undefined) request.query.criteria = value
|
|
49
|
+
else request.query.criteria = value + ';' + request.query.criteria
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const condition = req.get('if-match')
|
|
55
|
+
|
|
56
|
+
if (condition !== undefined && condition !== '*') {
|
|
57
|
+
const value = etag.get(condition)
|
|
58
|
+
|
|
59
|
+
if (value === null) throw new RequestSyntaxException('ETag value must match ' + etag.rx)
|
|
60
|
+
if (request.query === undefined) request.query = {}
|
|
61
|
+
|
|
62
|
+
request.query.version = +value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return request
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.request = request
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { exceptions: { codes } } = require('@toa.io/core')
|
|
4
|
+
|
|
5
|
+
const etag = require('./etag')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {toa.core.Reply} reply
|
|
9
|
+
* @param res
|
|
10
|
+
* @param req
|
|
11
|
+
* @hot
|
|
12
|
+
*/
|
|
13
|
+
const ok = (reply, res, req) => {
|
|
14
|
+
if (reply.output?._version !== undefined) {
|
|
15
|
+
const { _version, ...output } = reply.output
|
|
16
|
+
const condition = req.get('if-none-match')
|
|
17
|
+
|
|
18
|
+
if (condition !== undefined && req.safe) {
|
|
19
|
+
const value = etag.get(condition)
|
|
20
|
+
|
|
21
|
+
if (value === _version) {
|
|
22
|
+
res.status(304).end()
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
res.set('etag', etag.set(_version))
|
|
28
|
+
reply.output = output
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let status
|
|
32
|
+
|
|
33
|
+
if (req.method === 'POST') status = 201
|
|
34
|
+
else if (reply.output !== undefined || reply.error !== undefined) status = 200
|
|
35
|
+
else status = 204
|
|
36
|
+
|
|
37
|
+
res.status(status)
|
|
38
|
+
if (status !== 204) res.send(reply)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const missed = (response) => response.status(404)
|
|
42
|
+
|
|
43
|
+
const exception = (exception, response) => {
|
|
44
|
+
const status = STATUSES[exception.code] || 500
|
|
45
|
+
|
|
46
|
+
response.status(status)
|
|
47
|
+
response.send(exception)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const STATUSES = {
|
|
51
|
+
[codes.RequestContract]: 400,
|
|
52
|
+
[codes.RequestSyntax]: 400,
|
|
53
|
+
[codes.QuerySyntax]: 400,
|
|
54
|
+
[codes.RequestConflict]: 403,
|
|
55
|
+
[codes.StateNotFound]: 404,
|
|
56
|
+
[codes.NotImplemented]: 405,
|
|
57
|
+
[codes.StatePrecondition]: 412
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exports.ok = ok
|
|
61
|
+
exports.missed = missed
|
|
62
|
+
exports.exception = exception
|
package/src/tree.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { match } = require('path-to-regexp')
|
|
5
|
+
|
|
6
|
+
const { console } = require('@toa.io/console')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @implements {toa.extensions.exposition.Tree}
|
|
10
|
+
*/
|
|
11
|
+
class Tree {
|
|
12
|
+
/** @type {toa.extensions.exposition.tree.Node[]} */
|
|
13
|
+
#nodes
|
|
14
|
+
|
|
15
|
+
/** @type {toa.extensions.exposition.query.Factory} */
|
|
16
|
+
#query
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {toa.extensions.exposition.query.Factory} query
|
|
20
|
+
*/
|
|
21
|
+
constructor (query) {
|
|
22
|
+
this.#query = query
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @hot */
|
|
26
|
+
match (path) {
|
|
27
|
+
// dev only check
|
|
28
|
+
if (process.env.TOA_ENV === 'local') {
|
|
29
|
+
const nodes = this.#nodes.filter((node) => node.match(path) !== false)
|
|
30
|
+
|
|
31
|
+
if (nodes.length > 1) {
|
|
32
|
+
const routes = nodes.map((node) => node.route)
|
|
33
|
+
|
|
34
|
+
throw new Error('Ambiguous routes ' + routes.join(', '))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let match
|
|
39
|
+
|
|
40
|
+
const node = this.#nodes.find((node) => {
|
|
41
|
+
match = node.match(path)
|
|
42
|
+
return match !== false
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return node === undefined ? undefined : { node, params: match.params }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
update (tree) {
|
|
49
|
+
this.#nodes = []
|
|
50
|
+
this.#traverse(tree)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {toa.extensions.exposition.declarations.Node | any} node
|
|
55
|
+
* @param {string} route
|
|
56
|
+
* @param {toa.extensions.exposition.declarations.Node} parent
|
|
57
|
+
*/
|
|
58
|
+
#traverse (node, route = undefined, parent = undefined) {
|
|
59
|
+
const current = {}
|
|
60
|
+
|
|
61
|
+
if (route === undefined) route = '/'
|
|
62
|
+
else route = trail(route)
|
|
63
|
+
|
|
64
|
+
if (parent !== undefined) node.parent = parent
|
|
65
|
+
|
|
66
|
+
if (node.operations) {
|
|
67
|
+
current.route = route
|
|
68
|
+
current.match = match(route)
|
|
69
|
+
current.query = this.#query(node)
|
|
70
|
+
current.operations = {}
|
|
71
|
+
|
|
72
|
+
for (const operation of node.operations) current.operations[method(operation)] = operation
|
|
73
|
+
|
|
74
|
+
this.#nodes.push(current)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let branches = 0
|
|
78
|
+
|
|
79
|
+
for (const [key, value] of Object.entries(node)) {
|
|
80
|
+
if (key[0] === '/') {
|
|
81
|
+
branches++
|
|
82
|
+
|
|
83
|
+
const branch = path.posix.resolve(route, '.' + key)
|
|
84
|
+
|
|
85
|
+
this.#traverse(value, branch, node)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (branches === 0 && node.operations === undefined) {
|
|
90
|
+
console.warn(`Resource tree leaf '${route}' has no operations`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const trail = (path) => path[path.length - 1] === '/' ? path : path + '/'
|
|
96
|
+
|
|
97
|
+
const method = (operation) => {
|
|
98
|
+
if (operation.type === 'transition') {
|
|
99
|
+
if (operation.query === false) return 'POST'
|
|
100
|
+
else return 'PUT'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (operation.type === 'observation') return 'GET'
|
|
104
|
+
if (operation.type === 'assignment') return 'PATCH'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
exports.Tree = Tree
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const manifest = {
|
|
4
|
+
operations: {
|
|
5
|
+
one: {
|
|
6
|
+
type: 'transition',
|
|
7
|
+
scope: 'object',
|
|
8
|
+
query: false
|
|
9
|
+
},
|
|
10
|
+
two: {
|
|
11
|
+
type: 'observation',
|
|
12
|
+
scope: 'objects'
|
|
13
|
+
},
|
|
14
|
+
three: {
|
|
15
|
+
type: 'observation',
|
|
16
|
+
scope: 'objects'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const resources = {
|
|
22
|
+
'/': ['one', 'two'],
|
|
23
|
+
'/top': {
|
|
24
|
+
operations: ['one'],
|
|
25
|
+
'/nested': {
|
|
26
|
+
operations: ['two'],
|
|
27
|
+
'/deeper': {
|
|
28
|
+
operations: [{
|
|
29
|
+
operation: 'three'
|
|
30
|
+
}]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.manifest = manifest
|
|
37
|
+
exports.resources = resources
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const clone = require('clone-deep')
|
|
4
|
+
|
|
5
|
+
const { normalize } = require('../src/.manifest')
|
|
6
|
+
const fixtures = require('./manifest.normalize.fixtures')
|
|
7
|
+
|
|
8
|
+
describe('normalize', () => {
|
|
9
|
+
let manifest, resources
|
|
10
|
+
|
|
11
|
+
const map = (operation) => ({
|
|
12
|
+
...fixtures.manifest.operations[operation],
|
|
13
|
+
operation
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
manifest = clone(fixtures.manifest)
|
|
18
|
+
resources = clone(fixtures.resources)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should expand array', () => {
|
|
22
|
+
normalize(resources, manifest)
|
|
23
|
+
|
|
24
|
+
expect(resources['/'].operations)
|
|
25
|
+
.toStrictEqual(fixtures.resources['/'].map(map))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should expand operations', () => {
|
|
29
|
+
normalize(resources, manifest)
|
|
30
|
+
|
|
31
|
+
expect(resources['/top'].operations)
|
|
32
|
+
.toStrictEqual(fixtures.resources['/top'].operations.map(map))
|
|
33
|
+
|
|
34
|
+
expect(resources['/top']['/nested'].operations)
|
|
35
|
+
.toStrictEqual(fixtures.resources['/top']['/nested'].operations.map(map))
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { validate } = require('../src/.manifest')
|
|
4
|
+
|
|
5
|
+
it('should validate', () => {
|
|
6
|
+
const resources = {
|
|
7
|
+
'/path/to': {
|
|
8
|
+
operations: [
|
|
9
|
+
{
|
|
10
|
+
operation: 'foo',
|
|
11
|
+
type: 'observation'
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
'/deeper/': {
|
|
15
|
+
operations: [{
|
|
16
|
+
operation: 'bar',
|
|
17
|
+
type: 'transition',
|
|
18
|
+
query: false
|
|
19
|
+
}]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
expect(() => validate(resources)).not.toThrow()
|
|
25
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Range } = require('../src/query/range')
|
|
4
|
+
|
|
5
|
+
let range
|
|
6
|
+
|
|
7
|
+
const operation = { type: 'observation', scope: 'objects' }
|
|
8
|
+
|
|
9
|
+
describe('exact', () => {
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
range = new Range({ value: 10, range: [] })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should throw on value mismatch', () => {
|
|
15
|
+
expect(() => range.parse(10, operation)).not.toThrow()
|
|
16
|
+
expect(() => range.parse(11, operation)).toThrow(/out of range/)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const declaration = {
|
|
4
|
+
'/': {
|
|
5
|
+
operations: ['find'],
|
|
6
|
+
'/:id': {
|
|
7
|
+
operations: ['observe'],
|
|
8
|
+
'/segment': {
|
|
9
|
+
operations: ['delete']
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
'/segment/:param': {
|
|
13
|
+
operations: ['transit']
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
'/sibling': {
|
|
17
|
+
operations: ['update']
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.declaration = declaration
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Tree } = require('../src/tree')
|
|
4
|
+
const fixtures = require('./tree.fixtures')
|
|
5
|
+
|
|
6
|
+
let tree
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tree = new Tree(() => null)
|
|
10
|
+
tree.update(fixtures.declaration)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should find node', () => {
|
|
14
|
+
expect(tree.match('/12/')).toBeDefined()
|
|
15
|
+
expect(tree.match('/12/segment/')).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should return undefined on mismatch', () => {
|
|
19
|
+
expect(tree.match('/12')).not.toBeDefined()
|
|
20
|
+
expect(tree.match('/non/existent/')).not.toBeDefined()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should throw on resource conflicts on local dev env', () => {
|
|
24
|
+
const declaration = {
|
|
25
|
+
'/:id': {
|
|
26
|
+
operations: ['observe']
|
|
27
|
+
},
|
|
28
|
+
'/ok': {
|
|
29
|
+
operations: ['transit']
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const env = process.env.TOA_ENV
|
|
34
|
+
|
|
35
|
+
process.env.TOA_ENV = 'local'
|
|
36
|
+
|
|
37
|
+
tree = new Tree(() => null)
|
|
38
|
+
|
|
39
|
+
tree.update(declaration)
|
|
40
|
+
|
|
41
|
+
expect(() => tree.match('/ok/')).toThrow(/Ambiguous routes/)
|
|
42
|
+
|
|
43
|
+
process.env.TOA_ENV = env
|
|
44
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
declare namespace toa.extensions.exposition.declarations {
|
|
2
|
+
|
|
3
|
+
interface Operation {
|
|
4
|
+
operation: string
|
|
5
|
+
type: 'transition' | 'observation' | 'assignment'
|
|
6
|
+
scope: 'object' | 'objects' | 'changeset'
|
|
7
|
+
query?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Query {
|
|
11
|
+
criteria?: string
|
|
12
|
+
sort?: string[]
|
|
13
|
+
projection?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type Node = {
|
|
17
|
+
query?: Query
|
|
18
|
+
operations?: Operation[]
|
|
19
|
+
} & {
|
|
20
|
+
[key: string]: Node
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Exposition {
|
|
24
|
+
namespace: string
|
|
25
|
+
name: string
|
|
26
|
+
resources: Node
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Node = toa.extensions.exposition.declarations.Node
|
|
31
|
+
export type Operation = toa.extensions.exposition.declarations.Operation
|
package/types/http.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as express from 'express'
|
|
2
|
+
|
|
3
|
+
declare namespace toa.extensions.exposition.http {
|
|
4
|
+
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH'
|
|
5
|
+
|
|
6
|
+
interface Request extends express.Request {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Response extends express.Response {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Method = toa.extensions.exposition.http.Method
|
package/types/query.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Query as RequestQuery } from '@toa.io/core/types/request'
|
|
2
|
+
import type { Node, Operation } from './declarations'
|
|
3
|
+
|
|
4
|
+
declare namespace toa.extensions.exposition {
|
|
5
|
+
|
|
6
|
+
namespace query {
|
|
7
|
+
type Factory = (node: Node) => Query
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Query {
|
|
11
|
+
parse(query: RequestQuery, operation: Operation): RequestQuery
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Query = toa.extensions.exposition.Query
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// noinspection ES6UnusedImports
|
|
2
|
+
|
|
3
|
+
import type * as declarations from './declarations'
|
|
4
|
+
|
|
5
|
+
declare namespace toa.extensions.exposition {
|
|
6
|
+
|
|
7
|
+
namespace remotes {
|
|
8
|
+
|
|
9
|
+
type Factory = (namespace: string, name: string) => Promise<Remote>
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Remote {
|
|
14
|
+
update(declaration: declarations.Node): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Remote = toa.extensions.exposition.Remote
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Request, Response } from 'express'
|
|
2
|
+
|
|
3
|
+
declare namespace toa.extensions.exposition {
|
|
4
|
+
|
|
5
|
+
namespace server {
|
|
6
|
+
type Callback = (req: Request, res: Response) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Server {
|
|
10
|
+
route(route: string, callback: server.Callback)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|