openvibers/openvibe.trade Commits

Move Trade's error helpers onto openvibe-sdk/service (#3)

OP OpenVibers committed 2 Oct 2026 · 7c424edb2f · 4 files, +63 −55

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Trade/pull/3

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex f216a6c..21bd062 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.1.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
},
@@@ -1181,9 +1181,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.25.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
-- "integrity": "sha512-8eurZLyFUtB8XN16hLUuYkah+qwS3tkvdzul5qdZkJTi0aZ4q8dq2QGWSq6pN5mtVPdJyuJ8Mo53r36lixmP1g==",
++ "version": "0.26.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
++ "integrity": "sha512-+RK89ZG4WAKIRh89yr3ieIvnyJVNSOI+dsbBqGNcr9UHsJStdxtM75zSDq8cUNpBPQ1X6ElatZrjU9S3s0aEkg==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex dcbfdb8..186b813 100644
---- a/package.json
++++ b/package.json
@@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.79.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.1.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.2.0",
"pg": "^8.23.0"
},
ddiff --git a/server/http/errors.js b/server/http/errors.js
iindex da30dc4..1a95e3e 100644
---- a/server/http/errors.js
++++ b/server/http/errors.js
@@@ -2,64 +2,26 @@
/**
* Errors as RFC 9457 problems (contracts errors.problem@1, which keeps the legacy { error } field),
-- * and the small request helpers every router shares.
++ * and the small request helpers every router shares. Built from openvibe-sdk/service (docs/service.md,
++ * the Blog row) so every service answers the same way; the exports stay put so no call site moves.
*/
--const express = require('express');
--const contracts = require('openvibe-contracts');
--const cache = require('openvibe-shared/cache-policy');
++const svc = require('openvibe-sdk/service');
--/** A refusal with a stable problem code (e.g. 404 'post.not_found'). */
--class ApiError extends Error {
-- constructor(status, code, detail, extra) {
-- super(detail || code);
-- this.name = 'ApiError';
-- this.status = status;
-- this.code = code;
-- this.extra = extra || null;
-- }
--}
++/** A refusal with a stable problem code (e.g. 404 'instrument.not_found'). */
++const ApiError = svc.createServiceError('ApiError');
++
++const o = { name: 'Trade API', publishing: true, ServiceError: ApiError };
/** Publishing package errors carry { status, code } too. */
--function asApiError(err) {
-- if (err instanceof ApiError) return err;
-- if (err && err.name === 'PublishingError' && Number.isInteger(err.status)) {
-- const extra = err.code === 'revision.conflict' ? { expected: err.expected, current: err.current } : null;
-- return new ApiError(err.status, err.code, err.message, extra);
-- }
-- if (err instanceof TypeError && err.message && !/Cannot read|is not a function|undefined/.test(err.message)) {
-- return new ApiError(422, 'request.invalid', err.message);
-- }
-- return null;
--}
++const asApiError = (err) => svc.asServiceError(err, o);
/** Wrap a JSON handler: its return value is the body; errors become problems. */
--function run(fn, status = 200) {
-- return async (req, res) => {
-- try {
-- const out = await fn(req, res);
-- if (out === undefined || res.headersSent) return;
-- res.status(typeof status === 'function' ? status(out) : status).json(out);
-- } catch (err) {
-- if (res.headersSent) return;
-- const e = asApiError(err);
-- if (e) return contracts.http.sendProblem(res, e.status, e.code, { detail: e.message, ctx: req.ov, extra: e.extra || undefined });
-- console.error('[Trade API]', err && err.stack ? err.stack : err);
-- contracts.http.sendProblem(res, 500, 'internal.error', { detail: 'Internal error', ctx: req.ov });
-- }
-- };
--}
++const run = (fn, status) => svc.run(fn, status, o);
--const jsonParser = express.json({ limit: '512kb' });
--/** JSON body parser whose syntax errors are problems too. */
--function jsonBody(req, res, next) {
-- jsonParser(req, res, (err) => (err ? contracts.http.sendProblem(res, 400, 'request.invalid_json', { detail: 'Malformed JSON body', ctx: req.ov }) : next()));
--}
++/** JSON body parser whose failures are problems too: malformed 400, over the limit 413. */
++const jsonBody = svc.jsonBody();
/** Private, per-viewer responses: never stored by a shared cache, never indexed. */
--function privateNoStore(res) {
-- res.set('Cache-Control', cache.htmlHeaders({ private: true }));
-- res.vary('Cookie');
-- res.vary('Authorization');
--}
++const privateNoStore = svc.privateNoStore;
module.exports = { ApiError, asApiError, run, jsonBody, privateNoStore };
ddiff --git a/test/service-kit.test.js b/test/service-kit.test.js
nnew file mode 100644
iindex 0000000..65bf22a
---- /dev/null
++++ b/test/service-kit.test.js
@@@ -0,0 +1,46 @@
++'use strict';
++/**
++ * server/http/errors.js now sits on openvibe-sdk/service (plan T1): the exports stay put, so these pin
++ * the behaviour the hand-rolled helpers promised. The shared JSON body parser answers 413
++ * request.too_large for a body over the limit (the hand-rolled one folded that into 400
++ * request.invalid_json), malformed JSON is still 400 request.invalid_json, and an ApiError carrying
++ * `extra` still answers its status/code/detail with extra spread into the problem body.
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++const { ApiError, asApiError, run } = require('../server/http/errors');
++
++(async () => {
++ const t = await boot();
++ const svc = t.network.serviceToken('tools', ['trade.watchlist.create']);
++
++ await check('a JSON body over 512 kB is 413 request.too_large, not 400 request.invalid_json', async () => {
++ const r = await t.get('/api/v1/watchlists', { as: svc, json: { name: 'Big', padding: 'y'.repeat(600 * 1024) } });
++ assert.strictEqual(r.status, 413, r.text);
++ assert.strictEqual(r.json().code, 'request.too_large');
++ });
++
++ await check('a malformed JSON body is still 400 request.invalid_json', async () => {
++ const r = await t.get('/api/v1/watchlists', { as: svc, body: '{"name":', headers: { 'content-type': 'application/json' } });
++ assert.strictEqual(r.status, 400, r.text);
++ assert.strictEqual(r.json().code, 'request.invalid_json');
++ });
++
++ await check('an ApiError with extra answers its status/code/detail and spreads extra', async () => {
++ const err = new ApiError(409, 'watchlist.revision_conflict', 'The watchlist changed', { expected: 3, current: 4 });
++ assert.strictEqual(asApiError(err), err, 'a branded ApiError maps to itself');
++
++ const res = { statusCode: 0, headers: {}, body: '', headersSent: false, setHeader(k, v) { this.headers[k] = v; }, end(b) { this.body = b; } };
++ await run(async () => { throw err; })({}, res);
++
++ assert.strictEqual(res.statusCode, 409);
++ const b = JSON.parse(res.body);
++ assert.strictEqual(b.status, 409);
++ assert.strictEqual(b.code, 'watchlist.revision_conflict');
++ assert.strictEqual(b.detail, 'The watchlist changed');
++ assert.deepStrictEqual({ expected: b.expected, current: b.current }, { expected: 3, current: 4 });
++ });
++
++ await t.close();
++ done();
++})().catch((err) => { console.error(err); process.exit(1); });