Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,280 @@
|
||||
// Generated, do not edit. sha256:1db816dc6af267a04206f23a388019cbe76689adb83e89843b65ed1a797537a8
|
||||
import * as db from '../db.js';
|
||||
import { toSearchName } from '../../shared/search-name.js';
|
||||
import { AUTH_KINDS, CAPABILITIES, ENUMS, HTTP_METHODS } from '../../shared/config.js';
|
||||
import { assertEnum, nowIso } from './_shared.js';
|
||||
import { emitChange } from './_events.js';
|
||||
import { fail } from '../../shared/app-error.js';
|
||||
|
||||
export function emitProvidersChanged() {
|
||||
emitChange('providers-changed');
|
||||
}
|
||||
|
||||
export function listProviders() {
|
||||
return db.all(`SELECT * FROM Provider ORDER BY isSeed DESC, name`);
|
||||
}
|
||||
|
||||
export function getProvider(key) {
|
||||
const p = db.one(`SELECT * FROM Provider WHERE key = ?`, [key]);
|
||||
if (!p) return null;
|
||||
return {
|
||||
...p,
|
||||
coverage: db.all(`SELECT symbol FROM ProviderCoverage WHERE providerId = ? ORDER BY symbol`, [p.id]).map(r => r.symbol),
|
||||
hmac: db.one(`SELECT * FROM ProviderAuthenticationHmac WHERE providerId = ?`, [p.id]) || null,
|
||||
fixedRate: db.one(`SELECT fiatPrice FROM ProviderFixedRate WHERE providerId = ?`, [p.id]) || null,
|
||||
callsPerMonth: db.one(`SELECT callsPerMonth FROM ProviderCallLimit WHERE providerId = ?`, [p.id])?.callsPerMonth ?? null,
|
||||
endpoints: db.all(`SELECT id, capability, method, pathTemplate, queryTemplate, quoteCurrency, minIntervalSeconds FROM ProviderEndpoint WHERE providerId = ?`, [p.id]),
|
||||
symbols: db.all(`SELECT id, symbol, externalId, quoteUnit, isCatchAll FROM ProviderInputMap WHERE providerId = ?
|
||||
ORDER BY isCatchAll, symbol`, [p.id]),
|
||||
outputs: db.all(`SELECT id, capability, internalField, jsonPath, transform FROM ProviderOutputMap WHERE providerId = ?`, [p.id]),
|
||||
};
|
||||
}
|
||||
|
||||
export function providerCapabilityMap() {
|
||||
const m = new Map();
|
||||
for (const r of db.all(`SELECT providerId, capability FROM ProviderEndpoint`)) {
|
||||
if (!m.has(r.providerId)) m.set(r.providerId, new Set());
|
||||
m.get(r.providerId).add(r.capability);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function assertAuthKind(kind) {
|
||||
if (!Object.keys(AUTH_KINDS).includes(kind))
|
||||
throw new Error(`Invalid authenticationKind: '${kind}'. Allowed: ${Object.keys(AUTH_KINDS).join(', ')}.`);
|
||||
}
|
||||
function bit01(v, name) {
|
||||
const n = Number(v);
|
||||
if (n !== 0 && n !== 1) throw new Error(`Invalid value for ${name}: ${v} (0 or 1 expected).`);
|
||||
return n;
|
||||
}
|
||||
function toCoverageList(v) {
|
||||
const arr = Array.isArray(v) ? v : String(v ?? '').split(',');
|
||||
return [...new Set(arr.map(s => String(s).trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function schrijfCoverage(providerId, symbolen) {
|
||||
db.run(`DELETE FROM ProviderCoverage WHERE providerId = ?`, [providerId]);
|
||||
for (const symbool of symbolen) {
|
||||
db.run(`INSERT INTO ProviderCoverage (providerId, symbol) VALUES (?,?)`, [providerId, symbool]);
|
||||
}
|
||||
}
|
||||
|
||||
function keyFromName(name) {
|
||||
const basis = String(name).normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'integratie';
|
||||
let key = basis;
|
||||
for (let n = 2; db.one(`SELECT 1 AS x FROM Provider WHERE key = ?`, [key]); n++) key = `${basis}_${n}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function createProvider(cfg) {
|
||||
if (!String(cfg.name || '').trim()) fail('integrations.nameRequired', null, 'A provider name is required.');
|
||||
assertAuthKind(cfg.authenticationKind);
|
||||
const key = String(cfg.key || '').trim() || keyFromName(cfg.name);
|
||||
if (db.one(`SELECT 1 AS x FROM Provider WHERE key = ?`, [key])) {
|
||||
fail('integrations.keyInUse', { key }, `Provider '${key}' already exists.`);
|
||||
}
|
||||
db.run(`INSERT INTO Provider
|
||||
(key, name, searchName, baseUrl, authenticationHeaderName, authenticationParameterName,
|
||||
authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,0,0,?)`,
|
||||
[key, cfg.name.trim(), toSearchName(cfg.name), cfg.baseUrl ?? '',
|
||||
cfg.authenticationHeaderName ?? null, cfg.authenticationParameterName ?? null,
|
||||
cfg.authenticationKind, bit01(cfg.authenticationRequired, 'authenticationRequired'),
|
||||
bit01(cfg.isActive ?? 1, 'isActive'), nowIso()]);
|
||||
const pid = db.lastId();
|
||||
schrijfCoverage(pid, toCoverageList(cfg.coverage));
|
||||
await db.persist();
|
||||
emitProvidersChanged();
|
||||
return pid;
|
||||
}
|
||||
|
||||
export async function updateProvider(key, cfg) {
|
||||
const p = db.one(`SELECT * FROM Provider WHERE key = ?`, [key]);
|
||||
if (!p) fail('integrations.unknownProvider', { key }, `Provider '${key}' does not exist.`);
|
||||
assertAuthKind(cfg.authenticationKind);
|
||||
db.run(`UPDATE Provider SET name=?, searchName=?, baseUrl=?, authenticationHeaderName=?,
|
||||
authenticationParameterName=?, authenticationKind=?, authenticationRequired=?, isActive=? WHERE id=?`,
|
||||
[cfg.name, toSearchName(cfg.name), cfg.baseUrl ?? '',
|
||||
cfg.authenticationHeaderName ?? null, cfg.authenticationParameterName ?? null,
|
||||
cfg.authenticationKind, bit01(cfg.authenticationRequired, 'authenticationRequired'),
|
||||
bit01(cfg.isActive ?? p.isActive, 'isActive'), p.id]);
|
||||
if (cfg.coverage !== undefined) schrijfCoverage(p.id, toCoverageList(cfg.coverage));
|
||||
await db.persist();
|
||||
emitProvidersChanged();
|
||||
}
|
||||
|
||||
export async function setProviderActive(key, isActive) {
|
||||
const p = db.one(`SELECT id FROM Provider WHERE key = ?`, [key]);
|
||||
if (!p) fail('integrations.unknownProvider', { key }, `Provider '${key}' does not exist.`);
|
||||
db.run(`UPDATE Provider SET isActive = ? WHERE id = ?`, [bit01(isActive, 'isActive'), p.id]);
|
||||
await db.persist();
|
||||
emitProvidersChanged();
|
||||
}
|
||||
|
||||
export async function setProviderCallLimit(key, callsPerMonth) {
|
||||
const p = db.one(`SELECT id FROM Provider WHERE key = ?`, [key]);
|
||||
if (!p) fail('integrations.unknownProvider', { key }, `Provider '${key}' does not exist.`);
|
||||
if (callsPerMonth == null || String(callsPerMonth).trim() === '') {
|
||||
db.run(`DELETE FROM ProviderCallLimit WHERE providerId = ?`, [p.id]);
|
||||
} else {
|
||||
const n = Number(callsPerMonth);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
fail('integrations.callLimitInvalid', { waarde: callsPerMonth }, `A monthly limit must be a whole number above zero, got '${callsPerMonth}'.`);
|
||||
}
|
||||
db.run(`INSERT INTO ProviderCallLimit (providerId, callsPerMonth, createdAt) VALUES (?, ?, ?)
|
||||
ON CONFLICT(providerId) DO UPDATE SET callsPerMonth = excluded.callsPerMonth`,
|
||||
[p.id, n, new Date().toISOString()]);
|
||||
}
|
||||
await db.persist();
|
||||
}
|
||||
|
||||
export async function deleteProvider(key) {
|
||||
const p = db.one(`SELECT id, isSystem FROM Provider WHERE key = ?`, [key]);
|
||||
if (!p) fail('integrations.unknownProvider', { key }, `Provider '${key}' does not exist.`);
|
||||
if (p.isSystem) throw new Error(`Provider '${key}' is a system row and cannot be deleted.`);
|
||||
const used = db.one(`SELECT COUNT(*) AS n FROM PriceSource WHERE providerId = ?`, [p.id]).n;
|
||||
if (used > 0) {
|
||||
fail('integrations.providerInMatrix', { key, n: used },
|
||||
`Provider '${key}' is still in the source matrix (${used} choice(s)).`);
|
||||
}
|
||||
const prijsrijen = db.one(`SELECT COUNT(*) AS n FROM PriceHistory WHERE providerId = ?`, [p.id]).n;
|
||||
if (prijsrijen > 0) {
|
||||
fail('integrations.providerHasPrices', { key, n: prijsrijen },
|
||||
`Provider '${key}' still has ${prijsrijen} price row(s) in the history.`);
|
||||
}
|
||||
db.run(`DELETE FROM Provider WHERE id = ?`, [p.id]);
|
||||
await db.persist();
|
||||
emitProvidersChanged();
|
||||
}
|
||||
|
||||
function assertProviderId(pid) {
|
||||
if (!db.one(`SELECT 1 AS x FROM Provider WHERE id = ?`, [pid]))
|
||||
throw new Error(`Provider ${pid} does not exist.`);
|
||||
}
|
||||
function reqStr(value, name) {
|
||||
const s = String(value ?? '').trim();
|
||||
if (!s) fail('integrations.fieldRequired', { veld: name }, `${name} is required.`);
|
||||
return s;
|
||||
}
|
||||
|
||||
function quoteCurrency(value) {
|
||||
const s = String(value ?? '').trim().toUpperCase();
|
||||
if (!s) return null;
|
||||
if (!/^[A-Z]{3}$/.test(s)) fail('integrations.quoteCurrencyInvalid', { waarde: value }, `Quote currency '${value}' is not a three-letter code.`);
|
||||
return s;
|
||||
}
|
||||
|
||||
function minInterval(value) {
|
||||
if (value == null || String(value).trim() === '') return null;
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n <= 0) fail('integrations.minIntervalInvalid', { waarde: value }, `Minimum interval '${value}' is not a whole number of seconds above zero.`);
|
||||
return n;
|
||||
}
|
||||
|
||||
export async function createProviderEndpoint(providerId, e) {
|
||||
assertProviderId(providerId);
|
||||
assertEnum('capability', CAPABILITIES, e.capability);
|
||||
assertEnum('method', HTTP_METHODS, e.method); reqStr(e.pathTemplate, 'pathTemplate');
|
||||
db.run(`INSERT INTO ProviderEndpoint (providerId, capability, method, pathTemplate, queryTemplate, quoteCurrency, minIntervalSeconds, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?,?)`,
|
||||
[providerId, e.capability, e.method, e.pathTemplate, e.queryTemplate ?? null, quoteCurrency(e.quoteCurrency),
|
||||
minInterval(e.minIntervalSeconds), new Date().toISOString()]);
|
||||
const id = db.lastId(); await db.persist(); return id;
|
||||
}
|
||||
export async function updateProviderEndpoint(id, e) {
|
||||
assertEnum('capability', CAPABILITIES, e.capability);
|
||||
assertEnum('method', HTTP_METHODS, e.method); reqStr(e.pathTemplate, 'pathTemplate');
|
||||
if (!db.one(`SELECT 1 AS x FROM ProviderEndpoint WHERE id = ?`, [id])) fail('integrations.unknownEndpoint', { id }, `Endpoint ${id} does not exist.`);
|
||||
db.run(`UPDATE ProviderEndpoint SET capability=?, method=?, pathTemplate=?, queryTemplate=? WHERE id=?`,
|
||||
[e.capability, e.method, e.pathTemplate, e.queryTemplate ?? null, id]);
|
||||
if (Object.prototype.hasOwnProperty.call(e, 'quoteCurrency')) {
|
||||
db.run(`UPDATE ProviderEndpoint SET quoteCurrency = ? WHERE id = ?`, [quoteCurrency(e.quoteCurrency), id]);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(e, 'minIntervalSeconds')) {
|
||||
db.run(`UPDATE ProviderEndpoint SET minIntervalSeconds = ? WHERE id = ?`, [minInterval(e.minIntervalSeconds), id]);
|
||||
}
|
||||
await db.persist();
|
||||
}
|
||||
export async function deleteProviderEndpoint(id) {
|
||||
db.run(`DELETE FROM ProviderEndpoint WHERE id = ?`, [id]);
|
||||
await db.persist();
|
||||
}
|
||||
|
||||
function quoteUnit(v) {
|
||||
if (v == null || String(v).trim() === '') return null;
|
||||
const u = String(v).trim();
|
||||
if (!ENUMS.QUOTE_UNIT.includes(u)) fail('integrations.quoteUnitInvalid', { waarde: u }, `Unknown quote unit '${u}'.`);
|
||||
return u;
|
||||
}
|
||||
|
||||
function assertEenCatchAll(providerId, behalveId = null) {
|
||||
const r = db.one(`SELECT id FROM ProviderInputMap WHERE providerId = ? AND isCatchAll = 1 AND id IS NOT ?`, [providerId, behalveId]);
|
||||
if (r) fail('integrations.catchAllExists', null, 'This provider already has a catch-all row.');
|
||||
}
|
||||
|
||||
export async function createProviderSymbol(providerId, s) {
|
||||
assertProviderId(providerId);
|
||||
const catchAll = !!s.isCatchAll;
|
||||
if (catchAll) assertEenCatchAll(providerId); else reqStr(s.symbol, 'symbol');
|
||||
reqStr(s.externalId, 'externalId');
|
||||
db.run(`INSERT INTO ProviderInputMap (providerId, symbol, externalId, quoteUnit, isCatchAll, createdAt) VALUES (?,?,?,?,?,?)`,
|
||||
[providerId, catchAll ? null : s.symbol, s.externalId, quoteUnit(s.quoteUnit), catchAll ? 1 : 0, nowIso()]);
|
||||
const id = db.lastId(); await db.persist(); return id;
|
||||
}
|
||||
export async function updateProviderSymbol(id, s) {
|
||||
const rij = db.one(`SELECT providerId, isCatchAll FROM ProviderInputMap WHERE id = ?`, [id]);
|
||||
if (!rij) fail('integrations.unknownSymbolMap', { id }, `Symbol map ${id} does not exist.`);
|
||||
const catchAll = Object.hasOwn(s, 'isCatchAll') ? !!s.isCatchAll : !!rij.isCatchAll;
|
||||
if (catchAll) assertEenCatchAll(rij.providerId, id); else reqStr(s.symbol, 'symbol');
|
||||
reqStr(s.externalId, 'externalId');
|
||||
db.run(`UPDATE ProviderInputMap SET symbol=?, externalId=?, isCatchAll=? WHERE id=?`,
|
||||
[catchAll ? null : s.symbol, s.externalId, catchAll ? 1 : 0, id]);
|
||||
if (Object.hasOwn(s, 'quoteUnit')) db.run(`UPDATE ProviderInputMap SET quoteUnit=? WHERE id=?`, [quoteUnit(s.quoteUnit), id]);
|
||||
await db.persist();
|
||||
}
|
||||
export async function deleteProviderSymbol(id) {
|
||||
db.run(`DELETE FROM ProviderInputMap WHERE id = ?`, [id]);
|
||||
await db.persist();
|
||||
}
|
||||
|
||||
export async function createProviderOutput(providerId, o) {
|
||||
assertProviderId(providerId);
|
||||
assertEnum('capability', CAPABILITIES, o.capability);
|
||||
reqStr(o.internalField, 'internalField'); reqStr(o.jsonPath, 'jsonPath'); reqStr(o.transform, 'transform');
|
||||
db.run(`INSERT INTO ProviderOutputMap (providerId, capability, internalField, jsonPath, transform) VALUES (?,?,?,?,?)`,
|
||||
[providerId, o.capability, o.internalField, o.jsonPath, o.transform]);
|
||||
const id = db.lastId(); await db.persist(); return id;
|
||||
}
|
||||
export async function updateProviderOutput(id, o) {
|
||||
assertEnum('capability', CAPABILITIES, o.capability);
|
||||
reqStr(o.internalField, 'internalField'); reqStr(o.jsonPath, 'jsonPath'); reqStr(o.transform, 'transform');
|
||||
if (!db.one(`SELECT 1 AS x FROM ProviderOutputMap WHERE id = ?`, [id])) fail('integrations.unknownOutputMap', { id }, `Output map ${id} does not exist.`);
|
||||
db.run(`UPDATE ProviderOutputMap SET capability=?, internalField=?, jsonPath=?, transform=? WHERE id=?`,
|
||||
[o.capability, o.internalField, o.jsonPath, o.transform, id]);
|
||||
await db.persist();
|
||||
}
|
||||
export async function deleteProviderOutput(id) {
|
||||
db.run(`DELETE FROM ProviderOutputMap WHERE id = ?`, [id]);
|
||||
await db.persist();
|
||||
}
|
||||
|
||||
export async function setProviderHmac(providerId, h) {
|
||||
assertProviderId(providerId);
|
||||
reqStr(h.messageParts, 'messageParts'); reqStr(h.hashAlgorithm, 'hashAlgorithm'); reqStr(h.encoding, 'encoding');
|
||||
reqStr(h.keyHeader, 'keyHeader'); reqStr(h.signatureHeader, 'signatureHeader'); reqStr(h.timestampHeader, 'timestampHeader');
|
||||
db.run(`INSERT INTO ProviderAuthenticationHmac
|
||||
(providerId, messageParts, hashAlgorithm, encoding, keyHeader, signatureHeader, timestampHeader, windowHeader, windowMs)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(providerId) DO UPDATE SET
|
||||
messageParts=excluded.messageParts, hashAlgorithm=excluded.hashAlgorithm, encoding=excluded.encoding,
|
||||
keyHeader=excluded.keyHeader, signatureHeader=excluded.signatureHeader,
|
||||
timestampHeader=excluded.timestampHeader, windowHeader=excluded.windowHeader, windowMs=excluded.windowMs`,
|
||||
[providerId, h.messageParts, h.hashAlgorithm, h.encoding, h.keyHeader, h.signatureHeader, h.timestampHeader, h.windowHeader ?? null, h.windowMs ?? null]);
|
||||
await db.persist();
|
||||
}
|
||||
export async function clearProviderHmac(providerId) {
|
||||
db.run(`DELETE FROM ProviderAuthenticationHmac WHERE providerId = ?`, [providerId]);
|
||||
await db.persist();
|
||||
}
|
||||
Reference in new issue
Block a user