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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugins/api.js +51 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.2.0",
3
+ "version": "9.2.1",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -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 (query && !query(entity)) return false
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('Api[%s] list error (%dms): %s', name, Date.now() - t0, err.message)
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 (query && !query(entityShape)) {
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
- logger.error('Api[%s] render error: %s', name, err.message)
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
- // useRenderer tags an unrenderable entity (no layout)
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
  })