mikser-io 9.2.0 → 9.2.1
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 +1 -1
- package/src/plugins/api.js +51 -7
package/package.json
CHANGED
package/src/plugins/api.js
CHANGED
|
@@ -415,6 +415,20 @@ export function api(options = {}) {
|
|
|
415
415
|
const query = (typeof ep.query === 'function' || (ep.query && typeof ep.query === 'object'))
|
|
416
416
|
? ep.query
|
|
417
417
|
: null
|
|
418
|
+
|
|
419
|
+
// The same scope as a PREDICATE. Two paths hold one entity in
|
|
420
|
+
// hand and have no query to push a filter into — admitting a
|
|
421
|
+
// POST /render body, and the graph-subscription filter — so
|
|
422
|
+
// they have to test it directly. Calling `query` there works
|
|
423
|
+
// only while the scope is a function; the sift-object form
|
|
424
|
+
// this endpoint now accepts is not callable, and the failure
|
|
425
|
+
// is a bare `query is not a function` surfacing from whichever
|
|
426
|
+
// route touched it first.
|
|
427
|
+
const matchesScope = typeof ep.query === 'function'
|
|
428
|
+
? ep.query
|
|
429
|
+
: query
|
|
430
|
+
? sift(query)
|
|
431
|
+
: null
|
|
418
432
|
const pageSize = ep.pageSize ?? globalPageSize
|
|
419
433
|
const renderTimeout = ep.renderTimeout ?? globalRenderTimeout
|
|
420
434
|
|
|
@@ -614,7 +628,7 @@ export function api(options = {}) {
|
|
|
614
628
|
: null
|
|
615
629
|
const handle = runtime.refs.subscribeGraph({
|
|
616
630
|
filter: (entity) => {
|
|
617
|
-
if (
|
|
631
|
+
if (matchesScope && !matchesScope(entity)) return false
|
|
618
632
|
if (requestFilter && !requestFilter(entity)) return false
|
|
619
633
|
return true
|
|
620
634
|
},
|
|
@@ -645,7 +659,10 @@ export function api(options = {}) {
|
|
|
645
659
|
// is a 500. Same shape used by the POST handler below.
|
|
646
660
|
const status = err instanceof ExpandError ? err.status : 500
|
|
647
661
|
if (status >= 500) {
|
|
648
|
-
logger.error(
|
|
662
|
+
logger.error(
|
|
663
|
+
'Api[%s] list error (%dms): %s\n%s',
|
|
664
|
+
name, Date.now() - t0, err.message, err.stack || '(no stack)',
|
|
665
|
+
)
|
|
649
666
|
} else {
|
|
650
667
|
logger.debug('Api[%s] list rejected (%dms): %s', name, Date.now() - t0, err.message)
|
|
651
668
|
}
|
|
@@ -803,6 +820,10 @@ export function api(options = {}) {
|
|
|
803
820
|
})
|
|
804
821
|
|
|
805
822
|
router.post('/render', allow('render'), auth, async (req, res) => {
|
|
823
|
+
// Hoisted so the catch can name WHICH entity failed. A
|
|
824
|
+
// render that throws before this is assigned is itself the
|
|
825
|
+
// finding — it means the body never parsed.
|
|
826
|
+
let renderId
|
|
806
827
|
try {
|
|
807
828
|
// Body shape mirrors the JS API: entity fields at top
|
|
808
829
|
// level, control flags grouped under `options`. Forwarded
|
|
@@ -813,19 +834,42 @@ export function api(options = {}) {
|
|
|
813
834
|
// options.save: false → skip the final disk write
|
|
814
835
|
// (bytes still in the response)
|
|
815
836
|
const { options = {}, ...entityShape } = req.body
|
|
837
|
+
renderId = entityShape.id
|
|
816
838
|
// When the endpoint declares a scope, reject anything
|
|
817
839
|
// outside it BEFORE pushing through the renderer.
|
|
818
|
-
if (
|
|
840
|
+
if (matchesScope && !matchesScope(entityShape)) {
|
|
819
841
|
return res.status(403).json({ error: 'Entity is outside this endpoint\'s scope' })
|
|
820
842
|
}
|
|
821
843
|
const { output, entity } = await render(entityShape, options)
|
|
822
844
|
await sendRenderOutput(res, output, entity)
|
|
823
845
|
} catch (err) {
|
|
824
|
-
|
|
846
|
+
// useRenderer tags an unrenderable entity (no layout)
|
|
847
|
+
// with err.status = 422; everything else is a 500.
|
|
848
|
+
const status = err.status ?? 500
|
|
849
|
+
if (status >= 500) {
|
|
850
|
+
// The stack, not just the message — and this is the
|
|
851
|
+
// one route where that is not optional. A render
|
|
852
|
+
// reaches here through a renderer, a postprocessor
|
|
853
|
+
// chain and any template helper they call, so the
|
|
854
|
+
// message is routinely a bare TypeError from a frame
|
|
855
|
+
// the operator cannot name. Logging `{error: message}`
|
|
856
|
+
// alone leaves bisecting deployed versions as the only
|
|
857
|
+
// way to find out where it came from, which is exactly
|
|
858
|
+
// as expensive as it sounds. The id says which entity;
|
|
859
|
+
// `undefined` there means the body never parsed.
|
|
860
|
+
logger.error(
|
|
861
|
+
'Api[%s] render error for %s: %s\n%s',
|
|
862
|
+
name, renderId ?? '(no id in body)', err.message,
|
|
863
|
+
err.stack || '(no stack)',
|
|
864
|
+
)
|
|
865
|
+
} else {
|
|
866
|
+
logger.debug(
|
|
867
|
+
'Api[%s] render rejected for %s (%d): %s',
|
|
868
|
+
name, renderId ?? '(no id in body)', status, err.message,
|
|
869
|
+
)
|
|
870
|
+
}
|
|
825
871
|
if (!res.headersSent) {
|
|
826
|
-
|
|
827
|
-
// with err.status = 422; everything else is a 500.
|
|
828
|
-
res.status(err.status ?? 500).json({ error: err.message })
|
|
872
|
+
res.status(status).json({ error: err.message })
|
|
829
873
|
}
|
|
830
874
|
}
|
|
831
875
|
})
|